dsh-x-core 0.1.21 → 0.1.22

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.
Binary file
Binary file
package/core-npm.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // 安装包只负责外壳和 Electron;核心作为一个 npm 包独立发版,启动器在运行时从 registry
4
4
  // 取新版本,解到暂存目录后仍走 apply-update.mjs 换核心目录。
5
- // 核心包跨平台(两套 filemanager 和 node 包装都在里面),Windows 和 mac 用同一个
5
+ // 核心包跨平台(三套 filemanager 和 node 包装都在里面),Windows / mac / Linux 用同一个
6
6
  // dsh-x-core;只有外壳/Electron 按平台分开。
7
7
  // 完整性用 npm 的 dist.integrity(sha512)校验;registry 走设置里的下载源(官方/镜像)。
8
8
 
@@ -14,7 +14,7 @@ import { join } from 'node:path'
14
14
  import { netFetch } from './proxy.js'
15
15
  import { cmpVer, parseVer, VERSION_CHANNEL } from './version.js'
16
16
 
17
- /** 核心包跨平台:Windows 和 mac 共用同一个 dsh-x-core。 */
17
+ /** 核心包跨平台:Windows / mac / Linux 共用同一个 dsh-x-core。 */
18
18
  export const CORE_PACKAGE = String(process.env.DSH_CORE_PACKAGE || 'dsh-x-core').trim()
19
19
 
