dsh-account-pool 0.2.1 → 0.2.3
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 +19 -1
- package/lib/client.js +37 -59
- package/lib/earnings.js +146 -0
- package/lib/index.js +33 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,6 +117,19 @@ dsh plugin --profile web add dsh-account-pool
|
|
|
117
117
|
|
|
118
118
|
全完成显示绿色 `3/3 已完成`,部分完成显示黄色 `2/3`,没做显示灰色 `0/3`。
|
|
119
119
|
|
|
120
|
+
**账号明细**表里还有一列「今日获得」,按账号显示本次到手的积分:
|
|
121
|
+
|
|
122
|
+
| 情况 | 显示 |
|
|
123
|
+
|---|---|
|
|
124
|
+
| 还没执行过 | `未执行` |
|
|
125
|
+
| 算出收益 | `+150`(有收益时绿色) |
|
|
126
|
+
| 收益为 0 | `+0`——「确实没加」与「不知道」是两回事 |
|
|
127
|
+
| 算不出 | `—`,不编造数字 |
|
|
128
|
+
|
|
129
|
+
数值来自**执行前后的余额差**:签到接口只回成功/失败、不回奖励数量,
|
|
130
|
+
Trae 的任务流也没有计费字段,余额差是唯一可靠的真值。只把**正差值**
|
|
131
|
+
算作获得(负差值可能是期间用了额度)。
|
|
132
|
+
|
|
120
133
|
### 只读与执行分离
|
|
121
134
|
|
|
122
135
|
**打开页面只做只读查询,不会替你签到或派出。** 点「全部执行」才真正执行。
|
|
@@ -176,10 +189,15 @@ dsh plugin --profile web add dsh-account-pool
|
|
|
176
189
|
|---|---|
|
|
177
190
|
| 请求数 / 失败数 | **失败的尝试也计入请求数**,这样「重试放大」才看得出来 |
|
|
178
191
|
| 输入 / 输出 / 总 tokens | 取自上游 SSE 末尾的 `usage` 帧 |
|
|
179
|
-
| 积分消耗 | 上游 `usage.credit
|
|
192
|
+
| 积分消耗 | 上游 `usage.credit`。**只有逐次上报消耗的上游才有这一格**(WorkBuddy);Trae 的对话流没有计费字段,该指标整块隐藏,而不是显示恒为 0 |
|
|
180
193
|
| 平均延迟 | 按样本数加权,不是对每桶均值再取平均 |
|
|
181
194
|
| 按模型明细 | 每个模型的请求、失败、token、积分 |
|
|
182
195
|
|
|
196
|
+
> Trae 为什么没有积分消耗:它的对话流只有 6 种事件
|
|
197
|
+
> (`metadata` / `timing_cost` / `output` / `extra_info` / `token_usage` / `done`),
|
|
198
|
+
> 没有任何计费字段。积分接口虽然能给「账号累计已消耗」,但那是账号总量、
|
|
199
|
+
> 不是本页消耗,混着展示会误导,所以不提供。
|
|
200
|
+
|
|
183
201
|
保留策略:按小时分桶;当桶数超过上限(20 万)时,把**超过 90 天**的小时桶
|
|
184
202
|
折叠为日桶(日桶长期保留)。折叠由**桶数上限触发**,不是按时间定时执行——
|
|
185
203
|
所以在流量不大的情况下,历史小时桶会原样保留,不会自动精简。
|
package/lib/client.js
CHANGED
|
@@ -453,26 +453,6 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
453
453
|
h("td", { style: Object.assign({}, TD_STYLE, { fontSize: "12px", opacity: 0.7 }) }, note || ""),
|
|
454
454
|
);
|
|
455
455
|
|
|
456
|
-
/**
|
|
457
|
-
* 「今日获得积分」统计行。
|
|
458
|
-
*
|
|
459
|
-
* 数据来源是执行前后两次余额的差值(签到接口不回奖励数量,
|
|
460
|
-
* Trae 的任务流也没有计费字段)。所以:
|
|
461
|
-
* - 没执行过 → 显示「未执行」,不猜
|
|
462
|
-
* - 执行过但算式缺数据 → 显示「—」
|
|
463
|
-
* - 有数字 → 显示合计,部分账号缺数据时加注
|
|
464
|
-
*/
|
|
465
|
-
const earnedRow = () => h("tr", { key: "earned" },
|
|
466
|
-
h("td", { style: Object.assign({}, TD_STYLE, { fontWeight: 600 }) }, "今日获得积分"),
|
|
467
|
-
h("td", { style: Object.assign({}, TD_STYLE, NUM_STYLE, { color: "var(--dsw-alias-state-success-primary)" }) },
|
|
468
|
-
typeof lastRun?.totalEarned === "number"
|
|
469
|
-
? "+" + formatNumber(lastRun.totalEarned)
|
|
470
|
-
: h("span", { style: { opacity: 0.5 } }, lastRun ? "—" : "未执行")),
|
|
471
|
-
h("td", { style: Object.assign({}, TD_STYLE, { fontSize: "12px", opacity: 0.7 }) },
|
|
472
|
-
typeof lastRun?.totalEarned === "number"
|
|
473
|
-
? (lastRun.earnedPartial ? "部分账号余额未取到,合计可能偏低" : "执行前后余额之差")
|
|
474
|
-
: "点「全部执行」后统计"),
|
|
475
|
-
);
|
|
476
456
|
|
|
477
457
|
return h("div", null,
|
|
478
458
|
h("div", { style: CARD_STYLE },
|
|
@@ -501,7 +481,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
501
481
|
const map = {};
|
|
502
482
|
for (const r of out.results || []) map[r.accountId] = r;
|
|
503
483
|
// 把「本次获得的总积分」也带进 lastRun,供统计行显示
|
|
504
|
-
onRunDone(map
|
|
484
|
+
onRunDone(map);
|
|
505
485
|
// 提示里带上实际到手的积分,比干巴巴一句"已完成"有用
|
|
506
486
|
setNotice(region, "ok", grantedText(out.totalEarned, "每日任务"));
|
|
507
487
|
// 重新拉只读状态,让旅行/抽奖/连登的完成数同步更新
|
|
@@ -535,7 +515,6 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
535
515
|
taskRow("猫猫旅行", travelDone, "到站领奖 · 空闲派出", "今日已派或在途都算完成"),
|
|
536
516
|
taskRow("连登兑换", redeemDone, "满 7/14/28 天可兑换", redeemPending > 0 ? redeemPending + " 个账号有可兑换档位" : "没有待兑换的档位"),
|
|
537
517
|
taskRow("抽奖", lotteryDone, "次数来自连登兑换", lotteryPending > 0 ? lotteryPending + " 个账号有抽奖次数" : "暂无抽奖次数"),
|
|
538
|
-
earnedRow(),
|
|
539
518
|
),
|
|
540
519
|
),
|
|
541
520
|
|
|
@@ -555,6 +534,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
555
534
|
h("th", { style: TH_STYLE }, "猫"),
|
|
556
535
|
h("th", { style: TH_STYLE }, "旅行"),
|
|
557
536
|
h("th", { style: TH_STYLE }, "抽奖次数"),
|
|
537
|
+
h("th", { style: TH_STYLE }, "今日获得"),
|
|
558
538
|
)),
|
|
559
539
|
h("tbody", null, accounts.map(a => h("tr", { key: a.accountId },
|
|
560
540
|
h("td", { style: TD_STYLE }, a.nickname || a.accountId?.slice(0, 8) || "—"),
|
|
@@ -580,6 +560,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
580
560
|
: h("span", { style: { opacity: 0.5 } }, "—")),
|
|
581
561
|
h("td", { style: TD_STYLE },
|
|
582
562
|
a.lottery !== undefined ? String(a.lottery.chances) : h("span", { style: { opacity: 0.5 } }, "—")),
|
|
563
|
+
h("td", { style: TD_STYLE }, earnedCell(a, lastRun)),
|
|
583
564
|
))),
|
|
584
565
|
),
|
|
585
566
|
),
|
|
@@ -633,6 +614,33 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
633
614
|
return what + "完成,获得 " + formatNumber(totalEarned) + " 积分";
|
|
634
615
|
}
|
|
635
616
|
|
|
617
|
+
/**
|
|
618
|
+
* 账号明细里的「今日获得」单元格。
|
|
619
|
+
*
|
|
620
|
+
* 取值优先级:
|
|
621
|
+
* 1. account.earnedCredits —— 后端台账里的**今日累计**,打开页面就有
|
|
622
|
+
* 2. lastRun[id].earnedCredits —— 刚执行完的最新值(台账已含它,
|
|
623
|
+
* 这里是防状态还没刷新的兜底)
|
|
624
|
+
*
|
|
625
|
+
* 为什么不是"本次差值":同一天重复点执行时签到幂等、余额不变,
|
|
626
|
+
* 差值会是 0,但今天确实拿到过积分。台账按天累加才是用户要的口径。
|
|
627
|
+
*
|
|
628
|
+
* 三态刻意区分开:
|
|
629
|
+
* 有数字 → +N(有收益为绿);0 也显示 +0,因为「确实没加」与
|
|
630
|
+
* 「不知道」不同
|
|
631
|
+
* 没数字 → —;从没执行过也走这里(后端台账为空)
|
|
632
|
+
*/
|
|
633
|
+
function earnedCell(account, lastRun) {
|
|
634
|
+
const run = lastRun?.[account.accountId];
|
|
635
|
+
const earned = typeof account.earnedCredits === "number"
|
|
636
|
+
? account.earnedCredits
|
|
637
|
+
: run?.earnedCredits;
|
|
638
|
+
if (typeof earned !== "number") return h("span", { style: { opacity: 0.5 } }, "—");
|
|
639
|
+
return h("span", {
|
|
640
|
+
style: { color: earned > 0 ? "var(--dsw-alias-state-success-primary)" : "inherit" },
|
|
641
|
+
}, "+" + formatNumber(earned));
|
|
642
|
+
}
|
|
643
|
+
|
|
636
644
|
function checkinCell(account) {
|
|
637
645
|
if (account.checkinError) {
|
|
638
646
|
return h("span", { style: { color: "var(--dsw-alias-state-error-primary)", fontSize: "12px" } }, "查询失败");
|
|
@@ -665,23 +673,6 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
665
673
|
|
|
666
674
|
const sumCredits = accounts.reduce((sum, a) => sum + (typeof a.credits === "number" ? a.credits : 0), 0);
|
|
667
675
|
|
|
668
|
-
/**
|
|
669
|
-
* 「今日获得积分」统计行。
|
|
670
|
-
*
|
|
671
|
-
* 与 WorkBuddy 同源:执行前后两次余额的差值。Trae 的签到接口
|
|
672
|
-
* 只回成功/失败,任务流也没有计费字段,差值是真值。
|
|
673
|
-
*/
|
|
674
|
-
const earnedRow = () => h("tr", { key: "earned" },
|
|
675
|
-
h("td", { style: Object.assign({}, TD_STYLE, { fontWeight: 600 }) }, "今日获得积分"),
|
|
676
|
-
h("td", { style: Object.assign({}, TD_STYLE, NUM_STYLE, { color: "var(--dsw-alias-state-success-primary)" }) },
|
|
677
|
-
typeof lastRun?.totalEarned === "number"
|
|
678
|
-
? "+" + formatNumber(lastRun.totalEarned)
|
|
679
|
-
: h("span", { style: { opacity: 0.5 } }, lastRun ? "—" : "未执行")),
|
|
680
|
-
h("td", { style: Object.assign({}, TD_STYLE, { fontSize: "12px", opacity: 0.7 }) },
|
|
681
|
-
typeof lastRun?.totalEarned === "number"
|
|
682
|
-
? (lastRun.earnedPartial ? "部分账号余额未取到,合计可能偏低" : "执行前后余额之差")
|
|
683
|
-
: "点「全部执行」后统计"),
|
|
684
|
-
);
|
|
685
676
|
|
|
686
677
|
const taskRow = (name, done, note) => h("tr", { key: name },
|
|
687
678
|
h("td", { style: TD_STYLE }, name),
|
|
@@ -715,7 +706,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
715
706
|
if (!out.ok) throw new Error(out.error || "执行失败");
|
|
716
707
|
const map = {};
|
|
717
708
|
for (const r of out.results || []) map[r.accountId] = r;
|
|
718
|
-
onRunDone(map
|
|
709
|
+
onRunDone(map);
|
|
719
710
|
setNotice(region, "ok", grantedText(out.totalEarned, "签到"));
|
|
720
711
|
await reload();
|
|
721
712
|
} catch (error) {
|
|
@@ -745,7 +736,6 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
745
736
|
h("tbody", null,
|
|
746
737
|
taskRow("每日签到", checkedIn, "每天一次,先查状态再领取,不会重复领"),
|
|
747
738
|
taskRow("积分查询", withCredits, total > 0 ? "当前合计 " + formatNumber(sumCredits) + " 积分" : "只读查询,不消耗额度"),
|
|
748
|
-
earnedRow(),
|
|
749
739
|
),
|
|
750
740
|
),
|
|
751
741
|
),
|
|
@@ -759,6 +749,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
759
749
|
h("th", { style: TH_STYLE }, "账号"),
|
|
760
750
|
h("th", { style: TH_STYLE }, "今日签到"),
|
|
761
751
|
h("th", { style: TH_STYLE }, "积分"),
|
|
752
|
+
h("th", { style: TH_STYLE }, "今日获得"),
|
|
762
753
|
)),
|
|
763
754
|
h("tbody", null, accounts.map(a => h("tr", { key: a.accountId },
|
|
764
755
|
h("td", { style: TD_STYLE }, a.nickname || String(a.accountId).slice(0, 8)),
|
|
@@ -767,6 +758,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
767
758
|
a.creditsError
|
|
768
759
|
? "—"
|
|
769
760
|
: typeof a.credits === "number" ? formatNumber(a.credits) : "—"),
|
|
761
|
+
h("td", { style: TD_STYLE }, earnedCell(a, lastRun)),
|
|
770
762
|
))),
|
|
771
763
|
),
|
|
772
764
|
)
|
|
@@ -1750,24 +1742,10 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
|
|
|
1750
1742
|
const timer = setInterval(() => setTick(t => t + 1), 1000);
|
|
1751
1743
|
return () => clearInterval(timer);
|
|
1752
1744
|
}, []);
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const [lastRun, setLastRunState] = useState(null);
|
|
1758
|
-
/**
|
|
1759
|
-
* 保存执行结果。
|
|
1760
|
-
*
|
|
1761
|
-
* 按 accountId 建立索引,同时把汇总字段挂在同一个对象上——
|
|
1762
|
-
* 这样「按账号取结果」和「读合计」共用一份状态,不用两个 state
|
|
1763
|
-
* 引起额外的重渲染。
|
|
1764
|
-
*/
|
|
1765
|
-
const setLastRun = useCallback((results, summary) => {
|
|
1766
|
-
setLastRunState(Object.assign({}, results, {
|
|
1767
|
-
totalEarned: summary?.totalEarned,
|
|
1768
|
-
earnedPartial: summary?.earnedPartial === true,
|
|
1769
|
-
}));
|
|
1770
|
-
}, []);
|
|
1745
|
+
// 上次每日任务的执行结果,按 accountId 索引。
|
|
1746
|
+
// 清单里的「完成数」和明细里的「今日获得」都从它取
|
|
1747
|
+
//(签到没有只读接口,这些只能靠执行结果)。
|
|
1748
|
+
const [lastRun, setLastRun] = useState(null);
|
|
1771
1749
|
const pollTimer = useRef(null);
|
|
1772
1750
|
|
|
1773
1751
|
/** 给某个区域设一条提示信息。 */
|
package/lib/earnings.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每日积分收益记录器。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
*
|
|
6
|
+
* 「今日获得」不能直接用「执行前后余额差」表示——那个差值只反映
|
|
7
|
+
* **本次执行**的变化:
|
|
8
|
+
*
|
|
9
|
+
* ```text
|
|
10
|
+
* 第 1 次点执行 未签到 → 领取 → 余额 +150 显示 +150 ✓
|
|
11
|
+
* 第 2 次点执行 已签到 → 无变化 → 余额 +0 显示 0 ✗
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* 同一天重复点执行(幂等),第二次就开始显示 0,看起来像"没获得积分",
|
|
15
|
+
* 但今天确实拿到过 150。用户真正想知道的是**今天累计获得了多少**。
|
|
16
|
+
*
|
|
17
|
+
* 所以这里把每次算出的**正收益累加**并按天保存;跨天自动归零,
|
|
18
|
+
* 不需要清理任务。
|
|
19
|
+
*
|
|
20
|
+
* ## 为什么只累加正差值
|
|
21
|
+
*
|
|
22
|
+
* 差值为负说明期间有别的消耗(对话),不是"获得了负数积分"。
|
|
23
|
+
* 只有变多才算收益。另外重复减去同一笔收益不可能发生——每次执行
|
|
24
|
+
* 的差值本身就是增量。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises'
|
|
28
|
+
import { dirname, join } from 'node:path'
|
|
29
|
+
|
|
30
|
+
/** 文件格式版本,字段不兼容时便于识别旧数据。 */
|
|
31
|
+
const FILE_VERSION = 1
|
|
32
|
+
|
|
33
|
+
/** 把时间戳转成本地日期键(YYYY-MM-DD),与用户看到的"今天"一致。 */
|
|
34
|
+
export function dayKeyOf(timestamp = Date.now()) {
|
|
35
|
+
const d = new Date(timestamp)
|
|
36
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
37
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 按天的积分收益台账。
|
|
42
|
+
*
|
|
43
|
+
* 结构:`{ version, days: { '2026-09-21': { [accountId]: 150 } } }`
|
|
44
|
+
* 只保留最近若干天,避免文件无限增长。
|
|
45
|
+
*/
|
|
46
|
+
export class DailyEarningsRecorder {
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} options
|
|
49
|
+
* @param {string} options.file 落盘路径
|
|
50
|
+
* @param {number} [options.keepDays] 保留多少天(默认 30)
|
|
51
|
+
*/
|
|
52
|
+
constructor({ file, keepDays = 30 }) {
|
|
53
|
+
this.file = file
|
|
54
|
+
this.keepDays = keepDays
|
|
55
|
+
/** @type {Record<string, Record<string, number>>} */
|
|
56
|
+
this.days = {}
|
|
57
|
+
this.loaded = false
|
|
58
|
+
/** 写操作串行:read→改→write 不是原子的,并发会互相覆盖。 */
|
|
59
|
+
this.writeQueue = Promise.resolve()
|
|
60
|
+
/** 待写入标记:多次累加合并成一次写盘。 */
|
|
61
|
+
this.dirty = false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 读盘(幂等)。文件不存在或损坏时按空台账处理,不抛错。 */
|
|
65
|
+
async load() {
|
|
66
|
+
if (this.loaded) return
|
|
67
|
+
this.loaded = true
|
|
68
|
+
try {
|
|
69
|
+
const doc = JSON.parse(await readFile(this.file, 'utf8'))
|
|
70
|
+
if (doc?.version === FILE_VERSION && typeof doc.days === 'object' && doc.days !== null) {
|
|
71
|
+
this.days = doc.days
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// 首次运行或文件损坏:保持空台账
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 累加一个账号今天的收益,返回它今天的累计值。
|
|
80
|
+
*
|
|
81
|
+
* @param {string} accountId 账号 id
|
|
82
|
+
* @param {number} delta 本次新增的收益(<=0 时直接返回当前累计,不记负数)
|
|
83
|
+
* @param {number} [now] 当前时间戳(便于测试)
|
|
84
|
+
* @returns {Promise<number>} 该账号今日累计收益
|
|
85
|
+
*/
|
|
86
|
+
async add(accountId, delta, now = Date.now()) {
|
|
87
|
+
await this.load()
|
|
88
|
+
const key = dayKeyOf(now)
|
|
89
|
+
const today = this.days[key] ?? (this.days[key] = {})
|
|
90
|
+
const current = typeof today[accountId] === 'number' ? today[accountId] : 0
|
|
91
|
+
if (typeof delta !== 'number' || !Number.isFinite(delta) || delta <= 0) {
|
|
92
|
+
return current
|
|
93
|
+
}
|
|
94
|
+
today[accountId] = Math.round((current + delta) * 100) / 100
|
|
95
|
+
this.dirty = true
|
|
96
|
+
await this.flush()
|
|
97
|
+
return today[accountId]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 读某天各账号的累计收益。
|
|
102
|
+
*
|
|
103
|
+
* @param {number} [now] 当前时间戳(便于测试)
|
|
104
|
+
* @returns {Promise<Record<string, number>>}
|
|
105
|
+
*/
|
|
106
|
+
async today(now = Date.now()) {
|
|
107
|
+
await this.load()
|
|
108
|
+
return this.days[dayKeyOf(now)] ?? {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 落盘(先串行化,再清理过期天数)。 */
|
|
112
|
+
async flush() {
|
|
113
|
+
if (!this.dirty) return
|
|
114
|
+
this.dirty = false
|
|
115
|
+
this.prune()
|
|
116
|
+
const payload = JSON.stringify({ version: FILE_VERSION, days: this.days }, null, 0)
|
|
117
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
118
|
+
try {
|
|
119
|
+
await mkdir(dirname(this.file), { recursive: true })
|
|
120
|
+
// 先写临时文件再改名:避免写一半崩溃留下损坏的 JSON
|
|
121
|
+
const tmp = `${this.file}.${process.pid}.tmp`
|
|
122
|
+
await writeFile(tmp, payload, { encoding: 'utf8', mode: 0o600 })
|
|
123
|
+
await rename(tmp, this.file)
|
|
124
|
+
} catch {
|
|
125
|
+
// 落盘失败不影响本次收益的正常返回;下次累加会再试
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
await this.writeQueue
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 只保留最近 keepDays 天,防止文件无限增长。 */
|
|
132
|
+
prune() {
|
|
133
|
+
const keys = Object.keys(this.days).sort()
|
|
134
|
+
if (keys.length <= this.keepDays) return
|
|
135
|
+
for (const key of keys.slice(0, keys.length - this.keepDays)) {
|
|
136
|
+
delete this.days[key]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 按区域打开台账(每个区域一份,与凭证/用量文件命名一致)。 */
|
|
142
|
+
export function openDailyEarnings(dshHome, region) {
|
|
143
|
+
return new DailyEarningsRecorder({
|
|
144
|
+
file: join(dshHome, `.account-pool.earnings.${region}.json`),
|
|
145
|
+
})
|
|
146
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { fetchModels, fetchCredits } from './upstream.js'
|
|
|
35
35
|
import { AccountPicker } from './selector.js'
|
|
36
36
|
import { createShim } from './shim.js'
|
|
37
37
|
import { UsageRecorder } from './usage.js'
|
|
38
|
+
import { openDailyEarnings } from './earnings.js'
|
|
38
39
|
import { runDailyTasks, fetchStreak, fetchBuddy, fetchLottery, travelStatus as fetchTravelStatus } from './tasks.js'
|
|
39
40
|
import { openTraeStore, accountIdOf as accountIdOfTrae } from './trae-accounts.js'
|
|
40
41
|
import { parseTraeStorage } from './trae-storage.js'
|
|
@@ -220,6 +221,9 @@ async function createRegionStack({ region, dshHome, config, logger, ctx }) {
|
|
|
220
221
|
],
|
|
221
222
|
})
|
|
222
223
|
await usage.load()
|
|
224
|
+
// 每日积分收益台账:同一天多次执行要能累计,不能只看本次差值。
|
|
225
|
+
const earnings = openDailyEarnings(dshHome, region)
|
|
226
|
+
await earnings.load()
|
|
223
227
|
const picker = new AccountPicker({
|
|
224
228
|
softCooldownMs: (config.softCooldownSeconds ?? 600) * 1000,
|
|
225
229
|
breakerThreshold: config.breakerThreshold ?? 3,
|
|
@@ -373,6 +377,7 @@ async function createRegionStack({ region, dshHome, config, logger, ctx }) {
|
|
|
373
377
|
shim,
|
|
374
378
|
adapter,
|
|
375
379
|
usage,
|
|
380
|
+
earnings,
|
|
376
381
|
refreshCatalog,
|
|
377
382
|
loadCredits,
|
|
378
383
|
getCatalog: () => catalog,
|
|
@@ -981,24 +986,38 @@ export function apply(ctx, config = {}) {
|
|
|
981
986
|
const after = await stack.loadCredits(true).catch(() => ({}))
|
|
982
987
|
|
|
983
988
|
/**
|
|
984
|
-
*
|
|
989
|
+
* 算出每个账号本次的增量,并**累加进今天的台账**。
|
|
985
990
|
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
*
|
|
991
|
+
* 为什么要台账:差值只反映"本次执行"的变化。同一天再点一次
|
|
992
|
+
* 执行(签到幂等,余额不变)差值就是 0——但今天确实拿到过积分。
|
|
993
|
+
* 界面要显示的是"今天累计获得多少",所以按天累加并持久化,
|
|
994
|
+
* 跨天自动归零(按本地日期分键)。
|
|
995
|
+
*
|
|
996
|
+
* 只把**正差值**算作获得:差值为负说明期间有别的消耗(对话),
|
|
997
|
+
* 不是"获得负数"。
|
|
990
998
|
*/
|
|
991
999
|
const earnedOf = (accountId) => {
|
|
992
1000
|
const b = before[accountId]?.total
|
|
993
1001
|
const a = after[accountId]?.total
|
|
994
1002
|
if (typeof b !== 'number' || typeof a !== 'number') return undefined
|
|
995
1003
|
const delta = a - b
|
|
996
|
-
// 负差值可能是「期间用了额度」,不算获得;只取正收益
|
|
997
1004
|
return delta > 0 ? Math.round(delta * 100) / 100 : 0
|
|
998
1005
|
}
|
|
999
1006
|
for (const r of results) {
|
|
1000
|
-
const
|
|
1001
|
-
if (
|
|
1007
|
+
const delta = earnedOf(r.accountId)
|
|
1008
|
+
if (delta === undefined) continue
|
|
1009
|
+
// add() 返回该账号**今日累计**值;delta<=0 时原样返回已有累计,
|
|
1010
|
+
// 所以重复执行不会把数字冲掉
|
|
1011
|
+
r.earnedCredits = await stack.earnings.add(r.accountId, delta)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// 补上今天已经累计、但本次没参与执行(或算不出差值)的账号,
|
|
1015
|
+
// 否则它们的「今日获得」会显示成"未执行"或"—"。
|
|
1016
|
+
const todayEarnings = await stack.earnings.today()
|
|
1017
|
+
for (const r of results) {
|
|
1018
|
+
if (typeof r.earnedCredits === 'number') continue
|
|
1019
|
+
const accumulated = todayEarnings[r.accountId]
|
|
1020
|
+
if (typeof accumulated === 'number') r.earnedCredits = accumulated
|
|
1002
1021
|
}
|
|
1003
1022
|
|
|
1004
1023
|
const totalEarned = results.reduce(
|
|
@@ -1007,8 +1026,6 @@ export function apply(ctx, config = {}) {
|
|
|
1007
1026
|
ok: true,
|
|
1008
1027
|
results,
|
|
1009
1028
|
totalEarned: Math.round(totalEarned * 100) / 100,
|
|
1010
|
-
// 有账号算不出差值时,界面要对「合计」加注说明,避免误以为是全部
|
|
1011
|
-
earnedPartial: results.some(r => typeof r.earnedCredits !== 'number'),
|
|
1012
1029
|
})
|
|
1013
1030
|
} catch (error) {
|
|
1014
1031
|
json(res, 500, { ok: false, error: safeMessage(error) })
|
|
@@ -1031,6 +1048,10 @@ export function apply(ctx, config = {}) {
|
|
|
1031
1048
|
const stack = list.find(s => s.region === region)
|
|
1032
1049
|
if (stack === undefined) { json(res, 503, { ok: false, error: '该区域尚未初始化' }); return }
|
|
1033
1050
|
|
|
1051
|
+
// 今日已累计的收益:让「今日获得」在打开页面时就有值,
|
|
1052
|
+
// 不必等到点过执行(台账里存的是按天累计值)。
|
|
1053
|
+
const todayEarnings = await stack.earnings.today()
|
|
1054
|
+
|
|
1034
1055
|
const out = []
|
|
1035
1056
|
for (const account of await stack.store.list()) {
|
|
1036
1057
|
try {
|
|
@@ -1048,6 +1069,7 @@ export function apply(ctx, config = {}) {
|
|
|
1048
1069
|
creditsSize: status.creditsSize,
|
|
1049
1070
|
...(status.checkinError === undefined ? {} : { checkinError: status.checkinError }),
|
|
1050
1071
|
...(status.creditsError === undefined ? {} : { creditsError: status.creditsError }),
|
|
1072
|
+
earnedCredits: todayEarnings[account.id],
|
|
1051
1073
|
})
|
|
1052
1074
|
continue
|
|
1053
1075
|
}
|
|
@@ -1067,6 +1089,7 @@ export function apply(ctx, config = {}) {
|
|
|
1067
1089
|
buddy,
|
|
1068
1090
|
travel: travelNow,
|
|
1069
1091
|
lottery,
|
|
1092
|
+
earnedCredits: todayEarnings[account.id],
|
|
1070
1093
|
...(credential.expiresAtMs === undefined ? {} : { expiresAtMs: credential.expiresAtMs }),
|
|
1071
1094
|
})
|
|
1072
1095
|
} catch (error) {
|