dsh-dock 0.10.3 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/client.js +394 -18
- package/features/tokenlog/host.js +292 -99
- package/features/tokenlog/view.jsx +374 -13
- package/package.json +3 -3
- package/src/client.jsx +5 -4
- package/src/host-core.js +38 -0
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// - 秒级时间范围查询 + 会话ID/提供商/模型(联动)/状态/推理强度 筛选,条件本地暂存
|
|
5
5
|
// - 9 张 KPI 卡 + 分组统计表 + 明细表(点击表头排序、会话ID点击即筛选、100 行/页上下双分页)
|
|
6
6
|
// - 状态列显示 HTTP 状态码徽章,行内【查看详情】弹窗展示完整信息;CSV 导出
|
|
7
|
+
// - 独立的「单价设置」子弹窗:按模型配置单价(支持多段分时价)并持久化,费用按自填单价重算
|
|
7
8
|
// - 挂载即扫描历史+按暂存条件查询;挂载期间每 5s 静默自动刷新
|
|
8
9
|
// Host 通信:fetch('/dsh-dock/tokenlog/<method>')(见 features/tokenlog/host.js)。
|
|
9
|
-
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
10
|
+
import react, { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
10
11
|
import { openPanel } from "../../src/shared.js";
|
|
11
12
|
|
|
12
13
|
// ---------- Host RPC 桥接 ----------
|
|
@@ -141,6 +142,38 @@ const css = `
|
|
|
141
142
|
.dtok-pager.top{margin:0;}
|
|
142
143
|
.dtok-pager.bottom{margin:0;}
|
|
143
144
|
.dtok-err{color:var(--dsw-alias-state-error-primary,#ff7a7a);font-size:12px;}
|
|
145
|
+
/* ---- 单价设置(子弹窗) ---- */
|
|
146
|
+
.dtok-pm-backdrop{position:fixed;inset:0;z-index:2100;background:rgba(15,17,21,.5);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;}
|
|
147
|
+
.dtok-pm{box-sizing:border-box;width:min(980px,calc(100vw - 32px));height:min(760px,calc(100vh - 32px));display:flex;flex-direction:column;border-radius:14px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2,#1c212b);color:var(--dsw-alias-label-primary);box-shadow:0 20px 64px rgb(0 0 0 / .4);overflow:hidden;}
|
|
148
|
+
.dtok-pm.max{border-radius:10px;}
|
|
149
|
+
.dtok-pm-head{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);cursor:move;user-select:none;flex:none;background:var(--dsw-alias-bg-layer-1);}
|
|
150
|
+
.dtok-pm.max .dtok-pm-head{cursor:default;}
|
|
151
|
+
.dtok-pm-head b{font-size:14px;}
|
|
152
|
+
.dtok-pm-sub{font-size:11px;color:var(--dsw-alias-label-secondary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
153
|
+
.dtok-pm-ctrls{display:flex;gap:6px;flex:none;}
|
|
154
|
+
.dtok-pm-body{flex:1;min-height:0;overflow-y:auto;padding:12px 14px;}
|
|
155
|
+
.dtok-pm-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;touch-action:none;}
|
|
156
|
+
.dtok-pm{position:relative;}
|
|
157
|
+
/* ---- 单价设置(表单内容) ---- */
|
|
158
|
+
.dtok-price{display:flex;flex-direction:column;gap:12px;}
|
|
159
|
+
.dtok-price-grid{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;}
|
|
160
|
+
.dtok-price-field{display:flex;flex-direction:column;gap:3px;}
|
|
161
|
+
.dtok-price-field label{font-size:11px;color:var(--dsw-alias-label-secondary);}
|
|
162
|
+
.dtok-price-num{width:92px;}
|
|
163
|
+
.dtok-price input[type="text"],.dtok-price-num{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 6px;font-size:12px;font-family:inherit;}
|
|
164
|
+
.dtok-price .dtok-btn.tiny{padding:2px 8px;font-size:11px;}
|
|
165
|
+
.dtok-price-msg{font-size:12px;}
|
|
166
|
+
.dtok-price-msg.ok{color:var(--dsw-alias-state-success-primary,#4ade80);}
|
|
167
|
+
.dtok-price-msg.err{color:var(--dsw-alias-state-error-primary,#ff7a7a);}
|
|
168
|
+
.dtok-price-hint{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.6;}
|
|
169
|
+
.dtok-prow{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;margin-bottom:8px;background:var(--dsw-alias-bg-layer-1);}
|
|
170
|
+
.dtok-prow-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px;}
|
|
171
|
+
.dtok-pseg{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
|
|
172
|
+
.dtok-pseg-name{font-size:11px;color:var(--dsw-alias-label-secondary);min-width:112px;}
|
|
173
|
+
.dtok-pseg-tilde{font-size:11px;color:var(--dsw-alias-label-secondary);}
|
|
174
|
+
.dtok-src{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);}
|
|
175
|
+
.dtok-src.custom{color:var(--dsw-alias-state-success-primary,#4ade80);}
|
|
176
|
+
.dtok-src.fallback{color:var(--dk-warn);}
|
|
144
177
|
`;
|
|
145
178
|
|
|
146
179
|
// ---------- 详情弹窗 ----------
|
|
@@ -156,15 +189,16 @@ function Detail({ rec, onClose, rate }) {
|
|
|
156
189
|
["错误信息", rec.errorMsg || "—"],
|
|
157
190
|
["错误码", rec.errorCode || "—"],
|
|
158
191
|
["Request ID", rec.requestId || "—"],
|
|
159
|
-
["输入 Token(
|
|
192
|
+
["输入 Token(缓存命中)", fmtNum(rec.cacheReadTokens)],
|
|
193
|
+
["输入 Token(缓存未命中)", fmtNum(rec.inputTokens)],
|
|
160
194
|
["输出 Token", fmtNum(rec.outputTokens)],
|
|
161
|
-
["缓存命中 Token", fmtNum(rec.cacheReadTokens)],
|
|
162
195
|
["缓存写入 Token", fmtNum(rec.cacheWriteTokens)],
|
|
163
196
|
["推理 Token", fmtNum(rec.reasoningTokens)],
|
|
164
197
|
["计费输入", fmtNum(rec.billedInput)],
|
|
165
198
|
["缓存命中率", rec.cacheHitPercent + "%"],
|
|
166
199
|
["总 Token", fmtNum(rec.totalTokens)],
|
|
167
200
|
["消耗金额(估算)", fmtCostCny(rec.cost, rateCny)],
|
|
201
|
+
["计价来源", rec.pricingSource ? ({ custom: "自定义单价", official: "官网同步价", builtin: "内置默认价", fallback: "兜底单价" }[rec.pricingSource] || rec.pricingSource) : "—"],
|
|
168
202
|
["推理强度", rec.effort || "—"],
|
|
169
203
|
["耗时", fmtDuration(rec.llmMs)],
|
|
170
204
|
["Turn / Step", rec.turn + " / " + rec.step],
|
|
@@ -185,6 +219,316 @@ function Detail({ rec, onClose, rate }) {
|
|
|
185
219
|
);
|
|
186
220
|
}
|
|
187
221
|
|
|
222
|
+
// ---------- 单价设置 ----------
|
|
223
|
+
// 官网抓取的刊例价常与实际计费不符(第三方中转/折扣/自建端点),这里让用户按模型填写
|
|
224
|
+
// 真实单价并持久化到宿主 settings;保存后费用即时按新价重算(无需重扫历史)。
|
|
225
|
+
// 独立子弹窗(可最大化 / 拖动 / 缩放),与功能坞面板解耦,方便配置较长时段表。
|
|
226
|
+
function numStr(v) { return v === undefined || v === null ? "" : String(v); }
|
|
227
|
+
|
|
228
|
+
/** 时段草稿:一个 { start,end,input,output,cacheRead,cacheWrite } 的可编辑副本。 */
|
|
229
|
+
function segToDraft(s) {
|
|
230
|
+
return {
|
|
231
|
+
start: numStr(s && s.start), end: numStr(s && s.end),
|
|
232
|
+
input: numStr(s && s.input), output: numStr(s && s.output),
|
|
233
|
+
cacheRead: numStr(s && s.cacheRead), cacheWrite: numStr(s && s.cacheWrite),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function rowToDraft(r) {
|
|
237
|
+
return {
|
|
238
|
+
match: r.match || "",
|
|
239
|
+
input: numStr(r.input), output: numStr(r.output),
|
|
240
|
+
cacheRead: numStr(r.cacheRead), cacheWrite: numStr(r.cacheWrite),
|
|
241
|
+
// 旧数据可能是单段 peak(宿主已归一为 peaks,这里再兼容一次直连旧宿主的情况)
|
|
242
|
+
peaks: (Array.isArray(r.peaks) && r.peaks.length ? r.peaks
|
|
243
|
+
: (r.peak && typeof r.peak === "object" ? [r.peak] : [])).map(segToDraft),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/** 默认新时段草稿:给一组常见值,减少手填。 */
|
|
247
|
+
function newSegDraft() { return segToDraft({ start: 9, end: 14, input: "", output: "", cacheRead: "", cacheWrite: "" }); }
|
|
248
|
+
|
|
249
|
+
// ---------- 单价设置子弹窗(独立于功能坞弹层的覆盖层,可最大化/拖动/缩放) ----------
|
|
250
|
+
// 挂在 document.body 上:功能坞弹层自身 z-index:200,子弹窗用更高层级;面板滚动不影响定位。
|
|
251
|
+
function PriceModal({ onClose, onSaved }) {
|
|
252
|
+
const [maxed, setMaxed] = useState(false);
|
|
253
|
+
const [geom, setGeom] = useState(null); // { x, y, w, h };null = CSS 默认居中
|
|
254
|
+
const dlgRef = useRef(null);
|
|
255
|
+
const dragRef = useRef(null);
|
|
256
|
+
|
|
257
|
+
// 拖动(标题栏)/ 缩放(右下角手柄):用 window 级 pointer 监听,指针移出元素也不丢
|
|
258
|
+
const startDrag = useCallback((e, kind) => {
|
|
259
|
+
if (e.button !== undefined && e.button !== 0) return;
|
|
260
|
+
if (kind === "move" && e.target && e.target.closest && e.target.closest("button,select,input,label,details,summary")) return;
|
|
261
|
+
const node = dlgRef.current;
|
|
262
|
+
if (!node) return;
|
|
263
|
+
const rect = node.getBoundingClientRect();
|
|
264
|
+
dragRef.current = { kind, startX: e.clientX, startY: e.clientY, left: rect.left, top: rect.top, w: rect.width, h: rect.height };
|
|
265
|
+
if (e.preventDefault) e.preventDefault();
|
|
266
|
+
}, []);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
const onMove = (ev) => {
|
|
269
|
+
const d = dragRef.current;
|
|
270
|
+
if (!d) return;
|
|
271
|
+
const dx = ev.clientX - d.startX, dy = ev.clientY - d.startY;
|
|
272
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
273
|
+
if (d.kind === "move") {
|
|
274
|
+
const x = Math.min(Math.max(0, d.left + dx), Math.max(0, vw - 80));
|
|
275
|
+
const y = Math.min(Math.max(0, d.top + dy), Math.max(0, vh - 40));
|
|
276
|
+
setGeom({ x: x, y: y, w: d.w, h: d.h });
|
|
277
|
+
} else {
|
|
278
|
+
const w = Math.max(560, Math.min(d.w + dx, vw - 16));
|
|
279
|
+
const h = Math.max(360, Math.min(d.h + dy, vh - 16));
|
|
280
|
+
setGeom({ x: d.left, y: d.top, w: w, h: h });
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const onUp = () => { dragRef.current = null; };
|
|
284
|
+
window.addEventListener("pointermove", onMove);
|
|
285
|
+
window.addEventListener("pointerup", onUp);
|
|
286
|
+
return () => {
|
|
287
|
+
window.removeEventListener("pointermove", onMove);
|
|
288
|
+
window.removeEventListener("pointerup", onUp);
|
|
289
|
+
};
|
|
290
|
+
}, []);
|
|
291
|
+
// Esc 关闭(子弹窗在弹层之上,先关自己)
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
const onKey = (e) => { if (e.key === "Escape") onClose(); };
|
|
294
|
+
window.addEventListener("keydown", onKey);
|
|
295
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
296
|
+
}, [onClose]);
|
|
297
|
+
|
|
298
|
+
const style = maxed
|
|
299
|
+
? { position: "fixed", left: 8, top: 8, right: 8, bottom: 8, width: "auto", height: "auto" }
|
|
300
|
+
: geom
|
|
301
|
+
? { position: "fixed", left: geom.x, top: geom.y, width: geom.w, height: geom.h }
|
|
302
|
+
: null;
|
|
303
|
+
// 挂到 document.body:功能坞弹层祖先带 backdrop-filter(会改变 fixed 定位的包含块),
|
|
304
|
+
// 设置页祖先也不可控;portal 到 body 才能保证覆盖整屏、不被裁剪。
|
|
305
|
+
// 无 document(测试沙箱)/无 createPortal 时退回就地渲染,行为不变。
|
|
306
|
+
const portalTarget = typeof document !== "undefined" && document.body ? document.body : null;
|
|
307
|
+
const overlay = (
|
|
308
|
+
<div className="dtok-pm-backdrop" onClick={onClose}>
|
|
309
|
+
<div ref={dlgRef} className={"dtok-pm" + (maxed ? " max" : "")} style={style} onClick={(e) => e.stopPropagation()}>
|
|
310
|
+
<div className="dtok-pm-head" onPointerDown={(e) => startDrag(e, "move")} onDoubleClick={() => setMaxed((v) => !v)}>
|
|
311
|
+
<b>单价设置</b>
|
|
312
|
+
<span className="dtok-pm-sub">人民币元 / 百万 tokens · 自定义单价优先于官网价与内置价</span>
|
|
313
|
+
<span className="dtok-pm-ctrls">
|
|
314
|
+
<button type="button" className="dtok-btn tiny" title={maxed ? "还原" : "最大化"} onClick={() => setMaxed((v) => !v)}>{maxed ? "❐" : "▢"}</button>
|
|
315
|
+
<button type="button" className="dtok-btn tiny" title="关闭" onClick={onClose}>✕</button>
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="dtok-pm-body">
|
|
319
|
+
<PricingEditor onClose={onClose} onSaved={onSaved} embedded />
|
|
320
|
+
</div>
|
|
321
|
+
{maxed ? null : <div className="dtok-pm-resize" title="拖动缩放" onPointerDown={(e) => startDrag(e, "resize")} />}
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
return portalTarget && typeof react.createPortal === "function"
|
|
326
|
+
? react.createPortal(overlay, portalTarget)
|
|
327
|
+
: overlay;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function PricingEditor({ onClose, onSaved, embedded }) {
|
|
331
|
+
const [cfg, setCfg] = useState(null);
|
|
332
|
+
const [rows, setRows] = useState([]);
|
|
333
|
+
const [rate, setRate] = useState("7.2");
|
|
334
|
+
const [fetchOn, setFetchOn] = useState(true);
|
|
335
|
+
const [fb, setFb] = useState({ input: "", output: "", cacheRead: "", cacheWrite: "" });
|
|
336
|
+
const [newMatch, setNewMatch] = useState("");
|
|
337
|
+
const [msg, setMsg] = useState(null);
|
|
338
|
+
const [saving, setSaving] = useState(false);
|
|
339
|
+
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
let cancel = false;
|
|
342
|
+
rpcCall("pricing", {})
|
|
343
|
+
.then((d) => {
|
|
344
|
+
if (cancel) return;
|
|
345
|
+
setCfg(d);
|
|
346
|
+
setRate(numStr(d.usdCnyRate));
|
|
347
|
+
setFetchOn(!!d.fetchOfficial);
|
|
348
|
+
setFb({
|
|
349
|
+
input: numStr(d.fallback && d.fallback.input), output: numStr(d.fallback && d.fallback.output),
|
|
350
|
+
cacheRead: numStr(d.fallback && d.fallback.cacheRead), cacheWrite: numStr(d.fallback && d.fallback.cacheWrite),
|
|
351
|
+
});
|
|
352
|
+
setRows((d.pricing || []).map(rowToDraft));
|
|
353
|
+
})
|
|
354
|
+
.catch((e) => { if (!cancel) setMsg({ ok: false, text: String((e && e.message) || e) }); });
|
|
355
|
+
return () => { cancel = true; };
|
|
356
|
+
}, []);
|
|
357
|
+
|
|
358
|
+
const patchRow = (i, patch) => setRows((rs) => rs.map((r, j) => (j === i ? Object.assign({}, r, patch) : r)));
|
|
359
|
+
const delRow = (i) => setRows((rs) => rs.filter((_, j) => j !== i));
|
|
360
|
+
// 时段增删改:都作用在 rows[i].peaks 数组上
|
|
361
|
+
const addSeg = (i) => setRows((rs) => rs.map((r, j) => (j === i ? Object.assign({}, r, { peaks: r.peaks.concat([newSegDraft()]) }) : r)));
|
|
362
|
+
const delSeg = (i, k) => setRows((rs) => rs.map((r, j) => (j === i ? Object.assign({}, r, { peaks: r.peaks.filter((_, x) => x !== k) }) : r)));
|
|
363
|
+
const patchSeg = (i, k, patch) => setRows((rs) => rs.map((r, j) => (j === i
|
|
364
|
+
? Object.assign({}, r, { peaks: r.peaks.map((s, x) => (x === k ? Object.assign({}, s, patch) : s)) }) : r)));
|
|
365
|
+
const addRow = (m) => {
|
|
366
|
+
const match = String(m === undefined ? newMatch : m).trim();
|
|
367
|
+
if (!match) return;
|
|
368
|
+
if (rows.some((r) => r.match.toLowerCase() === match.toLowerCase())) { setMsg({ ok: false, text: "「" + match + "」已存在" }); return; }
|
|
369
|
+
setRows((rs) => rs.concat([rowToDraft({ match: match, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 })]));
|
|
370
|
+
setNewMatch("");
|
|
371
|
+
setMsg(null);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const save = () => {
|
|
375
|
+
if (saving) return;
|
|
376
|
+
const payload = { usdCnyRate: Number(rate), fetchOfficial: fetchOn };
|
|
377
|
+
payload.pricing = rows.map((r) => {
|
|
378
|
+
const out = {
|
|
379
|
+
match: String(r.match || "").trim(),
|
|
380
|
+
input: Number(r.input || 0), output: Number(r.output || 0),
|
|
381
|
+
cacheRead: Number(r.cacheRead || 0), cacheWrite: Number(r.cacheWrite || 0),
|
|
382
|
+
};
|
|
383
|
+
const peaks = (r.peaks || []).map((s) => ({
|
|
384
|
+
start: Number(s.start || 0), end: Number(s.end || 0),
|
|
385
|
+
input: Number(s.input || 0), output: Number(s.output || 0),
|
|
386
|
+
cacheRead: Number(s.cacheRead || 0), cacheWrite: Number(s.cacheWrite || 0),
|
|
387
|
+
}));
|
|
388
|
+
if (peaks.length) out.peaks = peaks;
|
|
389
|
+
return out;
|
|
390
|
+
});
|
|
391
|
+
payload.fallback = {
|
|
392
|
+
input: Number(fb.input || 0), output: Number(fb.output || 0),
|
|
393
|
+
cacheRead: Number(fb.cacheRead || 0), cacheWrite: Number(fb.cacheWrite || 0),
|
|
394
|
+
};
|
|
395
|
+
setSaving(true); setMsg(null);
|
|
396
|
+
rpcCall("setpricing", payload)
|
|
397
|
+
.then((d) => {
|
|
398
|
+
setCfg(d);
|
|
399
|
+
setRows((d.pricing || []).map(rowToDraft));
|
|
400
|
+
setMsg({ ok: true, text: "单价已保存,费用已按新价即时重算" });
|
|
401
|
+
if (onSaved) onSaved();
|
|
402
|
+
})
|
|
403
|
+
.catch((e) => setMsg({ ok: false, text: String((e && e.message) || e) }))
|
|
404
|
+
.finally(() => setSaving(false));
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const configured = rows.map((r) => String(r.match).toLowerCase());
|
|
408
|
+
const candidates = ((cfg && cfg.models) || []).filter((m) => configured.indexOf(String(m).toLowerCase()) < 0);
|
|
409
|
+
const srcOf = (m) => (cfg && cfg.sourcesByModel && cfg.sourcesByModel[m]) || "";
|
|
410
|
+
// 哪些已用模型当前走兜底价——提示用户优先补这些
|
|
411
|
+
const onFallback = ((cfg && cfg.models) || []).filter((m) => srcOf(m) === "兜底");
|
|
412
|
+
const num = (value, onChange, extra) => (
|
|
413
|
+
<input className="dtok-price-num" type="number" min="0" step="0.01" value={value} onChange={(e) => onChange(e.target.value)} style={extra} />
|
|
414
|
+
);
|
|
415
|
+
// 时段价输入带占位提示(四个数值框从左到右含义不同,仅靠下方说明易填错)
|
|
416
|
+
const segNum = (value, onChange, placeholder) => (
|
|
417
|
+
<input className="dtok-price-num" type="number" min="0" step="0.01" value={value} placeholder={placeholder}
|
|
418
|
+
onChange={(e) => onChange(e.target.value)} style={{ width: 80 }} />
|
|
419
|
+
);
|
|
420
|
+
const segLabel = (s) => {
|
|
421
|
+
const a = numStr(s.start), b = numStr(s.end);
|
|
422
|
+
if (a === "" || b === "") return "时段";
|
|
423
|
+
return a + "~" + b + (Number(a) === Number(b) ? "(全天)" : Number(a) > Number(b) ? "(跨零点)" : "");
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<div className={embedded ? "dtok-price emb" : "dtok-price"} onClick={(e) => e.stopPropagation()}>
|
|
428
|
+
<div className="dtok-price-grid">
|
|
429
|
+
<div className="dtok-price-field">
|
|
430
|
+
<label>USD→CNY 汇率</label>
|
|
431
|
+
{num(rate, setRate)}
|
|
432
|
+
</div>
|
|
433
|
+
<label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary)" }}>
|
|
434
|
+
<input type="checkbox" checked={fetchOn} onChange={(e) => setFetchOn(e.target.checked)} />
|
|
435
|
+
官网价目自动同步(关闭后仅用自定义/内置价)
|
|
436
|
+
</label>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<div>
|
|
440
|
+
<div className="dtok-section-title" style={{ margin: "0 0 4px" }}>按模型配置单价({rows.length} 条)</div>
|
|
441
|
+
{onFallback.length > 0 ? (
|
|
442
|
+
<div className="dtok-price-hint">以下用过的模型当前走「兜底价」,建议优先补单价:{onFallback.join("、")}</div>
|
|
443
|
+
) : null}
|
|
444
|
+
{rows.length === 0 ? (
|
|
445
|
+
<div className="dtok-price-hint">尚未配置自定义单价,当前使用官网同步价 / 内置价 / 兜底价。</div>
|
|
446
|
+
) : rows.map((r, i) => (
|
|
447
|
+
<div className="dtok-prow" key={i}>
|
|
448
|
+
<div className="dtok-prow-head">
|
|
449
|
+
<input className="dtok-input" style={{ maxWidth: 240 }} type="text" value={r.match} placeholder="模型匹配(子串)" onChange={(e) => patchRow(i, { match: e.target.value })} />
|
|
450
|
+
<span className={"dtok-src " + (srcOf(r.match) === "自定义" ? "custom" : srcOf(r.match) === "兜底" ? "fallback" : "")}>当前来源:{srcOf(r.match) || "—"}</span>
|
|
451
|
+
<button className="dtok-btn tiny" onClick={() => delRow(i)}>删除模型</button>
|
|
452
|
+
</div>
|
|
453
|
+
<div className="dtok-price-grid">
|
|
454
|
+
<div className="dtok-price-field"><label>基准输入(缓存命中)</label>{num(r.cacheRead, (v) => patchRow(i, { cacheRead: v }))}</div>
|
|
455
|
+
<div className="dtok-price-field"><label>基准输入(缓存未命中)</label>{num(r.input, (v) => patchRow(i, { input: v }))}</div>
|
|
456
|
+
<div className="dtok-price-field"><label>基准输出</label>{num(r.output, (v) => patchRow(i, { output: v }))}</div>
|
|
457
|
+
<div className="dtok-price-field"><label>基准缓存写入</label>{num(r.cacheWrite, (v) => patchRow(i, { cacheWrite: v }))}</div>
|
|
458
|
+
</div>
|
|
459
|
+
<div className="dtok-price-hint" style={{ marginTop: 2 }}>
|
|
460
|
+
缓存写入=未命中缓存、本次新建缓存的那部分输入(Claude 等按溢价单独收建缓存费);DeepSeek 不收此项,填 0 即可。
|
|
461
|
+
</div>
|
|
462
|
+
<div className="dtok-price-hint" style={{ marginTop: 2 }}>
|
|
463
|
+
分时段价(可多段,命中哪段用哪段;时段外回落到基准价。start>end 表示跨零点,如 23~7;start=end 表示全天)
|
|
464
|
+
</div>
|
|
465
|
+
{r.peaks.length === 0 ? <div className="dtok-price-hint">(未设置分时段,按基准价计费)</div> : null}
|
|
466
|
+
{r.peaks.map((s, k) => (
|
|
467
|
+
<div className="dtok-pseg" key={k}>
|
|
468
|
+
<span className="dtok-pseg-name">{segLabel(s)}</span>
|
|
469
|
+
{num(s.start, (v) => patchSeg(i, k, { start: v }), { width: 56 })}
|
|
470
|
+
<span className="dtok-pseg-tilde">~</span>
|
|
471
|
+
{num(s.end, (v) => patchSeg(i, k, { end: v }), { width: 56 })}
|
|
472
|
+
<span className="dtok-pseg-tilde">时</span>
|
|
473
|
+
{segNum(s.cacheRead, (v) => patchSeg(i, k, { cacheRead: v }), "缓存命中")}
|
|
474
|
+
{segNum(s.input, (v) => patchSeg(i, k, { input: v }), "未命中")}
|
|
475
|
+
{segNum(s.output, (v) => patchSeg(i, k, { output: v }), "输出")}
|
|
476
|
+
{segNum(s.cacheWrite, (v) => patchSeg(i, k, { cacheWrite: v }), "写入")}
|
|
477
|
+
<button className="dtok-btn tiny" onClick={() => delSeg(i, k)}>删除时段</button>
|
|
478
|
+
</div>
|
|
479
|
+
))}
|
|
480
|
+
<div className="dtok-price-hint" style={{ opacity: .85 }}>时段价从左到右:输入(命中)/ 输入(未命中)/ 输出 / 缓存写入</div>
|
|
481
|
+
<button className="dtok-btn tiny" onClick={() => addSeg(i)}>+ 添加时段</button>
|
|
482
|
+
</div>
|
|
483
|
+
))}
|
|
484
|
+
<div className="dtok-price-grid" style={{ marginTop: 6 }}>
|
|
485
|
+
<div className="dtok-price-field" style={{ flex: "0 0 220px" }}>
|
|
486
|
+
<label>新增模型匹配</label>
|
|
487
|
+
<input className="dtok-input" style={{ maxWidth: 220 }} type="text" list="dtok-model-candidates" placeholder="如 deepseek-v4-flash 或某中转模型名" value={newMatch} onChange={(e) => setNewMatch(e.target.value)} />
|
|
488
|
+
<datalist id="dtok-model-candidates">{candidates.map((m) => <option key={m} value={m} />)}</datalist>
|
|
489
|
+
</div>
|
|
490
|
+
<button className="dtok-btn" onClick={() => addRow()}>+ 添加</button>
|
|
491
|
+
</div>
|
|
492
|
+
<div className="dtok-price-hint">提示:匹配是「模型名包含该子串」,可只写关键片段(如 <code>v4-flash</code>);越具体的条目建议放越前(当前按列表顺序命中)。</div>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
<div>
|
|
496
|
+
<div className="dtok-section-title" style={{ margin: "0 0 4px" }}>兜底单价(未匹配任何条目时使用)</div>
|
|
497
|
+
<div className="dtok-price-grid">
|
|
498
|
+
<div className="dtok-price-field"><label>输入(缓存命中)</label>{num(fb.cacheRead, (v) => setFb((s) => Object.assign({}, s, { cacheRead: v })))}</div>
|
|
499
|
+
<div className="dtok-price-field"><label>输入(缓存未命中)</label>{num(fb.input, (v) => setFb((s) => Object.assign({}, s, { input: v })))}</div>
|
|
500
|
+
<div className="dtok-price-field"><label>输出</label>{num(fb.output, (v) => setFb((s) => Object.assign({}, s, { output: v })))}</div>
|
|
501
|
+
<div className="dtok-price-field"><label>缓存写入</label>{num(fb.cacheWrite, (v) => setFb((s) => Object.assign({}, s, { cacheWrite: v })))}</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
{cfg && cfg.builtin && cfg.builtin.length > 0 ? (
|
|
506
|
+
<details>
|
|
507
|
+
<summary className="dtok-price-hint" style={{ cursor: "pointer" }}>查看内置默认单价({cfg.builtin.length} 条,自定义配置未命中时按此兜底)</summary>
|
|
508
|
+
<div className="dtok-table-wrap" style={{ marginTop: 6 }}>
|
|
509
|
+
<table className="dtok-table">
|
|
510
|
+
<thead><tr><th>匹配</th><th>输入(命中)</th><th>输入(未命中)</th><th>输出</th><th>缓存写入</th><th>分时段</th></tr></thead>
|
|
511
|
+
<tbody>
|
|
512
|
+
{cfg.builtin.map((b) => (
|
|
513
|
+
<tr key={b.match}>
|
|
514
|
+
<td>{b.match}</td><td>{b.cacheRead}</td><td>{b.input}</td><td>{b.output}</td><td>{b.cacheWrite}</td>
|
|
515
|
+
<td>{b.peaks && b.peaks.length ? b.peaks.map((s) => s.start + "~" + s.end).join("、") + "时" : "—"}</td>
|
|
516
|
+
</tr>
|
|
517
|
+
))}
|
|
518
|
+
</tbody>
|
|
519
|
+
</table>
|
|
520
|
+
</div>
|
|
521
|
+
</details>
|
|
522
|
+
) : null}
|
|
523
|
+
|
|
524
|
+
<div className="dtok-price-grid">
|
|
525
|
+
<button className="dtok-btn primary" disabled={saving} onClick={save}>{saving ? "保存中…" : "保存单价"}</button>
|
|
526
|
+
{msg ? <span className={"dtok-price-msg " + (msg.ok ? "ok" : "err")}>{msg.text}</span> : null}
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
188
532
|
// ---------- 主视图(嵌入 dock 面板内容区) ----------
|
|
189
533
|
export function TokenLogView(props) {
|
|
190
534
|
// props.params.sessionId(chips 点击带入):立即按该会话筛选并查询
|
|
@@ -206,6 +550,9 @@ export function TokenLogView(props) {
|
|
|
206
550
|
const [err, setErr] = useState("");
|
|
207
551
|
const [page, setPage] = useState(0);
|
|
208
552
|
const [detailRec, setDetailRec] = useState(null);
|
|
553
|
+
// 「单价设置」折叠面板:默认收起(费用不准确时才需要展开调整)
|
|
554
|
+
// 「单价设置」子弹窗:默认收起;支持经 params.openPricing 直接打开(深链/测试用)
|
|
555
|
+
const [showPricing, setShowPricing] = useState(() => !!(props && props.params && props.params.openPricing));
|
|
209
556
|
const pageSize = 100;
|
|
210
557
|
|
|
211
558
|
// 暂存筛选条件: 任一筛选变化即写入 localStorage, 下次打开恢复同样条件
|
|
@@ -333,15 +680,15 @@ export function TokenLogView(props) {
|
|
|
333
680
|
const pageSafe = Math.min(page, pageCount - 1);
|
|
334
681
|
const pageRows = sorted.slice(pageSafe * pageSize, (pageSafe + 1) * pageSize);
|
|
335
682
|
|
|
336
|
-
// USD→CNY 汇率: host 返回(默认 7.2, 可被 settings dsh-dock
|
|
683
|
+
// USD→CNY 汇率: host 返回(默认 7.2, 可被 settings dsh-dock.tokenlog.usdCnyRate 覆盖)。
|
|
337
684
|
// 注意: 必须在本文件所有引用它的表达式(cards/summaryRows/detailRows/Detail)之前声明。
|
|
338
685
|
const rateCny = (data && data.rateUsdCny) || 7.2;
|
|
339
686
|
|
|
340
687
|
const cards = totals ? [
|
|
341
688
|
{ v: fmtNum(totals.calls), l: "调用次数" },
|
|
342
689
|
{ v: fmtCompact(totals.totalTokens), l: "总 Token" },
|
|
343
|
-
{ v: fmtCompact(totals.
|
|
344
|
-
{ v: fmtCompact(totals.
|
|
690
|
+
{ v: fmtCompact(totals.cacheReadTokens), l: "输入(命中)" },
|
|
691
|
+
{ v: fmtCompact(totals.inputTokens), l: "输入(未命中)" },
|
|
345
692
|
{ v: totals.cacheHitPct + "%", l: "缓存命中率" },
|
|
346
693
|
{ v: fmtCompact(totals.outputTokens), l: "输出" },
|
|
347
694
|
{ v: fmtCostCny(totals.cost, rateCny), l: "消耗金额(估算·人民币)" },
|
|
@@ -358,8 +705,8 @@ export function TokenLogView(props) {
|
|
|
358
705
|
|
|
359
706
|
const summaryRows = (data && data.summary || []).map((r) => (
|
|
360
707
|
<tr key={r.key}>
|
|
361
|
-
<td>{r.key}</td><td>{fmtNum(r.calls)}</td><td>{fmtCompact(r.
|
|
362
|
-
<td>{fmtCompact(r.
|
|
708
|
+
<td>{r.key}</td><td>{fmtNum(r.calls)}</td><td>{fmtCompact(r.cacheReadTokens)}</td>
|
|
709
|
+
<td>{fmtCompact(r.inputTokens)}</td><td>{r.cacheHitPct + "%"}</td><td>{fmtCompact(r.outputTokens)}</td>
|
|
363
710
|
<td>{fmtCompact(r.totalTokens)}</td>
|
|
364
711
|
<td>{fmtCostCny(r.cost, rateCny)}</td>
|
|
365
712
|
<td>{fmtDuration(r.llmMs)}</td>
|
|
@@ -380,13 +727,16 @@ export function TokenLogView(props) {
|
|
|
380
727
|
<td><span className="dtok-sid" title="点击按此会话筛选" onClick={() => { setSessionId(r.sessionId); runQuery(); }}>{shortId(r.sessionId)}</span></td>
|
|
381
728
|
<td>{r.provider || "—"}</td>
|
|
382
729
|
<td>{r.model || "—"}</td>
|
|
383
|
-
<td>{fmtNum(r.inputTokens)}</td>
|
|
384
730
|
<td>{fmtNum(r.cacheReadTokens)}</td>
|
|
731
|
+
<td>{fmtNum(r.inputTokens)}</td>
|
|
385
732
|
<td>{r.cacheHitPercent + "%"}</td>
|
|
386
733
|
<td>{fmtNum(r.outputTokens)}</td>
|
|
387
734
|
<td>{fmtNum(r.reasoningTokens)}</td>
|
|
388
735
|
<td>{fmtNum(r.totalTokens)}</td>
|
|
389
|
-
<td
|
|
736
|
+
<td title={r.pricingSource ? "计价来源:" + ({ custom: "自定义单价", official: "官网同步价", builtin: "内置默认价", fallback: "兜底单价" }[r.pricingSource] || r.pricingSource) : ""}>
|
|
737
|
+
{fmtCostCny(r.cost, rateCny)}
|
|
738
|
+
{r.pricingSource === "fallback" ? <span className="dtok-src fallback">{" "}兜底</span> : null}
|
|
739
|
+
</td>
|
|
390
740
|
<td>{r.effort || "—"}</td>
|
|
391
741
|
<td>
|
|
392
742
|
<div><span className={"dtok-code " + st.cls}>{st.label}</span></div>
|
|
@@ -409,7 +759,7 @@ export function TokenLogView(props) {
|
|
|
409
759
|
<div key="sumtab" className="dtok-table-wrap">
|
|
410
760
|
<table className="dtok-table">
|
|
411
761
|
<thead><tr>
|
|
412
|
-
<th>维度</th><th>调用</th><th
|
|
762
|
+
<th>维度</th><th>调用</th><th>输入(命中)</th><th>输入(未命中)</th><th>命中率</th><th>输出</th><th>总Token</th><th>金额(人民币)</th><th>耗时</th>
|
|
413
763
|
</tr></thead>
|
|
414
764
|
<tbody>{summaryRows}</tbody>
|
|
415
765
|
</table>
|
|
@@ -441,7 +791,7 @@ export function TokenLogView(props) {
|
|
|
441
791
|
<table className="dtok-table">
|
|
442
792
|
<thead><tr>
|
|
443
793
|
{sortTh("时间", "time")}{sortTh("会话ID", "sessionId")}{sortTh("提供商", "provider")}{sortTh("模型", "model")}
|
|
444
|
-
{sortTh("输入", "
|
|
794
|
+
{sortTh("输入(命中)", "cacheReadTokens")}{sortTh("输入(未命中)", "inputTokens")}{sortTh("命中%", "cacheHitPercent")}{sortTh("输出", "outputTokens")}
|
|
445
795
|
{sortTh("推理", "reasoningTokens")}{sortTh("总额", "totalTokens")}{sortTh("金额(人民币)", "cost")}{sortTh("强度", "effort")}
|
|
446
796
|
{sortTh("状态", "status")}{sortTh("耗时", "llmMs")}
|
|
447
797
|
</tr></thead>
|
|
@@ -486,7 +836,18 @@ export function TokenLogView(props) {
|
|
|
486
836
|
<button className="dtok-btn primary" onClick={runQuery}>查询</button>
|
|
487
837
|
<button className="dtok-btn" onClick={resetFilters}>重置</button>
|
|
488
838
|
<button className="dtok-btn" onClick={exportCsv}>导出 CSV</button>
|
|
839
|
+
<button className={"dtok-btn" + (showPricing ? " primary" : "")} onClick={() => setShowPricing(true)} title="配置各模型单价,费用按自填单价计算">单价设置</button>
|
|
489
840
|
</div>
|
|
841
|
+
{showPricing ? <PriceModal onClose={() => setShowPricing(false)} onSaved={runQuery} /> : null}
|
|
842
|
+
<div className="dtok-status">
|
|
843
|
+
{data && data.pricingInfo ? (
|
|
844
|
+
<span>
|
|
845
|
+
计价:{data.pricingInfo.hasCustom ? "自定义单价 " + data.pricingInfo.customCount + " 条" : "未配置自定义单价"}
|
|
846
|
+
{" · 官网自动同步:" + (data.pricingInfo.fetchOfficial ? "开" : "关")}
|
|
847
|
+
{data.pricingInfo.sources && data.pricingInfo.sources.length ? " · 本页涉及来源:" + data.pricingInfo.sources.join("/") : ""}
|
|
848
|
+
</span>
|
|
849
|
+
) : null}
|
|
850
|
+
</div>
|
|
490
851
|
<div className="dtok-body">{bodyNodes}</div>
|
|
491
852
|
{detailRec ? <Detail rec={detailRec} onClose={() => setDetailRec(null)} rate={rateCny} /> : null}
|
|
492
853
|
</div>
|
|
@@ -553,7 +914,7 @@ export const feature = {
|
|
|
553
914
|
name: "用量记录",
|
|
554
915
|
order: 110,
|
|
555
916
|
accent: "#fbbf24",
|
|
556
|
-
description: "记录全部 LLM API 调用:秒级时间筛选、Token
|
|
917
|
+
description: "记录全部 LLM API 调用:秒级时间筛选、Token/费用统计(可配置各模型单价,持久保存并按自填单价计费;内置峰谷计价+官网价目自动同步作兜底)、分组汇总、明细检索与 CSV 导出",
|
|
557
918
|
css,
|
|
558
919
|
View: TokenLogView,
|
|
559
920
|
HomeStat: TokenLogHomeStat,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价与多段分时价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build:client": "node scripts/build-client.mjs",
|
|
45
45
|
"test:client": "node scripts/test-client-views.mjs",
|
|
46
|
-
"test:host": "node scripts/test-task-host.mjs"
|
|
46
|
+
"test:host": "node scripts/test-task-host.mjs && node scripts/test-tokenlog-host.mjs"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
|
49
49
|
"type": "git",
|
package/src/client.jsx
CHANGED
|
@@ -30,7 +30,7 @@ import { feature as fGames } from "../features/games/view.jsx";
|
|
|
30
30
|
import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
|
|
31
31
|
|
|
32
32
|
const name = "dsh-dock";
|
|
33
|
-
const DOCK_VERSION = "0.
|
|
33
|
+
const DOCK_VERSION = "0.11.1";
|
|
34
34
|
|
|
35
35
|
// ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
|
|
36
36
|
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay];
|
|
@@ -547,7 +547,8 @@ function HomeView(props) {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
// ---- 设置页管理面板(settings.section):所有功能模块卡片一览 ----
|
|
550
|
-
|
|
550
|
+
// props.params 由插槽透传(设置页链接可能带 deep-link 参数,如用量记录的 openPricing)。
|
|
551
|
+
function DockPanel(props) {
|
|
551
552
|
const ctx = ctxRef.current;
|
|
552
553
|
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
553
554
|
useExternalVersion();
|
|
@@ -565,7 +566,7 @@ function DockPanel() {
|
|
|
565
566
|
const viewNode = (!f.planned && st.enabled && View)
|
|
566
567
|
? react.createElement("div", { className: "dock-body" },
|
|
567
568
|
react.createElement(FeatureBoundary, { key: f.id, label: f.name },
|
|
568
|
-
react.createElement(View, { ctx: ctx, feature: f })))
|
|
569
|
+
react.createElement(View, { ctx: ctx, feature: f, params: props && props.params })))
|
|
569
570
|
: null;
|
|
570
571
|
return react.createElement("div", { className: "dock-card", key: f.id },
|
|
571
572
|
react.createElement("div", { className: "dock-card-head" },
|
|
@@ -662,7 +663,7 @@ export function apply(ctx) {
|
|
|
662
663
|
() => react.createElement(FeatureOverlays, null)));
|
|
663
664
|
slots.inject("settings.section", () => slots.register(
|
|
664
665
|
{ name: "settings.section", id: "dsh-dock", order: 90, label: "功能坞" },
|
|
665
|
-
() => react.createElement(DockPanel,
|
|
666
|
+
(props) => react.createElement(DockPanel, props)));
|
|
666
667
|
// 会话输入卡工具行左端(模型选择器左侧):已启用功能的随身小控件
|
|
667
668
|
slots.inject("conversation.input.left", () => slots.register(
|
|
668
669
|
{ name: "conversation.input.left", id: "dsh-dock-chips", order: 10, label: "功能坞" },
|
package/src/host-core.js
CHANGED
|
@@ -22,6 +22,44 @@ export const DockConfig = z.object({
|
|
|
22
22
|
provider: z.string().default(''),
|
|
23
23
|
model: z.string().default(''),
|
|
24
24
|
}).default({}),
|
|
25
|
+
// 【用量记录】模块的单价配置:官网抓取的刊例价常与实际计费不符(第三方/中转、
|
|
26
|
+
// 折扣、自建端点等),这里保存用户自填单价并优先用于费用估算。
|
|
27
|
+
// 详见 features/tokenlog/host.js 的 resolvePricing(优先级:自定义 > 官网 > 内置 > 兜底)。
|
|
28
|
+
tokenlog: z.object({
|
|
29
|
+
/** USD→CNY 汇率(金额内部以 USD 记录,界面按人民币展示)。 */
|
|
30
|
+
usdCnyRate: z.number().default(7.2),
|
|
31
|
+
/** 是否仍抓取官网刊例价,作为「自定义单价」未命中时的兜底。 */
|
|
32
|
+
fetchOfficial: z.boolean().default(true),
|
|
33
|
+
/** 官网价目地址(仅 http/https,且拒绝本机/内网/保留地址)。 */
|
|
34
|
+
pricingUrl: z.string().default('https://api-docs.deepseek.com/zh-cn/quick_start/pricing'),
|
|
35
|
+
/** 官网价目抓取间隔(小时)。 */
|
|
36
|
+
pricingFetchIntervalHours: z.number().default(24),
|
|
37
|
+
/** 用户自定义单价(人民币元/百万 tokens):命中即用、优先级最高,按数组顺序匹配。 */
|
|
38
|
+
pricing: z.array(z.object({
|
|
39
|
+
match: z.string().default(''),
|
|
40
|
+
input: z.number().default(0),
|
|
41
|
+
output: z.number().default(0),
|
|
42
|
+
cacheRead: z.number().default(0),
|
|
43
|
+
cacheWrite: z.number().default(0),
|
|
44
|
+
/** 分时段价(可多段,如 DeepSeek 的高峰/优惠时段):命中某段用该段价,否则用上面的基准价。
|
|
45
|
+
* start/end 为本地小时数 0~24;start>end 表示跨零点;单段等价旧的 peak。 */
|
|
46
|
+
peaks: z.array(z.object({
|
|
47
|
+
start: z.number().default(0),
|
|
48
|
+
end: z.number().default(0),
|
|
49
|
+
input: z.number().default(0),
|
|
50
|
+
output: z.number().default(0),
|
|
51
|
+
cacheRead: z.number().default(0),
|
|
52
|
+
cacheWrite: z.number().default(0),
|
|
53
|
+
})).default([]),
|
|
54
|
+
})).default([]),
|
|
55
|
+
/** 未匹配任何条目的模型所用兜底单价。 */
|
|
56
|
+
fallback: z.object({
|
|
57
|
+
input: z.number().default(2.16),
|
|
58
|
+
output: z.number().default(6.48),
|
|
59
|
+
cacheRead: z.number().default(0.43),
|
|
60
|
+
cacheWrite: z.number().default(4.32),
|
|
61
|
+
}).default({}),
|
|
62
|
+
}).default({}),
|
|
25
63
|
// 【任务动画】模块:只放动画本身(动效开关/模式/桌面伙伴大小)。
|
|
26
64
|
// 通知相关的字段已全部迁往下面的 notify 段(见 migrateNotifyConfig);
|
|
27
65
|
// 旧值不在 schema 里也会被 schemastery 原样保留,迁移读得到。
|