dsh-mobile-plugin 0.1.95 → 0.1.97
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 +73 -50
- package/config/agent-presets/standard/agent.cordis.yml +13 -0
- package/dsh-mobile-credentials/lib/credentials.js +24 -1
- package/lib/git/exec.js +129 -0
- package/lib/git/package.json +3 -0
- package/lib/git/parse.js +247 -0
- package/lib/git/read.js +617 -0
- package/lib/impl.js +2 -2
- package/lib/mirror/pusher.cjs +25 -2
- package/lib/mobile/archive-clean.js +31 -1
- package/lib/mobile/channels.js +583 -0
- package/lib/mobile/events.js +10 -8
- package/lib/mobile/git-repos.js +210 -0
- package/lib/mobile/git.js +603 -0
- package/lib/mobile/history-derive.js +79 -1
- package/lib/mobile/host-bridge/endpoints.js +3 -0
- package/lib/mobile/host-bridge/event-synthesis.js +109 -1
- package/lib/mobile/host-bridge/shims.js +9 -2
- package/lib/mobile/host-bridge/transport.js +30 -1
- package/lib/mobile/host-settings.js +41 -0
- package/lib/mobile/inbox-clean.js +337 -0
- package/lib/mobile/pairing.js +18 -1
- package/lib/mobile/questions.js +31 -9
- package/lib/mobile/schedule.js +440 -0
- package/lib/mobile/session-cache.js +18 -0
- package/lib/mobile/session-file.js +192 -0
- package/lib/mobile/session-model-options.js +184 -0
- package/lib/mobile/session-models.js +24 -2
- package/lib/mobile/session-schedule-mark.js +70 -0
- package/lib/mobile/session-snapshot.js +12 -1
- package/lib/mobile/sessions.js +8 -0
- package/lib/mobile/stats-collect.cjs +257 -0
- package/lib/mobile/stats-ledger.cjs +407 -0
- package/lib/mobile/stats.js +38 -0
- package/lib/mobile/stream.js +18 -10
- package/lib/mobile/uploads.js +24 -6
- package/lib/mobile/workspaces.js +30 -6
- package/lib/panel/index.html +4 -4
- package/lib/relay/lan-proxy.js +10 -1
- package/lib/routes/control-plane.js +38 -0
- package/lib/routes/mobile-api.js +70 -1
- package/lib/session-cleanup.cjs +8 -0
- package/lib/shared/sse-write.js +17 -6
- package/lib/shell/updater.js +3 -3
- package/package.json +3 -2
package/lib/mirror/pusher.cjs
CHANGED
|
@@ -90,6 +90,14 @@ function createMirrorPusher(deps) {
|
|
|
90
90
|
* 别让一处漏接线的装配把镜像悄悄降成 5s 一份。
|
|
91
91
|
*/
|
|
92
92
|
const lastMobileAt = typeof deps.lastMobileAt === 'function' ? deps.lastMobileAt : null;
|
|
93
|
+
/**
|
|
94
|
+
* 本实例签发过、仍然有效的设备凭据摘要(凭据库 activeDigests 的形态)。
|
|
95
|
+
*
|
|
96
|
+
* 中转加了离线镜像缓存后,桌面还没上线时也要能判断「来读这份缓存的人是不是配对过的
|
|
97
|
+
* 手机」,而它手上只有手机发来的 `dshc_` 原文 —— 摘要随镜像给上去,它才能自己算 sha256
|
|
98
|
+
* 比对。不传就按「一份都没有」处理:镜像照推,只是中转认不出任何缓存读者。
|
|
99
|
+
*/
|
|
100
|
+
const credentialDigests = typeof deps.credentialDigests === 'function' ? deps.credentialDigests : () => [];
|
|
93
101
|
const hasAudience = () => {
|
|
94
102
|
if (listSignal.watching()) return true;
|
|
95
103
|
if (!lastMobileAt) return true;
|
|
@@ -129,6 +137,12 @@ function createMirrorPusher(deps) {
|
|
|
129
137
|
// 消息水位/条数 + 统计投影 + 模型 revision),不含 `at` 之类的时间戳,所以「同一份
|
|
130
138
|
// 数据」永远得到同一个指纹。只记成功推送的指纹:失败的那次必须能在下一 tick 重试。
|
|
131
139
|
let lastPushedFingerprint = null;
|
|
140
|
+
// 上一次推上去的摘要清单(判重键见 credsKeyOf)。**刻意不并进 pushFingerprint**:那是与
|
|
141
|
+
// 中转共享的载荷指纹,收的是镜像内容;摘要另立一份,撤销一台设备后哪怕清单一个字都没变,
|
|
142
|
+
// 新摘要也能在一个 tick 内传导过去(否则被撤销的手机还能继续读离线镜像)。
|
|
143
|
+
let lastPushedCreds = null;
|
|
144
|
+
/** 摘要清单的判重键:条数、顺序、过期时间任一变化都算新内容。 */
|
|
145
|
+
const credsKeyOf = (creds) => creds.map((entry) => (entry.expiresAt ? `${entry.hash}@${entry.expiresAt}` : entry.hash)).join(',');
|
|
132
146
|
// 「中转镜像里那份清单 / 工作区是哪一版」——同样只记真的推出去(或被判重跳过)的那一版,
|
|
133
147
|
// 供 processBgQueue 的门判断这一轮有没有必要重建再推一份。判据必须是 revision 而不是
|
|
134
148
|
// pushFingerprint:后者含轮转会话的消息水位与统计投影,每轮都不同。
|
|
@@ -238,13 +252,19 @@ function createMirrorPusher(deps) {
|
|
|
238
252
|
const workspacesRevision = payload.workspaces && typeof payload.workspaces.revision === 'string'
|
|
239
253
|
? payload.workspaces.revision
|
|
240
254
|
: '';
|
|
255
|
+
// 摘要只在这一处塞进载荷:这里是唯一出口,三条推送路径(事件、整批、后台 tick)自然
|
|
256
|
+
// 都带上,谁也不必记得自己补。放在隧道判定之后 —— 断线时这份清单没有任何去处,
|
|
257
|
+
// 白读一趟凭据库。
|
|
258
|
+
payload.creds = credentialDigests();
|
|
259
|
+
const credsKey = credsKeyOf(payload.creds);
|
|
241
260
|
const fingerprint = pushFingerprint(payload);
|
|
242
|
-
if (fingerprint === lastPushedFingerprint) {
|
|
261
|
+
if (fingerprint === lastPushedFingerprint && credsKey === lastPushedCreds) {
|
|
243
262
|
// 跳过不等于故障:内容与上一份逐字段相同。但 pushedAt 必须继续走,它是运维与
|
|
244
263
|
// App 诊断「镜像是否还在更新」的唯一抓手,停在这里会让它看起来像镜像死了。
|
|
245
264
|
recordMirrorPush(true, '内容未变,跳过重复推送');
|
|
246
265
|
lastPushedListRevision = listRevision;
|
|
247
266
|
lastPushedWorkspacesRevision = workspacesRevision;
|
|
267
|
+
lastPushedCreds = credsKey;
|
|
248
268
|
announceSessionActivity(payload.messages);
|
|
249
269
|
return false;
|
|
250
270
|
}
|
|
@@ -255,6 +275,7 @@ function createMirrorPusher(deps) {
|
|
|
255
275
|
throw error
|
|
256
276
|
}
|
|
257
277
|
lastPushedFingerprint = fingerprint;
|
|
278
|
+
lastPushedCreds = credsKey;
|
|
258
279
|
recordMirrorPush(true, null)
|
|
259
280
|
// 基准只在真的推进去(或被判重跳过)之后才前移:推送失败必须能在下一轮重试,
|
|
260
281
|
// 否则「清单变了」会被一次失败吃掉,中转镜像永远停在旧版(见 processBgQueue 的门)。
|
|
@@ -679,7 +700,9 @@ function createMirrorPusher(deps) {
|
|
|
679
700
|
const retryId = dueRetries(now).find((id) => list.some((s) => String(s.sessionId) === id));
|
|
680
701
|
if (retryId) await pushSessionNow(retryId, 'batch');
|
|
681
702
|
const coldRotateDue = now - lastColdRotateAt >= BG_COLD_ROTATE_MIN_INTERVAL_MS;
|
|
682
|
-
|
|
703
|
+
// 摘要清单也是一份要推的内容:撤销一台设备时清单往往一个字都没变,只看 revision
|
|
704
|
+
// 会把这一轮挡回去,被撤销的手机要等到下一个轮转窗口才失去离线镜像。
|
|
705
|
+
if (revision === lastPushedListRevision && workspacesRevision === lastPushedWorkspacesRevision && credsKeyOf(credentialDigests()) === lastPushedCreds && !coldRotateDue) {
|
|
683
706
|
// 清单与工作区都没变、也还没到轮转时间:这一轮除了刚做的重试什么都不用做。
|
|
684
707
|
if (retryId) announceSessionsRevision(revision);
|
|
685
708
|
return;
|
|
@@ -20,8 +20,25 @@ const { withIdempotency, requestIdOf } = require('./idempotency.js');
|
|
|
20
20
|
const { canonicalDeviceKey, rememberDeletedSessionIds } = require('./session-cache.js');
|
|
21
21
|
const { invalidateSessionList } = require('./sessions.js');
|
|
22
22
|
const { unwrap } = require('./envelope.js');
|
|
23
|
+
const { collectStats } = require('./stats-collect.cjs');
|
|
23
24
|
const { queueArchivedClearIds } = require('../session-cleanup.cjs');
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 清理前这次补采的预算。
|
|
28
|
+
*
|
|
29
|
+
* 统计页那次只给 `DEFAULT_BUDGET_MS`(1.5 秒)是为了别让用户点开统计转好久;清理是
|
|
30
|
+
* 不可恢复的删除,值得多等几秒把账补全。上限卡在 App 单发请求的 8 秒总预算之内
|
|
31
|
+
* (`app/src/api/core.ts` 的 `REQUEST_TIMEOUT_MS`),否则手机先报超时、清理由这边照旧
|
|
32
|
+
* 执行,用户会以为没成功。
|
|
33
|
+
*
|
|
34
|
+
* 同时这次补采跳过「最后一句」预览(`skipPreview`):那一步要读会话日志,是整轮里
|
|
35
|
+
* 最重的一趟。删日志前先把数字记住,比给即将消失的卡片多留一行摘要重要。
|
|
36
|
+
*
|
|
37
|
+
* 极端多的归档(几千条)仍可能到点停手 —— 彻底兜住要把补采挪到请求之外,那是另一套
|
|
38
|
+
* 生命周期;这里先把「几百条」这个真实量级覆盖掉,不让它再沿用统计页的 1.5 秒。
|
|
39
|
+
*/
|
|
40
|
+
const CLEAR_ARCHIVED_COLLECT_BUDGET_MS = 6_000;
|
|
41
|
+
|
|
25
42
|
/**
|
|
26
43
|
* POST /api/mobile/v1/workspace/clear-archived(token)
|
|
27
44
|
* body: { confirm: true } → { ok: true, deleted: N }。confirm 不为 true 一律 400。
|
|
@@ -44,6 +61,19 @@ async function handleClearArchived(ctx, req, res) {
|
|
|
44
61
|
const archivedIds = Array.isArray(workspaceResponse.value?.archivedSessionIds) ? workspaceResponse.value.archivedSessionIds : [];
|
|
45
62
|
const selected = archivedIds.filter((id) => typeof id === 'string' && id);
|
|
46
63
|
if (selected.length === 0) return { status: 200, body: { ok: true, deleted: 0 } };
|
|
64
|
+
// 清理之前先把这批会话的用量最后采一次:之后再读它们的统计就没了(日志要删),
|
|
65
|
+
// 而统计账本要保留历史 —— 漏掉这一刀,最后一段用量会凭空消失。
|
|
66
|
+
// 采不上不挡清理:统计偏旧好过用户点不动这个按钮。
|
|
67
|
+
try {
|
|
68
|
+
await collectStats(ctx, {
|
|
69
|
+
sessionIds: selected,
|
|
70
|
+
force: true,
|
|
71
|
+
budgetMs: CLEAR_ARCHIVED_COLLECT_BUDGET_MS,
|
|
72
|
+
skipPreview: true,
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
/* 旁路 */
|
|
76
|
+
}
|
|
47
77
|
// 排不进队列就整次失败(路由回 500):回「已清理」却没排上,重启之后它们原样回来。
|
|
48
78
|
queueArchivedClearIds(selected);
|
|
49
79
|
// 宿主那份快照可能还带着它们,先在本进程压住:手机列表与归档列表立刻看不到。
|
|
@@ -55,4 +85,4 @@ async function handleClearArchived(ctx, req, res) {
|
|
|
55
85
|
sendJson(res, outcome.status, outcome.body);
|
|
56
86
|
}
|
|
57
87
|
|
|
58
|
-
module.exports = { handleClearArchived };
|
|
88
|
+
module.exports = { handleClearArchived, CLEAR_ARCHIVED_COLLECT_BUDGET_MS };
|