🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

电源菜单 DDDMUC/dsh-power-menu-mac

DeepSeek Harnessv0.1.2
DDDMUC2 天前有提交已提供中文文档扫描:含敏感能力

DSH 桌面版macOS右上角电源按钮:关闭 / 重启 DSH。纯 macOS 移植版MIT,对应 Windows…

36.4/100
综合分
2
GitHub 星标
628
npm 周下载
—/5
用户评分

数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-power-menu-mac
npm 源 · dsh-power-menu-mac 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · desktop
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 2 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-power-menu-mac @ 0.1.2
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/30 21:12:17

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件
查看扫描证据(共 59 处,展示前 35 条)
  • 读取本地文件
    lib/index.js:52
    if (!existsSync(root)) return null
  • 读取本地文件
    lib/index.js:56
    top = readdirSync(root, { withFileTypes: true })
  • 读取本地文件
    lib/index.js:70
    for (const inner of readdirSync(join(root, entry.name), { withFileTypes: true })) {
  • 读取本地文件
    lib/index.js:82
    pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
  • 读取本地文件
    lib/index.js:90
    stat = statSync(join(dir, rel))
  • 写入本地文件
    lib/index.js:146
    mkdirSync(STATE_DIR, { recursive: true })
  • 写入本地文件
    lib/index.js:147
    appendFileSync(LOG_PATH, `${new Date().toISOString()} ${line}\n`, 'utf8')
  • 执行外部命令固定命令
    lib/index.js:181
    const comm = execFileSync('/bin/ps', ['-o', 'comm=', '-p', String(mainPid)], {
  • 执行外部命令固定命令
    lib/index.js:226
    const out = execFileSync('/bin/launchctl', ['print', `gui/${String(process.getuid?.() ?? 501)}/${label}`], {
  • 读取本地文件
    lib/index.js:379
    existed = existsSync(CONFIG_PATH)
  • 写入本地文件
    lib/index.js:398
    mkdirSync(STATE_DIR, { recursive: true })
  • 写入本地文件
    lib/index.js:399
    writeFileSync(
  • 写入本地文件
    lib/index.js:464
    mkdirSync(STATE_DIR, { recursive: true })
  • 写入本地文件
    lib/index.js:495
    writeFileSync(handoffPath, JSON.stringify(handoff, null, 2), 'utf8')
  • 执行外部命令来自配置/变量
    lib/index.js:498
    const helper = spawn(process.execPath, [HELPER, handoffPath], {
  • 读取本地文件
    lib/helper.cjs:80
    if (!fs.existsSync(file)) continue
  • 读取本地文件
    lib/helper.cjs:81
    const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
  • 读取本地文件
    lib/helper.cjs:132
    if (!fs.existsSync(db)) {
  • 执行外部命令固定命令
    lib/helper.cjs:143
    const left = execFileSync('/usr/bin/sqlite3', [db, sql], { encoding: 'utf8', timeout: 8000 }).trim()
  • 读取本地文件
    lib/helper.cjs:158
    if (fs.statSync(file).size > MAX_LOG_BYTES) {
  • 删除本地文件
    lib/helper.cjs:159
    try { fs.rmSync(`${file}.1`, { force: true }) } catch { }
  • 写入本地文件
    lib/helper.cjs:160
    fs.renameSync(file, `${file}.1`)
  • 写入本地文件
    lib/helper.cjs:171
    fs.appendFileSync(LOG_PATH, `[${new Date().toISOString()}] ${line}\n`, 'utf8')
  • 写入本地文件
    lib/helper.cjs:359
    fs.writeFileSync(jsonPath, JSON.stringify(payload), 'utf8')
  • 读取本地文件
    lib/helper.cjs:360
    const roundTrip = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
  • 写入本地文件
    lib/helper.cjs:366
    fs.writeFileSync(scriptPath, '#!/bin/sh\nexit 0\n', 'utf8')
  • 写入本地文件
    lib/helper.cjs:367
    fs.chmodSync(scriptPath, 0o755)
  • 执行外部命令来自配置/变量
    lib/helper.cjs:369
    const child = spawn(scriptPath, [], {
  • 删除本地文件
    lib/helper.cjs:386
    fs.rmSync(dir, { recursive: true, force: true })
  • 执行外部命令固定命令
    lib/helper.cjs:434
    const child = spawn('/usr/bin/osascript', ['-e', script], { detached: true, stdio: 'ignore' })
  • 执行外部命令固定命令
    lib/helper.cjs:487
    const out = execFileSync('/usr/bin/lsappinfo', ['find', `pid=${pid}`], { encoding: 'utf8', timeout: 2000 })
  • 执行外部命令固定命令
    lib/helper.cjs:538
    const out = execFileSync('/usr/bin/plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', plist], {
  • 执行外部命令固定命令
    lib/helper.cjs:561
    const out = execFileSync('/usr/bin/lsappinfo', ['find', `bundleid=${bundleId}`], {
  • 读取本地文件
    lib/helper.cjs:686
    if (!fs.existsSync(plist)) continue
  • 删除本地文件
    lib/helper.cjs:1126
    fs.unlinkSync(handoffPath)
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-client-ui-slots
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
展开完整介绍(90 字)↓

DSH 桌面版(macOS)右上角电源按钮:关闭 / 重启 DSH。纯 macOS 移植版(MIT),对应 Windows 原版 xichen-62/dsh-power-menu。

v0.1.42026-11-01
电源菜单现在也能用在 dsh web 部署上。 · 新增 · 支持 dsh web。之前只认桌面版,web 上按钮要么不出现、要么点了 409。现在目标换成 host 自己: - 关闭:有 launchd 作业就 launchctl bootout(真关掉,不会被 KeepAlive 立刻拉回来);没有就直接结束进程。 - 重启:有 launchd 就 kickstart -k 交给它;没有就自己杀掉、等端口释放、用原来的 argv 和工作目录重新 spawn。 - 两种重启都会从新进程输出里抓 dsh web: 并自动打开浏览器——每次启动 token 都会换,不自动打开的话你只能自己去日志里翻。无头部署可设 DSH_POWER_MENU_NO_OPEN=1。 - GET /health 返回 {desktop, web, supervised};web + 有 supervisor 时,「现在关闭,N 分钟后启动」会收起(杀掉会被立刻拉回来,helper 再 spawn 会抢端口)。 - 定时、倒计时胶囊、配置页、刷新保险这些在 web 上照旧。 · 修复 · 桌面版和 dsh web 跑的是同一个 cli.js:原来只看 ELECTRON_RUN_AS_NODE 和父进程 pid,从终端起 dsh web 时会把那个 shell 当成 Electron 主进程,一点重启就把终端杀了。现在认 DSH_DESKTOP_NODE_EXECUTABLE 并比对父进程可执行文件。 · XPC_SERVICE_NAME 会被继承,不能拿它判断"被 launchd 管着";现在回查 launchctl print 里那个作业的 pid 是不是本进程。 · launchd plist 在真实用户目录,而 web 部署常改 HOME;原来会找错地方导致 supervised 重启抓不到新链接。改用 os.userInfo().homedir 优先。 · 实测 · 无 supervisor(临时端口):kill → 端口释放 → 重新拉起 → 抓 URL,1.6 秒。 · 有 launchd(fish2 实机):kickstart → 2.5 秒抓到新链接 → 自动打开。 安装与启用见 README。
v0.1.32026-11-01
菜单从两项变成四项,另加一个定时弹窗、一个配置页,和一道防止「刷新把插件打挂」的保险。 · 新增 · 刷新:只重载渲染层,应用和 Host 都不动。之前想刷新只能重启,而重启一次就多下发一条 dsh-auth- cookie。 · 刷新的保险:点刷新前先比对 profile 里各插件 exports["./client"] 的 mtime:size(宿主新增 GET /bundles)。有文件变过就弹框列出改动的插件,给「重启 / 仍然刷新 / 取消」三个选择——DSH 的 client bundle 按 revision 发布且不可变,旧 revision 会被直接拒绝,刷新换来的是一整页「请关闭应用并重新安装」。宿主判断不了时不拦。 · 定时:居中弹窗,填分钟数后三选一——「N 分钟后关闭」「N 分钟后重启」「现在关闭,N 分钟后启动」。等待期间应用照常可用;到点前若应用被手动关掉或重启过,这条定时按 pid 比对自动作废。排定后按钮下方出现每秒跳的倒计时胶囊,右侧 × 直接取消。 · 配置页:插件管理页里这一行多了配置控件,可以改默认分钟数、开关提示词注入、替换提示词正文。 · 系统提示词注入:告诉模型「DSH 的重启与关闭由用户自己点电源菜单」,不再询问。只在桌面宿主渲染,可在配置页关掉或换掉。 · 按钮在所有页面都能用:会话页仍在标题栏最右侧;面板页(插件、设置这类)固定在窗口右上角。 · 变更 · 菜单项按破坏性从小到大排:刷新 → 重启 → 定时 → 关闭,误触最多落到刷新。 · 菜单收窄到约 100px,四个标签都是两个字。 · 修复 · 立即操作如果 30 秒还没把应用带走,自动放开 pending 闩并记日志,不再让之后每一次操作都 409。 · 定时到点确认应用还在时,改用 LaunchServices 拿 pid(bundle id → ASN → pid),失败再退到两级 pgrep。原来只用精确锚点,遇到 argv[0] 写成 …/Contents/Resources/../MacOS/ 的启动方式会一个都匹配不上,定时被误判成「应用已经不在跑」而直接放弃。 · 宿主半边在非 macOS 上直接跳过,避免同一个 profile 里同时装着 Windows 版时提示词被注入两遍。 安装与启用见 README。
v0.1.22026-10-29
修一个会让桌面版直接起不来的坑。 · 修复 · 重启 / 关闭时把攒下来的 dsh-auth-* cookie 删到只剩最近 3 条。宿主每次启动都下发一个新名字的 dsh-auth- cookie 且从不清理,cookie 又不区分端口,所有宿主共用同一批;攒到 71 条时 Cookie 请求头 16.1 KB,越过 Node 默认的 16 KB 上限,宿主回 431,桌面壳报 desktop welcome: Web authentication failed。用电源菜单反复重启会加速触发。 · 清理在 app 退出之后、重启之前进行(app 关着才安全),走 macOS 自带的 /usr/bin/sqlite3;保留最近 3 条,避免把同机另一个长期运行的 dsh web 界面当场登出。 安装与启用见 README。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-power-menu-mac 给 DeepSeek Harness 桌面版(macOS)加一个右上角电源按钮:点开有 刷新、重启、定时、关闭 四项,省得每次改完插件/配置还要去菜单栏里翻「退出」——多数时候你要的只是刷新,不必重启。 macOS 专用 · 只支持 DSH Desktop · 纯本地 · 无外部依赖 · MIT 这是 xichen-62/dsh-power-menu(Windows 版)的 macOS 移植,按 MIT 许可独立发布。原版的版权与思路保留在 LICENSE 和 致谢 里。 电源菜单截图 为什么做这个 DSH Desktop 关窗口只是收进 Dock,真正退出或重启要走菜单栏;而改插件、改配置之后又经常需要重启一次。这个插件把这两步搬到会话标题栏右上角:一个电源键,两项操作,点完就走。 原版只支持 Windows —— 它靠 taskkill 结束进程、靠 PowerShell 读进程表和窗口句柄。macOS 上这些都没有,所以这里把它们换成了 POSIX 的做法,并且顺手补上了原版在 Mac 上做不到的那件事(见下面「重启是怎么验证成功的」)。 功能 - 右上角电源按钮:会话页里位于标题栏图标行最右侧(顺序为「布局 / 侧栏 / 电源」);插件、自动化任务这类没有会话标题栏的页面,按钮改为固定在窗口右上角,换页不消失。
展开完整文档(README 剩余 1 节)
dsh-power-menu-mac

给 DeepSeek Harness 桌面版(macOS)加一个右上角电源按钮:点开有 刷新、重启、定时、关闭 四项,省得每次改完插件/配置还要去菜单栏里翻「退出」——多数时候你要的只是刷新,不必重启。

macOS 专用 · 只支持 DSH Desktop · 纯本地 · 无外部依赖 · MIT

这是 xichen-62/dsh-power-menu(Windows 版)的 macOS 移植,按 MIT 许可独立发布。原版的版权与思路保留在 LICENSE 和 致谢 里。

电源菜单截图

为什么做这个

DSH Desktop 关窗口只是收进 Dock,真正退出或重启要走菜单栏;而改插件、改配置之后又经常需要重启一次。这个插件把这两步搬到会话标题栏右上角:一个电源键,两项操作,点完就走。

原版只支持 Windows —— 它靠 taskkill 结束进程、靠 PowerShell 读进程表和窗口句柄。macOS 上这些都没有,所以这里把它们换成了 POSIX 的做法,并且顺手补上了原版在 Mac 上做不到的那件事(见下面「重启是怎么验证成功的」)。

功能

- 右上角电源按钮:会话页里位于标题栏图标行最右侧(顺序为「布局 / 侧栏 / 电源」);插件、自动化任务这类没有会话标题栏的页面,按钮改为固定在窗口右上角,换页不消失。
- 空会话也显示:欢迎页和对话内使用同一个挂载位置,透明无底色,不会破坏主题,也不会出现两个按钮。
- 四项操作:白色圆角卡片、悬浮阴影、左侧线性图标 + 右侧文本,不塞多余入口;按破坏性从小到大排,误触最多落到「刷新」。
- 刷新:只重载这一层页面,应用和 Host 都不动。点之前会先比对插件文件:如果某个 client.js 在你打开页面之后被改过,刷新会让它(及同组的条目)加载失败,这时会弹框列出改动的插件,建议改走重启。
- 重启:结束整个应用,等旧端口释放后用原可执行文件重新拉起,窗口自动回来。
- 定时:点开一个居中的弹窗,先填分钟数,再从三种里选一种 ——「N 分钟后关闭」(到点才关,期间照常用)、「N 分钟后重启」(到点关掉再开)、「现在关闭,N 分钟后启动」(立刻关,过一会儿自己回来)。弹窗会显示换算出的执行时刻,打开时焦点已在输入框里、直接打字就能改;有定时在等的时候,弹窗里多一行「取消已设定的定时」。Esc 或点背景关闭。排定之后按钮正下方出现一个倒计时胶囊(「3:43 后关闭」,右侧一个 × 直接取消),每秒跳;电源按钮右上角另有一个小圆点,菜单打开时胶囊让位、圆点还在。页面刷新后靠 /config 恢复,不用重新设。
- 关闭:结束整个 DeepSeek Harness 桌面应用(Electron 主进程 + Host),不重新启动。
- 重启会验证真的回来了:新进程必须注册到 macOS 窗口服务器(lsappinfo)并活过启动宽限期,才算成功;只是「进程还在」不算。
- 键盘可用:↑ / ↓ / Home / End 在四项间移动,Enter / 空格执行,Esc 关闭,点菜单外部也能关。
- 执行反馈:点击后整页盖一层半透明遮罩,显示「正在关闭DSH…」/「正在重启DSH…」。
- 有配置页:插件管理页里这一行带一个配置控件,打开就是自己的页面(#/panel/plugins/dsh-power-menu-mac/dsh-power-menu-mac)。分钟数、提示词开关、自定义文本都在里面,改完点保存才写入。
- 顺带管住模型的嘴:插件会向系统提示词注入一句「DSH 的重启与关闭由用户自己通过电源菜单完成,不要询问也不要主动提议」。装了电源菜单的人自己两下就点完了,模型再问一轮只是废话。只在桌面宿主生效——dsh web 上没有这个按钮,那段话会整段丢弃,不会给出错误指引。
- 纯本地:没有外部依赖、没有遥测;接口只监听本机回环地址,并做同源校验。
- 非 macOS 不加载:宿主半只说 POSIX,所以在别的系统上按钮根本不会画出来,而不是画出来点了没反应。

安装

重要:装完插件后,必须再运行一次 enable.sh(或直接用下面的 install.sh),把 dsh-power-menu-mac 补进 profile 的 dsh.profile.bundles,然后重启 DSH。少了这一步,按钮会 404 —— 这是 DSH 插件管理器只写 dependencies、不写 bundles 的已知行为,不是插件本身的问题。

方式 A:一键脚本(推荐)

git clone https://github.com/DDDMUC/dsh-power-menu-mac.git
cd dsh-power-menu-mac
./install.sh

脚本会:找到 DSH 自带的 dsh CLI → 安装插件(有 git 用 github: 依赖,没有就下载 Release 里的 .tgz)→ 补齐 dsh.profile.bundles → 提示重启。

GitHub 直连慢或不通时:

./install.sh --mirror

完全离线:

./install.sh --tgz ./dsh-power-menu-mac-0.1.0.tgz

dsh 不在 PATH 上也没关系 —— 脚本会去 .app 包里找:
/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh。

方式 B:通过 DSH 插件管理器

1. 打开 DSH Desktop 的 插件 / 插件市场。
2. 选择 添加插件,输入:

github:DDDMUC/dsh-power-menu-mac

3. 安装完成后,补一次 bundles(这一步不能省):

./enable.sh

4. 重启 DSH,并在 插件市场 → 已安装 里确认 dsh-power-menu-mac 处于「启用中」。

方式 C:命令行安装

bash
"/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh" \
plugin --profile desktop add github:DDDMUC/dsh-power-menu-mac
./enable.sh

方式 D:本地开发安装 bash
git clone https://github.com/DDDMUC/dsh-power-menu-mac.git
ln -s "$PWD/dsh-power-menu-mac" ~/.dsh/profiles/desktop/node_modules/dsh-power-menu-mac

然后在 ~/.dsh/profiles/desktop/package.json 里:

- dependencies 增加 "dsh-power-menu-mac": "link:<你 clone 下来的目录>"
- dsh.profile.bundles 增加 "dsh-power-menu-mac"

(或者直接跑 ./enable.sh desktop 帮你改 bundles。)

重启 DSH 生效。这个插件没有构建步骤,改完 lib/ 里的文件重启即可。

使用

1. 在会话标题栏右上角点电源图标。
2. 选择 刷新、重启、定时 或 关闭。
3. 刷新会立刻重载界面;关闭会退出应用;重启会在几秒后把窗口拉回来(后两者才有遮罩)。

| 动作 | 结果 | 需要手动重开? |
| 刷新 | 只重载渲染层,应用与 Host 继续运行 | 否,界面自己回来 |
| 重启 | 结束应用 → 等端口释放 → 重新拉起 → 验证窗口 | 否,窗口自动回来 |
| N 分钟后关闭 | 什么都不做 → 到点优雅关闭 → 清 cookie | 是 |
| N 分钟后重启 | 什么都不做 → 到点关闭 → 清 cookie → 重新拉起 | 否,到点自己回来 |
| 现在关闭,N 分钟后启动 | 立刻关闭 → 清 cookie → 等 N 分钟 → 重新拉起 | 否,到点自己回来 |
| 关闭 | 结束整个桌面应用,不重新启动 | 是 |

注意:点击即执行,没有二次确认。重启和关闭都会中断正在生成的回答、运行中的工具和后台任务,请在点之前保存手头的事情;刷新不会中断任务,它只重载界面,后台照常跑。

改完插件代码要重启,别用刷新。 刷新只重载页面,不会重新加载插件的客户端 bundle。而 DSH 的 bundle 是按 revision 发布的——磁盘上的文件一变,宿主算出的 revision 就跟页面手里那个对不上,请求会被直接拒绝(dsh-client-modules 的原话:Versioned code is immutable; mismatched revisions are rejected instead of serving newer bytes),表现为整组插件一起 bundle script … failed to load。刷新按钮是给「没改代码、只想重载界面」用的。

报 bundle script … failed to load 但你没改过本插件? 先看崩溃日志里的 renderer console 段。整组插件是拼成一个 加载的,任何一个插件的 client.js 有语法错误都会让全组一起挂,报错会落在别人的文件上(例如 dsh-url-router/client.js:3217 Uncaught SyntaxError),本插件只是被连坐。核对办法:对 ~/.dsh/profiles/desktop/node_modules 下每个 lib/client.js 跑一遍 node --check,坏的那个会直接报行号。常见成因是文件正被写入时恰好启动/刷新。

刷新的保险

DSH 的客户端 bundle 是「版本化且不可变」的:revision 由文件元数据(mtime / ctime / size)算出,页面手里那个旧 revision 在文件被改写之后会被宿主直接拒绝。表现就是 bundle script … failed to load,整页变成「如果应用文件缺失或损坏,请关闭应用并重新安装」。

所以「刷新」现在会先做一次判断:

点刷新 ├─ 插件文件没变 → 正常重载 └─ 有文件改过 → 弹框,列出是哪些插件,给三个选择:
· 重启DSH(推荐)
· 仍然刷新(这些插件会挂)
· 取消

判断依据是宿主新增的 GET /api/dsh-power-menu-mac/bundles:它扫一遍 profile 的 node_modules,给出每个插件 exports["./client"] 对应文件的 mtime:size。页面挂载时取一次快照,点刷新时再取一次做比对——不重建 revision,只回答"有没有变过"。

两个刻意的降级:

- 宿主拿不到 profile 目录(process.cwd() 下没有 node_modules)→ 返回 supported: false,前端不拦。判断不了的时候拦下来,比不拦更烦人。
- 请求失败、超时、字段缺失 → 同样不拦,直接刷新。

在 dsh web 上用

同一个包也支持 dsh web 部署(不是桌面版)。差别只有"目标是谁":

| | 桌面版 | dsh web |
| 目标进程 | Electron 主进程 | host 自己 |
| 关闭 | 结束主进程,Host 优雅退出 | 有 launchd 作业 → bootout(真关);没有 → 结束进程 |
| 重启 | 杀掉 → 等端口 → 拉起 → 等窗口 | 有 launchd → kickstart -k;没有 → 杀掉 → 用原 argv 拉起 |
| 就绪判据 | 主窗口出现 | 端口在听 + 输出里有 dsh web: 行 |
| cookie 修剪 | 需要 | 用不上(cookie 在用户浏览器里) |

重启后链接会变:dsh web 每次启动都生成新 token,所以插件会从新进程的输出里抓那条 dsh web: http://…?token=…,然后 open 到默认浏览器。无头部署可以设 DSH_POWER_MENU_NO_OPEN=1 关掉自动打开。
web 上的 helper 状态和日志在那个部署自己的 HOME 下:$HOME/.dsh/dsh-power-menu-mac/。

配置

在插件管理页里改:插件列表 → dsh-power-menu-mac → 组件行右侧的配置控件 → #/panel/plugins/dsh-power-menu-mac/dsh-power-menu-mac。里面有分钟数、提示词开关和自定义文本框,点「保存」才写入。

也可以直接改文件 ~/.dsh/dsh-power-menu-mac/config.json,第一次读取时会自动生成: json { "delayedRestartMinutes": 10, "restartGuidance": true, "restartGuidanceText": "" }

| 字段 | 作用 | 合法值 |
| delayedRestartMinutes | 定时弹窗输入框的默认分钟数 | 1–1440 的整数;小数、0、负数、超过 1440、字符串一律回落 10 |
| restartGuidance | 是否注入下面那段系统提示词 | 只有显式 false 才关;缺省或写错类型都当开 |
| restartGuidanceText | 提示词正文 | 界面里预填默认那句;与默认一字不差时存为空串(=继续跟随默认,以后默认改了能跟上);超过 2000 字截断 |

改完打开一次菜单就生效(提示词是下一个请求生效),都不用重启应用。弹窗里每次都能临时改数字,delayedRestartMinutes 只决定默认值。

老版本的配置文件只有 delayedRestartMinutes,缺的两个字段会走默认行为,不需要迁移。

注入的系统提示词

本插件声明了 systemPrompt 依赖,并在 apply 里注册一段全局提示词:

DSH 桌面应用的重启与关闭由用户自己通过界面右上角的电源菜单完成。
不要询问用户是否需要重启或关闭 DSH,也不要主动提议这样做。

三点说明:

- 只在桌面宿主渲染。text 是个函数,非桌面宿主(dsh web)返回空串,空段落会被整段丢掉——那里没有电源菜单,说"用户自己点"是错误指引。
- 范围刻意写窄。没有写成「不要问用户」,那样会连带压住改文件、跑命令之类的正常询问。
- 它是全局的。所有会话、所有 preset 都会看到。哪天你想让模型帮你重启,这句会压住它——用 dsh plugin 停用本插件即可恢复。
- 可以关,也可以换掉。config.json 里 restartGuidance: false 就不注入;restartGuidanceText 填一段自己的话就替换掉默认文本(上限 2000 字,超了截断并在日志里记一笔)。改完下一个请求就生效。

工作原理

桌面版的进程树大致是: text Electron 主进程 └── Host 子进程(dsh-desktop-host,ELECTRON_RUN_AS_NODE=1) └── 本插件

插件跑在 Host 里,不能直接调用 Electron 的 app.quit() / app.relaunch()。所以 Host 只负责"交接",真正的进程切换交给一个分离的 helper: mermaid
flowchart LR
UI[电源菜单] -->|POST| Host[Host 插件]
Host -->|写 handoff + spawn| Helper[detached helper]
Helper -->|SIGTERM → SIGKILL只对 Electron 主进程| Main[Electron 主进程]
Main -.->|IPC 断开| Host
Host -->|application.shutdown| Disk[(会话日志落盘)]
Helper -->|等端口释放 + 重新拉起| NewMain[新的 Electron 主进程]

具体步骤:

1. Host 把交接信息写到 ~/.dsh/dsh-power-menu-mac/,用 process.execPath + ELECTRON_RUN_AS_NODE=1 启动 lib/helper.cjs。
2. helper 等 1.2 秒,让 HTTP 应答先到浏览器。
3. helper 对 Electron 主进程发 SIGTERM(让它有机会收尾),必要时 SIGKILL。只对主进程,不碰它的子树。
4. Host 检测到断开后走自己的 application.shutdown,尽量把会话日志正常落盘。
5. 重启模式下,helper 等旧监听端口释放,清掉 ELECTRON_RUN_AS_NODE / DSH_DESKTOP_NODE_EXECUTABLE,用原可执行文件重新拉起,并验证新进程真的注册到了窗口服务器;失败会重试,仍失败就弹窗提示手动打开。
6. 关闭模式下不拉起,等 Host 自己退出。

重启是怎么验证成功的

这是 macOS 版和原版行为差别最大的地方。

原版在 Windows 上用 Get-Process 读 MainWindowHandle / MainWindowTitle,能真的看到窗口,还能认出「应用无法启动」这类崩溃对话框。macOS 上作者没有对应手段,只能退化成"进程还活着就算成功" —— 白屏、卡在启动、崩溃循环都会被误判成成功。

这里的做法是查窗口服务器: bash lsappinfo find pid=<新进程>

- 有输出 → 这个进程已经作为 GUI 应用注册到窗口服务器,有界面了。
- 空输出 → 进程在跑,但从来没连上窗口服务器(崩溃循环、headless 重启)。
- 之后再要求它活过启动宽限期(单实例锁意味着第二个实例会立刻退出,活下来就说明是它)。

lsappinfo 是 macOS 自带、不需要任何权限。另一条路(osascript + System Events 查窗口)需要辅助功能授权,会在用户面前弹一个同意框 —— 而这个框恰好出现在"应用刚重启失败"的时候,非常糟糕,所以没用。

接口

Host 半注册四个仅限本机回环的接口: text POST /api/dsh-power-menu-mac/shutdown POST /api/dsh-power-menu-mac/restart GET /api/dsh-power-menu-mac/health GET /api/dsh-power-menu-mac/plan

安全
- 四个接口只接受回环地址(127.0.0.1 / ::1),校验 Host 必须是本机,拒绝 sec-fetch-site: cross-site,带 Origin 时必须与本机同源。
- 没有鉴权令牌的第三方网页无法跨站触发关机/重启。
- 只支持桌面版;dsh web 下接口返回 409,不会去杀别的进程。
- 关机/重启时只清理本应用自己的进程(Electron 主进程的渲染/GPU/工具子进程)。宿主(dsh host)在 DSH 终端里被启动的其它子进程——第二个 dsh web 实例、dev server、构建任务——默认不碰;需要旧行为时设 DSH_POWER_MENU_KILL_HOST_CHILDREN=1。
- 重启会沿用当前实例的启动参数(按 home 读取 <DSH_HOME>/power-menu-relaunch-args.json),所以用 --user-data-dir 启动的隔离 home 不会在重启后掉回默认 Electron profile。
- 不收集任何数据,不联网;日志只写在本地 ~/.dsh/dsh-power-menu-mac/。

兼容性

- 系统:macOS。插件依赖 ps / kill / lsappinfo,Windows 和 Linux 未实现(package.json 里 os: ["darwin"],装到别的系统上会直接拒绝)。
- DSH:在 DSH Desktop 0.2.1-rc.3 上实测通过(2026-10-26)。
- Node:跟随 DSH 自带的 Node(22+),无需单独安装。

开发 text dsh-power-menu-mac/ ├── lib/ │ ├── index.js Host 半:四个回环接口 + 交接调度 │ ├── client.js 浏览器半:window.__ModuleLoader__ bundle │ └── helper.cjs 分离的交接 helper ├── install.sh 一键安装 ├── enable.sh 补 dsh.profile.bundles ├── cordis.patch.yml bundle 插入层 ├── package.json ├── docs/screenshot.png └── README.md

- 客户端 bundle 不是普通 ESM,必须用 window.__ModuleLoader__.load({ id, factory }) 格式;dsh.client.inject 声明 @deepseek-ai/dsh-client-ui-slots。
- 没有构建步骤,也没有运行时 npm 依赖。
- 提交前跑一次静态检查:

bash
npm run check

- 本地调试日志:~/.dsh/dsh-power-menu-mac/power-menu.log 和 helper-*.log。

常见问题

按钮没出现?
确认插件处于「启用中」,并重启过一次 DSH。会话页里按钮在标题栏图标行最右侧;其它页面(插件、自动化任务等)没有会话标题栏,按钮固定在窗口右上角。如果是在 Windows 上,按钮本来就不该出现 —— 请用原版 dsh-power-menu。

点了没反应 / 报 409?
dsh web(浏览器里的 Web 版)不支持,只支持桌面版。看 ~/.dsh/dsh-power-menu-mac/ 下的日志。

重启后端口变了?
正常。DSH Desktop 每次启动用随机端口(--port 0),插件只负责拉起新实例,不假设端口不变。

DSH 装在中文路径 / 用户名含中文能用吗?
可以。插件不拼 shell 字符串:启动用 spawn(exe, [], { cwd }),路径作为单个 argv 交给 Node 的 Unicode API;交接 JSON 和日志按 UTF-8 读写。helper 自带 --pathtest,会在「中文 + 空格」的临时目录里实测 UTF-8 读写和从中文路径启动脚本。

装完点按钮报 HTTP 404?
说明宿主半没加载:插件只在 dependencies 里,不在 dsh.profile.bundles 里。跑 ./enable.sh,或手动把 dsh-power-menu-mac 加进 profile 的 dsh.profile.bundles,再重启 DSH。

为什么按钮不挂真实 slot?
DSH 的 conversation.session.header.corner 是单占用槽,已经被侧栏开关占用;conversation.session.header.utilities 会排在侧栏左侧;sidebar.footer.action 在左下角。要在不修改 DSH 的前提下保持右上角,只能把按钮挂进 headerCorner 的 DOM。代价是上游改头部结构时可能失效;插件用 MutationObserver 跟随,不轮询。

关闭/重启会跳过 profile 收尾吗?
会有一点:插件只结束 Electron 主进程,Host 会走 application.shutdown,但主进程的 before-quit 可能被跳过,Chromium 的 Cookie / Local Storage 不保证落盘。追求最干净的退出请用菜单栏的「退出」。真正的 app.quit() 需要 DSH 提供协作退出 IPC,插件单方面做不到。

卸载

- 插件管理器里移除 dsh-power-menu-mac,或者从 profile 的 dependencies / dsh.profile.bundles 里删掉,再删掉 node_modules 里的链接。
- 可选:删除 ~/.dsh/dsh-power-menu-mac/ 下的日志与交接文件。
- 重启 DSH 生效。

更新日志

见 CHANGELOG.md。

许可证

MIT © 2026 DDDMUC(macOS 移植)· © 2026 xichen-62(原版 dsh-power-menu)

致谢
- 原版 xichen-62/dsh-power-menu(MIT):整体结构、客户端界面、菜单交互、安全边界都来自它,本项目只是把宿主侧的进程操作换成了 POSIX 的做法。截图也沿用原版。
- 分离 helper 做进程交接、只结束主进程让 Host 优雅退出的思路,原版参考了 dsh-desktop-restart(MIT)。
- 菜单样式参考 Windows 电源菜单;图标为内联 SVG,无第三方图标库。

dsh-power-menu(MIT)的 macOS 移植版。在 DSH Desktop 会话标题栏右上角加一个电源按钮,带三个操作,按破坏性从小到大排列:

- 刷新 — 只重载渲染进程;应用和它的 Host 继续运行,所以这个操作可以替代「为了刷新就重启」的老习惯。
- 重启 — 退出并重新启动应用,窗口会自动回来。
- 定时 — 弹窗:选分钟数,然后选 关闭 after N / 重启 after N / quit now and come back after N。等待中的定时任务可以在同一个面板里取消。
- 关闭 — 退出整个桌面应用(不重新启动)。

Windows 版调用的是 taskkill 和 PowerShell;这一版调用 POSIX 的 ps / kill / lsappinfo。关键区别在于:只有新进程已在 macOS 窗口服务器注册(lsappinfo find pid=…)并且熬过启动宽限期,重启才算成功——「进程还活着」不算证据,而这正是原版在 macOS 上唯一能检查的东西。

仅支持 macOS(os: ["darwin"]),仅支持 DSH Desktop。无遥测,无外部依赖;四个 HTTP 路由只监听回环地址,并做了同源限制。
bash
git clone https://github.com/DDDMUC/dsh-power-menu-mac.git
cd dsh-power-menu-mac
./install.sh

或者在 DSH 插件管理器里添加 github:DDDMUC/dsh-power-menu-mac,然后运行 ./enable.sh 并重启 DSH。

MIT 协议,保留原版版权。
预览分享卡片长什么样 ↓DDDMUC/dsh-power-menu-mac
分享 · DeepSeek Harness Hub
DDDMUC/dsh-power-menu-mac

DSH 桌面版macOS右上角电源按钮:关闭 / 重启 DSH。纯 macOS 移植版MIT,对应 Windows…

回到顶部