@dsh-agent-toolkit/token-usage 0.2.0 → 0.3.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/index.d.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/usage/index.d.ts
4
+ declare function setupUsage(ctx: Context, config: {
5
+ timezone: string;
6
+ }, owner: string): void;
7
+ //#endregion
3
8
  //#region src/index.d.ts
9
+ declare const name = "@dsh-agent-toolkit/token-usage";
10
+ declare const inject: string[];
11
+ /** 插件配置输出型。 */
4
12
  interface Config {
5
- /** 按日聚合的时区(IANA 名)。 */
6
13
  timezone: string;
7
14
  }
8
- declare const Config: z<Config>;
9
- declare const name = "token-usage";
10
- declare const inject: string[];
15
+ declare const Config: z<unknown, Config>;
11
16
  declare function apply(ctx: Context, config: Config): void;
12
17
  //#endregion
13
- export { Config, apply, inject, name };
18
+ export { Config, apply, inject, name, setupUsage };
14
19
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -1,7 +1,27 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { z as z$1 } from "zod";
3
3
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
4
- //#region src/aggregate.ts
4
+ //#region src/shared/storage.ts
5
+ function openDomainSafely(ctx, domain, warn, beforeClose) {
6
+ const ready = ctx.storageDomain.open(domain);
7
+ ready.catch((error) => {
8
+ warn(error instanceof Error ? error.message : String(error));
9
+ });
10
+ ctx.effect(() => async () => {
11
+ await Promise.resolve(beforeClose?.()).catch(() => void 0);
12
+ await ready.then((domain) => domain.close()).catch(() => void 0);
13
+ });
14
+ return ready;
15
+ }
16
+ //#endregion
17
+ //#region src/shared/webserver.ts
18
+ function registerOptionalRoutes(ctx, register) {
19
+ ctx.inject(["webServer"], (webCtx) => {
20
+ webCtx.effect(() => register(webCtx));
21
+ });
22
+ }
23
+ //#endregion
24
+ //#region src/usage/aggregate.ts
5
25
  /** 桶的计费总量(含估算)。 */
6
26
  function billedOf(b) {
7
27
  return b.input + b.output + b.cacheRead + b.cacheWrite + b.estimated;
@@ -157,8 +177,8 @@ function formatTokens(n) {
157
177
  return unit === 0 ? String(n) : `${value.toFixed(1)}${units[unit]}`;
158
178
  }
159
179
  //#endregion
160
- //#region src/heatmap.ts
161
- /** token-usage 纯函数:range 端点参数与摘要、13 周热力图网格、缓存/新增拆分。无运行时依赖,两半共用。 */
180
+ //#region src/usage/heatmap.ts
181
+ /** dsh-agent-toolkit 纯函数:range 端点参数与摘要、13 周热力图网格、缓存/新增拆分。无运行时依赖,两半共用。 */
162
182
  /** 解析 range 端点 days 参数:null → 默认 91;1..366 合法;其余返回 null(非法)。 */
163
183
  function parseDaysParam(raw) {
164
184
  if (raw === null) return 91;
@@ -183,7 +203,7 @@ function rangeSummaries(get, today, days) {
183
203
  });
184
204
  }
185
205
  //#endregion
186
- //#region src/render.ts
206
+ //#region src/usage/render.ts
187
207
  /** /token-usage 命令的文本视图(纯函数)。 */
