@linxin666/dsh-remote-web-ui 0.1.16 → 0.1.17
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/assets/apple-touch-icon.png +0 -0
- package/lib/index.js +76 -14
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +19 -2
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +32 -0
- package/lib/types/update.d.ts +5 -3
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +22 -6
- package/package.json +2 -1
- package/src/mobile-api.ts +19 -2
- package/src/mobile-routes.ts +33 -0
- package/src/update.ts +23 -6
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -981,6 +981,10 @@ function makeRoutes(deps) {
|
|
|
981
981
|
function mobileBundlePath() {
|
|
982
982
|
return fileURLToPath(new URL("../lib/mobile.js", import.meta.url));
|
|
983
983
|
}
|
|
984
|
+
/** The apple-touch-icon shipped with the package (iOS home-screen icon, 180x180 PNG). */
|
|
985
|
+
function appleTouchIconPath() {
|
|
986
|
+
return fileURLToPath(new URL("../assets/apple-touch-icon.png", import.meta.url));
|
|
987
|
+
}
|
|
984
988
|
/** The mobile page shell: minimal, offline-safe, no external assets. */
|
|
985
989
|
function pageHtml(bundleUrl) {
|
|
986
990
|
return [
|
|
@@ -991,6 +995,7 @@ function pageHtml(bundleUrl) {
|
|
|
991
995
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\">",
|
|
992
996
|
"<meta name=\"theme-color\" content=\"#f3f5f9\">",
|
|
993
997
|
"<meta name=\"referrer\" content=\"no-referrer\">",
|
|
998
|
+
"<link rel=\"apple-touch-icon\" href=\"/m/apple-touch-icon.png\">",
|
|
994
999
|
"<title>移动端远程控制</title>",
|
|
995
1000
|
"</head>",
|
|
996
1001
|
"<body>",
|
|
@@ -1009,6 +1014,15 @@ function writeStatic(res, status, type, body) {
|
|
|
1009
1014
|
});
|
|
1010
1015
|
res.end(body);
|
|
1011
1016
|
}
|
|
1017
|
+
/** Send the PNG icon as a binary body (unlike text assets, the PNG must not be decoded to utf-8). */
|
|
1018
|
+
function writePng(res, status, body) {
|
|
1019
|
+
res.writeHead(status, {
|
|
1020
|
+
"content-type": "image/png",
|
|
1021
|
+
"cache-control": "public, max-age=31536000, immutable",
|
|
1022
|
+
"referrer-policy": "no-referrer"
|
|
1023
|
+
});
|
|
1024
|
+
res.end(body);
|
|
1025
|
+
}
|
|
1012
1026
|
/**
|
|
1013
1027
|
* Build the mobile page routes.
|
|
1014
1028
|
* @returns the two exact routes to register on webServer.
|
|
@@ -1029,15 +1043,35 @@ function makeMobileRoutes() {
|
|
|
1029
1043
|
writeStatic(res, 500, "text/plain", "failed to read the mobile bundle");
|
|
1030
1044
|
}
|
|
1031
1045
|
};
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
path
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1046
|
+
const handleIcon = async (_req, res) => {
|
|
1047
|
+
const path = appleTouchIconPath();
|
|
1048
|
+
if (!existsSync(path)) {
|
|
1049
|
+
writeStatic(res, 404, "text/plain", "apple-touch-icon not found");
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
try {
|
|
1053
|
+
writePng(res, 200, await readFile(path));
|
|
1054
|
+
} catch {
|
|
1055
|
+
writeStatic(res, 500, "text/plain", "failed to read the apple-touch-icon");
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
return [
|
|
1059
|
+
{
|
|
1060
|
+
kind: "exact",
|
|
1061
|
+
path: "/m",
|
|
1062
|
+
handler: handlePage
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
kind: "exact",
|
|
1066
|
+
path: "/m/mobile.js",
|
|
1067
|
+
handler: handleBundle
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
kind: "exact",
|
|
1071
|
+
path: "/m/apple-touch-icon.png",
|
|
1072
|
+
handler: handleIcon
|
|
1073
|
+
}
|
|
1074
|
+
];
|
|
1041
1075
|
}
|
|
1042
1076
|
//#endregion
|
|
1043
1077
|
//#region src/mobile-api.ts
|
|
@@ -1096,10 +1130,22 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`;
|
|
|
1096
1130
|
function makeMobileApiRoutes(deps) {
|
|
1097
1131
|
const { service, apiProxy, mobileEnterToSend } = deps;
|
|
1098
1132
|
const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;
|
|
1133
|
+
/**
|
|
1134
|
+
* Refresh the paired device's presence and report whether it is live.
|
|
1135
|
+
* The mobile surface (unlike the desktop Web UI) has no `/api/pair/heartbeat`
|
|
1136
|
+
* sender, so any activity on the mobile channel — a gated RPC, or the live
|
|
1137
|
+
* SSE stream staying open — must count as presence. Without this, an
|
|
1138
|
+
* idle-but-connected phone ages past `offlineAfterMs` and the desktop panel
|
|
1139
|
+
* wrongly reports it as disconnected.
|
|
1140
|
+
*/
|
|
1141
|
+
const touchDeviceFor = (req) => {
|
|
1142
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
1143
|
+
if (deviceId === void 0) return false;
|
|
1144
|
+
return service.touchDevice(deviceId);
|
|
1145
|
+
};
|
|
1099
1146
|
/** The phone gate: a live paired-device cookie, or nothing else proceeds. */
|
|
1100
1147
|
const gateOk = (req) => {
|
|
1101
|
-
|
|
1102
|
-
return deviceId !== void 0 && service.hasDevice(deviceId);
|
|
1148
|
+
return touchDeviceFor(req);
|
|
1103
1149
|
};
|
|
1104
1150
|
const writeJson = (res, status, body) => {
|
|
1105
1151
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
@@ -1219,6 +1265,7 @@ function makeMobileApiRoutes(deps) {
|
|
|
1219
1265
|
let closed = false;
|
|
1220
1266
|
const heartbeat = setInterval(() => {
|
|
1221
1267
|
if (closed) return;
|
|
1268
|
+
touchDeviceFor(req);
|
|
1222
1269
|
try {
|
|
1223
1270
|
res.write(": ping\n\n");
|
|
1224
1271
|
} catch {}
|
|
@@ -1787,6 +1834,16 @@ const OUTPUT_CAP = 16 * 1024;
|
|
|
1787
1834
|
*/
|
|
1788
1835
|
const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
|
|
1789
1836
|
/**
|
|
1837
|
+
* Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
|
|
1838
|
+
* silently skips same-day releases — `pnpm update --latest` then exits 0
|
|
1839
|
+
* without moving anything and the post-run verification would misreport a
|
|
1840
|
+
* stale no-op. The update here is explicitly user-initiated and the panel
|
|
1841
|
+
* shows exactly which versions are being installed, so the gate only adds a
|
|
1842
|
+
* confusing silent no-op; override it per-invocation instead of asking every
|
|
1843
|
+
* user to edit the profile's pnpm-workspace.yaml.
|
|
1844
|
+
*/
|
|
1845
|
+
const MIN_RELEASE_AGE_OVERRIDE = "--config.minimumReleaseAge=0";
|
|
1846
|
+
/**
|
|
1790
1847
|
* Run the update inside the profile directory. Tries pnpm first, falls back
|
|
1791
1848
|
* to corepack and then npx when the previous command is missing (ENOENT);
|
|
1792
1849
|
* all candidates share one hard timeout and keep accumulating output.
|
|
@@ -1813,6 +1870,7 @@ function runUpdate(deps) {
|
|
|
1813
1870
|
args: [
|
|
1814
1871
|
"update",
|
|
1815
1872
|
"--latest",
|
|
1873
|
+
MIN_RELEASE_AGE_OVERRIDE,
|
|
1816
1874
|
...packages
|
|
1817
1875
|
]
|
|
1818
1876
|
},
|
|
@@ -1822,6 +1880,7 @@ function runUpdate(deps) {
|
|
|
1822
1880
|
"pnpm",
|
|
1823
1881
|
"update",
|
|
1824
1882
|
"--latest",
|
|
1883
|
+
MIN_RELEASE_AGE_OVERRIDE,
|
|
1825
1884
|
...packages
|
|
1826
1885
|
]
|
|
1827
1886
|
},
|
|
@@ -1832,6 +1891,7 @@ function runUpdate(deps) {
|
|
|
1832
1891
|
"pnpm",
|
|
1833
1892
|
"update",
|
|
1834
1893
|
"--latest",
|
|
1894
|
+
MIN_RELEASE_AGE_OVERRIDE,
|
|
1835
1895
|
...packages
|
|
1836
1896
|
]
|
|
1837
1897
|
}
|
|
@@ -1930,9 +1990,11 @@ function runUpdate(deps) {
|
|
|
1930
1990
|
}
|
|
1931
1991
|
/**
|
|
1932
1992
|
* Run the update, then verify the installed versions actually moved. pnpm
|
|
1933
|
-
* exits 0 even when it silently kept the installed versions —
|
|
1934
|
-
* `minimumReleaseAge` gate
|
|
1935
|
-
*
|
|
1993
|
+
* exits 0 even when it silently kept the installed versions — runUpdate
|
|
1994
|
+
* overrides pnpm 11's `minimumReleaseAge` gate (default 24 h) with
|
|
1995
|
+
* `--config.minimumReleaseAge=0`, but an older pnpm without the override or
|
|
1996
|
+
* another silent no-op can still keep versions in place — so a green exit
|
|
1997
|
+
* alone must not report a misleading "update complete". Re-read the
|
|
1936
1998
|
* installed versions afterwards and surface a `stale` failure (with the
|
|
1937
1999
|
* captured pnpm output) when nothing moved, so the panel can tell the user
|
|
1938
2000
|
* how to unblock the gate instead of claiming success.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAgDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAgDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA6InE"}
|
package/lib/types/mobile-api.js
CHANGED
|
@@ -77,10 +77,23 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`;
|
|
|
77
77
|
export function makeMobileApiRoutes(deps) {
|
|
78
78
|
const { service, apiProxy, mobileEnterToSend } = deps;
|
|
79
79
|
const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;
|
|
80
|
+
/**
|
|
81
|
+
* Refresh the paired device's presence and report whether it is live.
|
|
82
|
+
* The mobile surface (unlike the desktop Web UI) has no `/api/pair/heartbeat`
|
|
83
|
+
* sender, so any activity on the mobile channel — a gated RPC, or the live
|
|
84
|
+
* SSE stream staying open — must count as presence. Without this, an
|
|
85
|
+
* idle-but-connected phone ages past `offlineAfterMs` and the desktop panel
|
|
86
|
+
* wrongly reports it as disconnected.
|
|
87
|
+
*/
|
|
88
|
+
const touchDeviceFor = (req) => {
|
|
89
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
90
|
+
if (deviceId === undefined)
|
|
91
|
+
return false;
|
|
92
|
+
return service.touchDevice(deviceId);
|
|
93
|
+
};
|
|
80
94
|
/** The phone gate: a live paired-device cookie, or nothing else proceeds. */
|
|
81
95
|
const gateOk = (req) => {
|
|
82
|
-
|
|
83
|
-
return deviceId !== undefined && service.hasDevice(deviceId);
|
|
96
|
+
return touchDeviceFor(req);
|
|
84
97
|
};
|
|
85
98
|
const writeJson = (res, status, body) => {
|
|
86
99
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
@@ -164,6 +177,10 @@ export function makeMobileApiRoutes(deps) {
|
|
|
164
177
|
const heartbeat = setInterval(() => {
|
|
165
178
|
if (closed)
|
|
166
179
|
return;
|
|
180
|
+
// An open SSE stream proves the phone is still live even while the agent
|
|
181
|
+
// idles (no RPC traffic), so refresh presence alongside the transport
|
|
182
|
+
// keepalive — otherwise an idle phone drifts to "disconnected".
|
|
183
|
+
touchDeviceFor(req);
|
|
167
184
|
try {
|
|
168
185
|
res.write(': ping\n\n');
|
|
169
186
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;
|
|
1
|
+
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAwD/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAmC7C"}
|
|
@@ -14,6 +14,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
function mobileBundlePath() {
|
|
15
15
|
return fileURLToPath(new URL('../lib/mobile.js', import.meta.url));
|
|
16
16
|
}
|
|
17
|
+
/** The apple-touch-icon shipped with the package (iOS home-screen icon, 180x180 PNG). */
|
|
18
|
+
function appleTouchIconPath() {
|
|
19
|
+
return fileURLToPath(new URL('../assets/apple-touch-icon.png', import.meta.url));
|
|
20
|
+
}
|
|
17
21
|
/** The mobile page shell: minimal, offline-safe, no external assets. */
|
|
18
22
|
function pageHtml(bundleUrl) {
|
|
19
23
|
return [
|
|
@@ -24,6 +28,10 @@ function pageHtml(bundleUrl) {
|
|
|
24
28
|
'<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">',
|
|
25
29
|
'<meta name="theme-color" content="#f3f5f9">',
|
|
26
30
|
'<meta name="referrer" content="no-referrer">',
|
|
31
|
+
// iOS "Add to Home Screen" uses apple-touch-icon for the tile icon;
|
|
32
|
+
// the PNG ships with the package (opaque background — iOS composites
|
|
33
|
+
// transparency onto black, so the icon avoids transparent regions).
|
|
34
|
+
'<link rel="apple-touch-icon" href="/m/apple-touch-icon.png">',
|
|
27
35
|
'<title>移动端远程控制</title>',
|
|
28
36
|
'</head>',
|
|
29
37
|
'<body>',
|
|
@@ -42,6 +50,15 @@ function writeStatic(res, status, type, body) {
|
|
|
42
50
|
});
|
|
43
51
|
res.end(body);
|
|
44
52
|
}
|
|
53
|
+
/** Send the PNG icon as a binary body (unlike text assets, the PNG must not be decoded to utf-8). */
|
|
54
|
+
function writePng(res, status, body) {
|
|
55
|
+
res.writeHead(status, {
|
|
56
|
+
'content-type': 'image/png',
|
|
57
|
+
'cache-control': 'public, max-age=31536000, immutable',
|
|
58
|
+
'referrer-policy': 'no-referrer',
|
|
59
|
+
});
|
|
60
|
+
res.end(body);
|
|
61
|
+
}
|
|
45
62
|
/**
|
|
46
63
|
* Build the mobile page routes.
|
|
47
64
|
* @returns the two exact routes to register on webServer.
|
|
@@ -64,8 +81,23 @@ export function makeMobileRoutes() {
|
|
|
64
81
|
writeStatic(res, 500, 'text/plain', 'failed to read the mobile bundle');
|
|
65
82
|
}
|
|
66
83
|
};
|
|
84
|
+
const handleIcon = async (_req, res) => {
|
|
85
|
+
const path = appleTouchIconPath();
|
|
86
|
+
if (!existsSync(path)) {
|
|
87
|
+
writeStatic(res, 404, 'text/plain', 'apple-touch-icon not found');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const body = await readFile(path);
|
|
92
|
+
writePng(res, 200, body);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
writeStatic(res, 500, 'text/plain', 'failed to read the apple-touch-icon');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
67
98
|
return [
|
|
68
99
|
{ kind: 'exact', path: '/m', handler: handlePage },
|
|
69
100
|
{ kind: 'exact', path: '/m/mobile.js', handler: handleBundle },
|
|
101
|
+
{ kind: 'exact', path: '/m/apple-touch-icon.png', handler: handleIcon },
|
|
70
102
|
];
|
|
71
103
|
}
|
package/lib/types/update.d.ts
CHANGED
|
@@ -206,9 +206,11 @@ export interface UpdateRunVerifiedDeps {
|
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
208
208
|
* Run the update, then verify the installed versions actually moved. pnpm
|
|
209
|
-
* exits 0 even when it silently kept the installed versions —
|
|
210
|
-
* `minimumReleaseAge` gate
|
|
211
|
-
*
|
|
209
|
+
* exits 0 even when it silently kept the installed versions — runUpdate
|
|
210
|
+
* overrides pnpm 11's `minimumReleaseAge` gate (default 24 h) with
|
|
211
|
+
* `--config.minimumReleaseAge=0`, but an older pnpm without the override or
|
|
212
|
+
* another silent no-op can still keep versions in place — so a green exit
|
|
213
|
+
* alone must not report a misleading "update complete". Re-read the
|
|
212
214
|
* installed versions afterwards and surface a `stale` failure (with the
|
|
213
215
|
* captured pnpm output) when nothing moved, so the panel can tell the user
|
|
214
216
|
* how to unblock the gate instead of claiming success.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|
package/lib/types/update.js
CHANGED
|
@@ -302,6 +302,16 @@ const OUTPUT_CAP = 16 * 1024;
|
|
|
302
302
|
* detects this message instead of the spawn error event.
|
|
303
303
|
*/
|
|
304
304
|
const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
|
|
305
|
+
/**
|
|
306
|
+
* Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
|
|
307
|
+
* silently skips same-day releases — `pnpm update --latest` then exits 0
|
|
308
|
+
* without moving anything and the post-run verification would misreport a
|
|
309
|
+
* stale no-op. The update here is explicitly user-initiated and the panel
|
|
310
|
+
* shows exactly which versions are being installed, so the gate only adds a
|
|
311
|
+
* confusing silent no-op; override it per-invocation instead of asking every
|
|
312
|
+
* user to edit the profile's pnpm-workspace.yaml.
|
|
313
|
+
*/
|
|
314
|
+
const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0';
|
|
305
315
|
/**
|
|
306
316
|
* Run the update inside the profile directory. Tries pnpm first, falls back
|
|
307
317
|
* to corepack and then npx when the previous command is missing (ENOENT);
|
|
@@ -328,10 +338,14 @@ export function runUpdate(deps) {
|
|
|
328
338
|
// "0.1.12" without a range), and plain `pnpm update` treats an exact spec
|
|
329
339
|
// as pinned — it prints "Already up to date" and exits 0 without moving
|
|
330
340
|
// the installed version, so the panel would report a false success.
|
|
341
|
+
// `MIN_RELEASE_AGE_OVERRIDE` rides along every candidate: without it the
|
|
342
|
+
// pnpm 11 minimumReleaseAge gate (default 24 h) silently keeps same-day
|
|
343
|
+
// releases in place, which the post-run verification would then have to
|
|
344
|
+
// surface as `stale` even though the update is legitimate.
|
|
331
345
|
const candidates = [
|
|
332
|
-
{ command: 'pnpm', args: ['update', '--latest', ...packages] },
|
|
333
|
-
{ command: 'corepack', args: ['pnpm', 'update', '--latest', ...packages] },
|
|
334
|
-
{ command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', ...packages] },
|
|
346
|
+
{ command: 'pnpm', args: ['update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
|
|
347
|
+
{ command: 'corepack', args: ['pnpm', 'update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
|
|
348
|
+
{ command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
|
|
335
349
|
];
|
|
336
350
|
// `output` accumulates across candidates for UI display; `currentOutput`
|
|
337
351
|
// is reset per candidate and carries only that candidate's own diagnostics
|
|
@@ -444,9 +458,11 @@ export function runUpdate(deps) {
|
|
|
444
458
|
}
|
|
445
459
|
/**
|
|
446
460
|
* Run the update, then verify the installed versions actually moved. pnpm
|
|
447
|
-
* exits 0 even when it silently kept the installed versions —
|
|
448
|
-
* `minimumReleaseAge` gate
|
|
449
|
-
*
|
|
461
|
+
* exits 0 even when it silently kept the installed versions — runUpdate
|
|
462
|
+
* overrides pnpm 11's `minimumReleaseAge` gate (default 24 h) with
|
|
463
|
+
* `--config.minimumReleaseAge=0`, but an older pnpm without the override or
|
|
464
|
+
* another silent no-op can still keep versions in place — so a green exit
|
|
465
|
+
* alone must not report a misleading "update complete". Re-read the
|
|
450
466
|
* installed versions afterwards and surface a `stale` failure (with the
|
|
451
467
|
* captured pnpm output) when nothing moved, so the panel can tell the user
|
|
452
468
|
* how to unblock the gate instead of claiming success.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"lib/**/*.d.ts",
|
|
31
31
|
"lib/**/*.d.ts.map",
|
|
32
32
|
"src",
|
|
33
|
+
"assets",
|
|
33
34
|
"cordis.patch.yml"
|
|
34
35
|
],
|
|
35
36
|
"license": "Apache-2.0",
|
package/src/mobile-api.ts
CHANGED
|
@@ -103,10 +103,23 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
103
103
|
const { service, apiProxy, mobileEnterToSend } = deps
|
|
104
104
|
const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Refresh the paired device's presence and report whether it is live.
|
|
108
|
+
* The mobile surface (unlike the desktop Web UI) has no `/api/pair/heartbeat`
|
|
109
|
+
* sender, so any activity on the mobile channel — a gated RPC, or the live
|
|
110
|
+
* SSE stream staying open — must count as presence. Without this, an
|
|
111
|
+
* idle-but-connected phone ages past `offlineAfterMs` and the desktop panel
|
|
112
|
+
* wrongly reports it as disconnected.
|
|
113
|
+
*/
|
|
114
|
+
const touchDeviceFor = (req: IncomingMessage): boolean => {
|
|
115
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
|
|
116
|
+
if (deviceId === undefined) return false
|
|
117
|
+
return service.touchDevice(deviceId)
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
/** The phone gate: a live paired-device cookie, or nothing else proceeds. */
|
|
107
121
|
const gateOk = (req: IncomingMessage): boolean => {
|
|
108
|
-
|
|
109
|
-
return deviceId !== undefined && service.hasDevice(deviceId)
|
|
122
|
+
return touchDeviceFor(req)
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
const writeJson = (res: ServerResponse, status: number, body: unknown): void => {
|
|
@@ -190,6 +203,10 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
190
203
|
let closed = false
|
|
191
204
|
const heartbeat = setInterval(() => {
|
|
192
205
|
if (closed) return
|
|
206
|
+
// An open SSE stream proves the phone is still live even while the agent
|
|
207
|
+
// idles (no RPC traffic), so refresh presence alongside the transport
|
|
208
|
+
// keepalive — otherwise an idle phone drifts to "disconnected".
|
|
209
|
+
touchDeviceFor(req)
|
|
193
210
|
try {
|
|
194
211
|
res.write(': ping\n\n')
|
|
195
212
|
} catch {
|
package/src/mobile-routes.ts
CHANGED
|
@@ -19,6 +19,11 @@ function mobileBundlePath(): string {
|
|
|
19
19
|
return fileURLToPath(new URL('../lib/mobile.js', import.meta.url))
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** The apple-touch-icon shipped with the package (iOS home-screen icon, 180x180 PNG). */
|
|
23
|
+
function appleTouchIconPath(): string {
|
|
24
|
+
return fileURLToPath(new URL('../assets/apple-touch-icon.png', import.meta.url))
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
/** The mobile page shell: minimal, offline-safe, no external assets. */
|
|
23
28
|
function pageHtml(bundleUrl: string): string {
|
|
24
29
|
return [
|
|
@@ -29,6 +34,10 @@ function pageHtml(bundleUrl: string): string {
|
|
|
29
34
|
'<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">',
|
|
30
35
|
'<meta name="theme-color" content="#f3f5f9">',
|
|
31
36
|
'<meta name="referrer" content="no-referrer">',
|
|
37
|
+
// iOS "Add to Home Screen" uses apple-touch-icon for the tile icon;
|
|
38
|
+
// the PNG ships with the package (opaque background — iOS composites
|
|
39
|
+
// transparency onto black, so the icon avoids transparent regions).
|
|
40
|
+
'<link rel="apple-touch-icon" href="/m/apple-touch-icon.png">',
|
|
32
41
|
'<title>移动端远程控制</title>',
|
|
33
42
|
'</head>',
|
|
34
43
|
'<body>',
|
|
@@ -49,6 +58,16 @@ function writeStatic(res: ServerResponse, status: number, type: string, body: st
|
|
|
49
58
|
res.end(body)
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
/** Send the PNG icon as a binary body (unlike text assets, the PNG must not be decoded to utf-8). */
|
|
62
|
+
function writePng(res: ServerResponse, status: number, body: Buffer): void {
|
|
63
|
+
res.writeHead(status, {
|
|
64
|
+
'content-type': 'image/png',
|
|
65
|
+
'cache-control': 'public, max-age=31536000, immutable',
|
|
66
|
+
'referrer-policy': 'no-referrer',
|
|
67
|
+
})
|
|
68
|
+
res.end(body)
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
/**
|
|
53
72
|
* Build the mobile page routes.
|
|
54
73
|
* @returns the two exact routes to register on webServer.
|
|
@@ -70,8 +89,22 @@ export function makeMobileRoutes(): WebRoute[] {
|
|
|
70
89
|
writeStatic(res, 500, 'text/plain', 'failed to read the mobile bundle')
|
|
71
90
|
}
|
|
72
91
|
}
|
|
92
|
+
const handleIcon = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
|
93
|
+
const path = appleTouchIconPath()
|
|
94
|
+
if (!existsSync(path)) {
|
|
95
|
+
writeStatic(res, 404, 'text/plain', 'apple-touch-icon not found')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const body = await readFile(path)
|
|
100
|
+
writePng(res, 200, body)
|
|
101
|
+
} catch {
|
|
102
|
+
writeStatic(res, 500, 'text/plain', 'failed to read the apple-touch-icon')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
73
105
|
return [
|
|
74
106
|
{ kind: 'exact', path: '/m', handler: handlePage },
|
|
75
107
|
{ kind: 'exact', path: '/m/mobile.js', handler: handleBundle },
|
|
108
|
+
{ kind: 'exact', path: '/m/apple-touch-icon.png', handler: handleIcon },
|
|
76
109
|
]
|
|
77
110
|
}
|
package/src/update.ts
CHANGED
|
@@ -412,6 +412,17 @@ const OUTPUT_CAP = 16 * 1024
|
|
|
412
412
|
*/
|
|
413
413
|
const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i
|
|
414
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
|
|
417
|
+
* silently skips same-day releases — `pnpm update --latest` then exits 0
|
|
418
|
+
* without moving anything and the post-run verification would misreport a
|
|
419
|
+
* stale no-op. The update here is explicitly user-initiated and the panel
|
|
420
|
+
* shows exactly which versions are being installed, so the gate only adds a
|
|
421
|
+
* confusing silent no-op; override it per-invocation instead of asking every
|
|
422
|
+
* user to edit the profile's pnpm-workspace.yaml.
|
|
423
|
+
*/
|
|
424
|
+
const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0'
|
|
425
|
+
|
|
415
426
|
/**
|
|
416
427
|
* Run the update inside the profile directory. Tries pnpm first, falls back
|
|
417
428
|
* to corepack and then npx when the previous command is missing (ENOENT);
|
|
@@ -438,10 +449,14 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
438
449
|
// "0.1.12" without a range), and plain `pnpm update` treats an exact spec
|
|
439
450
|
// as pinned — it prints "Already up to date" and exits 0 without moving
|
|
440
451
|
// the installed version, so the panel would report a false success.
|
|
452
|
+
// `MIN_RELEASE_AGE_OVERRIDE` rides along every candidate: without it the
|
|
453
|
+
// pnpm 11 minimumReleaseAge gate (default 24 h) silently keeps same-day
|
|
454
|
+
// releases in place, which the post-run verification would then have to
|
|
455
|
+
// surface as `stale` even though the update is legitimate.
|
|
441
456
|
const candidates: ReadonlyArray<{ command: string; args: string[] }> = [
|
|
442
|
-
{ command: 'pnpm', args: ['update', '--latest', ...packages] },
|
|
443
|
-
{ command: 'corepack', args: ['pnpm', 'update', '--latest', ...packages] },
|
|
444
|
-
{ command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', ...packages] },
|
|
457
|
+
{ command: 'pnpm', args: ['update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
|
|
458
|
+
{ command: 'corepack', args: ['pnpm', 'update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
|
|
459
|
+
{ command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
|
|
445
460
|
]
|
|
446
461
|
// `output` accumulates across candidates for UI display; `currentOutput`
|
|
447
462
|
// is reset per candidate and carries only that candidate's own diagnostics
|
|
@@ -553,9 +568,11 @@ export interface UpdateRunVerifiedDeps {
|
|
|
553
568
|
|
|
554
569
|
/**
|
|
555
570
|
* Run the update, then verify the installed versions actually moved. pnpm
|
|
556
|
-
* exits 0 even when it silently kept the installed versions —
|
|
557
|
-
* `minimumReleaseAge` gate
|
|
558
|
-
*
|
|
571
|
+
* exits 0 even when it silently kept the installed versions — runUpdate
|
|
572
|
+
* overrides pnpm 11's `minimumReleaseAge` gate (default 24 h) with
|
|
573
|
+
* `--config.minimumReleaseAge=0`, but an older pnpm without the override or
|
|
574
|
+
* another silent no-op can still keep versions in place — so a green exit
|
|
575
|
+
* alone must not report a misleading "update complete". Re-read the
|
|
559
576
|
* installed versions afterwards and surface a `stale` failure (with the
|
|
560
577
|
* captured pnpm output) when nothing moved, so the panel can tell the user
|
|
561
578
|
* how to unblock the gate instead of claiming success.
|