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 +1 -1
- package/client/client.js +5 -3
- package/package.json +1 -1
- package/src/index.js +107 -61
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.
|
|
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:
|
|
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
|
-
"
|
|
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
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
|
-
/**
|
|
1707
|
-
|
|
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
|
|
1713
|
-
let
|
|
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')
|
|
1720
|
-
|
|
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 (
|
|
1729
|
+
if (entry !== null) sessionCacheFiles.set(id, entry)
|
|
1723
1730
|
if (sessionCacheFiles.size > 256) sessionCacheFiles.clear()
|
|
1724
|
-
return
|
|
1731
|
+
return entry
|
|
1725
1732
|
}
|
|
1726
1733
|
|
|
1727
|
-
/**
|
|
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
|
|
1730
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
2134
|
+
stateVersion: 3,
|
|
2089
2135
|
}
|
|
2090
2136
|
}
|
|
2091
2137
|
|