@linxin666/dsh-remote-web-ui 0.1.13 → 0.1.15

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.
Files changed (38) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +5 -2
  3. package/README.zh.md +1 -1
  4. package/lib/client.js +30 -17
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +177 -34
  7. package/lib/mobile.js +44 -25
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts +26 -11
  10. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  11. package/lib/types/client/PluginSettingsCard.js +27 -7
  12. package/lib/types/client/locales.js +2 -2
  13. package/lib/types/client/settings-form.d.ts +14 -5
  14. package/lib/types/client/settings-form.d.ts.map +1 -1
  15. package/lib/types/client/settings-form.js +38 -10
  16. package/lib/types/mobile/mux.d.ts +13 -4
  17. package/lib/types/mobile/mux.d.ts.map +1 -1
  18. package/lib/types/mobile/mux.js +50 -30
  19. package/lib/types/mobile/mux.test.js +75 -6
  20. package/lib/types/mobile-api.d.ts +2 -0
  21. package/lib/types/mobile-api.d.ts.map +1 -1
  22. package/lib/types/mobile-api.js +4 -1
  23. package/lib/types/routes.d.ts +17 -0
  24. package/lib/types/routes.d.ts.map +1 -1
  25. package/lib/types/routes.js +49 -10
  26. package/lib/types/update.d.ts +5 -1
  27. package/lib/types/update.d.ts.map +1 -1
  28. package/lib/types/update.js +122 -26
  29. package/package.json +1 -1
  30. package/src/client/PluginSettingsCard.tsx +83 -17
  31. package/src/client/locales.ts +2 -2
  32. package/src/client/settings-card.module.css +6 -0
  33. package/src/client/settings-form.ts +41 -9
  34. package/src/mobile/mux.test.ts +84 -6
  35. package/src/mobile/mux.ts +46 -25
  36. package/src/mobile-api.ts +6 -1
  37. package/src/routes.ts +51 -10
  38. package/src/update.ts +116 -26
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@ import { setInterval as setInterval$1 } from "node:timers";
3
3
  import { Service } from "@deepseek-ai/cordis";
4
4
  import z from "schemastery";
5
5
  import { randomBytes } from "node:crypto";
6
+ import { z as z$1 } from "zod";
6
7
  import { readFile } from "node:fs/promises";
7
8
  import { existsSync, readFileSync } from "node:fs";
8
9
  import { fileURLToPath } from "node:url";
@@ -638,6 +639,30 @@ const PAIR_PATHS = {
638
639
  status: "/api/pair/status",
639
640
  events: "/api/pair/events"
640
641
  };
642
+ /**
643
+ * /api/pair request payload contracts. Each POST endpoint validates its body
644
+ * against one of these instead of reaching into a hand-parsed object: the
645
+ * control-plane endpoints that carry no meaningful payload use the permissive
646
+ * pairActionPayloadSchema so their smoke calls keep working unchanged, while
647
+ * issue/accept enforce their optional/required fields. Unknown (extra) keys
648
+ * are tolerated exactly as the previous manual reads ignored them.
649
+ */
650
+ const issuePayloadSchema = z$1.object({
651
+ workspaceId: z$1.string().min(1).optional(),
652
+ address: z$1.string().min(1).optional()
653
+ });
654
+ const acceptPayloadSchema = z$1.object({ token: z$1.string().default("") });
655
+ const pairActionPayloadSchema = z$1.object({}).passthrough();
656
+ /**
657
+ * Parse a pair request body through schema. A missing/empty or non-object
658
+ * body is treated as an empty object (the same way the previous manual reads
659
+ * yielded absent fields), and a value that fails the schema returns
660
+ * `undefined` so the caller can answer with the existing error shape.
661
+ */
662
+ function parsePairPayload(schema, body) {
663
+ const result = schema.safeParse(body ?? {});
664
+ return result.success ? result.data : void 0;
665
+ }
641
666
  /** One JSON response. */
