@dsh-agent-toolkit/token-usage 0.1.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/LICENSE +21 -0
- package/README.md +30 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +286 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +295 -0
- package/lib/index.js.map +1 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-agent-toolkit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @dsh-agent-toolkit/token-usage
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness(dsh)插件:按日统计 token 用量。
|
|
4
|
+
|
|
5
|
+
- `/token-usage [YYYY-MM-DD]` 斜杠命令:今日 + 近 7 日汇总,或指定日期
|
|
6
|
+
- `GET /token-usage/api/daily?date=YYYY-MM-DD` JSON 端点(web 模式)
|
|
7
|
+
- Web UI 侧边栏用量面板(点击打开明细弹窗)
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
dsh plugin --profile web add @dsh-agent-toolkit/token-usage
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 配置
|
|
16
|
+
|
|
17
|
+
| 字段 | 类型 | 默认 | 说明 |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `timezone` | string(IANA 名) | 系统时区 | 按日聚合的分日时区 |
|
|
20
|
+
|
|
21
|
+
在 profile 的 `cordis.patch.yml` 中整行覆盖:
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
- id: token-usage
|
|
25
|
+
name: '@dsh-agent-toolkit/token-usage'
|
|
26
|
+
config:
|
|
27
|
+
timezone: Asia/Shanghai
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
依赖 dsh 提供 `storageDomain`、`tokenMeter`、`commands` 服务(`@deepseek-ai/dsh-base` 均含);`webServer` 为可选注入,headless/CLI 下不注册 HTTP 端点。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# token-usage 组合包层:profile 的 dsh.profile.bundles 列出本包时应用。
|
|
2
|
+
# 行 name 用包名(非路径),Node 模块解析以 profile 目录为锚点找到已安装代码。
|
|
3
|
+
# config 不写:timezone 由 Config schema 默认(系统时区),用户可在自己 profile 的
|
|
4
|
+
# cordis.patch.yml 中整行覆盖(patch 是整行替换,不深度合并)。
|
|
5
|
+
- insert:
|
|
6
|
+
- id: token-usage
|
|
7
|
+
name: '@dsh-agent-toolkit/token-usage'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@dsh-agent-toolkit/token-usage",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
11
|
+
function r(e) {
|
|
12
|
+
var t, f, n = "";
|
|
13
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
14
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
15
|
+
var o = e.length;
|
|
16
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
17
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
function clsx() {
|
|
21
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/aggregate.ts
|
|
26
|
+
/** 桶的计费总量(含估算)。 */
|
|
27
|
+
function billedOf(b) {
|
|
28
|
+
return b.input + b.output + b.cacheRead + b.cacheWrite + b.estimated;
|
|
29
|
+
}
|
|
30
|
+
/** 日期串加减天数(锚 UTC 正午,避开 DST)。 */
|
|
31
|
+
function shiftDate(date, days) {
|
|
32
|
+
return new Date(Date.parse(`${date}T12:00:00Z`) + days * 864e5).toISOString().slice(0, 10);
|
|
33
|
+
}
|
|
34
|
+
/** 计费 token 数自动换算 K/M/B(10 进制,1 位小数)。 */
|
|
35
|
+
function formatTokens(n) {
|
|
36
|
+
const units = [
|
|
37
|
+
"",
|
|
38
|
+
"K",
|
|
39
|
+
"M",
|
|
40
|
+
"B"
|
|
41
|
+
];
|
|
42
|
+
let value = n;
|
|
43
|
+
let unit = 0;
|
|
44
|
+
while (value >= 1e3 && unit < units.length - 1) {
|
|
45
|
+
value /= 1e3;
|
|
46
|
+
unit += 1;
|
|
47
|
+
}
|
|
48
|
+
return unit === 0 ? String(n) : `${value.toFixed(1)}${units[unit]}`;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region \0dsh-css:D:\work\github\dsh\dsh-agent-toolkit\packages\token-usage\src\client\UsageModal.module.css.mjs
|
|
52
|
+
const css$1 = ".DGSElG_dialog.DGSElG_dialog{width:min(720px,100%)}.DGSElG_pager{justify-content:center;align-items:center;gap:8px;display:flex}.DGSElG_pagerButton{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:8px;justify-content:center;align-items:center;font-family:inherit;font-size:14px;line-height:20px;display:inline-flex}.DGSElG_pagerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.DGSElG_pagerButton:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.DGSElG_dateLabel{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}.DGSElG_chart{align-items:flex-end;gap:2px;height:120px;margin-top:12px;display:flex}.DGSElG_barSlot{border-radius:1px;flex:1;align-items:flex-end;height:100%;display:flex}.DGSElG_barSlot:hover{background:var(--dsw-alias-interactive-bg-hover)}.DGSElG_bar{background:var(--dsw-alias-state-business-primary,currentColor);border-radius:1px;width:100%;min-height:1px}.DGSElG_hourLabels{color:var(--dsw-alias-label-tertiary);justify-content:space-between;font-size:11px;line-height:16px;display:flex}.DGSElG_total{color:var(--dsw-alias-label-primary);margin-top:12px;font-size:14px;font-weight:600;line-height:22px}.DGSElG_sectionTitle{color:var(--dsw-alias-label-secondary);margin:12px 0 4px;font-size:12px;line-height:18px}.DGSElG_row{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:12px;font-size:12px;line-height:18px;display:flex}.DGSElG_rowName{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.DGSElG_rowCalls{color:var(--dsw-alias-label-tertiary)}.DGSElG_compaction{color:var(--dsw-alias-label-secondary);margin-top:8px;font-size:12px;line-height:18px}";
|
|
53
|
+
const tagId$1 = "@dsh-agent-toolkit/token-usage/UsageModal.module.css";
|
|
54
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
55
|
+
const tag = document.createElement("style");
|
|
56
|
+
tag.dataset.plugin = "@dsh-agent-toolkit/token-usage";
|
|
57
|
+
tag.dataset.pluginCss = tagId$1;
|
|
58
|
+
tag.textContent = css$1;
|
|
59
|
+
document.head.appendChild(tag);
|
|
60
|
+
}
|
|
61
|
+
var UsageModal_module_css_default = {
|
|
62
|
+
"dialog": "DGSElG_dialog",
|
|
63
|
+
"bar": "DGSElG_bar",
|
|
64
|
+
"pagerButton": "DGSElG_pagerButton",
|
|
65
|
+
"row": "DGSElG_row",
|
|
66
|
+
"dateLabel": "DGSElG_dateLabel",
|
|
67
|
+
"chart": "DGSElG_chart",
|
|
68
|
+
"rowName": "DGSElG_rowName",
|
|
69
|
+
"total": "DGSElG_total",
|
|
70
|
+
"sectionTitle": "DGSElG_sectionTitle",
|
|
71
|
+
"rowCalls": "DGSElG_rowCalls",
|
|
72
|
+
"compaction": "DGSElG_compaction",
|
|
73
|
+
"barSlot": "DGSElG_barSlot",
|
|
74
|
+
"pager": "DGSElG_pager",
|
|
75
|
+
"hourLabels": "DGSElG_hourLabels"
|
|
76
|
+
};
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/client/UsageModal.tsx
|
|
79
|
+
/** Token 用量模态框:翻页头 + 24 小时柱状图 + 总量 + 模型/项目二维细分。 */
|
|
80
|
+
async function fetchDay(date) {
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(date === null ? "/token-usage/api/daily" : `/token-usage/api/daily?date=${date}`);
|
|
83
|
+
if (!res.ok) return { status: "error" };
|
|
84
|
+
const body = await res.json();
|
|
85
|
+
return {
|
|
86
|
+
status: "ok",
|
|
87
|
+
today: body.today,
|
|
88
|
+
record: body.record
|
|
89
|
+
};
|
|
90
|
+
} catch {
|
|
91
|
+
return { status: "error" };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function Breakdown({ title, rows }) {
|
|
95
|
+
if (rows.length === 0) return null;
|
|
96
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
97
|
+
className: UsageModal_module_css_default.sectionTitle,
|
|
98
|
+
children: title
|
|
99
|
+
}), rows.map(([name, b]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
100
|
+
className: UsageModal_module_css_default.row,
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
103
|
+
className: UsageModal_module_css_default.rowName,
|
|
104
|
+
children: name
|
|
105
|
+
}),
|
|
106
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTokens(billedOf(b)) }),
|
|
107
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
108
|
+
className: UsageModal_module_css_default.rowCalls,
|
|
109
|
+
children: [b.calls, " 次"]
|
|
110
|
+
})
|
|
111
|
+
]
|
|
112
|
+
}, name))] });
|
|
113
|
+
}
|
|
114
|
+
function UsageModal({ open, onClose, initialDate }) {
|
|
115
|
+
const [date, setDate] = (0, react.useState)(initialDate);
|
|
116
|
+
const [state, setState] = (0, react.useState)({ status: "loading" });
|
|
117
|
+
(0, react.useEffect)(() => {
|
|
118
|
+
if (open) setDate(initialDate);
|
|
119
|
+
}, [open, initialDate]);
|
|
120
|
+
(0, react.useEffect)(() => {
|
|
121
|
+
if (!open) return;
|
|
122
|
+
let stale = false;
|
|
123
|
+
setState({ status: "loading" });
|
|
124
|
+
fetchDay(date).then((s) => {
|
|
125
|
+
if (!stale) setState(s);
|
|
126
|
+
});
|
|
127
|
+
return () => {
|
|
128
|
+
stale = true;
|
|
129
|
+
};
|
|
130
|
+
}, [open, date]);
|
|
131
|
+
const record = state.status === "ok" ? state.record : void 0;
|
|
132
|
+
const today = state.status === "ok" ? state.today : void 0;
|
|
133
|
+
const current = record?.date ?? initialDate ?? "";
|
|
134
|
+
const peak = record === void 0 ? 1 : Math.max(1, ...record.hours.map(billedOf));
|
|
135
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
136
|
+
open,
|
|
137
|
+
onClose,
|
|
138
|
+
title: "Token 用量",
|
|
139
|
+
closeLabel: "关闭",
|
|
140
|
+
className: UsageModal_module_css_default.dialog,
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
143
|
+
className: UsageModal_module_css_default.pager,
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
146
|
+
type: "button",
|
|
147
|
+
className: UsageModal_module_css_default.pagerButton,
|
|
148
|
+
"aria-label": "前一天",
|
|
149
|
+
disabled: current === "",
|
|
150
|
+
onClick: () => setDate(shiftDate(current, -1)),
|
|
151
|
+
children: "←"
|
|
152
|
+
}),
|
|
153
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
154
|
+
className: UsageModal_module_css_default.dateLabel,
|
|
155
|
+
children: current
|
|
156
|
+
}),
|
|
157
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
158
|
+
type: "button",
|
|
159
|
+
className: UsageModal_module_css_default.pagerButton,
|
|
160
|
+
"aria-label": "后一天",
|
|
161
|
+
disabled: today === void 0 || shiftDate(current, 1) > today,
|
|
162
|
+
onClick: () => setDate(shiftDate(current, 1)),
|
|
163
|
+
children: "→"
|
|
164
|
+
})
|
|
165
|
+
]
|
|
166
|
+
}),
|
|
167
|
+
state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "加载中…" }),
|
|
168
|
+
state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "加载失败,请重试" }),
|
|
169
|
+
record !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
171
|
+
className: UsageModal_module_css_default.chart,
|
|
172
|
+
children: record.hours.map((b, h) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
173
|
+
className: UsageModal_module_css_default.barSlot,
|
|
174
|
+
title: `${h}:00 ${formatTokens(billedOf(b))} ${b.calls} 次`,
|
|
175
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
176
|
+
className: UsageModal_module_css_default.bar,
|
|
177
|
+
style: { height: `${billedOf(b) / peak * 100}%` }
|
|
178
|
+
})
|
|
179
|
+
}, h))
|
|
180
|
+
}),
|
|
181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
182
|
+
className: UsageModal_module_css_default.hourLabels,
|
|
183
|
+
children: [
|
|
184
|
+
0,
|
|
185
|
+
6,
|
|
186
|
+
12,
|
|
187
|
+
18
|
|
188
|
+
].map((h) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [h, ":00"] }, h))
|
|
189
|
+
}),
|
|
190
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
191
|
+
className: UsageModal_module_css_default.total,
|
|
192
|
+
children: [
|
|
193
|
+
"当日总量 ",
|
|
194
|
+
formatTokens(billedOf(record.totals)),
|
|
195
|
+
" · ",
|
|
196
|
+
record.totals.calls,
|
|
197
|
+
" 次调用",
|
|
198
|
+
record.totals.estimated > 0 && `(含估算 ${formatTokens(record.totals.estimated)})`,
|
|
199
|
+
record.totals.calls === 0 && " · 当日无用量"
|
|
200
|
+
]
|
|
201
|
+
}),
|
|
202
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Breakdown, {
|
|
203
|
+
title: "按模型",
|
|
204
|
+
rows: Object.entries(record.byModel).sort((a, b) => billedOf(b[1]) - billedOf(a[1]))
|
|
205
|
+
}),
|
|
206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Breakdown, {
|
|
207
|
+
title: "按项目",
|
|
208
|
+
rows: Object.entries(record.byProject).sort((a, b) => billedOf(b[1]) - billedOf(a[1]))
|
|
209
|
+
}),
|
|
210
|
+
record.compaction.calls > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
211
|
+
className: UsageModal_module_css_default.compaction,
|
|
212
|
+
children: [
|
|
213
|
+
"上下文压缩 ",
|
|
214
|
+
formatTokens(billedOf(record.compaction)),
|
|
215
|
+
" · ",
|
|
216
|
+
record.compaction.calls,
|
|
217
|
+
" 次"
|
|
218
|
+
]
|
|
219
|
+
})
|
|
220
|
+
] })
|
|
221
|
+
]
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region \0dsh-css:D:\work\github\dsh\dsh-agent-toolkit\packages\token-usage\src\client\UsageEntry.module.css.mjs
|
|
226
|
+
const css = "._0uegfW_trigger{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:12px;flex:none;align-items:center;gap:8px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}._0uegfW_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}._0uegfW_trigger._0uegfW_rail{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;margin:8px 0 10px;padding:0}._0uegfW_triggerLabel{white-space:nowrap;overflow:hidden}";
|
|
227
|
+
const tagId = "@dsh-agent-toolkit/token-usage/UsageEntry.module.css";
|
|
228
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
229
|
+
const tag = document.createElement("style");
|
|
230
|
+
tag.dataset.plugin = "@dsh-agent-toolkit/token-usage";
|
|
231
|
+
tag.dataset.pluginCss = tagId;
|
|
232
|
+
tag.textContent = css;
|
|
233
|
+
document.head.appendChild(tag);
|
|
234
|
+
}
|
|
235
|
+
var UsageEntry_module_css_default = {
|
|
236
|
+
"triggerLabel": "_0uegfW_triggerLabel",
|
|
237
|
+
"trigger": "_0uegfW_trigger",
|
|
238
|
+
"rail": "_0uegfW_rail"
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/client/UsageEntry.tsx
|
|
242
|
+
/** Sidebar footer entry: wide icon+label row vs rail icon-only; opens the usage modal. */
|
|
243
|
+
function UsageEntry({ wide }) {
|
|
244
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
245
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
246
|
+
label: "Token 用量",
|
|
247
|
+
delayMs: 500,
|
|
248
|
+
disabled: wide,
|
|
249
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
250
|
+
type: "button",
|
|
251
|
+
className: clsx(UsageEntry_module_css_default.trigger, !wide && UsageEntry_module_css_default.rail),
|
|
252
|
+
"aria-label": "Token 用量",
|
|
253
|
+
onClick: () => {
|
|
254
|
+
setOpen(true);
|
|
255
|
+
},
|
|
256
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutline16, { size: wide ? 16 : 18 }), wide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
257
|
+
className: UsageEntry_module_css_default.triggerLabel,
|
|
258
|
+
children: "Token 用量"
|
|
259
|
+
})]
|
|
260
|
+
})
|
|
261
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageModal, {
|
|
262
|
+
open,
|
|
263
|
+
onClose: () => {
|
|
264
|
+
setOpen(false);
|
|
265
|
+
},
|
|
266
|
+
initialDate: null
|
|
267
|
+
})] });
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/client/index.ts
|
|
271
|
+
const inject = ["slots"];
|
|
272
|
+
function apply(ctx) {
|
|
273
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
274
|
+
name: "sidebar.footer.action",
|
|
275
|
+
id: "token-usage",
|
|
276
|
+
order: 0
|
|
277
|
+
}, UsageEntry));
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
exports.apply = apply;
|
|
281
|
+
exports.inject = inject;
|
|
282
|
+
return module.exports;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["css","useState","Modal","useState","Tooltip","css","IconDataOutline16"],"sources":["../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs","../src/aggregate.ts","../src/client/UsageModal.tsx","../src/client/UsageEntry.tsx","../src/client/index.ts"],"sourcesContent":["function r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}export function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}export default clsx;","/** 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 用量模态框:翻页头 + 24 小时柱状图 + 总量 + 模型/项目二维细分。 */\r\nimport { useEffect, useState } from 'react'\r\nimport { Modal } from '@deepseek-ai/dsh-client-ui-primitives'\r\nimport { billedOf, formatTokens, shiftDate } from '../aggregate.ts'\r\nimport type { Bucket, DailyRecord } from '../store.ts'\r\nimport css from './UsageModal.module.css'\r\n\r\nexport interface UsageModalProps {\r\n open: boolean\r\n onClose: () => void\r\n /** 初始日期 YYYY-MM-DD;null = 今天(以端点 today 为准)。 */\r\n initialDate: string | null\r\n}\r\n\r\ntype LoadState =\r\n | { status: 'loading' }\r\n | { status: 'error' }\r\n | { status: 'ok'; today: string; record: DailyRecord }\r\n\r\nasync function fetchDay(date: string | null): Promise<LoadState> {\r\n try {\r\n const res = await fetch(date === null ? '/token-usage/api/daily' : `/token-usage/api/daily?date=${date}`)\r\n if (!res.ok) return { status: 'error' }\r\n const body = await res.json() as { today: string; record: DailyRecord }\r\n return { status: 'ok', today: body.today, record: body.record }\r\n } catch {\r\n return { status: 'error' }\r\n }\r\n}\r\n\r\nfunction Breakdown({ title, rows }: { title: string; rows: [string, Bucket][] }) {\r\n if (rows.length === 0) return null\r\n return (\r\n <section>\r\n <h3 className={css.sectionTitle}>{title}</h3>\r\n {rows.map(([name, b]) => (\r\n <div key={name} className={css.row}>\r\n <span className={css.rowName}>{name}</span>\r\n <span>{formatTokens(billedOf(b))}</span>\r\n <span className={css.rowCalls}>{b.calls} 次</span>\r\n </div>\r\n ))}\r\n </section>\r\n )\r\n}\r\n\r\nexport function UsageModal({ open, onClose, initialDate }: UsageModalProps) {\r\n const [date, setDate] = useState<string | null>(initialDate)\r\n const [state, setState] = useState<LoadState>({ status: 'loading' })\r\n\r\n useEffect(() => { if (open) setDate(initialDate) }, [open, initialDate])\r\n useEffect(() => {\r\n if (!open) return\r\n let stale = false\r\n setState({ status: 'loading' })\r\n void fetchDay(date).then((s) => { if (!stale) setState(s) })\r\n return () => { stale = true }\r\n }, [open, date])\r\n\r\n const record = state.status === 'ok' ? state.record : undefined\r\n const today = state.status === 'ok' ? state.today : undefined\r\n const current = record?.date ?? initialDate ?? ''\r\n const peak = record === undefined ? 1 : Math.max(1, ...record.hours.map(billedOf))\r\n\r\n return (\r\n <Modal open={open} onClose={onClose} title=\"Token 用量\" closeLabel=\"关闭\" className={css.dialog}>\r\n <div className={css.pager}>\r\n <button type=\"button\" className={css.pagerButton} aria-label=\"前一天\" disabled={current === ''} onClick={() => setDate(shiftDate(current, -1))}>←</button>\r\n <span className={css.dateLabel}>{current}</span>\r\n <button type=\"button\" className={css.pagerButton} aria-label=\"后一天\" disabled={today === undefined || shiftDate(current, 1) > today}\r\n onClick={() => setDate(shiftDate(current, 1))}>→</button>\r\n </div>\r\n {state.status === 'loading' && <p>加载中…</p>}\r\n {state.status === 'error' && <p>加载失败,请重试</p>}\r\n {record !== undefined && (\r\n <>\r\n <div className={css.chart}>\r\n {record.hours.map((b, h) => (\r\n <div key={h} className={css.barSlot} title={`${h}:00 ${formatTokens(billedOf(b))} ${b.calls} 次`}>\r\n <div className={css.bar} style={{ height: `${(billedOf(b) / peak) * 100}%` }} />\r\n </div>\r\n ))}\r\n </div>\r\n <div className={css.hourLabels}>\r\n {[0, 6, 12, 18].map((h) => <span key={h}>{h}:00</span>)}\r\n </div>\r\n <p className={css.total}>\r\n 当日总量 {formatTokens(billedOf(record.totals))} · {record.totals.calls} 次调用\r\n {record.totals.estimated > 0 && `(含估算 ${formatTokens(record.totals.estimated)})`}\r\n {record.totals.calls === 0 && ' · 当日无用量'}\r\n </p>\r\n <Breakdown title=\"按模型\" rows={Object.entries(record.byModel).sort((a, b) => billedOf(b[1]) - billedOf(a[1]))} />\r\n <Breakdown title=\"按项目\" rows={Object.entries(record.byProject).sort((a, b) => billedOf(b[1]) - billedOf(a[1]))} />\r\n {record.compaction.calls > 0 && (\r\n <p className={css.compaction}>上下文压缩 {formatTokens(billedOf(record.compaction))} · {record.compaction.calls} 次</p>\r\n )}\r\n </>\r\n )}\r\n </Modal>\r\n )\r\n}\r\n","/** Sidebar footer entry: wide icon+label row vs rail icon-only; opens the usage modal. */\r\nimport { useState, type ReactNode } from 'react'\r\nimport clsx from 'clsx'\r\nimport { IconDataOutline16, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'\r\nimport { UsageModal } from './UsageModal.tsx'\r\nimport css from './UsageEntry.module.css'\r\n\r\nexport interface UsageEntryProps {\r\n /** Owner share from the sidebar shell: wide content vs 56px rail. */\r\n wide: boolean\r\n}\r\n\r\nexport function UsageEntry({ wide }: UsageEntryProps): ReactNode {\r\n const [open, setOpen] = useState(false)\r\n return (\r\n <>\r\n {/* Wide rows carry their own label — tooltip only on the rail (mirrors\r\n the built-in New Session button's behavior). */}\r\n <Tooltip label=\"Token 用量\" delayMs={500} disabled={wide}>\r\n <button\r\n type=\"button\"\r\n className={clsx(css.trigger, !wide && css.rail)}\r\n aria-label=\"Token 用量\"\r\n onClick={() => { setOpen(true) }}\r\n >\r\n <IconDataOutline16 size={wide ? 16 : 18} />\r\n {wide && <span className={css.triggerLabel}>Token 用量</span>}\r\n </button>\r\n </Tooltip>\r\n <UsageModal open={open} onClose={() => { setOpen(false) }} initialDate={null} />\r\n </>\r\n )\r\n}\r\n","/** token-usage 浏览器半:注册侧边栏底栏入口。 */\r\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\r\n// 触发 ui-sidebar 对 SlotMap 的声明合并(sidebar.footer.action 键)。\r\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\r\nimport { UsageEntry } from './UsageEntry.tsx'\r\n\r\nexport const inject = ['slots']\r\n\r\nexport function apply(ctx: ClientContext): void {\r\n // inject() 等 slot 被 ui-sidebar 声明后再注册,声明消失自动回滚。\r\n ctx.slots.inject('sidebar.footer.action', () =>\r\n ctx.slots.register(\r\n { name: 'sidebar.footer.action', id: 'token-usage', order: 0 },\r\n UsageEntry,\r\n ))\r\n}\r\n"],"x_google_ignoreList":[0],"mappings":";;;;;;;;;;EAAA,SAAS,EAAE,GAAE;GAAC,IAAI,GAAE,GAAE,IAAE;GAAG,IAAG,YAAU,OAAO,KAAG,YAAU,OAAO,GAAE,KAAG;QAAO,IAAG,YAAU,OAAO,GAAE,IAAG,MAAM,QAAQ,CAAC,GAAE;IAAC,IAAI,IAAE,EAAE;IAAO,KAAI,IAAE,GAAE,IAAE,GAAE,KAAI,EAAE,OAAK,IAAE,EAAE,EAAE,EAAE,OAAK,MAAI,KAAG,MAAK,KAAG;GAAE,OAAM,KAAI,KAAK,GAAE,EAAE,OAAK,MAAI,KAAG,MAAK,KAAG;GAAG,OAAO;EAAC;EAAC,SAAgB,OAAM;GAAC,KAAI,IAAI,GAAE,GAAE,IAAE,GAAE,IAAE,IAAG,IAAE,UAAU,QAAO,IAAE,GAAE,KAAI,CAAC,IAAE,UAAU,QAAM,IAAE,EAAE,CAAC,OAAK,MAAI,KAAG,MAAK,KAAG;GAAG,OAAO;EAAC;;;;EC0B/W,SAAgB,SAAS,GAAmB;GAC1C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE;EAC7D;;EAwFA,SAAgB,UAAU,MAAc,MAAsB;GAC5D,OAAO,IAAI,KAAK,KAAK,MAAM,GAAG,KAAK,WAAW,IAAI,OAAO,KAAU,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE;EAChG;;EAGA,SAAgB,aAAa,GAAmB;GAC9C,MAAM,QAAQ;IAAC;IAAI;IAAK;IAAK;GAAG;GAChC,IAAI,QAAQ;GACZ,IAAI,OAAO;GACX,OAAO,SAAS,OAAQ,OAAO,MAAM,SAAS,GAAG;IAC/C,SAAS;IACT,QAAQ;GACV;GACA,OAAO,SAAS,IAAI,OAAO,CAAC,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,MAAM;EAC9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC/GA,eAAe,SAAS,MAAyC;GAC/D,IAAI;IACF,MAAM,MAAM,MAAM,MAAM,SAAS,OAAO,2BAA2B,+BAA+B,MAAM;IACxG,IAAI,CAAC,IAAI,IAAI,OAAO,EAAE,QAAQ,QAAQ;IACtC,MAAM,OAAO,MAAM,IAAI,KAAK;IAC5B,OAAO;KAAE,QAAQ;KAAM,OAAO,KAAK;KAAO,QAAQ,KAAK;IAAO;GAChE,QAAQ;IACN,OAAO,EAAE,QAAQ,QAAQ;GAC3B;EACF;EAEA,SAAS,UAAU,EAAE,OAAO,QAAqD;GAC/E,IAAI,KAAK,WAAW,GAAG,OAAO;GAC9B,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,WAAD,EAAA,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;IAAI,WAAWA,8BAAI;IAAe,UAAA;GAAU,CAAA,GAC3C,KAAK,KAAK,CAAC,MAAM,OAChB,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAgB,WAAWA,8BAAI;IAA/B,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAWA,8BAAI;MAAU,UAAA;KAAW,CAAA;KAC1C,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,aAAa,SAAS,CAAC,CAAC,EAAQ,CAAA;KACvC,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;MAAM,WAAWA,8BAAI;MAArB,UAAA,CAAgC,EAAE,OAAM,IAAQ;;IAC7C;GAJK,GAAA,IAIL,CACN,CACM,EAAA,CAAA;EAEb;EAEA,SAAgB,WAAW,EAAE,MAAM,SAAS,eAAgC;GAC1E,MAAM,CAAC,MAAM,YAAA,GAAWC,MAAAA,SAAAA,CAAwB,WAAW;GAC3D,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAoB,EAAE,QAAQ,UAAU,CAAC;GAEnE,CAAA,GAAA,MAAA,UAAA,OAAgB;IAAE,IAAI,MAAM,QAAQ,WAAW;GAAE,GAAG,CAAC,MAAM,WAAW,CAAC;GACvE,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,MAAM;IACX,IAAI,QAAQ;IACZ,SAAS,EAAE,QAAQ,UAAU,CAAC;IAC9B,SAAc,IAAI,CAAC,CAAC,MAAM,MAAM;KAAE,IAAI,CAAC,OAAO,SAAS,CAAC;IAAE,CAAC;IAC3D,aAAa;KAAE,QAAQ;IAAK;GAC9B,GAAG,CAAC,MAAM,IAAI,CAAC;GAEf,MAAM,SAAS,MAAM,WAAW,OAAO,MAAM,SAAS,KAAA;GACtD,MAAM,QAAQ,MAAM,WAAW,OAAO,MAAM,QAAQ,KAAA;GACpD,MAAM,UAAU,QAAQ,QAAQ,eAAe;GAC/C,MAAM,OAAO,WAAW,KAAA,IAAY,IAAI,KAAK,IAAI,GAAG,GAAG,OAAO,MAAM,IAAI,QAAQ,CAAC;GAEjF,OACE,iBAAA,GAAA,kBAAA,KAAA,CAACC,sCAAAA,OAAD;IAAa;IAAe;IAAS,OAAM;IAAW,YAAW;IAAK,WAAWF,8BAAI;IAArF,UAAA;KACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,8BAAI;MAApB,UAAA;OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;QAAQ,MAAK;QAAS,WAAWA,8BAAI;QAAa,cAAW;QAAM,UAAU,YAAY;QAAI,eAAe,QAAQ,UAAU,SAAS,EAAE,CAAC;QAAG,UAAA;OAAS,CAAA;OACtJ,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QAAM,WAAWA,8BAAI;QAAY,UAAA;OAAc,CAAA;OAC/C,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;QAAQ,MAAK;QAAS,WAAWA,8BAAI;QAAa,cAAW;QAAM,UAAU,UAAU,KAAA,KAAa,UAAU,SAAS,CAAC,IAAI;QAC1H,eAAe,QAAQ,UAAU,SAAS,CAAC,CAAC;QAAG,UAAA;OAAS,CAAA;MACvD;;KACJ,MAAM,WAAW,aAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD,EAAA,UAAG,OAAO,CAAA;KACxC,MAAM,WAAW,WAAW,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD,EAAA,UAAG,WAAW,CAAA;KAC1C,WAAW,KAAA,KACV,iBAAA,GAAA,kBAAA,KAAA,CAAA,kBAAA,UAAA,EAAA,UAAA;MACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAWA,8BAAI;OACjB,UAAA,OAAO,MAAM,KAAK,GAAG,MACpB,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;QAAa,WAAWA,8BAAI;QAAS,OAAO,GAAG,EAAE,OAAO,aAAa,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM;QAC5F,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;SAAK,WAAWA,8BAAI;SAAK,OAAO,EAAE,QAAQ,GAAI,SAAS,CAAC,IAAI,OAAQ,IAAI,GAAG;QAAI,CAAA;OAC5E,GAFK,CAEL,CACN;MACE,CAAA;MACL,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAWA,8BAAI;OACjB,UAAA;QAAC;QAAG;QAAG;QAAI;OAAE,CAAC,CAAC,KAAK,MAAM,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD,EAAA,UAAA,CAAe,GAAE,KAAS,EAAA,GAAf,CAAe,CAAC;MACnD,CAAA;MACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,KAAD;OAAG,WAAWA,8BAAI;OAAlB,UAAA;QAAyB;QACjB,aAAa,SAAS,OAAO,MAAM,CAAC;QAAE;QAAI,OAAO,OAAO;QAAM;QACnE,OAAO,OAAO,YAAY,KAAK,QAAQ,aAAa,OAAO,OAAO,SAAS,EAAE;QAC7E,OAAO,OAAO,UAAU,KAAK;OAC7B;;MACH,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OAAW,OAAM;OAAM,MAAM,OAAO,QAAQ,OAAO,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,SAAS,EAAE,EAAE,IAAI,SAAS,EAAE,EAAE,CAAC;MAAI,CAAA;MAC9G,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OAAW,OAAM;OAAM,MAAM,OAAO,QAAQ,OAAO,SAAS,CAAC,CAAC,MAAM,GAAG,MAAM,SAAS,EAAE,EAAE,IAAI,SAAS,EAAE,EAAE,CAAC;MAAI,CAAA;MAC/G,OAAO,WAAW,QAAQ,KACzB,iBAAA,GAAA,kBAAA,KAAA,CAAC,KAAD;OAAG,WAAWA,8BAAI;OAAlB,UAAA;QAA8B;QAAO,aAAa,SAAS,OAAO,UAAU,CAAC;QAAE;QAAI,OAAO,WAAW;QAAM;OAAK;;KAElH,EAAA,CAAA;IAEC;;EAEX;;;;;;;;;;;;;;;;;;;;ECxFA,SAAgB,WAAW,EAAE,QAAoC;GAC/D,MAAM,CAAC,MAAM,YAAA,GAAWG,MAAAA,SAAAA,CAAS,KAAK;GACtC,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAA,kBAAA,UAAA,EAAA,UAAA,CAGE,iBAAA,GAAA,kBAAA,IAAA,CAACC,sCAAAA,SAAD;IAAS,OAAM;IAAW,SAAS;IAAK,UAAU;IAChD,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;KACE,MAAK;KACL,WAAW,KAAKC,8BAAI,SAAS,CAAC,QAAQA,8BAAI,IAAI;KAC9C,cAAW;KACX,eAAe;MAAE,QAAQ,IAAI;KAAE;KAJjC,UAAA,CAME,iBAAA,GAAA,kBAAA,IAAA,CAACC,sCAAAA,mBAAD,EAAmB,MAAM,OAAO,KAAK,GAAK,CAAA,GACzC,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAWD,8BAAI;MAAc,UAAA;KAAc,CAAA,CACpD;;GACD,CAAA,GACT,iBAAA,GAAA,kBAAA,IAAA,CAAC,YAAD;IAAkB;IAAM,eAAe;KAAE,QAAQ,KAAK;IAAE;IAAG,aAAa;GAAO,CAAA,CAC/E,EAAA,CAAA;EAEN;;;EC1BA,MAAa,SAAS,CAAC,OAAO;EAE9B,SAAgB,MAAM,KAA0B;GAE9C,IAAI,MAAM,OAAO,+BACf,IAAI,MAAM,SACR;IAAE,MAAM;IAAyB,IAAI;IAAe,OAAO;GAAE,GAC7D,UACF,CAAC;EACL"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
interface Config {
|
|
5
|
+
/** 按日聚合的时区(IANA 名)。 */
|
|
6
|
+
timezone: string;
|
|
7
|
+
}
|
|
8
|
+
declare const Config: z<Config>;
|
|
9
|
+
declare const name = "token-usage";
|
|
10
|
+
declare const inject: string[];
|
|
11
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { Config, apply, inject, name };
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { z as z$1 } from "zod";
|
|
3
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
4
|
+
//#region src/aggregate.ts
|
|
5
|
+
/** 桶的计费总量(含估算)。 */
|
|
6
|
+
function billedOf(b) {
|
|
7
|
+
return b.input + b.output + b.cacheRead + b.cacheWrite + b.estimated;
|
|
8
|
+
}
|
|
9
|
+
function emptyBucket() {
|
|
10
|
+
return {
|
|
11
|
+
input: 0,
|
|
12
|
+
output: 0,
|
|
13
|
+
cacheRead: 0,
|
|
14
|
+
cacheWrite: 0,
|
|
15
|
+
calls: 0,
|
|
16
|
+
estimated: 0
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function emptyDaily(date) {
|
|
20
|
+
return {
|
|
21
|
+
date,
|
|
22
|
+
totals: {
|
|
23
|
+
...emptyBucket(),
|
|
24
|
+
estimatedCalls: 0
|
|
25
|
+
},
|
|
26
|
+
hours: Array.from({ length: 24 }, emptyBucket),
|
|
27
|
+
byModel: {},
|
|
28
|
+
bySession: {},
|
|
29
|
+
byProject: {},
|
|
30
|
+
compaction: emptyBucket()
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function addToBucket(b, s) {
|
|
34
|
+
return {
|
|
35
|
+
input: b.input + s.input,
|
|
36
|
+
output: b.output + s.output,
|
|
37
|
+
cacheRead: b.cacheRead + s.cacheRead,
|
|
38
|
+
cacheWrite: b.cacheWrite + s.cacheWrite,
|
|
39
|
+
calls: b.calls + 1,
|
|
40
|
+
estimated: b.estimated + s.estimated
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** 把一条样本并入日记录,返回新对象(存储记录禁止就地修改)。 */
|
|
44
|
+
function addSample(rec, s) {
|
|
45
|
+
const totals = {
|
|
46
|
+
...addToBucket(rec.totals, s),
|
|
47
|
+
estimatedCalls: rec.totals.estimatedCalls + (s.estimatedCall ? 1 : 0)
|
|
48
|
+
};
|
|
49
|
+
const hours = rec.hours.slice();
|
|
50
|
+
hours[s.hour] = addToBucket(hours[s.hour], s);
|
|
51
|
+
const byModel = { ...rec.byModel };
|
|
52
|
+
if (s.model !== void 0) byModel[s.model] = addToBucket(byModel[s.model] ?? emptyBucket(), s);
|
|
53
|
+
const bySession = { ...rec.bySession };
|
|
54
|
+
if (s.sessionId !== void 0 && s.cwd !== void 0) bySession[s.sessionId] = {
|
|
55
|
+
...addToBucket(bySession[s.sessionId] ?? {
|
|
56
|
+
...emptyBucket(),
|
|
57
|
+
cwd: s.cwd
|
|
58
|
+
}, s),
|
|
59
|
+
cwd: s.cwd
|
|
60
|
+
};
|
|
61
|
+
const byProject = { ...rec.byProject };
|
|
62
|
+
if (s.cwd !== void 0 && !s.compaction) byProject[s.cwd] = addToBucket(byProject[s.cwd] ?? emptyBucket(), s);
|
|
63
|
+
const compaction = s.compaction ? addToBucket(rec.compaction, s) : rec.compaction;
|
|
64
|
+
return {
|
|
65
|
+
...rec,
|
|
66
|
+
totals,
|
|
67
|
+
hours,
|
|
68
|
+
byModel,
|
|
69
|
+
bySession,
|
|
70
|
+
byProject,
|
|
71
|
+
compaction
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** 从 session 事件提取样本;不相关事件与无 usage 的 compaction 返回 undefined。 */
|
|
75
|
+
function sampleFromEvent(session, event, timeZone, estimate) {
|
|
76
|
+
const { date, hour } = dayParts(event.time, timeZone);
|
|
77
|
+
if (event.type === "assistant/message") {
|
|
78
|
+
const { message, usage } = event.data;
|
|
79
|
+
const base = {
|
|
80
|
+
date,
|
|
81
|
+
hour,
|
|
82
|
+
model: `${message.source.provider}/${message.source.model}`,
|
|
83
|
+
sessionId: String(session.header.id),
|
|
84
|
+
cwd: session.header.cwd,
|
|
85
|
+
compaction: false
|
|
86
|
+
};
|
|
87
|
+
if (usage === void 0) return {
|
|
88
|
+
...base,
|
|
89
|
+
input: 0,
|
|
90
|
+
output: 0,
|
|
91
|
+
cacheRead: 0,
|
|
92
|
+
cacheWrite: 0,
|
|
93
|
+
estimated: estimate(message),
|
|
94
|
+
estimatedCall: true
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
...base,
|
|
98
|
+
estimated: 0,
|
|
99
|
+
estimatedCall: false,
|
|
100
|
+
input: usage.inputTokens,
|
|
101
|
+
output: usage.outputTokens,
|
|
102
|
+
cacheRead: usage.cacheReadTokens ?? 0,
|
|
103
|
+
cacheWrite: usage.cacheWriteTokens ?? 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (event.type === "compaction/summary") {
|
|
107
|
+
const { usage } = event.data;
|
|
108
|
+
if (usage === void 0) return void 0;
|
|
109
|
+
return {
|
|
110
|
+
date,
|
|
111
|
+
hour,
|
|
112
|
+
model: void 0,
|
|
113
|
+
estimated: 0,
|
|
114
|
+
estimatedCall: false,
|
|
115
|
+
compaction: true,
|
|
116
|
+
input: usage.inputTokens,
|
|
117
|
+
output: usage.outputTokens,
|
|
118
|
+
cacheRead: usage.cacheReadTokens ?? 0,
|
|
119
|
+
cacheWrite: usage.cacheWriteTokens ?? 0
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** 把 UTC 毫秒换算成指定时区的日期串与小时序号。 */
|
|
124
|
+
function dayParts(time, timeZone) {
|
|
125
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
126
|
+
timeZone,
|
|
127
|
+
year: "numeric",
|
|
128
|
+
month: "2-digit",
|
|
129
|
+
day: "2-digit",
|
|
130
|
+
hour: "2-digit",
|
|
131
|
+
hour12: false
|
|
132
|
+
}).formatToParts(time);
|
|
133
|
+
const get = (type) => parts.find((p) => p.type === type).value;
|
|
134
|
+
return {
|
|
135
|
+
date: `${get("year")}-${get("month")}-${get("day")}`,
|
|
136
|
+
hour: Number(get("hour")) % 24
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** 计费 token 数自动换算 K/M/B(10 进制,1 位小数)。 */
|
|
140
|
+
function formatTokens(n) {
|
|
141
|
+
const units = [
|
|
142
|
+
"",
|
|
143
|
+
"K",
|
|
144
|
+
"M",
|
|
145
|
+
"B"
|
|
146
|
+
];
|
|
147
|
+
let value = n;
|
|
148
|
+
let unit = 0;
|
|
149
|
+
while (value >= 1e3 && unit < units.length - 1) {
|
|
150
|
+
value /= 1e3;
|
|
151
|
+
unit += 1;
|
|
152
|
+
}
|
|
153
|
+
return unit === 0 ? String(n) : `${value.toFixed(1)}${units[unit]}`;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/render.ts
|
|
157
|
+
/** /token-usage 命令的文本视图(纯函数)。 */
|
|
158
|
+
function line(name, b) {
|
|
159
|
+
return ` ${name} ${formatTokens(billedOf(b))} ${b.calls} 次调用`;
|
|
160
|
+
}
|
|
161
|
+
/** 当日详情:总量(含估算标注)+ 模型/项目二维细分 + compaction 单列。 */
|
|
162
|
+
function renderDay(rec) {
|
|
163
|
+
const est = rec.totals.estimated > 0 ? `(含估算 ${formatTokens(rec.totals.estimated)})` : "";
|
|
164
|
+
const rows = [`${rec.date} 用量:${formatTokens(billedOf(rec.totals))} ${rec.totals.calls} 次调用${est}`];
|
|
165
|
+
const models = Object.entries(rec.byModel).sort((a, b) => billedOf(b[1]) - billedOf(a[1]));
|
|
166
|
+
if (models.length > 0) rows.push("按模型:", ...models.map(([k, v]) => line(k, v)));
|
|
167
|
+
const projects = Object.entries(rec.byProject).sort((a, b) => billedOf(b[1]) - billedOf(a[1]));
|
|
168
|
+
if (projects.length > 0) rows.push("按项目:", ...projects.map(([k, v]) => line(k, v)));
|
|
169
|
+
if (rec.compaction.calls > 0) rows.push(`上下文压缩:${formatTokens(billedOf(rec.compaction))} ${rec.compaction.calls} 次调用`);
|
|
170
|
+
return rows.join("\n");
|
|
171
|
+
}
|
|
172
|
+
/** 今日详情 + 近 7 日逐日摘要行(days[0] 为今日)。 */
|
|
173
|
+
function renderWeek(today, days) {
|
|
174
|
+
const lines = days.map((d) => `${d.date} ${formatTokens(billedOf(d.totals))} ${d.totals.calls} 次调用`);
|
|
175
|
+
return `${renderDay(days[0])}\n\n近 7 日:\n${lines.join("\n")}`;
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/store.ts
|
|
179
|
+
/** token-usage 存储域声明:身份、版本、记录 zod schema 的单一来源。 */
|
|
180
|
+
const BucketSchema = z$1.object({
|
|
181
|
+
input: z$1.number().int().nonnegative(),
|
|
182
|
+
output: z$1.number().int().nonnegative(),
|
|
183
|
+
cacheRead: z$1.number().int().nonnegative(),
|
|
184
|
+
cacheWrite: z$1.number().int().nonnegative(),
|
|
185
|
+
calls: z$1.number().int().nonnegative(),
|
|
186
|
+
/** 估算样本的计费 token 量(usage 缺失时经 tokenMeter 启发式得出)。 */
|
|
187
|
+
estimated: z$1.number().int().nonnegative()
|
|
188
|
+
});
|
|
189
|
+
const DailyRecordSchema = z$1.object({
|
|
190
|
+
date: z$1.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
191
|
+
totals: BucketSchema.extend({ estimatedCalls: z$1.number().int().nonnegative() }),
|
|
192
|
+
/** 24 小时桶,空小时为全零桶。 */
|
|
193
|
+
hours: z$1.array(BucketSchema).length(24),
|
|
194
|
+
/** key = 'provider/model'。 */
|
|
195
|
+
byModel: z$1.record(z$1.string(), BucketSchema),
|
|
196
|
+
/** key = sessionId。 */
|
|
197
|
+
bySession: z$1.record(z$1.string(), BucketSchema.extend({ cwd: z$1.string() })),
|
|
198
|
+
/** key = cwd(原样存储)。 */
|
|
199
|
+
byProject: z$1.record(z$1.string(), BucketSchema),
|
|
200
|
+
/** 压缩摘要调用单列;数值同时已并入 totals/hours。 */
|
|
201
|
+
compaction: BucketSchema
|
|
202
|
+
});
|
|
203
|
+
/** domain 名受 UNIT_NAME_RE 约束(^[a-z][a-z0-9_]*$),不允许连字符。 */
|
|
204
|
+
const tokenUsageDomain = defineDomain({
|
|
205
|
+
name: "token_usage",
|
|
206
|
+
version: 1,
|
|
207
|
+
tables: { daily: domainTable(DailyRecordSchema) }
|
|
208
|
+
});
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/index.ts
|
|
211
|
+
const Config = z.object({ timezone: z.string().default(Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC") });
|
|
212
|
+
const name = "token-usage";
|
|
213
|
+
const inject = [
|
|
214
|
+
"storageDomain",
|
|
215
|
+
"tokenMeter",
|
|
216
|
+
"commands"
|
|
217
|
+
];
|
|
218
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
219
|
+
function apply(ctx, config) {
|
|
220
|
+
let daily;
|
|
221
|
+
let tail = Promise.resolve();
|
|
222
|
+
const domainReady = ctx.storageDomain.open(tokenUsageDomain).then((domain) => {
|
|
223
|
+
daily = domain.table("daily");
|
|
224
|
+
return domain;
|
|
225
|
+
});
|
|
226
|
+
domainReady.catch((error) => {
|
|
227
|
+
ctx.logger.warn(`[token-usage] 存储域打开失败,token 统计不可用:${error instanceof Error ? error.message : String(error)}`);
|
|
228
|
+
});
|
|
229
|
+
ctx.on("session/event", (session, event) => {
|
|
230
|
+
const sample = sampleFromEvent(session, event, config.timezone, (m) => ctx.tokenMeter.estimateMessage(m));
|
|
231
|
+
if (sample === void 0) return;
|
|
232
|
+
tail = tail.then(() => domainReady).then(() => {
|
|
233
|
+
const table = daily;
|
|
234
|
+
return table.put(sample.date, addSample(table.get(sample.date) ?? emptyDaily(sample.date), sample));
|
|
235
|
+
}).then(() => void 0, () => void 0);
|
|
236
|
+
});
|
|
237
|
+
ctx.commands.register({
|
|
238
|
+
name: "token-usage",
|
|
239
|
+
description: "查看 token 用量(今日+近7日,或指定日期)",
|
|
240
|
+
input: { hint: "YYYY-MM-DD,可空" },
|
|
241
|
+
handler: async ({ rawInput }) => {
|
|
242
|
+
const table = await domainReady.then(() => daily);
|
|
243
|
+
const arg = rawInput.trim();
|
|
244
|
+
dayParts(Date.now(), config.timezone).date;
|
|
245
|
+
if (arg !== "" && !DATE_RE.test(arg)) return {
|
|
246
|
+
kind: "error",
|
|
247
|
+
text: "用法:/token-usage [YYYY-MM-DD]"
|
|
248
|
+
};
|
|
249
|
+
if (arg !== "") return {
|
|
250
|
+
kind: "success",
|
|
251
|
+
text: renderDay(table.get(arg) ?? emptyDaily(arg))
|
|
252
|
+
};
|
|
253
|
+
const days = Array.from({ length: 7 }, (_, i) => {
|
|
254
|
+
const date = dayParts(Date.now() - i * 864e5, config.timezone).date;
|
|
255
|
+
return table.get(date) ?? emptyDaily(date);
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
kind: "success",
|
|
259
|
+
text: renderWeek(days[0].date, days)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
264
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
265
|
+
kind: "exact",
|
|
266
|
+
path: "/token-usage/api/daily",
|
|
267
|
+
handler: async (req, res) => {
|
|
268
|
+
if (req.method !== "GET") {
|
|
269
|
+
res.writeHead(405).end();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const date = new URL(req.url ?? "", "http://127.0.0.1").searchParams.get("date");
|
|
273
|
+
if (date !== null && !DATE_RE.test(date)) {
|
|
274
|
+
res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "bad date, want YYYY-MM-DD" }));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const table = await domainReady.then(() => daily);
|
|
278
|
+
const today = dayParts(Date.now(), config.timezone).date;
|
|
279
|
+
const key = date ?? today;
|
|
280
|
+
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
|
|
281
|
+
today,
|
|
282
|
+
record: table.get(key) ?? emptyDaily(key)
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
}), "token-usage: /token-usage/api/daily route");
|
|
286
|
+
});
|
|
287
|
+
ctx.effect(() => async () => {
|
|
288
|
+
await tail.catch(() => void 0);
|
|
289
|
+
await domainReady.then((domain) => domain.close());
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
export { Config, apply, inject, name };
|
|
294
|
+
|
|
295
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["z"],"sources":["../src/aggregate.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 命令的文本视图(纯函数)。 */\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 { 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\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":";;;;;AA0BA,SAAgB,SAAS,GAAmB;CAC1C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE;AAC7D;AAEA,SAAgB,cAAsB;CACpC,OAAO;EAAE,OAAO;EAAG,QAAQ;EAAG,WAAW;EAAG,YAAY;EAAG,OAAO;EAAG,WAAW;CAAE;AACpF;AAEA,SAAgB,WAAW,MAA2B;CACpD,OAAO;EACL;EACA,QAAQ;GAAE,GAAG,YAAY;GAAG,gBAAgB;EAAE;EAC9C,OAAO,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,WAAW;EAC7C,SAAS,CAAC;EACV,WAAW,CAAC;EACZ,WAAW,CAAC;EACZ,YAAY,YAAY;CAC1B;AACF;AAEA,SAAS,YAAY,GAAW,GAAwB;CACtD,OAAO;EACL,OAAO,EAAE,QAAQ,EAAE;EAAO,QAAQ,EAAE,SAAS,EAAE;EAC/C,WAAW,EAAE,YAAY,EAAE;EAAW,YAAY,EAAE,aAAa,EAAE;EACnE,OAAO,EAAE,QAAQ;EAAG,WAAW,EAAE,YAAY,EAAE;CACjD;AACF;;AAGA,SAAgB,UAAU,KAAkB,GAA6B;CACvE,MAAM,SAAS;EAAE,GAAG,YAAY,IAAI,QAAQ,CAAC;EAAG,gBAAgB,IAAI,OAAO,kBAAkB,EAAE,gBAAgB,IAAI;CAAG;CACtH,MAAM,QAAQ,IAAI,MAAM,MAAM;CAC9B,MAAM,EAAE,QAAQ,YAAY,MAAM,EAAE,OAAO,CAAC;CAC5C,MAAM,UAAU,EAAE,GAAG,IAAI,QAAQ;CACjC,IAAI,EAAE,UAAU,KAAA,GAAW,QAAQ,EAAE,SAAS,YAAY,QAAQ,EAAE,UAAU,YAAY,GAAG,CAAC;CAC9F,MAAM,YAAY,EAAE,GAAG,IAAI,UAAU;CACrC,IAAI,EAAE,cAAc,KAAA,KAAa,EAAE,QAAQ,KAAA,GACzC,UAAU,EAAE,aAAa;EAAE,GAAG,YAAY,UAAU,EAAE,cAAc;GAAE,GAAG,YAAY;GAAG,KAAK,EAAE;EAAI,GAAG,CAAC;EAAG,KAAK,EAAE;CAAI;CAEvH,MAAM,YAAY,EAAE,GAAG,IAAI,UAAU;CACrC,IAAI,EAAE,QAAQ,KAAA,KAAa,CAAC,EAAE,YAAY,UAAU,EAAE,OAAO,YAAY,UAAU,EAAE,QAAQ,YAAY,GAAG,CAAC;CAC7G,MAAM,aAAa,EAAE,aAAa,YAAY,IAAI,YAAY,CAAC,IAAI,IAAI;CACvE,OAAO;EAAE,GAAG;EAAK;EAAQ;EAAO;EAAS;EAAW;EAAW;CAAW;AAC5E;;AAGA,SAAgB,gBACd,SACA,OACA,UACA,UACyB;CACzB,MAAM,EAAE,MAAM,SAAS,SAAS,MAAM,MAAM,QAAQ;CACpD,IAAI,MAAM,SAAS,qBAAqB;EACtC,MAAM,EAAE,SAAS,UAAU,MAAM;EACjC,MAAM,OAAO;GAAE;GAAM;GAAM,OAAO,GAAG,QAAQ,OAAO,SAAS,GAAG,QAAQ,OAAO;GAAS,WAAW,OAAO,QAAQ,OAAO,EAAE;GAAG,KAAK,QAAQ,OAAO;GAAK,YAAY;EAAM;EACzK,IAAI,UAAU,KAAA,GACZ,OAAO;GAAE,GAAG;GAAM,OAAO;GAAG,QAAQ;GAAG,WAAW;GAAG,YAAY;GAAG,WAAW,SAAS,OAAO;GAAG,eAAe;EAAK;EAExH,OAAO;GACL,GAAG;GAAM,WAAW;GAAG,eAAe;GACtC,OAAO,MAAM;GAAa,QAAQ,MAAM;GACxC,WAAW,MAAM,mBAAmB;GAAG,YAAY,MAAM,oBAAoB;EAC/E;CACF;CACA,IAAI,MAAM,SAAS,sBAAsB;EACvC,MAAM,EAAE,UAAU,MAAM;EACxB,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAChC,OAAO;GACL;GAAM;GAAM,OAAO,KAAA;GAAW,WAAW;GAAG,eAAe;GAAO,YAAY;GAC9E,OAAO,MAAM;GAAa,QAAQ,MAAM;GACxC,WAAW,MAAM,mBAAmB;GAAG,YAAY,MAAM,oBAAoB;EAC/E;CACF;AAEF;;AAGA,SAAgB,SAAS,MAAc,UAAkD;CACvF,MAAM,QAAQ,IAAI,KAAK,eAAe,SAAS;EAC7C;EAAU,MAAM;EAAW,OAAO;EAAW,KAAK;EAAW,MAAM;EAAW,QAAQ;CACxF,CAAC,CAAC,CAAC,cAAc,IAAI;CACrB,MAAM,OAAO,SAAyB,MAAM,MAAM,MAAM,EAAE,SAAS,IAAI,CAAC,CAAE;CAC1E,OAAO;EACL,MAAM,GAAG,IAAI,MAAM,EAAE,GAAG,IAAI,OAAO,EAAE,GAAG,IAAI,KAAK;EACjD,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI;CAC9B;AACF;;AAQA,SAAgB,aAAa,GAAmB;CAC9C,MAAM,QAAQ;EAAC;EAAI;EAAK;EAAK;CAAG;CAChC,IAAI,QAAQ;CACZ,IAAI,OAAO;CACX,OAAO,SAAS,OAAQ,OAAO,MAAM,SAAS,GAAG;EAC/C,SAAS;EACT,QAAQ;CACV;CACA,OAAO,SAAS,IAAI,OAAO,CAAC,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,MAAM;AAC9D;;;;AC9HA,SAAS,KAAK,MAAc,GAAmB;CAC7C,OAAO,KAAK,KAAK,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM;AAC7D;;AAGA,SAAgB,UAAU,KAA0B;CAClD,MAAM,MAAM,IAAI,OAAO,YAAY,IAAI,QAAQ,aAAa,IAAI,OAAO,SAAS,EAAE,KAAK;CACvF,MAAM,OAAiB,CACrB,GAAG,IAAI,KAAK,MAAM,aAAa,SAAS,IAAI,MAAM,CAAC,EAAE,GAAG,IAAI,OAAO,MAAM,MAAM,KACjF;CACA,MAAM,SAAS,OAAO,QAAQ,IAAI,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,SAAS,EAAE,EAAE,IAAI,SAAS,EAAE,EAAE,CAAC;CACzF,IAAI,OAAO,SAAS,GAAG,KAAK,KAAK,QAAQ,GAAG,OAAO,KAAK,CAAC,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC;CAC9E,MAAM,WAAW,OAAO,QAAQ,IAAI,SAAS,CAAC,CAAC,MAAM,GAAG,MAAM,SAAS,EAAE,EAAE,IAAI,SAAS,EAAE,EAAE,CAAC;CAC7F,IAAI,SAAS,SAAS,GAAG,KAAK,KAAK,QAAQ,GAAG,SAAS,KAAK,CAAC,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC;CAClF,IAAI,IAAI,WAAW,QAAQ,GAAG,KAAK,KAAK,SAAS,aAAa,SAAS,IAAI,UAAU,CAAC,EAAE,GAAG,IAAI,WAAW,MAAM,KAAK;CACrH,OAAO,KAAK,KAAK,IAAI;AACvB;;AAGA,SAAgB,WAAW,OAAe,MAAsC;CAC9E,MAAM,QAAQ,KAAK,KAAK,MAAM,GAAG,EAAE,KAAK,IAAI,aAAa,SAAS,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,MAAM,KAAK;CACrG,OAAO,GAAG,UAAU,KAAK,EAAE,EAAE,cAAc,MAAM,KAAK,IAAI;AAC5D;;;;ACtBA,MAAa,eAAeA,IAAE,OAAO;CACnC,OAAOA,IAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;CACpC,QAAQA,IAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;CACrC,WAAWA,IAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;CACxC,YAAYA,IAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;CACzC,OAAOA,IAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;;CAEpC,WAAWA,IAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY;AAC1C,CAAC;AAGD,MAAa,oBAAoBA,IAAE,OAAO;CACxC,MAAMA,IAAE,OAAO,CAAC,CAAC,MAAM,qBAAqB;CAC5C,QAAQ,aAAa,OAAO,EAAE,gBAAgBA,IAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,CAAC;;CAE9E,OAAOA,IAAE,MAAM,YAAY,CAAC,CAAC,OAAO,EAAE;;CAEtC,SAASA,IAAE,OAAOA,IAAE,OAAO,GAAG,YAAY;;CAE1C,WAAWA,IAAE,OAAOA,IAAE,OAAO,GAAG,aAAa,OAAO,EAAE,KAAKA,IAAE,OAAO,EAAE,CAAC,CAAC;;CAExE,WAAWA,IAAE,OAAOA,IAAE,OAAO,GAAG,YAAY;;CAE5C,YAAY;AACd,CAAC;;AAID,MAAa,mBAAmB,aAAa;CAC3C,MAAM;CACN,SAAS;CACT,QAAQ,EAAE,OAAO,YAAiC,iBAAiB,EAAE;AACvE,CAAC;;;ACnBD,MAAa,SAAoB,EAAE,OAAO,EACxC,UAAU,EAAE,OAAO,CAAC,CAAC,QAAQ,KAAK,eAAe,CAAC,CAAC,gBAAgB,CAAC,CAAC,YAAY,KAAK,EACxF,CAAC;AAED,MAAa,OAAO;AAEpB,MAAa,SAAS;CAAC;CAAiB;CAAc;AAAU;AAEhE,MAAM,UAAU;AAEhB,SAAgB,MAAM,KAAc,QAAsB;CACxD,IAAI;CAGJ,IAAI,OAAyB,QAAQ,QAAQ;CAC7C,MAAM,cAAc,IAAI,cAAc,KAAK,gBAAgB,CAAC,CAAC,MAAM,WAAW;EAC5E,QAAQ,OAAO,MAAM,OAAO;EAC5B,OAAO;CACT,CAAC;CAKD,YAAY,OAAO,UAAU;EAC3B,IAAI,OAAO,KAAK,qCAAqC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;CAC/G,CAAC;CAED,IAAI,GAAG,kBAAkB,SAAS,UAAU;EAC1C,MAAM,SAAS,gBAAgB,SAAS,OAAO,OAAO,WAAW,MAAM,IAAI,WAAW,gBAAgB,CAAC,CAAC;EACxG,IAAI,WAAW,KAAA,GAAW;EAM1B,OAJc,KAAK,WAAW,WAAW,CAAC,CAAC,WAAW;GACpD,MAAM,QAAQ;GACd,OAAO,MAAM,IAAI,OAAO,MAAM,UAAU,MAAM,IAAI,OAAO,IAAI,KAAK,WAAW,OAAO,IAAI,GAAG,MAAM,CAAC;EACpG,CACW,CAAC,CAAC,WAAW,KAAA,SAAiB,KAAA,CAAS;CACpD,CAAC;CAED,IAAI,SAAS,SAAS;EACpB,MAAM;EACN,aAAa;EACb,OAAO,EAAE,MAAM,gBAAgB;EAC/B,SAAS,OAAO,EAAE,eAAe;GAC/B,MAAM,QAAQ,MAAM,YAAY,WAAW,KAAM;GACjD,MAAM,MAAM,SAAS,KAAK;GACZ,SAAS,KAAK,IAAI,GAAG,OAAO,QAAQ,CAAC,CAAC;GACpD,IAAI,QAAQ,MAAM,CAAC,QAAQ,KAAK,GAAG,GACjC,OAAO;IAAE,MAAM;IAAkB,MAAM;GAA+B;GAExE,IAAI,QAAQ,IACV,OAAO;IAAE,MAAM;IAAoB,MAAM,UAAU,MAAM,IAAI,GAAG,KAAK,WAAW,GAAG,CAAC;GAAE;GAExF,MAAM,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,MAAM;IAC/C,MAAM,OAAO,SAAS,KAAK,IAAI,IAAI,IAAI,OAAY,OAAO,QAAQ,CAAC,CAAC;IACpE,OAAO,MAAM,IAAI,IAAI,KAAK,WAAW,IAAI;GAC3C,CAAC;GAED,OAAO;IAAE,MAAM;IAAoB,MAAM,WAAW,KAAK,EAAE,CAAC,MAAM,IAAI;GAAE;EAC1E;CACF,CAAC;CAOD,IAAI,OAAO,CAAC,WAAW,IAAI,WAAW;EACpC,OAAO,aAAa,OAAO,UAAU,SAAS;GAC5C,MAAM;GACN,MAAM;GACN,SAAS,OAAO,KAAK,QAAQ;IAC3B,IAAI,IAAI,WAAW,OAAO;KACxB,IAAI,UAAU,GAAG,CAAC,CAAC,IAAI;KACvB;IACF;IACA,MAAM,OAAO,IAAI,IAAI,IAAI,OAAO,IAAI,kBAAkB,CAAC,CAAC,aAAa,IAAI,MAAM;IAC/E,IAAI,SAAS,QAAQ,CAAC,QAAQ,KAAK,IAAI,GAAG;KACxC,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,EAAE,OAAO,4BAA4B,CAAC,CAAC;KACrH;IACF;IACA,MAAM,QAAQ,MAAM,YAAY,WAAW,KAAM;IACjD,MAAM,QAAQ,SAAS,KAAK,IAAI,GAAG,OAAO,QAAQ,CAAC,CAAC;IACpD,MAAM,MAAM,QAAQ;IACpB,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC,CAAC,CACvD,IAAI,KAAK,UAAU;KAAE;KAAO,QAAQ,MAAM,IAAI,GAAG,KAAK,WAAW,GAAG;IAAE,CAAC,CAAC;GAC7E;EACF,CAAC,GAAG,2CAA2C;CACjD,CAAC;CAED,IAAI,aAAa,YAAY;EAC3B,MAAM,KAAK,YAAY,KAAA,CAAS;EAChC,MAAM,YAAY,MAAM,WAAW,OAAO,MAAM,CAAC;CACnD,CAAC;AACH"}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-agent-toolkit/token-usage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek Harness plugin: per-day token usage statistics — /token-usage command, JSON endpoint, sidebar UI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"dsh",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"token-usage"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./lib/index.js",
|
|
14
|
+
"types": "./lib/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./lib/index.d.ts",
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./client": "./lib/client.js",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
},
|
|
36
|
+
"client": {
|
|
37
|
+
"platform": "web",
|
|
38
|
+
"inject": [
|
|
39
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
46
|
+
"clsx": "^2.0.0",
|
|
47
|
+
"zod": "^4.4.3"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@deepseek-ai/cordis": "link:../../deepseek-harness/vendor/cordis",
|
|
54
|
+
"@deepseek-ai/dsh-client-runtime": "link:../../deepseek-harness/packages/client/runtime",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "link:../../deepseek-harness/packages/client/ui-sidebar",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-primitives": "link:../../deepseek-harness/packages/client/ui-primitives",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-slots": "link:../../deepseek-harness/packages/client/ui-slots",
|
|
58
|
+
"@deepseek-ai/dsh-commands": "link:../../deepseek-harness/packages/interaction/commands",
|
|
59
|
+
"@deepseek-ai/dsh-compaction": "link:../../deepseek-harness/packages/compaction/compaction",
|
|
60
|
+
"@deepseek-ai/dsh-host-webserver": "link:../../deepseek-harness/packages/host/webserver",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "link:../../deepseek-harness/packages/llm/llm",
|
|
62
|
+
"@deepseek-ai/dsh-session": "link:../../deepseek-harness/packages/core/session",
|
|
63
|
+
"@deepseek-ai/dsh-storage-domain": "link:../../deepseek-harness/packages/storage/storage-domain",
|
|
64
|
+
"@deepseek-ai/dsh-token-meter": "link:../../deepseek-harness/packages/llm/token-meter",
|
|
65
|
+
"@deepseek-ai/schemastery": "link:../../deepseek-harness/vendor/schemastery",
|
|
66
|
+
"@testing-library/react": "^16.1.0",
|
|
67
|
+
"@types/node": "^22.20.1",
|
|
68
|
+
"@types/react": "~18.3.1",
|
|
69
|
+
"@types/react-dom": "~18.3.0",
|
|
70
|
+
"jsdom": "^26.1.0",
|
|
71
|
+
"lightningcss": "^1.30.0",
|
|
72
|
+
"react": "^18.2.0",
|
|
73
|
+
"react-dom": "^18.2.0",
|
|
74
|
+
"tsdown": "^0.22.2",
|
|
75
|
+
"typescript": "^6.0.3",
|
|
76
|
+
"vitest": "^3.0.0"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"test": "vitest run",
|
|
80
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
81
|
+
"bundle": "tsdown",
|
|
82
|
+
"watch": "tsdown --watch"
|
|
83
|
+
}
|
|
84
|
+
}
|