@dijkspicy/opencode-stats-reporter 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.
@@ -0,0 +1,398 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="referrer" content="no-referrer">
7
+ <title>BlueRegion Usage - 个人用量面板</title>
8
+ <link rel="stylesheet" href="css/style.css">
9
+ <!-- ECharts CDN:jsdelivr 优先,失败时回退 unpkg(hash 无法确认故不加 integrity,避免写错) -->
10
+ <script>
11
+ window.__echartsFallback = function () {
12
+ var s = document.createElement('script');
13
+ s.src = 'https://unpkg.com/echarts@5.5.0/dist/echarts.min.js';
14
+ document.head.appendChild(s);
15
+ };
16
+ </script>
17
+ <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js" onerror="__echartsFallback()"></script>
18
+ </head>
19
+ <body>
20
+
21
+ <!-- http:// 打开时的安全提示 -->
22
+ <div id="insecure-banner" class="insecure-banner hidden">
23
+ 当前页面通过 http:// 打开,API Key 将以明文随请求传输,建议改用 https:// 或 file:// 打开
24
+ </div>
25
+
26
+ <!-- ========== 引导视图 ========== -->
27
+ <section id="view-onboarding" class="hidden">
28
+ <div class="onboard-card">
29
+ <h1 class="onboard-title">BlueRegion Usage</h1>
30
+ <p class="onboard-sub">个人 LLM 用量可视化面板,首次使用请完成配置</p>
31
+
32
+ <div id="onboard-error" class="onboard-error hidden"></div>
33
+
34
+ <label class="field">
35
+ <span class="field-label">网关地址</span>
36
+ <input id="onboard-url" type="url" placeholder="https://gateway.example.com" autocomplete="off">
37
+ </label>
38
+
39
+ <label class="field">
40
+ <span class="field-label">API Key</span>
41
+ <span class="field-pwd">
42
+ <input id="onboard-key" type="password" placeholder="api-key-xxxxxxxx" autocomplete="off">
43
+ <button id="onboard-toggle-key" type="button" class="btn btn-sm">显示</button>
44
+ </span>
45
+ </label>
46
+
47
+ <label class="field">
48
+ <span class="field-label">每日 Credit 限额(可选)</span>
49
+ <input id="onboard-daily-limit" type="number" min="0" step="0.01" placeholder="留空表示不限">
50
+ </label>
51
+
52
+ <label class="field">
53
+ <span class="field-label">每周 Credit 限额(可选)</span>
54
+ <input id="onboard-weekly-limit" type="number" min="0" step="0.01" placeholder="留空表示不限">
55
+ </label>
56
+
57
+ <label class="field">
58
+ <span class="field-label">每月 Credit 限额(可选)</span>
59
+ <input id="onboard-monthly-limit" type="number" min="0" step="0.01" placeholder="留空表示不限">
60
+ </label>
61
+
62
+ <button id="onboard-test" class="btn btn-primary btn-block">测试连接</button>
63
+ <p class="onboard-tip">也可直接编辑 config.js 后刷新本页</p>
64
+ </div>
65
+ </section>
66
+
67
+ <!-- ========== 面板视图 ========== -->
68
+ <section id="view-panel" class="hidden">
69
+ <header class="panel-header">
70
+ <div class="header-left">
71
+ <h1 class="panel-title">BlueRegion Usage</h1>
72
+ <span id="header-conn" class="conn-badge conn-pending" title="正在检测网关连通性…">
73
+ <span class="conn-icon"></span><span id="header-consumer">连接中…</span>
74
+ </span>
75
+ </div>
76
+
77
+ <div id="view-seg" class="seg-control" aria-label="视图切换">
78
+ <button data-view="usage" class="active">用量</button>
79
+ <button data-view="sessions">会话</button>
80
+ </div>
81
+
82
+ <div id="period-seg" class="seg-control">
83
+ <button data-period="today" class="active">今日</button>
84
+ <button data-period="week">本周</button>
85
+ <button data-period="month">本月</button>
86
+ </div>
87
+
88
+ <div class="header-tip" role="note">当前仅能统计火山AI网关Token数据,Credit用量数据为根据Token用量估算,未经复核,仅供参考,最终用量请以实际消耗为准;建议根据业务情况选择合适模型作业,<span class="tip-link"><a href="https://gitcode.com/solcao/BlueRegionUsage" target="_blank" rel="noopener">看板用爱发电,共建共享</a>。</span></div>
89
+
90
+ <div class="header-right">
91
+ <span id="last-updated" class="last-updated">--</span>
92
+ <button id="btn-refresh" class="btn">刷新</button>
93
+ <div class="seg-control seg-small">
94
+ <button id="theme-light" title="浅色">浅</button>
95
+ <button id="theme-dark" title="深色">深</button>
96
+ <button id="theme-auto" title="跟随系统" class="active">自动</button>
97
+ </div>
98
+ <button id="btn-settings" class="btn" title="设置">设置</button>
99
+ </div>
100
+ </header>
101
+
102
+ <div id="header-notice" class="header-notice hidden"></div>
103
+
104
+ <main class="panel-main">
105
+ <div id="view-usage">
106
+ <!-- KPI 卡片区:token 按 OpenAI 规范拆分(req 含缓存命中),三类分别计量;Credit 为估算用量 -->
107
+ <section class="kpi-grid">
108
+ <div class="kpi-card">
109
+ <div class="kpi-label">Credit 用量(估算)</div>
110
+ <div id="kpi-credit" class="kpi-value">--</div>
111
+ <div id="kpi-credit-remain" class="kpi-remain"></div>
112
+ <div class="kpi-delta"></div>
113
+ </div>
114
+ <div class="kpi-card">
115
+ <div class="kpi-label">总 Tokens</div>
116
+ <div id="kpi-total-tokens" class="kpi-value">--</div>
117
+ <div id="kpi-total-delta" class="kpi-delta"></div>
118
+ </div>
119
+ <div class="kpi-card">
120
+ <div class="kpi-label">输入(缓存未命中)</div>
121
+ <div id="kpi-uncached-tokens" class="kpi-value">--</div>
122
+ <div id="kpi-uncached-delta" class="kpi-delta"></div>
123
+ </div>
124
+ <div class="kpi-card">
125
+ <div class="kpi-label">输入(缓存命中)</div>
126
+ <div id="kpi-cached-tokens" class="kpi-value">--</div>
127
+ <div id="kpi-cached-delta" class="kpi-delta"></div>
128
+ </div>
129
+ <div class="kpi-card">
130
+ <div class="kpi-label">输出 Tokens</div>
131
+ <div id="kpi-rsp-tokens" class="kpi-value">--</div>
132
+ <div id="kpi-rsp-delta" class="kpi-delta"></div>
133
+ </div>
134
+ <div class="kpi-card">
135
+ <div class="kpi-label">缓存命中率</div>
136
+ <div id="kpi-cache-rate" class="kpi-value">--</div>
137
+ <div id="kpi-cache-rate-delta" class="kpi-delta"></div>
138
+ </div>
139
+ <div class="kpi-card">
140
+ <div class="kpi-label">请求数</div>
141
+ <div id="kpi-requests" class="kpi-value">--</div>
142
+ <div id="kpi-requests-delta" class="kpi-delta"></div>
143
+ </div>
144
+ <div class="kpi-card">
145
+ <div class="kpi-label">平均 TTFT</div>
146
+ <div id="kpi-ttft" class="kpi-value">--</div>
147
+ <div class="kpi-delta"></div>
148
+ </div>
149
+ </section>
150
+
151
+ <!-- 本月 Credit 进度条:月均 pacing,独立于分页 -->
152
+ <section id="card-quota" class="card quota-card">
153
+ <div class="quota-head">
154
+ <h2 class="card-title">本月 Credit 进度 <span class="quota-sub">(按月均限额 pacing,独立于上方分页)</span></h2>
155
+ <button id="btn-quota-config" class="btn btn-sm" title="配置每日/每周/每月 Credit 限额">配置限额</button>
156
+ </div>
157
+
158
+ <div id="quota-hint" class="quota-hint hidden">配置每日 / 每周 / 每月限额后,即可按月均速度查看本月平均用量进度</div>
159
+
160
+ <div id="quota-form" class="quota-form hidden">
161
+ <div class="quota-form-row">
162
+ <label>每日限额
163
+ <input id="q-daily-limit" type="number" min="0" step="0.01" placeholder="不限"></label>
164
+ <label>每周限额
165
+ <input id="q-weekly-limit" type="number" min="0" step="0.01" placeholder="不限"></label>
166
+ <label>每月限额
167
+ <input id="q-monthly-limit" type="number" min="0" step="0.01" placeholder="不限"></label>
168
+ </div>
169
+ <div class="quota-form-actions">
170
+ <button id="q-save" class="btn btn-primary btn-sm">保存</button>
171
+ <button id="q-cancel" class="btn btn-sm">取消</button>
172
+ </div>
173
+ <div id="q-msg" class="quota-form-msg hidden"></div>
174
+ </div>
175
+
176
+ <div id="quota-meta" class="quota-meta">--</div>
177
+ <div class="quota-track"><div id="quota-fill" class="quota-fill"></div></div>
178
+ <div id="quota-note" class="quota-note">--</div>
179
+ </section>
180
+
181
+ <!-- 趋势 + 分时 + 分布图 -->
182
+ <section class="charts-grid">
183
+ <div id="card-trend" class="card chart-card">
184
+ <h2 class="card-title">每日用量趋势</h2>
185
+ <div id="chart-trend" class="chart chart-tall"></div>
186
+ <div class="card-state hidden"></div>
187
+ </div>
188
+ <div id="card-hourly" class="card chart-card">
189
+ <h2 class="card-title">今日分时</h2>
190
+ <div id="chart-hourly" class="chart chart-tall"></div>
191
+ <div class="card-state hidden"></div>
192
+ </div>
193
+ <div id="card-model" class="card chart-card">
194
+ <h2 class="card-title">模型分布</h2>
195
+ <div id="chart-model" class="chart chart-tall"></div>
196
+ <div class="card-state hidden"></div>
197
+ </div>
198
+ <div id="card-provider" class="card chart-card">
199
+ <h2 class="card-title">供应商分布</h2>
200
+ <div id="chart-provider" class="chart chart-tall"></div>
201
+ <div class="card-state hidden"></div>
202
+ </div>
203
+ </section>
204
+
205
+ <!-- 可用模型目录(/v1/models):模型 ID + Credit 计量系数 -->
206
+ <section id="card-catalog" class="card card-catalog">
207
+ <h2 class="card-title">可用模型 <span id="catalog-count" class="catalog-count"></span></h2>
208
+ <div class="table-wrap">
209
+ <table id="catalog-table" class="catalog-table">
210
+ <thead>
211
+ <tr>
212
+ <th>模型</th><th class="num">Credit 系数</th>
213
+ <th>模型</th><th class="num">Credit 系数</th>
214
+ <th>模型</th><th class="num">Credit 系数</th>
215
+ </tr>
216
+ </thead>
217
+ <tbody id="catalog-tbody"></tbody>
218
+ </table>
219
+ </div>
220
+ </section>
221
+
222
+ <!-- 明细表 -->
223
+ <section id="card-table" class="card">
224
+ <div class="table-head">
225
+ <h2 class="card-title">模型明细</h2>
226
+ <button id="btn-export" class="btn">导出 CSV</button>
227
+ </div>
228
+ <div class="table-wrap">
229
+ <table id="model-table">
230
+ <thead>
231
+ <tr>
232
+ <th data-key="model">模型</th>
233
+ <th data-key="request_count" class="num">请求数</th>
234
+ <th data-key="input_uncached" class="num">输入(未命中)</th>
235
+ <th data-key="cached_tokens" class="num">输入(缓存命中)</th>
236
+ <th data-key="rsp_tokens" class="num">输出</th>
237
+ <th data-key="credit" class="num">Credit 用量</th>
238
+ <th data-key="cache_hit_rate" class="num">缓存命中率</th>
239
+ <th data-key="share" class="num">占比</th>
240
+ </tr>
241
+ </thead>
242
+ <tbody id="model-tbody"></tbody>
243
+ </table>
244
+ </div>
245
+ <div class="card-state hidden"></div>
246
+ </section>
247
+ </div><!-- /view-usage -->
248
+
249
+ <!-- ========== 会话视图(客户端上报数据,非网关权威) ========== -->
250
+ <section id="view-sessions" class="hidden">
251
+ <div class="session-source" role="note">会话数据来自客户端上报,可能延迟或遗漏,非网关权威数据,仅供对账参考</div>
252
+
253
+ <!-- 覆盖率 / 差额:网关权威总量 − 会话上报合计 = 未归属用量 -->
254
+ <section id="card-session-coverage" class="card sess-coverage">
255
+ <div class="table-head">
256
+ <h2 class="card-title">用量对账 <span class="quota-sub">网关权威总量 − 会话上报合计 = 未归属用量</span></h2>
257
+ </div>
258
+ <div class="cover-grid">
259
+ <div class="cover-cell">
260
+ <div class="kpi-label">网关权威总量</div>
261
+ <div id="sess-cov-gateway" class="kpi-value">--</div>
262
+ </div>
263
+ <div class="cover-cell">
264
+ <div class="kpi-label">会话上报合计</div>
265
+ <div id="sess-cov-reported" class="kpi-value">--</div>
266
+ </div>
267
+ <div class="cover-cell">
268
+ <div class="kpi-label">未归属用量</div>
269
+ <div id="sess-cov-diff" class="kpi-value">--</div>
270
+ </div>
271
+ <div class="cover-cell">
272
+ <div class="kpi-label">会话覆盖率</div>
273
+ <div id="sess-cov-rate" class="kpi-value">--</div>
274
+ </div>
275
+ </div>
276
+ <div id="sess-cov-note" class="sess-cov-note">未归属用量可能来自:标题生成、会话压缩、重试与其他客户端</div>
277
+ <div class="card-state hidden"></div>
278
+ </section>
279
+
280
+ <!-- 会话列表 -->
281
+ <section id="card-session-list" class="card">
282
+ <div class="table-head">
283
+ <h2 class="card-title">会话列表 <span class="src-badge" title="数据来自客户端上报,可能延迟或遗漏">客户端上报</span></h2>
284
+ <div class="sess-filter">
285
+ <label class="sess-filter-item">模型
286
+ <select id="sess-filter-model" aria-label="按模型筛选">
287
+ <option value="">全部模型</option>
288
+ </select></label>
289
+ <label class="sess-filter-item">代理
290
+ <select id="sess-filter-agent" aria-label="按代理筛选">
291
+ <option value="">全部代理</option>
292
+ </select></label>
293
+ </div>
294
+ </div>
295
+ <div class="table-wrap">
296
+ <table id="session-table" class="session-table">
297
+ <thead>
298
+ <tr>
299
+ <th>会话</th>
300
+ <th>模型</th>
301
+ <th>代理</th>
302
+ <th class="num" title="输入总量 = 输入 + 缓存读 + 缓存写">输入总量</th>
303
+ <th class="num" title="输出总量 = 输出 + 推理">输出总量</th>
304
+ <th class="num">推理</th>
305
+ <th class="num">缓存读</th>
306
+ <th class="num">缓存写</th>
307
+ <th class="num">消息数</th>
308
+ <th>最后活动</th>
309
+ </tr>
310
+ </thead>
311
+ <tbody id="session-tbody"></tbody>
312
+ </table>
313
+ </div>
314
+ <div class="sess-list-foot">
315
+ <span id="sess-count" class="sess-count">--</span>
316
+ <button id="btn-sess-more" class="btn btn-sm hidden">加载更多</button>
317
+ </div>
318
+ <div class="card-state hidden"></div>
319
+ </section>
320
+
321
+ <!-- 会话详情 -->
322
+ <section id="card-session-detail" class="card hidden">
323
+ <div class="table-head">
324
+ <h2 class="card-title">会话详情</h2>
325
+ <button id="btn-sess-detail-close" class="btn btn-sm">收起</button>
326
+ </div>
327
+ <div id="sess-detail-id" class="sess-detail-id mono"></div>
328
+ <div id="sess-detail-body" class="sess-detail-body"></div>
329
+ <div class="card-state hidden"></div>
330
+ </section>
331
+ </section><!-- /view-sessions -->
332
+ </main>
333
+
334
+ <footer class="panel-footer">BlueRegion Usage v0.3.1 · 数据口径:输入 Tokens 含缓存命中(OpenAI 规范),未命中/命中分别计量;Credit 用量 = Σ 逐日(当日总 Tokens × 当日生效 Credit 系数)÷ 百万(估算,以实际消耗为准)</footer>
335
+ </section>
336
+
337
+ <!-- ========== 设置抽屉 ========== -->
338
+ <div id="drawer-mask" class="drawer-mask hidden"></div>
339
+ <aside id="drawer" class="drawer hidden">
340
+ <div class="drawer-head">
341
+ <h2>设置</h2>
342
+ <button id="drawer-close" class="btn btn-sm">关闭</button>
343
+ </div>
344
+ <div id="set-masked" class="drawer-masked"></div>
345
+
346
+ <label class="field"><span class="field-label">网关地址</span>
347
+ <input id="set-url" type="url" autocomplete="off"></label>
348
+
349
+ <label class="field"><span class="field-label">API Key</span>
350
+ <span class="field-pwd">
351
+ <input id="set-key" type="password" autocomplete="off">
352
+ <button id="set-toggle-key" type="button" class="btn btn-sm">显示</button>
353
+ </span></label>
354
+
355
+ <label class="field"><span class="field-label">趋势天数(1-15)</span>
356
+ <input id="set-trenddays" type="number" min="1" max="15" step="1"></label>
357
+
358
+ <label class="field"><span class="field-label">主题</span>
359
+ <select id="set-theme">
360
+ <option value="auto">跟随系统</option>
361
+ <option value="light">浅色</option>
362
+ <option value="dark">深色</option>
363
+ </select></label>
364
+
365
+ <label class="field"><span class="field-label">数字风格</span>
366
+ <select id="set-numstyle">
367
+ <option value="wan">万 / 亿</option>
368
+ <option value="intl">K / M / B</option>
369
+ </select></label>
370
+
371
+ <label class="field"><span class="field-label">每日 Credit 限额(可选)</span>
372
+ <input id="set-daily-limit" type="number" min="0" step="0.01" placeholder="留空表示不限"></label>
373
+
374
+ <label class="field"><span class="field-label">每周 Credit 限额(可选)</span>
375
+ <input id="set-weekly-limit" type="number" min="0" step="0.01" placeholder="留空表示不限"></label>
376
+
377
+ <label class="field"><span class="field-label">每月 Credit 限额(可选)</span>
378
+ <input id="set-monthly-limit" type="number" min="0" step="0.01" placeholder="留空表示不限"></label>
379
+
380
+ <div id="set-message" class="drawer-message hidden"></div>
381
+
382
+ <div class="drawer-actions">
383
+ <button id="set-test" class="btn">测试连接</button>
384
+ <button id="set-save" class="btn btn-primary">保存到浏览器</button>
385
+ <button id="set-clear" class="btn btn-danger">清除已保存配置</button>
386
+ </div>
387
+ <p class="drawer-tip">保存到浏览器后优先级高于 config.js;config.js 含 API Key,请勿提交 git 或截图分享。</p>
388
+ </aside>
389
+
390
+ <script src="config.js"></script>
391
+ <script src="js/config.js"></script>
392
+ <script src="js/format.js"></script>
393
+ <script src="js/api.js"></script>
394
+ <script src="js/charts.js"></script>
395
+ <script src="js/sessions.js"></script>
396
+ <script src="js/app.js"></script>
397
+ </body>
398
+ </html>
@@ -0,0 +1,154 @@
1
+ /**
2
+ * fetch 封装:鉴权头、10s 超时(AbortController)、错误归一化
3
+ * 归一化错误结构:{ status: Number|null, code: string, message: string }
4
+ * code: 'unauthorized' | 'http' | 'timeout' | 'network' | 'bad_response'
5
+ */
6
+ (function () {
7
+ 'use strict';
8
+
9
+ var TIMEOUT_MS = 10000;
10
+
11
+ function normalizeError(err, status) {
12
+ if (err && err.name === 'AbortError') {
13
+ return { status: null, code: 'timeout', message: '请求超时(10 秒未响应)' };
14
+ }
15
+ if (status === 401) {
16
+ return { status: 401, code: 'unauthorized', message: 'API Key 无效或已过期(401)' };
17
+ }
18
+ if (typeof status === 'number') {
19
+ return { status: status, code: 'http', message: '网关返回错误(HTTP ' + status + ')' };
20
+ }
21
+ return { status: null, code: 'network', message: '网络失败:地址不可达或 CORS 未放行' };
22
+ }
23
+
24
+ /**
25
+ * 发起 GET 请求
26
+ * @param {Object} cfg 含 gatewayBaseUrl / apiKey
27
+ * @param {string} path 如 /v1/usage/summary
28
+ * @param {Object} [params] query 参数
29
+ * @returns {Promise<Object>} 成功 resolve 响应 JSON,失败 reject 归一化错误
30
+ */
31
+ function get(cfg, path, params) {
32
+ return new Promise(function (resolve, reject) {
33
+ var base = String(cfg.gatewayBaseUrl || '').replace(/\/+$/, '');
34
+ var url;
35
+ try {
36
+ url = new URL(base + path);
37
+ if (params) {
38
+ Object.keys(params).forEach(function (k) {
39
+ if (params[k] !== undefined && params[k] !== null) {
40
+ url.searchParams.set(k, params[k]);
41
+ }
42
+ });
43
+ }
44
+ } catch (e) {
45
+ reject({ status: null, code: 'bad_response', message: '网关地址非法:' + e.message });
46
+ return;
47
+ }
48
+
49
+ var ctrl = new AbortController();
50
+ var timer = setTimeout(function () { ctrl.abort(); }, TIMEOUT_MS);
51
+
52
+ fetch(url.toString(), {
53
+ method: 'GET',
54
+ headers: { 'Authorization': 'Bearer ' + cfg.apiKey },
55
+ signal: ctrl.signal
56
+ }).then(function (resp) {
57
+ clearTimeout(timer);
58
+ return resp.text().then(function (text) {
59
+ var body = null;
60
+ try { body = text ? JSON.parse(text) : null; } catch (e) { /* 非 JSON */ }
61
+ if (!resp.ok) {
62
+ var nerr = normalizeError(null, resp.status);
63
+ // 优先使用网关统一错误格式中的 message
64
+ if (body && body.error && body.error.message) nerr.message = body.error.message;
65
+ reject(nerr);
66
+ return;
67
+ }
68
+ if (body === null || typeof body !== 'object') {
69
+ reject({ status: resp.status, code: 'bad_response', message: '响应不是合法 JSON' });
70
+ return;
71
+ }
72
+ resolve(body);
73
+ });
74
+ }).catch(function (err) {
75
+ clearTimeout(timer);
76
+ reject(normalizeError(err));
77
+ });
78
+ });
79
+ }
80
+
81
+ // ---- 业务接口 ----
82
+ function summary(cfg, window_) { return get(cfg, '/v1/usage/summary', { window: window_ || 'today' }); }
83
+ function daily(cfg, days) { return get(cfg, '/v1/usage/daily', { days: days }); }
84
+ function hourly(cfg) { return get(cfg, '/v1/usage/hourly'); }
85
+ function byModel(cfg, window_) { return get(cfg, '/v1/usage/by-model', { window: window_ }); }
86
+ function byProvider(cfg, window_) { return get(cfg, '/v1/usage/by-provider', { window: window_ }); }
87
+ function performance(cfg, hours, groupBy) {
88
+ return get(cfg, '/v1/usage/performance', { hours: hours, group_by: groupBy || 'model' });
89
+ }
90
+
91
+ // ---- 会话用量接口(自建会话服务,经同一网关地址访问;数据为客户端上报口径,非网关权威) ----
92
+ /** 会话列表:window / model / agent / install_id / limit / cursor(游标分页) */
93
+ function sessions(cfg, params) { return get(cfg, '/v1/sessions', params || {}); }
94
+ /** 单会话详情:不存在或不属于当前消费者时返回 404 */
95
+ function sessionDetail(cfg, id) { return get(cfg, '/v1/sessions/' + encodeURIComponent(id)); }
96
+ /** 会话聚合:group_by=model|agent|install|day */
97
+ function sessionsSummary(cfg, params) { return get(cfg, '/v1/sessions/summary', params || {}); }
98
+ /** 模型目录:网关在 key-auth 后注入身份,函数回显 consumer,可用作连通性探测 */
99
+ function models(cfg) { return get(cfg, '/v1/models'); }
100
+
101
+ /**
102
+ * 连接测试:调稳定的 /v1/models
103
+ * 成功返回 { ok:true, consumer, models:[{id, credit, creditHistory}...], modelIds:[id...] },失败返回 { ok:false, error }
104
+ * credit 为当日生效计量系数;creditHistory 为版本历史 [{from, credit}] 按生效日升序
105
+ * (函数旧版无 credit_history 时为空数组,面板回退"总量×当前系数"的平铺算法)
106
+ */
107
+ function testConnection(cfg) {
108
+ return models(cfg).then(
109
+ function (data) {
110
+ var list = (data && data.data) || [];
111
+ var models = list.map(function (m) {
112
+ var hist = [];
113
+ if (Array.isArray(m.credit_history)) {
114
+ m.credit_history.forEach(function (e) {
115
+ if (e && typeof e.from === 'string' && typeof e.credit === 'number') {
116
+ hist.push({ from: e.from, credit: e.credit });
117
+ }
118
+ });
119
+ hist.sort(function (a, b) { return a.from < b.from ? -1 : (a.from > b.from ? 1 : 0); });
120
+ }
121
+ return {
122
+ id: m.id,
123
+ credit: (typeof m.credit === 'number') ? m.credit : null,
124
+ creditHistory: hist
125
+ };
126
+ });
127
+ return {
128
+ ok: true,
129
+ consumer: (data && data.consumer) || '',
130
+ models: models,
131
+ modelIds: models.map(function (m) { return m.id; })
132
+ };
133
+ },
134
+ function (err) { return { ok: false, error: err }; }
135
+ );
136
+ }
137
+
138
+ window.BRU = window.BRU || {};
139
+ window.BRU.api = {
140
+ get: get,
141
+ summary: summary,
142
+ daily: daily,
143
+ hourly: hourly,
144
+ byModel: byModel,
145
+ byProvider: byProvider,
146
+ performance: performance,
147
+ sessions: sessions,
148
+ sessionDetail: sessionDetail,
149
+ sessionsSummary: sessionsSummary,
150
+ models: models,
151
+ testConnection: testConnection,
152
+ TIMEOUT_MS: TIMEOUT_MS
153
+ };
154
+ })();