dsh-account-pool 0.1.1 → 0.2.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/lib/client.js CHANGED
@@ -453,6 +453,27 @@ 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
  return h("div", null,
457
478
  h("div", { style: CARD_STYLE },
458
479
  h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px" } },
@@ -479,8 +500,10 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
479
500
  // 直接调用会 ReferenceError(跨作用域)。
480
501
  const map = {};
481
502
  for (const r of out.results || []) map[r.accountId] = r;
482
- onRunDone(map);
483
- setNotice(region, "ok", "已完成 " + (out.results || []).length + " 个账号的每日任务");
503
+ // 把「本次获得的总积分」也带进 lastRun,供统计行显示
504
+ onRunDone(map, { totalEarned: out.totalEarned, earnedPartial: out.earnedPartial });
505
+ // 提示里带上实际到手的积分,比干巴巴一句"已完成"有用
506
+ setNotice(region, "ok", grantedText(out.totalEarned, "每日任务"));
484
507
  // 重新拉只读状态,让旅行/抽奖/连登的完成数同步更新
485
508
  await reload();
486
509
  } catch (error) {
@@ -512,6 +535,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
512
535
  taskRow("猫猫旅行", travelDone, "到站领奖 · 空闲派出", "今日已派或在途都算完成"),
513
536
  taskRow("连登兑换", redeemDone, "满 7/14/28 天可兑换", redeemPending > 0 ? redeemPending + " 个账号有可兑换档位" : "没有待兑换的档位"),
514
537
  taskRow("抽奖", lotteryDone, "次数来自连登兑换", lotteryPending > 0 ? lotteryPending + " 个账号有抽奖次数" : "暂无抽奖次数"),
538
+ earnedRow(),
515
539
  ),
516
540
  ),
517
541
 
@@ -597,6 +621,18 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
597
621
  * 设备已签到 本账号没领,但同设备今天签过(did_checked_in=true)
598
622
  * 未签到 都没签,可以执行
599
623
  */
