dsh-clean-desktop-shell 0.1.2 → 0.1.3

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,169 @@
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
+ > **状态提示**:插件形式安装(方式二)施工中,预计今日凌晨上线。**当前仅支持方式一安装包安装**。
35
+
36
+ **方式一:从 Release 下载安装包(推荐普通用户,唯一完整的桌面应用安装方式)**
37
+
38
+ - Windows:下载 `DSH-Clean-Desktop-Shell-Setup-<版本>.exe`
39
+ - macOS:下载 `DSH-Clean-Desktop-Shell-<版本>.dmg`(Intel)或 `-arm64.dmg`(Apple Silicon)
40
+
41
+ 安装包会**自动创建桌面快捷方式**,并提供系统托盘等完整桌面体验。首次运行 Windows 安装包可能触发 SmartScreen 警告——**这是未签名程序的正常现象,不是病毒**,见下方「Windows SmartScreen 警告说明」。
42
+
43
+ **方式二:作为 DSH 插件注册(仅开发者/高级用户)——⚠️ 这个命令不会安装任何桌面应用**
44
+
45
+ ```sh
46
+ dsh plugin --profile web add dsh-clean-desktop-shell
47
+ ```
48
+
49
+ > **重要**:方式二只是把壳"登记"进 DSH profile,**你的电脑上不会多出任何桌面应用**——没有安装包、没有桌面图标、没有系统托盘。它只用于开发者把壳挂进 DSH 生态(复用 web profile 配置、供未来设置集成)。**想要可双击使用的桌面应用,请用方式一**(或源码 `npm run dev` 运行)。
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.2
148
+ - Windows 自动更新:托盘「检查更新」改为后台下载 + 进度显示 + 重启安装(electron-updater);macOS 仍为手动下载。
149
+ - 移除启动时自动拉起后端(改纯手动,与「关闭后端」不冲突)。
150
+ - 新增贡献者全套文件(CONTRIBUTING / 行为准则 / 安全策略 / Issue 与 PR 模板)。
151
+ - README:新增 Windows SmartScreen 安装指引;澄清「插件注册 ≠ 安装桌面应用」。
152
+
153
+ ### 0.1.1
154
+ - 后端生命周期:修复 Windows 下启动报 `spawn EINVAL`、卡「启动中」的问题;「关闭后端」现在能真正停掉后端(含外部启动的实例);启动 / 重启 / 关闭带进度弹窗。
155
+ - 窗口可靠性:双击立即出窗;后端关闭窗口立刻切离线黑屏;后端恢复自动重连(Edge 式即时刷新)。
156
+ - 离线页自助:重新加载 / 启动后端 / 自动探测后端 / 设置后端安装文件夹。
157
+ - 托盘:新增「刷新窗口」;macOS 构建发布(Intel + Apple Silicon DMG)。
158
+
159
+ ### 0.1.0
160
+ - 初始版本:Electron 壳骨架,系统托盘/单实例,DSH 插件挂载。
161
+
162
+ ## 贡献
163
+
164
+ 欢迎任何形式的贡献——修 bug、加功能、改进文档都行。请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)(含项目结构、开发约定、提交规范、PR 流程),并遵守 [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)。安全漏洞请走 [SECURITY.md](SECURITY.md) 的私密报告流程。
165
+
166
+ ## 致谢
167
+
168
+ - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —— 内核本体。
169
+ - 架构参考 [Hermes Agent Desktop](https://github.com/NousResearch/hermes-agent) 的壳/内核分离设计。
@@ -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,12 +8,13 @@
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 } from 'electron'
12
12
  import { createMainWindow, reloadWindow } from './window.js'
13
13
  import { createTray, refreshTrayMenu } from './tray.js'
14
14
  import { loadConfig, saveConfig } from './config.js'
15
15
  import { detect } from './service.js'
16
16
  import { setupAutoUpdater } from './update.js'
17
+ import { shortcutSupported, hasDesktopShortcut, createDesktopShortcut } from './shortcut.js'
17
18
 
18
19
  const isMac = process.platform === 'darwin'
19
20
 
@@ -23,6 +24,11 @@ if (process.platform === 'win32') {
23
24
  app.setAppUserModelId('com.icather.dsh-clean-desktop-shell')
24
25
  }
25
26
 
27
+ // Uniform userData across both distribution branches (installer vs
28
+ // plugin-market), so config (target URL, backend path, ...) is shared
29
+ // no matter how the shell was launched.
30
+ app.setName('DSH Clean Desktop Shell')
31
+
26
32
  /** Single instance: a second launch just focuses the existing window. */
27
33
  const gotLock = app.requestSingleInstanceLock()
