dsh-key-usage 0.1.1 → 0.1.3

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/sessions.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * 服务缺失或读取失败时降级为空结果 + coverage 说明,绝不影响台账相关的其它端点。
7
7
  */
8
8
  import { attributeModelKey, foldSessionEvents, summarizeSession, } from './session-usage.js';
9
+ import { normalizeSessionId } from './session-meta.js';
9
10
  /**
10
11
  * 一次最多折叠的会话数:按更新时间倒序取,避免首次请求扫全量历史。
11
12
  * 本机会话约 120 个 / 143 MB,取 200 覆盖全量;折叠结果按 revision 缓存,
@@ -192,11 +193,15 @@ function attributedModels(models, knownProviders) {
192
193
  };
193
194
  });
194
195
  }
195
- export function sessionRowsOf(records, knownProviders) {
196
+ export function sessionRowsOf(records, knownProviders, meta = {}) {
197
+ const titles = meta.titles ?? new Map();
198
+ const archived = meta.archived ?? new Set();
196
199
  return records
197
200
  .map((record) => ({
198
201
  id: record.id,
199
- title: record.title,
202
+ // 标题优先级:宿主投影缓存的标题 → 会话头自带字段 → 首条用户提问 → 界面回落成短 id
203
+ title: titles.get(normalizeSessionId(record.id)) ?? record.title ?? record.summary.firstPrompt ?? null,
204
+ archived: archived.has(normalizeSessionId(record.id)),
200
205
  workspace: record.workspace,
201
206
  provider: record.summary.primaryModelKey === null ? null : attributeModelKey(record.summary.primaryModelKey, knownProviders).provider,
202
207
  firstTime: record.summary.firstTime,
@@ -209,7 +214,53 @@ export function sessionRowsOf(records, knownProviders) {
209
214
  }))
210
215
  .sort((first, second) => (second.lastTime ?? 0) - (first.lastTime ?? 0));
211
216
  }
212
- export function projectRowsOf(records, knownProviders) {
217
+ export function modelRowsOf(records, knownProviders) {
218
+ const byModel = new Map();
219
+ for (const record of records) {
220
+ const sessionLastTime = record.summary.lastTime;
221
+ for (const model of attributedModels(record.summary.models, knownProviders)) {
222
+ const key = model.provider + '|' + model.model;
223
+ let accumulator = byModel.get(key);
224
+ if (accumulator === undefined) {
225
+ accumulator = {
226
+ provider: model.provider, model: model.model, calls: 0,
227
+ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, sessionCount: 0, lastTime: null,
228
+ };
229
+ byModel.set(key, accumulator);
230
+ }
231
+ accumulator.calls += model.calls;
232
+ accumulator.inputTokens += model.buckets.inputTokens;
233
+ accumulator.outputTokens += model.buckets.outputTokens;
234
+ accumulator.cacheReadTokens += model.buckets.cacheReadTokens;
235
+ accumulator.sessionCount += 1;
236
+ // 会话事件只有会话级时间戳,「最近活跃」只能取用到该模型的最晚会话时间,是近似值
237
+ if (sessionLastTime !== null && (accumulator.lastTime === null || sessionLastTime > accumulator.lastTime)) {
238
+ accumulator.lastTime = sessionLastTime;
239
+ }
240
+ }
241
+ }
242
+ return [...byModel.values()]
243
+ .map((accumulator) => {
244
+ const displayTokens = accumulator.inputTokens + accumulator.outputTokens + accumulator.cacheReadTokens;
245
+ return {
246
+ provider: accumulator.provider,
247
+ model: accumulator.model,
248
+ calls: accumulator.calls,
249
+ tokens: displayTokens,
250
+ cacheHitRate: displayTokens > 0 ? Math.round(accumulator.cacheReadTokens / displayTokens * 1000) / 10 : null,
251
+ sessionCount: accumulator.sessionCount,
252
+ lastTime: accumulator.lastTime,
253
+ buckets: {
254
+ inputTokens: accumulator.inputTokens,
255
+ outputTokens: accumulator.outputTokens,
256
+ cacheReadTokens: accumulator.cacheReadTokens,
257
+ },
258
+ };
259
+ })
260
+ .sort((first, second) => second.tokens - first.tokens);
261
+ }
262
+ export function projectRowsOf(records, knownProviders, meta = {}) {
263
+ const archived = meta.archived ?? new Set();
213
264
  const byWorkspace = new Map();
214
265
  for (const record of records) {
215
266
  const workspace = record.workspace ?? '未归属工作区';
@@ -217,7 +268,7 @@ export function projectRowsOf(records, knownProviders) {
217
268
  if (accumulator === undefined) {
218
269
  accumulator = {
219
270
  workspace, calls: 0, tokens: 0, cacheReadTokens: 0, inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0,
220
- sessionCount: 0, lastTime: null, providers: new Map(), models: new Map(),
271
+ sessionCount: 0, archivedSessionCount: 0, lastTime: null, providers: new Map(), models: new Map(),
221
272
  };
222
273
  byWorkspace.set(workspace, accumulator);
223
274
  }
@@ -228,6 +279,8 @@ export function projectRowsOf(records, knownProviders) {
228
279
  accumulator.cacheReadTokens += record.summary.buckets.cacheReadTokens;
229
280
  accumulator.cacheWriteTokens += record.summary.buckets.cacheWriteTokens;
230
281
  accumulator.sessionCount += 1;
282
+ if (archived.has(normalizeSessionId(record.id)))
283
+ accumulator.archivedSessionCount += 1;
231
284
  const lastTime = record.summary.lastTime;
232
285
  if (lastTime !== null && (accumulator.lastTime === null || lastTime > accumulator.lastTime))
233
286
  accumulator.lastTime = lastTime;
@@ -259,6 +312,7 @@ export function projectRowsOf(records, knownProviders) {
259
312
  tokens: accumulator.tokens,
260
313
  cacheHitRate: displayTokens > 0 ? Math.round(accumulator.cacheReadTokens / displayTokens * 1000) / 10 : null,
261
314
  sessionCount: accumulator.sessionCount,
315
+ archivedSessionCount: accumulator.archivedSessionCount,
262
316
  lastTime: accumulator.lastTime,
263
317
  providers: Object.fromEntries(accumulator.providers),
264
318
  models: [...accumulator.models.values()].sort((first, second) => second.tokens - first.tokens),
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "dsh-key-usage",
3
3
  "description": "DeepSeek Harness 侧边栏「使用情况」:按通道展示用量明细、活跃热力图、项目/会话用量与官方余额/配额(实时优先,失败回落快照)",
4
- "version": "0.1.1",
4
+ "version": "0.1.3",
5
5
  "license": "MIT",
6
+ "packageManager": "pnpm@10.32.1",
6
7
  "type": "module",
7
8
  "keywords": [
8
9
  "deepseek",
@@ -45,9 +46,14 @@
45
46
  "registry": "https://registry.npmjs.org/"
46
47
  },
47
48
  "scripts": {
49
+ "build": "tsc -p tsconfig.server.json && tsdown && node scripts/normalize-client-banner.mjs",
48
50
  "build:client": "tsdown && node scripts/normalize-client-banner.mjs",
49
- "check": "node --check lib/index.js && node --check lib/ledger.js && node --check lib/collect.js && node --check lib/routes.js && node --check lib/sessions.js && node --check lib/session-usage.js && node --check lib/merge.js && node --check lib/live-quota.js",
50
- "test": "node test/ledger.test.mjs && node test/collect.test.mjs && node test/channels.test.mjs && node test/session-usage.test.mjs && node test/sessions.test.mjs && node test/merge.test.mjs && node test/live-quota.test.mjs && node test/routes.test.mjs"
51
+ "build:server": "tsc -p tsconfig.server.json",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "check": "node --check lib/index.js && node --check lib/ledger.js && node --check lib/collect.js && node --check lib/routes.js && node --check lib/sessions.js && node --check lib/session-usage.js && node --check lib/session-meta.js && node --check lib/quota-snapshots.js && node --check lib/merge.js && node --check lib/live-quota.js",
54
+ "test": "node test/ledger.test.mjs && node test/collect.test.mjs && node test/channels.test.mjs && node test/session-usage.test.mjs && node test/session-meta.test.mjs && node test/sessions.test.mjs && node test/merge.test.mjs && node test/live-quota.test.mjs && node test/routes.test.mjs",
55
+ "verify": "npm run typecheck && npm run build && npm run check && npm run test",
56
+ "pack:dry": "npm pack --dry-run"
51
57
  },
52
58
  "peerDependencies": {
53
59
  "@deepseek-ai/cordis": "^4.0.1"