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.
- package/config/agent-presets/standard/agent.cordis.yml +9 -0
- package/host.js +26 -4
- package/lib/canvas-app.html +441 -0
- package/lib/client.js +40 -1
- package/lib/host-compat.cjs +51 -4
- package/lib/mobile/attachment-store.js +111 -15
- package/lib/mobile/attachments.js +6 -2
- package/lib/mobile/cli/config-prefs.js +4 -21
- package/lib/mobile/cli/mobile.js +36 -4
- package/lib/mobile/host-bridge/endpoints.js +7 -1
- package/lib/mobile/local-images.js +1 -1
- package/lib/mobile/magpie-web.js +425 -0
- package/lib/mobile/phone/tools.js +94 -0
- package/lib/mobile/questions.js +9 -3
- package/lib/mobile/session-send.js +13 -13
- package/lib/mobile/stats-ledger.cjs +29 -29
- package/lib/mobile/stats.js +7 -3
- package/lib/panel/index.html +8 -8
- package/lib/routes/canvas-desktop.js +196 -0
- package/lib/routes/control-plane.js +40 -0
- package/lib/routes/mobile-api.js +6 -0
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
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,
|
|
518
|
+
* 一条会话在 [startDay, endDay] 里的用量。
|
|
520
519
|
*
|
|
521
520
|
* `startDay` 为 null(`all`)时直接用记录里的累计值 —— 累计值是权威,而它那份按天用量
|
|
522
521
|
* 只保留最近 `MAX_DAILY_DAYS` 天。
|
|
523
522
|
*/
|
|
524
|
-
function usageInRange(record, startDay,
|
|
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 >
|
|
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 >=
|
|
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,
|
|
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,
|
|
661
|
+
const usage = usageInRange(bucket, startDay, endDay);
|
|
662
662
|
const billedTokens = billedOf(usage);
|
|
663
663
|
const turns = num(usage.turns);
|
|
664
664
|
// 与卡片同一条规矩:范围内一个 token、一轮都没有的组合不上卡。
|
package/lib/mobile/stats.js
CHANGED
|
@@ -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
|
-
*
|
|
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';
|