28
34
  if (!gotLock) {
@@ -73,6 +79,32 @@ if (!gotLock) {
73
79
  refreshTrayMenu()
74
80
  }
75
81
 
82
+ /**
83
+ * First-run desktop shortcut prompt (Windows packaged apps only).
84
+ * Asks once; the tray "create desktop shortcut" item stays available
85
+ * forever, so saying no is never a dead end.
86
+ */
87
+ async function ensureShortcut() {
88
+ if (!shortcutSupported() || loadConfig().shortcutAsked) return
89
+ saveConfig({ ...loadConfig(), shortcutAsked: true })
90
+ if (await hasDesktopShortcut()) return
91
+ const choice = dialog.showMessageBoxSync({
92
+ type: 'question',
93
+ title: '创建桌面快捷方式?',
94
+ message: '是否在桌面创建「DSH Clean Desktop Shell」快捷方式?',
95
+ detail: '选择「跳过」也不影响使用——之后可随时在托盘右键菜单中一键添加。',
96
+ buttons: ['创建', '跳过'],
97
+ defaultId: 0,
98
+ cancelId: 1,
99
+ })
100
+ if (choice === 0) {
101
+ const ok = await createDesktopShortcut()
102
+ if (!ok) {
103
+ dialog.showErrorBox('创建快捷方式失败', '无法在桌面创建快捷方式。可稍后在托盘右键菜单中重试。')
104
+ }
105
+ }
106
+ }
107
+
76
108
  app.whenReady().then(async () => {
77
109
  await createWindow()
78
110
  tray = createTray({
@@ -81,11 +113,6 @@ if (!gotLock) {
81
113
  mainWindow.show()
82
114
  mainWindow.focus()
83
115
  },
84
- onToggleAutoStart: (enabled) => {
85
- const config = loadConfig()
86
- saveConfig({ ...config, autoLaunch: enabled })
87
- app.setLoginItemSettings({ openAtLogin: enabled })
88
- },
89
116
  onReload: () => {
90
117
  if (mainWindow && !mainWindow.isDestroyed()) {
91
118
  reloadWindow(mainWindow, loadConfig().targetUrl || 'http://127.0.0.1:3080')
@@ -103,6 +130,9 @@ if (!gotLock) {
103
130
  // Windows: wire the auto-updater (downloads new installers silently).
104
131
  setupAutoUpdater()
105
132
 
133
+ // First run: offer a desktop shortcut (never nags twice).
134
+ ensureShortcut().catch(() => {})
135
+
106
136
  app.on('activate', () => {
107
137
  if (BrowserWindow.getAllWindows().length === 0) createWindow()
108
138
  else mainWindow?.show()
@@ -0,0 +1,72 @@
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
+ * Only meaningful for packaged apps — in dev mode there is no stable
9
+ * executable to point the shortcut at, so the feature is disabled there.
10
+ */
11
+ import { app } from 'electron'
12
+ import { spawn } from 'node:child_process'
13
+ import { homedir } from 'node:os'
14
+ import { dirname, join } from 'node:path'
15
+
16
+ const isWin = process.platform === 'win32'
17
+ const SHORTCUT_NAME = 'DSH Clean Desktop Shell.lnk'
18
+
19
+ /** Shortcuts are a Windows packaged-app feature. */
20
+ export function shortcutSupported() {
21
+ return isWin && app.isPackaged
22
+ }
23
+
24
+ function ps(str) {
25
+ return "'" + String(str).replace(/'/g, "''") + "'"
26
+ }
27
+
28
+ function runPs(script) {
29
+ return new Promise((resolve) => {
30
+ const p = spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
31
+ windowsHide: true,
32
+ })
33
+ let out = ''
34
+ p.stdout.on('data', (d) => {
35
+ out += d.toString()
36
+ })
37
+ p.on('error', () => resolve(null))
38
+ p.on('exit', () => resolve(out))
39
+ })
40
+ }
41
+
42
+ /** True when a desktop shortcut already points at this app's exe. */
43
+ export async function hasDesktopShortcut() {
44
+ if (!isWin) return false
45
+ const script =
46
+ `$ws = New-Object -ComObject WScript.Shell; ` +
47
+ `Get-ChildItem ${ps(join(homedir(), 'Desktop', '*.lnk'))} | ` +
48
+ `ForEach-Object { $ws.CreateShortcut($_.FullName).TargetPath }`
49
+ const out = await runPs(script)
50
+ if (out === null) return false
51
+ const target = process.execPath.toLowerCase()
52
+ return out
53
+ .split(/\r?\n/)
54
+ .map((s) => s.trim().toLowerCase())
55
+ .some((line) => line === target)
56
+ }
57
+
58
+ /** Create (or overwrite) the desktop shortcut for this app. Returns bool. */
59
+ export async function createDesktopShortcut() {
60
+ if (!shortcutSupported()) return false
61
+ const target = process.execPath
62
+ const lnk = join(homedir(), 'Desktop', SHORTCUT_NAME)
63
+ const script =
64
+ `$ws = New-Object -ComObject WScript.Shell; ` +
65
+ `$s = $ws.CreateShortcut(${ps(lnk)}); ` +
66
+ `$s.TargetPath = ${ps(target)}; ` +
67
+ `$s.WorkingDirectory = ${ps(dirname(target))}; ` +
68
+ `$s.IconLocation = ${ps(`${target},0`)}; ` +
69
+ `$s.Save()`
70
+ const out = await runPs(script)
71
+ return out !== null
72
+ }
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, onToggleAutoStart, onReload, onQuit }) {
34
+ export function createTray({ onShow, onReload, onQuit }) {
34
35
  const icon = nativeImage.createFromPath(trayIconPath)
35
36
 
36
- handlers = { onShow, onToggleAutoStart, onReload, onQuit }
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, onToggleAutoStart, onReload, onQuit } = handlers
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)