642
667
  function writeJson$1(res, status, body) {
643
668
  const payload = JSON.stringify(body);
@@ -762,8 +787,15 @@ function makeRoutes(deps) {
762
787
  return;
763
788
  }
764
789
  const body = await readJsonBody$1(req);
765
- const workspaceId = body === void 0 || typeof body.workspaceId !== "string" || body.workspaceId === "" ? void 0 : body.workspaceId;
766
- const address = body === void 0 || typeof body.address !== "string" || body.address === "" ? void 0 : body.address;
790
+ const payload = parsePairPayload(issuePayloadSchema, body);
791
+ if (payload === void 0) {
792
+ writeJson$1(res, 400, {
793
+ ok: false,
794
+ code: "bad-payload"
795
+ });
796
+ return;
797
+ }
798
+ const { workspaceId, address } = payload;
767
799
  try {
768
800
  const { token, expiresAt } = service.issue(workspaceId, address);
769
801
  const base = address === void 0 ? service.publicBaseUrl ?? service.lanBaseUrl : service.lanBaseUrlFor(address);
@@ -801,8 +833,15 @@ function makeRoutes(deps) {
801
833
  return;
802
834
  }
803
835
  const body = await readJsonBody$1(req);
804
- const token = typeof body?.token === "string" ? body.token : "";
805
- const result = service.accept(token);
836
+ const payload = parsePairPayload(acceptPayloadSchema, body);
837
+ if (payload === void 0) {
838
+ writeJson$1(res, 400, {
839
+ ok: false,
840
+ code: "bad-payload"
841
+ });
842
+ return;
843
+ }
844
+ const result = service.accept(payload.token);
806
845
  if (!result.ok) {
807
846
  writeJson$1(res, result.code === "used" ? 409 : 404, {
808
847
  ok: false,
@@ -828,7 +867,14 @@ function makeRoutes(deps) {
828
867
  });
829
868
  return;
830
869
  }
831
- await readJsonBody$1(req);
870
+ const body = await readJsonBody$1(req);
871
+ if (parsePairPayload(pairActionPayloadSchema, body) === void 0) {
872
+ writeJson$1(res, 400, {
873
+ ok: false,
874
+ code: "bad-payload"
875
+ });
876
+ return;
877
+ }
832
878
  service.stop();
833
879
  writeJson$1(res, 200, { ok: true });
834
880
  };
@@ -841,7 +887,14 @@ function makeRoutes(deps) {
841
887
  });
842
888
  return;
843
889
  }
844
- await readJsonBody$1(req);
890
+ const body = await readJsonBody$1(req);
891
+ if (parsePairPayload(pairActionPayloadSchema, body) === void 0) {
892
+ writeJson$1(res, 400, {
893
+ ok: false,
894
+ code: "bad-payload"
895
+ });
896
+ return;
897
+ }
845
898
  const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
