dsh-api-dashboard 1.4.2 → 1.4.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
@@ -125,7 +125,7 @@ dsh plugin --profile web add link:/root/dsha-api-dashboard
125
125
 
126
126
  ## 更新日志
127
127
 
128
- 最近一版 **v1.4.1** —— 安全与稳定性审计修复。一版一行的完整历史见 [`CHANGELOG.md`](CHANGELOG.md)。
128
+ 最近一版 **v1.4.4** —— 子代理胶囊只显示**它自己新产生**的消耗(不再把父会话继承来的历史、兄弟或后代子代理的消耗算进去);并恢复 v1.4.3 升级后误显示为 `~—` 的历史子代理行。一版一行的完整历史见 [`CHANGELOG.md`](CHANGELOG.md)。
129
129
 
130
130
  ## License
131
131
 
package/client/client.js CHANGED
@@ -858,13 +858,15 @@ window.__ModuleLoader__.load({
858
858
  ref: (n) => { if (n) requestAnimationFrame(() => syncSubsOverflow(n)); },
859
859
  onScroll: (e) => syncSubsOverflow(e.currentTarget) },
860
860
  list.map((s, i) => {
861
- const f = formatSessionCost({ cost: s.cost, currency: mainCur, costByCurrency: s.costByCurrency, waiting: false });
861
+ const f = formatSessionCost({ cost: s.cost, currency: mainCur, costByCurrency: s.costByCurrency, waiting: s.waiting === true });
862
+ // waiting(等自身数据) 时 formatSessionCost 直接返回 null → "~—";
863
+ // 真实零消耗时 f 非 null 但 hasValue=false, 也必须回落 "~—"(与 v1.4.x 一致), 不能显示 ~¥0.00
862
864
  const amt = f !== null && f.hasValue ? "~" + f.text : "~—";
863
865
  const kind = s.mode === "continuable" ? "可续聊子代理" : "一次性子代理";
864
866
  const models = (s.models || []).join(", ");
865
867
  const tip = [
866
868
  kind + " " + (s.label || "子代理"),
867
- "消耗 " + (f !== null ? f.title : ""),
869
+ "自身消耗 " + (f !== null ? f.title : "等待自身用量数据"),
868
870
  models ? "模型 " + models : "",
869
871
  s.id ? "ID " + String(s.id).slice(0, 8) : "",
870
872
  ].filter(Boolean).join("\n");
@@ -2645,7 +2647,7 @@ window.__ModuleLoader__.load({
2645
2647
 
2646
2648
  // v1.4.0: 子代理消耗行 —— 有子代理时在状态条**下面换行**追加一行,
2647
2649
  // 按父会话里的创建顺序从左到右排列 (顺序由服务端 subagentCatalog 决定, 这里不排序)。
2648
- // 金额是「该子代理及其后代」的汇总, 与主板数字同一套计价口径。
2650
+ // 只计该子代理自身新用量,不含继承历史、兄弟或后代。
2649
2651
  const subRow = buildSubagentRow(cost);
2650
2652
 
2651
2653
  return react.createElement("span", { className: "dshadb_barwrap" }, [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-api-dashboard",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "DeepSeek Harness 多平台 API 余额 / 用量看板:输入框下方实时显示各平台余额与本会话估算消耗",
5
5
  "keywords": [
6
6
  "dsh",
package/src/index.js CHANGED
@@ -1703,31 +1703,64 @@ let subagentCostSummarize = null
1703
1703
 
1704
1704
  const safeSessionId = (id) => typeof id === 'string' && /^[A-Za-z0-9._-]{1,128}$/.test(id) ? id : null
1705
1705
 
1706
- /** 读某会话的投影缓存记录(带 TTL 内存缓存)。任何异常 → null。 */
1707
- const readSessionCacheRecord = (sessionId) => {
1706
+ /**
1707
+ * 读某会话的投影缓存记录(带 TTL 内存缓存)。任何异常 null。
1708
+ * v1.4.4: 同时把 `identity` 带出来 —— 里面记着 `isSeeded` / `inheritedEventCount`(fork 边界),
1709
+ * 旧版本的缓存行(状态里没有 own)要靠它判断「自身用量」能不能精确还原。
1710
+ */
1711
+ const loadCacheEntry = (sessionId) => {
1708
1712
  const id = safeSessionId(sessionId)
1709
1713
  if (id === null) return null
1710
1714
  const now = Date.now()
1711
1715
  const hit = sessionCacheFiles.get(id)
1712
- if (hit !== undefined && now - hit.at < SUBAGENT_FILE_TTL_MS) return hit.rows
1713
- let rows = null
1716
+ if (hit !== undefined && now - hit.at < SUBAGENT_FILE_TTL_MS) return hit
1717
+ let entry = null
1714
1718
  try {
1715
1719
  const home = process.env.DSH_HOME || join(homedir(), '.dsh')
1716
1720
  const file = join(home, 'storages', 'session_projcache', 'sessions', `${id}.json`)
1717
1721
  const parsed = JSON.parse(readFileSync(file, 'utf8'))
1718
1722
  const r = parsed?.record?.rows
1719
- if (r !== null && typeof r === 'object') rows = r
1720
- } catch { rows = null }
1723
+ if (r !== null && typeof r === 'object') {
1724
+ const identity = parsed?.record?.identity
1725
+ entry = { at: now, rows: r, identity: (identity !== null && typeof identity === 'object') ? identity : null }
1726
+ }
1727
+ } catch { entry = null }
1721
1728
  // 只缓存成功结果, 避免一次读失败被 TTL 钉住 3 秒
1722
- if (rows !== null) sessionCacheFiles.set(id, { at: now, rows })
1729
+ if (entry !== null) sessionCacheFiles.set(id, entry)
1723
1730
  if (sessionCacheFiles.size > 256) sessionCacheFiles.clear()
1724
- return rows
1731
+ return entry
1725
1732
  }
1726
1733
 
1727
- /** 冷路径: 从缓存文件里取该会话的 queryBalanceCost **状态**(不是 wire 视图)。 */
1734
+ /** 读某会话的投影缓存 rows。任何异常 null。 */
1735
+ const readSessionCacheRecord = (sessionId) => loadCacheEntry(sessionId)?.rows ?? null
1736
+
1737
+ /**
1738
+ * 冷路径: 从缓存文件里取该会话的 queryBalanceCost **状态**(不是 wire 视图)。
1739
+ *
1740
+ * ⚠️ v1.4.4 修正 v1.4.3 的一个过严判断:v1.4.3 只认 `ver === 3`,于是**升级后所有旧缓存行
1741
+ * 一律显示 `~—`**(老框架不会再给已结束的子会话重写缓存,那些行永远好不了)。
1742
+ * 实际上旧缓存也能精确还原**非分叉**子会话的自身用量:
1743
+ * · `inheritedEventCount === 0`(没有继承任何事件)→ 自身 == 全量,`byModel` 就是精确值;
1744
+ * · 分叉过的子会话 → 旧缓存分不出继承段,**仍然返回 null**(上层显示「等待」),
1745
+ * 绝不退化成把父会话历史算进去的错数字。
1746
+ */
1728
1747
  const cachedCostState = (sessionId) => {
1729
- const val = readSessionCacheRecord(sessionId)?.queryBalanceCost?.val
1730
- return (val !== null && typeof val === 'object' && Array.isArray(val.modelOrder) && val.byModel !== null && typeof val.byModel === 'object') ? val : null
1748
+ const entry = loadCacheEntry(sessionId)
1749
+ const row = entry?.rows?.queryBalanceCost
1750
+ const val = row?.val
1751
+ if (val === null || typeof val !== 'object' || !Array.isArray(val.modelOrder) || val.byModel === null || typeof val.byModel !== 'object') return null
1752
+ if (row.ver === 3 && val.own !== undefined) return val
1753
+ const inherited = entry?.identity?.inheritedEventCount
1754
+ if (inherited === 0) {
1755
+ return {
1756
+ ...val, ownBoundaryKnown: true, inheritedEventCount: 0,
1757
+ own: {
1758
+ currentModel: val.currentModel ?? null, currentProvider: val.currentProvider ?? null,
1759
+ last: val.last ?? null, byModel: val.byModel, modelOrder: val.modelOrder,
1760
+ },
1761
+ }
1762
+ }
1763
+ return null
1731
1764
  }
1732
1765
 
1733
1766
  /** 冷路径: 从缓存文件里取该会话的 subagentCatalog 条目。 */
@@ -1745,7 +1778,7 @@ const cachedCatalog = (sessionId) => {
1745
1778
  }))
1746
1779
  }
1747
1780
 
1748
- export function collectSubagentCosts(services, rootSessionId, summarize) {
1781
+ export function collectSubagentCosts(services, rootSessionId, summarize, ownChildIds) {
1749
1782
  const out = []
1750
1783
  if (typeof rootSessionId !== 'string' || rootSessionId === '') return out
1751
1784
  let sessions = null, projections = null
@@ -1779,32 +1812,26 @@ export function collectSubagentCosts(services, rootSessionId, summarize) {
1779
1812
  if (s !== null && projections !== null) {
1780
1813
  try {
1781
1814
  const st = projections.stateOf(s, 'queryBalanceCost')
1782
- if (st !== null && st !== undefined && Array.isArray(st.modelOrder) && st.modelOrder.length > 0) return st
1815
+ if (st !== null && st !== undefined) return st
1783
1816
  } catch { /* 落到冷路径 */ }
1784
1817
  }
1785
1818
  return cachedCostState(sessionId)
1786
1819
  }
1787
1820
 
1788
- /** 递归汇总: 自身 + 后代, 返回与 summarize 同形的汇总。 */
1789
- const rollup = (sessionId, depth) => {
1790
- const st = costStateOf(sessionId)
1791
- let acc = (st !== null && subagentCostSummarize !== null) ? subagentCostSummarize(st) : null
1792
- if (depth >= SUBAGENT_MAX_DEPTH) return acc
1793
- for (const entry of childrenOf(sessionId)) {
1794
- acc = mergeSummary(acc, rollup(entry.id, depth + 1))
1795
- }
1796
- return acc
1797
- }
1798
-
1799
1821
  for (const entry of childrenOf(rootSessionId)) {
1800
1822
  if (out.length >= SUBAGENT_MAX_ROWS) break
1801
- const s = rollup(entry.id, 1) ?? emptySummary()
1823
+ if (Array.isArray(ownChildIds) && !ownChildIds.includes(entry.id)) continue
1824
+ const st = costStateOf(entry.id)
1825
+ const ready = st?.ownBoundaryKnown === true && st?.own && Array.isArray(st.own.modelOrder)
1826
+ const waiting = !ready || st.own.modelOrder.length === 0
1827
+ const s = ready ? summarize(st.own) : emptySummary()
1802
1828
  out.push({
1803
1829
  id: String(entry.id),
1804
1830
  label: typeof entry.label === 'string' && entry.label !== '' ? entry.label : String(entry.id).slice(0, 12),
1805
1831
  mode: entry.mode === 'continuable' ? 'continuable' : 'one-shot',
1806
1832
  createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : 0,
1807
- cost: s.cost,
1833
+ cost: waiting ? -1 : s.cost,
1834
+ waiting,
1808
1835
  costByCurrency: s.costByCurrency,
1809
1836
  currencyByModel: s.currencyByModel,
1810
1837
  mixedCurrency: s.mixedCurrency,
@@ -1952,39 +1979,7 @@ export function makeCostProjection(configOrGetter, services) {
1952
1979
  }
1953
1980
  }
1954
1981
 
1955
- return {
1956
- key: 'queryBalanceCost',
1957
- // 框架要求的投影定义 API: stateSchema(内部状态) + wire.{viewSchema,view}(客户端可见视图)。
1958
- // 旧版误用顶层 schema+view, 导致 wire 缺失, 服务端 drive 永不通知、客户端永远拿不到值。
1959
- stateSchema: z.object({
1960
- currentModel: z.string().nullable(),
1961
- currentProvider: z.string().nullable(),
1962
- last: z.object({
1963
- turn: z.number(),
1964
- step: z.number(),
1965
- model: z.string(),
1966
- buckets: z.object({
1967
- uncachedInputTokens: z.number(),
1968
- cacheReadTokens: z.number(),
1969
- cacheWriteTokens: z.number(),
1970
- outputTokens: z.number(),
1971
- }),
1972
- }).nullable(),
1973
- byModel: z.record(z.string(), z.object({
1974
- uncachedInputTokens: z.number(),
1975
- cacheReadTokens: z.number(),
1976
- cacheWriteTokens: z.number(),
1977
- outputTokens: z.number(),
1978
- })),
1979
- modelOrder: z.array(z.string()),
1980
- /** v1.4.0: 本投影所属会话 id —— 子代理汇总要拿它去查 `subagentCatalog`。空串表示未知。 */
1981
- sessionId: z.string(),
1982
- }),
1983
- init: (header) => ({
1984
- currentModel: null, currentProvider: null, last: null, byModel: {}, modelOrder: [],
1985
- sessionId: typeof header?.id === 'string' ? header.id : '',
1986
- }),
1987
- apply: (state, event) => {
1982
+ const foldUsage = (state, event) => {
1988
1983
  // v1.4.0: `llm/retry-started` 关闭「替换槽位」—— 被重试的那次 attempt 的用量要**留在总量里**,
1989
1984
  // 下一次 attempt 是**新增**而不是替换。与 dsh-token-meter 的 usage-projection 对齐。
1990
1985
  if (event.type === 'llm/retry-started') {
@@ -2027,6 +2022,56 @@ export function makeCostProjection(configOrGetter, services) {
2027
2022
  if (prev !== null) byModel = { ...byModel, [prev.model]: subBuckets(byModel[prev.model] ?? zero(), prev.buckets) }
2028
2023
  byModel = { ...byModel, [model]: addBuckets(byModel[model] ?? zero(), buckets) }
2029
2024
  return { ...state, currentModel: nextModel, currentProvider: nextProvider, last: { turn, step, model, buckets }, byModel, modelOrder: isNewModel ? [...state.modelOrder, model] : state.modelOrder }
2025
+ }
2026
+
2027
+ return {
2028
+ key: 'queryBalanceCost',
2029
+ // 框架要求的投影定义 API: stateSchema(内部状态) + wire.{viewSchema,view}(客户端可见视图)。
2030
+ // 旧版误用顶层 schema+view, 导致 wire 缺失, 服务端 drive 永不通知、客户端永远拿不到值。
2031
+ stateSchema: z.object({
2032
+ currentModel: z.string().nullable(),
2033
+ currentProvider: z.string().nullable(),
2034
+ last: z.object({
2035
+ turn: z.number(),
2036
+ step: z.number(),
2037
+ model: z.string(),
2038
+ buckets: z.object({
2039
+ uncachedInputTokens: z.number(),
2040
+ cacheReadTokens: z.number(),
2041
+ cacheWriteTokens: z.number(),
2042
+ outputTokens: z.number(),
2043
+ }),
2044
+ }).nullable(),
2045
+ byModel: z.record(z.string(), z.object({
2046
+ uncachedInputTokens: z.number(),
2047
+ cacheReadTokens: z.number(),
2048
+ cacheWriteTokens: z.number(),
2049
+ outputTokens: z.number(),
2050
+ })),
2051
+ modelOrder: z.array(z.string()),
2052
+ /** v1.4.0: 本投影所属会话 id —— 子代理汇总要拿它去查 `subagentCatalog`。空串表示未知。 */
2053
+ sessionId: z.string(),
2054
+ inheritedEventCount: z.number().int().nonnegative(),
2055
+ ownBoundaryKnown: z.boolean(),
2056
+ ownChildIds: z.array(z.string()),
2057
+ own: z.object({ currentModel: z.string().nullable(), currentProvider: z.string().nullable(), last: z.any().nullable(), byModel: z.record(z.string(), z.object({ uncachedInputTokens: z.number().nonnegative(), cacheReadTokens: z.number().nonnegative(), cacheWriteTokens: z.number().nonnegative(), outputTokens: z.number().nonnegative() })), modelOrder: z.array(z.string()) }),
2058
+ }),
2059
+ init: (header, inheritedEventCount) => ({
2060
+ currentModel: null, currentProvider: null, last: null, byModel: {}, modelOrder: [],
2061
+ sessionId: typeof header?.id === 'string' ? header.id : '',
2062
+ inheritedEventCount: Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0 ? inheritedEventCount : 0,
2063
+ ownBoundaryKnown: (Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0) || !header?.isSeeded,
2064
+ ownChildIds: [],
2065
+ own: { currentModel: null, currentProvider: null, last: null, byModel: {}, modelOrder: [] },
2066
+ }),
2067
+ apply: (state, event) => {
2068
+ const full = foldUsage(state, event)
2069
+ const isOwn = state.ownBoundaryKnown && (state.inheritedEventCount === 0 || (Number.isSafeInteger(event.seq) && event.seq >= state.inheritedEventCount))
2070
+ // Inherited model context is useful; inherited usage and retry slots are not.
2071
+ const own = isOwn ? foldUsage(state.own, event) : { ...state.own, currentModel: full.currentModel, currentProvider: full.currentProvider }
2072
+ const childId = isOwn && event.type === 'subagent/catalog' ? event.data?.childId : null
2073
+ const ownChildIds = typeof childId === 'string' && !state.ownChildIds.includes(childId) ? [...state.ownChildIds, childId] : state.ownChildIds
2074
+ return { ...full, own, ownChildIds }
2030
2075
  },
2031
2076
  wire: {
2032
2077
  viewSchema: z.object({
@@ -2055,6 +2100,7 @@ export function makeCostProjection(configOrGetter, services) {
2055
2100
  mode: z.enum(['one-shot', 'continuable']),
2056
2101
  createdAt: z.number(),
2057
2102
  cost: z.number(),
2103
+ waiting: z.boolean().optional(),
2058
2104
  costByCurrency: z.record(z.string(), z.number().nonnegative()),
2059
2105
  currencyByModel: z.record(z.string(), z.string()),
2060
2106
  mixedCurrency: z.boolean(),
@@ -2071,7 +2117,7 @@ export function makeCostProjection(configOrGetter, services) {
2071
2117
  const cfg = getConfig()
2072
2118
  const mainCurrency = (cfg.currency ?? 'CNY').toUpperCase()
2073
2119
  // v1.4.0: 子代理消耗 (换行单独展示)。取不到服务/没有子代理 → 空数组。
2074
- const subagents = collectSubagentCosts(services, state.sessionId, summarize)
2120
+ const subagents = collectSubagentCosts(services, state.sessionId, summarize, state.ownChildIds)
2075
2121
  // 无事件时返回 waiting 标记, 客户端据此显示 "~—" 而非 "~¥0"
2076
2122
  if (state.modelOrder.length === 0) {
2077
2123
  return { models: [], currentModel: state.currentModel ?? null, currentProvider: state.currentProvider ?? null, cost: -1, costByModel: {}, costByCurrency: {}, currencyByModel: {}, mixedCurrency: false, tokens: { uncachedInput: 0, cacheRead: 0, cacheWrite: 0, output: 0 }, tokensByModel: {}, currency: mainCurrency, isPeak: isPeakTime(), waiting: true, subagents }
@@ -2085,7 +2131,7 @@ export function makeCostProjection(configOrGetter, services) {
2085
2131
  }
2086
2132
  },
2087
2133
  },
2088
- stateVersion: 2,
2134
+ stateVersion: 3,
2089
2135
  }
2090
2136
  }
2091
2137