dsh-mobile-plugin 0.5.18 → 0.5.20

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.
@@ -3,24 +3,12 @@
3
3
  /**
4
4
  * 统计账本:按天累计的用量 + 每条会话的累计与身份(`$DSH_HOME/mobile-stats.json`)。
5
5
  *
6
- * 它不寄在会话日志上:用户会清理已归档的会话,删的就是日志本身;统计每次现算的话,清理之后
7
- * 那部分用量会凭空消失。一旦读到增量就落进这个文件,之后会话在不在都不影响历史数字。
6
+ * 不寄在会话日志上:用户会清理已归档的会话,一旦读到增量就落进这个文件,之后会话在不在都不影响
7
+ * 历史数字。口径与桌面 dsh-context 的每日账本一致:一天的账落在「观测到增量」的宿主本地日期上,
8
+ * 计费 token = 未命中输入 + 缓存读 + 缓存写 + 输出。
8
9
  *
9
- * 口径(与桌面 dsh-context 的每日账本一致,只保留手机用得着的部分):一天的账落在「观测到
10
- * 增量」的宿主本地日期上;计费 token = 未命中输入 + 缓存读 + 缓存写 + 输出,四个桶分开存;
11
- * 每条会话一条记录,范围内有量的都上卡片 —— 卡片条数就是页面那个「会话」数字,所以
12
- * **不按展示需要裁剪**(裁了那个数字就变成上限值了,不再是真的条数)。
13
- *
14
- * 第三张表是模型维度 `models`:按「渠道 + 模型」攒量,服务统计页那块「模型用量」。它账的是
15
- * 同一个增量 —— 也就是说这里没有「按模型拆过一遍」的原始数据,模型名取自**采集那一刻**这条
16
- * 会话正在用的那个选择(见 stats-collect.cjs 文件头),会话中途换模型的那一段整体归到换后的
17
- * 那个名下。这是水位法本来就有的近似,不是这里另加的简化。
18
- *
19
- * 金额(`cost`)是**估算**,不落盘:折算时按模型桶的四个 token 桶乘单价(`model-prices.cjs`),
20
- * 价格变了下次出数就跟着变,账本里不留一份会过期的钱。模型归属的近似照旧继承下来;查不到单价
21
- * 的那一截 token 由 `unpricedBilled` 明说,不摊进合计。
22
- *
23
- * 只做纯数据(读写 + 折算),不碰网络;采集在 stats-collect.cjs,路由在 stats.js。
10
+ * 每条会话一条记录,范围内有量的都上卡片,**不按展示裁剪**(卡片条数就是页面那个「会话」数字)。
11
+ * 第三张表 `models` 按「渠道 + 模型」攒量,模型名取自采集那一刻这条会话的选择;金额是估算、不落盘,只做纯数据、不碰网络,采集在 stats-collect.cjs。
24
12
  */
25
13
 
26
14
  const fs = require('node:fs');