846
899
  if (deviceId === void 0 || !service.heartbeat(deviceId)) {
847
900
  writeJson$1(res, 401, {
@@ -1008,6 +1061,8 @@ const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
1008
1061
  const MOBILE_PREFERENCES_METHOD = "mobile.preferences";
1009
1062
  /** One session.list page (thin phones load incrementally). */
1010
1063
  const SESSION_PAGE_SIZE = 20;
1064
+ /** SSE keep-alive ping cadence for the live mux stream (single connection). */
1065
+ const DEFAULT_EVENTS_HEARTBEAT_MS = 15e3;
1011
1066
  /** Encode one list position as an opaque continuation cursor. */
1012
1067
  function sessionListCursor(updatedAt, sessionId) {
1013
1068
  return `${updatedAt}:${sessionId}`;
@@ -1040,6 +1095,7 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`;
1040
1095
  */
1041
1096
  function makeMobileApiRoutes(deps) {
1042
1097
  const { service, apiProxy, mobileEnterToSend } = deps;
1098
+ const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;
1043
1099
  /** The phone gate: a live paired-device cookie, or nothing else proceeds. */
1044
1100
  const gateOk = (req) => {
1045
1101
  const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
@@ -1166,7 +1222,7 @@ function makeMobileApiRoutes(deps) {
1166
1222
  try {
1167
1223
  res.write(": ping\n\n");
1168
1224
  } catch {}
1169
- }, 15e3);
1225
+ }, eventsHeartbeatMs);
1170
1226
  const onClose = () => {
1171
1227
  if (closed) return;
1172
1228
  closed = true;
@@ -1715,30 +1771,85 @@ async function checkUpdates(deps) {
1715
1771
  /** Cap on captured pnpm output (keeps error payloads bounded). */
1716
1772
  const OUTPUT_CAP = 16 * 1024;
1717
1773
  /**
1718
- * Run the update: `pnpm update <packages>` inside the profile directory.
1774
+ * Windows cmd command-not-found stderr. With shell:true a missing shim
1775
+ * exits with code 1 (cmd cannot report ENOENT), so the fallback chain
1776
+ * detects this message instead of the spawn error event.
1777
+ */
1778
+ const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
1779
+ /**
1780
+ * Run the update inside the profile directory. Tries pnpm first, falls back
1781
+ * to corepack and then npx when the previous command is missing (ENOENT);
1782
+ * all candidates share one hard timeout and keep accumulating output.
1719
1783
  * @param deps - profile dir, package list, and spawn/timeout seams.
1720
1784
  * @returns the outcome with captured output.
1721
1785
  */
1722
1786
  function runUpdate(deps) {
1723
1787
  return new Promise((resolve) => {
1724
- const child = (deps.spawnImpl ?? spawn)("pnpm", ["update", ...deps.packages], {
1788
+ const spawnImpl = deps.spawnImpl ?? spawn;
1789
+ const packages = deps.packages;
1790
+ const platform = deps.platform ?? process.platform;
1791
+ const spawnOptions = {
1725
1792
  cwd: deps.profileDir,
1726
1793
  stdio: [
1727
1794
  "ignore",
1728
1795
  "pipe",
1729
1796
  "pipe"
1730
- ]
1731
- });
1797
+ ],
1798
+ ...platform === "win32" ? { shell: true } : {}
1799
+ };
1800
+ const candidates = [
1801
+ {
1802
+ command: "pnpm",
1803
+ args: ["update", ...packages]
1804
+ },
1805
+ {
1806
+ command: "corepack",
1807
+ args: [
1808
+ "pnpm",
1809
+ "update",
1810
+ ...packages
1811
+ ]
1812
+ },
1813
+ {
1814
+ command: "npx",
1815
+ args: [
1816
+ "--yes",
1817
+ "pnpm",
1818
+ "update",
1819
+ ...packages
1820
+ ]
1821
+ }
1822
+ ];
1732
1823
  let output = "";
1824
+ let currentOutput = "";
1733
1825
  const append = (chunk) => {
1734
1826
  output += chunk.toString("utf8");
1735
1827
  if (output.length > OUTPUT_CAP) output = output.slice(output.length - OUTPUT_CAP);
1828
+ currentOutput += chunk.toString("utf8");
1829
+ if (currentOutput.length > OUTPUT_CAP) currentOutput = currentOutput.slice(currentOutput.length - OUTPUT_CAP);
1830
+ };
1831
+ let currentChild;
1832
+ let finished = false;
1833
+ const finish = (result) => {
1834
+ if (finished) return;
1835
+ finished = true;
1836
+ clearTimeout(timer);
1837
+ resolve(result);
1736
1838
  };
1737
- child.stdout?.on("data", append);
1738
- child.stderr?.on("data", append);
1739
1839
  const timer = setTimeout(() => {
1740
- child.kill("SIGTERM");
1741
- resolve({
1840
+ if (finished) return;
1841
+ if (platform === "win32") {
1842
+ const pid = currentChild?.pid;
1843
+ if (pid !== void 0 && pid > 0) try {
1844
+ spawnImpl("taskkill", [
1845
+ "/pid",
1846
+ String(pid),
1847
+ "/t",
1848
+ "/f"
1849
+ ], { stdio: "ignore" });
1850
+ } catch {}
1851
+ } else currentChild?.kill("SIGTERM");
1852
+ finish({
1742
1853
  ok: false,
1743
1854
  exitCode: null,
1744
1855
  output,
@@ -1746,27 +1857,59 @@ function runUpdate(deps) {
1746
1857
  errorCode: "timeout"
1747
1858
  });
1748
1859
  }, deps.timeoutMs ?? 10 * 6e4);
1749
- child.on("error", (error) => {
1750
- clearTimeout(timer);
1751
- const missing = error.code === "ENOENT";
1752
- resolve({
1753
- ok: false,
1754
- exitCode: null,
1755
- output,
1756
- error: missing ? "pnpm not found on PATH" : error.message,
1757
- errorCode: missing ? "pnpm-missing" : void 0
1860
+ const runCandidate = (index) => {
1861
+ if (finished) return;
1862
+ if (index >= candidates.length) {
1863
+ finish({
1864
+ ok: false,
1865
+ exitCode: null,
1866
+ output,
1867
+ error: "pnpm not found on PATH (tried pnpm, corepack, npx); install pnpm and restart the app",
1868
+ errorCode: "pnpm-missing"
1869
+ });
1870
+ return;
1871
+ }
1872
+ const candidate = candidates[index];
1873
+ currentOutput = "";
1874
+ const child = spawnImpl(candidate.command, candidate.args, spawnOptions);
1875
+ currentChild = child;
1876
+ let settled = false;
1877
+ const once = (fn) => {
1878
+ if (settled) return;
1879
+ settled = true;
1880
+ fn();
1881
+ };
1882
+ child.stdout?.on("data", append);
1883
+ child.stderr?.on("data", append);
1884
+ child.on("error", (error) => {
1885
+ once(() => {
1886
+ if (error.code === "ENOENT") runCandidate(index + 1);
1887
+ else finish({
1888
+ ok: false,
1889
+ exitCode: null,
1890
+ output,
1891
+ error: error.message,
1892
+ errorCode: void 0
1893
+ });
1894
+ });
1758
1895
  });
1759
- });
1760
- child.on("close", (code) => {
1761
- clearTimeout(timer);
1762
- resolve({
1763
- ok: code === 0,
1764
- exitCode: code,
1765
- output,
1766
- error: code === 0 ? void 0 : "pnpm exited with code " + String(code),
1767
- ...code === 0 ? {} : { errorCode: "pnpm-failed" }
1896
+ child.on("close", (code) => {
1897
+ if (settled || finished) return;
1898
+ if (platform === "win32" && code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput)) {
1899
+ runCandidate(index + 1);
1900
+ return;
1901
+ }
1902
+ settled = true;
1903
+ finish({
1904
+ ok: code === 0,
1905
+ exitCode: code,
1906
+ output,
1907
+ error: code === 0 ? void 0 : "pnpm exited with code " + String(code),
1908
+ ...code === 0 ? {} : { errorCode: "pnpm-failed" }
1909
+ });
1768
1910
  });
1769
- });
1911
+ };
1912
+ runCandidate(0);
1770
1913
  });
1771
1914
  }
1772
1915
  //#endregion
package/lib/mobile.js CHANGED
@@ -12770,6 +12770,12 @@ function browserSource(url) {
12770
12770
  }
12771
12771
  const DEFAULT_POLL_INTERVAL_MS = 3e3;
12772
12772
  const DEFAULT_STALL_THRESHOLD_MS = 12e3;
12773
+ /**
12774
+ * Stall-check granularity: the single scheduler tick runs at least this
12775
+ * often so fallback arms within a second of the stall threshold passing,
12776
+ * while the poll cadence itself stays {@link MuxClientOptions.pollIntervalMs}.
12777
+ */
12778
+ const STALL_CHECK_MS = 1e3;
12773
12779
  /** Poll window: enough recent events to cover a few seconds of agent output. */
12774
12780
  const DEFAULT_POLL_PAGE_SIZE = 50;
12775
12781
  /**
@@ -12800,9 +12806,11 @@ var MuxClient = class {
12800
12806
  sseAlive = false;
12801
12807
  /** Per-session highest event seq already emitted, for poll dedup. */
12802
12808
  pollWatermark = /* @__PURE__ */ new Map();
12803
- stallTimer;
12804
- pollTimer;
12809
+ /** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
12810
+ tickTimer;
12805
12811
  polling = false;
12812
+ /** Epoch ms of the next due poll while polling (kept on the same tick timer). */
12813
+ nextPollAt = 0;
12806
12814
  /**
12807
12815
  * @param url - the mobile events endpoint (browser-relative).
12808
12816
  * @param options - seams.
@@ -12820,15 +12828,16 @@ var MuxClient = class {
12820
12828
  this.stopped = false;
12821
12829
  this.lastDataAt = this.now();
12822
12830
  if (this.source === void 0) this.connect();
12823
- this.startStallChecker();
12831
+ this.startTick();
12824
12832
  }
12825
12833
  /** Close for good. */
12826
12834
  stop() {
12827
12835
  this.stopped = true;
12828
- this.stopStallChecker();
12836
+ this.stopTick();
12829
12837
  this.stopPolling();
12830
12838
  this.closeSource();
12831
12839
  this.observeSessionId = void 0;
12840
+ this.nextPollAt = 0;
12832
12841
  }
12833
12842
  /** Subscribe to validated frames; returns an unsubscribe function. */
12834
12843
  onFrame(listener) {
@@ -12866,36 +12875,46 @@ var MuxClient = class {
12866
12875
  if (this.observeSessionId !== void 0) this.startPolling();
12867
12876
  };
12868
12877
  }
12869
- startStallChecker() {
12870
- this.stopStallChecker();
12871
- this.stallTimer = setInterval(() => {
12872
- if (this.stopped) return;
12873
- if (this.observeSessionId === void 0) return;
12874
- if (this.polling) return;
12875
- if (this.sseAlive) return;
12876
- if (this.now() - this.lastDataAt > this.stallThresholdMs) this.startPolling();
12877
- }, 1e3);
12878
- }
12879
- stopStallChecker() {
12880
- if (this.stallTimer !== void 0) {
12881
- clearInterval(this.stallTimer);
12882
- this.stallTimer = void 0;
12878
+ /**
12879
+ * The single scheduler tick is the only interval this client owns. One
12880
+ * timer (at the finer of the stall-check and poll cadences) both arms the
12881
+ * polling fallback once the stall threshold passes and drives each poll at
12882
+ * {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
12883
+ */
12884
+ startTick() {
12885
+ if (this.tickTimer !== void 0) return;
12886
+ const cadence = Math.min(this.pollIntervalMs, STALL_CHECK_MS);
12887
+ this.tickTimer = setInterval(() => {
12888
+ this.tick();
12889
+ }, cadence);
12890
+ }
12891
+ stopTick() {
12892
+ if (this.tickTimer !== void 0) {
12893
+ clearInterval(this.tickTimer);
12894
+ this.tickTimer = void 0;
12895
+ }
12896
+ }
12897
+ tick() {
12898
+ if (this.stopped) return;
12899
+ if (this.observeSessionId === void 0) return;
12900
+ if (this.polling) {
12901
+ if (this.now() >= this.nextPollAt) {
12902
+ this.nextPollAt = this.now() + this.pollIntervalMs;
12903
+ this.pollTick();
12904
+ }
12905
+ return;
12883
12906
  }
12907
+ if (this.sseAlive) return;
12908
+ if (this.now() - this.lastDataAt > this.stallThresholdMs) this.startPolling();
12884
12909
  }
12885
12910
  startPolling() {
12886
12911
  if (this.polling || this.stopped) return;
12887
12912
  this.polling = true;
12913
+ this.nextPollAt = this.now() + this.pollIntervalMs;
12888
12914
  this.pollTick();
12889
- this.pollTimer = setInterval(() => {
12890
- this.pollTick();
12891
- }, this.pollIntervalMs);
12892
12915
  }
12893
12916
  stopPolling() {
12894
12917
  this.polling = false;
12895
- if (this.pollTimer !== void 0) {
12896
- clearInterval(this.pollTimer);
12897
- this.pollTimer = void 0;
12898
- }
12899
12918
  }
12900
12919
  /**
12901
12920
  * Fetch the latest history page for the observed session and re-emit any