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.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
- [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS-0078D6?logo=windows&logoColor=white)](https://github.com/Icather/dsh-clean-desktop-shell)
12
- [![License](https://img.shields.io/badge/License-MIT-22c55e)](LICENSE)
13
- [![Release](https://img.shields.io/github/v/release/Icather/dsh-clean-desktop-shell?color=blue)](https://github.com/Icather/dsh-clean-desktop-shell/releases/latest)
14
- [![DSH](https://img.shields.io/badge/DeepSeek_Harness-rc.7-4D6BFE)](https://github.com/deepseek-ai/deepseek-harness)
15
- [![Contributors](https://img.shields.io/github/contributors/Icather/dsh-clean-desktop-shell?color=blueviolet)](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
- ┌──────────────────── 内核(dsh web / headless 服务) ────────────────────┐
36
- │ 会话 · Agent · 插件 · 记忆 都在这层,与界面解耦 │
37
- └─────────────────────────────────────────────────────────────────────────┘
38
-
39
- ┌───────────────┴───────────────┐
40
- │ dsh-clean-desktop-shell │
41
- │ Electron 壳(客户端) │
42
- │ 托盘 · 单实例 · 开机自启 │
43
- └────────────────────────────────┘
44
- ```
45
-
46
- - **默认**:加载本地 `127.0.0.1:3080`(已配置好的 web profile,零迁移)。
47
- - **可配远程**:在设置里填入任意远程 DSH 地址,壳只当窗口——手机 / Linux / 其他设备通过浏览器或 PWA 也能接入内核,壳本身不绑定本地服务。
48
-
49
- ### 平台矩阵
50
-
51
- | 平台 | | 状态 |
52
- |:--|:--|:--|
53
- | Windows | ✅ Electron(无边框 + 原生窗口按钮) | 已发布(NSIS 安装包) |
54
- | macOS | ✅ Electron(hiddenInset) | 已发布(CI 构建 Intel + Apple Silicon DMG) |
55
- | Linux | —(浏览器 / PWA 直连内核) | 不做 |
56
- | Termux / 手机 / 平板 | —(headless / PWA 直连内核) | 由内核远程访问支持 |
57
-
58
- ## 安装
59
-
60
- **方式一:从 Release 下载安装包(推荐普通用户)**
61
-
62
- - Windows:下载 `DSH-Clean-Desktop-Shell-Setup-<版本>.exe`
63
- - macOS:下载 `DSH-Clean-Desktop-Shell-<版本>.dmg`(Intel)或 `-arm64.dmg`(Apple Silicon)
64
-
65
- 首次运行 Windows 安装包可能触发 SmartScreen 警告——**这是未签名程序的正常现象,不是病毒**,见下方「Windows SmartScreen 警告说明」。
66
-
67
- **方式二:作为 DSH 插件安装(开发者)**
68
-
69
- ```sh
70
- dsh plugin --profile web add dsh-clean-desktop-shell
71
- ```
72
-
73
- 重启 `dsh web` 后,从系统托盘/快捷方式启动桌面壳即可。
74
-
75
- > 桌面壳需要本机有可用的 `dsh web` 服务(或配置的远程地址)。见下方「使用」。
76
-
77
- ### Windows SmartScreen 警告说明
78
-
79
- **为什么会看到警告?**
80
-
81
- 我们的安装包**没有代码签名证书**(个人开源项目暂未购买,证书年费约数百美元)。Windows 的 Microsoft Defender SmartScreen 是一个**信誉系统**——它根据"下载量 + 干净运行的记录"判断一个程序是否可信。对下载量少、未签名的 exe,它无法确认信誉,就会警告。**这不代表文件有病毒**:本项目完全开源,代码可审阅,也可自行校验 SHA-256(见下)。
82
-
83
- **Edge 下载时会看到:**
84
-
85
- 下载面板里该文件被标记为"不常下载的文件",需要手动保留:
86
-
87
- 1. 悬停下载项,点击右侧的 `...` 菜单
88
- 2. 选择「保留」(Keep)
89
- 3. 弹窗确认,选择「仍要保留」(Keep anyway)
90
-
91
- **双击安装时会看到:**
92
-
93
- 蓝色对话框「Windows 已保护你的电脑」——Microsoft Defender SmartScreen 阻止了无法识别的应用启动:
94
-
95
- 1. 点击「更多信息」(More info)
96
- 2. 核对文件名确实是 `DSH-Clean-Desktop-Shell-Setup-<版本>.exe`
97
- 3. 点击「仍要运行」(Run anyway)
98
-
99
- **备选:一次性解除锁定(推荐)**
100
-
101
- 右键安装包 属性 → 常规 → 底部勾选「解除锁定」→ 确定。之后双击不再有警告。
102
-
103
- 或 PowerShell 批量解除:
104
-
105
- ```powershell
106
- Unblock-File -Path "$env:USERPROFILE\Downloads\DSH-Clean-Desktop-Shell-Setup-*.exe"
107
- ```
108
-
109
- **关于文件安全性的说明**
110
-
111
- 安装包由 GitHub Actions 从本仓库源码自动构建(见 `.github/workflows/build.yml`),代码完全开源可审阅。如仍有疑虑,可自行 `git clone` 后按「开发」一节本地构建比对,或稍等下载量积累——SmartScreen 信誉度上去后警告会自动消失。
112
-
113
- > 说明:代码签名证书(EV Azure Trusted Signing)可以彻底消除这个警告,但需要付费且对个人开源维护者不划算。本仓库会在条件允许时考虑接入签名。
114
-
115
- ## 使用
116
-
117
- 1. 启动 `dsh web`(或配置远程服务地址)。
118
- 2. 启动桌面壳:自动检测本地 3080;未运行时按配置拉起服务。
119
- 3. 窗口顶部(右侧留系统按钮)可拖动窗口;关闭默认缩到托盘。
120
-
121
- **后端的一切操作都在托盘右键**,主窗口保持纯壳:
122
-
123
- - 启动 / 重启 / 关闭后端(带进度弹窗;关闭会真正停掉 3080 上的服务,包括外部启动的实例)
124
- - 自动探测后端 · 设置后端安装文件夹(默认自动探测定位)
125
- - 刷新窗口 · 检查更新 · 仓库主页 · 开机自启
126
-
127
- **窗口的可靠性(Edge 式即时刷新)**:
128
-
129
- - 双击启动立即出窗,不等后端就绪
130
- - 后端没起来时显示「后端未连接」页,自动探测;后端一通立即加载
131
- - 后端关闭 / 被杀的一刻,窗口立刻切回离线页——不会停在旧页面假装还活着
132
- - 离线页内置快捷按钮:重新加载 / 启动后端 / 自动探测后端 / 设置后端安装文件夹
133
-
134
- ## 开发
135
-
136
- ```sh
137
- npm install
138
- npm run build # 构建插件 bundle
139
- npm run dev # 启动壳(开发模式)
140
- npm run pack # 打包 NSIS (Win) / DMG (mac)
141
- ```
142
-
143
- ## 更新历史
144
-
145
- ### 0.1.1
146
- - 后端生命周期:修复 Windows 下启动报 `spawn EINVAL`、卡「启动中」的问题;「关闭后端」现在能真正停掉后端(含外部启动的实例);启动 / 重启 / 关闭带进度弹窗。
147
- - 窗口可靠性:双击立即出窗;后端关闭窗口立刻切离线黑屏;后端恢复自动重连(Edge 式即时刷新)。
148
- - 离线页自助:重新加载 / 启动后端 / 自动探测后端 / 设置后端安装文件夹。
149
- - 托盘:新增「刷新窗口」;macOS 构建发布(Intel + Apple Silicon DMG)。
150
-
151
- ### 0.1.0
152
- - 初始版本:Electron 壳骨架,系统托盘/单实例/开机自启,DSH 插件挂载。
153
-
154
- ## 贡献
155
-
156
- 欢迎任何形式的贡献——修 bug、加功能、改进文档都行。请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)(含项目结构、开发约定、提交规范、PR 流程),并遵守 [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)。安全漏洞请走 [SECURITY.md](SECURITY.md) 的私密报告流程。
157
-
158
- ## 致谢
159
-
160
- - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —— 内核本体。
161
- - 架构参考 [Hermes Agent Desktop](https://github.com/NousResearch/hermes-agent) 的壳/内核分离设计。
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
+ [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS-0078D6?logo=windows&logoColor=white)](https://github.com/Icather/dsh-clean-desktop-shell)
12
+ [![License](https://img.shields.io/badge/License-MIT-22c55e)](LICENSE)
13
+ [![Release](https://img.shields.io/github/v/release/Icather/dsh-clean-desktop-shell?color=blue)](https://github.com/Icather/dsh-clean-desktop-shell/releases/latest)
14
+ [![DSH](https://img.shields.io/badge/DeepSeek_Harness-rc.7-4D6BFE)](https://github.com/deepseek-ai/deepseek-harness)
15
+ [![Contributors](https://img.shields.io/github/contributors/Icather/dsh-clean-desktop-shell?color=blueviolet)](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'
@@ -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('com.icather.dsh-clean-desktop-shell')
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
+ }