20
20
  export const CORE_REGISTRIES = {
@@ -15,14 +15,13 @@ import { RUNTIME_NODE } from './platform.js'
15
15
  * 里,每处各写一次 `process.platform === 'win32' ? … : …`,加一个平台就得把整份仓库
16
16
  * 重新翻一遍。现在业务代码只认 `fileManager.xxx()`,平台差异全压在实现类里。
17
17
  *
18
- * 实现类分开打包:`filemanager-windows.js` 只进 Windows 安装包,`filemanager-mac.js`
19
- * 只进 macOS 的 .app(见 scripts/pack-common.mjs 的 APP_FILES)。所以这里只能用动态
20
- * import —— 静态 import 会在「另一套实现没被打进来」的那一侧直接报模块找不到。
18
+ * 实现类都随核心包发:Windows / mac / Linux 各一套,运行时由 filemanager.js
19
+ * 动态 import 当前平台那一套;核心 npm 包本身是跨平台的(见 scripts/pack-common.mjs
20
+ * 的 FILE_MANAGERS)。
21
21
  *
22
- * 要加 Linux:照着两个实现类写一个 `filemanager-linux.js`(整份契约都在下面这个基类
23
- * 的注释里),在 `pickFileManager()` 里挂上,再把它加进 `APP_FILES` 的平台分支。
24
- * 在那之前 Linux 落到 PosixFileManager:xdg-open 能用,系统对话框和自启直接报不支持——
25
- * 和这次重构之前的行为一致。
22
+ * 要加新平台:照着三个实现类写一个 `filemanager-<平台>.js`(整份契约都在下面这个基类
23
+ * 的注释里),在 `pickFileManager()` 里挂上,再把它加进 FILE_MANAGERS。没有专门实现的
24
+ * POSIX 平台落到 PosixFileManager:xdg-open 能用,系统对话框和自启直接报不支持。
26
25
  */
27
26
  export class FileManager {
28
27
  /**
@@ -184,5 +183,5 @@ export class FileManager {
184
183
  ignoreOpenError() { return false }
185
184
  }
186
185
 
187
- /** 还没有专门实现的平台(现在是 Linux):xdg-open 这条路能走,其余照契约报不支持。 */
186
+ /** 没有专门实现的其他 POSIX 平台:xdg-open 这条路能走,其余照契约报不支持。 */
188
187
  export class PosixFileManager extends FileManager {}
@@ -0,0 +1,124 @@
1
+ import { mkdir, rm, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import { PosixFileManager } from './filemanager-base.js'
4
+ import { APP_DIR_NAME, IN_ELECTRON } from './platform.js'
5
+
6
+ /** 对话框等用户的时间:目录 5 分钟、文件 10 分钟,和其他平台一致。 */
7
+ const DIRECTORY_TIMEOUT = 5 * 60 * 1000
8
+ const FILE_TIMEOUT = 10 * 60 * 1000
9
+
10
+ /**
11
+ * Linux 这一套:xdg-open、zenity/kdialog 的系统对话框、XDG autostart。
12
+ *
13
+ * 目录放在 $XDG_CONFIG_HOME/DSH(默认 ~/.config/DSH),和 AppImage 的只读挂载分开。
14
+ * 对话框优先 zenity,其次 kdialog;两个都没装时提示用户直接输入路径。
15
+ * 自启写 ~/.config/autostart/dsh-x.desktop,不依赖 systemd。
16
+ */
17
+ export class LinuxFileManager extends PosixFileManager {
18
+ constructor(deps = {}) {
19
+ super(deps)
20
+ this.writeFile = deps.writeFile || writeFile
21
+ this.mkdir = deps.mkdir || mkdir
22
+ this.rm = deps.rm || rm
23
+ }
24
+
25
+ get platform() { return 'linux' }
26
+
27
+ get directoryOpener() { return 'xdg-open' }
28
+
29
+ /** $XDG_CONFIG_HOME/DSH(默认 ~/.config/DSH)。 */
30
+ userAppDir() {
31
+ return join(this.env.XDG_CONFIG_HOME || join(this.home, '.config'), APP_DIR_NAME)
32
+ }
33
+
34
+ /**
35
+ * 登录自启要执行的外壳:AppImage 用 APPIMAGE 本身(挂载目录每次启动都会变),
36
+ * 安装版用当前 Electron 可执行文件;源码运行返回空串。
37
+ */
38
+ launcherExecutable() {
39
+ const appImage = String(this.env.APPIMAGE || '').trim()
40
+ if (appImage && this.exists(appImage)) return appImage
41
+ if (IN_ELECTRON && !process.defaultApp) return process.execPath
42
+ return ''
43
+ }
44
+
45
+ /** XDG autostart 的 .desktop 文件。 */
46
+ get autoStartPath() {
47
+ return join(this.env.XDG_CONFIG_HOME || join(this.home, '.config'), 'autostart', 'dsh-x.desktop')
48
+ }
49
+
50
+ autoStartEntry() {
51
+ return [
52
+ '[Desktop Entry]',
53
+ 'Type=Application',
54
+ 'Name=DSH-X',
55
+ 'Comment=DeepSeek Harness launcher',
56
+ `Exec=${this.launchCommand()}`,
57
+ 'Terminal=false',
58
+ 'X-GNOME-Autostart-enabled=true',
59
+ '',
60
+ ].join('\n')
61
+ }
62
+
63
+ async autoStartEnabled() {
64
+ return this.exists(this.autoStartPath)
65
+ }
66
+
67
+ async setAutoStart(enabled) {
68
+ if (!enabled) {
69
+ await this.rm(this.autoStartPath, { force: true })
70
+ return
71
+ }
72
+ await this.mkdir(dirname(this.autoStartPath), { recursive: true })
73
+ await this.writeFile(this.autoStartPath, this.autoStartEntry())
74
+ }
75
+
76
+ async pickDirectory(initialPath = '') {
77
+ const start = initialPath && this.exists(initialPath) ? initialPath : ''
78
+ const zenity = await this.dialog('zenity', [
79
+ '--file-selection', '--directory', '--title=选择目录',
80
+ ...(start ? [`--filename=${start}${start.endsWith('/') ? '' : '/'}`] : []),
81
+ ], DIRECTORY_TIMEOUT)
82
+ if (zenity !== null) return zenity
83
+ const kdialog = await this.dialog('kdialog', ['--getexistingdirectory', start || this.home, '--title', '选择目录'], DIRECTORY_TIMEOUT)
84
+ if (kdialog !== null) return kdialog
85
+ throw new Error('目录选择需要安装 zenity 或 kdialog;也可以直接输入路径')
86
+ }
87
+
88
+ async pickFile({ save = false, defaultName = '', title = '', prompt = '' } = {}) {
89
+ const ask = prompt || title || (save ? '选择保存位置' : '选择文件')
90
+ const zenity = await this.dialog('zenity', [
91
+ '--file-selection', '--title', ask,
92
+ ...(save ? ['--save', '--confirm-overwrite'] : []),
93
+ ...(defaultName ? [`--filename=${defaultName}`] : []),
94
+ ], FILE_TIMEOUT)
95
+ if (zenity !== null) return zenity
96
+ const kdialog = await this.dialog('kdialog', save
97
+ ? ['--getsavefilename', defaultName || this.home, '--title', ask]
98
+ : ['--getopenfilename', this.home, '--title', ask], FILE_TIMEOUT)
99
+ if (kdialog !== null) return kdialog
100
+ throw new Error('文件选择需要安装 zenity 或 kdialog;也可以直接输入路径')
101
+ }
102
+
103
+ async revealPath(path) {
104
+ await this.openWith('xdg-open', [dirname(String(path))], { windowsHide: true })
105
+ }
106
+
107
+ async openUrl(url) {
108
+ await this.openWith('xdg-open', [String(url)], {})
109
+ }
110
+
111
+ /**
112
+ * 跑 zenity / kdialog:返回路径;用户取消返回空串;命令不存在返回 null(换下一个)。
113
+ * 两个都没装时调用方给出明确提示。
114
+ */
115
+ async dialog(command, args, timeout) {
116
+ try {
117
+ return String(await this.exec(command, args, { timeout, encoding: 'utf8' })).trim()
118
+ } catch (error) {
119
+ if (error?.code === 'ENOENT') return null
120
+ if (error?.code === 1) return ''
121
+ throw error
122
+ }
123
+ }
124
+ }
package/filemanager.js CHANGED
@@ -1,23 +1,20 @@
1
- import { IS_MAC, IS_WINDOWS } from './platform.js'
1
+ import { IS_LINUX, IS_MAC, IS_WINDOWS } from './platform.js'
2
2
  import { FileManager, PosixFileManager } from './filemanager-base.js'
3
3
 
4
4
  /**
5
5
  * 按平台挑一套文件管理实现,业务代码只认这里导出的 `fileManager`。
6
6
  *
7
- * 两套实现都随核心包发(核心 npm 包是跨平台的,见 scripts/pack-common.mjs 的
7
+ * 三套实现都随核心包发(核心 npm 包是跨平台的,见 scripts/pack-common.mjs 的
8
8
  * FILE_MANAGERS);运行时用动态 import 只加载当前平台那一套。这样同一个 dsh-x-core
9
- * 在 Windows 和 mac 上都能用,只有外壳/Electron 按平台分开。
9
+ * 在 Windows / mac / Linux 上都能用,只有外壳/Electron 按平台分开。
10
10
  *
11
11
  * 契约、基类和 Linux 的兜底实现都在 filemanager-base.js:实现类静态 import 的是它,
12
12
  * 不是本文件——本文件有顶层 await,实现类再回头 import 它就会死锁。
13
- *
14
- * 要加 Linux:照着两个实现类写一个 `filemanager-linux.js`(整份契约在基类的注释里),
15
- * 在下面挂上,再把它加进 FILE_MANAGERS。在那之前 Linux 落到 PosixFileManager:
16
- * xdg-open 能用,系统对话框和自启直接报不支持——和这次重构之前的行为一致。
17
13
  */
18
14
  async function pickFileManager() {
19
15
  if (IS_WINDOWS) return (await import('./filemanager-windows.js')).WindowsFileManager
20
16
  if (IS_MAC) return (await import('./filemanager-mac.js')).MacFileManager
17
+ if (IS_LINUX) return (await import('./filemanager-linux.js')).LinuxFileManager
21
18
  return PosixFileManager
22
19
  }
23
20
 
package/node/node CHANGED
@@ -2,4 +2,5 @@
2
2
  if [ -n "$DSH_ELECTRON_BIN" ]; then ELECTRON_RUN_AS_NODE=1 exec "$DSH_ELECTRON_BIN" "$@"; fi
3
3
  dir="$(cd "$(dirname "$0")" && pwd)"
4
4
  if [ -x "$dir/../../../DSH.exe" ]; then ELECTRON_RUN_AS_NODE=1 exec "$dir/../../../DSH.exe" "$@"; fi
5
- ELECTRON_RUN_AS_NODE=1 exec "$dir/../../../MacOS/DSH" "$@"
5
+ if [ -x "$dir/../../../MacOS/DSH" ]; then ELECTRON_RUN_AS_NODE=1 exec "$dir/../../../MacOS/DSH" "$@"; fi
6
+ ELECTRON_RUN_AS_NODE=1 exec "$dir/../../../DSH" "$@"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-x-core",
3
3
  "productName": "DSH",
4
- "version": "0.1.21",
4
+ "version": "0.1.22",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "server.js",
package/platform.js CHANGED
@@ -14,6 +14,7 @@ const ROOT = dirname(fileURLToPath(import.meta.url))
14
14
 
15
15
  export const IS_WINDOWS = process.platform === 'win32'
16
16
  export const IS_MAC = process.platform === 'darwin'
17
+ export const IS_LINUX = process.platform === 'linux'
17
18
 
18
19
  /** 管理服务是不是跑在 Electron 主进程里(Windows/macOS 外壳都是,见 shell/main.mjs)。 */
19
20
  export const IN_ELECTRON = Boolean(process.versions.electron)
package/public/index.html CHANGED
@@ -4772,6 +4772,9 @@
4772
4772
  if (wasLive) {
4773
4773
  // 跟点卡片启动一样带上启动项的固定端口:端口随版本挪到新版,不然更新后这次落在随机端口上。
4774
4774
  await startVersion(latest, targetProfile, entry ? entry.port : null, target?.launchId)
4775
+ // 这条分支直接 return,不在这里收尾的话首页会一直停在「正在安装」、按钮全锁死。
4776
+ action = ''
4777
+ render()
4775
4778
  return
4776
4779
  }
4777
4780
  action = ''
package/server.js CHANGED
@@ -127,7 +127,7 @@ import {
127
127
  import { netFetch, proxyEnv, resetProxyCache, resolveProxy } from './proxy.js'
128
128
  import { APP_DIR, fileManager } from './filemanager.js'
129
129
  import { checkCoreUpdate, downloadCoreTarball, extractCoreTarball, stagingExists, stagingStats } from './core-npm.js'
130
- import { IN_ELECTRON, IS_MAC, IS_WINDOWS, LAUNCHER_NAME, MAC_APP_NAME, NPM_MODE, PACKAGED, RUNTIME_NODE, RUNTIME_NODE_ENV } from './platform.js'
130
+ import { IN_ELECTRON, IS_LINUX, IS_MAC, IS_WINDOWS, LAUNCHER_NAME, MAC_APP_NAME, NPM_MODE, PACKAGED, RUNTIME_NODE, RUNTIME_NODE_ENV } from './platform.js'
131
131
 
132
132
  const execFileAsync = promisify(execFile)
133
133
 
@@ -2765,7 +2765,7 @@ export async function refreshShortcuts({ installRoot, appDir = APP_DIR, version
2765
2765
  */
2766
2766
  async function checkAppUpdate({ force = false } = {}) {
2767
2767
  if (NPM_MODE) return { available: false, reason: 'npm' }
2768
- if (!PACKAGED || (!IS_WINDOWS && !IS_MAC)) return { available: false, reason: PACKAGED ? 'platform' : 'source' }
2768
+ if (!PACKAGED || (!IS_WINDOWS && !IS_MAC && !IS_LINUX)) return { available: false, reason: PACKAGED ? 'platform' : 'source' }
2769
2769
  if (!force && appUpdateCache.data && Date.now() - appUpdateCache.at < APP_UPDATE_CACHE_MS) return appUpdateCache.data
2770
2770
  const remember = (data) => { appUpdateCache = { at: Date.now(), data }; return data }
2771
2771
  const latest = APP_VERSION
@@ -5557,7 +5557,8 @@ async function handleApi(req, res, url) {
5557
5557
  const child = spawn(RUNTIME_NODE, [helper,
5558
5558
  '--app', ROOT,
5559
5559
  '--staged', stagedApp.dir,
5560
- '--exe', process.execPath,
5560
+ // AppImage 的 process.execPath 是挂载目录里的临时二进制,重启要用 APPIMAGE 本身
5561
+ '--exe', process.env.APPIMAGE || process.execPath,
5561
5562
  '--pid', String(process.pid),
5562
5563
  '--backup', `${ROOT}.bak-${APP_VERSION}`,
5563
5564
  '--marker', join(APP_DIR, 'update-ok'),