@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/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;
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
.header {
|
|
25
25
|
appearance: none;
|
|
26
26
|
width: 100%;
|
|
27
|
+
/* The suite does not force a global border-box, so width:100% + padding
|
|
28
|
+
would stretch the row 32px past the card and push the trailing pending
|
|
29
|
+
badge (last flex item) outside the card border. */
|
|
30
|
+
box-sizing: border-box;
|
|
27
31
|
font: inherit;
|
|
28
32
|
color: inherit;
|
|
29
33
|
text-align: left;
|
|
@@ -44,6 +48,7 @@
|
|
|
44
48
|
|
|
45
49
|
.headerStatic {
|
|
46
50
|
width: 100%;
|
|
51
|
+
box-sizing: border-box;
|
|
47
52
|
border-radius: 12px;
|
|
48
53
|
align-items: center;
|
|
49
54
|
gap: 12px;
|
|
@@ -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/dsh-home.ts
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { homedir } from 'node:os'
|
|
9
9
|
import { isAbsolute, join } from 'node:path'
|
|
10
|
+
import { isAbsolute as posixIsAbsolute, join as posixJoin } from 'node:path/posix'
|
|
10
11
|
|
|
11
12
|
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
12
13
|
export function expandHome(path: string, home: string = homedir()): string {
|
|
14
|
+
const isPosix = home.startsWith('/')
|
|
15
|
+
const j = isPosix ? posixJoin : join
|
|
13
16
|
if (path === '~') return home
|
|
14
|
-
if (path.startsWith('~/') || path.startsWith('~\\')) return
|
|
17
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return j(home, path.slice(2))
|
|
15
18
|
return path
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -22,12 +25,15 @@ export function expandHome(path: string, home: string = homedir()): string {
|
|
|
22
25
|
* @returns the absolute DSH home path.
|
|
23
26
|
*/
|
|
24
27
|
export function resolveDshHome(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string {
|
|
28
|
+
const isPosix = home.startsWith('/')
|
|
29
|
+
const j = isPosix ? posixJoin : join
|
|
30
|
+
const isAbs = isPosix ? posixIsAbsolute : isAbsolute
|
|
25
31
|
const raw = env.DSH_HOME
|
|
26
32
|
if (raw !== undefined && raw.trim() !== '') {
|
|
27
33
|
const expanded = expandHome(raw.trim(), home)
|
|
28
|
-
return
|
|
34
|
+
return isAbs(expanded) ? expanded : j(process.cwd(), expanded)
|
|
29
35
|
}
|
|
30
|
-
return
|
|
36
|
+
return j(home, '.dsh')
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/** Resolve the DSH home directory from the live environment. */
|
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'
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
16
16
|
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
17
17
|
import z from 'schemastery'
|
|
18
18
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-agent'
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-commands'
|
|
19
21
|
import { DEFAULT_IDLE_EXPIRE_MS, PairingService, type PairingConfig } from './pairing.ts'
|
|
20
22
|
import { dshHome } from './dsh-home.ts'
|
|
21
23
|
import { isPairedDeviceRequest, makeGateListener } from './gate.ts'
|
|
@@ -31,10 +33,13 @@ import { lanIPv4Addresses } from './lan.ts'
|
|
|
31
33
|
import { TunnelManager, type TunnelInfo } from './tunnel.ts'
|
|
32
34
|
import {
|
|
33
35
|
checkUpdates,
|
|
36
|
+
fetchGitHubReleaseNotes,
|
|
34
37
|
fetchLatestVersion,
|
|
38
|
+
RELEASE_NOTES_CACHE_TTL_MS,
|
|
35
39
|
resolveAnchorManifest,
|
|
36
40
|
resolveUpdateTarget,
|
|
37
41
|
runUpdateVerified,
|
|
42
|
+
type UpdateReleaseNotes,
|
|
38
43
|
type UpdateRunResult,
|
|
39
44
|
} from './update.ts'
|
|
40
45
|
import { makeUpdateRoutes } from './update-routes.ts'
|
|
@@ -63,7 +68,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
63
68
|
export const name = 'remote-web-ui'
|
|
64
69
|
|
|
65
70
|
/** Services required before the pairing surfaces can mount. */
|
|
66
|
-
export const inject = ['webServer', 'apiProxy']
|
|
71
|
+
export const inject = ['webServer', 'apiProxy', 'commands', 'agents']
|
|
67
72
|
|
|
68
73
|
/**
|
|
69
74
|
* Settings namespace of the remote-control capability — the section the web
|
|
@@ -294,7 +299,7 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
294
299
|
console.warn('remote-web-ui: apiProxy service unavailable — the mobile data channel is disabled')
|
|
295
300
|
}
|
|
296
301
|
// ── remote update ────────────────────────────────────────────────────────
|
|
297
|
-
// The dsh-web
|
|
302
|
+
// The dsh-web self-update surface: probe the npm registry for family
|
|
298
303
|
// releases and run `pnpm update --latest` in the owning profile. Resolutions
|
|
299
304
|
// anchor on the host process's own module graph, so the update always
|
|
300
305
|
// targets the profile the running web GUI was booted from. The anchor path
|
|
@@ -309,6 +314,15 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
309
314
|
return undefined
|
|
310
315
|
}
|
|
311
316
|
})
|
|
317
|
+
|
|
318
|
+
const releaseNotesCache = new Map<string, { at: number; notes?: UpdateReleaseNotes }>()
|
|
319
|
+
const fetchReleaseNotesCached = async (version: string): Promise<UpdateReleaseNotes | undefined> => {
|
|
320
|
+
const cached = releaseNotesCache.get(version)
|
|
321
|
+
if (cached !== undefined && Date.now() - cached.at < RELEASE_NOTES_CACHE_TTL_MS) return cached.notes
|
|
322
|
+
const notes = await fetchGitHubReleaseNotes(version, fetch)
|
|
323
|
+
releaseNotesCache.set(version, { at: Date.now(), notes })
|
|
324
|
+
return notes
|
|
325
|
+
}
|
|
312
326
|
const updateRoutes = makeUpdateRoutes({
|
|
313
327
|
// Control endpoints are host-surface only: a LAN/phone origin must never
|
|
314
328
|
// trigger a real install on this machine.
|
|
@@ -323,6 +337,7 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
323
337
|
}
|
|
324
338
|
},
|
|
325
339
|
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
340
|
+
fetchReleaseNotes: fetchReleaseNotesCached,
|
|
326
341
|
}),
|
|
327
342
|
run: async (): Promise<UpdateRunResult> => {
|
|
328
343
|
const target = resolveUpdateTarget({ anchorManifestPath: resolveAnchorPath() })
|
|
@@ -332,7 +347,7 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
332
347
|
ok: false,
|
|
333
348
|
exitCode: null,
|
|
334
349
|
output: '',
|
|
335
|
-
error: code === 'not-found' ? 'dsh-web
|
|
350
|
+
error: code === 'not-found' ? 'dsh-web aggregate not installed' : 'local link install — update unavailable',
|
|
336
351
|
errorCode: code,
|
|
337
352
|
}
|
|
338
353
|
}
|
|
@@ -352,6 +367,7 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
352
367
|
}
|
|
353
368
|
},
|
|
354
369
|
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
370
|
+
fetchReleaseNotes: fetchReleaseNotesCached,
|
|
355
371
|
},
|
|
356
372
|
})
|
|
357
373
|
},
|
|
@@ -360,7 +376,23 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
360
376
|
...makeRoutes({ service, lanAddresses, requirePairingForLan: () => resolve().requirePairingForLan }),
|
|
361
377
|
...makeMobileRoutes(),
|
|
362
378
|
...(apiProxy !== undefined
|
|
363
|
-
? makeMobileApiRoutes({
|
|
379
|
+
? makeMobileApiRoutes({
|
|
380
|
+
service,
|
|
381
|
+
apiProxy,
|
|
382
|
+
pendingTracker: new PendingTracker(),
|
|
383
|
+
mobileEnterToSend: () => resolve().mobileEnterToSend,
|
|
384
|
+
commandDispatcher: ctx.commands !== undefined && ctx.agents !== undefined
|
|
385
|
+
? {
|
|
386
|
+
async execute(sessionId, line, signal) {
|
|
387
|
+
const agent = ctx.agents.get(sessionId as never)
|
|
388
|
+
if (agent === undefined) return { error: 'session-not-found' as const }
|
|
389
|
+
const execution = await ctx.commands.execute(agent, line, [], signal)
|
|
390
|
+
if (execution === undefined) return { error: 'unknown-command' as const }
|
|
391
|
+
return { ok: true as const, result: execution.result }
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
: undefined,
|
|
395
|
+
})
|
|
364
396
|
: []),
|
|
365
397
|
...(apiProxy !== undefined ? makePairedModelCatalogRoutes({ service, apiProxy, lanAddresses }) : []),
|
|
366
398
|
// The remote desktop channel: policy-gated `/remote` prefix that
|
package/src/loopback.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
|
|
9
9
|
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
|
|
10
10
|
* markers (sec-fetch-site and Origin) for the request-level fence.
|
|
11
|
-
* @module dsh-web-
|
|
11
|
+
* @module dsh-web-shared/host/loopback
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { IncomingMessage } from 'node:http'
|
package/src/mobile/api.ts
CHANGED
|
@@ -105,13 +105,20 @@ export async function prompt(sessionId: string, text: string): Promise<void> {
|
|
|
105
105
|
|
|
106
106
|
/** Send one slash command line (e.g. `/permission workspace-write`). */
|
|
107
107
|
export async function sendCommand(sessionId: string, line: string): Promise<unknown> {
|
|
108
|
-
return await callUnary<unknown>('
|
|
108
|
+
return await callUnary<unknown>('mobile.command', {
|
|
109
109
|
sessionId,
|
|
110
|
-
|
|
111
|
-
content: [{ type: 'text', text: line }],
|
|
110
|
+
line,
|
|
112
111
|
})
|
|
113
112
|
}
|
|
114
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Stop the session's active turn (the mobile stop button). Pending queued
|
|
116
|
+
* work is preserved and resumes in FIFO order once cancellation settles.
|
|
117
|
+
*/
|
|
118
|
+
export async function cancelSession(sessionId: string): Promise<{ accepted: true }> {
|
|
119
|
+
return await callUnary<{ accepted: true }>('session.cancel', { sessionId })
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
/** Fresh advisory model directory for one session (current + groups + failures). */
|
|
116
123
|
export async function models(sessionId: string): Promise<SessionModels> {
|
|
117
124
|
return await callUnary<SessionModels>('session.models', { sessionId })
|
|
@@ -1170,6 +1170,16 @@ body {
|
|
|
1170
1170
|
opacity: 0.5;
|
|
1171
1171
|
}
|
|
1172
1172
|
|
|
1173
|
+
/* Running state: the primary button carries only the square stop icon, so
|
|
1174
|
+
it centers the glyph instead of sizing to text. */
|
|
1175
|
+
.chat-send-stop {
|
|
1176
|
+
display: inline-flex;
|
|
1177
|
+
align-items: center;
|
|
1178
|
+
justify-content: center;
|
|
1179
|
+
min-width: 44px;
|
|
1180
|
+
padding: 0 12px;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1173
1183
|
.chat-load-older {
|
|
1174
1184
|
align-self: center;
|
|
1175
1185
|
margin: 4px 0;
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* to the plain renderMarkdown output) the moment the turn closes.
|
|
8
8
|
*/
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
10
|
-
import { act, cleanup, render } from '@testing-library/react'
|
|
11
|
-
import { ChatView, STREAM_RENDER_INTERVAL_MS } from './ChatView.tsx'
|
|
10
|
+
import { act, cleanup, render, screen } from '@testing-library/react'
|
|
11
|
+
import { ChatView, STREAM_RENDER_INTERVAL_MS, LONG_TEXT_LIMIT } from './ChatView.tsx'
|
|
12
12
|
import { type SessionView } from './App.tsx'
|
|
13
13
|
import { renderMarkdown } from '../markdown.ts'
|
|
14
14
|
import type { SessionModels } from '@deepseek-ai/dsh-host-apiproxy/api/sessions'
|
|
@@ -22,6 +22,8 @@ vi.mock('../api.ts', () => ({
|
|
|
22
22
|
models: vi.fn(),
|
|
23
23
|
selectModel: vi.fn(),
|
|
24
24
|
sendCommand: vi.fn(),
|
|
25
|
+
cancelSession: vi.fn(),
|
|
26
|
+
fetchPending: vi.fn(),
|
|
25
27
|
}))
|
|
26
28
|
vi.mock('./App.tsx', async importOriginal => {
|
|
27
29
|
const actual = await importOriginal<typeof import('./App.tsx')>()
|
|
@@ -228,4 +230,34 @@ describe('ChatView streaming markdown throttle', () => {
|
|
|
228
230
|
expect(renderMarkdownMock.mock.calls.length - beforeFinal).toBe(0)
|
|
229
231
|
expect(normalize(body().innerHTML)).toBe(normalize(renderMarkdown(full)))
|
|
230
232
|
})
|
|
233
|
+
|
|
234
|
+
it('does not collapse long text while streaming (pending) and collapses once turn finishes', async () => {
|
|
235
|
+
const mux = new FakeMux()
|
|
236
|
+
let container: HTMLElement
|
|
237
|
+
await act(async () => {
|
|
238
|
+
const rendered = render(<ChatView session={session} mux={mux as never} onBack={() => {}} />)
|
|
239
|
+
container = rendered.container
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const hugeChunk = 'C'.repeat(LONG_TEXT_LIMIT + 200)
|
|
243
|
+
|
|
244
|
+
// Stream a chunk exceeding LONG_TEXT_LIMIT
|
|
245
|
+
await act(async () => {
|
|
246
|
+
chunk(mux, hugeChunk, 6)
|
|
247
|
+
vi.advanceTimersByTime(STREAM_RENDER_INTERVAL_MS + 5)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// During streaming (pending), message is not collapsed
|
|
251
|
+
expect(container!.querySelector('.chat-md-collapsed')).toBeNull()
|
|
252
|
+
expect(screen.queryByRole('button', { name: /展开全文/ })).toBeNull()
|
|
253
|
+
|
|
254
|
+
// Turn closes
|
|
255
|
+
await act(async () => {
|
|
256
|
+
finalMessage(mux, hugeChunk, 7)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// Once turn ends (not pending), message is collapsed and has expand button
|
|
260
|
+
expect(container!.querySelector('.chat-md-collapsed')).not.toBeNull()
|
|
261
|
+
expect(screen.getByRole('button', { name: new RegExp(`展开全文(${LONG_TEXT_LIMIT + 200} 字)`) })).toBeTruthy()
|
|
262
|
+
})
|
|
231
263
|
})
|