dsh-clean-desktop-shell 0.1.0 → 0.1.2
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/LICENSE +21 -21
- package/README.en.md +168 -91
- package/README.md +161 -90
- package/cordis.patch.yml +4 -4
- package/electron/config.js +46 -48
- package/electron/error.html +113 -0
- package/electron/main.js +20 -15
- package/electron/preload.js +14 -1
- package/electron/progress-preload.js +11 -0
- package/electron/progress.html +79 -0
- package/electron/progress.js +56 -0
- package/electron/service.js +181 -47
- package/electron/tray.js +103 -48
- package/electron/update.js +201 -74
- package/electron/window.js +179 -5
- package/lib/client.js +10 -10
- package/lib/index.js +13 -13
- package/package.json +11 -3
- package/scripts/build.mjs +12 -12
- package/scripts/gen-icons.mjs +70 -70
- package/src/client/client.js +10 -10
- package/src/host/index.js +13 -13
- package/version.txt +1 -1
package/electron/update.js
CHANGED
|
@@ -1,74 +1,201 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Update handling — Windows auto-update, macOS manual download.
|
|
3
|
+
*
|
|
4
|
+
* Windows (packaged app): uses electron-updater to download the new
|
|
5
|
+
* installer from GitHub Releases in the background and install it on
|
|
6
|
+
* restart. Progress is shown in the small progress window.
|
|
7
|
+
*
|
|
8
|
+
* macOS / dev mode: falls back to a manual check that opens the GitHub
|
|
9
|
+
* Releases page (macOS auto-update needs a Developer ID signature which
|
|
10
|
+
* this project does not have yet).
|
|
11
|
+
*/
|
|
12
|
+
import { app, shell, dialog } from 'electron'
|
|
13
|
+
import { showProgress, setProgress, closeProgress } from './progress.js'
|
|
14
|
+
|
|
15
|
+
const REPO_URL = 'https://github.com/Icather/dsh-clean-desktop-shell'
|
|
16
|
+
const RELEASES_API = 'https://api.github.com/repos/Icather/dsh-clean-desktop-shell/releases/latest'
|
|
17
|
+
|
|
18
|
+
let autoUpdater = null
|
|
19
|
+
let updaterPromise = null
|
|
20
|
+
|
|
21
|
+
/** Lazy-load electron-updater (CJS → ESM interop via dynamic import). */
|
|
22
|
+
function getAutoUpdater() {
|
|
23
|
+
if (!updaterPromise) {
|
|
24
|
+
updaterPromise = import('electron-updater').then((m) => m.autoUpdater)
|
|
25
|
+
}
|
|
26
|
+
return updaterPromise
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Windows packaged apps can auto-update; everything else uses manual. */
|
|
30
|
+
export function isAutoUpdateSupported() {
|
|
31
|
+
return process.platform === 'win32' && app.isPackaged
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Wire the auto-updater events once (no-op on mac / dev mode). */
|
|
35
|
+
export function setupAutoUpdater() {
|
|
36
|
+
if (!isAutoUpdateSupported()) return
|
|
37
|
+
getAutoUpdater()
|
|
38
|
+
.then((au) => {
|
|
39
|
+
if (autoUpdater) return
|
|
40
|
+
autoUpdater = au
|
|
41
|
+
autoUpdater.autoDownload = true
|
|
42
|
+
autoUpdater.autoInstallOnAppQuit = true
|
|
43
|
+
|
|
44
|
+
autoUpdater.on('update-available', () => {
|
|
45
|
+
showProgress({ title: '检查更新', message: '发现新版本,正在后台下载…' })
|
|
46
|
+
})
|
|
47
|
+
autoUpdater.on('download-progress', (p) => {
|
|
48
|
+
const pct = Math.round(p.percent)
|
|
49
|
+
setProgress({
|
|
50
|
+
title: '检查更新',
|
|
51
|
+
message: `正在下载更新:${pct}%`,
|
|
52
|
+
state: pct >= 100 ? 'ok' : 'busy',
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
autoUpdater.on('update-downloaded', () => {
|
|
56
|
+
setProgress({ title: '检查更新', message: '更新已下载', state: 'ok' })
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
closeProgress()
|
|
59
|
+
const choice = dialog.showMessageBoxSync({
|
|
60
|
+
type: 'info',
|
|
61
|
+
title: '更新已就绪',
|
|
62
|
+
message: '新版本已下载完成,重启应用即可安装。',
|
|
63
|
+
detail: '是否立即重启安装?',
|
|
64
|
+
buttons: ['立即重启', '稍后'],
|
|
65
|
+
defaultId: 0,
|
|
66
|
+
cancelId: 1,
|
|
67
|
+
})
|
|
68
|
+
if (choice === 0) autoUpdater.quitAndInstall()
|
|
69
|
+
}, 800)
|
|
70
|
+
})
|
|
71
|
+
autoUpdater.on('update-not-available', () => {
|
|
72
|
+
closeProgress()
|
|
73
|
+
dialog.showMessageBoxSync({
|
|
74
|
+
type: 'info',
|
|
75
|
+
title: '已是最新版本',
|
|
76
|
+
message: '当前已是最新版本。',
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
autoUpdater.on('error', (err) => {
|
|
80
|
+
closeProgress()
|
|
81
|
+
dialog.showMessageBoxSync({
|
|
82
|
+
type: 'warning',
|
|
83
|
+
title: '检查更新失败',
|
|
84
|
+
message: `自动更新失败:${err?.message || '未知错误'}`,
|
|
85
|
+
detail: '可前往 GitHub Releases 手动下载。',
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
.catch(() => {
|
|
90
|
+
// electron-updater failed to load — auto-update silently disabled.
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Tray "check for update": auto flow on Windows, manual fallback elsewhere. */
|
|
95
|
+
export async function checkForUpdatesAuto() {
|
|
96
|
+
if (isAutoUpdateSupported()) {
|
|
97
|
+
try {
|
|
98
|
+
await getAutoUpdater()
|
|
99
|
+
await autoUpdater?.checkForUpdates()
|
|
100
|
+
} catch (err) {
|
|
101
|
+
closeProgress()
|
|
102
|
+
dialog.showMessageBoxSync({
|
|
103
|
+
type: 'warning',
|
|
104
|
+
title: '检查更新失败',
|
|
105
|
+
message: `自动更新失败:${err?.message || '未知错误'}`,
|
|
106
|
+
detail: '可前往 GitHub Releases 手动下载。',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Manual path (macOS / dev mode): compare versions, offer GitHub page.
|
|
113
|
+
const r = await checkForUpdate()
|
|
114
|
+
if (r.hasUpdate) {
|
|
115
|
+
const choice = dialog.showMessageBoxSync({
|
|
116
|
+
type: 'info',
|
|
117
|
+
title: '发现新版本',
|
|
118
|
+
message: `当前版本 ${r.current},最新版本 ${r.latest}。`,
|
|
119
|
+
detail: 'macOS 自动更新需要代码签名,当前请前往 GitHub Releases 手动下载。',
|
|
120
|
+
buttons: ['前往下载', '取消'],
|
|
121
|
+
defaultId: 0,
|
|
122
|
+
cancelId: 1,
|
|
123
|
+
})
|
|
124
|
+
if (choice === 0) openUrl(r.url)
|
|
125
|
+
} else if (r.latest) {
|
|
126
|
+
dialog.showMessageBoxSync({
|
|
127
|
+
type: 'info',
|
|
128
|
+
title: '已是最新版本',
|
|
129
|
+
message: `当前版本 ${r.current} 已是最新(${r.latest})。`,
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
dialog.showMessageBoxSync({
|
|
133
|
+
type: 'warning',
|
|
134
|
+
title: '检查更新失败',
|
|
135
|
+
message: '无法连接 GitHub 检查更新,请检查网络后重试。',
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Parse "v1.2.3" / "1.2.3" → [1,2,3]; null when malformed. */
|
|
141
|
+
function parseVersion(v) {
|
|
142
|
+
if (!v) return null
|
|
143
|
+
const m = String(v).replace(/^v/i, '').trim().match(/^(\d+)\.(\d+)\.(\d+)/)
|
|
144
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** True when a is strictly newer than b. */
|
|
148
|
+
function isNewer(a, b) {
|
|
149
|
+
if (!a || !b) return false
|
|
150
|
+
for (let i = 0; i < 3; i++) {
|
|
151
|
+
if (a[i] !== b[i]) return a[i] > b[i]
|
|
152
|
+
}
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Manual version check against the latest GitHub release (macOS path).
|
|
158
|
+
* @returns {{ hasUpdate: boolean, latest?: string, current: string, url: string }}
|
|
159
|
+
*/
|
|
160
|
+
export async function checkForUpdate() {
|
|
161
|
+
const current = app.getVersion()
|
|
162
|
+
let latest = null
|
|
163
|
+
let tag = null
|
|
164
|
+
let url = REPO_URL
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const controller = new AbortController()
|
|
168
|
+
const timer = setTimeout(() => controller.abort(), 8000)
|
|
169
|
+
const res = await fetch(RELEASES_API, {
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
headers: { Accept: 'application/vnd.github+json' },
|
|
172
|
+
})
|
|
173
|
+
clearTimeout(timer)
|
|
174
|
+
if (res.ok) {
|
|
175
|
+
const data = await res.json()
|
|
176
|
+
tag = data.tag_name || null
|
|
177
|
+
latest = parseVersion(tag)
|
|
178
|
+
if (data.html_url) url = data.html_url
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// Network error — no update known.
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const currentV = parseVersion(current)
|
|
185
|
+
return {
|
|
186
|
+
hasUpdate: isNewer(latest, currentV),
|
|
187
|
+
latest: tag,
|
|
188
|
+
current,
|
|
189
|
+
url,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Open the repository homepage in the default browser. */
|
|
194
|
+
export function openRepo() {
|
|
195
|
+
shell.openExternal(REPO_URL)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Open an arbitrary URL in the default browser. */
|
|
199
|
+
export function openUrl(url) {
|
|
200
|
+
shell.openExternal(url)
|
|
201
|
+
}
|
package/electron/window.js
CHANGED
|
@@ -4,16 +4,142 @@
|
|
|
4
4
|
* Pure shell philosophy: the window is a normal native frame with
|
|
5
5
|
* window-controls overlay (Win) / hiddenInset (mac), and nothing else.
|
|
6
6
|
* No Mica, no vibrancy — keep it clean.
|
|
7
|
+
*
|
|
8
|
+
* Window reliability (Edge-style instant refresh):
|
|
9
|
+
* - the window shows immediately on launch (never waits for the backend);
|
|
10
|
+
* - while the backend is unreachable the page load fails and we swap in a
|
|
11
|
+
* local "backend offline" screen;
|
|
12
|
+
* - a background poll keeps probing the target; as soon as the backend
|
|
13
|
+
* answers, the real page is loaded automatically;
|
|
14
|
+
* - the moment the backend goes down (tray stop, external kill, crash) the
|
|
15
|
+
* window flips back to the offline screen instead of showing a stale page
|
|
16
|
+
* that suggests the app is still alive.
|
|
7
17
|
*/
|
|
8
|
-
import { BrowserWindow } from 'electron'
|
|
18
|
+
import { BrowserWindow, ipcMain } from 'electron'
|
|
9
19
|
import { fileURLToPath } from 'node:url'
|
|
10
|
-
import {
|
|
20
|
+
import { probe, onStatusChange, detect } from './service.js'
|
|
21
|
+
import { startBackendWithProgress, chooseBackendFolder } from './tray.js'
|
|
11
22
|
|
|
12
23
|
export const WINDOWS_TITLEBAR_HEIGHT = 32
|
|
13
24
|
|
|
14
25
|
const PRELOAD_PATH = fileURLToPath(new URL('./preload.js', import.meta.url))
|
|
15
26
|
// Black-whale app icon (matches the DSH web favicon).
|
|
16
27
|
const ICON_PATH = fileURLToPath(new URL('../build/icon.png', import.meta.url))
|
|
28
|
+
// Local fallback page shown while the backend is down.
|
|
29
|
+
const ERROR_PAGE = fileURLToPath(new URL('./error.html', import.meta.url))
|
|
30
|
+
|
|
31
|
+
// How often we re-probe the backend while the window is in "offline" mode.
|
|
32
|
+
const RECONNECT_INTERVAL_MS = 2500
|
|
33
|
+
// How often we check the backend is still alive while the page is shown.
|
|
34
|
+
const WATCH_INTERVAL_MS = 4000
|
|
35
|
+
// ERR_ABORTED — navigation was cancelled, not a real failure. Ignore it.
|
|
36
|
+
const ERR_ABORTED = -3
|
|
37
|
+
|
|
38
|
+
// Per-window state, keyed by webContents id.
|
|
39
|
+
const reconnectTimers = new Map()
|
|
40
|
+
const watchTimers = new Map()
|
|
41
|
+
const windowTargets = new Map()
|
|
42
|
+
const statusUnsubs = new Map()
|
|
43
|
+
|
|
44
|
+
// Manual reload requests come from the tray button and from the offline
|
|
45
|
+
// screen's retry button (via preload -> ipcRenderer). Route them to the
|
|
46
|
+
// window that sent the message.
|
|
47
|
+
ipcMain.on('shell:reload', (event) => {
|
|
48
|
+
const win = BrowserWindow.fromWebContents(event.sender)
|
|
49
|
+
if (win && !win.isDestroyed()) {
|
|
50
|
+
reloadWindow(win, windowTargets.get(win.id))
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Offline-screen quick actions: start / detect backend, pick install
|
|
55
|
+
// folder. The resulting state changes propagate via onStatusChange
|
|
56
|
+
// (window flip + tray refresh), so no extra wiring is needed here.
|
|
57
|
+
ipcMain.on('shell:start-backend', () => startBackendWithProgress())
|
|
58
|
+
ipcMain.on('shell:detect-backend', () => detect())
|
|
59
|
+
ipcMain.on('shell:choose-backend-folder', () => chooseBackendFolder())
|
|
60
|
+
|
|
61
|
+
// ---------- offline mode ----------
|
|
62
|
+
|
|
63
|
+
function stopReconnect(win) {
|
|
64
|
+
const timer = reconnectTimers.get(win.id)
|
|
65
|
+
if (timer) {
|
|
66
|
+
clearInterval(timer)
|
|
67
|
+
reconnectTimers.delete(win.id)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function startReconnect(win, target) {
|
|
72
|
+
if (reconnectTimers.has(win.id)) return
|
|
73
|
+
const timer = setInterval(async () => {
|
|
74
|
+
if (win.isDestroyed()) {
|
|
75
|
+
stopReconnect(win)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
const up = await probe(target)
|
|
79
|
+
if (up) {
|
|
80
|
+
stopReconnect(win)
|
|
81
|
+
win.webContents.loadURL(target).catch(() => startReconnect(win, target))
|
|
82
|
+
}
|
|
83
|
+
}, RECONNECT_INTERVAL_MS)
|
|
84
|
+
reconnectTimers.set(win.id, timer)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------- online mode (backend liveness watch) ----------
|
|
88
|
+
|
|
89
|
+
function stopWatch(win) {
|
|
90
|
+
const timer = watchTimers.get(win.id)
|
|
91
|
+
if (timer) {
|
|
92
|
+
clearInterval(timer)
|
|
93
|
+
watchTimers.delete(win.id)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** While the real page is shown, watch that the backend stays alive. */
|
|
98
|
+
function startWatch(win, target) {
|
|
99
|
+
if (watchTimers.has(win.id)) return
|
|
100
|
+
const timer = setInterval(async () => {
|
|
101
|
+
if (win.isDestroyed()) {
|
|
102
|
+
stopWatch(win)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const up = await probe(target, 1500)
|
|
106
|
+
if (!up) {
|
|
107
|
+
// Backend vanished — flip to the offline screen immediately so the
|
|
108
|
+
// stale page cannot fool the user into thinking the app is alive.
|
|
109
|
+
showOffline(win)
|
|
110
|
+
}
|
|
111
|
+
}, WATCH_INTERVAL_MS)
|
|
112
|
+
watchTimers.set(win.id, timer)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- state flips ----------
|
|
116
|
+
|
|
117
|
+
/** Switch to the offline screen and start re-probing. */
|
|
118
|
+
function showOffline(win) {
|
|
119
|
+
if (win.isDestroyed()) return
|
|
120
|
+
const target = windowTargets.get(win.id)
|
|
121
|
+
stopWatch(win)
|
|
122
|
+
win.loadFile(ERROR_PAGE).catch(() => {})
|
|
123
|
+
if (target) startReconnect(win, target)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Load the real backend page and start watching it. */
|
|
127
|
+
function showOnline(win) {
|
|
128
|
+
if (win.isDestroyed()) return
|
|
129
|
+
const target = windowTargets.get(win.id)
|
|
130
|
+
if (!target) return
|
|
131
|
+
stopReconnect(win)
|
|
132
|
+
win.webContents.loadURL(target).catch(() => startReconnect(win, target))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Load the real target URL in a window (used by tray + offline retry). */
|
|
136
|
+
export function reloadWindow(win, target) {
|
|
137
|
+
if (!win || win.isDestroyed()) return
|
|
138
|
+
stopReconnect(win)
|
|
139
|
+
win.webContents.loadURL(target).catch(() => startReconnect(win, target))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------- window creation ----------
|
|
17
143
|
|
|
18
144
|
export function createMainWindow({ target }) {
|
|
19
145
|
const platform = process.platform
|
|
@@ -25,7 +151,9 @@ export function createMainWindow({ target }) {
|
|
|
25
151
|
height: 800,
|
|
26
152
|
minWidth: 760,
|
|
27
153
|
minHeight: 520,
|
|
28
|
-
|
|
154
|
+
// Show immediately — the backend may be starting, the window must not
|
|
155
|
+
// wait for `ready-to-show` (which lags when the page fails to load).
|
|
156
|
+
show: true,
|
|
29
157
|
title: 'DeepSeek Harness',
|
|
30
158
|
backgroundColor: '#10131A',
|
|
31
159
|
icon: isWin ? ICON_PATH : undefined,
|
|
@@ -61,9 +189,55 @@ export function createMainWindow({ target }) {
|
|
|
61
189
|
// Linux / other: keep the native frame.
|
|
62
190
|
|
|
63
191
|
const win = new BrowserWindow(options)
|
|
64
|
-
win.
|
|
192
|
+
windowTargets.set(win.id, target)
|
|
193
|
+
win.loadURL(target).catch(() => startReconnect(win, target))
|
|
65
194
|
|
|
66
|
-
|
|
195
|
+
// Instant flip when the backend state machine changes (tray stop/start).
|
|
196
|
+
const unsub = onStatusChange((st) => {
|
|
197
|
+
if (win.isDestroyed()) return
|
|
198
|
+
const current = win.webContents.getURL()
|
|
199
|
+
const isOffline = current.includes('error.html')
|
|
200
|
+
if ((st.status === 'stopped' || st.status === 'error') && !isOffline) {
|
|
201
|
+
// Backend went down while a real page is showing — go dark at once.
|
|
202
|
+
showOffline(win)
|
|
203
|
+
} else if (st.status === 'running' && isOffline) {
|
|
204
|
+
// Backend came up while we are on the offline screen — load it.
|
|
205
|
+
showOnline(win)
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
statusUnsubs.set(win.id, unsub)
|
|
209
|
+
|
|
210
|
+
win.webContents.on('did-fail-load', (_e, code, _desc, url, isMainFrame) => {
|
|
211
|
+
if (!isMainFrame || code === ERR_ABORTED) return
|
|
212
|
+
// Offline screen already showing — just keep re-probing, do not
|
|
213
|
+
// reload the offline page again (avoids a reload loop if it fails).
|
|
214
|
+
if (win.webContents.getURL().includes('error.html')) {
|
|
215
|
+
startReconnect(win, target)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
// Backend unreachable: show the offline screen and start re-probing.
|
|
219
|
+
showOffline(win)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
win.webContents.on('did-finish-load', () => {
|
|
223
|
+
const current = win.webContents.getURL()
|
|
224
|
+
if (current.startsWith(target)) {
|
|
225
|
+
// Real backend page reached — stop re-probing and watch it.
|
|
226
|
+
stopReconnect(win)
|
|
227
|
+
startWatch(win, target)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
win.on('closed', () => {
|
|
232
|
+
stopReconnect(win)
|
|
233
|
+
stopWatch(win)
|
|
234
|
+
const unsub = statusUnsubs.get(win.id)
|
|
235
|
+
if (unsub) {
|
|
236
|
+
unsub()
|
|
237
|
+
statusUnsubs.delete(win.id)
|
|
238
|
+
}
|
|
239
|
+
windowTargets.delete(win.id)
|
|
240
|
+
})
|
|
67
241
|
|
|
68
242
|
return win
|
|
69
243
|
}
|
package/lib/client.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-clean-desktop-shell — client half (web browser bundle).
|
|
3
|
-
*
|
|
4
|
-
* Registers a settings row so the shell's target URL / material options
|
|
5
|
-
* are editable from Settings → General. This is a minimal placeholder:
|
|
6
|
-
* real UI wiring lands with the Electron shell work.
|
|
7
|
-
*/
|
|
8
|
-
export function apply() {
|
|
9
|
-
// placeholder: settings-row wiring arrives with the shell implementation
|
|
10
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-clean-desktop-shell — client half (web browser bundle).
|
|
3
|
+
*
|
|
4
|
+
* Registers a settings row so the shell's target URL / material options
|
|
5
|
+
* are editable from Settings → General. This is a minimal placeholder:
|
|
6
|
+
* real UI wiring lands with the Electron shell work.
|
|
7
|
+
*/
|
|
8
|
+
export function apply() {
|
|
9
|
+
// placeholder: settings-row wiring arrives with the shell implementation
|
|
10
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-clean-desktop-shell — host half (plugin loader entry).
|
|
3
|
-
*
|
|
4
|
-
* The desktop shell is an Electron client; the host half only registers
|
|
5
|
-
* the plugin so it mounts cleanly into a dsh profile and exposes its
|
|
6
|
-
* settings surface. Window material (Mica / vibrancy) lives in the
|
|
7
|
-
* Electron main process (see src/main/).
|
|
8
|
-
*/
|
|
9
|
-
export function apply(ctx) {
|
|
10
|
-
ctx.on('ready', () => {
|
|
11
|
-
ctx.logger.info('[clean-desktop-shell] mounted (host half)')
|
|
12
|
-
})
|
|
13
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-clean-desktop-shell — host half (plugin loader entry).
|
|
3
|
+
*
|
|
4
|
+
* The desktop shell is an Electron client; the host half only registers
|
|
5
|
+
* the plugin so it mounts cleanly into a dsh profile and exposes its
|
|
6
|
+
* settings surface. Window material (Mica / vibrancy) lives in the
|
|
7
|
+
* Electron main process (see src/main/).
|
|
8
|
+
*/
|
|
9
|
+
export function apply(ctx) {
|
|
10
|
+
ctx.on('ready', () => {
|
|
11
|
+
ctx.logger.info('[clean-desktop-shell] mounted (host half)')
|
|
12
|
+
})
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-clean-desktop-shell",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Clean desktop shell for DeepSeek Harness (DSH) as a DSH plugin — reuses your web profile, tray/single-instance/auto-launch, zero visual changes. DSH 插件形态的纯净桌面壳:复用现有 web profile,托盘管理后端,零视觉改造。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -91,14 +91,22 @@
|
|
|
91
91
|
"category": "public.app-category.developer-tools"
|
|
92
92
|
},
|
|
93
93
|
"nsis": {
|
|
94
|
-
"oneClick":
|
|
95
|
-
"
|
|
94
|
+
"oneClick": true,
|
|
95
|
+
"perMachine": false,
|
|
96
96
|
"createDesktopShortcut": true,
|
|
97
97
|
"createStartMenuShortcut": true
|
|
98
|
+
},
|
|
99
|
+
"publish": {
|
|
100
|
+
"provider": "github",
|
|
101
|
+
"owner": "Icather",
|
|
102
|
+
"repo": "dsh-clean-desktop-shell"
|
|
98
103
|
}
|
|
99
104
|
},
|
|
100
105
|
"license": "MIT",
|
|
101
106
|
"allowScripts": {
|
|
102
107
|
"electron@33.4.11": true
|
|
108
|
+
},
|
|
109
|
+
"dependencies": {
|
|
110
|
+
"electron-updater": "^6.8.9"
|
|
103
111
|
}
|
|
104
112
|
}
|
package/scripts/build.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Minimal zero-dependency build: copies src → lib.
|
|
3
|
-
// Real bundling (tsdown/vite) lands with the Electron shell work.
|
|
4
|
-
import { cpSync, mkdirSync } from 'node:fs'
|
|
5
|
-
import { dirname, join } from 'node:path'
|
|
6
|
-
import { fileURLToPath } from 'node:url'
|
|
7
|
-
|
|
8
|
-
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
9
|
-
mkdirSync(join(root, 'lib'), { recursive: true })
|
|
10
|
-
cpSync(join(root, 'src', 'host', 'index.js'), join(root, 'lib', 'index.js'))
|
|
11
|
-
cpSync(join(root, 'src', 'client', 'client.js'), join(root, 'lib', 'client.js'))
|
|
12
|
-
console.log('[dsh-clean-desktop-shell] built lib/ from src/')
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Minimal zero-dependency build: copies src → lib.
|
|
3
|
+
// Real bundling (tsdown/vite) lands with the Electron shell work.
|
|
4
|
+
import { cpSync, mkdirSync } from 'node:fs'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
9
|
+
mkdirSync(join(root, 'lib'), { recursive: true })
|
|
10
|
+
cpSync(join(root, 'src', 'host', 'index.js'), join(root, 'lib', 'index.js'))
|
|
11
|
+
cpSync(join(root, 'src', 'client', 'client.js'), join(root, 'lib', 'client.js'))
|
|
12
|
+
console.log('[dsh-clean-desktop-shell] built lib/ from src/')
|