dsh-clean-desktop-shell 0.1.2 → 0.1.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.en.md +192 -168
- package/README.md +175 -161
- package/build/icon.ico +0 -0
- package/electron/aumid.js +17 -0
- package/electron/config.js +3 -2
- package/electron/main.js +65 -8
- package/electron/shortcut.js +130 -0
- package/electron/tray.js +24 -10
- package/electron/window.js +21 -2
- package/lib/client.js +15 -6
- package/lib/index.js +323 -6
- package/package.json +5 -1
- package/scripts/build.mjs +12 -3
- package/src/client/client.js +15 -6
- package/src/host/index.js +323 -6
- package/version.txt +1 -1
package/electron/tray.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from './service.js'
|
|
21
21
|
import { showProgress, setProgress, closeProgress } from './progress.js'
|
|
22
22
|
import { checkForUpdate, checkForUpdatesAuto, isAutoUpdateSupported, openRepo, openUrl } from './update.js'
|
|
23
|
+
import { shortcutSupported, createDesktopShortcut } from './shortcut.js'
|
|
23
24
|
|
|
24
25
|
const trayIconPath = join(
|
|
25
26
|
fileURLToPath(new URL('.', import.meta.url)),
|
|
@@ -30,10 +31,10 @@ const trayIconPath = join(
|
|
|
30
31
|
let trayInstance = null
|
|
31
32
|
let handlers = null
|
|
32
33
|
|
|
33
|
-
export function createTray({ onShow,
|
|
34
|
+
export function createTray({ onShow, onReload, onQuit }) {
|
|
34
35
|
const icon = nativeImage.createFromPath(trayIconPath)
|
|
35
36
|
|
|
36
|
-
handlers = { onShow,
|
|
37
|
+
handlers = { onShow, onReload, onQuit }
|
|
37
38
|
|
|
38
39
|
trayInstance = new Tray(icon)
|
|
39
40
|
trayInstance.setToolTip('DSH Clean Desktop Shell')
|
|
@@ -50,7 +51,7 @@ export function createTray({ onShow, onToggleAutoStart, onReload, onQuit }) {
|
|
|
50
51
|
/** Rebuild the context menu (call after backend state changes). */
|
|
51
52
|
export function refreshTrayMenu() {
|
|
52
53
|
if (!trayInstance || !handlers) return
|
|
53
|
-
const { onShow,
|
|
54
|
+
const { onShow, onReload, onQuit } = handlers
|
|
54
55
|
const st = getStatus()
|
|
55
56
|
const label = statusLabel(st)
|
|
56
57
|
|
|
@@ -60,6 +61,26 @@ export function refreshTrayMenu() {
|
|
|
60
61
|
label: '刷新窗口',
|
|
61
62
|
click: onReload,
|
|
62
63
|
},
|
|
64
|
+
{
|
|
65
|
+
label: '创建桌面快捷方式',
|
|
66
|
+
enabled: shortcutSupported(),
|
|
67
|
+
click: async () => {
|
|
68
|
+
const ok = await createDesktopShortcut()
|
|
69
|
+
if (ok) {
|
|
70
|
+
dialog.showMessageBoxSync({
|
|
71
|
+
type: 'info',
|
|
72
|
+
title: '已创建',
|
|
73
|
+
message: '桌面快捷方式已创建。',
|
|
74
|
+
})
|
|
75
|
+
} else {
|
|
76
|
+
dialog.showMessageBoxSync({
|
|
77
|
+
type: 'warning',
|
|
78
|
+
title: '创建失败',
|
|
79
|
+
message: '无法创建桌面快捷方式,请稍后重试。',
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
},
|
|
63
84
|
{ type: 'separator' },
|
|
64
85
|
{ label: `后端:${label}`, enabled: false },
|
|
65
86
|
{
|
|
@@ -153,13 +174,6 @@ export function refreshTrayMenu() {
|
|
|
153
174
|
click: () => openRepo(),
|
|
154
175
|
},
|
|
155
176
|
{ type: 'separator' },
|
|
156
|
-
{
|
|
157
|
-
label: '开机自启',
|
|
158
|
-
type: 'checkbox',
|
|
159
|
-
checked: !!loadConfig().autoLaunch,
|
|
160
|
-
click: (item) => onToggleAutoStart(item.checked),
|
|
161
|
-
},
|
|
162
|
-
{ type: 'separator' },
|
|
163
177
|
{ label: '退出', click: onQuit },
|
|
164
178
|
])
|
|
165
179
|
trayInstance.setContextMenu(menu)
|
package/electron/window.js
CHANGED
|
@@ -15,16 +15,26 @@
|
|
|
15
15
|
* window flips back to the offline screen instead of showing a stale page
|
|
16
16
|
* that suggests the app is still alive.
|
|
17
17
|
*/
|
|
18
|
-
import { BrowserWindow, ipcMain } from 'electron'
|
|
18
|
+
import { app, BrowserWindow, ipcMain } from 'electron'
|
|
19
|
+
import { existsSync } from 'node:fs'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
19
21
|
import { fileURLToPath } from 'node:url'
|
|
20
22
|
import { probe, onStatusChange, detect } from './service.js'
|
|
21
23
|
import { startBackendWithProgress, chooseBackendFolder } from './tray.js'
|
|
24
|
+
import { APP_USER_MODEL_ID } from './aumid.js'
|
|
22
25
|
|
|
23
26
|
export const WINDOWS_TITLEBAR_HEIGHT = 32
|
|
24
27
|
|
|
28
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
25
29
|
const PRELOAD_PATH = fileURLToPath(new URL('./preload.js', import.meta.url))
|
|
30
|
+
// Windows taskbar follows the window icon only when it is an .ico; a png
|
|
31
|
+
// covers the title bar / alt-tab but not the taskbar button. In plugin
|
|
32
|
+
// mode there is no exe icon resource, so prefer the bundled .ico.
|
|
33
|
+
const TASKBAR_ICO = join(PKG_ROOT, 'build', 'icon.ico')
|
|
26
34
|
// Black-whale app icon (matches the DSH web favicon).
|
|
27
|
-
const ICON_PATH =
|
|
35
|
+
const ICON_PATH = existsSync(TASKBAR_ICO)
|
|
36
|
+
? TASKBAR_ICO
|
|
37
|
+
: fileURLToPath(new URL('../build/icon.png', import.meta.url))
|
|
28
38
|
// Local fallback page shown while the backend is down.
|
|
29
39
|
const ERROR_PAGE = fileURLToPath(new URL('./error.html', import.meta.url))
|
|
30
40
|
|
|
@@ -189,6 +199,15 @@ export function createMainWindow({ target }) {
|
|
|
189
199
|
// Linux / other: keep the native frame.
|
|
190
200
|
|
|
191
201
|
const win = new BrowserWindow(options)
|
|
202
|
+
// Windows taskbar button: bare runtime electron.exe has no custom icon,
|
|
203
|
+
// so pin the button to our .ico via setAppDetails (appId must match the
|
|
204
|
+
// app-level AppUserModelId set in main.js, else the options are ignored).
|
|
205
|
+
if (process.platform === 'win32' && existsSync(TASKBAR_ICO)) {
|
|
206
|
+
win.setAppDetails({
|
|
207
|
+
appId: APP_USER_MODEL_ID,
|
|
208
|
+
appIconPath: TASKBAR_ICO,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
192
211
|
windowTargets.set(win.id, target)
|
|
193
212
|
win.loadURL(target).catch(() => startReconnect(win, target))
|
|
194
213
|
|
package/lib/client.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-clean-desktop-shell — client half (web browser bundle).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The shell is a standalone Electron window; it injects nothing into the
|
|
5
|
+
* dsh web UI. This client module exists only to satisfy the client-modules
|
|
6
|
+
* loader contract — a bundle's client entry must register itself via
|
|
7
|
+
* `window.__ModuleLoader__.load({ id, factory })`, otherwise dsh reports
|
|
8
|
+
* "loaded without registering … via __ModuleLoader__.load".
|
|
7
9
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
window.__ModuleLoader__.load({
|
|
11
|
+
id: 'dsh-clean-desktop-shell',
|
|
12
|
+
factory: () => {
|
|
13
|
+
var module = { exports: {} };
|
|
14
|
+
var exports = module.exports;
|
|
15
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
16
|
+
exports.apply = () => {};
|
|
17
|
+
return module.exports;
|
|
18
|
+
},
|
|
19
|
+
});
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,330 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-clean-desktop-shell — host half (plugin loader entry).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Branch 2 (plugin-market distribution): when installed through the DSH
|
|
5
|
+
* plugin market, this host half brings up the Electron shell itself.
|
|
6
|
+
*
|
|
7
|
+
* The electron runtime is NOT an npm dependency (electron-builder forbids
|
|
8
|
+
* electron in "dependencies", and pnpm's allowBuilds would block its
|
|
9
|
+
* postinstall anyway). Instead the host half manages the runtime on its
|
|
10
|
+
* own, under $DSH_HOME/desktop-shell-runtime/:
|
|
11
|
+
*
|
|
12
|
+
* 1. resolve the version to run (package.json → desktopShell.electronVersion)
|
|
13
|
+
* 2. if that version dir exists → reuse it
|
|
14
|
+
* 3. otherwise download the electron zip from the best source for the
|
|
15
|
+
* network (official GitHub releases vs npmmirror mirror), extract it,
|
|
16
|
+
* and drop any older version dirs (no unbounded disk growth)
|
|
17
|
+
* 4. spawn the shell (runtime electron + electron/main.js) — the same
|
|
18
|
+
* code branch 1 (installer) ships
|
|
19
|
+
*
|
|
20
|
+
* Window, tray, backend management etc. are identical to branch 1; only
|
|
21
|
+
* the runtime provisioning differs.
|
|
8
22
|
*/
|
|
23
|
+
import { spawn } from 'node:child_process'
|
|
24
|
+
import {
|
|
25
|
+
cpSync,
|
|
26
|
+
existsSync,
|
|
27
|
+
mkdirSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
readdirSync,
|
|
30
|
+
renameSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
symlinkSync,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
} from 'node:fs'
|
|
35
|
+
import { homedir } from 'node:os'
|
|
36
|
+
import { dirname, join } from 'node:path'
|
|
37
|
+
import { fileURLToPath } from 'node:url'
|
|
38
|
+
|
|
39
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
40
|
+
const MAIN_JS = join(PKG_ROOT, 'electron', 'main.js')
|
|
41
|
+
const isWin = process.platform === 'win32'
|
|
42
|
+
const EXE_NAME = isWin ? 'electron.exe' : 'electron'
|
|
43
|
+
const ARCH = process.arch === 'arm64' ? 'arm64' : 'x64'
|
|
44
|
+
const PLATFORM = isWin ? 'win32' : process.platform === 'darwin' ? 'darwin' : 'linux'
|
|
45
|
+
|
|
46
|
+
// cordis registers the plugin by this name — bundles without an explicit
|
|
47
|
+
// `name` export are silently skipped by the dsh loader.
|
|
48
|
+
export const name = 'dsh-clean-desktop-shell'
|
|
49
|
+
|
|
50
|
+
// Disable auto-launch with DSH_SHELL_AUTO_LAUNCH=0.
|
|
51
|
+
const AUTO_LAUNCH = process.env.DSH_SHELL_AUTO_LAUNCH !== '0'
|
|
52
|
+
|
|
53
|
+
let launched = false
|
|
54
|
+
|
|
9
55
|
export function apply(ctx) {
|
|
10
|
-
ctx.
|
|
11
|
-
|
|
56
|
+
ctx.logger.info('[clean-desktop-shell] mounted (host half)')
|
|
57
|
+
if (!AUTO_LAUNCH) return
|
|
58
|
+
// The dsh bundle loader calls apply() during early boot, but the cordis
|
|
59
|
+
// 'ready' event never fires for bundle plugins (dshmarket / agent-teams
|
|
60
|
+
// use ctx.inject or run inline instead). Launch directly: the shell is a
|
|
61
|
+
// separate process with its own offline screen + auto-reconnect, so an
|
|
62
|
+
// early launch is safe — it shows the offline page until 3080 answers.
|
|
63
|
+
;(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const exe = await ensureRuntime(ctx)
|
|
66
|
+
launchShell(exe, ctx)
|
|
67
|
+
} catch (err) {
|
|
68
|
+
ctx.logger.warn(`[clean-desktop-shell] shell launch failed: ${err?.message ?? err}`)
|
|
69
|
+
}
|
|
70
|
+
})()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------- electron runtime provisioning ----------
|
|
74
|
+
|
|
75
|
+
function electronVersion() {
|
|
76
|
+
try {
|
|
77
|
+
const meta = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
|
|
78
|
+
return meta.desktopShell?.electronVersion || null
|
|
79
|
+
} catch {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runtimeRoot() {
|
|
85
|
+
return join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'desktop-shell-runtime')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function versionDir(root, version) {
|
|
89
|
+
return join(root, `electron-v${version}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function zipName(version) {
|
|
93
|
+
return `electron-v${version}-${PLATFORM}-${ARCH}.zip`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function ensureRuntime(ctx) {
|
|
97
|
+
const version = electronVersion()
|
|
98
|
+
if (!version) throw new Error('desktopShell.electronVersion missing in package.json')
|
|
99
|
+
const root = runtimeRoot()
|
|
100
|
+
const dir = versionDir(root, version)
|
|
101
|
+
const exe = join(dir, EXE_NAME)
|
|
102
|
+
|
|
103
|
+
// 1) Already provisioned for this version?
|
|
104
|
+
if (existsSync(exe)) {
|
|
105
|
+
cleanupOldVersions(root, dir)
|
|
106
|
+
await patchExeIcon(ctx, exe, root).catch(() => {})
|
|
107
|
+
return exe
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 2) Local reuse: DSH_SHELL_ELECTRON_DIR → link/copy its dist/ (fast).
|
|
111
|
+
const localSrc = process.env.DSH_SHELL_ELECTRON_DIR
|
|
112
|
+
if (localSrc) {
|
|
113
|
+
const srcExe = join(localSrc, 'dist', EXE_NAME)
|
|
114
|
+
if (existsSync(srcExe) && provisionLocalDist(localSrc, dir)) {
|
|
115
|
+
ctx.logger.info(`[clean-desktop-shell] reused electron runtime from ${localSrc}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 3) Download + extract the official zip from a network-appropriate source.
|
|
120
|
+
if (!existsSync(exe)) {
|
|
121
|
+
mkdirSync(root, { recursive: true })
|
|
122
|
+
await downloadRuntime(ctx, version, root, dir)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!existsSync(exe)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
'electron runtime provisioning failed — check network, or point DSH_SHELL_ELECTRON_DIR at an electron package',
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
cleanupOldVersions(root, dir)
|
|
131
|
+
ctx.logger.info(`[clean-desktop-shell] electron runtime ${version} ready at ${dir}`)
|
|
132
|
+
await patchExeIcon(ctx, exe, root).catch(() => {})
|
|
133
|
+
return exe
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Patch the runtime electron.exe's icon resource so the Windows taskbar
|
|
138
|
+
* shows our whale icon. A bare runtime exe ships Electron's default icon
|
|
139
|
+
* and — as documented — no runtime API (BrowserWindow icon, setAppDetails,
|
|
140
|
+
* AUMID shortcuts) can change the taskbar button: it reads the exe's icon
|
|
141
|
+
* resource. rcedit (electron team's official tool) rewrites it in place.
|
|
142
|
+
*
|
|
143
|
+
* Best-effort: icon patching must never block the shell from launching.
|
|
144
|
+
* Idempotent: a marker file next to the exe records success; a re-provisioned
|
|
145
|
+
* (new version) exe has no marker and gets patched again.
|
|
146
|
+
*/
|
|
147
|
+
async function patchExeIcon(ctx, exe, root) {
|
|
148
|
+
if (!isWin) return
|
|
149
|
+
const ico = join(PKG_ROOT, 'build', 'icon.ico')
|
|
150
|
+
if (!existsSync(ico)) return
|
|
151
|
+
const marker = `${exe}.whale-icon`
|
|
152
|
+
if (existsSync(marker)) return
|
|
153
|
+
|
|
154
|
+
// rcedit is a single self-contained exe, cached next to the runtimes.
|
|
155
|
+
const rcedit = join(root, 'rcedit-x64.exe')
|
|
156
|
+
if (!existsSync(rcedit)) {
|
|
157
|
+
const url = 'https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe'
|
|
158
|
+
ctx.logger.info('[clean-desktop-shell] downloading rcedit for icon patching')
|
|
159
|
+
if (!(await fetchFile(url, rcedit))) {
|
|
160
|
+
ctx.logger.warn('[clean-desktop-shell] rcedit download failed — taskbar icon stays default')
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const child = spawn(rcedit, [exe, '--set-icon', ico], {
|
|
166
|
+
windowsHide: true,
|
|
167
|
+
stdio: 'ignore',
|
|
168
|
+
})
|
|
169
|
+
const ok = await new Promise((resolve) => {
|
|
170
|
+
child.on('error', () => resolve(false))
|
|
171
|
+
child.on('exit', (code) => resolve(code === 0))
|
|
172
|
+
})
|
|
173
|
+
if (ok) {
|
|
174
|
+
writeFileSync(marker, String(Date.now()), 'utf8')
|
|
175
|
+
ctx.logger.info('[clean-desktop-shell] taskbar icon patched (rcedit)')
|
|
176
|
+
} else {
|
|
177
|
+
ctx.logger.warn('[clean-desktop-shell] rcedit patch failed — taskbar icon stays default')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function downloadRuntime(ctx, version, root, dir) {
|
|
182
|
+
const tmpZip = join(root, `.electron-${version}.zip.tmp`)
|
|
183
|
+
rmSync(tmpZip, { force: true })
|
|
184
|
+
const urls = await runtimeUrls(version)
|
|
185
|
+
for (const url of urls) {
|
|
186
|
+
ctx.logger.info(`[clean-desktop-shell] downloading electron ${version} from ${url}`)
|
|
187
|
+
if (await fetchFile(url, tmpZip)) {
|
|
188
|
+
// Zip extracts to an inner dir named like the zip basename.
|
|
189
|
+
const inner = join(root, zipName(version).replace(/\.zip$/, ''))
|
|
190
|
+
try {
|
|
191
|
+
await extractZip(tmpZip, root)
|
|
192
|
+
if (existsSync(join(inner, EXE_NAME)) && inner !== dir) {
|
|
193
|
+
rmSync(dir, { recursive: true, force: true })
|
|
194
|
+
renameSync(inner, dir)
|
|
195
|
+
}
|
|
196
|
+
rmSync(tmpZip, { force: true })
|
|
197
|
+
return
|
|
198
|
+
} catch (err) {
|
|
199
|
+
ctx.logger.warn(`[clean-desktop-shell] extract failed: ${err?.message ?? err}`)
|
|
200
|
+
rmSync(inner, { recursive: true, force: true })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Failed download — drop the partial file so a later run starts clean.
|
|
204
|
+
rmSync(tmpZip, { force: true })
|
|
205
|
+
}
|
|
206
|
+
throw new Error('electron download failed from all sources')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Pick the download source by racing a HEAD probe against each candidate
|
|
211
|
+
* (direct connection, 3s each). The fastest reachable source goes first —
|
|
212
|
+
* this naturally prefers the domestic npmmirror mirror on CN networks,
|
|
213
|
+
* the official GitHub source on international/well-proxied networks, and
|
|
214
|
+
* never wastes a full download on a dead source.
|
|
215
|
+
*/
|
|
216
|
+
async function runtimeUrls(version) {
|
|
217
|
+
const candidates = [
|
|
218
|
+
{ name: 'github', url: `https://github.com/electron/electron/releases/download/v${version}/${zipName(version)}` },
|
|
219
|
+
{ name: 'npmmirror', url: `https://npmmirror.com/mirrors/electron/${version}/${zipName(version)}` },
|
|
220
|
+
]
|
|
221
|
+
const results = await Promise.all(
|
|
222
|
+
candidates.map(async (c) => {
|
|
223
|
+
const t0 = Date.now()
|
|
224
|
+
try {
|
|
225
|
+
const ctrl = new AbortController()
|
|
226
|
+
const timer = setTimeout(() => ctrl.abort(), 3000)
|
|
227
|
+
const res = await fetch(c.url, { signal: ctrl.signal, method: 'HEAD' })
|
|
228
|
+
clearTimeout(timer)
|
|
229
|
+
if (res.status < 500) return { ...c, ms: Date.now() - t0 }
|
|
230
|
+
} catch {
|
|
231
|
+
// unreachable — drop
|
|
232
|
+
}
|
|
233
|
+
return null
|
|
234
|
+
}),
|
|
235
|
+
)
|
|
236
|
+
const ok = results.filter(Boolean).sort((a, b) => a.ms - b.ms)
|
|
237
|
+
if (ok.length === 0) {
|
|
238
|
+
// Probes all failed (offline?) — still try both, mirror first (cheap).
|
|
239
|
+
return [candidates[1].url, candidates[0].url]
|
|
240
|
+
}
|
|
241
|
+
const rest = candidates.map((c) => c.url).filter((u) => u !== ok[0].url)
|
|
242
|
+
return [ok[0].url, ...rest]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function fetchFile(url, dest) {
|
|
246
|
+
return new Promise((resolve) => {
|
|
247
|
+
// curl is available on Windows 10+; streams to disk, honors proxy env.
|
|
248
|
+
// --max-time keeps a stalled download from hanging forever (a proxy
|
|
249
|
+
// stall previously left a half-written .zip.tmp and blocked the shell
|
|
250
|
+
// launch); --retry 2 rides out transient failures.
|
|
251
|
+
const child = spawn(
|
|
252
|
+
'curl',
|
|
253
|
+
['-L', '--fail', '--silent', '--show-error', '--retry', '2', '--max-time', '600', '-o', dest, url],
|
|
254
|
+
{ windowsHide: true, stdio: 'ignore' },
|
|
255
|
+
)
|
|
256
|
+
child.on('error', () => resolve(false))
|
|
257
|
+
child.on('exit', (code) => resolve(code === 0))
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function extractZip(zipPath, dest) {
|
|
262
|
+
// Windows ships bsdtar (tar.exe) which reads zip; fall back to
|
|
263
|
+
// PowerShell Expand-Archive if needed.
|
|
264
|
+
const child = spawn('tar', ['-xf', zipPath, '-C', dest], { windowsHide: true, stdio: 'ignore' })
|
|
265
|
+
return new Promise((resolve, reject) => {
|
|
266
|
+
child.on('error', reject)
|
|
267
|
+
child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`tar exit ${code}`))))
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Remove version dirs older than the current one (dead weight). */
|
|
272
|
+
function cleanupOldVersions(root, currentDir) {
|
|
273
|
+
try {
|
|
274
|
+
for (const entry of readdirSync(root)) {
|
|
275
|
+
if (!entry.startsWith('electron-v')) continue
|
|
276
|
+
const full = join(root, entry)
|
|
277
|
+
if (full === currentDir) continue
|
|
278
|
+
rmSync(full, { recursive: true, force: true })
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
// best-effort
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Provision a local electron package's dist/ as the version dir itself,
|
|
287
|
+
* so the layout matches a downloaded runtime: <dir>/electron(.exe) at the
|
|
288
|
+
* version-dir root. Windows: junction (zero-copy, instant) — a 269MB
|
|
289
|
+
* recursive cpSync can be killed by sandbox/AV on large trees, so only
|
|
290
|
+
* fall back to a copy.
|
|
291
|
+
*/
|
|
292
|
+
function provisionLocalDist(srcPkg, destDir) {
|
|
293
|
+
if (isWin) {
|
|
294
|
+
try {
|
|
295
|
+
rmSync(destDir, { recursive: true, force: true })
|
|
296
|
+
symlinkSync(join(srcPkg, 'dist'), destDir, 'junction')
|
|
297
|
+
return true
|
|
298
|
+
} catch {
|
|
299
|
+
// fall through to a real copy
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
rmSync(destDir, { recursive: true, force: true })
|
|
304
|
+
cpSync(join(srcPkg, 'dist'), destDir, { recursive: true })
|
|
305
|
+
return true
|
|
306
|
+
} catch {
|
|
307
|
+
return false
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---------- shell launch ----------
|
|
312
|
+
|
|
313
|
+
function launchShell(exe, ctx) {
|
|
314
|
+
if (launched) return
|
|
315
|
+
const child = spawn(exe, [MAIN_JS], {
|
|
316
|
+
cwd: PKG_ROOT,
|
|
317
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined },
|
|
318
|
+
stdio: 'ignore',
|
|
319
|
+
windowsHide: false,
|
|
320
|
+
})
|
|
321
|
+
launched = true
|
|
322
|
+
child.on('error', (err) => {
|
|
323
|
+
launched = false
|
|
324
|
+
ctx.logger.warn(`[clean-desktop-shell] shell spawn error: ${err.message}`)
|
|
325
|
+
})
|
|
326
|
+
child.on('exit', (code) => {
|
|
327
|
+
launched = false
|
|
328
|
+
ctx.logger.info(`[clean-desktop-shell] shell exited (${code})`)
|
|
12
329
|
})
|
|
13
330
|
}
|
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.4",
|
|
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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"src/",
|
|
11
11
|
"scripts/",
|
|
12
12
|
"build/icon.png",
|
|
13
|
+
"build/icon.ico",
|
|
13
14
|
"cordis.patch.yml",
|
|
14
15
|
"README.md",
|
|
15
16
|
"README.en.md",
|
|
@@ -108,5 +109,8 @@
|
|
|
108
109
|
},
|
|
109
110
|
"dependencies": {
|
|
110
111
|
"electron-updater": "^6.8.9"
|
|
112
|
+
},
|
|
113
|
+
"desktopShell": {
|
|
114
|
+
"electronVersion": "33.4.11"
|
|
111
115
|
}
|
|
112
116
|
}
|
package/scripts/build.mjs
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Minimal zero-dependency build: copies src → lib.
|
|
3
3
|
// Real bundling (tsdown/vite) lands with the Electron shell work.
|
|
4
|
-
|
|
4
|
+
//
|
|
5
|
+
// Uses read+write (not cpSync/rmSync): Windows cpSync fails to overwrite
|
|
6
|
+
// existing files, and the sandbox's safe-delete shim intercepts rmSync.
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
5
8
|
import { dirname, join } from 'node:path'
|
|
6
9
|
import { fileURLToPath } from 'node:url'
|
|
7
10
|
|
|
8
11
|
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
9
12
|
mkdirSync(join(root, 'lib'), { recursive: true })
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
const pairs = [
|
|
15
|
+
['src/host/index.js', 'lib/index.js'],
|
|
16
|
+
['src/client/client.js', 'lib/client.js'],
|
|
17
|
+
]
|
|
18
|
+
for (const [src, dest] of pairs) {
|
|
19
|
+
writeFileSync(join(root, dest), readFileSync(join(root, src)))
|
|
20
|
+
}
|
|
12
21
|
console.log('[dsh-clean-desktop-shell] built lib/ from src/')
|
package/src/client/client.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-clean-desktop-shell — client half (web browser bundle).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The shell is a standalone Electron window; it injects nothing into the
|
|
5
|
+
* dsh web UI. This client module exists only to satisfy the client-modules
|
|
6
|
+
* loader contract — a bundle's client entry must register itself via
|
|
7
|
+
* `window.__ModuleLoader__.load({ id, factory })`, otherwise dsh reports
|
|
8
|
+
* "loaded without registering … via __ModuleLoader__.load".
|
|
7
9
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
window.__ModuleLoader__.load({
|
|
11
|
+
id: 'dsh-clean-desktop-shell',
|
|
12
|
+
factory: () => {
|
|
13
|
+
var module = { exports: {} };
|
|
14
|
+
var exports = module.exports;
|
|
15
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
16
|
+
exports.apply = () => {};
|
|
17
|
+
return module.exports;
|
|
18
|
+
},
|
|
19
|
+
});
|