@marktrue/dsh-mobile-remote 3.2.1-alpha.3 → 3.2.1-rc.1
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/CHANGELOG.md +25 -0
- package/lib/app-carrier.js +7 -0
- package/lib/client.js +468 -0
- package/lib/endpoint-registry.js +5 -2
- package/lib/index.js +69 -23
- package/lib/npm-update-source.js +226 -0
- package/lib/plugin-self-update.js +313 -74
- package/lib/plugin-update-controller.js +200 -21
- package/lib/plugin-update-log.js +164 -0
- package/lib/plugin-update-transaction.js +4 -4
- package/lib/tool-process.js +20 -0
- package/lib/update-lock-arbiter.js +15 -3
- package/lib/update-supervisor.js +48 -14
- package/package.json +2 -2
package/lib/update-supervisor.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
17
|
+
import { appendOperationLog } from "./plugin-update-log.js";
|
|
17
18
|
import {
|
|
18
19
|
TRANSACTION_PHASE,
|
|
19
20
|
acquireLock,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
import { pluginContentDigest } from "./plugin-content-digest.js";
|
|
36
37
|
import { verifyFrozenPackage } from "./runtime-dependency-snapshot.js";
|
|
37
38
|
import { restartPlan } from "./plugin-self-update.js";
|
|
39
|
+
import { shellOptions } from "./tool-process.js";
|
|
38
40
|
|
|
39
41
|
/** 等待宿主的默认超时:足够冷启动(含语料预热),又不至于让用户无限等。 */
|
|
40
42
|
export const DEFAULT_WAIT_TIMEOUT_MS = 180_000;
|
|
@@ -154,21 +156,23 @@ export function defaultRestart({ restartForm, port, pid = null, profile = "web",
|
|
|
154
156
|
* 默认恢复器(F4):把依赖指回**快照**(而不是可能已变的原 clone/tag/source),
|
|
155
157
|
* 再重装;并检查安装结果。
|
|
156
158
|
*/
|
|
157
|
-
export async function defaultRestore({ profileDir, snapshotDir: snapDir, expectedContentSha256, profileSnapshot, lockPath, lease }) {
|
|
159
|
+
export async function defaultRestore({ profileDir, snapshotDir: snapDir, expectedContentSha256, profileSnapshot, dependencyKey, preserveSpecifier = false, lockPath, lease }) {
|
|
158
160
|
if (!snapDir || !snapshotReady(snapDir) || (expectedContentSha256 && pluginContentDigest(snapDir) !== expectedContentSha256)) {
|
|
159
161
|
return { ok: false, error: "snapshot-unavailable" };
|
|
160
162
|
}
|
|
161
163
|
try {
|
|
162
164
|
assertUpdateLease(lockPath, lease);
|
|
163
165
|
// 一次受代际保护的仲裁里恢复 profile/lockfile(A 阻塞:不再有 assert→write 空窗)。
|
|
164
|
-
const
|
|
166
|
+
const restoreKey = dependencyKey || profileSnapshot?.dependencyKey || 'dsh-mobile-remote';
|
|
167
|
+
const restored = restoreOwnedProfile({ profileDir, snapshotDir: snapDir, snapshot: profileSnapshot, key: restoreKey,
|
|
168
|
+
preserveSpecifier, lockPath, lease });
|
|
165
169
|
if (!restored.ok) return { ok: false, error: restored.error };
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
const published = publishOwnedSnapshotLink({ profileDir, snapshotDir: snapDir, lockPath, lease });
|
|
170
|
+
// 直接把**已验证自包含**的快照发布为安装目录:旧字节精确、离线可加载;
|
|
171
|
+
// npm 事务保留原精确 registry spec,legacy 事务继续使用 file: 快照 spec。
|
|
172
|
+
const published = publishOwnedSnapshotLink({ profileDir, snapshotDir: snapDir, key: restoreKey, lockPath, lease });
|
|
169
173
|
if (!published.ok) return { ok: false, error: published.error };
|
|
170
174
|
assertUpdateLease(lockPath, lease);
|
|
171
|
-
const actualRoot = join(profileDir, "node_modules", "
|
|
175
|
+
const actualRoot = join(profileDir, "node_modules", ...restoreKey.split("/"));
|
|
172
176
|
if (pluginContentDigest(actualRoot) !== expectedContentSha256) return { ok: false, error: "restored-content-mismatch" };
|
|
173
177
|
const verified = verifyFrozenPackage(actualRoot);
|
|
174
178
|
if (!verified.ok) return { ok: false, error: `restored-snapshot-invalid: ${verified.error}` };
|
|
@@ -178,7 +182,8 @@ export async function defaultRestore({ profileDir, snapshotDir: snapDir, expecte
|
|
|
178
182
|
|
|
179
183
|
/** 默认安装器(监督器是独立进程,阻塞没关系)。 */
|
|
180
184
|
export function defaultRunInstall(profileDir) {
|
|
181
|
-
|
|
185
|
+
// Windows 上 pnpm 是 pnpm.cmd,必须经 shell 解析。
|
|
186
|
+
const r = spawnSync("pnpm", ["install"], { cwd: profileDir, encoding: "utf8", ...shellOptions() });
|
|
182
187
|
if (r.error) return { ok: false, error: r.error.message };
|
|
183
188
|
if (r.status !== 0) return { ok: false, error: (r.stderr || `exit ${r.status}`).trim() };
|
|
184
189
|
return { ok: true };
|
|
@@ -203,14 +208,22 @@ export async function runTransaction({ stateFile, tx, timeoutMs = DEFAULT_WAIT_T
|
|
|
203
208
|
// 已经处于 rolling-back(安装失败/恢复重试):**直接续做恢复**,不重复"先重启再等待"。
|
|
204
209
|
if (tx.phase !== TRANSACTION_PHASE.ROLLING_BACK) {
|
|
205
210
|
const entryPhase = phaseAt();
|
|
211
|
+
log("supervisor: restarting host to load candidate plugin");
|
|
206
212
|
const result = await restart({ ...tx, pid: tx.hostPid, log });
|
|
207
213
|
assertUpdateLease(lockPath, lease);
|
|
208
|
-
if (!result?.ok)
|
|
209
|
-
|
|
214
|
+
if (!result?.ok) {
|
|
215
|
+
log("supervisor: candidate restart failed");
|
|
216
|
+
failure = `restart-failed: ${result?.error ?? "unknown"}`;
|
|
217
|
+
} else {
|
|
218
|
+
log("supervisor: waiting for candidate health confirmation");
|
|
210
219
|
const healthy = await waitImpl({ ...tx, expectContentSha256: tx.targetContentSha256,
|
|
211
220
|
expectVersion: tx.targetVersion, timeoutMs, pollMs });
|
|
212
221
|
assertUpdateLease(lockPath, lease);
|
|
213
|
-
if (healthy.ok)
|
|
222
|
+
if (healthy.ok) {
|
|
223
|
+
log("supervisor: candidate plugin health confirmed");
|
|
224
|
+
return finish(TRANSACTION_PHASE.SETTLED, { ok: true, matched: healthy.matched, at: new Date(now()).toISOString() }, [entryPhase]);
|
|
225
|
+
}
|
|
226
|
+
log("supervisor: candidate health check failed; rollback required");
|
|
214
227
|
failure = healthy.error;
|
|
215
228
|
}
|
|
216
229
|
if (!tx.previousSnapshotDir) return finish(TRANSACTION_PHASE.FAILED, { ok: false, reason: "rollback-target-missing", error: failure }, [entryPhase]);
|
|
@@ -227,16 +240,28 @@ export async function runTransaction({ stateFile, tx, timeoutMs = DEFAULT_WAIT_T
|
|
|
227
240
|
const claimed = claimOwnedRecovery(stateFile, { owner }, { lockPath, lease, currentFile: tx.currentFile });
|
|
228
241
|
if (!claimed.ok) return { ok: false, error: claimed.error, transaction: claimed.transaction };
|
|
229
242
|
tx = claimed.transaction;
|
|
243
|
+
log("supervisor: restoring previous plugin snapshot");
|
|
230
244
|
const restored = await restore({ profileDir: tx.profileDir, snapshotDir: tx.previousSnapshotDir,
|
|
231
|
-
expectedContentSha256: tx.previousContentSha256, profileSnapshot: tx.profileSnapshot,
|
|
245
|
+
expectedContentSha256: tx.previousContentSha256, profileSnapshot: tx.profileSnapshot,
|
|
246
|
+
dependencyKey: tx.dependencyKey || tx.profileSnapshot?.dependencyKey,
|
|
247
|
+
preserveSpecifier: Boolean(tx.npmCandidate), lockPath, lease, runInstall });
|
|
232
248
|
assertUpdateLease(lockPath, lease);
|
|
233
|
-
if (!restored?.ok)
|
|
249
|
+
if (!restored?.ok) {
|
|
250
|
+
log("supervisor: previous plugin snapshot restore failed");
|
|
251
|
+
return finish(TRANSACTION_PHASE.FAILED, { ok: false, reason: "rollback-restore-failed", error: restored?.error }, ["rolling-back"]);
|
|
252
|
+
}
|
|
253
|
+
log("supervisor: restarting host with previous plugin");
|
|
234
254
|
const result = await restart({ ...tx, pid: null, log });
|
|
235
255
|
assertUpdateLease(lockPath, lease);
|
|
236
|
-
if (!result?.ok)
|
|
256
|
+
if (!result?.ok) {
|
|
257
|
+
log("supervisor: rollback restart failed");
|
|
258
|
+
return finish(TRANSACTION_PHASE.FAILED, { ok: false, reason: "rollback-restart-failed", error: result?.error }, ["rolling-back"]);
|
|
259
|
+
}
|
|
260
|
+
log("supervisor: waiting for previous plugin health confirmation");
|
|
237
261
|
const back = await waitImpl({ ...tx, expectContentSha256: tx.previousContentSha256,
|
|
238
262
|
expectVersion: "", timeoutMs, pollMs });
|
|
239
263
|
assertUpdateLease(lockPath, lease);
|
|
264
|
+
log(back.ok ? "supervisor: previous plugin health confirmed" : "supervisor: previous plugin health not confirmed");
|
|
240
265
|
return finish(back.ok ? TRANSACTION_PHASE.ROLLED_BACK : TRANSACTION_PHASE.FAILED,
|
|
241
266
|
{ ok: false, reason: back.ok ? "rolled-back" : "rollback-unconfirmed", error: back.ok ? failure : back.error }, ["rolling-back"]);
|
|
242
267
|
} catch(e) {
|
|
@@ -297,7 +322,16 @@ async function main() {
|
|
|
297
322
|
}
|
|
298
323
|
const timeoutMs = Number(args.timeout ?? 0) > 0 ? Number(args.timeout) * 1000 : DEFAULT_WAIT_TIMEOUT_MS;
|
|
299
324
|
const pollMs = Number(args.poll ?? 0) > 0 ? Number(args.poll) : DEFAULT_POLL_MS;
|
|
300
|
-
const
|
|
325
|
+
const txAtStart = readTransaction(stateFile);
|
|
326
|
+
const generation = txAtStart?.lease?.generation ?? args.generation;
|
|
327
|
+
const currentFile = txAtStart?.currentFile ?? "";
|
|
328
|
+
const logHome = currentFile ? dirname(dirname(dirname(currentFile))) : (process.env.HOME ?? "");
|
|
329
|
+
const log = (message) => {
|
|
330
|
+
if (logHome && generation) {
|
|
331
|
+
try { appendOperationLog({ home: logHome, generation, stage: "supervisor", text: message, redactPaths: [logHome, txAtStart?.profileDir] }); } catch { /* progress logging must not block recovery */ }
|
|
332
|
+
}
|
|
333
|
+
console.log("supervisor: progress event recorded");
|
|
334
|
+
};
|
|
301
335
|
const finalState = args.arm
|
|
302
336
|
? await superviseArmed({ stateFile, readyFile: args.ready, opId: args.op, generation: args.generation, timeoutMs, pollMs, log })
|
|
303
337
|
: await superviseUpdate({ stateFile, opId: args.op, generation: args.generation, timeoutMs, pollMs, log });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marktrue/dsh-mobile-remote",
|
|
3
|
-
"version": "3.2.1-
|
|
3
|
+
"version": "3.2.1-rc.1",
|
|
4
4
|
"description": "DSH Mobile:把 DeepSeek Harness 变成随身控制台——手机远程发消息派活、看进度、收通知、审批决策、管理会话;配套 Flutter App(当前 Android 端,iOS 未开发)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"qrcode"
|
|
53
53
|
],
|
|
54
54
|
"optionalDependencies": {
|
|
55
|
-
"@marktrue/dsh-mobile-remote-app": "npm:@marktrue/dsh-mobile-remote@3.2.1-
|
|
55
|
+
"@marktrue/dsh-mobile-remote-app": "npm:@marktrue/dsh-mobile-remote@3.2.1-rc.1-app"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@deepseek-ai/dsh-credentials": ">=0.1.0-rc.6 <0.3.0",
|