dsh-clean-desktop-shell 0.1.7 → 0.1.9

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.en.md CHANGED
@@ -87,11 +87,43 @@ following in person:
87
87
  set up signed + notarized builds.
88
88
  - Post-extract executable bits and quarantine extended attributes can only be
89
89
  confirmed on real hardware.
90
+ - **If the window still does not appear**: a failed launch writes diagnostics
91
+ to `desktop-shell-launch.log` inside your DSH home:
92
+
93
+ ```sh
94
+ cat "${DSH_HOME:-$HOME/.dsh}/desktop-shell-launch.log"
95
+ ```
96
+
97
+ Paste the contents into an issue — it records platform, arch, Node version,
98
+ DSH home, runtime directory and the exact error. With no window on screen,
99
+ this is the only thing that can tell us what went wrong.
90
100
 
91
101
  If you have a Mac and want to co-maintain macOS support (test the .dmg, set up
92
102
  signing, or add a launch-at-login tray item), PRs and verified issues are very
93
103
  welcome. You will be added to [CONTRIBUTORS.md](./CONTRIBUTORS.md).
94
104
 
105
+ ## Security & permissions: what it actually does
106
+
107
+ Third-party scanners (e.g. [dsh-xray](https://github.com/unStone/dsh-xray)) rate
108
+ this project as "high capability combined with sensitive behavior". **That rating
109
+ is not a false positive** — every item is real, and every item has a concrete,
110
+ necessary reason. You should know what runs on your machine.
111
+
112
+ | Behavior | Why it is required | Where |
113
+ |:--|:--|:--|
114
+ | Spawning system commands | The shell's core job is **starting / restarting / stopping the `dsh web` backend** and probing port 3080. There is no way to do that without system commands. | `electron/service.js` |
115
+ | Downloading the ~100MB Electron runtime | Needed on first launch. Two sources race with a 3s timeout: `github.com` and `npmmirror.com` — the latter is a CN mirror that is usually much faster on Chinese networks. | `src/host/runtime.js` |
116
+ | Calling `api.github.com` | Only for the tray's "Check for updates" action, to read the latest release metadata. | `electron/update.js` |
117
+ | Reading env vars | Path resolution and feature switches only: `DSH_HOME` (DSH home), `DSH_SHELL_ELECTRON_DIR` (reuse a local Electron, skip the download), `DSH_SHELL_AUTO_LAUNCH=0` (disable auto-launch), `USERPROFILE` / `APPDATA` (locate `dsh.cmd` and the shortcuts folder on Windows). | `src/host/common.js`, `src/host/index.js`, `electron/shortcut.js` |
118
+ | Patching the DSH runtime (`cordis.patch.yml`) | **DSH's official plugin registration mechanism** — every DSH plugin mounts this way, it is not specific to this project. | `cordis.patch.yml` |
119
+
120
+ **The line it does not cross**: no telemetry, no uploads, no reading of your
121
+ conversation data. The network requests above are the only two kinds that exist,
122
+ and both can be avoided entirely by setting `DSH_SHELL_ELECTRON_DIR`.
123
+
124
+ The unsigned-installer warnings (Windows SmartScreen, macOS Gatekeeper) come from
125
+ the **absence of a code-signing certificate**, not from any of the above.
126
+
95
127
  ## Install
96
128
 
97
129
  **Option 1: download the installer from Releases (for a standalone desktop app)**
@@ -199,6 +231,21 @@ npm run pack # package NSIS (Win) / DMG (mac)
199
231
 
200
232
  ## Changelog
201
233
 
234
+ ### 0.1.7
235
+ - **Fixed a silent failure that made the window never open on macOS**: the
236
+ Electron macOS archive is an `Electron.app` bundle with the binary at
237
+ `Electron.app/Contents/MacOS/Electron`; the launcher was looking for a
238
+ top-level `electron` (the Linux layout) and gave up without any feedback.
239
+ - Extraction now tries multiple strategies (ditto / unzip / tar) and verifies
240
+ the payload; the executable bit is restored and the macOS quarantine
241
+ attribute is cleared afterwards.
242
+ - Launch failures no longer vanish into the log: diagnostics are written to
243
+ `desktop-shell-launch.log` and its full path is reported.
244
+ - macOS tray now uses a template image so it adapts to light/dark menu bars;
245
+ the Windows-only "create desktop shortcut" item is hidden elsewhere.
246
+ - Added `CONTRIBUTORS.md` and an open call for Mac co-maintainers (signing,
247
+ notarization, real-device verification).
248
+
202
249
  ### 0.1.6
203
250
  - Fixed "check for updates" reporting the Electron runtime version in plugin mode.
204
251
 
package/README.md CHANGED
@@ -79,9 +79,32 @@ v0.1.7 修复了插件形态在 macOS 上无法定位 `Electron.app` 路径的
79
79
  - 临时解决:`xattr -cr "/Applications/DSH Clean Desktop Shell.app"`,然后右键 → 打开。
80
80
  - 长期解决:需要一位有 Apple Developer 账号的 Mac 合作者协助签名/公证,或长期把 .dmg 安装体验写为「需要右键打开 / 执行 xattr」。
81
81
  - **Electron.app 解压后的可执行位、quarantine 扩展属性等**只有真机能确认行为是否完全正确。
82
+ - **如果窗口还是没弹出来**:启动失败时会把诊断信息写到 DSH home 下的 `desktop-shell-launch.log`:
83
+
84
+ ```sh
85
+ cat "${DSH_HOME:-$HOME/.dsh}/desktop-shell-launch.log"
86
+ ```
87
+
88
+ 把内容贴到 Issue 即可——里面记录了平台、架构、Node 版本、DSH home、运行时目录和具体报错。没有界面时,这是唯一能回传的信息。
82
89
 
83
90
  诚挚邀请有 Mac 环境、愿意一起打磨的同学参与:能帮忙验证安装流程、补充签名配置、或者把开机自启/登录项做进 Electron 托盘,欢迎直接提 PR 或在 Issue 里 @ 我,我会把你加入 [CONTRIBUTORS.md](./CONTRIBUTORS.md)。
84
91
 
92
+ ## 安全与权限:它到底做了什么
93
+
94
+ 第三方安全扫描器(如 [dsh-xray](https://github.com/unStone/dsh-xray))会给本项目打出「高能力 + 敏感行为」的评级。这个评级**没有误报**——列出的每一条都属实,但每一条都有明确且必要的原因。既然要装进你的机器,就该摊开讲清楚。
95
+
96
+ | 行为 | 为什么必须这么做 | 代码位置 |
97
+ |:--|:--|:--|
98
+ | 执行系统命令(spawn) | 壳的核心功能就是**启动 / 重启 / 停止 `dsh web` 后端**,以及探测 3080 端口占用。不调用系统命令无法实现。 | `electron/service.js` |
99
+ | 下载约 100MB 的 Electron 运行时 | 首次启动需要。两个源按网络环境自动竞速(3 秒超时):`github.com` 与 `npmmirror.com`——后者是国内镜像,CN 网络下通常更快。 | `src/host/runtime.js` |
100
+ | 访问 `api.github.com` | 仅用于托盘「检查更新」拉取最新 Release 信息。 | `electron/update.js` |
101
+ | 读取环境变量 | 只用于定位路径和功能开关:`DSH_HOME`(DSH 主目录)、`DSH_SHELL_ELECTRON_DIR`(复用本地 Electron,跳过下载)、`DSH_SHELL_AUTO_LAUNCH=0`(关闭自动弹窗)、`USERPROFILE` / `APPDATA`(Windows 下定位 `dsh.cmd` 与快捷方式目录)。 | `src/host/common.js`、`src/host/index.js`、`electron/shortcut.js` |
102
+ | 修改 DSH 运行时(`cordis.patch.yml`) | **DSH 官方的插件注册机制**,所有 DSH 插件都靠它挂载,并非本项目特有行为。 | `cordis.patch.yml` |
103
+
104
+ **边界**:不上传任何数据、不读取会话内容、不回传遥测。全部网络请求只有上面两类(下载运行时 / 查更新),且都可通过设置 `DSH_SHELL_ELECTRON_DIR` 完全避免。
105
+
106
+ 安装包的未签名警告(Windows SmartScreen、macOS Gatekeeper)来自**缺少代码签名证书**,与上述行为无关。
107
+
85
108
  ## 安装
86
109
 
87
110
  **方式一:从 Release 下载安装包(想要独立桌面应用的用户)**
@@ -189,6 +212,13 @@ npm run pack # 打包 NSIS (Win) / DMG (mac)
189
212
 
190
213
  ## 更新历史
191
214
 
215
+ ### 0.1.7
216
+ - **修复 macOS 上窗口完全打不开的静默失败**:Electron 的 macOS 包是 `Electron.app` 应用包,可执行文件位于 `Electron.app/Contents/MacOS/Electron`;此前按 Linux 布局去找顶层 `electron`,导致 Mac 上必然启动失败且无任何提示。
217
+ - 解压改为多策略回退(ditto / unzip / tar)并校验产物;解压后补齐可执行位,清除 macOS quarantine 扩展属性。
218
+ - 启动失败不再只写日志:诊断信息落盘到 `desktop-shell-launch.log`,并在提示中给出完整路径。
219
+ - macOS 托盘改用模板图,深浅色菜单栏自适应;非 Windows 平台隐藏「创建桌面快捷方式」。
220
+ - 新增 `CONTRIBUTORS.md`,公开招募 Mac 合作开发者(代码签名 / 公证 / 实机验证)。
221
+
192
222
  ### 0.1.6
193
223
  - 修复插件形态下「检查更新」误报 Electron 运行时版本号的问题。
194
224
 
@@ -1,35 +1,47 @@
1
1
  /**
2
- * Update handling — Windows auto-update, macOS manual download.
2
+ * Update handling — three install forms, three update sources.
3
3
  *
4
4
  * Windows (packaged app): uses electron-updater to download the new
5
5
  * installer from GitHub Releases in the background and install it on
6
6
  * restart. Progress is shown in the small progress window.
7
7
  *
8
- * macOS / dev mode: falls back to a manual check that opens the GitHub
9
- * Releases page (macOS auto-update needs a Developer ID signature which
10
- * this project does not have yet).
8
+ * macOS (packaged app): manual check that opens the GitHub Releases page
9
+ * (macOS auto-update needs a Developer ID signature which this project
10
+ * does not have yet).
11
+ *
12
+ * Plugin mode (npm-installed, !app.isPackaged): checks the npm registry for
13
+ * dist-tags.latest and offers a one-click update executed through the
14
+ * package manager that actually installed the plugin (inferred from the
15
+ * lockfile next to the install, vercel-style), with command variants to
16
+ * absorb environment and pnpm-version differences.
11
17
  */
12
18
  import { app, shell, dialog } from 'electron'
13
- import { readFileSync } from 'node:fs'
14
- import { dirname, join } from 'node:path'
19
+ import { existsSync, readFileSync } from 'node:fs'
20
+ import { spawn } from 'node:child_process'
21
+ import { basename, dirname, join } from 'node:path'
15
22
  import { fileURLToPath } from 'node:url'
16
23
  import { showProgress, setProgress, closeProgress } from './progress.js'
17
24
  import { loadConfig } from './config.js'
18
25
 
19
26
  const REPO_URL = 'https://github.com/Icather/dsh-clean-desktop-shell'
20
27
  const RELEASES_API = 'https://api.github.com/repos/Icather/dsh-clean-desktop-shell/releases/latest'
28
+ // npm registry — the update source for plugin (npm-installed) mode.
29
+ const NPM_REGISTRY_API = 'https://registry.npmjs.org/dsh-clean-desktop-shell'
21
30
 
22
31
  // Plugin (bare-runtime) mode has no app bundle, so app.getVersion() returns
23
32
  // the Electron runtime version (e.g. 33.4.11). Read the plugin's own version
24
33
  // from its package.json instead.
25
34
  const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
26
- const PKG_VERSION = (() => {
35
+ const PKG_INFO = (() => {
27
36
  try {
28
- return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version
37
+ return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
29
38
  } catch {
30
39
  return null
31
40
  }
32
41
  })()
42
+ const PKG_VERSION = PKG_INFO?.version ?? null
43
+ // Read from the plugin's own manifest — never hardcode the package name.
44
+ const PKG_NAME = PKG_INFO?.name ?? 'dsh-clean-desktop-shell'
33
45
 
34
46
  let autoUpdater = null
35
47
  let updaterPromise = null
@@ -125,33 +137,62 @@ export async function checkForUpdatesAuto() {
125
137
  return
126
138
  }
127
139
 
128
- // Manual path (macOS / dev mode): compare versions, offer GitHub page.
129
- const r = await checkForUpdate()
140
+ // Manual path. Plugin mode is installed via npm/DSH market, so it checks the
141
+ // npm registry; the packaged desktop app checks GitHub Releases.
130
142
  const isPlugin = !app.isPackaged
143
+ const r = isPlugin ? await checkForUpdateNpm() : await checkForUpdate()
131
144
  if (r.hasUpdate) {
145
+ if (!isPlugin) {
146
+ const choice = dialog.showMessageBoxSync({
147
+ type: 'info',
148
+ title: '发现新版本',
149
+ message: `当前版本 ${r.current},最新版本 ${r.latest}。`,
150
+ detail: 'macOS 自动更新需要代码签名,当前请前往 GitHub Releases 手动下载。',
151
+ buttons: ['前往下载', '取消'],
152
+ defaultId: 0,
153
+ cancelId: 1,
154
+ })
155
+ if (choice === 0) openUrl(r.url)
156
+ return
157
+ }
158
+
159
+ // Plugin mode (Plan B + A): offer a one-click update executed through the
160
+ // package manager that actually installed this package, and show the
161
+ // equivalent manual command plus the DSH market path.
162
+ const plan = detectPluginUpdatePlan()
163
+ const variants = plan ? updateCommandVariants(plan.pm) : []
164
+ const cmdLine = variants[0] || null
165
+ const marketHint = '① DSH 网页「设置 → 插件市场 → 已安装」→「更新」(完成后按提示重启)。'
166
+ const detail = cmdLine
167
+ ? `${marketHint}\n② 或在目录 ${plan.profileRoot} 下执行:\n${cmdLine}\n(按 Ctrl+C 可复制本对话框全部文字)`
168
+ : marketHint
132
169
  const choice = dialog.showMessageBoxSync({
133
170
  type: 'info',
134
171
  title: '发现新版本',
135
172
  message: `当前版本 ${r.current},最新版本 ${r.latest}。`,
136
- detail: isPlugin
137
- ? '插件形态请到 DSH 网页的「设置 插件市场 已安装」里点「更新」,完成后按提示重启即可生效。'
138
- : 'macOS 自动更新需要代码签名,当前请前往 GitHub Releases 手动下载。',
139
- buttons: isPlugin ? ['打开 DSH 网页', '稍后'] : ['前往下载', '取消'],
173
+ detail,
174
+ buttons: cmdLine ? ['立即更新', '打开 DSH 网页', '稍后'] : ['打开 DSH 网页', '稍后'],
140
175
  defaultId: 0,
141
- cancelId: 1,
176
+ cancelId: cmdLine ? 2 : 1,
142
177
  })
143
- if (choice === 0) openUrl(isPlugin ? loadConfig().targetUrl : r.url)
178
+ if (cmdLine && choice === 0) {
179
+ await updatePluginViaPackageManager(plan, variants, r)
180
+ } else if (choice === (cmdLine ? 1 : 0)) {
181
+ openUrl(loadConfig().targetUrl)
182
+ }
144
183
  } else if (r.latest) {
145
184
  dialog.showMessageBoxSync({
146
185
  type: 'info',
147
186
  title: '已是最新版本',
148
- message: `当前版本 ${r.current} 已是最新(${r.latest})。`,
187
+ message: `当前版本 ${r.current} 已是最新。`,
149
188
  })
150
189
  } else {
151
190
  dialog.showMessageBoxSync({
152
191
  type: 'warning',
153
192
  title: '检查更新失败',
154
- message: '无法连接 GitHub 检查更新,请检查网络后重试。',
193
+ message: isPlugin
194
+ ? '无法连接 npm 检查更新,请检查网络后重试。'
195
+ : '无法连接 GitHub 检查更新,请检查网络后重试。',
155
196
  })
156
197
  }
157
198
  }
@@ -227,3 +268,185 @@ export function openRepo() {
227
268
  export function openUrl(url) {
228
269
  shell.openExternal(url)
229
270
  }
271
+
272
+ /**
273
+ * Version check for plugin (npm-installed) mode. The plugin is updated through
274
+ * the npm registry / DSH market, so its "latest" must come from npm — NOT from
275
+ * GitHub Releases (that endpoint is only for the packaged desktop app).
276
+ * @returns {{ hasUpdate: boolean, latest?: string, current: string, url: string }}
277
+ */
278
+ export async function checkForUpdateNpm() {
279
+ const current = currentVersion()
280
+ let latestStr = null
281
+ const url = 'https://www.npmjs.com/package/dsh-clean-desktop-shell'
282
+
283
+ try {
284
+ const controller = new AbortController()
285
+ const timer = setTimeout(() => controller.abort(), 8000)
286
+ const res = await fetch(NPM_REGISTRY_API, { signal: controller.signal })
287
+ clearTimeout(timer)
288
+ if (res.ok) {
289
+ const data = await res.json()
290
+ latestStr = data['dist-tags']?.latest || null
291
+ }
292
+ } catch {
293
+ // Network error — no update known.
294
+ }
295
+
296
+ const currentV = parseVersion(current)
297
+ const latestV = parseVersion(latestStr)
298
+ return {
299
+ hasUpdate: isNewer(latestV, currentV),
300
+ latest: latestStr ? `v${latestStr}` : null,
301
+ current,
302
+ url,
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Locate the directory whose package.json declares this plugin as a dependency
308
+ * (the DSH profile root: <root>/node_modules/<pkg>), and infer the package
309
+ * manager that manages it from the lockfile found there (vercel-style).
310
+ * @returns {{ pm: 'pnpm'|'npm'|'yarn'|'bun', profileRoot: string } | null}
311
+ */
312
+ function detectPluginUpdatePlan() {
313
+ try {
314
+ const nm = dirname(PKG_ROOT)
315
+ if (basename(nm) !== 'node_modules') return null
316
+ const profileRoot = dirname(nm)
317
+ if (!existsSync(join(profileRoot, 'package.json'))) return null
318
+ const pm = existsSync(join(profileRoot, 'pnpm-lock.yaml')) ? 'pnpm'
319
+ : existsSync(join(profileRoot, 'package-lock.json')) ? 'npm'
320
+ : existsSync(join(profileRoot, 'yarn.lock')) ? 'yarn'
321
+ : (existsSync(join(profileRoot, 'bun.lockb')) || existsSync(join(profileRoot, 'bun.lock'))) ? 'bun'
322
+ : null
323
+ return pm ? { pm, profileRoot } : null
324
+ } catch {
325
+ return null
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Command candidates for the detected package manager, ordered from the most
331
+ * explicit to the most tolerant. Environment differences (pnpm only reachable
332
+ * via corepack) and pnpm version/flag differences are absorbed by trying them
333
+ * in order — the first variant that exits 0 wins. Quoting via JSON.stringify
334
+ * keeps paths/args safe in cmd.exe, PowerShell and POSIX shells alike.
335
+ */
336
+ function updateCommandVariants(pm) {
337
+ const q = JSON.stringify(PKG_NAME)
338
+ switch (pm) {
339
+ case 'pnpm':
340
+ return [
341
+ `pnpm update ${q} --latest --config.minimumReleaseAge=0`,
342
+ `pnpm update ${q} --latest`,
343
+ `pnpm update ${q}`,
344
+ `corepack pnpm update ${q} --latest`,
345
+ ]
346
+ case 'npm':
347
+ return [`npm install ${q}@latest`]
348
+ case 'yarn':
349
+ return [`yarn up ${q}@latest`, `yarn upgrade ${q} --latest`]
350
+ case 'bun':
351
+ return [`bun update ${q}`, `bun add ${q}@latest`]
352
+ default:
353
+ return []
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Run a shell command, capturing tail output. Node's spawn with shell:true
359
+ * always uses cmd.exe on Windows (regardless of the user's login shell being
360
+ * PowerShell or cmd), which is also required for .cmd shims like pnpm — a
361
+ * bare spawn would throw EINVAL. Resolves { ok, out, err }.
362
+ */
363
+ function runShell(cmd, cwd, timeoutMs = 5 * 60 * 1000) {
364
+ return new Promise((resolve) => {
365
+ let out = ''
366
+ let err = ''
367
+ let settled = false
368
+ const child = spawn(cmd, {
369
+ cwd,
370
+ shell: true,
371
+ windowsHide: true, // no console flash from the GUI process
372
+ })
373
+ const timer = setTimeout(() => {
374
+ if (!settled) child.kill()
375
+ }, timeoutMs)
376
+ child.stdout?.on('data', (d) => { out += d })
377
+ child.stderr?.on('data', (d) => { err += d })
378
+ child.on('error', () => {
379
+ settled = true
380
+ clearTimeout(timer)
381
+ resolve({ ok: false, out: tail(out), err: tail(err) })
382
+ })
383
+ child.on('close', (code) => {
384
+ if (settled) return
385
+ settled = true
386
+ clearTimeout(timer)
387
+ resolve({ ok: code === 0, out: tail(out), err: tail(err) })
388
+ })
389
+ })
390
+ }
391
+
392
+ function tail(s, n = 4000) {
393
+ return s.length > n ? s.slice(-n) : s
394
+ }
395
+
396
+ /** Read the currently installed plugin version from disk (post-update check). */
397
+ function installedPluginVersion() {
398
+ try {
399
+ return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version || null
400
+ } catch {
401
+ return null
402
+ }
403
+ }
404
+
405
+ /**
406
+ * One-click plugin update: try each command variant until one exits 0, then
407
+ * verify the on-disk version actually changed. Three outcomes — success,
408
+ * "ran but version unchanged" (constraint/cooldown), or failure with the
409
+ * attempted commands and output tail for manual retry.
410
+ */
411
+ async function updatePluginViaPackageManager(plan, variants, check) {
412
+ showProgress({ title: '插件更新', message: `正在通过 ${plan.pm} 更新插件…` })
413
+ let result = null
414
+ for (const cmd of variants) {
415
+ setProgress({ title: '插件更新', message: `正在执行:${cmd}`, state: 'busy' })
416
+ result = await runShell(cmd, plan.profileRoot)
417
+ if (result.ok) break
418
+ }
419
+ closeProgress()
420
+
421
+ const newVer = installedPluginVersion()
422
+ if (result?.ok && newVer && newVer !== check.current) {
423
+ dialog.showMessageBoxSync({
424
+ type: 'info',
425
+ title: '更新完成',
426
+ message: `插件已更新到 ${newVer}。`,
427
+ detail: '重启 dsh web 后生效(可从本应用托盘菜单重启后端)。',
428
+ })
429
+ return
430
+ }
431
+ if (result?.ok) {
432
+ dialog.showMessageBoxSync({
433
+ type: 'warning',
434
+ title: '版本未变化',
435
+ message: `命令执行成功,但插件版本仍为 ${check.current}。`,
436
+ detail: '可能被 package.json 的版本约束或新版本冷却策略限制,可稍后重试,或到 DSH 插件市场更新。',
437
+ })
438
+ return
439
+ }
440
+ const logTail = [result?.out, result?.err].filter(Boolean).join('\n').trim()
441
+ dialog.showMessageBoxSync({
442
+ type: 'warning',
443
+ title: '自动更新失败',
444
+ message: '无法自动更新插件,请手动执行或到 DSH 插件市场更新。',
445
+ detail: [
446
+ `已依次尝试:\n${variants.join('\n')}`,
447
+ logTail ? `命令输出(末尾):\n${logTail}` : '',
448
+ `手动命令(在目录 ${plan.profileRoot} 下):\n${variants[0]}`,
449
+ '按 Ctrl+C 可复制本对话框全部文字。',
450
+ ].filter(Boolean).join('\n\n'),
451
+ })
452
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-clean-desktop-shell",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Clean desktop shell for DeepSeek Harness (DSH) as a DSH plugin — wraps your web profile in a native window, tray-managed backend, offline auto-reconnect, zero visual changes. DSH 插件形态的纯净桌面壳:复用现有 web profile,托盘管理后端,零视觉改造。",
5
5
  "repository": {
6
6
  "type": "git",
package/version.txt CHANGED
@@ -1 +1 @@
1
- 0.1.7
1
+ 0.1.9