@kenz1117/dsh-ui-usage-billing 1.2.6 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +5 -5
- package/lib/index.js +354 -72
- package/lib/types/aggregate.d.ts +60 -4
- package/lib/types/client/TokenPanel.d.ts +2 -0
- package/lib/types/client/UsageBilling.d.ts +39 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/usage-billing-settings.d.ts +17 -5
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { mkdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, readFile, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
|
|
@@ -305,7 +305,7 @@ function tierAt(timeMs) {
|
|
|
305
305
|
}
|
|
306
306
|
/** 时刻是否落在北京时间周末(周六/周日)。 */
|
|
307
307
|
function isBeijingWeekend(timeMs) {
|
|
308
|
-
const day = new Date(timeMs +
|
|
308
|
+
const day = new Date(timeMs + 8 * 36e5).getUTCDay();
|
|
309
309
|
return day === 0 || day === 6;
|
|
310
310
|
}
|
|
311
311
|
/**
|
|
@@ -331,7 +331,7 @@ const MODEL_CATALOG = [
|
|
|
331
331
|
key: "flash",
|
|
332
332
|
name: "DeepSeek V4.1 Flash",
|
|
333
333
|
provider: "DeepSeek",
|
|
334
|
-
colorVar: "
|
|
334
|
+
colorVar: "ds-blue",
|
|
335
335
|
price: {
|
|
336
336
|
currency: "CNY",
|
|
337
337
|
input: 2,
|
|
@@ -349,7 +349,7 @@ const MODEL_CATALOG = [
|
|
|
349
349
|
key: "flash-vision-exp",
|
|
350
350
|
name: "DeepSeek V4 Flash Vision (Exp)",
|
|
351
351
|
provider: "DeepSeek",
|
|
352
|
-
colorVar: "
|
|
352
|
+
colorVar: "ds-blue",
|
|
353
353
|
retired: true,
|
|
354
354
|
price: {
|
|
355
355
|
currency: "CNY",
|
|
@@ -398,7 +398,7 @@ const MODEL_CATALOG = [
|
|
|
398
398
|
key: "glm-5.3",
|
|
399
399
|
name: "GLM-5.3",
|
|
400
400
|
provider: "智谱 AI",
|
|
401
|
-
colorVar: "
|
|
401
|
+
colorVar: "ds-blue",
|
|
402
402
|
price: {
|
|
403
403
|
currency: "CNY",
|
|
404
404
|
input: 8,
|
|
@@ -464,7 +464,7 @@ const MODEL_CATALOG = [
|
|
|
464
464
|
key: "glm-5-turbo",
|
|
465
465
|
name: "GLM-5-Turbo",
|
|
466
466
|
provider: "智谱 AI",
|
|
467
|
-
colorVar: "
|
|
467
|
+
colorVar: "ds-blue",
|
|
468
468
|
price: {
|
|
469
469
|
currency: "CNY",
|
|
470
470
|
input: 5,
|
|
@@ -606,7 +606,7 @@ const MODEL_CATALOG = [
|
|
|
606
606
|
key: "qwen-plus",
|
|
607
607
|
name: "Qwen3.5-Plus",
|
|
608
608
|
provider: "阿里通义",
|
|
609
|
-
colorVar: "
|
|
609
|
+
colorVar: "ds-blue",
|
|
610
610
|
price: {
|
|
611
611
|
currency: "CNY",
|
|
612
612
|
input: .8,
|
|
@@ -659,7 +659,7 @@ const MODEL_CATALOG = [
|
|
|
659
659
|
key: "doubao",
|
|
660
660
|
name: "Doubao Seed-2.0 Pro",
|
|
661
661
|
provider: "字节豆包",
|
|
662
|
-
colorVar: "
|
|
662
|
+
colorVar: "ds-red",
|
|
663
663
|
price: {
|
|
664
664
|
currency: "CNY",
|
|
665
665
|
input: 3.2,
|
|
@@ -695,7 +695,7 @@ const MODEL_CATALOG = [
|
|
|
695
695
|
key: "doubao-seed-evolving",
|
|
696
696
|
name: "Doubao-Seed-Evolving",
|
|
697
697
|
provider: "字节豆包",
|
|
698
|
-
colorVar: "
|
|
698
|
+
colorVar: "ds-red",
|
|
699
699
|
price: {
|
|
700
700
|
currency: "CNY",
|
|
701
701
|
input: 6,
|
|
@@ -803,7 +803,7 @@ const MODEL_CATALOG = [
|
|
|
803
803
|
key: "minimax",
|
|
804
804
|
name: "MiniMax-M3",
|
|
805
805
|
provider: "MiniMax",
|
|
806
|
-
colorVar: "
|
|
806
|
+
colorVar: "ds-amber",
|
|
807
807
|
price: {
|
|
808
808
|
currency: "CNY",
|
|
809
809
|
input: 2.1,
|
|
@@ -827,7 +827,7 @@ const MODEL_CATALOG = [
|
|
|
827
827
|
key: "minimax-m2.7-highspeed",
|
|
828
828
|
name: "MiniMax-M2.7-highspeed",
|
|
829
829
|
provider: "MiniMax",
|
|
830
|
-
colorVar: "
|
|
830
|
+
colorVar: "ds-amber",
|
|
831
831
|
price: {
|
|
832
832
|
currency: "CNY",
|
|
833
833
|
input: 4.2,
|
|
@@ -875,7 +875,7 @@ const MODEL_CATALOG = [
|
|
|
875
875
|
key: "yi",
|
|
876
876
|
name: "Yi-Lightning",
|
|
877
877
|
provider: "零一万物",
|
|
878
|
-
colorVar: "
|
|
878
|
+
colorVar: "ds-green",
|
|
879
879
|
price: {
|
|
880
880
|
currency: "CNY",
|
|
881
881
|
input: .99,
|
|
@@ -937,7 +937,7 @@ const MODEL_CATALOG = [
|
|
|
937
937
|
key: "gpt-6-astra",
|
|
938
938
|
name: "GPT-6 Astra",
|
|
939
939
|
provider: "OpenAI",
|
|
940
|
-
colorVar: "
|
|
940
|
+
colorVar: "ds-green",
|
|
941
941
|
price: {
|
|
942
942
|
currency: "USD",
|
|
943
943
|
input: 10,
|
|
@@ -949,7 +949,7 @@ const MODEL_CATALOG = [
|
|
|
949
949
|
key: "gpt-5.6-sol",
|
|
950
950
|
name: "GPT-5.6 Sol",
|
|
951
951
|
provider: "OpenAI",
|
|
952
|
-
colorVar: "
|
|
952
|
+
colorVar: "ds-green",
|
|
953
953
|
price: {
|
|
954
954
|
currency: "USD",
|
|
955
955
|
input: 5,
|
|
@@ -979,7 +979,7 @@ const MODEL_CATALOG = [
|
|
|
979
979
|
key: "gpt-5.6-luna",
|
|
980
980
|
name: "GPT-5.6 Luna",
|
|
981
981
|
provider: "OpenAI",
|
|
982
|
-
colorVar: "
|
|
982
|
+
colorVar: "ds-green",
|
|
983
983
|
price: {
|
|
984
984
|
currency: "USD",
|
|
985
985
|
input: .2,
|
|
@@ -1053,7 +1053,7 @@ const MODEL_CATALOG = [
|
|
|
1053
1053
|
key: "llama",
|
|
1054
1054
|
name: "Llama 4 Maverick",
|
|
1055
1055
|
provider: "Meta",
|
|
1056
|
-
colorVar: "
|
|
1056
|
+
colorVar: "ds-red",
|
|
1057
1057
|
price: {
|
|
1058
1058
|
currency: "USD",
|
|
1059
1059
|
input: .2,
|
|
@@ -1077,7 +1077,7 @@ const MODEL_CATALOG = [
|
|
|
1077
1077
|
key: "claude-opus-4-6",
|
|
1078
1078
|
name: "Claude Opus 4.6",
|
|
1079
1079
|
provider: "Anthropic",
|
|
1080
|
-
colorVar: "
|
|
1080
|
+
colorVar: "ds-red",
|
|
1081
1081
|
price: {
|
|
1082
1082
|
currency: "USD",
|
|
1083
1083
|
input: 5,
|
|
@@ -1113,7 +1113,7 @@ const MODEL_CATALOG = [
|
|
|
1113
1113
|
key: "claude-opus-5",
|
|
1114
1114
|
name: "Claude Opus 5",
|
|
1115
1115
|
provider: "Anthropic",
|
|
1116
|
-
colorVar: "
|
|
1116
|
+
colorVar: "ds-red",
|
|
1117
1117
|
price: {
|
|
1118
1118
|
currency: "USD",
|
|
1119
1119
|
input: 5,
|
|
@@ -1137,7 +1137,7 @@ const MODEL_CATALOG = [
|
|
|
1137
1137
|
key: "mistral-large-2512",
|
|
1138
1138
|
name: "Mistral Large 3",
|
|
1139
1139
|
provider: "Mistral AI",
|
|
1140
|
-
colorVar: "
|
|
1140
|
+
colorVar: "ds-violet",
|
|
1141
1141
|
price: {
|
|
1142
1142
|
currency: "USD",
|
|
1143
1143
|
input: .5,
|
|
@@ -1173,7 +1173,7 @@ const MODEL_CATALOG = [
|
|
|
1173
1173
|
key: "command-a-03-2025",
|
|
1174
1174
|
name: "Command A",
|
|
1175
1175
|
provider: "Cohere",
|
|
1176
|
-
colorVar: "
|
|
1176
|
+
colorVar: "ds-cyan",
|
|
1177
1177
|
price: {
|
|
1178
1178
|
currency: "USD",
|
|
1179
1179
|
input: 2.5,
|
|
@@ -1197,7 +1197,7 @@ const MODEL_CATALOG = [
|
|
|
1197
1197
|
key: "longcat-2.0",
|
|
1198
1198
|
name: "LongCat 2.0",
|
|
1199
1199
|
provider: "美团",
|
|
1200
|
-
colorVar: "
|
|
1200
|
+
colorVar: "ds-amber",
|
|
1201
1201
|
price: {
|
|
1202
1202
|
currency: "CNY",
|
|
1203
1203
|
input: 4,
|
|
@@ -1210,7 +1210,7 @@ const MODEL_CATALOG = [
|
|
|
1210
1210
|
key: "minicpm-v-4.5",
|
|
1211
1211
|
name: "MiniCPM-V 4.5",
|
|
1212
1212
|
provider: "面壁智能",
|
|
1213
|
-
colorVar: "
|
|
1213
|
+
colorVar: "ds-green",
|
|
1214
1214
|
price: {
|
|
1215
1215
|
currency: "CNY",
|
|
1216
1216
|
input: 1,
|
|
@@ -1236,7 +1236,7 @@ const MODEL_CATALOG = [
|
|
|
1236
1236
|
key: "dots-3-note-preview",
|
|
1237
1237
|
name: "Dots3-Note Preview",
|
|
1238
1238
|
provider: "小红书",
|
|
1239
|
-
colorVar: "
|
|
1239
|
+
colorVar: "ds-red",
|
|
1240
1240
|
price: {
|
|
1241
1241
|
currency: "CNY",
|
|
1242
1242
|
input: 2,
|
|
@@ -1403,7 +1403,7 @@ const MODEL_CATALOG = [
|
|
|
1403
1403
|
key: "doubao-seed-2.0-code",
|
|
1404
1404
|
name: "Doubao Seed-2.0 Code",
|
|
1405
1405
|
provider: "字节豆包",
|
|
1406
|
-
colorVar: "
|
|
1406
|
+
colorVar: "ds-red",
|
|
1407
1407
|
price: {
|
|
1408
1408
|
currency: "CNY",
|
|
1409
1409
|
input: 3.2,
|
|
@@ -2047,9 +2047,151 @@ function workspaceNameOf(cwd) {
|
|
|
2047
2047
|
}
|
|
2048
2048
|
/** TTFT 尖峰阈值(毫秒):超过计为一次尖峰样本,用于定位服务端抖动。 */
|
|
2049
2049
|
const PERF_SPIKE_MS = 1e4;
|
|
2050
|
+
/** 全零摘要(合并的恒等元)。 */
|
|
2051
|
+
const EMPTY_MODEL_DIGEST = {
|
|
2052
|
+
samples: 0,
|
|
2053
|
+
ttftSum: 0,
|
|
2054
|
+
ttftMax: 0,
|
|
2055
|
+
ttftSpikes: 0,
|
|
2056
|
+
ttftP50: 0,
|
|
2057
|
+
ttftP90: 0,
|
|
2058
|
+
tpsCount: 0,
|
|
2059
|
+
tpsSum: 0,
|
|
2060
|
+
latencyCount: 0,
|
|
2061
|
+
latencySum: 0,
|
|
2062
|
+
estimated: 0
|
|
2063
|
+
};
|
|
2064
|
+
/** 从样本集构建模型级摘要(样本侧精确:P50/P90 为精确插值)。 */
|
|
2065
|
+
function modelDigestOf(samples) {
|
|
2066
|
+
const ttfts = [];
|
|
2067
|
+
let ttftSum = 0;
|
|
2068
|
+
let ttftMax = 0;
|
|
2069
|
+
let ttftSpikes = 0;
|
|
2070
|
+
let tpsCount = 0;
|
|
2071
|
+
let tpsSum = 0;
|
|
2072
|
+
let latencyCount = 0;
|
|
2073
|
+
let latencySum = 0;
|
|
2074
|
+
let estimated = 0;
|
|
2075
|
+
for (const sample of samples) {
|
|
2076
|
+
ttfts.push(sample.ttftMs);
|
|
2077
|
+
ttftSum += sample.ttftMs;
|
|
2078
|
+
if (sample.ttftMs > ttftMax) ttftMax = sample.ttftMs;
|
|
2079
|
+
if (sample.ttftMs > 1e4) ttftSpikes += 1;
|
|
2080
|
+
if (sample.tps !== void 0) {
|
|
2081
|
+
tpsCount += 1;
|
|
2082
|
+
tpsSum += sample.tps;
|
|
2083
|
+
}
|
|
2084
|
+
if (sample.latencyMs !== void 0) {
|
|
2085
|
+
latencyCount += 1;
|
|
2086
|
+
latencySum += sample.latencyMs;
|
|
2087
|
+
}
|
|
2088
|
+
if (sample.estimated) estimated += 1;
|
|
2089
|
+
}
|
|
2090
|
+
return {
|
|
2091
|
+
samples: samples.length,
|
|
2092
|
+
ttftSum,
|
|
2093
|
+
ttftMax,
|
|
2094
|
+
ttftSpikes,
|
|
2095
|
+
ttftP50: percentile(ttfts, .5),
|
|
2096
|
+
ttftP90: percentile(ttfts, .9),
|
|
2097
|
+
tpsCount,
|
|
2098
|
+
tpsSum,
|
|
2099
|
+
latencyCount,
|
|
2100
|
+
latencySum,
|
|
2101
|
+
estimated
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
/** 合并两个模型级摘要:计数/求和/极值精确;分位数按样本数加权。 */
|
|
2105
|
+
function mergeModelDigest(a, b) {
|
|
2106
|
+
const n = a.samples + b.samples;
|
|
2107
|
+
const weighted = (x, y) => n === 0 ? 0 : (x * a.samples + y * b.samples) / n;
|
|
2108
|
+
return {
|
|
2109
|
+
samples: n,
|
|
2110
|
+
ttftSum: a.ttftSum + b.ttftSum,
|
|
2111
|
+
ttftMax: Math.max(a.ttftMax, b.ttftMax),
|
|
2112
|
+
ttftSpikes: a.ttftSpikes + b.ttftSpikes,
|
|
2113
|
+
ttftP50: weighted(a.ttftP50, b.ttftP50),
|
|
2114
|
+
ttftP90: weighted(a.ttftP90, b.ttftP90),
|
|
2115
|
+
tpsCount: a.tpsCount + b.tpsCount,
|
|
2116
|
+
tpsSum: a.tpsSum + b.tpsSum,
|
|
2117
|
+
latencyCount: a.latencyCount + b.latencyCount,
|
|
2118
|
+
latencySum: a.latencySum + b.latencySum,
|
|
2119
|
+
estimated: a.estimated + b.estimated
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
/** 从样本集构建小时级摘要(均值可无损重建)。 */
|
|
2123
|
+
function hourDigestOf(samples) {
|
|
2124
|
+
let ttftSum = 0;
|
|
2125
|
+
let tpsCount = 0;
|
|
2126
|
+
let tpsSum = 0;
|
|
2127
|
+
for (const sample of samples) {
|
|
2128
|
+
ttftSum += sample.ttftMs;
|
|
2129
|
+
if (sample.tps !== void 0) {
|
|
2130
|
+
tpsCount += 1;
|
|
2131
|
+
tpsSum += sample.tps;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return {
|
|
2135
|
+
samples: samples.length,
|
|
2136
|
+
ttftSum,
|
|
2137
|
+
tpsCount,
|
|
2138
|
+
tpsSum
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
/** 全零小时摘要(合并的恒等元)。 */
|
|
2142
|
+
const EMPTY_HOUR_DIGEST = {
|
|
2143
|
+
samples: 0,
|
|
2144
|
+
ttftSum: 0,
|
|
2145
|
+
tpsCount: 0,
|
|
2146
|
+
tpsSum: 0
|
|
2147
|
+
};
|
|
2148
|
+
/** 合并两个小时级摘要(纯字段加法,无损)。 */
|
|
2149
|
+
function mergeHourDigest(a, b) {
|
|
2150
|
+
return {
|
|
2151
|
+
samples: a.samples + b.samples,
|
|
2152
|
+
ttftSum: a.ttftSum + b.ttftSum,
|
|
2153
|
+
tpsCount: a.tpsCount + b.tpsCount,
|
|
2154
|
+
tpsSum: a.tpsSum + b.tpsSum
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* 样本集 → 账本摘要形态(模型 → 摘要;小时 → 模型 → 摘要)。serializeFold
|
|
2159
|
+
* 与账本迁移共用同一转换,两处口径不会漂移。
|
|
2160
|
+
* @param samples - 一个会话的全部性能样本。
|
|
2161
|
+
* @returns `{ byModel, byHourModel }` 的 JSON-safe 记录。
|
|
2162
|
+
*/
|
|
2163
|
+
function perfDigestsOf(samples) {
|
|
2164
|
+
const byModelSamples = /* @__PURE__ */ new Map();
|
|
2165
|
+
const byHourModelSamples = /* @__PURE__ */ new Map();
|
|
2166
|
+
for (const sample of samples) {
|
|
2167
|
+
let modelSamples = byModelSamples.get(sample.model);
|
|
2168
|
+
if (modelSamples === void 0) {
|
|
2169
|
+
modelSamples = [];
|
|
2170
|
+
byModelSamples.set(sample.model, modelSamples);
|
|
2171
|
+
}
|
|
2172
|
+
modelSamples.push(sample);
|
|
2173
|
+
let hourModels = byHourModelSamples.get(sample.hour);
|
|
2174
|
+
if (hourModels === void 0) {
|
|
2175
|
+
hourModels = /* @__PURE__ */ new Map();
|
|
2176
|
+
byHourModelSamples.set(sample.hour, hourModels);
|
|
2177
|
+
}
|
|
2178
|
+
let hourModelSamples = hourModels.get(sample.model);
|
|
2179
|
+
if (hourModelSamples === void 0) {
|
|
2180
|
+
hourModelSamples = [];
|
|
2181
|
+
hourModels.set(sample.model, hourModelSamples);
|
|
2182
|
+
}
|
|
2183
|
+
hourModelSamples.push(sample);
|
|
2184
|
+
}
|
|
2185
|
+
return {
|
|
2186
|
+
byModel: Object.fromEntries([...byModelSamples].map(([model, list]) => [model, modelDigestOf(list)])),
|
|
2187
|
+
byHourModel: Object.fromEntries([...byHourModelSamples].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, list]) => [model, hourDigestOf(list)]))]))
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2050
2190
|
/**
|
|
2051
2191
|
* 账本迁移注册表。首条迁移给 1.0.6 及更早的行回填 foldVersion = 1(它们全部出自
|
|
2052
|
-
* header
|
|
2192
|
+
* header 归因算法);perf 摘要化迁移把 v15 及更早行的逐样本 `perf` 数组转换为
|
|
2193
|
+
* `perfDigest`/`perfHourDigest` 摘要(日志已删的行不再被重折,迁移是它们唯一的
|
|
2194
|
+
* 收缩机会);此后新写入的行总带当前 {@link FOLD_VERSION} 与摘要形态。
|
|
2053
2195
|
*/
|
|
2054
2196
|
const LEDGER_MIGRATIONS = [{
|
|
2055
2197
|
id: "fold-version-backfill",
|
|
@@ -2061,6 +2203,26 @@ const LEDGER_MIGRATIONS = [{
|
|
|
2061
2203
|
}
|
|
2062
2204
|
return changed;
|
|
2063
2205
|
}
|
|
2206
|
+
}, {
|
|
2207
|
+
id: "perf-sample-to-digest",
|
|
2208
|
+
apply(document) {
|
|
2209
|
+
let changed = false;
|
|
2210
|
+
for (const session of document.sessions) {
|
|
2211
|
+
const fold = session.fold;
|
|
2212
|
+
if (Array.isArray(fold.perf)) {
|
|
2213
|
+
const { byModel, byHourModel } = perfDigestsOf(fold.perf);
|
|
2214
|
+
fold.perfDigest = byModel;
|
|
2215
|
+
fold.perfHourDigest = byHourModel;
|
|
2216
|
+
delete fold.perf;
|
|
2217
|
+
changed = true;
|
|
2218
|
+
} else if (fold.perfDigest === void 0 || fold.perfHourDigest === void 0) {
|
|
2219
|
+
fold.perfDigest ??= {};
|
|
2220
|
+
fold.perfHourDigest ??= {};
|
|
2221
|
+
changed = true;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
return changed;
|
|
2225
|
+
}
|
|
2064
2226
|
}];
|
|
2065
2227
|
/**
|
|
2066
2228
|
* 在加载边界对账本文档应用未执行的迁移,并记录已应用 id 供写回。
|
|
@@ -2084,6 +2246,7 @@ function runLedgerMigrations(document, migrations = LEDGER_MIGRATIONS) {
|
|
|
2084
2246
|
}
|
|
2085
2247
|
/** Serialize Map/Set-heavy fold state into a JSON-safe ledger entry. */
|
|
2086
2248
|
function serializeFold(fold) {
|
|
2249
|
+
const { byModel: perfDigest, byHourModel: perfHourDigest } = perfDigestsOf(fold.perf);
|
|
2087
2250
|
return {
|
|
2088
2251
|
total: fold.total,
|
|
2089
2252
|
byModel: Object.fromEntries(fold.byModel),
|
|
@@ -2096,7 +2259,8 @@ function serializeFold(fold) {
|
|
|
2096
2259
|
unpricedModels: [...fold.unpricedModels],
|
|
2097
2260
|
planCalls: Object.fromEntries(fold.planCalls),
|
|
2098
2261
|
turns: fold.turns,
|
|
2099
|
-
|
|
2262
|
+
perfDigest,
|
|
2263
|
+
perfHourDigest,
|
|
2100
2264
|
roles: fold.roles,
|
|
2101
2265
|
lastActive: fold.lastActive,
|
|
2102
2266
|
...fold.title !== void 0 ? { title: fold.title } : {}
|
|
@@ -2116,7 +2280,9 @@ function deserializeFold(fold) {
|
|
|
2116
2280
|
unpricedModels: new Set(fold.unpricedModels),
|
|
2117
2281
|
planCalls: new Map(Object.entries(fold.planCalls)),
|
|
2118
2282
|
turns: fold.turns,
|
|
2119
|
-
perf:
|
|
2283
|
+
perf: [],
|
|
2284
|
+
perfDigest: new Map(Object.entries(fold.perfDigest)),
|
|
2285
|
+
perfHourDigest: new Map(Object.entries(fold.perfHourDigest).map(([hour, models]) => [hour, new Map(Object.entries(models))])),
|
|
2120
2286
|
roles: fold.roles,
|
|
2121
2287
|
lastActive: fold.lastActive,
|
|
2122
2288
|
...fold.title !== void 0 ? { title: fold.title } : {}
|
|
@@ -2132,7 +2298,7 @@ function ledgerSessionsOf(value) {
|
|
|
2132
2298
|
const row = entry;
|
|
2133
2299
|
if (typeof row.id !== "string" || row.id === "" || row.fold === null || typeof row.fold !== "object") return false;
|
|
2134
2300
|
const fold = row.fold;
|
|
2135
|
-
return fold.total !== void 0 && fold.byModel !== void 0 && fold.byDay !== void 0 && fold.byDayModels !== void 0 && fold.bySite !== void 0 && Array.isArray(fold.unpricedModels) && fold.planCalls !== void 0 && Array.isArray(fold.turns) &&
|
|
2301
|
+
return fold.total !== void 0 && fold.byModel !== void 0 && fold.byDay !== void 0 && fold.byDayModels !== void 0 && fold.bySite !== void 0 && Array.isArray(fold.unpricedModels) && fold.planCalls !== void 0 && Array.isArray(fold.turns) && fold.perfDigest !== void 0 && fold.perfHourDigest !== void 0 && fold.roles !== void 0 && typeof fold.lastActive === "number";
|
|
2136
2302
|
});
|
|
2137
2303
|
}
|
|
2138
2304
|
/**
|
|
@@ -2227,6 +2393,8 @@ function freshFold() {
|
|
|
2227
2393
|
planCalls: /* @__PURE__ */ new Map(),
|
|
2228
2394
|
turns: [],
|
|
2229
2395
|
perf: [],
|
|
2396
|
+
perfDigest: /* @__PURE__ */ new Map(),
|
|
2397
|
+
perfHourDigest: /* @__PURE__ */ new Map(),
|
|
2230
2398
|
roles: {
|
|
2231
2399
|
userChars: 0,
|
|
2232
2400
|
toolChars: 0,
|
|
@@ -2468,11 +2636,6 @@ function mergeUsageInto(acc, cell) {
|
|
|
2468
2636
|
acc.officialCost += cell.officialCost;
|
|
2469
2637
|
if (cell.searchCalls !== void 0) acc.searchCalls = (acc.searchCalls ?? 0) + cell.searchCalls;
|
|
2470
2638
|
}
|
|
2471
|
-
/** 均值(数组非空时调用;空数组按 0 兜底)。 */
|
|
2472
|
-
function mean(values) {
|
|
2473
|
-
if (values.length === 0) return 0;
|
|
2474
|
-
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
2475
|
-
}
|
|
2476
2639
|
/** 分位数(0..1):先拷贝排序,再线性插值;空数组返回 0。 */
|
|
2477
2640
|
function percentile(values, p) {
|
|
2478
2641
|
if (values.length === 0) return 0;
|
|
@@ -2489,6 +2652,53 @@ function percentile(values, p) {
|
|
|
2489
2652
|
if (a === void 0 || b === void 0) return 0;
|
|
2490
2653
|
return a + (b - a) * (idx - lo);
|
|
2491
2654
|
}
|
|
2655
|
+
/** 活样本累加器 → 模型级摘要(跨会话合并前先归一到摘要形态)。 */
|
|
2656
|
+
function accumToModelDigest(acc) {
|
|
2657
|
+
return {
|
|
2658
|
+
samples: acc.ttfts.length,
|
|
2659
|
+
ttftSum: acc.ttfts.reduce((sum, value) => sum + value, 0),
|
|
2660
|
+
ttftMax: acc.ttfts.length === 0 ? 0 : Math.max(...acc.ttfts),
|
|
2661
|
+
ttftSpikes: acc.ttfts.filter((ttft) => ttft > PERF_SPIKE_MS).length,
|
|
2662
|
+
ttftP50: percentile(acc.ttfts, .5),
|
|
2663
|
+
ttftP90: percentile(acc.ttfts, .9),
|
|
2664
|
+
tpsCount: acc.tps.length,
|
|
2665
|
+
tpsSum: acc.tps.reduce((sum, value) => sum + value, 0),
|
|
2666
|
+
latencyCount: acc.latencies.length,
|
|
2667
|
+
latencySum: acc.latencies.reduce((sum, value) => sum + value, 0),
|
|
2668
|
+
estimated: acc.estimated
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
/** 活样本小时累加器 → 小时级摘要。 */
|
|
2672
|
+
function accumToHourDigest(acc) {
|
|
2673
|
+
return {
|
|
2674
|
+
samples: acc.ttfts.length,
|
|
2675
|
+
ttftSum: acc.ttfts.reduce((sum, value) => sum + value, 0),
|
|
2676
|
+
tpsCount: acc.tps.length,
|
|
2677
|
+
tpsSum: acc.tps.reduce((sum, value) => sum + value, 0)
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
/** 模型级摘要 → 面板行(均值由求和/计数派生)。 */
|
|
2681
|
+
function modelPerfOf(digest) {
|
|
2682
|
+
return {
|
|
2683
|
+
samples: digest.samples,
|
|
2684
|
+
ttftAvg: digest.samples === 0 ? 0 : digest.ttftSum / digest.samples,
|
|
2685
|
+
ttftP50: digest.ttftP50,
|
|
2686
|
+
ttftP90: digest.ttftP90,
|
|
2687
|
+
ttftMax: digest.ttftMax,
|
|
2688
|
+
ttftSpikes: digest.ttftSpikes,
|
|
2689
|
+
...digest.tpsCount === 0 ? {} : { tpsAvg: digest.tpsSum / digest.tpsCount },
|
|
2690
|
+
latencyAvg: digest.latencyCount === 0 ? 0 : digest.latencySum / digest.latencyCount,
|
|
2691
|
+
estimatedSamples: digest.estimated
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
/** 小时级摘要 → 面板行(均值由求和/计数派生)。 */
|
|
2695
|
+
function hourPerfOf(digest) {
|
|
2696
|
+
return {
|
|
2697
|
+
samples: digest.samples,
|
|
2698
|
+
ttftAvg: digest.samples === 0 ? 0 : digest.ttftSum / digest.samples,
|
|
2699
|
+
...digest.tpsCount === 0 ? {} : { tpsAvg: digest.tpsSum / digest.tpsCount }
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2492
2702
|
/**
|
|
2493
2703
|
* 聚合配置指纹:影响折叠语义的全部配置(订阅豁免、官方名单、路由别名、搜索估值)
|
|
2494
2704
|
* 的稳定序列化。账本行的复用判定携带该指纹——用户改配置后(如为 grok build 加
|
|
@@ -2536,7 +2746,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2536
2746
|
id,
|
|
2537
2747
|
...cwd === void 0 ? {} : { cwd },
|
|
2538
2748
|
...stamp === null ? {} : { stamp },
|
|
2539
|
-
foldVersion:
|
|
2749
|
+
foldVersion: 16,
|
|
2540
2750
|
fingerprint: foldFingerprint,
|
|
2541
2751
|
fold: serializeFold(fold)
|
|
2542
2752
|
};
|
|
@@ -2633,7 +2843,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2633
2843
|
try {
|
|
2634
2844
|
const cwd = meta.cwd;
|
|
2635
2845
|
const ledgerRow = ledger.get(id);
|
|
2636
|
-
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) ===
|
|
2846
|
+
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 16 && ledgerRow.fingerprint === foldFingerprint) {
|
|
2637
2847
|
const fold = deserializeFold(ledgerRow.fold);
|
|
2638
2848
|
folds.push({
|
|
2639
2849
|
id,
|
|
@@ -2715,7 +2925,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2715
2925
|
for (const entry of ledger.values()) {
|
|
2716
2926
|
if (included.has(entry.id)) continue;
|
|
2717
2927
|
try {
|
|
2718
|
-
const stale = (entry.foldVersion ?? 1) <
|
|
2928
|
+
const stale = (entry.foldVersion ?? 1) < 16 || entry.fingerprint !== foldFingerprint;
|
|
2719
2929
|
folds.push({
|
|
2720
2930
|
id: entry.id,
|
|
2721
2931
|
...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
|
|
@@ -2752,6 +2962,8 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2752
2962
|
};
|
|
2753
2963
|
const perfModel = /* @__PURE__ */ new Map();
|
|
2754
2964
|
const perfHourModel = /* @__PURE__ */ new Map();
|
|
2965
|
+
const perfModelDigest = /* @__PURE__ */ new Map();
|
|
2966
|
+
const perfHourDigest = /* @__PURE__ */ new Map();
|
|
2755
2967
|
for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
|
|
2756
2968
|
mergeUsageInto(total, fold.total);
|
|
2757
2969
|
roles.userChars += fold.roles.userChars;
|
|
@@ -2798,6 +3010,15 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2798
3010
|
hourAccum.ttfts.push(sample.ttftMs);
|
|
2799
3011
|
if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
|
|
2800
3012
|
}
|
|
3013
|
+
for (const [model, digest] of fold.perfDigest) perfModelDigest.set(model, mergeModelDigest(perfModelDigest.get(model) ?? EMPTY_MODEL_DIGEST, digest));
|
|
3014
|
+
for (const [hour, models] of fold.perfHourDigest) {
|
|
3015
|
+
let hourModels = perfHourDigest.get(hour);
|
|
3016
|
+
if (hourModels === void 0) {
|
|
3017
|
+
hourModels = /* @__PURE__ */ new Map();
|
|
3018
|
+
perfHourDigest.set(hour, hourModels);
|
|
3019
|
+
}
|
|
3020
|
+
for (const [model, digest] of models) hourModels.set(model, mergeHourDigest(hourModels.get(model) ?? EMPTY_HOUR_DIGEST, digest));
|
|
3021
|
+
}
|
|
2801
3022
|
for (const row of fold.turns) turnRows.push({
|
|
2802
3023
|
sessionId,
|
|
2803
3024
|
...row
|
|
@@ -2841,23 +3062,26 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2841
3062
|
};
|
|
2842
3063
|
const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
|
|
2843
3064
|
const toModelDaySiteRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries([...models].map(([model, sites]) => [model, Object.fromEntries(sites)]))]));
|
|
2844
|
-
const
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
3065
|
+
const modelKeys = new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
|
|
3066
|
+
const hourKeys = new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
|
|
3067
|
+
const perf = modelKeys.size === 0 && hourKeys.size === 0 ? void 0 : {
|
|
3068
|
+
byModel: Object.fromEntries([...modelKeys].map((model) => {
|
|
3069
|
+
const live = perfModel.get(model);
|
|
3070
|
+
const restored = perfModelDigest.get(model);
|
|
3071
|
+
const digest = live === void 0 ? restored : mergeModelDigest(accumToModelDigest(live), restored ?? EMPTY_MODEL_DIGEST);
|
|
3072
|
+
return digest === void 0 ? null : [model, modelPerfOf(digest)];
|
|
3073
|
+
}).filter((entry) => entry !== null)),
|
|
3074
|
+
byHourModel: Object.fromEntries([...hourKeys].sort().map((hour) => {
|
|
3075
|
+
const liveModels = perfHourModel.get(hour);
|
|
3076
|
+
const digestModels = perfHourDigest.get(hour);
|
|
3077
|
+
const modelKeysInHour = new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
|
|
3078
|
+
return [hour, Object.fromEntries([...modelKeysInHour].map((model) => {
|
|
3079
|
+
const live = liveModels?.get(model);
|
|
3080
|
+
const restored = digestModels?.get(model);
|
|
3081
|
+
const digest = live === void 0 ? restored : mergeHourDigest(accumToHourDigest(live), restored ?? EMPTY_HOUR_DIGEST);
|
|
3082
|
+
return digest === void 0 ? null : [model, hourPerfOf(digest)];
|
|
3083
|
+
}).filter((entry) => entry !== null))];
|
|
3084
|
+
}))
|
|
2861
3085
|
};
|
|
2862
3086
|
lastDoc = {
|
|
2863
3087
|
version: 4,
|
|
@@ -2903,6 +3127,29 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2903
3127
|
});
|
|
2904
3128
|
return inflight;
|
|
2905
3129
|
},
|
|
3130
|
+
async sessionUsageOf(sessionId) {
|
|
3131
|
+
await this.aggregate();
|
|
3132
|
+
const hit = cache.get(sessionId);
|
|
3133
|
+
if (hit !== void 0) return {
|
|
3134
|
+
calls: hit.fold.total.calls,
|
|
3135
|
+
cost: hit.fold.total.cost,
|
|
3136
|
+
input: hit.fold.total.input,
|
|
3137
|
+
output: hit.fold.total.output
|
|
3138
|
+
};
|
|
3139
|
+
const row = ledger.get(sessionId);
|
|
3140
|
+
if (row === void 0) return void 0;
|
|
3141
|
+
try {
|
|
3142
|
+
const fold = deserializeFold(row.fold);
|
|
3143
|
+
return {
|
|
3144
|
+
calls: fold.total.calls,
|
|
3145
|
+
cost: fold.total.cost,
|
|
3146
|
+
input: fold.total.input,
|
|
3147
|
+
output: fold.total.output
|
|
3148
|
+
};
|
|
3149
|
+
} catch {
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
},
|
|
2906
3153
|
async flush() {
|
|
2907
3154
|
if (inflight !== void 0) await inflight.catch(() => {});
|
|
2908
3155
|
await saveLedger(true);
|
|
@@ -3030,11 +3277,7 @@ function tc3Authorization(secretId, secretKey, payload, timestamp) {
|
|
|
3030
3277
|
const canonicalRequest = `POST\n/\n\n${`content-type:application/json; charset=utf-8\nhost:${TOKENHUB_HOST}\n`}\ncontent-type;host\n${createHash("sha256").update(payload).digest("hex")}`;
|
|
3031
3278
|
const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
|
|
3032
3279
|
const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
|
|
3033
|
-
|
|
3034
|
-
const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
|
|
3035
|
-
const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
|
|
3036
|
-
const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
3037
|
-
return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
|
|
3280
|
+
return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${createHmac("sha256", createHmac("sha256", createHmac("sha256", createHmac("sha256", date).update(secretKey).digest()).update(TOKENHUB_SERVICE).digest()).update("tc3_request").digest()).update(stringToSign).digest("hex")}`;
|
|
3038
3281
|
}
|
|
3039
3282
|
/**
|
|
3040
3283
|
* 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。
|
|
@@ -3798,7 +4041,7 @@ const declaredGate = createCooldownGate({
|
|
|
3798
4041
|
cooldownMs: 6e4
|
|
3799
4042
|
});
|
|
3800
4043
|
/** 会触及原型链的路径段:一律拒绝(读结果不是文档自带字段)。 */
|
|
3801
|
-
const FORBIDDEN_SEGMENTS =
|
|
4044
|
+
const FORBIDDEN_SEGMENTS = new Set([
|
|
3802
4045
|
"__proto__",
|
|
3803
4046
|
"constructor",
|
|
3804
4047
|
"prototype"
|
|
@@ -4348,7 +4591,7 @@ function asPrice(value) {
|
|
|
4348
4591
|
*/
|
|
4349
4592
|
function buildExtraModels(data) {
|
|
4350
4593
|
if (data === null || typeof data !== "object") return [];
|
|
4351
|
-
const catalogKeys =
|
|
4594
|
+
const catalogKeys = new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
|
|
4352
4595
|
const extras = [];
|
|
4353
4596
|
for (const [providerId, providerDoc] of Object.entries(data)) {
|
|
4354
4597
|
if (providerDoc === null || typeof providerDoc !== "object") continue;
|
|
@@ -5068,7 +5311,7 @@ const relayGate = createCooldownGate({
|
|
|
5068
5311
|
/** Abort a relay fetch when the upstream hangs beyond this budget. */
|
|
5069
5312
|
const FETCH_TIMEOUT_MS = 8e3;
|
|
5070
5313
|
/** 指纹识别缓存 TTL(毫秒):识别结果低频变化,5 分钟内同 origin 不再重复探测。 */
|
|
5071
|
-
const FINGERPRINT_TTL_MS =
|
|
5314
|
+
const FINGERPRINT_TTL_MS = 300 * 1e3;
|
|
5072
5315
|
/** 每 origin 的识别结果缓存:`kind` 是识别出的中转站程序,`at` 是探测时刻。 */
|
|
5073
5316
|
const fingerprintCache = /* @__PURE__ */ new Map();
|
|
5074
5317
|
/** Number, or null when the value is not a finite number (nor numeric string). */
|
|
@@ -5204,7 +5447,7 @@ function originOf(baseURL) {
|
|
|
5204
5447
|
* 子路径只会得到 404/非中转站格式,因而面板应排除它们,避免误判为
|
|
5205
5448
|
* 「未识别」。中转站面板只列真正的第三方中转程序。
|
|
5206
5449
|
*/
|
|
5207
|
-
const OFFICIAL_HOSTS =
|
|
5450
|
+
const OFFICIAL_HOSTS = new Set([
|
|
5208
5451
|
"api.deepseek.com",
|
|
5209
5452
|
"api.openai.com",
|
|
5210
5453
|
"open.bigmodel.cn",
|
|
@@ -5457,10 +5700,10 @@ const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESP
|
|
|
5457
5700
|
/** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
|
|
5458
5701
|
const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
|
|
5459
5702
|
/** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
|
|
5460
|
-
const PRICING_REFRESH_INTERVAL_MS =
|
|
5703
|
+
const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
|
|
5461
5704
|
/** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
|
|
5462
|
-
const SUBSCRIPTION_CACHE_MS =
|
|
5463
|
-
const BALANCE_CACHE_MS =
|
|
5705
|
+
const SUBSCRIPTION_CACHE_MS = 300 * 1e3;
|
|
5706
|
+
const BALANCE_CACHE_MS = 300 * 1e3;
|
|
5464
5707
|
/** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
|
|
5465
5708
|
const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
5466
5709
|
/**
|
|
@@ -5468,14 +5711,16 @@ const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
|
5468
5711
|
* 下发,供「设置 → 插件信息卡」展示。发布版 lib/index.js 相对包根解析。
|
|
5469
5712
|
*/
|
|
5470
5713
|
const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version ?? "0.0.0";
|
|
5471
|
-
/**
|
|
5472
|
-
|
|
5714
|
+
/** 统计快照的落盘节流(毫秒):快照只服务聚合失败回退与重启首屏(展示级
|
|
5715
|
+
* 新鲜度即可),降到 5 分钟一次——原子写含上一版备份是两次全量 JSON 落盘,
|
|
5716
|
+
* 重度用户快照上百 KB,跟随前端轮询 30 秒写一次纯属写放大。 */
|
|
5717
|
+
const SNAPSHOT_INTERVAL_MS = 3e5;
|
|
5473
5718
|
/** usage-stats 响应的等待预算(毫秒):聚合在此预算内完成就回实时数据,
|
|
5474
5719
|
* 超预算立即回最近快照(stale-while-revalidate),后台折叠继续跑。
|
|
5475
5720
|
* 轻量用户暖缓存下的聚合是毫秒级,预算几乎不会触发。 */
|
|
5476
5721
|
const RESPONSE_BUDGET_MS = 1500;
|
|
5477
5722
|
/** 鉴权失败告警冷却(毫秒):同一 provider 在窗口内只提示一次,避免 30 秒轮询刷屏。 */
|
|
5478
|
-
const AUTH_WARN_COOLDOWN_MS =
|
|
5723
|
+
const AUTH_WARN_COOLDOWN_MS = 1800 * 1e3;
|
|
5479
5724
|
/**
|
|
5480
5725
|
* 鉴权失败分类告警(P1-5):余额 / 订阅查询返回 unauthorized 时,按
|
|
5481
5726
|
* `source:provider` 去重并冷却告警,提示检查 llm-pi-ai 里该 provider 的 apiKeyEnv。
|
|
@@ -5489,6 +5734,32 @@ function warnAuthOnce(source, provider, displayName) {
|
|
|
5489
5734
|
authWarnedAt.set(key, now);
|
|
5490
5735
|
console.warn(`[usage-billing] ${displayName}(${provider})鉴权失败:请检查 llm-pi-ai 设置中该 provider 的凭据环境变量是否正确/有效。`);
|
|
5491
5736
|
}
|
|
5737
|
+
/** 锁等待上限:账本提交是毫秒级 read-render-commit,2s 已数倍于正常持锁时长。 */
|
|
5738
|
+
const LEDGER_LOCK_WAIT_MS = 2e3;
|
|
5739
|
+
/**
|
|
5740
|
+
* 锁年龄超过该值判为崩溃残留的孤儿锁。正常持锁是毫秒级文件提交,没有活跃
|
|
5741
|
+
* writer 会合法持有 60s;库层刻意不做年龄判定(它无法假设调用方的操作时长),
|
|
5742
|
+
* 本调用方知道自己操作的上界,可以给出有依据的清理阈值。
|
|
5743
|
+
*/
|
|
5744
|
+
const STALE_LEDGER_LOCK_MS = 6e4;
|
|
5745
|
+
/**
|
|
5746
|
+
* 锁等待超时后的孤儿锁自愈(issue #48):仅当锁文件年龄超过
|
|
5747
|
+
* STALE_LEDGER_LOCK_MS 才清理,避免误删慢 writer 的活跃锁。
|
|
5748
|
+
* @param ledgerPath - 账本文件路径(锁为其 `.lock` 兄弟文件)。
|
|
5749
|
+
* @returns 是否清理了孤儿锁;false = 锁已消失或太新鲜,调用方上抛原错误。
|
|
5750
|
+
*/
|
|
5751
|
+
async function reclaimStaleLedgerLock(ledgerPath) {
|
|
5752
|
+
let ageMs;
|
|
5753
|
+
try {
|
|
5754
|
+
ageMs = Date.now() - (await lstat(`${ledgerPath}.lock`)).mtimeMs;
|
|
5755
|
+
} catch {
|
|
5756
|
+
return false;
|
|
5757
|
+
}
|
|
5758
|
+
if (ageMs < STALE_LEDGER_LOCK_MS) return false;
|
|
5759
|
+
console.warn(`[usage-billing] 账本锁已存在 ${Math.round(ageMs / 1e3)}s(崩溃残留),清理后重试:${ledgerPath}.lock`);
|
|
5760
|
+
await rm(`${ledgerPath}.lock`, { force: true });
|
|
5761
|
+
return true;
|
|
5762
|
+
}
|
|
5492
5763
|
/**
|
|
5493
5764
|
* Create the atomic file-backed durable-ledger store. The previous complete file
|
|
5494
5765
|
* is retained as `.bak`; a malformed/missing main file falls back to that backup.
|
|
@@ -5509,7 +5780,7 @@ function createFileUsageLedgerStore(ledgerPath) {
|
|
|
5509
5780
|
recursive: true,
|
|
5510
5781
|
mode: 448
|
|
5511
5782
|
});
|
|
5512
|
-
|
|
5783
|
+
const commit = async () => {
|
|
5513
5784
|
try {
|
|
5514
5785
|
const existing = await readFile(ledgerPath, "utf8");
|
|
5515
5786
|
JSON.parse(existing);
|
|
@@ -5522,7 +5793,14 @@ function createFileUsageLedgerStore(ledgerPath) {
|
|
|
5522
5793
|
mode: 384,
|
|
5523
5794
|
dirMode: 448
|
|
5524
5795
|
});
|
|
5525
|
-
}
|
|
5796
|
+
};
|
|
5797
|
+
try {
|
|
5798
|
+
await withFileLock(ledgerPath, commit, { waitMs: LEDGER_LOCK_WAIT_MS });
|
|
5799
|
+
} catch (error) {
|
|
5800
|
+
if (!(error instanceof Error) || !error.message.includes("timed out waiting for the writer lock")) throw error;
|
|
5801
|
+
if (!await reclaimStaleLedgerLock(ledgerPath)) throw error;
|
|
5802
|
+
await withFileLock(ledgerPath, commit, { waitMs: LEDGER_LOCK_WAIT_MS });
|
|
5803
|
+
}
|
|
5526
5804
|
}
|
|
5527
5805
|
};
|
|
5528
5806
|
}
|
|
@@ -5763,8 +6041,7 @@ function apply(ctx, config = {}) {
|
|
|
5763
6041
|
})();
|
|
5764
6042
|
const persistReconcileRef = () => {
|
|
5765
6043
|
if (reconcileRef === null) return;
|
|
5766
|
-
|
|
5767
|
-
writeFileAtomic(reconcilePath, payload, {
|
|
6044
|
+
writeFileAtomic(reconcilePath, JSON.stringify({ ref: reconcileRef }), {
|
|
5768
6045
|
mode: 384,
|
|
5769
6046
|
dirMode: 448
|
|
5770
6047
|
}).catch(() => {});
|
|
@@ -6002,6 +6279,11 @@ function apply(ctx, config = {}) {
|
|
|
6002
6279
|
}
|
|
6003
6280
|
const sessionId = exec.agent?.id;
|
|
6004
6281
|
if (sessionId === void 0) throw new Error("usage_stats 的 session 范围需要 agent 会话上下文");
|
|
6282
|
+
const summary = await aggregator.sessionUsageOf(String(sessionId));
|
|
6283
|
+
if (summary !== void 0) return {
|
|
6284
|
+
range: args.range,
|
|
6285
|
+
...summary
|
|
6286
|
+
};
|
|
6005
6287
|
let cost = 0;
|
|
6006
6288
|
let calls = 0;
|
|
6007
6289
|
let input = 0;
|