dsh-power-menu-mac 0.0.0-stage → 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ 本项目遵循 [Semantic Versioning](https://semver.org/spec/v2.0.0.html),格式参考 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)。
4
+
5
+ ## [0.1.0] - 2026-10-26
6
+
7
+ 首个 macOS 版本。基于 [xichen-62/dsh-power-menu](https://github.com/xichen-62/dsh-power-menu) 0.1.18(MIT)移植。
8
+
9
+ ### Changed
10
+
11
+ - 宿主侧改成 POSIX:结束进程从 `taskkill /PID x /F` 改为 `SIGTERM`(必要时 `SIGKILL`);读进程表从 PowerShell `Get-CimInstance Win32_Process` 改为 `ps -A -o pid=,ppid=,comm=`;失败提示从 PowerShell 消息框改为 `osascript display alert`(不需要辅助功能授权)。
12
+ - 去掉 Windows 专属的「中文路径 cmd.exe 拷贝」自检,改成在「中文 + 空格」目录里启动一个中文名脚本。
13
+ - 接口前缀 `/api/dsh-power-menu/*` → `/api/dsh-power-menu-mac/*`;状态目录 `~/.dsh/dsh-power-menu/` → `~/.dsh/dsh-power-menu-mac/`,可与原版同时安装而不打架。
14
+ - 安装脚本从 403 行 PowerShell(`install.ps1` / `enable.ps1`)改为 POSIX shell(`install.sh` / `enable.sh`)。不再需要 MinGit、不改 PATH、不处理 junction;会自动去 `.app` 包里找 `dsh` CLI。
15
+ - 许可证保留原版权声明,并叠加本项目版权。
16
+
17
+ ### Fixed
18
+
19
+ - **macOS 上的孤儿清理其实没生效**:`ps` 分支原来只读 `pid` 和 `ppid`,不返回进程名,于是「只清理本应用自己的进程」那道按镜像名过滤的判断在 macOS 上等于没有 —— 会把 Electron 主进程的所有子进程都算进来。现在读 `comm` 取 basename,过滤真的生效。
20
+
21
+ ### Added
22
+
23
+ - **重启验证**:新进程必须注册到 macOS 窗口服务器(`lsappinfo find pid=<pid>`)并活过启动宽限期才算成功。原版在 macOS 上只能检查「进程还活着」,白屏或崩溃循环会被误判为成功。
24
+ - **平台门控**:`package.json` 声明 `os: ["darwin"]`;客户端在非 macOS 上直接不加载,不画一个点了没反应的按钮。
25
+
26
+ ## 上游历史
27
+
28
+ 0.1.18 及之前的所有版本属于 [xichen-62/dsh-power-menu](https://github.com/xichen-62/dsh-power-menu)(Windows 版),其更新日志见该仓库的 `CHANGELOG.md`。本仓库的版本号从 0.1.0 重新开始。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 暮迟
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,278 @@
1
- # Temporary Holding Version
1
+ # dsh-power-menu-mac
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 给 DeepSeek Harness 桌面版(**macOS**)加一个右上角电源按钮:点开只有 **关闭DSH** 和 **重启DSH** 两项,省得每次改完插件/配置还要去菜单栏里翻「退出」。
4
+
5
+ > macOS 专用 · 只支持 DSH Desktop · 纯本地 · 无外部依赖 · MIT
6
+ >
7
+ > 这是 [xichen-62/dsh-power-menu](https://github.com/xichen-62/dsh-power-menu)(Windows 版)的 **macOS 移植**,按 MIT 许可独立发布。原版的版权与思路保留在 [LICENSE](LICENSE) 和 [致谢](#致谢) 里。
8
+
9
+ ![电源菜单截图](docs/screenshot.png)
10
+
11
+ ## 为什么做这个
12
+
13
+ DSH Desktop 关窗口只是收进 Dock,真正退出或重启要走菜单栏;而改插件、改配置之后又经常需要重启一次。这个插件把这两步搬到会话标题栏右上角:一个电源键,两项操作,点完就走。
14
+
15
+ 原版只支持 Windows —— 它靠 `taskkill` 结束进程、靠 PowerShell 读进程表和窗口句柄。macOS 上这些都没有,所以这里把它们换成了 POSIX 的做法,并且**顺手补上了原版在 Mac 上做不到的那件事**(见下面「重启是怎么验证成功的」)。
16
+
17
+ ## 功能
18
+
19
+ - **右上角电源按钮**:位于会话标题栏图标行最右侧,顺序为「布局 / 侧栏 / 电源」。
20
+ - **空会话也显示**:欢迎页和对话内使用同一个挂载位置,透明无底色,不会破坏主题,也不会出现两个按钮。
21
+ - **两项操作**:白色圆角卡片、悬浮阴影、左侧线性图标 + 右侧文本,不塞多余入口。
22
+ - **关闭DSH**:结束整个 DeepSeek Harness 桌面应用(Electron 主进程 + Host),不重新启动。
23
+ - **重启DSH**:结束整个应用,等旧端口释放后用原可执行文件重新拉起,窗口自动回来。
24
+ - **重启会验证真的回来了**:新进程必须**注册到 macOS 窗口服务器**(`lsappinfo`)并活过启动宽限期,才算成功;只是「进程还在」不算。
25
+ - **键盘可用**:`↑` / `↓` / `Home` / `End` 在两项间移动,`Enter` / 空格执行,`Esc` 关闭,点菜单外部也能关。
26
+ - **执行反馈**:点击后整页盖一层半透明遮罩,显示「正在关闭DSH…」/「正在重启DSH…」。
27
+ - **纯本地**:没有外部依赖、没有遥测;接口只监听本机回环地址,并做同源校验。
28
+ - **非 macOS 不加载**:宿主半只说 POSIX,所以在别的系统上按钮**根本不会画出来**,而不是画出来点了没反应。
29
+
30
+ ## 安装
31
+
32
+ > **重要**:装完插件后,必须再运行一次 `enable.sh`(或直接用下面的 `install.sh`),把 `dsh-power-menu-mac` 补进 profile 的 `dsh.profile.bundles`,然后重启 DSH。少了这一步,按钮会 404 —— 这是 DSH 插件管理器只写 `dependencies`、不写 bundles 的已知行为,不是插件本身的问题。
33
+
34
+ ### 方式 A:一键脚本(推荐)
35
+
36
+ ```bash
37
+ git clone https://github.com/DDDMUC/dsh-power-menu-mac.git
38
+ cd dsh-power-menu-mac
39
+ ./install.sh
40
+ ```
41
+
42
+ 脚本会:找到 DSH 自带的 `dsh` CLI → 安装插件(有 `git` 用 `github:` 依赖,没有就下载 Release 里的 `.tgz`)→ 补齐 `dsh.profile.bundles` → 提示重启。
43
+
44
+ GitHub 直连慢或不通时:
45
+
46
+ ```bash
47
+ ./install.sh --mirror
48
+ ```
49
+
50
+ 完全离线:
51
+
52
+ ```bash
53
+ ./install.sh --tgz ./dsh-power-menu-mac-0.1.0.tgz
54
+ ```
55
+
56
+ > `dsh` 不在 `PATH` 上也没关系 —— 脚本会去 `.app` 包里找:
57
+ > `/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh`。
58
+
59
+ ### 方式 B:通过 DSH 插件管理器
60
+
61
+ 1. 打开 DSH Desktop 的 **插件 / 插件市场**。
62
+ 2. 选择 **添加插件**,输入:
63
+
64
+ ```text
65
+ github:DDDMUC/dsh-power-menu-mac
66
+ ```
67
+
68
+ 3. 安装完成后,补一次 bundles(这一步不能省):
69
+
70
+ ```bash
71
+ ./enable.sh
72
+ ```
73
+
74
+ 4. 重启 DSH,并在 **插件市场 → 已安装** 里确认 `dsh-power-menu-mac` 处于「启用中」。
75
+
76
+ ### 方式 C:命令行安装
77
+
78
+ ```bash
79
+ "/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh" \
80
+ plugin --profile desktop add github:DDDMUC/dsh-power-menu-mac
81
+ ./enable.sh
82
+ ```
83
+
84
+ ### 方式 D:本地开发安装
85
+
86
+ ```bash
87
+ git clone https://github.com/DDDMUC/dsh-power-menu-mac.git
88
+ ln -s "$PWD/dsh-power-menu-mac" ~/.dsh/profiles/desktop/node_modules/dsh-power-menu-mac
89
+ ```
90
+
91
+ 然后在 `~/.dsh/profiles/desktop/package.json` 里:
92
+
93
+ - `dependencies` 增加 `"dsh-power-menu-mac": "link:<你 clone 下来的目录>"`
94
+ - `dsh.profile.bundles` 增加 `"dsh-power-menu-mac"`
95
+
96
+ (或者直接跑 `./enable.sh desktop` 帮你改 bundles。)
97
+
98
+ 重启 DSH 生效。这个插件没有构建步骤,改完 `lib/` 里的文件重启即可。
99
+
100
+ ## 使用
101
+
102
+ 1. 在会话标题栏右上角点电源图标。
103
+ 2. 选择 **关闭DSH** 或 **重启DSH**。
104
+ 3. 等待遮罩出现:关闭会退出应用;重启会在几秒后把窗口拉回来。
105
+
106
+ | 动作 | 结果 | 需要手动重开? |
107
+ | --- | --- | --- |
108
+ | 关闭DSH | 结束整个桌面应用,不重新启动 | 是 |
109
+ | 重启DSH | 结束应用 → 等端口释放 → 重新拉起 → 验证窗口 | 否,窗口自动回来 |
110
+
111
+ > **注意**:点击即执行,没有二次确认。两个动作都会中断正在生成的回答、运行中的工具和后台任务。请在点之前保存手头的事情。
112
+
113
+ ## 工作原理
114
+
115
+ 桌面版的进程树大致是:
116
+
117
+ ```text
118
+ Electron 主进程
119
+ └── Host 子进程(dsh-desktop-host,ELECTRON_RUN_AS_NODE=1)
120
+ └── 本插件
121
+ ```
122
+
123
+ 插件跑在 Host 里,不能直接调用 Electron 的 `app.quit()` / `app.relaunch()`。所以 Host 只负责"交接",真正的进程切换交给一个分离的 helper:
124
+
125
+ ```mermaid
126
+ flowchart LR
127
+ UI[电源菜单] -->|POST| Host[Host 插件]
128
+ Host -->|写 handoff + spawn| Helper[detached helper]
129
+ Helper -->|SIGTERM → SIGKILL<br/>只对 Electron 主进程| Main[Electron 主进程]
130
+ Main -.->|IPC 断开| Host
131
+ Host -->|application.shutdown| Disk[(会话日志落盘)]
132
+ Helper -->|等端口释放 + 重新拉起| NewMain[新的 Electron 主进程]
133
+ ```
134
+
135
+ 具体步骤:
136
+
137
+ 1. Host 把交接信息写到 `~/.dsh/dsh-power-menu-mac/`,用 `process.execPath` + `ELECTRON_RUN_AS_NODE=1` 启动 `lib/helper.cjs`。
138
+ 2. helper 等 1.2 秒,让 HTTP 应答先到浏览器。
139
+ 3. helper 对 Electron 主进程发 `SIGTERM`(让它有机会收尾),必要时 `SIGKILL`。**只对主进程**,不碰它的子树。
140
+ 4. Host 检测到断开后走自己的 `application.shutdown`,尽量把会话日志正常落盘。
141
+ 5. 重启模式下,helper 等旧监听端口释放,清掉 `ELECTRON_RUN_AS_NODE` / `DSH_DESKTOP_NODE_EXECUTABLE`,用原可执行文件重新拉起,**并验证新进程真的注册到了窗口服务器**;失败会重试,仍失败就弹窗提示手动打开。
142
+ 6. 关闭模式下不拉起,等 Host 自己退出。
143
+
144
+ ### 重启是怎么验证成功的
145
+
146
+ 这是 macOS 版和原版行为差别最大的地方。
147
+
148
+ 原版在 Windows 上用 `Get-Process` 读 `MainWindowHandle` / `MainWindowTitle`,能真的看到窗口,还能认出「应用无法启动」这类崩溃对话框。macOS 上作者没有对应手段,只能退化成**"进程还活着就算成功"** —— 白屏、卡在启动、崩溃循环都会被误判成成功。
149
+
150
+ 这里的做法是查窗口服务器:
151
+
152
+ ```bash
153
+ lsappinfo find pid=<新进程>
154
+ ```
155
+
156
+ - 有输出 → 这个进程**已经作为 GUI 应用注册到窗口服务器**,有界面了。
157
+ - 空输出 → 进程在跑,但从来没连上窗口服务器(崩溃循环、headless 重启)。
158
+ - 之后再要求它活过启动宽限期(单实例锁意味着第二个实例会立刻退出,活下来就说明是它)。
159
+
160
+ `lsappinfo` 是 macOS 自带、**不需要任何权限**。另一条路(`osascript` + System Events 查窗口)需要辅助功能授权,会在用户面前弹一个同意框 —— 而这个框恰好出现在"应用刚重启失败"的时候,非常糟糕,所以没用。
161
+
162
+ ### 接口
163
+
164
+ Host 半注册四个仅限本机回环的接口:
165
+
166
+ ```text
167
+ POST /api/dsh-power-menu-mac/shutdown
168
+ POST /api/dsh-power-menu-mac/restart
169
+ GET /api/dsh-power-menu-mac/health
170
+ GET /api/dsh-power-menu-mac/plan
171
+ ```
172
+
173
+ ## 安全
174
+
175
+ - 四个接口只接受回环地址(`127.0.0.1` / `::1`),校验 `Host` 必须是本机,拒绝 `sec-fetch-site: cross-site`,带 `Origin` 时必须与本机同源。
176
+ - 没有鉴权令牌的第三方网页无法跨站触发关机/重启。
177
+ - 只支持桌面版;`dsh web` 下接口返回 409,不会去杀别的进程。
178
+ - 关机/重启时只清理**本应用自己的进程**(Electron 主进程的渲染/GPU/工具子进程)。宿主(`dsh` host)在 DSH 终端里被启动的其它子进程——第二个 `dsh web` 实例、dev server、构建任务——**默认不碰**;需要旧行为时设 `DSH_POWER_MENU_KILL_HOST_CHILDREN=1`。
179
+ - 重启会沿用当前实例的启动参数(按 home 读取 `<DSH_HOME>/power-menu-relaunch-args.json`),所以用 `--user-data-dir` 启动的隔离 home 不会在重启后掉回默认 Electron profile。
180
+ - 不收集任何数据,不联网;日志只写在本地 `~/.dsh/dsh-power-menu-mac/`。
181
+
182
+ ## 兼容性
183
+
184
+ - **系统**:macOS。插件依赖 `ps` / `kill` / `lsappinfo`,Windows 和 Linux 未实现(`package.json` 里 `os: ["darwin"]`,装到别的系统上会直接拒绝)。
185
+ - **DSH**:在 **DSH Desktop 0.2.1-rc.3** 上实测通过(2026-10-26)。
186
+ - **Node**:跟随 DSH 自带的 Node(22+),无需单独安装。
187
+
188
+ ## 开发
189
+
190
+ ```text
191
+ dsh-power-menu-mac/
192
+ ├── lib/
193
+ │ ├── index.js Host 半:四个回环接口 + 交接调度
194
+ │ ├── client.js 浏览器半:window.__ModuleLoader__ bundle
195
+ │ └── helper.cjs 分离的交接 helper
196
+ ├── install.sh 一键安装
197
+ ├── enable.sh 补 dsh.profile.bundles
198
+ ├── cordis.patch.yml bundle 插入层
199
+ ├── package.json
200
+ ├── docs/screenshot.png
201
+ └── README.md
202
+ ```
203
+
204
+ - 客户端 bundle 不是普通 ESM,必须用 `window.__ModuleLoader__.load({ id, factory })` 格式;`dsh.client.inject` 声明 `@deepseek-ai/dsh-client-ui-slots`。
205
+ - 没有构建步骤,也没有运行时 npm 依赖。
206
+ - 提交前跑一次静态检查:
207
+
208
+ ```bash
209
+ npm run check
210
+ ```
211
+
212
+ - 本地调试日志:`~/.dsh/dsh-power-menu-mac/power-menu.log` 和 `helper-*.log`。
213
+
214
+ ## 常见问题
215
+
216
+ **按钮没出现?**
217
+ 确认插件处于「启用中」,并重启过一次 DSH;按钮只出现在有会话标题栏的页面。如果是在 Windows 上,按钮**本来就不该出现** —— 请用原版 [dsh-power-menu](https://github.com/xichen-62/dsh-power-menu)。
218
+
219
+ **点了没反应 / 报 409?**
220
+ `dsh web`(浏览器里的 Web 版)不支持,只支持桌面版。看 `~/.dsh/dsh-power-menu-mac/` 下的日志。
221
+
222
+ **重启后端口变了?**
223
+ 正常。DSH Desktop 每次启动用随机端口(`--port 0`),插件只负责拉起新实例,不假设端口不变。
224
+
225
+ **DSH 装在中文路径 / 用户名含中文能用吗?**
226
+ 可以。插件不拼 shell 字符串:启动用 `spawn(exe, [], { cwd })`,路径作为单个 argv 交给 Node 的 Unicode API;交接 JSON 和日志按 UTF-8 读写。helper 自带 `--pathtest`,会在「中文 + 空格」的临时目录里实测 UTF-8 读写和从中文路径启动脚本。
227
+
228
+ **装完点按钮报 `HTTP 404`?**
229
+ 说明宿主半没加载:插件只在 `dependencies` 里,不在 `dsh.profile.bundles` 里。跑 `./enable.sh`,或手动把 `dsh-power-menu-mac` 加进 profile 的 `dsh.profile.bundles`,再重启 DSH。
230
+
231
+ **为什么按钮不挂真实 slot?**
232
+ DSH 的 `conversation.session.header.corner` 是单占用槽,已经被侧栏开关占用;`conversation.session.header.utilities` 会排在侧栏左侧;`sidebar.footer.action` 在左下角。要在不修改 DSH 的前提下保持右上角,只能把按钮挂进 `headerCorner` 的 DOM。代价是上游改头部结构时可能失效;插件用 `MutationObserver` 跟随,不轮询。
233
+
234
+ **关闭/重启会跳过 profile 收尾吗?**
235
+ 会有一点:插件只结束 Electron 主进程,Host 会走 `application.shutdown`,但主进程的 `before-quit` 可能被跳过,Chromium 的 Cookie / Local Storage 不保证落盘。追求最干净的退出请用菜单栏的「退出」。真正的 `app.quit()` 需要 DSH 提供协作退出 IPC,插件单方面做不到。
236
+
237
+ ## 卸载
238
+
239
+ - 插件管理器里移除 `dsh-power-menu-mac`,或者从 profile 的 `dependencies` / `dsh.profile.bundles` 里删掉,再删掉 `node_modules` 里的链接。
240
+ - 可选:删除 `~/.dsh/dsh-power-menu-mac/` 下的日志与交接文件。
241
+ - 重启 DSH 生效。
242
+
243
+ ## 更新日志
244
+
245
+ 见 [CHANGELOG.md](CHANGELOG.md)。
246
+
247
+ ## 许可证
248
+
249
+ [MIT](LICENSE) © 2026 DDDMUC(macOS 移植)· © 2026 xichen-62(原版 dsh-power-menu)
250
+
251
+ ## 致谢
252
+
253
+ - 原版 [xichen-62/dsh-power-menu](https://github.com/xichen-62/dsh-power-menu)(MIT):整体结构、客户端界面、菜单交互、安全边界都来自它,本项目只是把宿主侧的进程操作换成了 POSIX 的做法。截图也沿用原版。
254
+ - 分离 helper 做进程交接、只结束主进程让 Host 优雅退出的思路,原版参考了 [dsh-desktop-restart](https://github.com/luoshuizhiwei/dsh-desktop-restart)(MIT)。
255
+ - 菜单样式参考 Windows 电源菜单;图标为内联 SVG,无第三方图标库。
256
+
257
+ ---
258
+
259
+ ## English
260
+
261
+ A macOS port of [dsh-power-menu](https://github.com/xichen-62/dsh-power-menu) (MIT). Adds a power button to the top-right of the DSH Desktop session header with exactly two actions:
262
+
263
+ - **关闭DSH** — quit the whole desktop app (no relaunch).
264
+ - **重启DSH** — quit and relaunch the app, so the window comes back automatically.
265
+
266
+ The Windows build drove `taskkill` and PowerShell; this one drives POSIX `ps` / `kill` / `lsappinfo`. The meaningful difference: a restart is only reported as successful once the new process has **registered with the macOS window server** (`lsappinfo find pid=…`) and survived its startup grace — "the process is still alive" is not accepted as proof, which is all the original could check on macOS.
267
+
268
+ macOS only (`os: ["darwin"]`), DSH Desktop only. No telemetry, no external dependencies; the four HTTP routes are loopback-only and same-origin fenced.
269
+
270
+ ```bash
271
+ git clone https://github.com/DDDMUC/dsh-power-menu-mac.git
272
+ cd dsh-power-menu-mac
273
+ ./install.sh
274
+ ```
275
+
276
+ Or add `github:DDDMUC/dsh-power-menu-mac` in the DSH plugin manager, then run `./enable.sh` and restart DSH.
277
+
278
+ MIT licensed, retaining the original copyright.
@@ -0,0 +1,8 @@
1
+ # dsh-power-menu-mac:bundle patch 层。
2
+ # 安装到 profile 后,这一行把插件挂进组合配置。
3
+ #
4
+ # 和原版 `dsh-power-menu` 用的是不同的 id,两个包可以同时装在同一个 profile 里
5
+ # 而不会互相覆盖 —— 但它们都会往会话头部挂一个电源按钮,所以实际上只应该启用一个。
6
+ - insert:
7
+ - id: dsh-power-menu-mac
8
+ name: dsh-power-menu-mac
Binary file
package/enable.sh ADDED
@@ -0,0 +1,125 @@
1
+ #!/bin/sh
2
+ # dsh-power-menu-mac 一键启用脚本(macOS)
3
+ #
4
+ # 用途:DSH 插件管理器安装后只写 dependencies,不一定会把插件加进
5
+ # `dsh.profile.bundles`;少了这一条,宿主半不加载,点按钮就会 HTTP 404。
6
+ # 这个脚本只做一件事:把 dsh-power-menu-mac 补进指定 profile 的 bundles,
7
+ # 写之前备份 profile 的 package.json。
8
+ #
9
+ # 用法:
10
+ # ./enable.sh # 默认 desktop profile
11
+ # ./enable.sh web # 指定 profile
12
+ #
13
+ # 然后重启 DSH。
14
+ #
15
+ # 实现说明:原版(PowerShell)用正则往 JSON 文本里插一段字符串,只为了保留
16
+ # 原文件排版。macOS 这边直接用 node 或 python3 解析再写回 —— 同样的 2 空格
17
+ # 缩进,但不会因为格式差异把文件写坏。两个都没有时才退回文本插入。
18
+
19
+ set -eu
20
+
21
+ PROFILE="${1:-desktop}"
22
+ PLUGIN="dsh-power-menu-mac"
23
+ PKG="$HOME/.dsh/profiles/$PROFILE/package.json"
24
+
25
+ if [ ! -f "$PKG" ]; then
26
+ echo "找不到 DSH profile 的 package.json:$PKG" >&2
27
+ echo "可用的 profile:" >&2
28
+ ls -1 "$HOME/.dsh/profiles" 2>/dev/null >&2 || echo " (~/.dsh/profiles 不存在)" >&2
29
+ exit 1
30
+ fi
31
+
32
+ # 退出码 0 = 写好了,2 = 本来就有,1 = 失败。
33
+ with_node() {
34
+ node - "$PKG" "$PLUGIN" <<'JS'
35
+ const fs = require('node:fs')
36
+ const [pkg, plugin] = process.argv.slice(2)
37
+ const data = JSON.parse(fs.readFileSync(pkg, 'utf8'))
38
+ data.dsh = data.dsh ?? {}
39
+ data.dsh.profile = data.dsh.profile ?? {}
40
+ const bundles = (data.dsh.profile.bundles = data.dsh.profile.bundles ?? [])
41
+ if (bundles.includes(plugin)) process.exit(2)
42
+ bundles.push(plugin)
43
+ fs.copyFileSync(pkg, `${pkg}.bak-${plugin}`)
44
+ fs.writeFileSync(pkg, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
45
+ JS
46
+ }
47
+
48
+ with_python() {
49
+ python3 - "$PKG" "$PLUGIN" <<'PY'
50
+ import json, shutil, sys
51
+ pkg, plugin = sys.argv[1], sys.argv[2]
52
+ with open(pkg, encoding='utf-8') as fh:
53
+ data = json.load(fh)
54
+ profile = data.setdefault('dsh', {}).setdefault('profile', {})
55
+ bundles = profile.setdefault('bundles', [])
56
+ if plugin in bundles:
57
+ sys.exit(2)
58
+ bundles.append(plugin)
59
+ shutil.copyfile(pkg, pkg + '.bak-' + plugin)
60
+ with open(pkg, 'w', encoding='utf-8') as fh:
61
+ json.dump(data, fh, ensure_ascii=False, indent=2)
62
+ fh.write('\n')
63
+ PY
64
+ }
65
+
66
+ # 兜底:纯文本插入。只在既没有 node 也没有 python3 时用。
67
+ #
68
+ # 必须先判断文件里到底有没有 bundles 这一项,再决定往哪儿插:逐行扫描时
69
+ # `"profile": {` 出现在 `"bundles"` 之前,如果边扫边插就会先插出一个重复的
70
+ # bundles 键 —— JSON 解析取最后一个,于是新加的那条被静默丢掉。
71
+ with_sed() {
72
+ if grep -q "\"$PLUGIN\"" "$PKG"; then return 2; fi
73
+ cp "$PKG" "$PKG.bak-$PLUGIN"
74
+ if grep -q '"bundles"' "$PKG"; then
75
+ awk -v plugin="$PLUGIN" '
76
+ BEGIN { done = 0 }
77
+ {
78
+ if (!done && match($0, /"bundles"[[:space:]]*:[[:space:]]*\[[[:space:]]*\]/)) {
79
+ sub(/\[[[:space:]]*\]/, "[\"" plugin "\"]")
80
+ print; done = 1; next
81
+ }
82
+ if (!done && match($0, /"bundles"[[:space:]]*:[[:space:]]*\[/)) {
83
+ print; print " \"" plugin "\","; done = 1; next
84
+ }
85
+ print
86
+ }
87
+ ' "$PKG" > "$PKG.tmp" && mv "$PKG.tmp" "$PKG"
88
+ else
89
+ awk -v plugin="$PLUGIN" '
90
+ BEGIN { done = 0 }
91
+ {
92
+ print
93
+ if (!done && match($0, /"profile"[[:space:]]*:[[:space:]]*\{/)) {
94
+ print " \"bundles\": [\"" plugin "\"],"; done = 1
95
+ }
96
+ }
97
+ ' "$PKG" > "$PKG.tmp" && mv "$PKG.tmp" "$PKG"
98
+ fi
99
+ }
100
+
101
+ status=1
102
+ if command -v node >/dev/null 2>&1; then
103
+ set +e; with_node; status=$?; set -e
104
+ elif command -v python3 >/dev/null 2>&1; then
105
+ set +e; with_python; status=$?; set -e
106
+ else
107
+ echo "[$PLUGIN] 没找到 node 或 python3,改用文本插入(请检查结果)。"
108
+ set +e; with_sed; status=$?; set -e
109
+ fi
110
+
111
+ case "$status" in
112
+ 0)
113
+ echo "[$PLUGIN] 已加入 dsh.profile.bundles。"
114
+ echo "原文件备份:$PKG.bak-$PLUGIN"
115
+ echo "请重启 DSH 让宿主半加载。"
116
+ ;;
117
+ 2)
118
+ echo "[$PLUGIN] 已经在 dsh.profile.bundles 里。"
119
+ echo "如果按钮仍然 404,请确认 dependencies 里有 ${PLUGIN},并重启 DSH。"
120
+ ;;
121
+ *)
122
+ echo "[$PLUGIN] 写入失败。请手动把 \"$PLUGIN\" 加进 $PKG 的 dsh.profile.bundles。" >&2
123
+ exit 1
124
+ ;;
125
+ esac
package/install.sh ADDED
@@ -0,0 +1,180 @@
1
+ #!/bin/sh
2
+ # dsh-power-menu-mac 一键安装脚本(macOS)
3
+ #
4
+ # 把 dsh-power-menu-mac 装进 DSH Desktop 的 profile,并补齐
5
+ # `dsh.profile.bundles`,让宿主半在重启后真正加载。
6
+ #
7
+ # 安装方式(默认自动选择):
8
+ # - 机器上有 git:用 `github:DDDMUC/dsh-power-menu-mac`(pnpm 的 git 依赖)
9
+ # - 没有 git,或 --tarball:下载 GitHub Release 里的 .tgz,再用本地文件安装,
10
+ # 全程不调用 git
11
+ #
12
+ # 用法:
13
+ # ./install.sh
14
+ # ./install.sh --profile desktop
15
+ # ./install.sh --tarball # 强制走 tgz
16
+ # ./install.sh --tgz ./dsh-power-menu-mac-0.1.0.tgz # 完全离线
17
+ # ./install.sh --sha256 <hash> # 手动指定安装包 SHA-256
18
+ # ./install.sh --mirror # GitHub 直连不通时改走 gh-proxy
19
+ # ./install.sh --version 0.1.0 # 指定要下载的 Release 版本
20
+ #
21
+ # 装完请重启 DSH Desktop。
22
+ #
23
+ # 和原版(Windows)的区别:这里不需要 MinGit、不需要改 PATH、不需要处理
24
+ # junction —— macOS 自带 /usr/bin/git(Xcode CLT),link 安装就是普通符号链接。
25
+ # 真正的坑只有一个:dsh 不在 PATH 上,它在 .app 包里。
26
+
27
+ set -eu
28
+
29
+ PROFILE=desktop
30
+ VERSION=0.1.0
31
+ REPO=DDDMUC/dsh-power-menu-mac
32
+ PLUGIN=dsh-power-menu-mac
33
+ MODE=auto
34
+ MIRROR=0
35
+ TGZ_PATH=
36
+ SHA256=
37
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
38
+
39
+ usage() {
40
+ sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'
41
+ }
42
+
43
+ while [ $# -gt 0 ]; do
44
+ case "$1" in
45
+ --profile) PROFILE="${2:?--profile 需要参数}"; shift 2 ;;
46
+ --version) VERSION="${2:?--version 需要参数}"; shift 2 ;;
47
+ --tgz) TGZ_PATH="${2:?--tgz 需要参数}"; MODE=tarball; shift 2 ;;
48
+ --sha256) SHA256="${2:?--sha256 需要参数}"; shift 2 ;;
49
+ --git) MODE=git; shift ;;
50
+ --tarball) MODE=tarball; shift ;;
51
+ --mirror) MIRROR=1; shift ;;
52
+ -h|--help) usage; exit 0 ;;
53
+ *) echo "未知参数:$1(--help 看用法)" >&2; exit 1 ;;
54
+ esac
55
+ done
56
+
57
+ say() { printf '[%s] %s\n' "$PLUGIN" "$*"; }
58
+ warn() { printf '[%s] %s\n' "$PLUGIN" "$*" >&2; }
59
+
60
+ # ---------------------------------------------------------------- dsh 命令行
61
+ find_dsh() {
62
+ if command -v dsh >/dev/null 2>&1; then
63
+ command -v dsh
64
+ return 0
65
+ fi
66
+ for candidate in \
67
+ "/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh" \
68
+ "$HOME/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh" \
69
+ "/Applications/DeepSeekHarness.app/Contents/Resources/runtime/cli/bin/dsh"
70
+ do
71
+ if [ -x "$candidate" ]; then
72
+ printf '%s\n' "$candidate"
73
+ return 0
74
+ fi
75
+ done
76
+ return 1
77
+ }
78
+
79
+ DSH=$(find_dsh) || {
80
+ warn "没找到 dsh 命令行。"
81
+ warn "它通常在 /Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
82
+ warn "装上 DSH Desktop 后再跑一次,或用下面的命令手动安装:"
83
+ warn " <dsh> plugin --profile $PROFILE add github:$REPO"
84
+ exit 1
85
+ }
86
+ say "dsh: $DSH"
87
+
88
+ # ---------------------------------------------------------------- SHA-256
89
+ sha256_of() {
90
+ shasum -a 256 "$1" | awk '{print $1}'
91
+ }
92
+
93
+ verify_sha256() {
94
+ file=$1
95
+ expected=$(printf '%s' "$SHA256" | tr '[:upper:]' '[:lower:]' | sed 's/^sha256://')
96
+ if [ -z "$expected" ]; then
97
+ # 没有显式给 hash:尝试 Release 里的 SHA256SUMS.txt。取不到只是警告,
98
+ # 不静默通过 —— 用户能从这条输出里看到"这次没校验"。
99
+ sums=$(mktemp -t dsh-power-menu-mac-sums)
100
+ url=$(release_url SHA256SUMS.txt)
101
+ if curl -fsSL "$url" -o "$sums" 2>/dev/null; then
102
+ expected=$(awk -v f="dsh-power-menu-mac-$VERSION.tgz" '$2 == f || $2 == "*"f { print $1; exit }' "$sums" | tr '[:upper:]' '[:lower:]')
103
+ fi
104
+ rm -f "$sums"
105
+ if [ -z "$expected" ]; then
106
+ warn "没有取到 SHA256SUMS.txt,跳过自动校验;可用 --sha256 手动指定。"
107
+ return 0
108
+ fi
109
+ fi
110
+ actual=$(sha256_of "$file")
111
+ if [ "$actual" != "$expected" ]; then
112
+ rm -f "$file"
113
+ warn "SHA-256 不匹配:期望 ${expected},实际 ${actual}。安装包已删除。"
114
+ exit 1
115
+ fi
116
+ say "SHA-256 校验通过。"
117
+ }
118
+
119
+ # ---------------------------------------------------------------- 下载地址
120
+ release_url() {
121
+ name=$1
122
+ base="https://github.com/$REPO/releases/download/v$VERSION/$name"
123
+ if [ "$MIRROR" = "1" ]; then
124
+ printf 'https://gh-proxy.com/%s\n' "$base"
125
+ else
126
+ printf '%s\n' "$base"
127
+ fi
128
+ }
129
+
130
+ # ---------------------------------------------------------------- 安装
131
+ install_from_git() {
132
+ say "用 git 依赖安装:github:$REPO"
133
+ "$DSH" plugin --profile "$PROFILE" add "github:$REPO"
134
+ }
135
+
136
+ install_from_tarball() {
137
+ if [ -n "$TGZ_PATH" ]; then
138
+ file=$TGZ_PATH
139
+ [ -f "$file" ] || { warn "找不到安装包:$file"; exit 1; }
140
+ say "用本地安装包:$file"
141
+ else
142
+ file=$(mktemp -t "$PLUGIN-$VERSION")
143
+ file="$file.tgz"
144
+ url=$(release_url "dsh-power-menu-mac-$VERSION.tgz")
145
+ say "下载 $url"
146
+ curl -fL --retry 2 --connect-timeout 15 "$url" -o "$file" || {
147
+ warn "下载失败。GitHub 直连不通时可以加 --mirror,或手动下载后用 --tgz 指定。"
148
+ exit 1
149
+ }
150
+ fi
151
+ verify_sha256 "$file"
152
+ "$DSH" plugin --profile "$PROFILE" add "$file"
153
+ }
154
+
155
+ case "$MODE" in
156
+ git) install_from_git ;;
157
+ tarball) install_from_tarball ;;
158
+ auto)
159
+ if command -v git >/dev/null 2>&1; then
160
+ install_from_git || {
161
+ warn "git 方式失败,改用 tgz 重试。"
162
+ install_from_tarball
163
+ }
164
+ else
165
+ install_from_tarball
166
+ fi
167
+ ;;
168
+ esac
169
+
170
+ # ---------------------------------------------------------------- 补 bundles
171
+ # 插件管理器只写 dependencies,不写 dsh.profile.bundles;少了这一步,宿主半
172
+ # 不加载,点按钮就是 HTTP 404。这一步和 enable.sh 是同一件事。
173
+ if [ -x "$SCRIPT_DIR/enable.sh" ]; then
174
+ "$SCRIPT_DIR/enable.sh" "$PROFILE"
175
+ else
176
+ warn "没找到 enable.sh,请手动把 \"$PLUGIN\" 加进 profile 的 dsh.profile.bundles。"
177
+ warn "profile: $HOME/.dsh/profiles/$PROFILE/package.json"
178
+ fi
179
+
180
+ say "完成。请重启 DSH Desktop。"