188
208
  function line(name, b) {
189
209
  return ` ${name} ${formatTokens(billedOf(b))} ${b.calls} 次调用`;
@@ -205,8 +225,8 @@ function renderWeek(today, days) {
205
225
  return `${renderDay(days[0])}\n\n近 7 日:\n${lines.join("\n")}`;
206
226
  }
207
227
  //#endregion
208
- //#region src/store.ts
209
- /** token-usage 存储域声明:身份、版本、记录 zod schema 的单一来源。 */
228
+ //#region src/usage/store.ts
229
+ /** dsh-agent-toolkit 存储域声明:身份、版本、记录 zod schema 的单一来源。 */
210
230
  const BucketSchema = z$1.object({
211
231
  input: z$1.number().int().nonnegative(),
212
232
  output: z$1.number().int().nonnegative(),
@@ -230,117 +250,157 @@ const DailyRecordSchema = z$1.object({
230
250
  /** 压缩摘要调用单列;数值同时已并入 totals/hours。 */
231
251
  compaction: BucketSchema
232
252
  });
253
+ /** meta 表单例标记的值形状(meter_owner 等)。 */
254
+ const MetaRowSchema = z$1.object({ value: z$1.string() });
233
255
  /** domain 名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */
234
256
  const tokenUsageDomain = defineDomain({
235
257
  name: "token_usage",
236
258
  version: 1,
237
- tables: { daily: domainTable(DailyRecordSchema) }
259
+ tables: {
260
+ daily: domainTable(DailyRecordSchema),
261
+ meta: domainTable(MetaRowSchema)
262
+ }
238
263
  });
239
264
  //#endregion
240
- //#region src/index.ts
241
- const Config = z.object({ timezone: z.string().default(Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC") });
242
- const name = "token-usage";
243
- const inject = [
244
- "storageDomain",
245
- "tokenMeter",
246
- "commands"
247
- ];
265
+ //#region src/usage/index.ts
248
266
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
249
- function apply(ctx, config) {
267
+ const METER_OWNER_KEY = "meter_owner";
268
+ /** storage-domain 对已在进程内打开的域抛 DomainError(code='already-open'):双装时后到实例据此停用。 */
269
+ function isAlreadyOpen(error) {
270
+ return typeof error === "object" && error !== null && "code" in error && error.code === "already-open";
271
+ }
272
+ function setupUsage(ctx, config, owner) {
250
273
  let daily;
274
+ let meta;
251
275
  let tail = Promise.resolve();
252
- const domainReady = ctx.storageDomain.open(tokenUsageDomain).then((domain) => {
276
+ const domainReady = openDomainSafely(ctx, tokenUsageDomain, (msg) => ctx.logger.warn(msg), () => tail.then(async () => {
277
+ if (ownsMeter) await meta?.delete(METER_OWNER_KEY).catch(() => void 0);
278
+ }, () => void 0)).then((domain) => {
253
279
  daily = domain.table("daily");
280
+ meta = domain.table("meta");
254
281
  return domain;
255
282
  });
256
- domainReady.catch((error) => {
257
- ctx.logger.warn(`[token-usage] 存储域打开失败,token 统计不可用:${error instanceof Error ? error.message : String(error)}`);
258
- });
259
- ctx.on("session/event", (session, event) => {
260
- const sample = sampleFromEvent(session, event, config.timezone, (m) => ctx.tokenMeter.estimateMessage(m));
261
- if (sample === void 0) return;
262
- tail = tail.then(() => domainReady).then(() => {
263
- const table = daily;
264
- return table.put(sample.date, addSample(table.get(sample.date) ?? emptyDaily(sample.date), sample));
265
- }).then(() => void 0, () => void 0);
266
- });
267
- ctx.commands.register({
268
- name: "token-usage",
269
- description: "查看 token 用量(今日+近7日,或指定日期)",
270
- input: { hint: "YYYY-MM-DD,可空" },
271
- handler: async ({ rawInput }) => {
272
- const table = await domainReady.then(() => daily);
273
- const arg = rawInput.trim();
274
- dayParts(Date.now(), config.timezone).date;
275
- if (arg !== "" && !DATE_RE.test(arg)) return {
276
- kind: "error",
277
- text: "用法:/token-usage [YYYY-MM-DD]"
278
- };
279
- if (arg !== "") return {
280
- kind: "success",
281
- text: renderDay(table.get(arg) ?? emptyDaily(arg))
282
- };
283
- const days = Array.from({ length: 7 }, (_, i) => {
284
- const date = dayParts(Date.now() - i * 864e5, config.timezone).date;
285
- return table.get(date) ?? emptyDaily(date);
286
- });
287
- return {
288
- kind: "success",
289
- text: renderWeek(days[0].date, days)
290
- };
283
+ let ownsMeter = false;
284
+ const meteringReady = domainReady.then(async () => {
285
+ const existing = meta.get(METER_OWNER_KEY);
286
+ if (existing !== void 0) {
287
+ ctx.logger.warn(`token 计量已由 ${existing.value} 挂载,本实例(${owner})跳过计量采集`);
288
+ return false;
291
289
  }
290
+ ownsMeter = true;
291
+ await meta.put(METER_OWNER_KEY, { value: owner });
292
+ return true;
293
+ }, (error) => {
294
+ if (isAlreadyOpen(error)) ctx.logger.warn("token 用量域已被先到实例挂载(storage domain 的 token_usage 域名已打开,无法读取其 meter_owner),本实例停用用量功能:不采集、不注册 /token-usage 命令与路由");
295
+ return false;
292
296
  });
293
- ctx.inject(["webServer"], (webCtx) => {
294
- webCtx.effect(() => webCtx.webServer.register({
295
- kind: "exact",
296
- path: "/token-usage/api/daily",
297
- handler: async (req, res) => {
298
- if (req.method !== "GET") {
299
- res.writeHead(405).end();
300
- return;
301
- }
302
- const date = new URL(req.url ?? "", "http://127.0.0.1").searchParams.get("date");
303
- if (date !== null && !DATE_RE.test(date)) {
304
- res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "bad date, want YYYY-MM-DD" }));
305
- return;
306
- }
297
+ const openSucceeded = domainReady.then(() => true, () => false);
298
+ meteringReady.then((metering) => {
299
+ if (!metering) return;
300
+ ctx.on("session/event", (session, event) => {
301
+ const sample = sampleFromEvent(session, event, config.timezone, (m) => ctx.tokenMeter.estimateMessage(m));
302
+ if (sample === void 0) return;
303
+ tail = tail.then(() => domainReady).then(() => {
304
+ const table = daily;
305
+ return table.put(sample.date, addSample(table.get(sample.date) ?? emptyDaily(sample.date), sample));
306
+ }).then(() => void 0, () => void 0);
307
+ });
308
+ }, () => void 0);
309
+ openSucceeded.then((ok) => {
310
+ if (!ok) return;
311
+ ctx.commands.register({
312
+ name: "token-usage",
313
+ description: "查看 token 用量(今日+近7日,或指定日期)",
314
+ input: { hint: "YYYY-MM-DD,可空" },
315
+ handler: async ({ rawInput }) => {
307
316
  const table = await domainReady.then(() => daily);
308
- const today = dayParts(Date.now(), config.timezone).date;
309
- const key = date ?? today;
310
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
311
- today,
312
- record: table.get(key) ?? emptyDaily(key)
313
- }));
317
+ const arg = rawInput.trim();
318
+ dayParts(Date.now(), config.timezone).date;
319
+ if (arg !== "" && !DATE_RE.test(arg)) return {
320
+ kind: "error",
321
+ text: "用法:/token-usage [YYYY-MM-DD]"
322
+ };
323
+ if (arg !== "") return {
324
+ kind: "success",
325
+ text: renderDay(table.get(arg) ?? emptyDaily(arg))
326
+ };
327
+ const days = Array.from({ length: 7 }, (_, i) => {
328
+ const date = dayParts(Date.now() - i * 864e5, config.timezone).date;
329
+ return table.get(date) ?? emptyDaily(date);
330
+ });
331
+ return {
332
+ kind: "success",
333
+ text: renderWeek(days[0].date, days)
334
+ };
314
335
  }
315
- }), "token-usage: /token-usage/api/daily route");
316
- webCtx.effect(() => webCtx.webServer.register({
317
- kind: "exact",
318
- path: "/token-usage/api/range",
319
- handler: async (req, res) => {
320
- if (req.method !== "GET") {
321
- res.writeHead(405).end();
322
- return;
336
+ });
337
+ });
338
+ openSucceeded.then((ok) => {
339
+ if (!ok) return;
340
+ registerOptionalRoutes(ctx, (webCtx) => {
341
+ const disposeDaily = webCtx.webServer.register({
342
+ kind: "exact",
343
+ path: "/dsh-agent-toolkit/api/usage/daily",
344
+ handler: async (req, res) => {
345
+ if (req.method !== "GET") {
346
+ res.writeHead(405).end();
347
+ return;
348
+ }
349
+ const date = new URL(req.url ?? "", "http://127.0.0.1").searchParams.get("date");
350
+ if (date !== null && !DATE_RE.test(date)) {
351
+ res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "bad date, want YYYY-MM-DD" }));
352
+ return;
353
+ }
354
+ const table = await domainReady.then(() => daily);
355
+ const today = dayParts(Date.now(), config.timezone).date;
356
+ const key = date ?? today;
357
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
358
+ today,
359
+ record: table.get(key) ?? emptyDaily(key)
360
+ }));
323
361
  }
324
- const days = parseDaysParam(new URL(req.url ?? "", "http://127.0.0.1").searchParams.get("days"));
325
- if (days === null) {
326
- res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "bad days, want integer 1..366" }));
327
- return;
362
+ });
363
+ const disposeRange = webCtx.webServer.register({
364
+ kind: "exact",
365
+ path: "/dsh-agent-toolkit/api/usage/range",
366
+ handler: async (req, res) => {
367
+ if (req.method !== "GET") {
368
+ res.writeHead(405).end();
369
+ return;
370
+ }
371
+ const days = parseDaysParam(new URL(req.url ?? "", "http://127.0.0.1").searchParams.get("days"));
372
+ if (days === null) {
373
+ res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "bad days, want integer 1..366" }));
374
+ return;
375
+ }
376
+ const table = await domainReady.then(() => daily);
377
+ const today = dayParts(Date.now(), config.timezone).date;
378
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
379
+ today,
380
+ days: rangeSummaries((d) => table.get(d), today, days)
381
+ }));
328
382
  }
329
- const table = await domainReady.then(() => daily);
330
- const today = dayParts(Date.now(), config.timezone).date;
331
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
332
- today,
333
- days: rangeSummaries((d) => table.get(d), today, days)
334
- }));
335
- }
336
- }), "token-usage: /token-usage/api/range route");
337
- });
338
- ctx.effect(() => async () => {
339
- await tail.catch(() => void 0);
340
- await domainReady.then((domain) => domain.close());
383
+ });
384
+ return () => {
385
+ disposeDaily();
386
+ disposeRange();
387
+ };
388
+ });
341
389
  });
342
390
  }
343
391
  //#endregion
344
- export { Config, apply, inject, name };
392
+ //#region src/index.ts
393
+ const name = "@dsh-agent-toolkit/token-usage";
394
+ const inject = [
395
+ "storageDomain",
396
+ "tokenMeter",
397
+ "commands"
398
+ ];
399
+ const Config = z.object({ timezone: z.string().default("Asia/Shanghai") });
400
+ function apply(ctx, config) {
401
+ setupUsage(ctx, { timezone: config.timezone }, name);
402
+ }
403
+ //#endregion
404
+ export { Config, apply, inject, name, setupUsage };
345
405
 
