@linxin666/dsh-remote-web-ui 0.3.2 → 0.3.4
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.i18n.yaml +2 -2
- package/README.md +22 -8
- package/README.zh.md +12 -7
- package/assets/mobile-service-worker.js +1 -2
- package/lib/client.js +177 -19
- package/lib/client.js.map +1 -1
- package/lib/index.js +222 -15
- package/lib/mobile.js +64 -13
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +2 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/RemoteEntry.js +1 -1
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +20 -3
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +4 -0
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +22 -10
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/client/settings-form.js +1 -1
- package/lib/types/client/telemetry.d.ts +27 -0
- package/lib/types/client/telemetry.d.ts.map +1 -0
- package/lib/types/client/telemetry.js +97 -0
- package/lib/types/dsh-home.d.ts.map +1 -1
- package/lib/types/dsh-home.js +9 -3
- package/lib/types/http.d.ts +1 -1
- package/lib/types/http.js +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +34 -5
- package/lib/types/loopback.d.ts +1 -1
- package/lib/types/loopback.js +1 -1
- package/lib/types/mobile/api.d.ts +7 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +9 -3
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +10 -0
- package/lib/types/mobile/views/ChatView.d.ts +1 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +32 -5
- package/lib/types/mobile/views/ChatView.stream.test.js +28 -2
- package/lib/types/mobile/views/ChatView.test.js +152 -2
- package/lib/types/mobile-api.d.ts +14 -0
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +56 -1
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +25 -2
- package/lib/types/mount-once.d.ts +1 -1
- package/lib/types/mount-once.js +2 -2
- package/lib/types/update.d.ts +35 -4
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +98 -4
- package/package.json +4 -2
- package/src/client/PluginSettingsCard.tsx +4 -2
- package/src/client/RemoteEntry.tsx +1 -1
- package/src/client/UpdatePanel.tsx +43 -3
- package/src/client/index.ts +6 -1
- package/src/client/locales.ts +22 -10
- package/src/client/remote.module.css +56 -3
- package/src/client/settings-card.module.css +5 -0
- package/src/client/settings-form.ts +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/dsh-home.ts +9 -3
- package/src/http.ts +1 -1
- package/src/index.ts +36 -4
- package/src/loopback.ts +1 -1
- package/src/mobile/api.ts +10 -3
- package/src/mobile/mobile-styles.ts +10 -0
- package/src/mobile/views/ChatView.stream.test.tsx +34 -2
- package/src/mobile/views/ChatView.test.tsx +177 -2
- package/src/mobile/views/ChatView.tsx +44 -6
- package/src/mobile-api.ts +61 -0
- package/src/mobile-routes.ts +24 -2
- package/src/mount-once.ts +2 -2
- package/src/update.ts +117 -4
package/lib/types/mobile-api.js
CHANGED
|
@@ -45,6 +45,7 @@ const MOBILE_ALLOWLIST = new Set([
|
|
|
45
45
|
'session.models',
|
|
46
46
|
'session.selectModel',
|
|
47
47
|
'session.rename',
|
|
48
|
+
'session.cancel',
|
|
48
49
|
]);
|
|
49
50
|
/**
|
|
50
51
|
* Locally answered display-preference method (the phone's read-only
|
|
@@ -54,6 +55,8 @@ const MOBILE_ALLOWLIST = new Set([
|
|
|
54
55
|
const MOBILE_PREFERENCES_METHOD = 'mobile.preferences';
|
|
55
56
|
const MOBILE_PENDING_METHOD = 'mobile.pending';
|
|
56
57
|
const MOBILE_RESPOND_METHOD = 'mobile.respond';
|
|
58
|
+
/** Execute a slash command through the host command registry (#1125). */
|
|
59
|
+
const MOBILE_COMMAND_METHOD = 'mobile.command';
|
|
57
60
|
/** One session.list page (thin phones load incrementally). */
|
|
58
61
|
const SESSION_PAGE_SIZE = 20;
|
|
59
62
|
/** SSE keep-alive ping cadence for the live mux stream (single connection). */
|
|
@@ -129,7 +132,8 @@ export function makeMobileApiRoutes(deps) {
|
|
|
129
132
|
const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length);
|
|
130
133
|
const local = method === MOBILE_PREFERENCES_METHOD
|
|
131
134
|
|| method === MOBILE_PENDING_METHOD
|
|
132
|
-
|| method === MOBILE_RESPOND_METHOD
|
|
135
|
+
|| method === MOBILE_RESPOND_METHOD
|
|
136
|
+
|| method === MOBILE_COMMAND_METHOD;
|
|
133
137
|
if (!MOBILE_ALLOWLIST.has(method) && !local) {
|
|
134
138
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: `method ${method} is not exposed to the mobile surface` } });
|
|
135
139
|
return;
|
|
@@ -187,6 +191,55 @@ export function makeMobileApiRoutes(deps) {
|
|
|
187
191
|
});
|
|
188
192
|
}
|
|
189
193
|
}
|
|
194
|
+
else if (method === MOBILE_COMMAND_METHOD) {
|
|
195
|
+
const payload = parsed.payload;
|
|
196
|
+
const sessionId = payload?.sessionId;
|
|
197
|
+
const line = payload?.line;
|
|
198
|
+
if (typeof sessionId !== 'string' || typeof line !== 'string') {
|
|
199
|
+
writeJson(res, 200, {
|
|
200
|
+
type: 'server-response',
|
|
201
|
+
rpcId,
|
|
202
|
+
result: { ok: false, error: { code: 'bad-request', message: 'missing sessionId or line' } },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else if (deps.commandDispatcher === undefined) {
|
|
206
|
+
writeJson(res, 200, {
|
|
207
|
+
type: 'server-response',
|
|
208
|
+
rpcId,
|
|
209
|
+
result: { ok: false, error: { code: 'unavailable', message: 'command dispatcher is not available' } },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
try {
|
|
214
|
+
const abort = new AbortController();
|
|
215
|
+
res.on('close', () => { if (!res.writableEnded)
|
|
216
|
+
abort.abort(); });
|
|
217
|
+
const outcome = await deps.commandDispatcher.execute(sessionId, line, abort.signal);
|
|
218
|
+
if ('error' in outcome) {
|
|
219
|
+
writeJson(res, 200, {
|
|
220
|
+
type: 'server-response',
|
|
221
|
+
rpcId,
|
|
222
|
+
result: { ok: false, error: { code: outcome.error, message: outcome.error } },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
writeJson(res, 200, {
|
|
227
|
+
type: 'server-response',
|
|
228
|
+
rpcId,
|
|
229
|
+
result: { ok: true, value: { matched: true, result: outcome.result } },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
235
|
+
writeJson(res, 200, {
|
|
236
|
+
type: 'server-response',
|
|
237
|
+
rpcId,
|
|
238
|
+
result: { ok: false, error: { code: 'internal', message } },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
190
243
|
return;
|
|
191
244
|
}
|
|
192
245
|
try {
|
|
@@ -345,5 +398,7 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
|
345
398
|
return wrap(await apiProxy.sessions.selectModel(request));
|
|
346
399
|
if (method === 'session.rename')
|
|
347
400
|
return wrap(await apiProxy.sessions.rename(request));
|
|
401
|
+
if (method === 'session.cancel')
|
|
402
|
+
return wrap(await apiProxy.sessions.cancel(request));
|
|
348
403
|
throw new Error(`unhandled allowlisted method ${method}`);
|
|
349
404
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAwH/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CA8F7C"}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { readFile } from 'node:fs/promises';
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { gzipSync } from 'node:zlib';
|
|
10
11
|
const MOBILE_ROOT = '/m/';
|
|
11
12
|
const MOBILE_BUNDLE_URL = '/m/mobile.js';
|
|
12
13
|
const MOBILE_MANIFEST_URL = '/m/manifest.webmanifest';
|
|
@@ -132,7 +133,8 @@ export function makeMobileRoutes() {
|
|
|
132
133
|
// The bundle is immutable for the process lifetime (a rebuild requires a
|
|
133
134
|
// host restart), so the body is read from disk once, not per phone.
|
|
134
135
|
let bundleBody;
|
|
135
|
-
|
|
136
|
+
let bundleGzip;
|
|
137
|
+
const handleBundle = async (req, res) => {
|
|
136
138
|
if (bundleBody === undefined) {
|
|
137
139
|
const path = mobileBundlePath();
|
|
138
140
|
if (!existsSync(path)) {
|
|
@@ -141,13 +143,34 @@ export function makeMobileRoutes() {
|
|
|
141
143
|
}
|
|
142
144
|
try {
|
|
143
145
|
bundleBody = await readFile(path, 'utf8');
|
|
146
|
+
bundleGzip = gzipSync(Buffer.from(bundleBody, 'utf8'));
|
|
144
147
|
}
|
|
145
148
|
catch {
|
|
146
149
|
writeStatic(res, 500, 'text/plain', 'failed to read the mobile bundle');
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
|
-
|
|
153
|
+
const acceptEncoding = req.headers['accept-encoding'] ?? '';
|
|
154
|
+
const acceptsGzip = typeof acceptEncoding === 'string' && acceptEncoding.includes('gzip') && bundleGzip !== undefined;
|
|
155
|
+
if (acceptsGzip) {
|
|
156
|
+
res.writeHead(200, {
|
|
157
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
158
|
+
'content-encoding': 'gzip',
|
|
159
|
+
'cache-control': 'no-cache',
|
|
160
|
+
vary: 'Accept-Encoding',
|
|
161
|
+
'referrer-policy': 'no-referrer',
|
|
162
|
+
});
|
|
163
|
+
res.end(bundleGzip);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
res.writeHead(200, {
|
|
167
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
168
|
+
'cache-control': 'no-cache',
|
|
169
|
+
vary: 'Accept-Encoding',
|
|
170
|
+
'referrer-policy': 'no-referrer',
|
|
171
|
+
});
|
|
172
|
+
res.end(bundleBody);
|
|
173
|
+
}
|
|
151
174
|
};
|
|
152
175
|
let workerBody;
|
|
153
176
|
const handleWorker = async (_req, res) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
3
|
-
* (dsh-web-
|
|
3
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
4
4
|
* the loader accepts a standalone install of the same package side by side;
|
|
5
5
|
* without this guard the second instance would still re-register the same
|
|
6
6
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
package/lib/types/mount-once.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
2
|
/**
|
|
3
3
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
4
|
-
* (dsh-web-
|
|
4
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
5
5
|
* the loader accepts a standalone install of the same package side by side;
|
|
6
6
|
* without this guard the second instance would still re-register the same
|
|
7
7
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
15
15
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
16
16
|
*/
|
|
17
|
-
const MOUNTED = Symbol.for('dsh-web
|
|
17
|
+
const MOUNTED = Symbol.for('dsh-web.mounted-plugins');
|
|
18
18
|
function mountedSet() {
|
|
19
19
|
const registry = globalThis;
|
|
20
20
|
return (registry[MOUNTED] ??= new Set());
|
package/lib/types/update.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remote update support for the dsh-web
|
|
3
|
-
* installed aggregate package (@linxin666/dsh-web-
|
|
2
|
+
* Remote update support for the dsh-web family — host half. Detects the
|
|
3
|
+
* installed aggregate package (@linxin666/dsh-web-all), or the directly
|
|
4
4
|
* installed family packages when the aggregate is absent, probes npm for newer
|
|
5
5
|
* releases, and runs `pnpm update --latest` inside the owning dsh profile.
|
|
6
6
|
*
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
12
|
/** npm registry base used for version probes. */
|
|
13
13
|
export declare const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
14
|
-
/** The family scope every dsh-web
|
|
14
|
+
/** The family scope every dsh-web package is published under. */
|
|
15
15
|
export declare const FAMILY_SCOPE = "@linxin666/";
|
|
16
16
|
/** The aggregate package that is the canonical update entry point. */
|
|
17
|
-
export declare const AGGREGATE_PACKAGE = "@linxin666/dsh-web-
|
|
17
|
+
export declare const AGGREGATE_PACKAGE = "@linxin666/dsh-web-all";
|
|
18
18
|
/** Fallback anchor: this plugin's own package when the aggregate is absent. */
|
|
19
19
|
export declare const SELF_PACKAGE = "@linxin666/dsh-remote-web-ui";
|
|
20
|
+
/** GitHub repository used to surface human-readable release notes. */
|
|
21
|
+
export declare const UPDATE_RELEASE_REPO = "zhu1090093659/dsh-web";
|
|
22
|
+
/** Release-notes cache freshness window (host process). */
|
|
23
|
+
export declare const RELEASE_NOTES_CACHE_TTL_MS: number;
|
|
20
24
|
/** A parsed semantic version (prerelease identifiers kept as strings). */
|
|
21
25
|
export interface SemverParts {
|
|
22
26
|
major: number;
|
|
@@ -74,6 +78,17 @@ export interface UpdatePackageStatus {
|
|
|
74
78
|
/** Whether npm carries a strictly newer release. */
|
|
75
79
|
outdated: boolean;
|
|
76
80
|
}
|
|
81
|
+
/** Structured release-note sections shown in the update panel. */
|
|
82
|
+
export interface UpdateReleaseNotes {
|
|
83
|
+
/** Version the notes describe. */
|
|
84
|
+
version: string;
|
|
85
|
+
/** New feature bullets. */
|
|
86
|
+
features: string[];
|
|
87
|
+
/** Bug-fix bullets. */
|
|
88
|
+
fixes: string[];
|
|
89
|
+
/** Other-change bullets. */
|
|
90
|
+
other: string[];
|
|
91
|
+
}
|
|
77
92
|
/** The full update-status snapshot served to the browser half. */
|
|
78
93
|
export interface UpdateStatus {
|
|
79
94
|
/** npm = registry-managed (updatable); link = local dev install; missing = no anchor package. */
|
|
@@ -86,6 +101,8 @@ export interface UpdateStatus {
|
|
|
86
101
|
packages: UpdatePackageStatus[];
|
|
87
102
|
/** True when any package has a newer npm release. */
|
|
88
103
|
outdated: boolean;
|
|
104
|
+
/** Human-readable release notes for the target release, when available. */
|
|
105
|
+
notes?: UpdateReleaseNotes;
|
|
89
106
|
/** Whole-check failure (e.g. registry unreachable). */
|
|
90
107
|
error?: string;
|
|
91
108
|
}
|
|
@@ -97,6 +114,8 @@ export interface UpdateCheckDeps {
|
|
|
97
114
|
resolve(specifier: string): string | undefined;
|
|
98
115
|
/** Probe one package's latest npm version; undefined on failure. */
|
|
99
116
|
fetchLatest(name: string): Promise<string | undefined>;
|
|
117
|
+
/** Fetch structured release notes for one target version, when available. */
|
|
118
|
+
fetchReleaseNotes?(version: string): Promise<UpdateReleaseNotes | undefined>;
|
|
100
119
|
}
|
|
101
120
|
/**
|
|
102
121
|
* Resolve the anchor package's manifest path. The aggregate package is the
|
|
@@ -141,6 +160,18 @@ export declare function fetchLatestVersion(name: string, fetchImpl: (url: string
|
|
|
141
160
|
ok: boolean;
|
|
142
161
|
json(): Promise<unknown>;
|
|
143
162
|
}>, timeoutMs?: number): Promise<string | undefined>;
|
|
163
|
+
export declare function parseReleaseNotesBody(version: string, body: string): UpdateReleaseNotes | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* Fetch and parse the GitHub Release for one `dsh-web` version.
|
|
166
|
+
* @param version - the npm release version (without the `v` prefix).
|
|
167
|
+
* @param fetchImpl - injected fetch; global `fetch` in the host.
|
|
168
|
+
* @param timeoutMs - hard timeout for the GitHub API probe.
|
|
169
|
+
* @returns structured notes, or undefined when unavailable.
|
|
170
|
+
*/
|
|
171
|
+
export declare function fetchGitHubReleaseNotes(version: string, fetchImpl?: (url: string, init?: RequestInit) => Promise<{
|
|
172
|
+
ok: boolean;
|
|
173
|
+
json(): Promise<unknown>;
|
|
174
|
+
}>, timeoutMs?: number): Promise<UpdateReleaseNotes | undefined>;
|
|
144
175
|
/**
|
|
145
176
|
* Build the update status: locate the anchor, detect the install mode, and
|
|
146
177
|
* compare every family package against the npm registry.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,iDAAiD;AACjD,eAAO,MAAM,YAAY,+BAA+B,CAAA;AAExD,
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|
package/lib/types/update.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remote update support for the dsh-web
|
|
3
|
-
* installed aggregate package (@linxin666/dsh-web-
|
|
2
|
+
* Remote update support for the dsh-web family — host half. Detects the
|
|
3
|
+
* installed aggregate package (@linxin666/dsh-web-all), or the directly
|
|
4
4
|
* installed family packages when the aggregate is absent, probes npm for newer
|
|
5
5
|
* releases, and runs `pnpm update --latest` inside the owning dsh profile.
|
|
6
6
|
*
|
|
@@ -13,12 +13,16 @@ import { dirname, join } from 'node:path';
|
|
|
13
13
|
import { spawn } from 'node:child_process';
|
|
14
14
|
/** npm registry base used for version probes. */
|
|
15
15
|
export const NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
16
|
-
/** The family scope every dsh-web
|
|
16
|
+
/** The family scope every dsh-web package is published under. */
|
|
17
17
|
export const FAMILY_SCOPE = '@linxin666/';
|
|
18
18
|
/** The aggregate package that is the canonical update entry point. */
|
|
19
|
-
export const AGGREGATE_PACKAGE = '@linxin666/dsh-web-
|
|
19
|
+
export const AGGREGATE_PACKAGE = '@linxin666/dsh-web-all';
|
|
20
20
|
/** Fallback anchor: this plugin's own package when the aggregate is absent. */
|
|
21
21
|
export const SELF_PACKAGE = '@linxin666/dsh-remote-web-ui';
|
|
22
|
+
/** GitHub repository used to surface human-readable release notes. */
|
|
23
|
+
export const UPDATE_RELEASE_REPO = 'zhu1090093659/dsh-web';
|
|
24
|
+
/** Release-notes cache freshness window (host process). */
|
|
25
|
+
export const RELEASE_NOTES_CACHE_TTL_MS = 10 * 60_000;
|
|
22
26
|
/** A profile manifest `name` prefix (e.g. `dsh-profile-web`). */
|
|
23
27
|
const PROFILE_NAME_PREFIX = 'dsh-profile-';
|
|
24
28
|
/** How many ancestor directories a profile search walks before giving up. */
|
|
@@ -242,6 +246,91 @@ export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
|
|
|
242
246
|
return undefined;
|
|
243
247
|
}
|
|
244
248
|
}
|
|
249
|
+
/** Normalize one release-note bullet. */
|
|
250
|
+
function cleanNoteItem(text) {
|
|
251
|
+
// Keep the visible label and drop Markdown link targets; issue references
|
|
252
|
+
// become plain #123 because the panel has no Markdown renderer.
|
|
253
|
+
return text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/^[-*]\s+/, '').trim();
|
|
254
|
+
}
|
|
255
|
+
/** Determine the section kind for a Markdown heading line. */
|
|
256
|
+
function noteSectionOf(line) {
|
|
257
|
+
const heading = /^#{1,6}\s+(.+)$/.exec(line.trim());
|
|
258
|
+
if (heading === null)
|
|
259
|
+
return undefined;
|
|
260
|
+
const title = heading[1].replace(/[::].*$/, '').trim().toLowerCase();
|
|
261
|
+
if (title === '新功能' || title === 'new features')
|
|
262
|
+
return 'features';
|
|
263
|
+
if (title === '修复' || title === 'bug fixes' || title === 'bugfixes')
|
|
264
|
+
return 'fixes';
|
|
265
|
+
if (title === '其他改动' || title === 'other changes')
|
|
266
|
+
return 'other';
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Parse the GitHub Release body into the three conventional sections.
|
|
271
|
+
*
|
|
272
|
+
* The committed release notes are bilingual: Chinese is the default view and
|
|
273
|
+
* English lives inside a `<details>` block. Prefer the Chinese sections when
|
|
274
|
+
* present and fall back to the whole body for English-only releases. Bullets
|
|
275
|
+
* are escaped as plain text for the panel (no Markdown renderer).
|
|
276
|
+
*/
|
|
277
|
+
function parseReleaseNotesSource(version, source) {
|
|
278
|
+
const notes = { version, features: [], fixes: [], other: [] };
|
|
279
|
+
let current;
|
|
280
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
281
|
+
const line = rawLine.trim();
|
|
282
|
+
const section = noteSectionOf(line);
|
|
283
|
+
if (section !== undefined) {
|
|
284
|
+
current = section;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (current === undefined || !/^-\s+/.test(line))
|
|
288
|
+
continue;
|
|
289
|
+
const item = cleanNoteItem(line);
|
|
290
|
+
if (item !== '')
|
|
291
|
+
notes[current].push(item);
|
|
292
|
+
}
|
|
293
|
+
if (notes.features.length === 0 && notes.fixes.length === 0 && notes.other.length === 0)
|
|
294
|
+
return undefined;
|
|
295
|
+
return notes;
|
|
296
|
+
}
|
|
297
|
+
export function parseReleaseNotesBody(version, body) {
|
|
298
|
+
// The committed release notes put Chinese in the default view and English in
|
|
299
|
+
// a <details> block. Parse only the default view first, then fall back to
|
|
300
|
+
// the full body for English-only releases.
|
|
301
|
+
return parseReleaseNotesSource(version, body.split('<details>')[0] ?? body)
|
|
302
|
+
?? parseReleaseNotesSource(version, body);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Fetch and parse the GitHub Release for one `dsh-web` version.
|
|
306
|
+
* @param version - the npm release version (without the `v` prefix).
|
|
307
|
+
* @param fetchImpl - injected fetch; global `fetch` in the host.
|
|
308
|
+
* @param timeoutMs - hard timeout for the GitHub API probe.
|
|
309
|
+
* @returns structured notes, or undefined when unavailable.
|
|
310
|
+
*/
|
|
311
|
+
export async function fetchGitHubReleaseNotes(version, fetchImpl = fetch, timeoutMs = 10_000) {
|
|
312
|
+
const controller = new AbortController();
|
|
313
|
+
const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
|
|
314
|
+
try {
|
|
315
|
+
const response = await fetchImpl(`https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`, {
|
|
316
|
+
headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-web-update' },
|
|
317
|
+
signal: controller.signal,
|
|
318
|
+
});
|
|
319
|
+
if (!response.ok)
|
|
320
|
+
return undefined;
|
|
321
|
+
const body = await response.json();
|
|
322
|
+
if (typeof body !== 'object' || body === null)
|
|
323
|
+
return undefined;
|
|
324
|
+
const text = body.body;
|
|
325
|
+
return typeof text === 'string' ? parseReleaseNotesBody(version, text) : undefined;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
245
334
|
/**
|
|
246
335
|
* Sentinel for an installed version that could not be read (missing manifest
|
|
247
336
|
* or resolve failure). It is a real-looking version so `checkUpdates` can
|
|
@@ -319,12 +408,17 @@ export async function checkUpdates(deps) {
|
|
|
319
408
|
// Registry unreachable: every probe failed — report the outage distinctly
|
|
320
409
|
// instead of a misleading "all up to date" (the panel needs the reason).
|
|
321
410
|
const error = probeFailures === names.length && names.length > 0 ? 'registry-unreachable' : undefined;
|
|
411
|
+
const targetVersion = packages.find(packageStatus => packageStatus.latest !== undefined)?.latest;
|
|
412
|
+
const notes = targetVersion !== undefined && deps.fetchReleaseNotes !== undefined
|
|
413
|
+
? await deps.fetchReleaseNotes(targetVersion).catch(() => undefined)
|
|
414
|
+
: undefined;
|
|
322
415
|
return {
|
|
323
416
|
mode: linked ? 'link' : 'npm',
|
|
324
417
|
profileName: profile.name,
|
|
325
418
|
anchor,
|
|
326
419
|
packages,
|
|
327
420
|
outdated: packages.some(packageStatus => packageStatus.outdated),
|
|
421
|
+
...(notes !== undefined ? { notes } : {}),
|
|
328
422
|
...(error !== undefined ? { error } : {}),
|
|
329
423
|
};
|
|
330
424
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair mobile and PC remote control for the dsh web GUI: QR entry beside the settings button, one-time pairing tokens, a gated /remote/api desktop channel, live device status, and revocable sessions",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -75,6 +75,8 @@
|
|
|
75
75
|
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
|
|
76
76
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
77
77
|
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
|
|
78
80
|
"@testing-library/dom": "^10.4.1",
|
|
79
81
|
"@testing-library/react": "^16.3.2",
|
|
80
82
|
"@types/node": "^22.20.0",
|
|
@@ -90,7 +92,7 @@
|
|
|
90
92
|
},
|
|
91
93
|
"repository": {
|
|
92
94
|
"type": "git",
|
|
93
|
-
"url": "https://github.com/zhu1090093659/dsh-web
|
|
95
|
+
"url": "https://github.com/zhu1090093659/dsh-web.git"
|
|
94
96
|
},
|
|
95
97
|
"scripts": {
|
|
96
98
|
"build": "tsc -b && tsdown",
|
|
@@ -39,6 +39,8 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
39
39
|
titleKey: TKey
|
|
40
40
|
/** Locale key of the line describing what this plugin's settings govern. */
|
|
41
41
|
descriptionKey: TKey
|
|
42
|
+
/** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */
|
|
43
|
+
descriptionNode?: ReactNode
|
|
42
44
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
43
45
|
state: CardShell
|
|
44
46
|
/** Write every staged edit. */
|
|
@@ -87,7 +89,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
87
89
|
<div className={css.headerStatic}>
|
|
88
90
|
<span className={css.headText}>
|
|
89
91
|
<span className={css.name} title={title}>{title}</span>
|
|
90
|
-
<span className={css.description} title={description}>{description}</span>
|
|
92
|
+
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
91
93
|
</span>
|
|
92
94
|
{state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
|
|
93
95
|
</div>
|
|
@@ -102,7 +104,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
102
104
|
>
|
|
103
105
|
<span className={css.headText}>
|
|
104
106
|
<span className={css.name} title={title}>{title}</span>
|
|
105
|
-
<span className={css.description} title={description}>{description}</span>
|
|
107
|
+
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
106
108
|
</span>
|
|
107
109
|
{state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
|
|
108
110
|
<svg
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* trigger beside the settings button, and the pairing panel modal. Owns the
|
|
4
4
|
* panel behavior — token minting on open, the status SSE subscription,
|
|
5
5
|
* stop/refresh/copy — and renders the pure {@link RemotePanel} body. The
|
|
6
|
-
* update seat (the dsh-web
|
|
6
|
+
* update seat (the dsh-web self-update flow) rides the same footer row,
|
|
7
7
|
* rendered by {@link UpdateEntry}. Component-local state per the client
|
|
8
8
|
* stack rules: nothing here survives remounts or crosses entries.
|
|
9
9
|
*/
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
8
|
import { IconCloseOutline16, IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
9
|
-
import type { UpdateRunResult, UpdateStatus } from '../update.ts'
|
|
9
|
+
import type { UpdateReleaseNotes, UpdateRunResult, UpdateStatus } from '../update.ts'
|
|
10
10
|
import css from "./remote.module.css"
|
|
11
11
|
|
|
12
12
|
/** The panel view state, owned by the entry component. */
|
|
@@ -73,7 +73,7 @@ export function UpdatePanel({ t, view, onClose, onRecheck, onStartUpdate }: Upda
|
|
|
73
73
|
{t("update.updating", { name: anchorName(status) ?? "", version: anchorLatest(status) ?? "" })}
|
|
74
74
|
</p>
|
|
75
75
|
<p className={css.updateDetail}>{t("update.cooldownNotice")}</p>
|
|
76
|
-
<
|
|
76
|
+
<PackageSummary t={t} status={status} />
|
|
77
77
|
</div>
|
|
78
78
|
)}
|
|
79
79
|
{view.kind === "done" && <DoneBody t={t} result={view.result} />}
|
|
@@ -147,6 +147,7 @@ function ResultBody({ t, status }: { t: TranslateNS<"remote">; status: UpdateSta
|
|
|
147
147
|
{anchor !== undefined ? t("update.foundDetail", { name: anchor, version: latest ?? "" }) : ""}
|
|
148
148
|
</p>
|
|
149
149
|
<p className={css.updateDetail}>{t("update.cooldownNotice")}</p>
|
|
150
|
+
<PackageSummary t={t} status={status} />
|
|
150
151
|
</div>
|
|
151
152
|
)
|
|
152
153
|
}
|
|
@@ -156,7 +157,46 @@ function ResultBody({ t, status }: { t: TranslateNS<"remote">; status: UpdateSta
|
|
|
156
157
|
{anchor !== undefined && latest !== undefined && (
|
|
157
158
|
<p className={css.updateDetail}>{t("update.upToDateDetail", { name: anchor, version: latest })}</p>
|
|
158
159
|
)}
|
|
159
|
-
<
|
|
160
|
+
<PackageSummary t={t} status={status} />
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Release-note summary when available; otherwise fall back to the package list. */
|
|
166
|
+
function PackageSummary({ t, status }: { t: TranslateNS<'remote'>; status: UpdateStatus }) {
|
|
167
|
+
if (status.notes === undefined) return <PackageList status={status} />
|
|
168
|
+
return (
|
|
169
|
+
<div>
|
|
170
|
+
<ReleaseNotes t={t} notes={status.notes} />
|
|
171
|
+
<details className={css.updateVersions}>
|
|
172
|
+
<summary className={css.updateVersionsSummary}>{t("update.componentVersions")}</summary>
|
|
173
|
+
<PackageList status={status} />
|
|
174
|
+
</details>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Render GitHub Release sections as a compact three-group list. */
|
|
180
|
+
function ReleaseNotes({ t, notes }: { t: TranslateNS<'remote'>; notes: UpdateReleaseNotes }) {
|
|
181
|
+
const sections = [
|
|
182
|
+
{ key: 'features', title: t("update.releaseFeatures"), items: notes.features },
|
|
183
|
+
{ key: 'fixes', title: t("update.releaseFixes"), items: notes.fixes },
|
|
184
|
+
{ key: 'other', title: t("update.releaseOther"), items: notes.other },
|
|
185
|
+
].filter(section => section.items.length > 0)
|
|
186
|
+
if (sections.length === 0) return <p className={css.updateDetail}>{t("update.releaseUnavailable")}</p>
|
|
187
|
+
return (
|
|
188
|
+
<div className={css.updateNotes}>
|
|
189
|
+
<h3 className={css.updateNotesTitle}>{t("update.releaseNotes", { version: notes.version })}</h3>
|
|
190
|
+
{sections.map(section => (
|
|
191
|
+
<section key={section.key} className={css.updateNoteSection}>
|
|
192
|
+
<h4 className={css.updateNoteHeading}>{section.title}</h4>
|
|
193
|
+
<ul className={css.updateNoteList}>
|
|
194
|
+
{section.items.map((item, index) => (
|
|
195
|
+
<li key={index} className={css.updateNoteItem}>{item}</li>
|
|
196
|
+
))}
|
|
197
|
+
</ul>
|
|
198
|
+
</section>
|
|
199
|
+
))}
|
|
160
200
|
</div>
|
|
161
201
|
)
|
|
162
202
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type RemoteChannelBootSeat,
|
|
34
34
|
} from './remote-channel.ts'
|
|
35
35
|
import { FenceNotice } from './FenceNotice.tsx'
|
|
36
|
+
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
36
37
|
|
|
37
38
|
export type { RemoteEntryProps } from './RemoteEntry.tsx'
|
|
38
39
|
export type { PanelState, RemotePanelProps } from './RemotePanel.tsx'
|
|
@@ -80,7 +81,7 @@ export interface SettingsPluginItemOwnerProps {
|
|
|
80
81
|
declare module '@deepseek-ai/cordis' {
|
|
81
82
|
interface Context {
|
|
82
83
|
/**
|
|
83
|
-
* Optional rc.6 compatibility binder provided by dsh-web-
|
|
84
|
+
* Optional rc.6 compatibility binder provided by dsh-web-settings;
|
|
84
85
|
* absent when that group plugin is not installed, so callers fall back to
|
|
85
86
|
* the official settings scope.
|
|
86
87
|
*/
|
|
@@ -106,6 +107,10 @@ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote
|
|
|
106
107
|
* @param ctx - client root context.
|
|
107
108
|
*/
|
|
108
109
|
export function apply(ctx: ClientContext): void {
|
|
110
|
+
// Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
|
|
111
|
+
// UTC day, package name only, silent failure.
|
|
112
|
+
reportDailyHeartbeat([{ name: '@linxin666/dsh-remote-web-ui' }])
|
|
113
|
+
|
|
109
114
|
ctx.effect(() => {
|
|
110
115
|
try {
|
|
111
116
|
return ctx.locale.register(NS, { zh, en })
|