@@ -120,12 +108,9 @@ function modelKeyOf(provider, model) {
120
108
  /**
121
109
  * 模型选择的清洗:provider / model 缺一个才算没有,展示名缺了留空串。
122
110
  *
123
- * 展示名不算有效性判据:目录里那条模型下架了、渠道被卸载了,都只是「名字对不上」,这条用量
124
- * 该记在哪个渠道哪个模型名下仍然是确定的。以前把「展示名齐不齐」当成归属是否有效的判据,
125
- * 于是这种记录一重启读盘就变回「没有模型」,下一轮又触发一次老账本回填,同一段累计量在
126
- * 模型桶里翻倍。
127
- *
128
- * 展示名按「非空才覆盖」的规矩用,空串不能把已经记下的名字擦掉。
111
+ * 展示名不算有效性判据:目录下架、渠道卸载只是「名字对不上」,这条用量记在哪个渠道哪个模型名下
112
+ * 仍是确定的。别拿「展示名齐不齐」当归属判据 —— 那样记录一重启读盘就变回「没有模型」,下一轮
113
+ * 又触发老账本回填,同一段累计量在模型桶里翻倍。展示名按「非空才覆盖」用,空串不能擦掉已知名。
129
114
  */
130
115
  function sanitizeModelSelection(source) {
131
116
  if (!source || typeof source !== 'object') return null;
@@ -185,11 +170,25 @@ function addDays(day, delta) {
185
170
  /** 范围起点(含);`all` 返回 null,表示不设下限。 */
186
171
  function startDayOf(range, now) {
187
172
  const today = dayKeyOf(now);
173
+ if (range === 'today') return today;
174
+ if (range === 'yesterday') return addDays(today, -1);
175
+ if (range === '3d') return addDays(today, -2);
188
176
  if (range === '7d') return addDays(today, -6);
189
177
  if (range === '30d') return addDays(today, -29);
190
178
  return null;
191
179
  }
192
180
 
181
+ /**
182
+ * 范围终点(含):只有「昨天」不看今天。
183
+ *
184
+ * 逐日补零、合计、`usageInRange` 都走它。单看昨天却把今天的量算进去,是这一档最容易错的
185
+ * 地方 —— 用户多半正是用着这台机器的时候打开统计页。
186
+ */
187
+ function endDayOf(range, now) {
188
+ const today = dayKeyOf(now);
189
+ return range === 'yesterday' ? addDays(today, -1) : today;
190
+ }
191
+
193
192
  /**
194
193
  * `YYYY-MM-DD` → 那天本地零点的毫秒数。
195
194
  *
@@ -516,18 +515,18 @@ function moneyOf(value) {
516
515
  }
517
516
 
518
517
  /**
519
- * 一条会话在 [startDay, today] 里的用量。
518
+ * 一条会话在 [startDay, endDay] 里的用量。
520
519
  *
521
520
  * `startDay` 为 null(`all`)时直接用记录里的累计值 —— 累计值是权威,而它那份按天用量
522
521
  * 只保留最近 `MAX_DAILY_DAYS` 天。
523
522
  */
524
- function usageInRange(record, startDay, today) {
523
+ function usageInRange(record, startDay, endDay) {
525
524
  const billed = record.billed || emptyUsage();
526
525
  if (!startDay) return sanitizeUsage(billed);
527
526
  const usage = emptyUsage();
528
527
  const daily = record.daily || {};
529
528
  for (const [day, value] of Object.entries(daily)) {
530
- if (day < startDay || day > today) continue;
529
+ if (day < startDay || day > endDay) continue;
531
530
  for (const key of USAGE_KEYS) usage[key] += num(value[key]);
532
531
  }
533
532
  return usage;
@@ -544,6 +543,7 @@ function usageInRange(record, startDay, today) {
544
543
  function summarize(ledger, range, now) {
545
544
  const today = dayKeyOf(now);
546
545
  const startDay = startDayOf(range, now);
546
+ const endDay = endDayOf(range, now);
547
547
  const allDays = Object.keys(ledger.days).sort();
548
548
  const firstDay = startDay || allDays[0] || today;
549
549
 
@@ -559,7 +559,7 @@ function summarize(ledger, range, now) {
559
559
  requests: num(source.requests),
560
560
  turns: num(source.turns),
561
561
  });
562
- if (cursor >= today) break;
562
+ if (cursor >= endDay) break;
563
563
  }
564
564
 
565
565
  const kpi = {
@@ -604,7 +604,7 @@ function summarize(ledger, range, now) {
604
604
  let composition = null;
605
605
  let compositionSession = null;
606
606
  for (const [sessionId, record] of Object.entries(ledger.sessions)) {
607
- const usage = usageInRange(record, startDay, today);
607
+ const usage = usageInRange(record, startDay, endDay);
608
608
  if (record.composition && num(record.lastAt) > num(compositionSession && compositionSession.lastAt)) {
609
609
  composition = {
610
610
  systemTokens: num(record.composition.systemTokens),
@@ -658,7 +658,7 @@ function summarize(ledger, range, now) {
658
658
  let costPricedRows = 0;
659
659
  for (const bucket of Object.values(ledger.models || {})) {
660
660
  if (!bucket || typeof bucket !== 'object') continue;
661
- const usage = usageInRange(bucket, startDay, today);
661
+ const usage = usageInRange(bucket, startDay, endDay);
662
662
  const billedTokens = billedOf(usage);
663
663
  const turns = num(usage.turns);
664
664
  // 与卡片同一条规矩:范围内一个 token、一轮都没有的组合不上卡。
@@ -1,21 +1,25 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * GET /api/mobile/v1/stats/overview?range=7d|30d|all[&fresh=1](token)
4
+ * GET /api/mobile/v1/stats/overview?range=today|yesterday|3d|7d|30d|all[&fresh=1](token)
5
5
  *
6
6
  * 统计页的唯一接口:先采一轮(水位差记账),再把账本按范围折算成页面要的那份。
7
7
  *
8
8
  * 采集失败**不**让整页报错:账本里已有的数字照样是真的,宁可让用户看到一份偏旧的统计,
9
9
  * 也不要为了「这次没采上」把整页变成错误页。要重新采就用 `fresh=1`(下拉刷新那条)。
10
10
  *
11
- * 范围词只有三档,别的值一律按 `7d`:多一档就要在页面和这里各维护一份日历口径。
11
+ * 范围词是 App 与这里共用的一份词汇表(`app/src/types/stats.ts` 的 `StatsRange`):
12
+ * 「今天 / 昨天」是单天窗口,`3d` / `7d` / `30d` 是含今天往前数的天窗,`all` 不设下限。
13
+ * 日历口径只有 `stats-ledger.cjs` 的 `startDayOf` / `endDayOf` 一份,App 不自己算。
14
+ * 认不出来的词一律按 `7d`(老 App 照旧发那三档);**新范围词遇到老插件就会落到这一条**,
15
+ * 所以 App 拿回包里的 `range` 与请求的对不对得上当作「插件较旧」的判据(见 A2-60)。
12
16
  */
13
17
 
14
18
  const { sendJson } = require('../http/routing.js');
15
19
  const { collectStats, DEFAULT_BUDGET_MS } = require('./stats-collect.cjs');
16
20
  const ledger = require('./stats-ledger.cjs');
17
21
 
18
- const RANGES = new Set(['7d', '30d', 'all']);
22
+ const RANGES = new Set(['today', 'yesterday', '3d', '7d', '30d', 'all']);
19
23
 
20
24
  function normalizeRange(value) {
21
25
  return typeof value === 'string' && RANGES.has(value) ? value : '7d';