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/README.md
CHANGED
|
@@ -1,161 +1,175 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# dsh-clean-desktop-shell
|
|
4
|
-
|
|
5
|
-
**DeepSeek Harness 的纯净桌面壳(DSH 插件形态)**
|
|
6
|
-
|
|
7
|
-
只做一件事:给已配置好的 DSH Web
|
|
8
|
-
|
|
9
|
-
[English](README.en.md) · [中文](README.md)
|
|
10
|
-
|
|
11
|
-
[](https://github.com/Icather/dsh-clean-desktop-shell)
|
|
12
|
-
[](LICENSE)
|
|
13
|
-
[](https://github.com/Icather/dsh-clean-desktop-shell/releases/latest)
|
|
14
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
15
|
-
[](https://github.com/Icather/dsh-clean-desktop-shell/graphs/contributors)
|
|
16
|
-
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
## 这是什么
|
|
20
|
-
|
|
21
|
-
`dsh-clean-desktop-shell` 是一个 **DSH 插件形态** 的纯净桌面壳:它给已经跑起来的 DSH Web(默认 `http://127.0.0.1:3080
|
|
22
|
-
|
|
23
|
-
与生态里其他桌面端方案的最大区别:
|
|
24
|
-
|
|
25
|
-
| | 其他桌面端(如 dsh-desktop 系列) | 本插件 |
|
|
26
|
-
|:--|:--|:--|
|
|
27
|
-
| **形态** | 独立 Electron 应用,自带独立 profile | **DSH 插件**,挂载进现有 profile |
|
|
28
|
-
| **Profile** | 新建 desktop profile,插件/配置要重装 | **复用现有 web profile**,零迁移 |
|
|
29
|
-
| **视觉改造** | 自绘标题栏 / 毛玻璃等 | **零改造**,纯净窗口壳 |
|
|
30
|
-
| **跟随上游** | 固定版本 | **跟随 rc.7** |
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
npm
|
|
140
|
-
npm run
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# dsh-clean-desktop-shell
|
|
4
|
+
|
|
5
|
+
**DeepSeek Harness 的纯净桌面壳(DSH 插件形态)**
|
|
6
|
+
|
|
7
|
+
只做一件事:给已配置好的 DSH Web 加一层干净的桌面窗口——系统托盘、单实例、像普通软件一样用。无毛玻璃、无花哨材质,**纯净**。
|
|
8
|
+
|
|
9
|
+
[English](README.en.md) · [中文](README.md)
|
|
10
|
+
|
|
11
|
+
[](https://github.com/Icather/dsh-clean-desktop-shell)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
[](https://github.com/Icather/dsh-clean-desktop-shell/releases/latest)
|
|
14
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
15
|
+
[](https://github.com/Icather/dsh-clean-desktop-shell/graphs/contributors)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
## 这是什么
|
|
20
|
+
|
|
21
|
+
`dsh-clean-desktop-shell` 是一个 **DSH 插件形态** 的纯净桌面壳:它给已经跑起来的 DSH Web(默认 `http://127.0.0.1:3080`)套一层原生桌面窗口——系统托盘、单实例,像普通桌面软件一样使用。**不做任何视觉改造**:不加毛玻璃、不改界面,纯粹是"窗口壳"。
|
|
22
|
+
|
|
23
|
+
与生态里其他桌面端方案的最大区别:
|
|
24
|
+
|
|
25
|
+
| | 其他桌面端(如 dsh-desktop 系列) | 本插件 |
|
|
26
|
+
|:--|:--|:--|
|
|
27
|
+
| **形态** | 独立 Electron 应用,自带独立 profile | **DSH 插件**,挂载进现有 profile |
|
|
28
|
+
| **Profile** | 新建 desktop profile,插件/配置要重装 | **复用现有 web profile**,零迁移 |
|
|
29
|
+
| **视觉改造** | 自绘标题栏 / 毛玻璃等 | **零改造**,纯净窗口壳 |
|
|
30
|
+
| **跟随上游** | 固定版本 | **跟随 rc.7** |
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
**方式一:从 Release 下载安装包(想要独立桌面应用的用户)**
|
|
35
|
+
|
|
36
|
+
- Windows:下载 `DSH-Clean-Desktop-Shell-Setup-<版本>.exe`
|
|
37
|
+
- macOS:下载 `DSH-Clean-Desktop-Shell-<版本>.dmg`(Intel)或 `-arm64.dmg`(Apple Silicon)
|
|
38
|
+
|
|
39
|
+
安装包会**自动创建桌面快捷方式**,并提供系统托盘等完整桌面体验。首次运行 Windows 安装包可能触发 SmartScreen 警告——**这是未签名程序的正常现象,不是病毒**,见下方「Windows SmartScreen 警告说明」。
|
|
40
|
+
|
|
41
|
+
**方式二:作为 DSH 插件安装(DSH 生态用户)**
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
dsh plugin --profile web add dsh-clean-desktop-shell
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
重启 `dsh web` 后,桌面壳窗口会**自动弹出**(首次运行需联网准备 Electron 运行时,约 1-2 分钟)。
|
|
48
|
+
|
|
49
|
+
> 方式二得到的是"随 DSH 启动的桌面壳":窗口由插件在 `dsh web` 启动时自动拉起,**不产生独立安装包 / 桌面图标**。想要可双击启动、带桌面快捷方式和自动更新的独立应用,请用方式一。两种方式的核心窗口体验一致。
|
|
50
|
+
|
|
51
|
+
> 桌面壳需要本机有可用的 `dsh web` 服务(或配置的远程地址)。见下方「使用」。
|
|
52
|
+
|
|
53
|
+
### Windows SmartScreen 警告说明
|
|
54
|
+
|
|
55
|
+
**为什么会看到警告?**
|
|
56
|
+
|
|
57
|
+
我们的安装包**没有代码签名证书**(个人开源项目暂未购买,证书年费约数百美元)。Windows 的 Microsoft Defender SmartScreen 是一个**信誉系统**——它根据"下载量 + 干净运行的记录"判断一个程序是否可信。对下载量少、未签名的 exe,它无法确认信誉,就会警告。**这不代表文件有病毒**:本项目完全开源,代码可审阅,也可本地构建比对(见下)。
|
|
58
|
+
|
|
59
|
+
**Edge 下载时会看到:**
|
|
60
|
+
|
|
61
|
+
下载面板里该文件被标记为"不常下载的文件",需要手动保留:
|
|
62
|
+
|
|
63
|
+
1. 悬停下载项,点击右侧的 `...` 菜单
|
|
64
|
+
2. 选择「保留」(Keep)
|
|
65
|
+
3. 弹窗确认,选择「仍要保留」(Keep anyway)
|
|
66
|
+
|
|
67
|
+
**双击安装时会看到:**
|
|
68
|
+
|
|
69
|
+
蓝色对话框「Windows 已保护你的电脑」——Microsoft Defender SmartScreen 阻止了无法识别的应用启动:
|
|
70
|
+
|
|
71
|
+
1. 点击「更多信息」(More info)
|
|
72
|
+
2. 核对文件名确实是 `DSH-Clean-Desktop-Shell-Setup-<版本>.exe`
|
|
73
|
+
3. 点击「仍要运行」(Run anyway)
|
|
74
|
+
|
|
75
|
+
**备选:一次性解除锁定(推荐)**
|
|
76
|
+
|
|
77
|
+
右键安装包 → 属性 → 常规 → 底部勾选「解除锁定」→ 确定。之后双击不再有警告。
|
|
78
|
+
|
|
79
|
+
或 PowerShell 批量解除:
|
|
80
|
+
|
|
81
|
+
```powershell
|
|
82
|
+
Unblock-File -Path "$env:USERPROFILE\Downloads\DSH-Clean-Desktop-Shell-Setup-*.exe"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**关于文件安全性的说明**
|
|
86
|
+
|
|
87
|
+
安装包由 GitHub Actions 从本仓库源码自动构建(见 `.github/workflows/build.yml`),代码完全开源可审阅。如仍有疑虑,可自行 `git clone` 后按「开发」一节本地构建比对,或稍等下载量积累——SmartScreen 信誉度上去后警告会自动消失。
|
|
88
|
+
|
|
89
|
+
> 说明:代码签名证书(EV 或 Azure Trusted Signing)可以彻底消除这个警告,但需要付费且对个人开源维护者不划算。本仓库会在条件允许时考虑接入签名。
|
|
90
|
+
|
|
91
|
+
## 架构
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
┌──────────────────── 内核(dsh web / headless 服务) ────────────────────┐
|
|
95
|
+
│ 会话 · Agent · 插件 · 记忆 都在这层,与界面解耦 │
|
|
96
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
97
|
+
▲
|
|
98
|
+
┌───────────────┴───────────────┐
|
|
99
|
+
│ dsh-clean-desktop-shell │
|
|
100
|
+
│ Electron 壳(客户端) │
|
|
101
|
+
│ 托盘 · 单实例 │
|
|
102
|
+
└────────────────────────────────┘
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- **默认**:加载本地 `127.0.0.1:3080`(已配置好的 web profile,零迁移)。
|
|
106
|
+
- **可配远程**:在设置里填入任意远程 DSH 地址,壳只当窗口——手机 / Linux / 其他设备通过浏览器或 PWA 也能接入内核,壳本身不绑定本地服务。
|
|
107
|
+
|
|
108
|
+
### 平台矩阵
|
|
109
|
+
|
|
110
|
+
| 平台 | 壳 | 状态 |
|
|
111
|
+
|:--|:--|:--|
|
|
112
|
+
| Windows | ✅ Electron(无边框 + 原生窗口按钮) | 已发布(NSIS 安装包) |
|
|
113
|
+
| macOS | ✅ Electron(hiddenInset) | 已发布(CI 构建 Intel + Apple Silicon DMG) |
|
|
114
|
+
| Linux | —(浏览器 / PWA 直连内核) | 不做 |
|
|
115
|
+
| Termux / 手机 / 平板 | —(headless / PWA 直连内核) | 由内核远程访问支持 |
|
|
116
|
+
|
|
117
|
+
## 使用
|
|
118
|
+
|
|
119
|
+
1. 启动 `dsh web`(或配置远程服务地址)。
|
|
120
|
+
2. 启动桌面壳:自动检测本地 3080——后端在跑则直接加载,未运行则显示「后端未连接」页,可在离线页一键启动。
|
|
121
|
+
3. 窗口顶部(右侧留系统按钮)可拖动窗口;关闭默认缩到托盘。
|
|
122
|
+
|
|
123
|
+
**后端的一切操作都在托盘右键**,主窗口保持纯壳:
|
|
124
|
+
|
|
125
|
+
- 启动 / 重启 / 关闭后端(带进度弹窗;关闭会真正停掉 3080 上的服务,包括外部启动的实例)
|
|
126
|
+
- 自动探测后端 · 设置后端安装文件夹(默认自动探测定位)
|
|
127
|
+
- 刷新窗口 · 检查更新 · 仓库主页
|
|
128
|
+
|
|
129
|
+
**窗口的可靠性(Edge 式即时刷新)**:
|
|
130
|
+
|
|
131
|
+
- 双击启动立即出窗,不等后端就绪
|
|
132
|
+
- 后端没起来时显示「后端未连接」页,自动探测;后端一通立即加载
|
|
133
|
+
- 后端关闭 / 被杀的一刻,窗口立刻切回离线页——不会停在旧页面假装还活着
|
|
134
|
+
- 离线页内置快捷按钮:重新加载 / 启动后端 / 自动探测后端 / 设置后端安装文件夹
|
|
135
|
+
|
|
136
|
+
## 开发
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
npm install
|
|
140
|
+
npm run build # 构建插件 bundle
|
|
141
|
+
npm run dev # 启动壳(开发模式)
|
|
142
|
+
npm run pack # 打包 NSIS (Win) / DMG (mac)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 更新历史
|
|
146
|
+
|
|
147
|
+
### 0.1.4
|
|
148
|
+
- 分支二(插件市场分发)正式可用:`dsh plugin add` 装插件 → 重启 `dsh web` → 桌面壳自动弹出;Electron 运行时由插件自管理(本地复用 / 按网络环境自动选源下载)。
|
|
149
|
+
- 桌面快捷方式:首次启动询问创建 + 托盘「创建桌面快捷方式」一键添加(安装包与插件两种形态均支持)。
|
|
150
|
+
- 图标:Windows 任务栏 / macOS Dock 均显示鲸鱼图标(裸运行时场景)。
|
|
151
|
+
- 移除开机自启(两种形态统一为纯手动启动)。
|
|
152
|
+
|
|
153
|
+
### 0.1.2
|
|
154
|
+
- Windows 自动更新:托盘「检查更新」改为后台下载 + 进度显示 + 重启安装(electron-updater);macOS 仍为手动下载。
|
|
155
|
+
- 移除启动时自动拉起后端(改纯手动,与「关闭后端」不冲突)。
|
|
156
|
+
- 新增贡献者全套文件(CONTRIBUTING / 行为准则 / 安全策略 / Issue 与 PR 模板)。
|
|
157
|
+
- README:新增 Windows SmartScreen 安装指引;澄清「插件注册 ≠ 安装桌面应用」。
|
|
158
|
+
|
|
159
|
+
### 0.1.1
|
|
160
|
+
- 后端生命周期:修复 Windows 下启动报 `spawn EINVAL`、卡「启动中」的问题;「关闭后端」现在能真正停掉后端(含外部启动的实例);启动 / 重启 / 关闭带进度弹窗。
|
|
161
|
+
- 窗口可靠性:双击立即出窗;后端关闭窗口立刻切离线黑屏;后端恢复自动重连(Edge 式即时刷新)。
|
|
162
|
+
- 离线页自助:重新加载 / 启动后端 / 自动探测后端 / 设置后端安装文件夹。
|
|
163
|
+
- 托盘:新增「刷新窗口」;macOS 构建发布(Intel + Apple Silicon DMG)。
|
|
164
|
+
|
|
165
|
+
### 0.1.0
|
|
166
|
+
- 初始版本:Electron 壳骨架,系统托盘/单实例,DSH 插件挂载。
|
|
167
|
+
|
|
168
|
+
## 贡献
|
|
169
|
+
|
|
170
|
+
欢迎任何形式的贡献——修 bug、加功能、改进文档都行。请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)(含项目结构、开发约定、提交规范、PR 流程),并遵守 [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)。安全漏洞请走 [SECURITY.md](SECURITY.md) 的私密报告流程。
|
|
171
|
+
|
|
172
|
+
## 致谢
|
|
173
|
+
|
|
174
|
+
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —— 内核本体。
|
|
175
|
+
- 架构参考 [Hermes Agent Desktop](https://github.com/NousResearch/hermes-agent) 的壳/内核分离设计。
|
package/build/icon.ico
ADDED
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { app } from 'electron'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AppUserModelID, shared by main (process-level), window (taskbar button)
|
|
5
|
+
* and shortcut (Start-menu .lnk) so the three always agree.
|
|
6
|
+
*
|
|
7
|
+
* Why plugin mode gets its own ID: Windows caches the taskbar button icon
|
|
8
|
+
* per AUMID. If the plugin shell reuses the installer's ID that ever ran
|
|
9
|
+
* with Electron's default icon (an early dev launch / an old build), the
|
|
10
|
+
* taskbar keeps showing that stale icon — no window `icon`, `setAppDetails`
|
|
11
|
+
* or exe patch can override the cache. A distinct ID makes Windows treat it
|
|
12
|
+
* as a brand-new app and re-read the icon. It also keeps the plugin shell
|
|
13
|
+
* from merging with an installed copy on the same machine.
|
|
14
|
+
*/
|
|
15
|
+
export const APP_USER_MODEL_ID = app.isPackaged
|
|
16
|
+
? 'com.icather.dsh-clean-desktop-shell'
|
|
17
|
+
: 'com.icather.dsh-clean-desktop-shell.plugin'
|
package/electron/config.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* DSH address to run the shell as a pure window.
|
|
7
7
|
* - closeToTray: default true.
|
|
8
8
|
* - windowMode: 'advanced' (mica/vibrancy) | 'compatibility'.
|
|
9
|
-
* - autoLaunch: open at login.
|
|
10
9
|
*/
|
|
11
10
|
import { app } from 'electron'
|
|
12
11
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
@@ -16,8 +15,10 @@ const DEFAULTS = {
|
|
|
16
15
|
targetUrl: 'http://127.0.0.1:3080',
|
|
17
16
|
closeToTray: true,
|
|
18
17
|
windowMode: 'advanced',
|
|
19
|
-
autoLaunch: false,
|
|
20
18
|
backendPath: null,
|
|
19
|
+
// True once the first-run "create a desktop shortcut?" prompt was shown
|
|
20
|
+
// (so it never nags again). The tray item stays available regardless.
|
|
21
|
+
shortcutAsked: false,
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
let cached = null
|
package/electron/main.js
CHANGED
|
@@ -8,21 +8,46 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The main window is a pure shell — all backend controls live in the tray.
|
|
10
10
|
*/
|
|
11
|
-
import { app, BrowserWindow } from 'electron'
|
|
11
|
+
import { app, BrowserWindow, dialog, nativeImage } from 'electron'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
12
13
|
import { createMainWindow, reloadWindow } from './window.js'
|
|
13
14
|
import { createTray, refreshTrayMenu } from './tray.js'
|
|
14
15
|
import { loadConfig, saveConfig } from './config.js'
|
|
15
16
|
import { detect } from './service.js'
|
|
16
17
|
import { setupAutoUpdater } from './update.js'
|
|
18
|
+
import { shortcutSupported, hasDesktopShortcut, createDesktopShortcut, ensureStartMenuShortcut } from './shortcut.js'
|
|
19
|
+
import { APP_USER_MODEL_ID } from './aumid.js'
|
|
17
20
|
|
|
18
21
|
const isMac = process.platform === 'darwin'
|
|
19
22
|
|
|
20
23
|
// Windows: pin the AppUserModelId so the taskbar shows our whale icon
|
|
21
|
-
// instead of the generic Electron icon.
|
|
24
|
+
// instead of the generic Electron icon. Plugin mode uses a distinct ID
|
|
25
|
+
// (see aumid.js) so Windows re-reads the icon instead of serving a
|
|
26
|
+
// stale per-AUMID cached one.
|
|
22
27
|
if (process.platform === 'win32') {
|
|
23
|
-
app.setAppUserModelId(
|
|
28
|
+
app.setAppUserModelId(APP_USER_MODEL_ID)
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
// macOS: a bare runtime has no .app bundle (no icon resource), so set the
|
|
32
|
+
// Dock icon at runtime. Unlike Windows there is no per-AUMID taskbar cache
|
|
33
|
+
// here — app.dock.setIcon applies directly. Packaged builds already carry
|
|
34
|
+
// the icon in their bundle, so skip those.
|
|
35
|
+
if (isMac && !app.isPackaged) {
|
|
36
|
+
try {
|
|
37
|
+
const dockIcon = nativeImage.createFromPath(
|
|
38
|
+
fileURLToPath(new URL('../build/icon.png', import.meta.url)),
|
|
39
|
+
)
|
|
40
|
+
if (!dockIcon.isEmpty()) app.dock.setIcon(dockIcon)
|
|
41
|
+
} catch {
|
|
42
|
+
// non-fatal: keep the default icon
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Uniform userData across both distribution branches (installer vs
|
|
47
|
+
// plugin-market), so config (target URL, backend path, ...) is shared
|
|
48
|
+
// no matter how the shell was launched.
|
|
49
|
+
app.setName('DSH Clean Desktop Shell')
|
|
50
|
+
|
|
26
51
|
/** Single instance: a second launch just focuses the existing window. */
|
|
27
52
|
const gotLock = app.requestSingleInstanceLock()
|
|
28
53
|
if (!gotLock) {
|
|
@@ -73,6 +98,32 @@ if (!gotLock) {
|
|
|
73
98
|
refreshTrayMenu()
|
|
74
99
|
}
|
|
75
100
|
|
|
101
|
+
/**
|
|
102
|
+
* First-run desktop shortcut prompt (Windows packaged apps only).
|
|
103
|
+
* Asks once; the tray "create desktop shortcut" item stays available
|
|
104
|
+
* forever, so saying no is never a dead end.
|
|
105
|
+
*/
|
|
106
|
+
async function ensureShortcut() {
|
|
107
|
+
if (!shortcutSupported() || loadConfig().shortcutAsked) return
|
|
108
|
+
saveConfig({ ...loadConfig(), shortcutAsked: true })
|
|
109
|
+
if (await hasDesktopShortcut()) return
|
|
110
|
+
const choice = dialog.showMessageBoxSync({
|
|
111
|
+
type: 'question',
|
|
112
|
+
title: '创建桌面快捷方式?',
|
|
113
|
+
message: '是否在桌面创建「DSH Clean Desktop Shell」快捷方式?',
|
|
114
|
+
detail: '选择「跳过」也不影响使用——之后可随时在托盘右键菜单中一键添加。',
|
|
115
|
+
buttons: ['创建', '跳过'],
|
|
116
|
+
defaultId: 0,
|
|
117
|
+
cancelId: 1,
|
|
118
|
+
})
|
|
119
|
+
if (choice === 0) {
|
|
120
|
+
const ok = await createDesktopShortcut()
|
|
121
|
+
if (!ok) {
|
|
122
|
+
dialog.showErrorBox('创建快捷方式失败', '无法在桌面创建快捷方式。可稍后在托盘右键菜单中重试。')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
76
127
|
app.whenReady().then(async () => {
|
|
77
128
|
await createWindow()
|
|
78
129
|
tray = createTray({
|
|
@@ -81,11 +132,6 @@ if (!gotLock) {
|
|
|
81
132
|
mainWindow.show()
|
|
82
133
|
mainWindow.focus()
|
|
83
134
|
},
|
|
84
|
-
onToggleAutoStart: (enabled) => {
|
|
85
|
-
const config = loadConfig()
|
|
86
|
-
saveConfig({ ...config, autoLaunch: enabled })
|
|
87
|
-
app.setLoginItemSettings({ openAtLogin: enabled })
|
|
88
|
-
},
|
|
89
135
|
onReload: () => {
|
|
90
136
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
91
137
|
reloadWindow(mainWindow, loadConfig().targetUrl || 'http://127.0.0.1:3080')
|
|
@@ -103,6 +149,17 @@ if (!gotLock) {
|
|
|
103
149
|
// Windows: wire the auto-updater (downloads new installers silently).
|
|
104
150
|
setupAutoUpdater()
|
|
105
151
|
|
|
152
|
+
// Windows: ensure the AUMID-carrying Start-menu shortcut exists so the
|
|
153
|
+
// taskbar button shows our icon (see shortcut.js). Best-effort.
|
|
154
|
+
try {
|
|
155
|
+
ensureStartMenuShortcut()
|
|
156
|
+
} catch {
|
|
157
|
+
// non-fatal
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// First run: offer a desktop shortcut (never nags twice).
|
|
161
|
+
ensureShortcut().catch(() => {})
|
|
162
|
+
|
|
106
163
|
app.on('activate', () => {
|
|
107
164
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
|
108
165
|
else mainWindow?.show()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop shortcut management (Windows .lnk via WScript.Shell).
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - first-run prompt in main.js ("create a desktop shortcut?")
|
|
6
|
+
* - the tray "create desktop shortcut" item (always available)
|
|
7
|
+
*
|
|
8
|
+
* Works in both distribution branches:
|
|
9
|
+
* - installer (packaged): targets the installed exe, no arguments
|
|
10
|
+
* - plugin-market (bare runtime): targets the provisioned electron.exe
|
|
11
|
+
* with the plugin's electron/main.js as its argument
|
|
12
|
+
*
|
|
13
|
+
* Pure dev mode (npm run dev from a checkout) has no stable executable,
|
|
14
|
+
* so the feature stays disabled there.
|
|
15
|
+
*/
|
|
16
|
+
import { app, shell } from 'electron'
|
|
17
|
+
import { spawn } from 'node:child_process'
|
|
18
|
+
import { existsSync } from 'node:fs'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
import { APP_USER_MODEL_ID } from './aumid.js'
|
|
23
|
+
|
|
24
|
+
const isWin = process.platform === 'win32'
|
|
25
|
+
const SHORTCUT_NAME = 'DSH Clean Desktop Shell.lnk'
|
|
26
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
27
|
+
const ICON_ICO = join(PKG_ROOT, 'build', 'icon.ico')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Plugin-market mode: the host half spawns a bare electron runtime with the
|
|
31
|
+
* plugin's electron/main.js as its entry. process.execPath is the runtime
|
|
32
|
+
* electron.exe; process.argv[1] is the entry script (packaged installers
|
|
33
|
+
* have no such argument).
|
|
34
|
+
*/
|
|
35
|
+
function pluginArgs() {
|
|
36
|
+
if (app.isPackaged) return []
|
|
37
|
+
const entry = process.argv[1]
|
|
38
|
+
if (entry && /\.js$/i.test(entry)) return [entry]
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Shortcuts need a stable executable — supported when packaged or in
|
|
43
|
+
* plugin mode (where the runtime path is fixed under DSH_HOME). */
|
|
44
|
+
export function shortcutSupported() {
|
|
45
|
+
return isWin && (app.isPackaged || pluginArgs().length > 0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ps(str) {
|
|
49
|
+
return "'" + String(str).replace(/'/g, "''") + "'"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runPs(script) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const p = spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
})
|
|
57
|
+
let out = ''
|
|
58
|
+
p.stdout.on('data', (d) => {
|
|
59
|
+
out += d.toString()
|
|
60
|
+
})
|
|
61
|
+
p.on('error', () => resolve(null))
|
|
62
|
+
p.on('exit', () => resolve(out))
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** True when a desktop shortcut already points at this app's exe. */
|
|
67
|
+
export async function hasDesktopShortcut() {
|
|
68
|
+
if (!isWin) return false
|
|
69
|
+
const script =
|
|
70
|
+
`$ws = New-Object -ComObject WScript.Shell; ` +
|
|
71
|
+
`Get-ChildItem ${ps(join(homedir(), 'Desktop', '*.lnk'))} | ` +
|
|
72
|
+
`ForEach-Object { $ws.CreateShortcut($_.FullName).TargetPath }`
|
|
73
|
+
const out = await runPs(script)
|
|
74
|
+
if (out === null) return false
|
|
75
|
+
const target = process.execPath.toLowerCase()
|
|
76
|
+
return out
|
|
77
|
+
.split(/\r?\n/)
|
|
78
|
+
.map((s) => s.trim().toLowerCase())
|
|
79
|
+
.some((line) => line === target)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Create (or overwrite) the desktop shortcut for this app. Returns bool. */
|
|
83
|
+
export async function createDesktopShortcut() {
|
|
84
|
+
if (!shortcutSupported()) return false
|
|
85
|
+
const target = process.execPath
|
|
86
|
+
const args = pluginArgs()
|
|
87
|
+
const lnk = join(homedir(), 'Desktop', SHORTCUT_NAME)
|
|
88
|
+
// Plugin mode: point the shortcut at the runtime exe + main.js argument.
|
|
89
|
+
// WScript needs the path double-quoted inside the Arguments string.
|
|
90
|
+
const argPart = args.length
|
|
91
|
+
? `; $s.Arguments = ${ps(`"${args[0]}"`)}; `
|
|
92
|
+
: ''
|
|
93
|
+
// Plugin-mode exe has no custom icon resource — point the shortcut at the
|
|
94
|
+
// bundled .ico when present (packaged installers use the exe itself).
|
|
95
|
+
const iconLoc = existsSync(ICON_ICO) ? ICON_ICO : `${target},0`
|
|
96
|
+
const script =
|
|
97
|
+
`$ws = New-Object -ComObject WScript.Shell; ` +
|
|
98
|
+
`$s = $ws.CreateShortcut(${ps(lnk)}); ` +
|
|
99
|
+
`$s.TargetPath = ${ps(target)}; ` +
|
|
100
|
+
argPart +
|
|
101
|
+
`$s.WorkingDirectory = ${ps(dirname(target))}; ` +
|
|
102
|
+
`$s.IconLocation = ${ps(iconLoc)}; ` +
|
|
103
|
+
`$s.Save()`
|
|
104
|
+
const out = await runPs(script)
|
|
105
|
+
return out !== null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Ensure a Start-menu shortcut carrying the AppUserModelID. This is what
|
|
110
|
+
* makes the taskbar button show our icon: Windows matches a running
|
|
111
|
+
* window's AUMID to a shortcut's icon (bare runtime electron.exe has no
|
|
112
|
+
* icon resource of its own). Idempotent — the shortcut is only created
|
|
113
|
+
* when missing.
|
|
114
|
+
*/
|
|
115
|
+
export function ensureStartMenuShortcut() {
|
|
116
|
+
if (!isWin || !shortcutSupported()) return false
|
|
117
|
+
const lnkDir = join(
|
|
118
|
+
process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'),
|
|
119
|
+
'Microsoft', 'Windows', 'Start Menu', 'Programs',
|
|
120
|
+
)
|
|
121
|
+
const lnk = join(lnkDir, SHORTCUT_NAME)
|
|
122
|
+
const args = pluginArgs()
|
|
123
|
+
return shell.writeShortcutLink(lnk, 'create', {
|
|
124
|
+
target: process.execPath,
|
|
125
|
+
args: args.length ? args.join(' ') : undefined,
|
|
126
|
+
icon: existsSync(ICON_ICO) ? ICON_ICO : undefined,
|
|
127
|
+
iconIndex: 0,
|
|
128
|
+
appUserModelId: APP_USER_MODEL_ID,
|
|
129
|
+
})
|
|
130
|
+
}
|