dsh-account-pool 0.2.2 → 0.2.4

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
@@ -117,13 +117,18 @@ dsh plugin --profile web add dsh-account-pool
117
117
 
118
118
  全完成显示绿色 `3/3 已完成`,部分完成显示黄色 `2/3`,没做显示灰色 `0/3`。
119
119
 
120
- 清单底部还有一行**今日获得积分**:点「全部执行」后,用**执行前后的余额差**
121
- 算出这次实际到手的积分。
120
+ **账号明细**表里还有一列「今日获得」,按账号显示本次到手的积分:
122
121
 
123
- 为什么用差值:签到接口只回成功/失败、不回奖励数量,Trae 的任务流也没有
124
- 计费字段——余额差是唯一可靠的真值。只把**正差值**算作获得(负差值可能是
125
- 期间用了额度);算不出时显示 `—` 而不是编造数字;部分账号缺数据时会注明
126
- 「合计可能偏低」。
122
+ | 情况 | 显示 |
123
+ |---|---|
124
+ | 还没执行过 | `未执行` |
125
+ | 算出收益 | `+150`(有收益时绿色) |
126
+ | 收益为 0 | `+0`——「确实没加」与「不知道」是两回事 |
127
+ | 算不出 | `—`,不编造数字 |
128
+
129
+ 数值来自**执行前后的余额差**:签到接口只回成功/失败、不回奖励数量,
130
+ Trae 的任务流也没有计费字段,余额差是唯一可靠的真值。只把**正差值**
131
+ 算作获得(负差值可能是期间用了额度)。
127
132
 
128
133
  ### 只读与执行分离
129
134
 
package/lib/client.js CHANGED
@@ -617,18 +617,24 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
617
617
  /**
618
618
  * 账号明细里的「今日获得」单元格。
619
619
  *
620
- * 数值来自本次执行的**余额前后差**(后端在 tasks/daily 里算好,
621
- * 挂在每账号结果的 earnedCredits 上)。
620
+ * 取值优先级:
621
+ * 1. account.earnedCredits —— 后端台账里的**今日累计**,打开页面就有
622
+ * 2. lastRun[id].earnedCredits —— 刚执行完的最新值(台账已含它,
623
+ * 这里是防状态还没刷新的兜底)
624
+ *
625
+ * 为什么不是"本次差值":同一天重复点执行时签到幂等、余额不变,
626
+ * 差值会是 0,但今天确实拿到过积分。台账按天累加才是用户要的口径。
622
627
  *
623
628
  * 三态刻意区分开:
624
- * 没执行过未执行(灰)
625
- * 算出来了 → +N(绿);0 也显示 +0,因为「确实没加」与「不知道」不同
626
- * 算不出 —(灰),不编造
629
+ * 有数字+N(有收益为绿);0 也显示 +0,因为「确实没加」与
630
+ * 「不知道」不同
631
+ * 没数字 —;从没执行过也走这里(后端台账为空)
627
632
  */
628
633
  function earnedCell(account, lastRun) {
629
634
  const run = lastRun?.[account.accountId];
630
- if (!run) return h("span", { style: { opacity: 0.5 } }, "未执行");
631
- const earned = run.earnedCredits;
635
+ const earned = typeof account.earnedCredits === "number"
636
+ ? account.earnedCredits
637
+ : run?.earnedCredits;
632
638
  if (typeof earned !== "number") return h("span", { style: { opacity: 0.5 } }, "—");
633
639
  return h("span", {
634
640
  style: { color: earned > 0 ? "var(--dsw-alias-state-success-primary)" : "inherit" },
@@ -1036,15 +1042,17 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
1036
1042
  }, busy ? "刷新中…" : "刷新"),
1037
1043
  ),
1038
1044
 
1039
- // ---- 核心指标:强制 4 列一行 ----
1040
- // 用 grid 而不是 flex-wrap:wrap 在窄面板里会把第 4 块挤到下一行,
1045
+ // ---- 核心指标:一行铺满 ----
1046
+ // 用 grid 而不是 flex-wrap:wrap 在窄面板里会把最后一块挤到下一行,
1041
1047
  // grid 的 1fr 会按可用宽度均分,永远保持一行。
1048
+ //
1049
+ // 列数必须跟着**实际指标数**走。之前只换类名、内联仍是 4 列,
1050
+ // 去掉积分后 Trae 就变成「3 个块占 4 列」——右边空一格,很难看。
1042
1051
  h("div", {
1043
- // 少一个指标时用 3 列网格,避免留下一个空位
1044
1052
  className: reportsCredits ? "wb-kpi-grid-4" : "wb-kpi-grid-3",
1045
1053
  style: {
1046
1054
  display: "grid",
1047
- gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
1055
+ gridTemplateColumns: "repeat(" + (reportsCredits ? 4 : 3) + ", minmax(0, 1fr))",
1048
1056
  gap: "8px",
1049
1057
  marginBottom: "8px",
1050
1058
  },
@@ -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
- * - 差值为负 → 期间有别的消耗(对话),不算获得,记 0
989
- * - 两边都取不到 → 标 unknown,界面显示 —,不编造数字
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 earned = earnedOf(r.accountId)
1001
- if (earned !== undefined) r.earnedCredits = earned
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) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-account-pool",
3
3
  "displayName": "Account Pool",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "description": "把多个 WorkBuddy / Trae 账号汇成账号池接入 DeepSeek Harness:自动切换、限流熔断退避、凭证安全落盘",
6
6
  "type": "module",
7
7
  "license": "MIT",