dsh-cost-meter 1.5.7 → 1.5.9
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.en.md +5 -5
- package/README.md +5 -5
- package/lib/backfill.js +41 -7
- package/lib/client.js +190 -10
- package/lib/index.js +44 -3
- package/lib/store.js +9 -2
- package/lib/typert.host.js +49 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Per-conversation cost · daily totals · OpenCode Go subscription quota display · budget with usage percentage · official account balance · custom provider balance · balance progress bar · history · peak/off-peak pricing hours display (peak hours UTC 01:00–04:00, 06:00–10:00) · one-click price sync from the official docs · Codex-style token usage heat grid · multi-vendor model pricing (built-in 90+ model price catalog with auto-matching) · mainstream Coding Plan quota queries & display (Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -191,22 +191,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
191
191
|
dsh plugin --profile web add dsh-cost-meter
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
**PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.5.
|
|
194
|
+
**PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.5.9`** — review the script before running):
|
|
195
195
|
|
|
196
196
|
```powershell
|
|
197
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
197
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.9/install.ps1 | iex
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
201
201
|
|
|
202
202
|
```sh
|
|
203
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
203
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.9
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
Without git, use the GitHub tag archive:
|
|
207
207
|
|
|
208
208
|
```sh
|
|
209
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
209
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.9.tar.gz
|
|
210
210
|
```
|
|
211
211
|
|
|
212
212
|
After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow 六家)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -193,22 +193,22 @@
|
|
|
193
193
|
dsh plugin --profile web add dsh-cost-meter
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.
|
|
196
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.9`**,建议先下载审阅再运行):
|
|
197
197
|
|
|
198
198
|
```powershell
|
|
199
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
199
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.9/install.ps1 | iex
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
203
203
|
|
|
204
204
|
```sh
|
|
205
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
205
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.9
|
|
206
206
|
```
|
|
207
207
|
|
|
208
208
|
没有 git 时可用 GitHub tag 打包直链:
|
|
209
209
|
|
|
210
210
|
```sh
|
|
211
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
211
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.9.tar.gz
|
|
212
212
|
```
|
|
213
213
|
|
|
214
214
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
package/lib/backfill.js
CHANGED
|
@@ -141,7 +141,7 @@ export function listSessionLogs(root) {
|
|
|
141
141
|
* @param records - readSessionRecords 的输出。
|
|
142
142
|
* @param config - 账本配置(prices / peak* / priceMatch / priceOverrides)。
|
|
143
143
|
* @param wantDates - 只统计这些日期键(YYYY-MM-DD)内的调用;null = 全部。
|
|
144
|
-
* @returns { sessionId, days: { date: { providerKey: 桶 } } }。
|
|
144
|
+
* @returns { sessionId, title, days: { date: { providerKey: 桶 } } }。
|
|
145
145
|
*/
|
|
146
146
|
export function replaySessionRecords(records, config, wantDates = null) {
|
|
147
147
|
const zeroBuckets = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, calls: 0, cost: 0 })
|
|
@@ -150,6 +150,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
150
150
|
return Number.isFinite(n) && n > 0 ? n : 0
|
|
151
151
|
}
|
|
152
152
|
let sessionId = ''
|
|
153
|
+
let title = ''
|
|
153
154
|
let provider = 'deepseek'
|
|
154
155
|
let model = 'default'
|
|
155
156
|
let last = null
|
|
@@ -173,6 +174,12 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
173
174
|
sessionId = event.id
|
|
174
175
|
continue
|
|
175
176
|
}
|
|
177
|
+
// 会话标题(宿主生成的 session/title 事件;同名多次时取最后一次)。
|
|
178
|
+
if (event.type === 'session/title') {
|
|
179
|
+
const nextTitle = event.data?.title
|
|
180
|
+
if (typeof nextTitle === 'string' && nextTitle.trim().length > 0) title = nextTitle.trim()
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
176
183
|
if (PACKED_ROW_TYPES.has(event.type)) continue
|
|
177
184
|
if (event.type === 'request/header') {
|
|
178
185
|
const nextModel = event.data?.header?.config?.model
|
|
@@ -231,7 +238,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
231
238
|
shift(sample, 1)
|
|
232
239
|
last = sample
|
|
233
240
|
}
|
|
234
|
-
return { sessionId, days }
|
|
241
|
+
return { sessionId, title, days }
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
/**
|
|
@@ -241,10 +248,10 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
241
248
|
* 写入该会话当日的回放拆分。
|
|
242
249
|
* @param ledger - 已打开的账本。
|
|
243
250
|
* @param sessionsRoot - 宿主会话根目录($DSH_HOME/sessions)。
|
|
244
|
-
* @returns { days, sessions, scanned }
|
|
251
|
+
* @returns { days, sessions, scanned, titles } 实际填补的日期/会话数、扫描文件数与补齐的会话标题数。
|
|
245
252
|
*/
|
|
246
253
|
export async function backfillLegacyLedger(ledger, sessionsRoot) {
|
|
247
|
-
const result = { days: 0, sessions: 0, scanned: 0 }
|
|
254
|
+
const result = { days: 0, sessions: 0, scanned: 0, titles: 0 }
|
|
248
255
|
const needDates = new Set()
|
|
249
256
|
for (const [date, day] of Object.entries(ledger.days ?? {})) {
|
|
250
257
|
if ((day?.calls ?? 0) > 0 && Object.keys(day?.byProviderModel ?? {}).length === 0) needDates.add(date)
|
|
@@ -260,8 +267,20 @@ export async function backfillLegacyLedger(ledger, sessionsRoot) {
|
|
|
260
267
|
}
|
|
261
268
|
}
|
|
262
269
|
}
|
|
263
|
-
|
|
270
|
+
// 会话标题补齐:任一会话条目缺标题则需扫描(日志里的 session/title 事件是标题权威来源)。
|
|
271
|
+
let needTitles = false
|
|
272
|
+
for (const day of Object.values(ledger.days ?? {})) {
|
|
273
|
+
for (const session of day?.sessions ?? []) {
|
|
274
|
+
if (typeof session?.title !== 'string' || session.title.length === 0) {
|
|
275
|
+
needTitles = true
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (needTitles) break
|
|
280
|
+
}
|
|
281
|
+
if (needDates.size === 0 && !needTitles) return result
|
|
264
282
|
const bySession = new Map()
|
|
283
|
+
const titles = new Map()
|
|
265
284
|
let scannedCount = 0
|
|
266
285
|
for (const path of listSessionLogs(sessionsRoot)) {
|
|
267
286
|
// 会话日志多时逐份解压会长时间占住事件循环:每 8 份让出一次,不卡宿主 UI。
|
|
@@ -269,11 +288,13 @@ export async function backfillLegacyLedger(ledger, sessionsRoot) {
|
|
|
269
288
|
result.scanned += 1
|
|
270
289
|
let replayed
|
|
271
290
|
try {
|
|
272
|
-
replayed = replaySessionRecords(readSessionRecords(path), ledger.config, needDates)
|
|
291
|
+
replayed = replaySessionRecords(readSessionRecords(path), ledger.config, needDates.size > 0 ? needDates : new Set(['-']))
|
|
273
292
|
} catch {
|
|
274
293
|
continue // 单文件损坏不阻断整体回填。
|
|
275
294
|
}
|
|
276
295
|
if (replayed.sessionId.length === 0) continue
|
|
296
|
+
if (replayed.title.length > 0 && !titles.has(replayed.sessionId)) titles.set(replayed.sessionId, replayed.title)
|
|
297
|
+
if (needDates.size === 0) continue
|
|
277
298
|
const existing = bySession.get(replayed.sessionId)
|
|
278
299
|
if (existing === undefined) bySession.set(replayed.sessionId, replayed.days)
|
|
279
300
|
else mergeDayMaps(existing, replayed.days)
|
|
@@ -351,7 +372,20 @@ export async function backfillLegacyLedger(ledger, sessionsRoot) {
|
|
|
351
372
|
}
|
|
352
373
|
}
|
|
353
374
|
}
|
|
354
|
-
|
|
375
|
+
// 会话标题补齐:只填缺失的标题,不覆盖已有(幂等;实时新建的会话下次启动补齐)。
|
|
376
|
+
if (titles.size > 0) {
|
|
377
|
+
for (const day of Object.values(ledger.days ?? {})) {
|
|
378
|
+
for (const session of day?.sessions ?? []) {
|
|
379
|
+
if (session === null || typeof session !== 'object') continue
|
|
380
|
+
if (typeof session.title === 'string' && session.title.length > 0) continue
|
|
381
|
+
const found = titles.get(session.id)
|
|
382
|
+
if (found === undefined) continue
|
|
383
|
+
session.title = found
|
|
384
|
+
result.titles += 1
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (result.days > 0 || result.sessions > 0 || result.titles > 0) ledger.scheduleWrite()
|
|
355
389
|
return result
|
|
356
390
|
}
|
|
357
391
|
|
package/lib/client.js
CHANGED
|
@@ -77,6 +77,11 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
'.cm-table th{color:var(--dsw-alias-label-tertiary);font-weight:500}',
|
|
78
78
|
'.cm-table td.num,.cm-table th.num{text-align:right;font-variant-numeric:tabular-nums}',
|
|
79
79
|
'.cm-table tr:last-child td{border-bottom:none}',
|
|
80
|
+
'.cm-table tr.cm-row-click{cursor:pointer}',
|
|
81
|
+
'.cm-table tr.cm-row-click:hover td{background:var(--dsw-alias-bg-hover,rgba(127,127,127,.08))}',
|
|
82
|
+
// 会话单元格:标题为主行(超出省略),会话 ID 为辅行(可选显示,等宽淡化)。
|
|
83
|
+
'.cm-sess-title{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
84
|
+
'.cm-sess-id{font-size:11px;font-family:ui-monospace,Consolas,monospace;color:var(--dsw-alias-label-tertiary)}',
|
|
80
85
|
'.cm-empty{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:8px 0}',
|
|
81
86
|
'.cm-scroll{max-height:320px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}',
|
|
82
87
|
'.cm-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:12px}',
|
|
@@ -481,6 +486,17 @@ window.__ModuleLoader__.load({
|
|
|
481
486
|
cardTotal: '累计费用',
|
|
482
487
|
cardTotalSub: '自账本建立以来 · 调用 {calls} 次',
|
|
483
488
|
todaySessions: '今日会话',
|
|
489
|
+
historyExpandHint: '点击日期行可展开当日会话明细',
|
|
490
|
+
historySessionsLoading: '会话明细加载中…',
|
|
491
|
+
historyNoSessions: '该日无会话明细(早期数据或会话日志已清理)',
|
|
492
|
+
historySessionsError: '会话明细加载失败',
|
|
493
|
+
sessionRankTitle: '按会话统计(全部历史)',
|
|
494
|
+
sessionRankHint: '不按日期分组,全部历史会话按费用降序',
|
|
495
|
+
sessionRankEmpty: '暂无会话数据',
|
|
496
|
+
sessionRankLoading: '会话排行加载中…',
|
|
497
|
+
sessionRankError: '会话排行加载失败',
|
|
498
|
+
sessionRankLimit: '显示条数',
|
|
499
|
+
showSessionIdLabel: '会话列表附显会话 ID',
|
|
484
500
|
history: '历史记录',
|
|
485
501
|
usageTitle: 'Token 用量统计',
|
|
486
502
|
usageTotal: '累计 {tokens} tokens · 输入 {input} · 缓存 {cache} · 输出 {output} · {calls} 次调用',
|
|
@@ -757,6 +773,17 @@ window.__ModuleLoader__.load({
|
|
|
757
773
|
cardTotal: 'All time',
|
|
758
774
|
cardTotalSub: 'Since the ledger was created · Calls {calls}',
|
|
759
775
|
todaySessions: "Today's sessions",
|
|
776
|
+
historyExpandHint: 'Click a date row to expand its session details',
|
|
777
|
+
historySessionsLoading: 'Loading session details…',
|
|
778
|
+
historyNoSessions: 'No session details for this day (early data or session logs cleaned)',
|
|
779
|
+
historySessionsError: 'Failed to load session details',
|
|
780
|
+
sessionRankTitle: 'By session (all history)',
|
|
781
|
+
sessionRankHint: 'All historical sessions sorted by cost, not grouped by date',
|
|
782
|
+
sessionRankEmpty: 'No session data yet',
|
|
783
|
+
sessionRankLoading: 'Loading session ranking…',
|
|
784
|
+
sessionRankError: 'Failed to load session ranking',
|
|
785
|
+
sessionRankLimit: 'Rows',
|
|
786
|
+
showSessionIdLabel: 'Show session IDs in session lists',
|
|
760
787
|
history: 'History',
|
|
761
788
|
usageTitle: 'Token usage stats',
|
|
762
789
|
usageTotal: 'All-time {tokens} tokens · input {input} · cache {cache} · output {output} · {calls} calls',
|
|
@@ -1143,6 +1170,22 @@ window.__ModuleLoader__.load({
|
|
|
1143
1170
|
if (typeof v !== 'string') fail('provider', 'string')
|
|
1144
1171
|
return v
|
|
1145
1172
|
})
|
|
1173
|
+
const dateCodec = codecOf(v => {
|
|
1174
|
+
if (typeof v !== 'string') fail('date', 'string')
|
|
1175
|
+
return v
|
|
1176
|
+
})
|
|
1177
|
+
const dayCodec = codecOf(v => {
|
|
1178
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) fail('day', 'object')
|
|
1179
|
+
return v
|
|
1180
|
+
})
|
|
1181
|
+
const limitCodec = codecOf(v => {
|
|
1182
|
+
if (!Number.isFinite(Number(v))) fail('limit', 'number')
|
|
1183
|
+
return Number(v)
|
|
1184
|
+
})
|
|
1185
|
+
const topSessionsCodec = codecOf(v => {
|
|
1186
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) fail('topSessions', 'object')
|
|
1187
|
+
return v
|
|
1188
|
+
})
|
|
1146
1189
|
|
|
1147
1190
|
// ── RPC 贡献(与服务端 ./typert 清单一一对应) ───────────────────────────
|
|
1148
1191
|
|
|
@@ -1191,6 +1234,18 @@ window.__ModuleLoader__.load({
|
|
|
1191
1234
|
invocation: { kind: 'direct' }, parameters: [],
|
|
1192
1235
|
result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#CostState', schema: stateCodec },
|
|
1193
1236
|
},
|
|
1237
|
+
{
|
|
1238
|
+
id: 'dsh-cost-meter#costMeter/getDaySessions', service: 'costMeter', namespace: 'costMeter', method: 'getDaySessions',
|
|
1239
|
+
invocation: { kind: 'direct' },
|
|
1240
|
+
parameters: [{ name: 'date', wire: 'date', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#DayKey', schema: dateCodec } }],
|
|
1241
|
+
result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#DayRecord', schema: dayCodec },
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
id: 'dsh-cost-meter#costMeter/getTopSessions', service: 'costMeter', namespace: 'costMeter', method: 'getTopSessions',
|
|
1245
|
+
invocation: { kind: 'direct' },
|
|
1246
|
+
parameters: [{ name: 'limit', wire: 'limit', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionLimit', schema: limitCodec } }],
|
|
1247
|
+
result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#TopSessions', schema: topSessionsCodec },
|
|
1248
|
+
},
|
|
1194
1249
|
],
|
|
1195
1250
|
}
|
|
1196
1251
|
|
|
@@ -2281,23 +2336,123 @@ window.__ModuleLoader__.load({
|
|
|
2281
2336
|
}
|
|
2282
2337
|
|
|
2283
2338
|
function HistoryTable(props) {
|
|
2284
|
-
const { state } = props
|
|
2339
|
+
const { state, api } = props
|
|
2285
2340
|
const t = makeT(resolveLocale(state.config?.locale))
|
|
2341
|
+
// 点击日期行展开当日会话明细(issue #22):按需经 getDaySessions 拉取并缓存。
|
|
2342
|
+
const [openDate, setOpenDate] = useState(null)
|
|
2343
|
+
const [cache, setCache] = useState({})
|
|
2344
|
+
const [busyDate, setBusyDate] = useState(null)
|
|
2286
2345
|
const rows = state.history ?? []
|
|
2287
2346
|
if (rows.length === 0) return el('p', { className: 'cm-empty' }, t('noHistory'))
|
|
2347
|
+
const toggle = date => {
|
|
2348
|
+
if (openDate === date) { setOpenDate(null); return }
|
|
2349
|
+
setOpenDate(date)
|
|
2350
|
+
if (cache[date] !== undefined || busyDate !== null) return
|
|
2351
|
+
setBusyDate(date)
|
|
2352
|
+
api.getDaySessions(date)
|
|
2353
|
+
.then(day => setCache(c => ({ ...c, [date]: Array.isArray(day?.sessions) ? day.sessions : [] })))
|
|
2354
|
+
.catch(() => setCache(c => ({ ...c, [date]: 'error' })))
|
|
2355
|
+
.finally(() => setBusyDate(null))
|
|
2356
|
+
}
|
|
2357
|
+
const sessionRows = sessions => sessions.map(session => el('tr', { key: session.id },
|
|
2358
|
+
sessionCell(session, state.config?.showSessionId === true, t),
|
|
2359
|
+
el('td', { className: 'num' }, String(session.calls)),
|
|
2360
|
+
el('td', { className: 'num' }, formatTokens(session.input)),
|
|
2361
|
+
el('td', { className: 'num' }, formatTokens(session.cacheRead + session.cacheWrite)),
|
|
2362
|
+
el('td', { className: 'num' }, formatTokens(session.output)),
|
|
2363
|
+
el('td', { className: 'num' }, formatMoneyUsd(session.cost, state.config))))
|
|
2288
2364
|
return el('div', { className: 'cm-scroll' },
|
|
2365
|
+
el('p', { className: 'cm-hint', style: { padding: '6px 10px 0' } }, t('historyExpandHint')),
|
|
2289
2366
|
el('table', { className: 'cm-table' },
|
|
2290
2367
|
el('thead', null, el('tr', null,
|
|
2291
2368
|
el('th', null, t('colDate')), el('th', { className: 'num' }, t('colCalls')),
|
|
2292
2369
|
el('th', { className: 'num' }, t('colInTok')), el('th', { className: 'num' }, t('colCacheTok')), el('th', { className: 'num' }, t('colOutTok')),
|
|
2293
2370
|
el('th', { className: 'num' }, t('colCost')))),
|
|
2294
|
-
el('tbody', null, rows.
|
|
2295
|
-
el('
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2371
|
+
el('tbody', null, rows.flatMap(day => {
|
|
2372
|
+
const base = el('tr', { key: day.date, className: 'cm-row-click', onClick: () => toggle(day.date) },
|
|
2373
|
+
el('td', null, (openDate === day.date ? '▾ ' : '▸ ') + day.date),
|
|
2374
|
+
el('td', { className: 'num' }, String(day.calls)),
|
|
2375
|
+
el('td', { className: 'num' }, formatTokens(day.input)),
|
|
2376
|
+
el('td', { className: 'num' }, formatTokens(day.cacheRead + day.cacheWrite)),
|
|
2377
|
+
el('td', { className: 'num' }, formatTokens(day.output)),
|
|
2378
|
+
el('td', { className: 'num' }, formatMoneyUsd(day.cost, state.config)))
|
|
2379
|
+
if (openDate !== day.date) return [base]
|
|
2380
|
+
const cached = cache[day.date]
|
|
2381
|
+
const detail = busyDate === day.date && cached === undefined
|
|
2382
|
+
? el('p', { className: 'cm-empty' }, t('historySessionsLoading'))
|
|
2383
|
+
: cached === 'error'
|
|
2384
|
+
? el('p', { className: 'cm-empty' }, t('historySessionsError'))
|
|
2385
|
+
: !Array.isArray(cached) || cached.length === 0
|
|
2386
|
+
? el('p', { className: 'cm-empty' }, t('historyNoSessions'))
|
|
2387
|
+
: el('table', { className: 'cm-table' },
|
|
2388
|
+
el('thead', null, el('tr', null,
|
|
2389
|
+
el('th', null, t('colSession')), el('th', { className: 'num' }, t('colCalls')),
|
|
2390
|
+
el('th', { className: 'num' }, t('colInTok')), el('th', { className: 'num' }, t('colCacheTok')), el('th', { className: 'num' }, t('colOutTok')),
|
|
2391
|
+
el('th', { className: 'num' }, t('colCost')))),
|
|
2392
|
+
el('tbody', null, sessionRows(cached)))
|
|
2393
|
+
return [base, el('tr', { key: day.date + '-sessions' }, el('td', { colSpan: 6 }, detail))]
|
|
2394
|
+
}))))
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// 按会话统计(issue #22 不分日期视角):全部历史会话按费用降序,默认收起、展开时按需拉取。
|
|
2398
|
+
function SessionRankPanel(props) {
|
|
2399
|
+
const { state, api } = props
|
|
2400
|
+
const t = makeT(resolveLocale(state.config?.locale))
|
|
2401
|
+
const [open, setOpen] = useState(false)
|
|
2402
|
+
const [limit, setLimit] = useState(100)
|
|
2403
|
+
const [rows, setRows] = useState(null)
|
|
2404
|
+
const [busy, setBusy] = useState(false)
|
|
2405
|
+
const [err, setErr] = useState(false)
|
|
2406
|
+
const load = n => {
|
|
2407
|
+
setBusy(true)
|
|
2408
|
+
setErr(false)
|
|
2409
|
+
api.getTopSessions(n)
|
|
2410
|
+
.then(res => setRows(Array.isArray(res?.sessions) ? res.sessions : []))
|
|
2411
|
+
.catch(() => setErr(true))
|
|
2412
|
+
.finally(() => setBusy(false))
|
|
2413
|
+
}
|
|
2414
|
+
const toggle = () => {
|
|
2415
|
+
setOpen(o => {
|
|
2416
|
+
const next = !o
|
|
2417
|
+
if (next && rows === null && !busy) load(limit)
|
|
2418
|
+
return next
|
|
2419
|
+
})
|
|
2420
|
+
}
|
|
2421
|
+
const changeLimit = n => {
|
|
2422
|
+
setLimit(n)
|
|
2423
|
+
if (open || rows !== null) load(n)
|
|
2424
|
+
}
|
|
2425
|
+
return el('div', { className: 'cm-budget' },
|
|
2426
|
+
el('div', { className: 'cm-budget-head' },
|
|
2427
|
+
el('button', { type: 'button', className: 'cm-collapse-h', 'aria-expanded': String(open), onClick: toggle },
|
|
2428
|
+
el('span', { className: 'cm-caret' + (open ? ' open' : '') }),
|
|
2429
|
+
el('h3', { className: 'cm-h' }, t('sessionRankTitle')))),
|
|
2430
|
+
open ? el('div', { className: 'cm-collapse-body' },
|
|
2431
|
+
el('p', { className: 'cm-hint' }, t('sessionRankHint')),
|
|
2432
|
+
el('div', { className: 'cm-field', style: { margin: '6px 0' } },
|
|
2433
|
+
el('label', null, t('sessionRankLimit')),
|
|
2434
|
+
el('select', { className: 'cm-input', value: String(limit), onChange: event => changeLimit(Number(event.target.value)) },
|
|
2435
|
+
el('option', { value: '50' }, '50'),
|
|
2436
|
+
el('option', { value: '100' }, '100'),
|
|
2437
|
+
el('option', { value: '200' }, '200'))),
|
|
2438
|
+
busy ? el('p', { className: 'cm-empty' }, t('sessionRankLoading'))
|
|
2439
|
+
: err ? el('p', { className: 'cm-empty' }, t('sessionRankError'))
|
|
2440
|
+
: rows === null || rows.length === 0 ? el('p', { className: 'cm-empty' }, t('sessionRankEmpty'))
|
|
2441
|
+
: el('div', { className: 'cm-scroll' },
|
|
2442
|
+
el('table', { className: 'cm-table' },
|
|
2443
|
+
el('thead', null, el('tr', null,
|
|
2444
|
+
el('th', null, t('colDate')), el('th', null, t('colSession')), el('th', { className: 'num' }, t('colCalls')),
|
|
2445
|
+
el('th', { className: 'num' }, t('colInTok')), el('th', { className: 'num' }, t('colCacheTok')), el('th', { className: 'num' }, t('colOutTok')),
|
|
2446
|
+
el('th', { className: 'num' }, t('colCost')))),
|
|
2447
|
+
el('tbody', null, rows.map(session => el('tr', { key: session.date + ':' + session.id },
|
|
2448
|
+
el('td', null, session.date),
|
|
2449
|
+
sessionCell(session, state.config?.showSessionId === true, t),
|
|
2450
|
+
el('td', { className: 'num' }, String(session.calls)),
|
|
2451
|
+
el('td', { className: 'num' }, formatTokens(session.input)),
|
|
2452
|
+
el('td', { className: 'num' }, formatTokens((session.cacheRead ?? 0) + (session.cacheWrite ?? 0))),
|
|
2453
|
+
el('td', { className: 'num' }, formatTokens(session.output)),
|
|
2454
|
+
el('td', { className: 'num' }, formatMoneyUsd(session.cost, state.config))))))))
|
|
2455
|
+
: null)
|
|
2301
2456
|
}
|
|
2302
2457
|
|
|
2303
2458
|
function TodaySessions(props) {
|
|
@@ -2305,6 +2460,7 @@ window.__ModuleLoader__.load({
|
|
|
2305
2460
|
const t = makeT(resolveLocale(state.config?.locale))
|
|
2306
2461
|
const sessions = state.today.sessions ?? []
|
|
2307
2462
|
if (sessions.length === 0) return el('p', { className: 'cm-empty' }, t('noSessionsToday'))
|
|
2463
|
+
const showId = state.config?.showSessionId === true
|
|
2308
2464
|
return el('div', { className: 'cm-scroll' },
|
|
2309
2465
|
el('table', { className: 'cm-table' },
|
|
2310
2466
|
el('thead', null, el('tr', null,
|
|
@@ -2312,7 +2468,7 @@ window.__ModuleLoader__.load({
|
|
|
2312
2468
|
el('th', { className: 'num' }, t('colInTok')), el('th', { className: 'num' }, t('colCacheTok')), el('th', { className: 'num' }, t('colOutTok')),
|
|
2313
2469
|
el('th', { className: 'num' }, t('colCost')))),
|
|
2314
2470
|
el('tbody', null, sessions.map(session => el('tr', { key: session.id },
|
|
2315
|
-
|
|
2471
|
+
sessionCell(session, showId, t),
|
|
2316
2472
|
el('td', { className: 'num' }, String(session.calls)),
|
|
2317
2473
|
el('td', { className: 'num' }, formatTokens(session.input)),
|
|
2318
2474
|
el('td', { className: 'num' }, formatTokens(session.cacheRead + session.cacheWrite)),
|
|
@@ -2320,6 +2476,17 @@ window.__ModuleLoader__.load({
|
|
|
2320
2476
|
el('td', { className: 'num' }, formatMoneyUsd(session.cost, state.config)))))))
|
|
2321
2477
|
}
|
|
2322
2478
|
|
|
2479
|
+
/** 会话单元格:标题为主行(未命名回落短 id),showId 时附显等宽短 id;悬停看完整标题与 id。 */
|
|
2480
|
+
function sessionCell(session, showId, t) {
|
|
2481
|
+
const rawTitle = typeof session.title === 'string' ? session.title.trim() : ''
|
|
2482
|
+
const shortId = String(session.id).slice(0, 14) + '…'
|
|
2483
|
+
const main = rawTitle.length > 0 ? rawTitle : shortId
|
|
2484
|
+
const tooltip = rawTitle.length > 0 ? rawTitle + ' · ' + session.id : session.id
|
|
2485
|
+
return el('td', { title: tooltip },
|
|
2486
|
+
el('div', { className: 'cm-sess-title' + (rawTitle.length === 0 ? ' cm-sess-id' : '') }, main),
|
|
2487
|
+
showId && rawTitle.length > 0 ? el('div', { className: 'cm-sess-id' }, shortId) : null)
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2323
2490
|
const CURRENCY_PRESETS = {
|
|
2324
2491
|
CNY: { symbol: '¥', decimals: 4, exchangeRate: 7.2 },
|
|
2325
2492
|
USD: { symbol: '$', decimals: 6, exchangeRate: 1 },
|
|
@@ -3589,7 +3756,9 @@ window.__ModuleLoader__.load({
|
|
|
3589
3756
|
// 历史
|
|
3590
3757
|
el('div', null,
|
|
3591
3758
|
el('h3', { className: 'cm-h' }, t('history')),
|
|
3592
|
-
el(HistoryTable, { state,
|
|
3759
|
+
el(HistoryTable, { state, api })),
|
|
3760
|
+
// 按会话统计(全部历史,不分日期;issue #22)
|
|
3761
|
+
el(SessionRankPanel, { state, api }),
|
|
3593
3762
|
// 显示设置
|
|
3594
3763
|
el('div', null,
|
|
3595
3764
|
el('h3', { className: 'cm-h' }, t('displaySettings')),
|
|
@@ -3629,6 +3798,13 @@ window.__ModuleLoader__.load({
|
|
|
3629
3798
|
},
|
|
3630
3799
|
el('option', { value: 'on' }, t('sidebarOn')),
|
|
3631
3800
|
el('option', { value: 'off' }, t('off')))),
|
|
3801
|
+
el('label', { className: 'cm-check' },
|
|
3802
|
+
el('input', {
|
|
3803
|
+
type: 'checkbox',
|
|
3804
|
+
checked: draft?.showSessionId === true,
|
|
3805
|
+
onChange: event => setField('showSessionId', event.target.checked),
|
|
3806
|
+
}),
|
|
3807
|
+
el('span', null, t('showSessionIdLabel'))),
|
|
3632
3808
|
el('div', { className: 'cm-grid-group' }, t('groupMoney')),
|
|
3633
3809
|
el('div', { className: 'cm-field' },
|
|
3634
3810
|
el('label', null, t('currencyLabel')),
|
|
@@ -4005,6 +4181,10 @@ window.__ModuleLoader__.load({
|
|
|
4005
4181
|
store.set({ status: 'ready', error: null, state })
|
|
4006
4182
|
return state
|
|
4007
4183
|
},
|
|
4184
|
+
// 按需拉取某天会话明细(issue #22),返回当日完整记录。
|
|
4185
|
+
getDaySessions: async date => call('getDaySessions', [date]),
|
|
4186
|
+
// 跨全部日期按费用降序的会话排行(issue #22 不分日期视角)。
|
|
4187
|
+
getTopSessions: async limit => call('getTopSessions', [limit]),
|
|
4008
4188
|
refreshBalance: async () => {
|
|
4009
4189
|
const result = await costMeter.refreshBalance()
|
|
4010
4190
|
if (result === null || typeof result !== 'object' || result.ok !== true) {
|
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import fs from 'node:fs'
|
|
|
18
18
|
import { join } from 'node:path'
|
|
19
19
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
20
20
|
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
21
|
-
import { Ledger, applyConfigPatch, localDayKey, reconcileBalanceDelta } from './store.js'
|
|
21
|
+
import { Ledger, applyConfigPatch, localDayKey, reconcileBalanceDelta, zeroDay } from './store.js'
|
|
22
22
|
import { backfillLegacyLedger } from './backfill.js'
|
|
23
23
|
import { OFFICIAL_PRICING_URL, normalizePrice, parsePricingHtml, costOf, priceEntryFor, providerPriceEntryFor, buildPriceCatalog } from './pricing.js'
|
|
24
24
|
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, emptyCustomBalance, queryCustomBalance } from './coding-plans.js'
|
|
@@ -898,6 +898,42 @@ function createService(ctx, ledger) {
|
|
|
898
898
|
ledger.scheduleWrite()
|
|
899
899
|
return build(false)
|
|
900
900
|
},
|
|
901
|
+
|
|
902
|
+
// 按需读取某一天的完整记录(含会话明细;issue #22):history() 输出为轻量副本
|
|
903
|
+
// 不含会话,历史各天的会话明细由本 RPC 展开时才拉取,避免 state 膨胀。
|
|
904
|
+
async getDaySessions(date) {
|
|
905
|
+
if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
906
|
+
throw new Error('invalid date')
|
|
907
|
+
}
|
|
908
|
+
const day = ledger.days[date]
|
|
909
|
+
return day === undefined ? zeroDay(date) : ledger.copyDay(day)
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
// 跨全部日期按费用降序返回前 N 个会话(issue #22 按会话视角,不分日期)。
|
|
913
|
+
async getTopSessions(limit) {
|
|
914
|
+
const n = Math.max(1, Math.min(500, Math.floor(Number(limit)) || 100))
|
|
915
|
+
const all = []
|
|
916
|
+
for (const [date, day] of Object.entries(ledger.days)) {
|
|
917
|
+
if (!Array.isArray(day.sessions)) continue
|
|
918
|
+
for (const s of day.sessions) {
|
|
919
|
+
if (s === null || typeof s !== 'object') continue
|
|
920
|
+
all.push({
|
|
921
|
+
date,
|
|
922
|
+
id: String(s.id ?? ''),
|
|
923
|
+
input: s.input ?? 0,
|
|
924
|
+
output: s.output ?? 0,
|
|
925
|
+
cacheRead: s.cacheRead ?? 0,
|
|
926
|
+
cacheWrite: s.cacheWrite ?? 0,
|
|
927
|
+
reasoning: s.reasoning ?? 0,
|
|
928
|
+
calls: s.calls ?? 0,
|
|
929
|
+
cost: s.cost ?? 0,
|
|
930
|
+
byProviderModel: s.byProviderModel ?? {},
|
|
931
|
+
})
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
all.sort((a, b) => b.cost - a.cost)
|
|
935
|
+
return { sessions: all.slice(0, n) }
|
|
936
|
+
},
|
|
901
937
|
}
|
|
902
938
|
Object.defineProperty(service, 'typertRemote', {
|
|
903
939
|
configurable: false,
|
|
@@ -925,8 +961,13 @@ export function apply(ctx) {
|
|
|
925
961
|
// 宿主会话日志填补空 byProviderModel(幂等,只填空条目,不重复计数)。
|
|
926
962
|
const backfillTimer = setTimeout(() => {
|
|
927
963
|
backfillLegacyLedger(ledger, join(resolveDshHome(), 'sessions')).then(filled => {
|
|
928
|
-
if (filled.days > 0 || filled.sessions > 0) {
|
|
929
|
-
|
|
964
|
+
if (filled.days > 0 || filled.sessions > 0 || filled.titles > 0) {
|
|
965
|
+
const extras = [
|
|
966
|
+
filled.days > 0 || filled.sessions > 0 ? `${filled.days} 天 / ${filled.sessions} 个会话` : null,
|
|
967
|
+
filled.titles > 0 ? `${filled.titles} 个会话标题` : null,
|
|
968
|
+
filled.recosted > 0 ? `重算 ${filled.recosted} 天金额` : null,
|
|
969
|
+
].filter(Boolean).join(',')
|
|
970
|
+
console.log(`[dsh-cost-meter] 历史按模型统计回填完成:${extras}(扫描 ${filled.scanned} 份会话日志)`)
|
|
930
971
|
}
|
|
931
972
|
}, error => {
|
|
932
973
|
console.warn(`[dsh-cost-meter] 历史按模型统计回填失败: ${String(error?.message ?? error)}`)
|
package/lib/store.js
CHANGED
|
@@ -37,6 +37,7 @@ export function defaultConfig() {
|
|
|
37
37
|
peakEffectiveAt: DEFAULT_PEAK_EFFECTIVE_AT,
|
|
38
38
|
peakWindows: DEFAULT_PEAK_WINDOWS.map(w => ({ ...w })),
|
|
39
39
|
peakNotice: true, // 峰时高价时段显著提示(侧边栏预算框/今日费用/设置页预算面板)
|
|
40
|
+
showSessionId: false, // 会话列表中附显会话 ID(默认只显示标题,需要时开启)
|
|
40
41
|
peakStyle: 'compact', // 峰谷时段条样式:compact(简洁单行/竖向同构) | classic(经典分段/胶囊芯片)
|
|
41
42
|
priceMatch: 'auto', // 未知模型名自动匹配价格表:auto(去后缀/前缀/家族相似) | exact(仅精确)
|
|
42
43
|
priceOverrides: {}, // 手动匹配覆盖:{ 'provider:modelId': '同provider模型 | provider:模型 | deepseek:__default__' }
|
|
@@ -125,7 +126,7 @@ export function localDayKey(ms) {
|
|
|
125
126
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
function zeroDay(date) {
|
|
129
|
+
export function zeroDay(date) {
|
|
129
130
|
return { date, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, calls: 0, cost: 0, byProviderModel: {}, sessions: [] }
|
|
130
131
|
}
|
|
131
132
|
|
|
@@ -153,7 +154,7 @@ function sanitizeBuckets(target) {
|
|
|
153
154
|
* 历史版本曾写入 `reasoning: null` 等非法数值,会导致 Typert strict 状态
|
|
154
155
|
* codec 拒绝整个 getState 结果(账本不可用 / 额度刷新连带失败)。
|
|
155
156
|
*/
|
|
156
|
-
function sanitizeDays(days) {
|
|
157
|
+
export function sanitizeDays(days) {
|
|
157
158
|
for (const day of Object.values(days)) {
|
|
158
159
|
if (sanitizeBuckets(day) === null) continue
|
|
159
160
|
day.byProviderModel = day.byProviderModel !== null && typeof day.byProviderModel === 'object' && !Array.isArray(day.byProviderModel)
|
|
@@ -169,6 +170,8 @@ function sanitizeDays(days) {
|
|
|
169
170
|
for (const session of day.sessions) {
|
|
170
171
|
if (sanitizeBuckets(session) === null) continue
|
|
171
172
|
session.id = typeof session.id === 'string' ? session.id : ''
|
|
173
|
+
// 会话标题(由历史回填从会话日志补齐):非字符串一律剔除,防击穿 strict sessionSchema。
|
|
174
|
+
if (typeof session.title !== 'string' || session.title.length === 0) delete session.title
|
|
172
175
|
session.byProviderModel = session.byProviderModel !== null && typeof session.byProviderModel === 'object' && !Array.isArray(session.byProviderModel)
|
|
173
176
|
? session.byProviderModel
|
|
174
177
|
: {}
|
|
@@ -211,6 +214,7 @@ const VALIDATION_MESSAGES = {
|
|
|
211
214
|
peakEffectiveAt: 'peakEffectiveAt 非法',
|
|
212
215
|
peakWindows: 'peakWindows 必须是数组',
|
|
213
216
|
peakNotice: 'peakNotice 必须是布尔值',
|
|
217
|
+
showSessionId: 'showSessionId 必须是布尔值',
|
|
214
218
|
peakStyle: 'peakStyle 必须是 compact / classic',
|
|
215
219
|
priceMatch: 'priceMatch 必须是 auto / exact',
|
|
216
220
|
priceOverrides: 'priceOverrides 必须是字符串→字符串映射',
|
|
@@ -271,6 +275,7 @@ const VALIDATION_MESSAGES = {
|
|
|
271
275
|
peakEffectiveAt: 'Invalid peakEffectiveAt',
|
|
272
276
|
peakWindows: 'peakWindows must be an array',
|
|
273
277
|
peakNotice: 'peakNotice must be a boolean',
|
|
278
|
+
showSessionId: 'showSessionId must be a boolean',
|
|
274
279
|
peakStyle: 'peakStyle must be compact / classic',
|
|
275
280
|
priceMatch: 'priceMatch must be auto / exact',
|
|
276
281
|
priceOverrides: 'priceOverrides must be a string→string map',
|
|
@@ -387,6 +392,7 @@ export function applyConfigPatch(current, patch) {
|
|
|
387
392
|
if (typeof candidate.peakEffectiveAt !== 'string') errors.push(vmsg(locale, 'peakEffectiveAt'))
|
|
388
393
|
if (!Array.isArray(candidate.peakWindows)) errors.push(vmsg(locale, 'peakWindows'))
|
|
389
394
|
if (typeof candidate.peakNotice !== 'boolean') errors.push(vmsg(locale, 'peakNotice'))
|
|
395
|
+
if (candidate.showSessionId !== undefined && typeof candidate.showSessionId !== 'boolean') errors.push(vmsg(locale, 'showSessionId'))
|
|
390
396
|
if (candidate.peakStyle !== 'compact' && candidate.peakStyle !== 'classic') errors.push(vmsg(locale, 'peakStyle'))
|
|
391
397
|
if (candidate.priceMatch !== 'auto' && candidate.priceMatch !== 'exact') errors.push(vmsg(locale, 'priceMatch'))
|
|
392
398
|
const overrides = candidate.priceOverrides
|
|
@@ -569,6 +575,7 @@ export function sanitizeConfig(raw) {
|
|
|
569
575
|
out.priceMatch = oneOf(out.priceMatch, ['auto', 'exact'], 'auto')
|
|
570
576
|
out.decimals = Math.max(0, Math.min(10, Math.floor(Number(out.decimals) || 0)))
|
|
571
577
|
out.historyDays = Math.max(7, Math.min(3650, Math.floor(Number(out.historyDays) || 180)))
|
|
578
|
+
out.showSessionId = out.showSessionId === true
|
|
572
579
|
if (!isNum(out.exchangeRate) || out.exchangeRate <= 0) out.exchangeRate = base.exchangeRate
|
|
573
580
|
if (!Array.isArray(out.peakWindows)) out.peakWindows = base.peakWindows
|
|
574
581
|
else out.peakWindows = out.peakWindows.filter(w => w !== null && typeof w === 'object' && isNum(Number(w.start)) && isNum(Number(w.end)))
|
package/lib/typert.host.js
CHANGED
|
@@ -10,6 +10,7 @@ const num = z.number()
|
|
|
10
10
|
|
|
11
11
|
const sessionSchema = z.object({
|
|
12
12
|
id: z.string(),
|
|
13
|
+
title: z.string().optional(),
|
|
13
14
|
input: num,
|
|
14
15
|
output: num,
|
|
15
16
|
cacheRead: num,
|
|
@@ -33,6 +34,13 @@ const daySchema = z.object({
|
|
|
33
34
|
sessions: z.array(sessionSchema),
|
|
34
35
|
})
|
|
35
36
|
|
|
37
|
+
// 带所属日期的会话条目(issue #22 按会话排行:不分日期视角)。
|
|
38
|
+
const datedSessionSchema = sessionSchema.extend({ date: z.string() })
|
|
39
|
+
|
|
40
|
+
const topSessionsSchema = z.object({
|
|
41
|
+
sessions: z.array(datedSessionSchema),
|
|
42
|
+
})
|
|
43
|
+
|
|
36
44
|
const priceTierSchema = z.object({
|
|
37
45
|
cacheHit: num,
|
|
38
46
|
cacheMiss: num,
|
|
@@ -94,6 +102,7 @@ const configSchema = z.object({
|
|
|
94
102
|
peakEffectiveAt: z.string(),
|
|
95
103
|
peakWindows: z.array(z.object({ start: num, end: num })),
|
|
96
104
|
peakNotice: z.boolean().optional(),
|
|
105
|
+
showSessionId: z.boolean().optional(),
|
|
97
106
|
peakStyle: z.enum(['compact', 'classic']).optional(),
|
|
98
107
|
priceMatch: z.enum(['auto', 'exact']).optional(),
|
|
99
108
|
priceOverrides: z.record(z.string(), z.string()).optional(),
|
|
@@ -241,6 +250,10 @@ const _state$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#CostState', s
|
|
|
241
250
|
const _patch$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#ConfigPatch', schema: patchSchema }
|
|
242
251
|
const _fetch$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#FetchPricesResult', schema: fetchPricesSchema }
|
|
243
252
|
const _provider$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#CodingPlanProvider', schema: z.string() }
|
|
253
|
+
const _day$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#DayRecord', schema: daySchema }
|
|
254
|
+
const _date$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#DayKey', schema: z.string() }
|
|
255
|
+
const _topSessions$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#TopSessions', schema: topSessionsSchema }
|
|
256
|
+
const _limit$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionLimit', schema: num }
|
|
244
257
|
|
|
245
258
|
export const TYPERT = {
|
|
246
259
|
package: 'dsh-cost-meter',
|
|
@@ -323,6 +336,28 @@ export const TYPERT = {
|
|
|
323
336
|
parameters: [],
|
|
324
337
|
result: _state$codec,
|
|
325
338
|
},
|
|
339
|
+
{
|
|
340
|
+
id: 'dsh-cost-meter#costMeter/getDaySessions',
|
|
341
|
+
service: 'costMeter',
|
|
342
|
+
namespace: 'costMeter',
|
|
343
|
+
method: 'getDaySessions',
|
|
344
|
+
invocation: { kind: 'direct' },
|
|
345
|
+
parameters: [
|
|
346
|
+
{ name: 'date', wire: 'date', source: 'json', codec: _date$codec },
|
|
347
|
+
],
|
|
348
|
+
result: _day$codec,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: 'dsh-cost-meter#costMeter/getTopSessions',
|
|
352
|
+
service: 'costMeter',
|
|
353
|
+
namespace: 'costMeter',
|
|
354
|
+
method: 'getTopSessions',
|
|
355
|
+
invocation: { kind: 'direct' },
|
|
356
|
+
parameters: [
|
|
357
|
+
{ name: 'limit', wire: 'limit', source: 'json', codec: _limit$codec },
|
|
358
|
+
],
|
|
359
|
+
result: _topSessions$codec,
|
|
360
|
+
},
|
|
326
361
|
],
|
|
327
362
|
model: {
|
|
328
363
|
services: [
|
|
@@ -390,6 +425,20 @@ export const TYPERT = {
|
|
|
390
425
|
summary: '清空全部历史记录。Clear all history records.',
|
|
391
426
|
jsDoc: '/**\n * 清空全部历史记录。\n * @returns 清空后的完整快照。\n * Clear all history records.\n * @returns The full snapshot after clearing.\n */',
|
|
392
427
|
},
|
|
428
|
+
{
|
|
429
|
+
kind: 'method',
|
|
430
|
+
name: 'getDaySessions',
|
|
431
|
+
signature: 'getDaySessions(date: string): DayRecord',
|
|
432
|
+
summary: '按需读取某一天的完整记录(含会话明细)。Read a day\'s full record on demand (with session details).',
|
|
433
|
+
jsDoc: '/**\n * 按需读取某一天的完整记录(含会话明细;历史记录列表为轻量副本不含会话)。\n * @param date - 本地日期键 YYYY-MM-DD。\n * @returns 该日完整记录;不存在时返回零值记录。\n * Read a day\'s full record on demand (with session details; the history list is a light copy without sessions).\n * @param date - Local day key YYYY-MM-DD.\n * @returns The full day record; a zero record when absent.\n */',
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
kind: 'method',
|
|
437
|
+
name: 'getTopSessions',
|
|
438
|
+
signature: 'getTopSessions(limit: number): TopSessions',
|
|
439
|
+
summary: '跨全部日期按费用降序返回前 N 个会话(不分日期视角)。Return the top N sessions by cost across all days.',
|
|
440
|
+
jsDoc: '/**\n * 跨全部日期按费用降序返回前 N 个会话(每条带所属日期;不含今日实时条目之外的去重)。\n * @param limit - 返回条数上限(服务端限制 1-500)。\n * @returns 会话列表(含 date 字段)。\n * Return the top N sessions by cost across all days (each tagged with its date).\n * @param limit - Max rows to return (server clamps to 1-500).\n * @returns Session list with a date field.\n */',
|
|
441
|
+
},
|
|
393
442
|
],
|
|
394
443
|
types: [],
|
|
395
444
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
4
4
|
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(6 家)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条,界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 6 vendors, custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, in a bilingual (Chinese/English) UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|