346
406
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["z"],"sources":["../src/aggregate.ts","../src/heatmap.ts","../src/render.ts","../src/store.ts","../src/index.ts"],"sourcesContent":["/** token-usage 纯函数:日期换算、K/M/B 格式化、聚合。无运行时依赖,浏览器半可内联。 */\r\nimport type { Session, SessionEvent } from '@deepseek-ai/dsh-session'\r\nimport type { Message } from '@deepseek-ai/dsh-llm/types'\r\n// 激活 @deepseek-ai/dsh-compaction 对 SessionEventMap 的声明合并(compaction/summary 等事件类型)。\r\nimport type {} from '@deepseek-ai/dsh-compaction/types'\r\nimport type { Bucket, DailyRecord } from './store.ts'\r\n\r\n/** 一条待聚合样本;每个样本隐含 calls=1。 */\r\nexport interface UsageSample {\r\n date: string\r\n hour: number\r\n input: number\r\n output: number\r\n cacheRead: number\r\n cacheWrite: number\r\n /** 估算样本的计费 token 量;真实样本为 0。 */\r\n estimated: number\r\n estimatedCall: boolean\r\n /** 'provider/model';compaction 样本无。 */\r\n model?: string\r\n sessionId?: string\r\n cwd?: string\r\n compaction: boolean\r\n}\r\n\r\n/** 桶的计费总量(含估算)。 */\r\nexport function billedOf(b: Bucket): number {\r\n return b.input + b.output + b.cacheRead + b.cacheWrite + b.estimated\r\n}\r\n\r\nexport function emptyBucket(): Bucket {\r\n return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, calls: 0, estimated: 0 }\r\n}\r\n\r\nexport function emptyDaily(date: string): DailyRecord {\r\n return {\r\n date,\r\n totals: { ...emptyBucket(), estimatedCalls: 0 },\r\n hours: Array.from({ length: 24 }, emptyBucket),\r\n byModel: {},\r\n bySession: {},\r\n byProject: {},\r\n compaction: emptyBucket(),\r\n }\r\n}\r\n\r\nfunction addToBucket(b: Bucket, s: UsageSample): Bucket {\r\n return {\r\n input: b.input + s.input, output: b.output + s.output,\r\n cacheRead: b.cacheRead + s.cacheRead, cacheWrite: b.cacheWrite + s.cacheWrite,\r\n calls: b.calls + 1, estimated: b.estimated + s.estimated,\r\n }\r\n}\r\n\r\n/** 把一条样本并入日记录,返回新对象(存储记录禁止就地修改)。 */\r\nexport function addSample(rec: DailyRecord, s: UsageSample): DailyRecord {\r\n const totals = { ...addToBucket(rec.totals, s), estimatedCalls: rec.totals.estimatedCalls + (s.estimatedCall ? 1 : 0) }\r\n const hours = rec.hours.slice()\r\n hours[s.hour] = addToBucket(hours[s.hour], s)\r\n const byModel = { ...rec.byModel }\r\n if (s.model !== undefined) byModel[s.model] = addToBucket(byModel[s.model] ?? emptyBucket(), s)\r\n const bySession = { ...rec.bySession }\r\n if (s.sessionId !== undefined && s.cwd !== undefined) {\r\n bySession[s.sessionId] = { ...addToBucket(bySession[s.sessionId] ?? { ...emptyBucket(), cwd: s.cwd }, s), cwd: s.cwd }\r\n }\r\n const byProject = { ...rec.byProject }\r\n if (s.cwd !== undefined && !s.compaction) byProject[s.cwd] = addToBucket(byProject[s.cwd] ?? emptyBucket(), s)\r\n const compaction = s.compaction ? addToBucket(rec.compaction, s) : rec.compaction\r\n return { ...rec, totals, hours, byModel, bySession, byProject, compaction }\r\n}\r\n\r\n/** 从 session 事件提取样本;不相关事件与无 usage 的 compaction 返回 undefined。 */\r\nexport function sampleFromEvent(\r\n session: Session,\r\n event: SessionEvent,\r\n timeZone: string,\r\n estimate: (message: Message) => number,\r\n): UsageSample | undefined {\r\n const { date, hour } = dayParts(event.time, timeZone)\r\n if (event.type === 'assistant/message') {\r\n const { message, usage } = event.data\r\n const base = { date, hour, model: `${message.source.provider}/${message.source.model}`, sessionId: String(session.header.id), cwd: session.header.cwd, compaction: false }\r\n if (usage === undefined) {\r\n return { ...base, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, estimated: estimate(message), estimatedCall: true }\r\n }\r\n return {\r\n ...base, estimated: 0, estimatedCall: false,\r\n input: usage.inputTokens, output: usage.outputTokens,\r\n cacheRead: usage.cacheReadTokens ?? 0, cacheWrite: usage.cacheWriteTokens ?? 0,\r\n }\r\n }\r\n if (event.type === 'compaction/summary') {\r\n const { usage } = event.data\r\n if (usage === undefined) return undefined // 摘要块不是 Message,不做启发式估算\r\n return {\r\n date, hour, model: undefined, estimated: 0, estimatedCall: false, compaction: true,\r\n input: usage.inputTokens, output: usage.outputTokens,\r\n cacheRead: usage.cacheReadTokens ?? 0, cacheWrite: usage.cacheWriteTokens ?? 0,\r\n }\r\n }\r\n return undefined\r\n}\r\n\r\n/** 把 UTC 毫秒换算成指定时区的日期串与小时序号。 */\r\nexport function dayParts(time: number, timeZone: string): { date: string; hour: number } {\r\n const parts = new Intl.DateTimeFormat('en-CA', {\r\n timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hour12: false,\r\n }).formatToParts(time)\r\n const get = (type: string): string => parts.find((p) => p.type === type)!.value\r\n return {\r\n date: `${get('year')}-${get('month')}-${get('day')}`,\r\n hour: Number(get('hour')) % 24, // 部分 ICU 版本午夜给 24\r\n }\r\n}\r\n\r\n/** 日期串加减天数(锚 UTC 正午,避开 DST)。 */\r\nexport function shiftDate(date: string, days: number): string {\r\n return new Date(Date.parse(`${date}T12:00:00Z`) + days * 86_400_000).toISOString().slice(0, 10)\r\n}\r\n\r\n/** 计费 token 数自动换算 K/M/B(10 进制,1 位小数)。 */\r\nexport function formatTokens(n: number): string {\r\n const units = ['', 'K', 'M', 'B'] as const\r\n let value = n\r\n let unit = 0\r\n while (value >= 1000 && unit < units.length - 1) {\r\n value /= 1000\r\n unit += 1\r\n }\r\n return unit === 0 ? String(n) : `${value.toFixed(1)}${units[unit]}`\r\n}\r\n","/** token-usage 纯函数:range 端点参数与摘要、13 周热力图网格、缓存/新增拆分。无运行时依赖,两半共用。 */\r\nimport { billedOf, shiftDate } from './aggregate.ts'\r\nimport type { Bucket, DailyRecord } from './store.ts'\r\n\r\n/** range 端点与热力图共用的每日紧凑摘要。 */\r\nexport interface HeatmapDay { date: string; billed: number; calls: number }\r\n\r\nconst DAY_MS = 86_400_000\r\n\r\n/** 解析 range 端点 days 参数:null → 默认 91;1..366 合法;其余返回 null(非法)。 */\r\nexport function parseDaysParam(raw: string | null): number | null {\r\n if (raw === null) return 91\r\n if (!/^\\d+$/.test(raw)) return null\r\n const n = Number(raw)\r\n return n >= 1 && n <= 366 ? n : null\r\n}\r\n\r\n/** 以 today 为终点向前取 days 天的紧凑摘要(缺失日记 0),日期升序。 */\r\nexport function rangeSummaries(get: (date: string) => DailyRecord | undefined, today: string, days: number): HeatmapDay[] {\r\n return Array.from({ length: days }, (_, i) => {\r\n const date = shiftDate(today, i - (days - 1))\r\n const rec = get(date)\r\n return rec === undefined\r\n ? { date, billed: 0, calls: 0 }\r\n : { date, billed: billedOf(rec.totals), calls: rec.totals.calls }\r\n })\r\n}\r\n\r\nexport type HeatmapLevel = 0 | 1 | 2 | 3 | 4\r\n\r\n/** 分档:0 用量 → 0;非零按 max 线性 1..4 档(max <= 0 时兜底 1)。 */\r\nexport function levelOf(billed: number, max: number): HeatmapLevel {\r\n if (billed <= 0) return 0\r\n if (max <= 0) return 1\r\n return Math.min(4, Math.max(1, Math.ceil((billed / max) * 4))) as HeatmapLevel\r\n}\r\n\r\nexport interface HeatmapCell {\r\n date: string\r\n day: HeatmapDay | undefined\r\n level: HeatmapLevel\r\n /** today 之后的格子:禁用、不计档位。 */\r\n future: boolean\r\n}\r\n\r\n/** 13 列 × 每列 7 格(周日在上);末列为 today 所在周(周六结束)。 */\r\nexport function heatmapGrid(today: string, days: HeatmapDay[]): HeatmapCell[][] {\r\n const byDate = new Map(days.map((d) => [d.date, d]))\r\n const todayMs = Date.parse(`${today}T12:00:00Z`)\r\n const endMs = todayMs + (6 - new Date(todayMs).getUTCDay()) * DAY_MS\r\n const startMs = endMs - 90 * DAY_MS\r\n const max = Math.max(0, ...days.map((d) => d.billed))\r\n return Array.from({ length: 13 }, (_, c) =>\r\n Array.from({ length: 7 }, (_, r) => {\r\n const ms = startMs + (c * 7 + r) * DAY_MS\r\n const date = new Date(ms).toISOString().slice(0, 10)\r\n const day = byDate.get(date)\r\n const future = ms > todayMs\r\n return { date, day, level: future || day === undefined ? 0 : levelOf(day.billed, max), future }\r\n }))\r\n}\r\n\r\n/** 缓存/新增拆分:缓存 = cacheRead;新增 = input+output+cacheWrite+estimated。 */\r\nexport function cacheSplit(b: Bucket): { fresh: number; cached: number } {\r\n return { fresh: b.input + b.output + b.cacheWrite + b.estimated, cached: b.cacheRead }\r\n}\r\n\r\n/** 缓存命中率 = cacheRead/(input+cacheRead);分母为 0 返回 null。 */\r\nexport function cacheHitRate(totals: Bucket): number | null {\r\n const denom = totals.input + totals.cacheRead\r\n return denom === 0 ? null : totals.cacheRead / denom\r\n}\r\n","/** /token-usage 命令的文本视图(纯函数)。 */\r\nimport { billedOf, formatTokens } from './aggregate.ts'\r\nimport type { Bucket, DailyRecord } from './store.ts'\r\n\r\nfunction line(name: string, b: Bucket): string {\r\n return ` ${name} ${formatTokens(billedOf(b))} ${b.calls} 次调用`\r\n}\r\n\r\n/** 当日详情:总量(含估算标注)+ 模型/项目二维细分 + compaction 单列。 */\r\nexport function renderDay(rec: DailyRecord): string {\r\n const est = rec.totals.estimated > 0 ? `(含估算 ${formatTokens(rec.totals.estimated)})` : ''\r\n const rows: string[] = [\r\n `${rec.date} 用量:${formatTokens(billedOf(rec.totals))} ${rec.totals.calls} 次调用${est}`,\r\n ]\r\n const models = Object.entries(rec.byModel).sort((a, b) => billedOf(b[1]) - billedOf(a[1]))\r\n if (models.length > 0) rows.push('按模型:', ...models.map(([k, v]) => line(k, v)))\r\n const projects = Object.entries(rec.byProject).sort((a, b) => billedOf(b[1]) - billedOf(a[1]))\r\n if (projects.length > 0) rows.push('按项目:', ...projects.map(([k, v]) => line(k, v)))\r\n if (rec.compaction.calls > 0) rows.push(`上下文压缩:${formatTokens(billedOf(rec.compaction))} ${rec.compaction.calls} 次调用`)\r\n return rows.join('\\n')\r\n}\r\n\r\n/** 今日详情 + 近 7 日逐日摘要行(days[0] 为今日)。 */\r\nexport function renderWeek(today: string, days: readonly DailyRecord[]): string {\r\n const lines = days.map((d) => `${d.date} ${formatTokens(billedOf(d.totals))} ${d.totals.calls} 次调用`)\r\n return `${renderDay(days[0])}\\n\\n近 7 日:\\n${lines.join('\\n')}`\r\n}\r\n","/** token-usage 存储域声明:身份、版本、记录 zod schema 的单一来源。 */\r\nimport { z } from 'zod'\r\nimport { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'\r\n\r\nexport const BucketSchema = z.object({\r\n input: z.number().int().nonnegative(),\r\n output: z.number().int().nonnegative(),\r\n cacheRead: z.number().int().nonnegative(),\r\n cacheWrite: z.number().int().nonnegative(),\r\n calls: z.number().int().nonnegative(),\r\n /** 估算样本的计费 token 量(usage 缺失时经 tokenMeter 启发式得出)。 */\r\n estimated: z.number().int().nonnegative(),\r\n})\r\nexport type Bucket = z.infer<typeof BucketSchema>\r\n\r\nexport const DailyRecordSchema = z.object({\r\n date: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/),\r\n totals: BucketSchema.extend({ estimatedCalls: z.number().int().nonnegative() }),\r\n /** 24 小时桶,空小时为全零桶。 */\r\n hours: z.array(BucketSchema).length(24),\r\n /** key = 'provider/model'。 */\r\n byModel: z.record(z.string(), BucketSchema),\r\n /** key = sessionId。 */\r\n bySession: z.record(z.string(), BucketSchema.extend({ cwd: z.string() })),\r\n /** key = cwd(原样存储)。 */\r\n byProject: z.record(z.string(), BucketSchema),\r\n /** 压缩摘要调用单列;数值同时已并入 totals/hours。 */\r\n compaction: BucketSchema,\r\n})\r\nexport type DailyRecord = z.infer<typeof DailyRecordSchema>\r\n\r\n/** domain 名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */\r\nexport const tokenUsageDomain = defineDomain({\r\n name: 'token_usage',\r\n version: 1,\r\n tables: { daily: domainTable<string, DailyRecord>(DailyRecordSchema) },\r\n})\r\n","/** token-usage 插件 Node 半:采集、按日聚合持久化、/token-usage 命令、JSON 查询端点。 */\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\nimport type { KvTable } from '@deepseek-ai/dsh-storage-domain'\r\n// Type-only 激活对应包对 cordis Context 的声明合并(inject 的 service 属性)。\r\nimport type { WebServer } from '@deepseek-ai/dsh-host-webserver'\r\nimport type {} from '@deepseek-ai/dsh-token-meter'\r\nimport type {} from '@deepseek-ai/dsh-commands'\r\nimport { addSample, dayParts, emptyDaily, sampleFromEvent } from './aggregate.ts'\r\nimport { parseDaysParam, rangeSummaries } from './heatmap.ts'\r\nimport { renderDay, renderWeek } from './render.ts'\r\nimport { tokenUsageDomain, type DailyRecord } from './store.ts'\r\n\r\nexport interface Config {\r\n /** 按日聚合的时区(IANA 名)。 */\r\n timezone: string\r\n}\r\n\r\nexport const Config: z<Config> = z.object({\r\n timezone: z.string().default(Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC'),\r\n})\r\n\r\nexport const name = 'token-usage'\r\n\r\nexport const inject = ['storageDomain', 'tokenMeter', 'commands']\r\n\r\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/\r\n\r\nexport function apply(ctx: Context, config: Config): void {\r\n let daily: KvTable<string, DailyRecord> | undefined\r\n // 写串行化:session/event 监听可并发触发,所有聚合写排进同一条 tail 链,\r\n // 之后 get+put 才不会互相覆盖(KvTable 不串行化并发读改写)。\r\n let tail: Promise<unknown> = Promise.resolve()\r\n const domainReady = ctx.storageDomain.open(tokenUsageDomain).then((domain) => {\r\n daily = domain.table('daily')\r\n return domain\r\n })\r\n // open 失败(version-mismatch/malformed-medium/invalid-record)时创建即挂\r\n // rejection handler:避免 unhandled rejection 崩掉宿主进程(Node 默认 throw)。\r\n // domainReady 仍保持 reject,写链(tail 吞掉)/命令(宿主转 kind:'error')/\r\n // 端点(宿主 500)/卸载(cordis 记录)均感知失败而不次生崩溃。\r\n domainReady.catch((error) => {\r\n ctx.logger.warn(`[token-usage] 存储域打开失败,token 统计不可用:${error instanceof Error ? error.message : String(error)}`)\r\n })\r\n\r\n ctx.on('session/event', (session, event) => {\r\n const sample = sampleFromEvent(session, event, config.timezone, (m) => ctx.tokenMeter.estimateMessage(m))\r\n if (sample === undefined) return\r\n // 链到前一条写上,使读改写按序执行;单次失败被吞掉,不中断后续写(采集尽力而为)。\r\n const write = tail.then(() => domainReady).then(() => {\r\n const table = daily!\r\n return table.put(sample.date, addSample(table.get(sample.date) ?? emptyDaily(sample.date), sample))\r\n })\r\n tail = write.then(() => undefined, () => undefined)\r\n })\r\n\r\n ctx.commands.register({\r\n name: 'token-usage',\r\n description: '查看 token 用量(今日+近7日,或指定日期)',\r\n input: { hint: 'YYYY-MM-DD,可空' },\r\n handler: async ({ rawInput }) => {\r\n const table = await domainReady.then(() => daily!)\r\n const arg = rawInput.trim()\r\n const today = dayParts(Date.now(), config.timezone).date\r\n if (arg !== '' && !DATE_RE.test(arg)) {\r\n return { kind: 'error' as const, text: '用法:/token-usage [YYYY-MM-DD]' }\r\n }\r\n if (arg !== '') {\r\n return { kind: 'success' as const, text: renderDay(table.get(arg) ?? emptyDaily(arg)) }\r\n }\r\n const days = Array.from({ length: 7 }, (_, i) => {\r\n const date = dayParts(Date.now() - i * 86_400_000, config.timezone).date\r\n return table.get(date) ?? emptyDaily(date)\r\n })\r\n void today\r\n return { kind: 'success' as const, text: renderWeek(days[0].date, days) }\r\n },\r\n })\r\n\r\n // webServer 是可选能力(headless/CLI 无此服务),不进入顶层 inject。经 ctx.inject\r\n // 子 fiber 等待其挂载后再注册:子 fiber 未激活时惰性、随父 fiber 卸载而清理(宿主\r\n // 对可选服务的既有模式,见 client/connection 对可选 apiProxy 的处理);headless 下\r\n // webServer 永不出现,任何分支都不注册。注册走 ctx.effect 接线 disposer\r\n // (\"Registrations are effects\"),HMR 重挂不会因重复 exact 路径抛错。\r\n ctx.inject(['webServer'], (webCtx) => {\r\n webCtx.effect(() => webCtx.webServer.register({\r\n kind: 'exact',\r\n path: '/token-usage/api/daily',\r\n handler: async (req, res) => {\r\n if (req.method !== 'GET') {\r\n res.writeHead(405).end()\r\n return\r\n }\r\n const date = new URL(req.url ?? '', 'http://127.0.0.1').searchParams.get('date')\r\n if (date !== null && !DATE_RE.test(date)) {\r\n res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'bad date, want YYYY-MM-DD' }))\r\n return\r\n }\r\n const table = await domainReady.then(() => daily!)\r\n const today = dayParts(Date.now(), config.timezone).date\r\n const key = date ?? today\r\n res.writeHead(200, { 'content-type': 'application/json' })\r\n .end(JSON.stringify({ today, record: table.get(key) ?? emptyDaily(key) }))\r\n },\r\n }), 'token-usage: /token-usage/api/daily route')\r\n\r\n webCtx.effect(() => webCtx.webServer.register({\r\n kind: 'exact',\r\n path: '/token-usage/api/range',\r\n handler: async (req, res) => {\r\n if (req.method !== 'GET') {\r\n res.writeHead(405).end()\r\n return\r\n }\r\n const days = parseDaysParam(new URL(req.url ?? '', 'http://127.0.0.1').searchParams.get('days'))\r\n if (days === null) {\r\n res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'bad days, want integer 1..366' }))\r\n return\r\n }\r\n const table = await domainReady.then(() => daily!)\r\n const today = dayParts(Date.now(), config.timezone).date\r\n res.writeHead(200, { 'content-type': 'application/json' })\r\n .end(JSON.stringify({ today, days: rangeSummaries((d) => table.get(d), today, days) }))\r\n },\r\n }), 'token-usage: /token-usage/api/range route')\r\n })\r\n\r\n ctx.effect(() => async () => {\r\n await tail.catch(() => undefined) // 排空的写链落到后端后再关 domain\r\n await domainReady.then((domain) => domain.close())\r\n })\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["z"],"sources":["../src/shared/storage.ts","../src/shared/webserver.ts","../src/usage/aggregate.ts","../src/usage/heatmap.ts","../src/usage/render.ts","../src/usage/store.ts","../src/usage/index.ts","../src/index.ts"],"sourcesContent":["/** shared storage 助手:安全打开存储域(open 失败挂 rejection handler)并在卸载时关闭。 */\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport type { Domain, DomainSpec } from '@deepseek-ai/dsh-storage-domain'\r\n\r\n/** 类型擦除后的存储域句柄(跨 spec 泛型的统一承载面)。 */\r\nexport type DomainHandle = Domain<DomainSpec>\r\n\r\nexport function openDomainSafely(\r\n ctx: Context,\r\n domain: DomainSpec,\r\n warn: (msg: string) => void,\r\n beforeClose?: () => void | Promise<void>,\r\n): Promise<DomainHandle> {\r\n const ready = ctx.storageDomain.open(domain)\r\n // open 失败(version-mismatch/malformed-medium/invalid-record)时创建即挂\r\n // rejection handler:避免 unhandled rejection 崩掉宿主进程(Node 默认 throw)。\r\n // ready 仍保持 reject,调用方仍感知失败而不次生崩溃。\r\n ready.catch((error) => {\r\n warn(error instanceof Error ? error.message : String(error))\r\n })\r\n // 域句柄由本 fiber 持有:卸载时 close(消费方自己不关闭)。open 失败时\r\n // close 无从谈起,吞掉该 rejection,避免卸载路径次生崩溃。\r\n ctx.effect(() => async () => {\r\n // beforeClose 在 close 前排空消费方自己的写链(如 usage 的 tail):close 一旦\r\n // 开始(disposing=true)就拒绝新入队的写,未落队的采集会被静默丢弃。drain\r\n // 失败不阻断 close——卸载路径不因 drain 异常次生崩溃。\r\n await Promise.resolve(beforeClose?.()).catch(() => undefined)\r\n await ready.then((domain) => domain.close()).catch(() => undefined)\r\n })\r\n return ready\r\n}\r\n","/** shared webServer 助手:可选服务模式下注册路由(headless/CLI 无 webServer 时惰性、不注册、不抛错)。 */\r\nimport type { Context } from '@deepseek-ai/cordis'\r\n\r\nexport function registerOptionalRoutes(\r\n ctx: Context,\r\n register: (webCtx: Context) => () => void,\r\n): void {\r\n // 经 ctx.inject 子 fiber 等待 webServer 挂载后再注册:子 fiber 未激活时惰性、\r\n // 随父 fiber 卸载而清理;webServer 永不出现时任何分支都不注册。注册走\r\n // ctx.effect 接线 disposer(\"Registrations are effects\"),HMR 重挂不会因\r\n // 重复 exact 路径抛错。\r\n ctx.inject(['webServer'], (webCtx) => {\r\n webCtx.effect(() => register(webCtx))\r\n })\r\n}\r\n","/** dsh-agent-toolkit 纯函数:日期换算、K/M/B 格式化、聚合。无运行时依赖,浏览器半可内联。 */\r\nimport type { Session, SessionEvent } from '@deepseek-ai/dsh-session'\r\nimport type { Message } from '@deepseek-ai/dsh-llm/types'\r\n// 激活 @deepseek-ai/dsh-compaction 对 SessionEventMap 的声明合并(compaction/summary 等事件类型)。\r\nimport type {} from '@deepseek-ai/dsh-compaction/types'\r\nimport type { Bucket, DailyRecord } from './store.ts'\r\n\r\n/** 一条待聚合样本;每个样本隐含 calls=1。 */\r\nexport interface UsageSample {\r\n date: string\r\n hour: number\r\n input: number\r\n output: number\r\n cacheRead: number\r\n cacheWrite: number\r\n /** 估算样本的计费 token 量;真实样本为 0。 */\r\n estimated: number\r\n estimatedCall: boolean\r\n /** 'provider/model';compaction 样本无。 */\r\n model?: string\r\n sessionId?: string\r\n cwd?: string\r\n compaction: boolean\r\n}\r\n\r\n/** 桶的计费总量(含估算)。 */\r\nexport function billedOf(b: Bucket): number {\r\n return b.input + b.output + b.cacheRead + b.cacheWrite + b.estimated\r\n}\r\n\r\nexport function emptyBucket(): Bucket {\r\n return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, calls: 0, estimated: 0 }\r\n}\r\n\r\nexport function emptyDaily(date: string): DailyRecord {\r\n return {\r\n date,\r\n totals: { ...emptyBucket(), estimatedCalls: 0 },\r\n hours: Array.from({ length: 24 }, emptyBucket),\r\n byModel: {},\r\n bySession: {},\r\n byProject: {},\r\n compaction: emptyBucket(),\r\n }\r\n}\r\n\r\nfunction addToBucket(b: Bucket, s: UsageSample): Bucket {\r\n return {\r\n input: b.input + s.input, output: b.output + s.output,\r\n cacheRead: b.cacheRead + s.cacheRead, cacheWrite: b.cacheWrite + s.cacheWrite,\r\n calls: b.calls + 1, estimated: b.estimated + s.estimated,\r\n }\r\n}\r\n\r\n/** 把一条样本并入日记录,返回新对象(存储记录禁止就地修改)。 */\r\nexport function addSample(rec: DailyRecord, s: UsageSample): DailyRecord {\r\n const totals = { ...addToBucket(rec.totals, s), estimatedCalls: rec.totals.estimatedCalls + (s.estimatedCall ? 1 : 0) }\r\n const hours = rec.hours.slice()\r\n hours[s.hour] = addToBucket(hours[s.hour], s)\r\n const byModel = { ...rec.byModel }\r\n if (s.model !== undefined) byModel[s.model] = addToBucket(byModel[s.model] ?? emptyBucket(), s)\r\n const bySession = { ...rec.bySession }\r\n if (s.sessionId !== undefined && s.cwd !== undefined) {\r\n bySession[s.sessionId] = { ...addToBucket(bySession[s.sessionId] ?? { ...emptyBucket(), cwd: s.cwd }, s), cwd: s.cwd }\r\n }\r\n const byProject = { ...rec.byProject }\r\n if (s.cwd !== undefined && !s.compaction) byProject[s.cwd] = addToBucket(byProject[s.cwd] ?? emptyBucket(), s)\r\n const compaction = s.compaction ? addToBucket(rec.compaction, s) : rec.compaction\r\n return { ...rec, totals, hours, byModel, bySession, byProject, compaction }\r\n}\r\n\r\n/** 从 session 事件提取样本;不相关事件与无 usage 的 compaction 返回 undefined。 */\r\nexport function sampleFromEvent(\r\n session: Session,\r\n event: SessionEvent,\r\n timeZone: string,\r\n estimate: (message: Message) => number,\r\n): UsageSample | undefined {\r\n const { date, hour } = dayParts(event.time, timeZone)\r\n if (event.type === 'assistant/message') {\r\n const { message, usage } = event.data\r\n const base = { date, hour, model: `${message.source.provider}/${message.source.model}`, sessionId: String(session.header.id), cwd: session.header.cwd, compaction: false }\r\n if (usage === undefined) {\r\n return { ...base, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, estimated: estimate(message), estimatedCall: true }\r\n }\r\n return {\r\n ...base, estimated: 0, estimatedCall: false,\r\n input: usage.inputTokens, output: usage.outputTokens,\r\n cacheRead: usage.cacheReadTokens ?? 0, cacheWrite: usage.cacheWriteTokens ?? 0,\r\n }\r\n }\r\n if (event.type === 'compaction/summary') {\r\n const { usage } = event.data\r\n if (usage === undefined) return undefined // 摘要块不是 Message,不做启发式估算\r\n return {\r\n date, hour, model: undefined, estimated: 0, estimatedCall: false, compaction: true,\r\n input: usage.inputTokens, output: usage.outputTokens,\r\n cacheRead: usage.cacheReadTokens ?? 0, cacheWrite: usage.cacheWriteTokens ?? 0,\r\n }\r\n }\r\n return undefined\r\n}\r\n\r\n/** 把 UTC 毫秒换算成指定时区的日期串与小时序号。 */\r\nexport function dayParts(time: number, timeZone: string): { date: string; hour: number } {\r\n const parts = new Intl.DateTimeFormat('en-CA', {\r\n timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hour12: false,\r\n }).formatToParts(time)\r\n const get = (type: string): string => parts.find((p) => p.type === type)!.value\r\n return {\r\n date: `${get('year')}-${get('month')}-${get('day')}`,\r\n hour: Number(get('hour')) % 24, // 部分 ICU 版本午夜给 24\r\n }\r\n}\r\n\r\n/** 日期串加减天数(锚 UTC 正午,避开 DST)。 */\r\nexport function shiftDate(date: string, days: number): string {\r\n return new Date(Date.parse(`${date}T12:00:00Z`) + days * 86_400_000).toISOString().slice(0, 10)\r\n}\r\n\r\n/** 计费 token 数自动换算 K/M/B(10 进制,1 位小数)。 */\r\nexport function formatTokens(n: number): string {\r\n const units = ['', 'K', 'M', 'B'] as const\r\n let value = n\r\n let unit = 0\r\n while (value >= 1000 && unit < units.length - 1) {\r\n value /= 1000\r\n unit += 1\r\n }\r\n return unit === 0 ? String(n) : `${value.toFixed(1)}${units[unit]}`\r\n}\r\n","/** dsh-agent-toolkit 纯函数:range 端点参数与摘要、13 周热力图网格、缓存/新增拆分。无运行时依赖,两半共用。 */\r\nimport { billedOf, shiftDate } from './aggregate.ts'\r\nimport type { Bucket, DailyRecord } from './store.ts'\r\n\r\n/** range 端点与热力图共用的每日紧凑摘要。 */\r\nexport interface HeatmapDay { date: string; billed: number; calls: number }\r\n\r\nconst DAY_MS = 86_400_000\r\n\r\n/** 解析 range 端点 days 参数:null → 默认 91;1..366 合法;其余返回 null(非法)。 */\r\nexport function parseDaysParam(raw: string | null): number | null {\r\n if (raw === null) return 91\r\n if (!/^\\d+$/.test(raw)) return null\r\n const n = Number(raw)\r\n return n >= 1 && n <= 366 ? n : null\r\n}\r\n\r\n/** 以 today 为终点向前取 days 天的紧凑摘要(缺失日记 0),日期升序。 */\r\nexport function rangeSummaries(get: (date: string) => DailyRecord | undefined, today: string, days: number): HeatmapDay[] {\r\n return Array.from({ length: days }, (_, i) => {\r\n const date = shiftDate(today, i - (days - 1))\r\n const rec = get(date)\r\n return rec === undefined\r\n ? { date, billed: 0, calls: 0 }\r\n : { date, billed: billedOf(rec.totals), calls: rec.totals.calls }\r\n })\r\n}\r\n\r\nexport type HeatmapLevel = 0 | 1 | 2 | 3 | 4\r\n\r\n/** 分档:0 用量 → 0;非零按 max 线性 1..4 档(max <= 0 时兜底 1)。 */\r\nexport function levelOf(billed: number, max: number): HeatmapLevel {\r\n if (billed <= 0) return 0\r\n if (max <= 0) return 1\r\n return Math.min(4, Math.max(1, Math.ceil((billed / max) * 4))) as HeatmapLevel\r\n}\r\n\r\nexport interface HeatmapCell {\r\n date: string\r\n day: HeatmapDay | undefined\r\n level: HeatmapLevel\r\n /** today 之后的格子:禁用、不计档位。 */\r\n future: boolean\r\n}\r\n\r\n/** 13 列 × 每列 7 格(周日在上);末列为 today 所在周(周六结束)。 */\r\nexport function heatmapGrid(today: string, days: HeatmapDay[]): HeatmapCell[][] {\r\n const byDate = new Map(days.map((d) => [d.date, d]))\r\n const todayMs = Date.parse(`${today}T12:00:00Z`)\r\n const endMs = todayMs + (6 - new Date(todayMs).getUTCDay()) * DAY_MS\r\n const startMs = endMs - 90 * DAY_MS\r\n const max = Math.max(0, ...days.map((d) => d.billed))\r\n return Array.from({ length: 13 }, (_, c) =>\r\n Array.from({ length: 7 }, (_, r) => {\r\n const ms = startMs + (c * 7 + r) * DAY_MS\r\n const date = new Date(ms).toISOString().slice(0, 10)\r\n const day = byDate.get(date)\r\n const future = ms > todayMs\r\n return { date, day, level: future || day === undefined ? 0 : levelOf(day.billed, max), future }\r\n }))\r\n}\r\n\r\n/** 缓存/新增拆分:缓存 = cacheRead;新增 = input+output+cacheWrite+estimated。 */\r\nexport function cacheSplit(b: Bucket): { fresh: number; cached: number } {\r\n return { fresh: b.input + b.output + b.cacheWrite + b.estimated, cached: b.cacheRead }\r\n}\r\n\r\n/** 缓存命中率 = cacheRead/(input+cacheRead);分母为 0 返回 null。 */\r\nexport function cacheHitRate(totals: Bucket): number | null {\r\n const denom = totals.input + totals.cacheRead\r\n return denom === 0 ? null : totals.cacheRead / denom\r\n}\r\n","/** /token-usage 命令的文本视图(纯函数)。 */\r\nimport { billedOf, formatTokens } from './aggregate.ts'\r\nimport type { Bucket, DailyRecord } from './store.ts'\r\n\r\nfunction line(name: string, b: Bucket): string {\r\n return ` ${name} ${formatTokens(billedOf(b))} ${b.calls} 次调用`\r\n}\r\n\r\n/** 当日详情:总量(含估算标注)+ 模型/项目二维细分 + compaction 单列。 */\r\nexport function renderDay(rec: DailyRecord): string {\r\n const est = rec.totals.estimated > 0 ? `(含估算 ${formatTokens(rec.totals.estimated)})` : ''\r\n const rows: string[] = [\r\n `${rec.date} 用量:${formatTokens(billedOf(rec.totals))} ${rec.totals.calls} 次调用${est}`,\r\n ]\r\n const models = Object.entries(rec.byModel).sort((a, b) => billedOf(b[1]) - billedOf(a[1]))\r\n if (models.length > 0) rows.push('按模型:', ...models.map(([k, v]) => line(k, v)))\r\n const projects = Object.entries(rec.byProject).sort((a, b) => billedOf(b[1]) - billedOf(a[1]))\r\n if (projects.length > 0) rows.push('按项目:', ...projects.map(([k, v]) => line(k, v)))\r\n if (rec.compaction.calls > 0) rows.push(`上下文压缩:${formatTokens(billedOf(rec.compaction))} ${rec.compaction.calls} 次调用`)\r\n return rows.join('\\n')\r\n}\r\n\r\n/** 今日详情 + 近 7 日逐日摘要行(days[0] 为今日)。 */\r\nexport function renderWeek(today: string, days: readonly DailyRecord[]): string {\r\n const lines = days.map((d) => `${d.date} ${formatTokens(billedOf(d.totals))} ${d.totals.calls} 次调用`)\r\n return `${renderDay(days[0])}\\n\\n近 7 日:\\n${lines.join('\\n')}`\r\n}\r\n","/** dsh-agent-toolkit 存储域声明:身份、版本、记录 zod schema 的单一来源。 */\r\nimport { z } from 'zod'\r\nimport { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'\r\n\r\nexport const BucketSchema = z.object({\r\n input: z.number().int().nonnegative(),\r\n output: z.number().int().nonnegative(),\r\n cacheRead: z.number().int().nonnegative(),\r\n cacheWrite: z.number().int().nonnegative(),\r\n calls: z.number().int().nonnegative(),\r\n /** 估算样本的计费 token 量(usage 缺失时经 tokenMeter 启发式得出)。 */\r\n estimated: z.number().int().nonnegative(),\r\n})\r\nexport type Bucket = z.infer<typeof BucketSchema>\r\n\r\nexport const DailyRecordSchema = z.object({\r\n date: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/),\r\n totals: BucketSchema.extend({ estimatedCalls: z.number().int().nonnegative() }),\r\n /** 24 小时桶,空小时为全零桶。 */\r\n hours: z.array(BucketSchema).length(24),\r\n /** key = 'provider/model'。 */\r\n byModel: z.record(z.string(), BucketSchema),\r\n /** key = sessionId。 */\r\n bySession: z.record(z.string(), BucketSchema.extend({ cwd: z.string() })),\r\n /** key = cwd(原样存储)。 */\r\n byProject: z.record(z.string(), BucketSchema),\r\n /** 压缩摘要调用单列;数值同时已并入 totals/hours。 */\r\n compaction: BucketSchema,\r\n})\r\nexport type DailyRecord = z.infer<typeof DailyRecordSchema>\r\n\r\n/** meta 表单例标记的值形状(meter_owner 等)。 */\r\nexport const MetaRowSchema = z.object({ value: z.string() })\r\n\r\n/** domain 名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */\r\nexport const tokenUsageDomain = defineDomain({\r\n name: 'token_usage',\r\n // version 保持 1:加载侧缺表即空表(snapshot.tables[table] ?? {}),加 meta 表无需 bump。\r\n version: 1,\r\n tables: {\r\n daily: domainTable<string, DailyRecord>(DailyRecordSchema),\r\n meta: domainTable<string, { value: string }>(MetaRowSchema),\r\n },\r\n})\r\n","/** usage 模块:per-day token 用量采集、/token-usage 命令与 /dsh-agent-toolkit/api/usage JSON 路由。 */\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport type { KvTable } from '@deepseek-ai/dsh-storage-domain'\r\n// Type-only 激活对应包对 cordis Context 的声明合并(inject 的 service 属性)。\r\nimport type { WebServer } from '@deepseek-ai/dsh-host-webserver'\r\nimport type {} from '@deepseek-ai/dsh-token-meter'\r\nimport type {} from '@deepseek-ai/dsh-commands'\r\nimport { openDomainSafely } from '../shared/storage.ts'\r\nimport { registerOptionalRoutes } from '../shared/webserver.ts'\r\nimport { addSample, dayParts, emptyDaily, sampleFromEvent } from './aggregate.ts'\r\nimport { parseDaysParam, rangeSummaries } from './heatmap.ts'\r\nimport { renderDay, renderWeek } from './render.ts'\r\nimport { tokenUsageDomain, type DailyRecord } from './store.ts'\r\n\r\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/\r\n\r\nconst METER_OWNER_KEY = 'meter_owner'\r\n\r\n/** storage-domain 对已在进程内打开的域抛 DomainError(code='already-open'):双装时后到实例据此停用。 */\r\nfunction isAlreadyOpen(error: unknown): boolean {\r\n return typeof error === 'object' && error !== null\r\n && 'code' in error\r\n && (error as { code?: unknown }).code === 'already-open'\r\n}\r\n\r\nexport function setupUsage(ctx: Context, config: { timezone: string }, owner: string): void {\r\n let daily: KvTable<string, DailyRecord> | undefined\r\n let meta: KvTable<string, { value: string }> | undefined\r\n // 写串行化:session/event 监听可并发触发,所有聚合写排进同一条 tail 链,\r\n // 之后 get+put 才不会互相覆盖(KvTable 不串行化并发读改写)。\r\n let tail: Promise<unknown> = Promise.resolve()\r\n // open 失败(version-mismatch/malformed-medium/invalid-record)时创建即挂\r\n // rejection handler:避免 unhandled rejection 崩掉宿主进程(Node 默认 throw)。\r\n // domainReady 仍保持 reject,命令/路由注册收窄到 open 成功后(见 openSucceeded),\r\n // 写链(tail 吞掉)/卸载(cordis 记录)感知失败而不次生崩溃。\r\n const domainReady = openDomainSafely(\r\n ctx,\r\n tokenUsageDomain,\r\n (msg) => ctx.logger.warn(msg),\r\n // 卸载前排空本模块写链:close 一旦开始(disposing=true)就拒绝新入队的写,\r\n // 未落队的采集会被静默丢弃——drain 先于 close 是归档的保证。\r\n () => tail.then(async () => {\r\n // 释放计量所有权(仅占位方);失败不阻断 close。\r\n if (ownsMeter) await meta?.delete(METER_OWNER_KEY).catch(() => undefined)\r\n }, () => undefined),\r\n ).then((domain) => {\r\n // openDomainSafely 的句柄是类型擦除的 DomainHandle,table 值类型回落 unknown,此处窄化回 DailyRecord。\r\n daily = domain.table('daily') as KvTable<string, DailyRecord>\r\n meta = domain.table('meta') as KvTable<string, { value: string }>\r\n return domain\r\n })\r\n\r\n // 双装守卫与降级:token 用量功能先到先得。真实宿主里 storage-domain 的 open 对已在\r\n // 进程内打开的域名抛 code='already-open'(域名互斥),后到实例在拿到域句柄前就被挡下\r\n // ——此时读不到 meter_owner,只能按 already-open 直接停用;meter_owner meta 守卫保留作\r\n // 跨场景防御(共享介质/历史版本等),占位方卸载时释放。\r\n let ownsMeter = false\r\n const meteringReady = domainReady.then(\r\n async () => {\r\n const existing = meta!.get(METER_OWNER_KEY)\r\n if (existing !== undefined) {\r\n // 已被他包占用:本实例跳过计量(命令/路由仍在,读同一份数据)。\r\n ctx.logger.warn(`token 计量已由 ${existing.value} 挂载,本实例(${owner})跳过计量采集`)\r\n return false\r\n }\r\n // 占位意图先置位再落盘:即便 put 在途时就卸载,beforeClose 也会释放。真实 KvTable 的\r\n // put/delete 走同一串行链(host.enqueue),delete 排在 put 之后会观测到落盘再删除。\r\n ownsMeter = true\r\n await meta!.put(METER_OWNER_KEY, { value: owner })\r\n return true\r\n },\r\n // open 失败分支:already-open 即双装,后到实例自动停用——不采集、不注册命令/路由;\r\n // 其余 open 失败维持现状(openDomainSafely 已 warn,这里静默收尾,不次生未处理拒绝)。\r\n (error) => {\r\n if (isAlreadyOpen(error)) {\r\n ctx.logger.warn('token 用量域已被先到实例挂载(storage domain 的 token_usage 域名已打开,无法读取其 meter_owner),本实例停用用量功能:不采集、不注册 /token-usage 命令与路由')\r\n }\r\n return false\r\n },\r\n )\r\n\r\n // 命令/路由注册收窄到 open 成功后:已停用的后到实例绝不注册重复命令 / 重复 exact 路径\r\n //(webServer 重复 exact 路径会抛错)。registerOptionalRoutes 的 ctx.inject 惰性语义不变:\r\n // webServer 挂载后才注册、随父 fiber 清理。\r\n const openSucceeded = domainReady.then(() => true, () => false)\r\n\r\n // 守卫失败(已有他包采集)时不挂监听,tail 恒为空链;open 失败(domainReady reject)\r\n // 时这里拒绝吞掉,不次生 unhandled rejection 崩掉宿主(本模块开头的既定不变式)。\r\n void meteringReady.then((metering) => {\r\n if (!metering) return\r\n ctx.on('session/event', (session, event) => {\r\n const sample = sampleFromEvent(session, event, config.timezone, (m) => ctx.tokenMeter.estimateMessage(m))\r\n if (sample === undefined) return\r\n // 链到前一条写上,使读改写按序执行;单次失败被吞掉,不中断后续写(采集尽力而为)。\r\n const write = tail.then(() => domainReady).then(() => {\r\n const table = daily!\r\n return table.put(sample.date, addSample(table.get(sample.date) ?? emptyDaily(sample.date), sample))\r\n })\r\n tail = write.then(() => undefined, () => undefined)\r\n })\r\n }, () => undefined)\r\n\r\n void openSucceeded.then((ok) => {\r\n if (!ok) return\r\n ctx.commands.register({\r\n name: 'token-usage',\r\n description: '查看 token 用量(今日+近7日,或指定日期)',\r\n input: { hint: 'YYYY-MM-DD,可空' },\r\n handler: async ({ rawInput }) => {\r\n const table = await domainReady.then(() => daily!)\r\n const arg = rawInput.trim()\r\n const today = dayParts(Date.now(), config.timezone).date\r\n if (arg !== '' && !DATE_RE.test(arg)) {\r\n return { kind: 'error' as const, text: '用法:/token-usage [YYYY-MM-DD]' }\r\n }\r\n if (arg !== '') {\r\n return { kind: 'success' as const, text: renderDay(table.get(arg) ?? emptyDaily(arg)) }\r\n }\r\n const days = Array.from({ length: 7 }, (_, i) => {\r\n const date = dayParts(Date.now() - i * 86_400_000, config.timezone).date\r\n return table.get(date) ?? emptyDaily(date)\r\n })\r\n void today\r\n return { kind: 'success' as const, text: renderWeek(days[0].date, days) }\r\n },\r\n })\r\n })\r\n\r\n // webServer 是可选能力(headless/CLI 无此服务),不进入顶层 inject。经\r\n // registerOptionalRoutes 子 fiber 等待其挂载后再注册:子 fiber 未激活时惰性、\r\n // 随父 fiber 卸载而清理;headless 下 webServer 永不出现,任何分支都不注册。\r\n // 注册走 ctx.effect 接线 disposer(\"Registrations are effects\"),HMR 重挂不会\r\n // 因重复 exact 路径抛错。整段只在 open 成功后执行(已停用的后到实例不注册路由)。\r\n void openSucceeded.then((ok) => {\r\n if (!ok) return\r\n registerOptionalRoutes(ctx, (webCtx) => {\r\n const disposeDaily = webCtx.webServer.register({\r\n kind: 'exact',\r\n path: '/dsh-agent-toolkit/api/usage/daily',\r\n handler: async (req, res) => {\r\n if (req.method !== 'GET') {\r\n res.writeHead(405).end()\r\n return\r\n }\r\n const date = new URL(req.url ?? '', 'http://127.0.0.1').searchParams.get('date')\r\n if (date !== null && !DATE_RE.test(date)) {\r\n res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'bad date, want YYYY-MM-DD' }))\r\n return\r\n }\r\n const table = await domainReady.then(() => daily!)\r\n const today = dayParts(Date.now(), config.timezone).date\r\n const key = date ?? today\r\n res.writeHead(200, { 'content-type': 'application/json' })\r\n .end(JSON.stringify({ today, record: table.get(key) ?? emptyDaily(key) }))\r\n },\r\n })\r\n\r\n const disposeRange = webCtx.webServer.register({\r\n kind: 'exact',\r\n path: '/dsh-agent-toolkit/api/usage/range',\r\n handler: async (req, res) => {\r\n if (req.method !== 'GET') {\r\n res.writeHead(405).end()\r\n return\r\n }\r\n const days = parseDaysParam(new URL(req.url ?? '', 'http://127.0.0.1').searchParams.get('days'))\r\n if (days === null) {\r\n res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'bad days, want integer 1..366' }))\r\n return\r\n }\r\n const table = await domainReady.then(() => daily!)\r\n const today = dayParts(Date.now(), config.timezone).date\r\n res.writeHead(200, { 'content-type': 'application/json' })\r\n .end(JSON.stringify({ today, days: rangeSummaries((d) => table.get(d), today, days) }))\r\n },\r\n })\r\n\r\n return () => {\r\n disposeDaily()\r\n disposeRange()\r\n }\r\n })\r\n })\r\n}\r\n","/** @dsh-agent-toolkit/token-usage 插件入口:per-day token 用量统计(/token-usage 命令 + JSON API + 侧边栏面板)。 */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only 激活对应包对 cordis Context 的声明合并(inject 的 service 属性)。\nimport type {} from '@deepseek-ai/dsh-token-meter'\nimport type {} from '@deepseek-ai/dsh-commands'\nimport z from '@deepseek-ai/schemastery'\nimport { setupUsage } from './usage/index.ts'\n\nexport const name = '@dsh-agent-toolkit/token-usage'\n\n// 硬依赖服务全集。webServer 为可选服务(经 registerOptionalRoutes 惰性等待),不进 inject。\nexport const inject = ['storageDomain', 'tokenMeter', 'commands']\n\n/** 插件配置输出型。 */\nexport interface Config {\n timezone: string\n}\n\nexport const Config: z<unknown, Config> = z.object({\n timezone: z.string().default('Asia/Shanghai'),\n}) as z<unknown, Config>\n\nexport function apply(ctx: Context, config: Config): void {\n setupUsage(ctx, { timezone: config.timezone }, name)\n}\n\n// 供 dsh-agent-toolkit 函数级转发复用(见设计 spec「toolkit 集成方式」)。\nexport { setupUsage } from './usage/index.ts'\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-agent-toolkit/token-usage",
3
- "version": "0.2.0",
4
- "description": "DeepSeek Harness plugin: per-day token usage statistics — /token-usage command, JSON endpoint, sidebar UI",
3
+ "version": "0.3.0",
4
+ "description": "DeepSeek Harness plugin: per-day token usage statistics — 13-week heatmap, stacked daily chart, /token-usage command",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "deepseek-harness",
@@ -9,6 +9,13 @@
9
9
  "dsh-plugin",
10
10
  "token-usage"
11
11
  ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/EsonXie/dsh-agent-toolkit.git",
15
+ "directory": "packages/usage"
16
+ },
17
+ "homepage": "https://github.com/EsonXie/dsh-agent-toolkit#readme",
18
+ "bugs": "https://github.com/EsonXie/dsh-agent-toolkit/issues",
12
19
  "type": "module",
13
20
  "main": "./lib/index.js",
14
21
  "types": "./lib/index.d.ts",
@@ -18,6 +25,10 @@
18
25
  "default": "./lib/index.js"
19
26
  },