624
+ /**
625
+ * 执行完成的提示文案,带上本次获得的积分。
626
+ *
627
+ * 拿不到数字时不编造——只说完成了几个账号。
628
+ * 部分账号算不出差值时加「部分」字样,避免让人以为这是全部。
629
+ */
630
+ function grantedText(totalEarned, what) {
631
+ if (typeof totalEarned !== "number") return "已完成" + what;
632
+ if (totalEarned === 0) return "已完成" + what + "(本次没有新增积分)";
633
+ return what + "完成,获得 " + formatNumber(totalEarned) + " 积分";
634
+ }
635
+
600
636
  function checkinCell(account) {
601
637
  if (account.checkinError) {
602
638
  return h("span", { style: { color: "var(--dsw-alias-state-error-primary)", fontSize: "12px" } }, "查询失败");
@@ -615,7 +651,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
615
651
  return h("span", { style: { opacity: 0.7 } }, "未签到");
616
652
  }
617
653
 
618
- function TraeTasksView({ region, data, busy, setBusy, setNotice, notice, reload, onRunDone }) {
654
+ function TraeTasksView({ region, data, busy, setBusy, setNotice, notice, lastRun, reload, onRunDone }) {
619
655
  const accounts = data.accounts || [];
620
656
  const total = accounts.length;
621
657
 
@@ -629,6 +665,24 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
629
665
 
630
666
  const sumCredits = accounts.reduce((sum, a) => sum + (typeof a.credits === "number" ? a.credits : 0), 0);
631
667
 
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
+
632
686
  const taskRow = (name, done, note) => h("tr", { key: name },
633
687
  h("td", { style: TD_STYLE }, name),
634
688
  h("td", { style: TD_STYLE },
@@ -661,8 +715,8 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
661
715
  if (!out.ok) throw new Error(out.error || "执行失败");
662
716
  const map = {};
663
717
  for (const r of out.results || []) map[r.accountId] = r;
664
- onRunDone(map);
665
- setNotice(region, "ok", "已完成 " + (out.results || []).length + " 个账号的签到");
718
+ onRunDone(map, { totalEarned: out.totalEarned, earnedPartial: out.earnedPartial });
719
+ setNotice(region, "ok", grantedText(out.totalEarned, "签到"));
666
720
  await reload();
667
721
  } catch (error) {
668
722
  setNotice(region, "error", error.message);
@@ -691,6 +745,7 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
691
745
  h("tbody", null,
692
746
  taskRow("每日签到", checkedIn, "每天一次,先查状态再领取,不会重复领"),
693
747
  taskRow("积分查询", withCredits, total > 0 ? "当前合计 " + formatNumber(sumCredits) + " 积分" : "只读查询,不消耗额度"),
748
+ earnedRow(),
694
749
  ),
695
750
  ),
696
751
  ),
@@ -900,6 +955,28 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
900
955
  const totalCalls = t.requests || 0;
901
956
  const failRate = totalCalls > 0 ? (t.errors || 0) / totalCalls : 0;
902
957
 
958
+ /**
959
+ * 积分消耗指标。
960
+ *
961
+ * 优先用「按请求累加」的值;它为 0 而上游给了累计消耗时,改用
962
+ * 累计值并注明口径不同——这两个数字含义不一样,不能混着看:
963
+ * 按请求累加:本页时间窗口内消耗了多少
964
+ * 累计已消耗:账号从开通到现在的总量(不受时间窗口影响)
965
+ */
966
+ const creditsKpi = (totals, whole) => {
967
+ const perRequest = typeof totals.credits === "number" ? totals.credits : 0;
968
+ const upstream = whole.upstreamConsumed;
969
+ if (perRequest > 0) {
970
+ return kpi("积分消耗", formatNumber(perRequest), { full: formatNumber(perRequest) + " 积分" });
971
+ }
972
+ if (typeof upstream === "number" && upstream > 0) {
973
+ return kpi("累计消耗", formatNumber(upstream), {
974
+ full: "上游未上报单次消耗,这里显示账号累计已消耗 " + formatNumber(upstream) + " 积分",
975
+ });
976
+ }
977
+ return kpi("积分消耗", formatNumber(perRequest), { full: formatNumber(perRequest) + " 积分" });
978
+ };
979
+
903
980
  /** 核心指标块:大号数字 + 小标签。 */
904
981
  const kpi = (label, value, opts = {}) => h("div", {
905
982
  style: {
@@ -1013,7 +1090,13 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
1013
1090
  kpi("总 tokens", formatCompact(t.totalTokens), { full: formatNumber(t.totalTokens) }),
1014
1091
  // 积分精确到个位、不缩写:它是能对账的数字,
1015
1092
  // 显示成 8.4K 会让人不知道到底扣了多少。
1016
- kpi("积分消耗", formatNumber(t.credits), { full: formatNumber(t.credits) + " 积分" }),
1093
+ //
1094
+ // 两种口径:
1095
+ // 按请求累加(t.credits)—— 上游逐次上报时可用(WorkBuddy)
1096
+ // 累计已消耗(upstreamConsumed)—— 上游不报单次时用(Trae)
1097
+ // Trae 的对话流没有计费字段,所以逐次累加恒为 0;那种情况下
1098
+ // 显示上游累计值,并在悬停里说明口径,避免误读成"没消耗"。
1099
+ creditsKpi(t, usage),
1017
1100
  // 延迟只统计成功请求。没有成功请求时显示 —,
1018
1101
  // 而不是误导性的 0(或把失败请求的快速返回当成"很快")。
1019
1102
  h("div", {
@@ -1684,7 +1767,23 @@ window.__ModuleLoader__.load({ id: "dsh-account-pool", factory: (require) => {
1684
1767
  return () => clearInterval(timer);
1685
1768
  }, []);
1686
1769
  // 上次每日任务的执行结果:签到没有只读接口,靠它显示完成情况。
1687
- const [lastRun, setLastRun] = useState(null);
1770
+ // 除了每账号结果(key accountId),还带两个汇总结算字段:
1771
+ // totalEarned 本次获得的总积分(执行前后余额之差)
1772
+ // earnedPartial 是否有账号算不出差值(合计可能偏低)
1773
+ const [lastRun, setLastRunState] = useState(null);
1774
+ /**
1775
+ * 保存执行结果。
1776
+ *
1777
+ * 按 accountId 建立索引,同时把汇总字段挂在同一个对象上——
1778
+ * 这样「按账号取结果」和「读合计」共用一份状态,不用两个 state
1779
+ * 引起额外的重渲染。
1780
+ */
1781
+ const setLastRun = useCallback((results, summary) => {
1782
+ setLastRunState(Object.assign({}, results, {
1783
+ totalEarned: summary?.totalEarned,
1784
+ earnedPartial: summary?.earnedPartial === true,
1785
+ }));
1786
+ }, []);
1688
1787
  const pollTimer = useRef(null);
1689
1788
 
1690
1789
  /** 给某个区域设一条提示信息。 */
package/lib/index.js CHANGED
@@ -299,22 +299,41 @@ async function createRegionStack({ region, dshHome, config, logger, ctx }) {
299
299
  let creditsCache = { at: 0, value: null }
300
300
 
301
301
  /** 查各账号积分(带缓存);单个账号失败不影响其他账号。 */
302
+ /**
303
+ * 查本区域各账号的积分(带 60 秒缓存)。
304
+ *
305
+ * 账号之间**并行**查询:每个账号是一次独立的上游请求,串行会让
306
+ * 设置页的等待时间等于所有账号之和(实测 3 个账号串行约 1.5s)。
307
+ * 并行后约等于最慢的那个。
308
+ *
309
+ * 单个账号失败只丢它自己的数据,不影响其他账号。
310
+ */
302
311
  const loadCredits = async (force = false) => {
303
312
  const now = Date.now()
304
313
  if (!force && creditsCache.value !== null && now - creditsCache.at < 60_000) {
305
314
  return creditsCache.value
306
315
  }
307
- const out = {}
308
- for (const account of await store.list()) {
316
+ const accounts = await store.list()
317
+
318
+ /** 查一个账号的积分;失败返回 undefined 而不是抛错。 */
319
+ const queryOne = async (account) => {
309
320
  try {
310
321
  const credential = await store.usable(account.id)
311
- out[account.id] = await fetchRegionCredits(credential)
322
+ const credits = await fetchRegionCredits(credential)
312
323
  // 顺手把总积分喂给选号器,让加权用上真实余额。
313
- picker.setCredits(account.id, out[account.id].total)
324
+ picker.setCredits(account.id, credits.total)
325
+ return [account.id, credits]
314
326
  } catch (error) {
315
327
  logger.warn(`账号 ${account.id} 查询积分失败:${error?.message ?? error}`)
328
+ return undefined
316
329
  }
317
330
  }
331
+
332
+ const settled = await Promise.all(accounts.map(queryOne))
333
+ const out = {}
334
+ for (const entry of settled) {
335
+ if (entry !== undefined) out[entry[0]] = entry[1]
336
+ }
318
337
  creditsCache = { at: now, value: out }
319
338
  return out
320
339
  }
@@ -625,34 +644,41 @@ export function apply(ctx, config = {}) {
625
644
  if (!loopbackOrigin(req)) { json(res, 403, { ok: false, error: '仅允许本机访问' }); return }
626
645
  try {
627
646
  const list = await ready
628
- const out = {}
629
- for (const stack of list) {
647
+
648
+ /**
649
+ * 取一个区域的状态。
650
+ *
651
+ * 区域之间**并行**:cn 与 trae 是两套独立上游,串行会让设置页
652
+ * 的等待时间变成两者之和(冷缓存实测约 1.5s);并行后约等于
653
+ * 最慢的那个。
654
+ */
655
+ const regionState = async (stack) => {
630
656
  const snapshot = stack.picker.snapshot()
631
- out[stack.region] = {
657
+ const listed = await stack.store.list()
658
+ const credits = await stack.loadCredits()
659
+ return [stack.region, {
632
660
  provider: stack.providerId,
633
661
  displayName: REGION_DISPLAY_NAMES[stack.region],
634
662
  models: stack.getCatalog().length,
635
- accounts: (await (async () => {
636
- const listed = await stack.store.list()
637
- const credits = await stack.loadCredits()
638
- return listed.map(account => ({
639
- id: account.id,
640
- nickname: account.nickname,
641
- uid: account.uid,
642
- domain: account.domain,
643
- expiresAtMs: account.expiresAtMs,
644
- ...snapshot[account.id],
645
- cooling: snapshot[account.id]?.cooling ?? false,
646
- // 积分:剩余 + 总量 + 套餐明细(界面展示用)。
647
- // size 供悬停提示展示「总量 / 已用 / 剩余」构成。
648
- credits: credits[account.id]?.total,
649
- creditsSize: credits[account.id]?.size,
650
- creditPackages: credits[account.id]?.packages?.slice(0, 5) ?? [],
651
- }))
652
- })()),
653
- }
663
+ accounts: listed.map(account => ({
664
+ id: account.id,
665
+ nickname: account.nickname,
666
+ uid: account.uid,
667
+ domain: account.domain,
668
+ expiresAtMs: account.expiresAtMs,
669
+ ...snapshot[account.id],
670
+ cooling: snapshot[account.id]?.cooling ?? false,
671
+ // 积分:剩余 + 总量 + 套餐明细(界面展示用)。
672
+ // size 供悬停提示展示「总量 / 已用 / 剩余」构成。
673
+ credits: credits[account.id]?.total,
674
+ creditsSize: credits[account.id]?.size,
675
+ creditPackages: credits[account.id]?.packages?.slice(0, 5) ?? [],
676
+ })),
677
+ }]
654
678
  }
655
- json(res, 200, { ok: true, regions: out })
679
+
680
+ const settled = await Promise.all(list.map(regionState))
681
+ json(res, 200, { ok: true, regions: Object.fromEntries(settled) })
656
682
  } catch (error) {
657
683
  json(res, 500, { ok: false, error: safeMessage(error) })
658
684
  }
@@ -918,6 +944,13 @@ export function apply(ctx, config = {}) {
918
944
  const accounts = await stack.store.list()
919
945
  if (accounts.length === 0) { json(res, 200, { ok: true, results: [] }); return }
920
946
 
947
+ // 跑任务前先取一次积分:跑完再取一次,差值就是这次**实际获得的积分**。
948
+ //
949
+ // 为什么不直接读接口返回值:签到接口只回成功/失败,不回奖励数量;
950
+ // Trae 的对话/任务流也没有任何计费字段(实测只有六种事件)。
951
+ // 唯一可靠的真值就是「积分余额的前后差」。
952
+ const before = await stack.loadCredits(true).catch(() => ({}))
953
+
921
954
  // 账号间串行:上游对短时间内的密集请求有风控,串行更稳。
922
955
  const results = []
923
956
  for (const account of accounts) {
@@ -934,9 +967,39 @@ export function apply(ctx, config = {}) {
934
967
  }
935
968
  }
936
969
 
937
- // 任务跑完刷新积分与目录
938
- await stack.loadCredits(true).catch(() => {})
939
- json(res, 200, { ok: true, results })
970
+ // 任务跑完刷新积分:这里的结果既是缓存更新,也是「跑完后」的快照
971
+ const after = await stack.loadCredits(true).catch(() => ({}))
972
+
973
+ /**
974
+ * 算出每个账号这次获得的积分。
975
+ *
976
+ * 只把**正差值**当作「获得」:
977
+ * - 差值为正 → 签到/活动奖励到账
978
+ * - 差值为负 → 期间有别的消耗(对话),不算获得,记 0
979
+ * - 两边都取不到 → 标 unknown,界面显示 —,不编造数字
980
+ */
981
+ const earnedOf = (accountId) => {
982
+ const b = before[accountId]?.total
983
+ const a = after[accountId]?.total
984
+ if (typeof b !== 'number' || typeof a !== 'number') return undefined
985
+ const delta = a - b
986
+ // 负差值可能是「期间用了额度」,不算获得;只取正收益
987
+ return delta > 0 ? Math.round(delta * 100) / 100 : 0
988
+ }
989
+ for (const r of results) {
990
+ const earned = earnedOf(r.accountId)
991
+ if (earned !== undefined) r.earnedCredits = earned
992
+ }
993
+
994
+ const totalEarned = results.reduce(
995
+ (sum, r) => sum + (typeof r.earnedCredits === 'number' ? r.earnedCredits : 0), 0)
996
+ json(res, 200, {
997
+ ok: true,
998
+ results,
999
+ totalEarned: Math.round(totalEarned * 100) / 100,
1000
+ // 有账号算不出差值时,界面要对「合计」加注说明,避免误以为是全部
1001
+ earnedPartial: results.some(r => typeof r.earnedCredits !== 'number'),
1002
+ })
940
1003
  } catch (error) {
941
1004
  json(res, 500, { ok: false, error: safeMessage(error) })
942
1005
  }
@@ -1019,7 +1082,32 @@ export function apply(ctx, config = {}) {
1019
1082
  const list = await ready
1020
1083
  const out = {}
1021
1084
  for (const stack of list) {
1022
- out[stack.region] = stack.usage.snapshot(hours)
1085
+ const snapshot = stack.usage.snapshot(hours)
1086
+ /**
1087
+ * 上游不报单次消耗时,用积分接口的**累计已消耗**做真值。
1088
+ *
1089
+ * Trae 的对话流没有任何计费字段(实测只有 metadata /
1090
+ * timing_cost / output / extra_info / token_usage / done
1091
+ * 六种事件),所以按请求累加的 credits 恒为 0。它的积分
1092
+ * 接口能给出累计已消耗,虽然分不到「哪次请求」,但至少
1093
+ * 是真实数字,比显示 0 有意义。
1094
+ *
1095
+ * 用缓存(不强制刷新)以免打开用量页就打一轮上游。
1096
+ */
1097
+ const credits = await stack.loadCredits().catch(() => ({}))
1098
+ let consumedTotal = 0
1099
+ let hasConsumed = false
1100
+ for (const account of Object.values(credits)) {
1101
+ if (typeof account?.consumed === 'number') {
1102
+ consumedTotal += account.consumed
1103
+ hasConsumed = true
1104
+ }
1105
+ }
1106
+ out[stack.region] = {
1107
+ ...snapshot,
1108
+ // 上游累计口径(目前只有 Trae 有);undefined 表示这家不支持
1109
+ upstreamConsumed: hasConsumed ? Math.round(consumedTotal * 100) / 100 : undefined,
1110
+ }
1023
1111
  }
1024
1112
  json(res, 200, { ok: true, regions: out })
1025
1113
  } catch (error) {
@@ -421,10 +421,15 @@ export async function fetchCredits(credential) {
421
421
  const consumed = Number(summary?.consumed_amount)
422
422
  if (Number.isFinite(totalAmount) && Number.isFinite(consumed)) {
423
423
  // 保留两位小数:桌面显示也是两位(1621.73)。
424
- // size = 总量,供界面悬停展示「总量 / 已用 / 剩余」。
424
+ // size = 总量,供界面悬停展示「总量 / 已用 / 剩余」。
425
+ // consumed = 累计已消耗。Trae 的对话流**不报告单次消耗**
426
+ // (实测上游只有 metadata/timing_cost/output/extra_info/
427
+ // token_usage/done 六种事件,没有任何计费字段),
428
+ // 所以用量统计只能靠这个累计值算差值。
425
429
  return {
426
430
  total: Math.round((totalAmount - consumed) * 100) / 100,
427
431
  size: totalAmount,
432
+ consumed: Math.round(consumed * 100) / 100,
428
433
  }
429
434
  }
430
435
  return { total: 0 }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-account-pool",
3
3
  "displayName": "Account Pool",
4
- "version": "0.1.1",
4
+ "version": "0.2.0",
5
5
  "description": "把多个 WorkBuddy / Trae 账号汇成账号池接入 DeepSeek Harness:自动切换、限流熔断退避、凭证安全落盘",
6
6
  "type": "module",
7
7
  "license": "MIT",