@linxin666/dsh-remote-web-ui 0.3.2 → 0.3.3
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/lib/client.js +174 -16
- package/lib/client.js.map +1 -1
- package/lib/index.js +104 -7
- package/lib/mobile.js +62 -7
- package/lib/mobile.js.map +1 -1
- 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/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 +14 -3
- 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 +7 -0
- 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.map +1 -1
- package/lib/types/mobile-api.js +3 -0
- 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 +2 -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-form.ts +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/http.ts +1 -1
- package/src/index.ts +16 -2
- package/src/loopback.ts +1 -1
- package/src/mobile/api.ts +8 -0
- 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 +2 -0
- package/src/mount-once.ts +2 -2
- package/src/update.ts +117 -4
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.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
},
|
|
91
91
|
"repository": {
|
|
92
92
|
"type": "git",
|
|
93
|
-
"url": "https://github.com/zhu1090093659/dsh-web
|
|
93
|
+
"url": "https://github.com/zhu1090093659/dsh-web.git"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"build": "tsc -b && tsdown",
|
|
@@ -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 })
|
package/src/client/locales.ts
CHANGED
|
@@ -114,21 +114,27 @@ export const zh = {
|
|
|
114
114
|
'update.doneDetail': '所有组件已更新到最新版本。',
|
|
115
115
|
'update.restartHint': '请重启 dsh web 使新版本生效:在终端按 Ctrl+C 停止后重新运行 dsh web。',
|
|
116
116
|
'update.linkMode': '当前为本地开发模式',
|
|
117
|
-
'update.linkModeDetail': 'dsh-web
|
|
118
|
-
'update.missing': '未找到 dsh-web
|
|
119
|
-
'update.missingDetail': '无法定位已安装的 dsh-web
|
|
117
|
+
'update.linkModeDetail': 'dsh-web 或其组件通过本地链接安装(开发模式),无法自动更新。npm 上最新版本:{version}。请同步本地仓库,或移除与全家桶重复的独立链接。',
|
|
118
|
+
'update.missing': '未找到 dsh-web 安装',
|
|
119
|
+
'update.missingDetail': '无法定位已安装的 dsh-web 聚合包。请先运行 dsh plugin --profile <profile> add @linxin666/dsh-web-all。',
|
|
120
120
|
'update.offline': '无法连接更新源',
|
|
121
121
|
'update.offlineDetail': '网络不可用或更新服务异常,请稍后重试。',
|
|
122
122
|
'update.unmounted': '更新服务未加载',
|
|
123
123
|
'update.unmountedDetail': '当前 dsh web 进程运行的插件版本不含更新服务,请重启 dsh web(Ctrl+C 后重新运行)后再试。',
|
|
124
124
|
'update.error': '更新失败',
|
|
125
125
|
'update.packages': '组件版本',
|
|
126
|
+
'update.releaseNotes': 'v{version} 更新说明',
|
|
127
|
+
'update.releaseFeatures': '新增功能',
|
|
128
|
+
'update.releaseFixes': '修复',
|
|
129
|
+
'update.releaseOther': '其他变动',
|
|
130
|
+
'update.releaseUnavailable': '本次发布说明暂不可用,可前往 GitHub Release 查看。',
|
|
131
|
+
'update.componentVersions': '查看组件版本',
|
|
126
132
|
'update.retry': '重新检查',
|
|
127
133
|
'update.close': '关闭更新面板',
|
|
128
134
|
'update.error.pnpmMissing': '未找到 pnpm,已尝试 pnpm / corepack / npx 仍不可用。请安装 pnpm 后重启应用(Windows: npm install -g pnpm 或 corepack enable;macOS: brew install pnpm 或 corepack enable)。',
|
|
129
135
|
'update.error.timeout': '更新超时,已终止安装进程,请重试。',
|
|
130
|
-
'update.error.notFound': '未找到 dsh-web
|
|
131
|
-
'update.error.link': 'dsh-web
|
|
136
|
+
'update.error.notFound': '未找到 dsh-web 聚合包安装,无法更新。',
|
|
137
|
+
'update.error.link': 'dsh-web 或其组件使用本地链接,无法自动更新;请同步本地仓库,或移除与全家桶重复的独立链接。',
|
|
132
138
|
'update.error.pnpmFailed': '更新执行失败(pnpm 退出码 {code}),详见下方输出。',
|
|
133
139
|
'update.error.stale': '更新命令已执行,但安装版本未变化。常见原因:pnpm 11 的 minimumReleaseAge 门禁会静默跳过发布不足 24 小时的新版本。请在 profile 目录(~/.dsh/profiles/<profile>)的 pnpm-workspace.yaml 中配置 minimumReleaseAgeExclude(例如 "@linxin666/*")或 minimumReleaseAge: 0 后重新点击更新。',
|
|
134
140
|
'update.error.verifyFailed': '更新命令已执行,但无法核对安装后的版本(可能是 registry 探测失败或安装路径变化)。请查看下方输出,或稍后重试。',
|
|
@@ -255,21 +261,27 @@ export const en = {
|
|
|
255
261
|
'update.doneDetail': 'All components are up to date.',
|
|
256
262
|
'update.restartHint': 'Restart dsh web for the new version to take effect: stop it with Ctrl+C in the terminal, then run dsh web again.',
|
|
257
263
|
'update.linkMode': 'Local development mode',
|
|
258
|
-
'update.linkModeDetail': 'dsh-web
|
|
259
|
-
'update.missing': 'dsh-web
|
|
260
|
-
'update.missingDetail': 'Could not locate the installed dsh-web
|
|
264
|
+
'update.linkModeDetail': 'dsh-web or one of its components is installed via a local link and cannot be updated remotely. Latest npm release: {version}. Sync the local checkout, or remove a standalone link duplicated by the aggregate.',
|
|
265
|
+
'update.missing': 'dsh-web not found',
|
|
266
|
+
'update.missingDetail': 'Could not locate the installed dsh-web aggregate package. Install it with: dsh plugin --profile <profile> add @linxin666/dsh-web-all',
|
|
261
267
|
'update.offline': 'Cannot reach the update source',
|
|
262
268
|
'update.offlineDetail': 'The network is unavailable or the update service is failing; try again later.',
|
|
263
269
|
'update.unmounted': 'The update service is not loaded',
|
|
264
270
|
'update.unmountedDetail': 'The running dsh web process loads a plugin build without the update service; restart dsh web (Ctrl+C, then run dsh web again) and retry.',
|
|
265
271
|
'update.error': 'Update failed',
|
|
266
272
|
'update.packages': 'Component versions',
|
|
273
|
+
'update.releaseNotes': 'v{version} release notes',
|
|
274
|
+
'update.releaseFeatures': 'New Features',
|
|
275
|
+
'update.releaseFixes': 'Bug Fixes',
|
|
276
|
+
'update.releaseOther': 'Other Changes',
|
|
277
|
+
'update.releaseUnavailable': 'Release notes are unavailable for this version; see the GitHub release instead.',
|
|
278
|
+
'update.componentVersions': 'View component versions',
|
|
267
279
|
'update.retry': 'Check again',
|
|
268
280
|
'update.close': 'Close update panel',
|
|
269
281
|
'update.error.pnpmMissing': 'pnpm was not found (tried pnpm / corepack / npx). Install pnpm and restart the app (Windows: npm install -g pnpm or corepack enable; macOS: brew install pnpm or corepack enable).',
|
|
270
282
|
'update.error.timeout': 'The update timed out and the install process was killed; try again.',
|
|
271
|
-
'update.error.notFound': 'The dsh-web
|
|
272
|
-
'update.error.link': 'dsh-web
|
|
283
|
+
'update.error.notFound': 'The dsh-web aggregate package is not installed; nothing to update.',
|
|
284
|
+
'update.error.link': 'dsh-web or one of its components uses a local link. Sync the checkout, or remove a standalone link duplicated by the aggregate.',
|
|
273
285
|
'update.error.pnpmFailed': 'The update failed (pnpm exited with code {code}); see the output below.',
|
|
274
286
|
'update.error.stale': 'The update command ran but the installed versions did not change. Likely cause: the pnpm 11 minimumReleaseAge gate silently skips releases published less than 24 hours ago. Add minimumReleaseAgeExclude (e.g. "@linxin666/*") or set minimumReleaseAge: 0 in pnpm-workspace.yaml under the profile directory (~/.dsh/profiles/<profile>), then run the update again.',
|
|
275
287
|
'update.error.verifyFailed': 'The update command ran but the installed versions could not be verified afterwards (registry probe failed or the install path changed). Inspect the output below, or retry later.',
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
backdrop-filter: var(--dsw-mask-blur);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/* Trigger:
|
|
22
|
-
|
|
21
|
+
/* Trigger: the 36px rail circle, or a fully rounded (pill) full-row button
|
|
22
|
+
in wide mode — both maximally rounded so the footer action pair shares one
|
|
23
|
+
shape family (#1035); the seat's owner (ui-sidebar) lays out the
|
|
24
|
+
geometry. */
|
|
23
25
|
.trigger {
|
|
24
26
|
position: relative;
|
|
25
27
|
flex: none;
|
|
@@ -57,7 +59,9 @@
|
|
|
57
59
|
justify-content: flex-start;
|
|
58
60
|
width: auto;
|
|
59
61
|
min-width: 0;
|
|
60
|
-
|
|
62
|
+
/* Pill: maximal rounding, same shape family as the 36px rail circle next
|
|
63
|
+
to it (the update trigger) — one consistent footer-action look (#1035). */
|
|
64
|
+
border-radius: 999px;
|
|
61
65
|
gap: 8px;
|
|
62
66
|
padding: 0 10px;
|
|
63
67
|
}
|
|
@@ -750,6 +754,55 @@
|
|
|
750
754
|
font-variant-numeric: tabular-nums;
|
|
751
755
|
}
|
|
752
756
|
|
|
757
|
+
.updateNotes {
|
|
758
|
+
margin-top: 14px;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.updateNotesTitle {
|
|
762
|
+
margin: 0 0 8px;
|
|
763
|
+
font-size: 14px;
|
|
764
|
+
font-weight: 600;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.updateNoteSection {
|
|
768
|
+
display: flex;
|
|
769
|
+
flex-direction: column;
|
|
770
|
+
gap: 4px;
|
|
771
|
+
margin-top: 10px;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.updateNoteHeading {
|
|
775
|
+
margin: 0;
|
|
776
|
+
font-size: 13px;
|
|
777
|
+
font-weight: 600;
|
|
778
|
+
color: var(--dsw-alias-label-secondary);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.updateNoteList {
|
|
782
|
+
margin: 0;
|
|
783
|
+
padding-left: 18px;
|
|
784
|
+
display: flex;
|
|
785
|
+
flex-direction: column;
|
|
786
|
+
gap: 4px;
|
|
787
|
+
font-size: 13px;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.updateNoteItem {
|
|
791
|
+
color: var(--dsw-alias-label-primary);
|
|
792
|
+
overflow-wrap: anywhere;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.updateVersions {
|
|
796
|
+
margin-top: 12px;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.updateVersionsSummary {
|
|
800
|
+
cursor: pointer;
|
|
801
|
+
color: var(--dsw-alias-label-secondary);
|
|
802
|
+
font-size: 12px;
|
|
803
|
+
user-select: none;
|
|
804
|
+
}
|
|
805
|
+
|
|
753
806
|
.updateActions {
|
|
754
807
|
display: flex;
|
|
755
808
|
justify-content: flex-end;
|
|
@@ -320,7 +320,7 @@ export class CardForm<T> {
|
|
|
320
320
|
/**
|
|
321
321
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
322
322
|
*
|
|
323
|
-
* When the scope carries the optional batch surface (the dsh-web
|
|
323
|
+
* When the scope carries the optional batch surface (the dsh-web
|
|
324
324
|
* bridge scope), every planned write rides one mutation so cross-field
|
|
325
325
|
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
326
326
|
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/telemetry.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Anonymous install telemetry shared by the DSH Web UI family plugins.
|
|
4
|
+
*
|
|
5
|
+
* Once per UTC day per browser, each wired plugin sends one heartbeat to
|
|
6
|
+
* the dsh-market edge API listing its package name (and version when known).
|
|
7
|
+
* The payload carries no conversation data and no identifiers beyond a random
|
|
8
|
+
* UUID generated in localStorage; the server hashes it with a deployment salt
|
|
9
|
+
* before storage and never persists IP addresses. Sends are fire-and-forget:
|
|
10
|
+
* failures stay silent and simply retry on a later mount or day. The system
|
|
11
|
+
* is documented in docs/telemetry.md.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type TelemetryChannel = 'market' | 'npm' | 'unknown'
|
|
15
|
+
|
|
16
|
+
export interface TelemetryItem {
|
|
17
|
+
/** npm package name or asset id, e.g. "@linxin666/dsh-pet" or "skin:harbor". */
|
|
18
|
+
name: string
|
|
19
|
+
/** Installed version when known; omitted otherwise. */
|
|
20
|
+
version?: string
|
|
21
|
+
/** Install channel when determinable (market = Workshop install). */
|
|
22
|
+
channel?: TelemetryChannel
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const VISITOR_KEY = 'dsh-web-ui-telemetry-visitor'
|
|
26
|
+
const DAY_KEY_PREFIX = 'dsh-web-ui-telemetry-day:'
|
|
27
|
+
const ENDPOINT = 'https://dsh-market.com/api/telemetry/event'
|
|
28
|
+
|
|
29
|
+
// Baked into each client bundle by shared/tsdown.client.ts (the package's own
|
|
30
|
+
// package.json version at build time). Undefined in dev/test builds.
|
|
31
|
+
declare const __DSH_PKG_VERSION__: string | undefined
|
|
32
|
+
|
|
33
|
+
/** The building package's version, when the bundle carries it. */
|
|
34
|
+
function bakedVersion(): string | undefined {
|
|
35
|
+
try {
|
|
36
|
+
return typeof __DSH_PKG_VERSION__ === 'string' && __DSH_PKG_VERSION__ !== ''
|
|
37
|
+
? __DSH_PKG_VERSION__
|
|
38
|
+
: undefined
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Read or lazily create the anonymous visitor id; null when storage is unavailable. */
|
|
45
|
+
function visitorId(): string | null {
|
|
46
|
+
try {
|
|
47
|
+
const existing = localStorage.getItem(VISITOR_KEY)
|
|
48
|
+
if (existing && /^[A-Za-z0-9_-]{16,64}$/.test(existing)) return existing
|
|
49
|
+
const fresh = crypto.randomUUID().replaceAll('-', '')
|
|
50
|
+
localStorage.setItem(VISITOR_KEY, fresh)
|
|
51
|
+
return fresh
|
|
52
|
+
} catch {
|
|
53
|
+
// Storage unavailable (privacy mode, sandboxed frame): report nothing.
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Drop stale per-day dedup keys so localStorage does not grow forever. */
|
|
59
|
+
function pruneDayKeys(today: string): void {
|
|
60
|
+
try {
|
|
61
|
+
for (let index = localStorage.length - 1; index >= 0; index -= 1) {
|
|
62
|
+
const key = localStorage.key(index)
|
|
63
|
+
if (key !== null && key.startsWith(DAY_KEY_PREFIX) && key !== DAY_KEY_PREFIX + today) {
|
|
64
|
+
localStorage.removeItem(key)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch { /* best effort */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fire the daily heartbeat for the given items at most once per UTC day per
|
|
72
|
+
* browser. Never throws and never blocks the caller. Items without an explicit
|
|
73
|
+
* version inherit the bundle's baked build version.
|
|
74
|
+
*/
|
|
75
|
+
export function reportDailyHeartbeat(items: readonly TelemetryItem[]): void {
|
|
76
|
+
try {
|
|
77
|
+
if (items.length === 0) return
|
|
78
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
79
|
+
// Automation contexts (CDP-driven QA browsers, fresh isolated profiles)
|
|
80
|
+
// mint a new visitor id per run and would inflate instance counts.
|
|
81
|
+
if (navigator.webdriver) return
|
|
82
|
+
if (localStorage.getItem(DAY_KEY_PREFIX + today) !== null) return
|
|
83
|
+
const visitor = visitorId()
|
|
84
|
+
if (visitor === null) return
|
|
85
|
+
pruneDayKeys(today)
|
|
86
|
+
const payloadItems = items.map((item) => {
|
|
87
|
+
const out: Record<string, string> = { name: item.name }
|
|
88
|
+
const version = item.version ?? bakedVersion()
|
|
89
|
+
if (version !== undefined) out.version = version
|
|
90
|
+
if (item.channel !== undefined) out.channel = item.channel
|
|
91
|
+
return out
|
|
92
|
+
})
|
|
93
|
+
const body = JSON.stringify({ kind: 'heartbeat', visitor, items: payloadItems })
|
|
94
|
+
void fetch(ENDPOINT, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'content-type': 'application/json' },
|
|
97
|
+
body,
|
|
98
|
+
keepalive: true,
|
|
99
|
+
}).then((response) => {
|
|
100
|
+
// Mark the day only after an accepted send, so offline browsers retry
|
|
101
|
+
// on a later mount instead of going dark until tomorrow.
|
|
102
|
+
if (response.ok) localStorage.setItem(DAY_KEY_PREFIX + today, '1')
|
|
103
|
+
}).catch(() => { /* unreachable endpoint: stay silent */ })
|
|
104
|
+
} catch { /* never break the host UI over telemetry */ }
|
|
105
|
+
}
|
package/src/http.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* edit this shared source and re-run the sync instead of editing a copy.
|
|
12
12
|
* Consumer code is migrated onto it in follow-up waves; no call site changes
|
|
13
13
|
* belong in the same change as its introduction.
|
|
14
|
-
* @module dsh-web-
|
|
14
|
+
* @module dsh-web-shared/host/http
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http'
|