@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.
@@ -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 restored = restoreOwnedProfile({ profileDir, snapshotDir: snapDir, snapshot: profileSnapshot, lockPath, lease });
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
- // 不依赖 pnpm 把依赖内联进插件目录(真实 pnpm 会放到 sibling,containment 判据必错)。
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", "dsh-mobile-remote");
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
- const r = spawnSync("pnpm", ["install"], { cwd: profileDir, encoding: "utf8" });
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) failure = `restart-failed: ${result?.error ?? "unknown"}`;
209
- else {
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) return finish(TRANSACTION_PHASE.SETTLED, { ok: true, matched: healthy.matched, at: new Date(now()).toISOString() }, [entryPhase]);
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, lockPath, lease, runInstall });
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) return finish(TRANSACTION_PHASE.FAILED, { ok: false, reason: "rollback-restore-failed", error: restored?.error }, ["rolling-back"]);
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) return finish(TRANSACTION_PHASE.FAILED, { ok: false, reason: "rollback-restart-failed", error: result?.error }, ["rolling-back"]);
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 log = (m) => console.log(m);
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-alpha.3",
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-alpha.3-app"
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",