dsh-power-menu-mac 0.1.0 → 0.1.2
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 +17 -0
- package/docs/screenshot.png +0 -0
- package/lib/client.js +16 -7
- package/lib/helper.cjs +63 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Semantic Versioning](https://semver.org/spec/v2.0.0.html),格式参考 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)。
|
|
4
4
|
|
|
5
|
+
## [0.1.2] - 2026-10-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 重启 / 关闭时顺手把攒下来的 `dsh-auth-*` cookie 删到只剩最近 3 条。宿主每次启动都会下发一个**新名字**的 `dsh-auth-<随机>` cookie 且从不清理,而 cookie 不区分端口,所有端口上的宿主共用同一批;攒到 71 条时 `Cookie` 请求头就有 16.1 KB,越过 Node 默认的 16 KB 请求头上限,桌面版启动直接报 `desktop welcome: Web authentication failed`(2026-10-29 实测;用电源菜单反复重启会加速这个过程)。清理在 app 退出之后、重启之前进行,用 macOS 自带的 `/usr/bin/sqlite3` 改 Chromium 的 `Cookies` 库;只保留最近 3 条而不是全删,是为了不把同机另一个长期运行的 `dsh web`(浏览器里那个界面)当场登出。
|
|
10
|
+
|
|
11
|
+
## [0.1.1] - 2026-10-29
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- 打开菜单不再把焦点放进第一项。第一项是「关闭DSH」,焦点落上去之后误按一个回车/空格就会把应用关掉。焦点现在留在触发按钮上;ArrowDown / ArrowUp / Home / End 仍然能进菜单(由触发按钮转发给菜单的键盘处理),Esc 照旧关闭。
|
|
16
|
+
- 打开菜单时显式把焦点收回触发按钮。有些 WebKit 表面里点按钮不会自动聚焦,否则会出现「菜单开了但方向键没反应」。
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- 菜单按钮上的契约属性:`data-dsh-power-action="shutdown"` / `data-dsh-power-action="restart"`。`MenuItem` 现在把多余 props 透传给底层 `button`,选择器和自动化检查可以直接按动作定位,不用匹配标签文字。
|
|
21
|
+
|
|
5
22
|
## [0.1.0] - 2026-10-26
|
|
6
23
|
|
|
7
24
|
首个 macOS 版本。基于 [xichen-62/dsh-power-menu](https://github.com/xichen-62/dsh-power-menu) 0.1.18(MIT)移植。
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -98,8 +98,11 @@ window.__ModuleLoader__.load({
|
|
|
98
98
|
h('path', { d: 'M3 3v5h5' }))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// 多余的 props 原样透传给 button:契约属性(如 data-dsh-power-action)挂在真正的
|
|
102
|
+
// 那个按钮上,选择器才抓得到。
|
|
103
|
+
function MenuItem({ icon, label, onSelect, itemRef, ...rest }) {
|
|
102
104
|
return h('button', {
|
|
105
|
+
...rest,
|
|
103
106
|
type: 'button',
|
|
104
107
|
role: 'menuitem',
|
|
105
108
|
tabIndex: -1,
|
|
@@ -175,11 +178,8 @@ window.__ModuleLoader__.load({
|
|
|
175
178
|
}
|
|
176
179
|
}, [open])
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const first = itemRefs.current[0]
|
|
181
|
-
if (first !== null && first !== undefined) first.focus()
|
|
182
|
-
}, [open])
|
|
181
|
+
// 打开时不把焦点丢进菜单:第一项是「关闭DSH」,焦点落上去之后一个回车就把
|
|
182
|
+
// 应用关了。焦点留在触发按钮上,方向键进菜单(见触发按钮的 onKeyDown)。
|
|
183
183
|
|
|
184
184
|
const onMenuKeyDown = (event) => {
|
|
185
185
|
const items = itemRefs.current.filter(Boolean)
|
|
@@ -245,12 +245,14 @@ window.__ModuleLoader__.load({
|
|
|
245
245
|
h(MenuItem, {
|
|
246
246
|
icon: h(PowerIcon, { size: 21 }),
|
|
247
247
|
label: '关闭DSH',
|
|
248
|
+
'data-dsh-power-action': 'shutdown',
|
|
248
249
|
itemRef: (el) => { itemRefs.current[0] = el },
|
|
249
250
|
onSelect: () => run('shutdown'),
|
|
250
251
|
}),
|
|
251
252
|
h(MenuItem, {
|
|
252
253
|
icon: h(RestartIcon, { size: 21 }),
|
|
253
254
|
label: '重启DSH',
|
|
255
|
+
'data-dsh-power-action': 'restart',
|
|
254
256
|
itemRef: (el) => { itemRefs.current[1] = el },
|
|
255
257
|
onSelect: () => run('restart'),
|
|
256
258
|
}))
|
|
@@ -274,9 +276,16 @@ window.__ModuleLoader__.load({
|
|
|
274
276
|
'aria-haspopup': 'menu',
|
|
275
277
|
'aria-expanded': open ? 'true' : 'false',
|
|
276
278
|
'data-dshpm-slot': placement,
|
|
279
|
+
// 焦点留在按钮上,所以方向键要在这里接住:ArrowDown 进第一项、ArrowUp 进
|
|
280
|
+
// 最后一项(`onMenuKeyDown` 用 activeElement 定位,按钮不在菜单里时下标是 -1)。
|
|
281
|
+
onKeyDown: open ? onMenuKeyDown : undefined,
|
|
277
282
|
onClick: () => {
|
|
278
283
|
setError(null)
|
|
279
|
-
|
|
284
|
+
const next = !open
|
|
285
|
+
setOpen(next)
|
|
286
|
+
// 明确把焦点留在按钮上:有的 WebKit 里点按钮不会自动聚焦,靠默认行为
|
|
287
|
+
// 会出现"打开了菜单但方向键没反应"。
|
|
288
|
+
if (next) triggerRef.current?.focus()
|
|
280
289
|
},
|
|
281
290
|
}, h(PowerIcon, { size: 16 })))
|
|
282
291
|
|
package/lib/helper.cjs
CHANGED
|
@@ -89,6 +89,62 @@ function readLaunchArgs() {
|
|
|
89
89
|
const WINDOW_VERIFY_TIMEOUT_MS = 20000
|
|
90
90
|
const WINDOW_POLL_MS = 500
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 保留几条 dsh-auth cookie。见 `pruneAuthCookies`。
|
|
94
|
+
*/
|
|
95
|
+
const AUTH_COOKIE_KEEP = 3
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* app 的 Electron user-data 目录。relaunch 参数里带 `--user-data-dir` 时以它为准,
|
|
99
|
+
* 否则是 Electron 的默认 profile 目录。
|
|
100
|
+
*
|
|
101
|
+
* @param {string[]} argv - 重启时用的 argv。
|
|
102
|
+
* @returns {string} user-data 目录的绝对路径。
|
|
103
|
+
*/
|
|
104
|
+
function userDataDir(argv = []) {
|
|
105
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
106
|
+
const value = String(argv[index])
|
|
107
|
+
if (value === '--user-data-dir' && typeof argv[index + 1] === 'string') return path.resolve(argv[index + 1])
|
|
108
|
+
if (value.startsWith('--user-data-dir=')) return path.resolve(value.slice('--user-data-dir='.length))
|
|
109
|
+
}
|
|
110
|
+
return path.join(os.homedir(), 'Library', 'Application Support', '@deepseek-ai', 'dsh-desktop')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 把攒下来的 `dsh-auth-*` cookie 删到只剩最近几条。
|
|
115
|
+
*
|
|
116
|
+
* 宿主每次启动都会下发一个**新名字**的 `dsh-auth-<随机>` cookie,旧的一个都不清;
|
|
117
|
+
* 而 cookie 不区分端口,所有端口上的宿主共用同一批。攒到 71 条时 `Cookie` 请求头
|
|
118
|
+
* 就有 16.1 KB,越过 Node 默认的 16 KB 请求头上限,宿主回 431,桌面壳报
|
|
119
|
+
* "desktop welcome: Web authentication failed"(2026-10-29 实测,重启越频繁越快)。
|
|
120
|
+
*
|
|
121
|
+
* 退出/重启之后 app 已经关了,这时清理是安全的:新实例会自己下发一条新的。
|
|
122
|
+
* 只保留最近 3 条而不是全删,是为了不把同一台机器上另一个长期运行的 `dsh web`
|
|
123
|
+
* (浏览器里那个界面)当场登出。
|
|
124
|
+
*
|
|
125
|
+
* @param {string} label - 日志前缀(shutdown / restart)。
|
|
126
|
+
* @param {string[]} argv - 重启时用的 argv,用来定位 user-data 目录。
|
|
127
|
+
*/
|
|
128
|
+
function pruneAuthCookies(label, argv = []) {
|
|
129
|
+
const db = path.join(userDataDir(argv), 'Cookies')
|
|
130
|
+
if (!fs.existsSync(db)) {
|
|
131
|
+
note(`${label}: no cookie jar at ${db}; nothing to prune`)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const sql = [
|
|
135
|
+
"delete from cookies where name like 'dsh-auth-%' and rowid not in",
|
|
136
|
+
`(select rowid from cookies where name like 'dsh-auth-%' order by creation_utc desc limit ${AUTH_COOKIE_KEEP});`,
|
|
137
|
+
"select count(*) from cookies where name like 'dsh-auth-%';",
|
|
138
|
+
].join(' ')
|
|
139
|
+
try {
|
|
140
|
+
// macOS 自带 /usr/bin/sqlite3;Chromium 的 Cookies 就是一个 SQLite 库。
|
|
141
|
+
const left = execFileSync('/usr/bin/sqlite3', [db, sql], { encoding: 'utf8', timeout: 8000 }).trim()
|
|
142
|
+
note(`${label}: pruned stale dsh-auth cookies (kept ${left === '' ? '0' : left})`)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
note(`${label}: could not prune dsh-auth cookies: ${String(error?.message ?? error)}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
92
148
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
93
149
|
|
|
94
150
|
let LOG_PATH = ''
|
|
@@ -401,6 +457,7 @@ async function performShutdown(handoff) {
|
|
|
401
457
|
note('shutdown: host exited on its own')
|
|
402
458
|
}
|
|
403
459
|
await cleanupOrphans(handoff.orphans, 'shutdown', !alive(handoff.hostPid))
|
|
460
|
+
pruneAuthCookies('shutdown', readLaunchArgs())
|
|
404
461
|
note('shutdown: complete')
|
|
405
462
|
}
|
|
406
463
|
|
|
@@ -490,6 +547,11 @@ async function performRestart(handoff) {
|
|
|
490
547
|
await cleanupOrphans(handoff.orphans, 'restart', !alive(handoff.hostPid))
|
|
491
548
|
await sleep(portFree ? 800 : 1500)
|
|
492
549
|
|
|
550
|
+
// The app is gone, so its cookie jar is free to edit. Prune the accumulated
|
|
551
|
+
// dsh-auth cookies before the new instance reads them (see pruneAuthCookies).
|
|
552
|
+
const relaunchArgs = readLaunchArgs()
|
|
553
|
+
pruneAuthCookies('restart', relaunchArgs)
|
|
554
|
+
|
|
493
555
|
const env = { ...process.env }
|
|
494
556
|
for (const key of Object.keys(env)) {
|
|
495
557
|
const upper = key.toUpperCase()
|
|
@@ -523,7 +585,7 @@ async function performRestart(handoff) {
|
|
|
523
585
|
}
|
|
524
586
|
let child = null
|
|
525
587
|
try {
|
|
526
|
-
child = spawn(exe,
|
|
588
|
+
child = spawn(exe, relaunchArgs, {
|
|
527
589
|
detached: true,
|
|
528
590
|
stdio: 'ignore',
|
|
529
591
|
cwd: exeDir,
|
package/package.json
CHANGED