20
27
  "./client": "./lib/client.js",
28
+ "./client-module": {
29
+ "types": "./lib/client-module.d.ts",
30
+ "default": "./lib/client-module.js"
31
+ },
21
32
  "./package.json": "./package.json"
22
33
  },
23
34
  "files": [
@@ -37,6 +48,7 @@
37
48
  "platform": "web",
38
49
  "inject": [
39
50
  "@deepseek-ai/dsh-client-runtime",
51
+ "@deepseek-ai/dsh-client-locale",
40
52
  "@deepseek-ai/dsh-client-ui-sidebar"
41
53
  ]
42
54
  }
@@ -52,6 +64,7 @@
52
64
  },
53
65
  "devDependencies": {
54
66
  "@deepseek-ai/cordis": "link:../../deepseek-harness/vendor/cordis",
67
+ "@deepseek-ai/dsh-client-locale": "link:../../deepseek-harness/packages/client/locale",
55
68
  "@deepseek-ai/dsh-client-runtime": "link:../../deepseek-harness/packages/client/runtime",
56
69
  "@deepseek-ai/dsh-client-ui-primitives": "link:../../deepseek-harness/packages/client/ui-primitives",
57
70
  "@deepseek-ai/dsh-client-ui-sidebar": "link:../../deepseek-harness/packages/client/ui-sidebar",
@@ -63,6 +76,7 @@
63
76
  "@deepseek-ai/dsh-session": "link:../../deepseek-harness/packages/core/session",
64
77
  "@deepseek-ai/dsh-storage-domain": "link:../../deepseek-harness/packages/storage/storage-domain",
65
78
  "@deepseek-ai/dsh-token-meter": "link:../../deepseek-harness/packages/llm/token-meter",
79
+ "@deepseek-ai/schemastery": "link:../../deepseek-harness/vendor/schemastery",
66
80
  "@testing-library/react": "^16.1.0",
67
81
  "@types/node": "^22.20.1",
68
82
  "@types/react": "~18.3.1",