mirror of
https://github.com/TommyFang2077/dsh-desktop.git
synced 2026-08-18 05:16:35 +08:00
release: v0.1.3
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -949,7 +949,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dsh-core"
|
name = "dsh-core"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -966,7 +966,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dsh-desktop"
|
name = "dsh-desktop"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"dsh-core",
|
"dsh-core",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ members = ["crates/dsh-core", "src-tauri"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["TommyFang2077"]
|
authors = ["TommyFang2077"]
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -68,18 +68,19 @@ dsh 原本运行在浏览器标签页中;本项目用 Tauri 2 和系统 WebVie
|
|||||||
|
|
||||||
| 平台 | 产物 | 运行时要求 |
|
| 平台 | 产物 | 运行时要求 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Windows | NSIS `.exe` / `.msi` | [WebView2](https://developer.microsoft.com/microsoft-edge/webview2/)(安装器可引导下载)+ 本机 `dsh` |
|
| Windows | NSIS `.exe` / `.msi` | [WebView2](https://developer.microsoft.com/microsoft-edge/webview2/)(安装器可引导下载)+ Node.js/npm(首次启动自动安装 `dsh` 到内置目录) |
|
||||||
| macOS | Apple Silicon / Intel `.dmg` | 未公证,首次打开需在「隐私与安全性」允许 + 本机 `dsh` |
|
| macOS | Apple Silicon / Intel `.dmg` | 未公证,首次打开需在「隐私与安全性」允许 + Node.js/npm(首次启动自动安装 `dsh` 到内置目录) |
|
||||||
| Linux | `.deb` / `.rpm` | WebKitGTK 4.1 + 本机 `dsh` |
|
| Linux | `.deb` / `.rpm` | WebKitGTK 4.1 + Node.js/npm(首次启动自动安装 `dsh` 到内置目录) |
|
||||||
| Linux Flatpak | `.flatpak` | **零依赖**:自带 Node.js 24 与 `@deepseek-ai/dsh` |
|
| Linux Flatpak | `.flatpak` | **零依赖**:自带 Node.js 24 与 `@deepseek-ai/dsh` |
|
||||||
|
|
||||||
除 Flatpak 外,需要先安装 `dsh`:
|
除 Flatpak 外,需要本机有 Node.js/npm,用于首次启动自动下载并安装 `dsh`。也可以提前安装或指定路径:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @deepseek-ai/dsh
|
npm install -g @deepseek-ai/dsh
|
||||||
|
# 或设置 DSH_DESKTOP_DSH_BIN 指向 dsh 可执行文件
|
||||||
```
|
```
|
||||||
|
|
||||||
安装后直接启动,等待启动页完成即可进入官方 WebUI。
|
未检测到 `dsh` 时,第一次启动会显示「正在下载并安装内置 dsh…」,完成后自动进入官方 WebUI。
|
||||||
|
|
||||||
## 功能一览
|
## 功能一览
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ impl DshNotFound {
|
|||||||
fn new(msg: impl Into<String>) -> Self {
|
fn new(msg: impl Into<String>) -> Self {
|
||||||
Self::Message(msg.into())
|
Self::Message(msg.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True when resolution successfully analyzed every candidate and found no
|
||||||
|
/// usable dsh at all (as opposed to an invalid user-supplied override).
|
||||||
|
pub fn is_missing(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
DshNotFound::Message(message) if message.contains("找不到")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn strip_ansi(text: &str) -> String {
|
pub fn strip_ansi(text: &str) -> String {
|
||||||
@@ -59,6 +68,18 @@ pub fn find_npx_dsh_bins() -> Vec<PathBuf> {
|
|||||||
if let Some(globbed) = pattern.to_str() {
|
if let Some(globbed) = pattern.to_str() {
|
||||||
if let Ok(paths) = glob_simple(globbed) {
|
if let Ok(paths) = glob_simple(globbed) {
|
||||||
for path in paths {
|
for path in paths {
|
||||||
|
// npm also drops an extensionless POSIX shim (`dsh`) next to
|
||||||
|
// `dsh.cmd`; Windows cannot CreateProcess that file, so prefer
|
||||||
|
// the cmd wrapper when present.
|
||||||
|
#[cfg(windows)]
|
||||||
|
let path = {
|
||||||
|
let cmd = path.with_extension("cmd");
|
||||||
|
if cmd.is_file() {
|
||||||
|
cmd
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
};
|
||||||
if is_executable(&path) {
|
if is_executable(&path) {
|
||||||
bins.push(path);
|
bins.push(path);
|
||||||
}
|
}
|
||||||
@@ -105,7 +126,7 @@ impl DshLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve(&self) -> Result<Vec<String>, DshNotFound> {
|
fn collect_candidates(&self) -> Result<Vec<Vec<String>>, DshNotFound> {
|
||||||
let mut candidates: Vec<Vec<String>> = Vec::new();
|
let mut candidates: Vec<Vec<String>> = Vec::new();
|
||||||
|
|
||||||
if let Ok(env_override) = std::env::var(ENV_BIN_OVERRIDE) {
|
if let Ok(env_override) = std::env::var(ENV_BIN_OVERRIDE) {
|
||||||
@@ -163,6 +184,11 @@ impl DshLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(&self) -> Result<Vec<String>, DshNotFound> {
|
||||||
|
let candidates = self.collect_candidates()?;
|
||||||
let chosen = candidates.into_iter().next().ok_or_else(|| {
|
let chosen = candidates.into_iter().next().ok_or_else(|| {
|
||||||
if self.in_flatpak {
|
if self.in_flatpak {
|
||||||
DshNotFound::new(
|
DshNotFound::new(
|
||||||
@@ -170,13 +196,22 @@ impl DshLauncher {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
DshNotFound::new(
|
DshNotFound::new(
|
||||||
"找不到 DeepSeek Harness (dsh),也没有可用的 npx。\n请先安装:npm install -g @deepseek-ai/dsh\n或用 DSH_DESKTOP_DSH_BIN 指定 dsh 的完整路径。",
|
"找不到 DeepSeek Harness (dsh)。\n请安装 Node.js/npm(首次启动会自动安装 dsh),或用 DSH_DESKTOP_DSH_BIN 指定 dsh 的完整路径。",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
Ok(chosen)
|
Ok(chosen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True when a real dsh binary is available without relying on the
|
||||||
|
/// transient `npx --yes @deepseek-ai/dsh` fallback.
|
||||||
|
pub fn has_installed_dsh(&self) -> Result<bool, DshNotFound> {
|
||||||
|
let candidates = self.collect_candidates()?;
|
||||||
|
Ok(candidates
|
||||||
|
.iter()
|
||||||
|
.any(|candidate| candidate.first().map(String::as_str) != Some("npx")))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn web_argv(&self) -> Result<Vec<String>, DshNotFound> {
|
pub fn web_argv(&self) -> Result<Vec<String>, DshNotFound> {
|
||||||
let mut argv = self.resolve()?;
|
let mut argv = self.resolve()?;
|
||||||
argv.extend([
|
argv.extend([
|
||||||
@@ -406,6 +441,15 @@ mod tests {
|
|||||||
assert_eq!(argv, vec!["echo"]);
|
assert_eq!(argv, vec!["echo"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn has_installed_dsh_with_override() {
|
||||||
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
|
std::env::set_var(ENV_BIN_OVERRIDE, "echo");
|
||||||
|
let launcher = DshLauncher::new(None, None);
|
||||||
|
assert!(launcher.has_installed_dsh().unwrap());
|
||||||
|
std::env::remove_var(ENV_BIN_OVERRIDE);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_env_override_raises() {
|
fn invalid_env_override_raises() {
|
||||||
let _guard = ENV_LOCK.lock().unwrap();
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
|
|||||||
@@ -41,6 +41,15 @@
|
|||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.1.3" date="2026-08-17">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Non-Flatpak installs auto-install dsh into the app's built-in prefix on first launch when no dsh is found</li>
|
||||||
|
<li>Fixed Gitea mirror publish failing on a first-version 404 (release lookup no longer retries 404)</li>
|
||||||
|
<li>Windows prefers the dsh.cmd shim over the extensionless POSIX script in the npx cache</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.1.2" date="2026-08-17">
|
<release version="0.1.2" date="2026-08-17">
|
||||||
<description>
|
<description>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
21
docs/release-notes/v0.1.3.md
Normal file
21
docs/release-notes/v0.1.3.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# DeepSeek Harness Desktop v0.1.3
|
||||||
|
|
||||||
|
**摘要 / Summary:** 首次启动自动安装内置 dsh(非 Flatpak 不再要求手动安装);Gitea 镜像发布修复首个版本的 404 卡死;Windows 上优先使用 `dsh.cmd` shim。
|
||||||
|
|
||||||
|
## 新功能 / Features
|
||||||
|
- **未检测到 dsh 时自动安装**:非 Flatpak 安装包首次启动如果找不到可用的 dsh,会自动下载并安装到应用内置目录(Windows 为 `%APPDATA%\dsh-desktop\dsh-prefix`,Linux/macOS 为 `~/.local/share/dsh-desktop/dsh-prefix`),完成后直接进入官方 WebUI,不再要求先手动 `npm install -g @deepseek-ai/dsh`
|
||||||
|
- 需要本机有 Node.js/npm;没有 npm 时仍会给出明确提示
|
||||||
|
- `DSH_DESKTOP_DSH_BIN` / `--dsh`、`--no-update` / `DSH_DESKTOP_NO_UPDATE` 的优先级与原有行为不变
|
||||||
|
|
||||||
|
## 修复 / Fixes
|
||||||
|
- **Gitea 镜像发布在首个版本上失败**:Release 查询把 404(tag 已同步但 Release 尚未创建)误当瞬时故障重试 4 次后退出;现在 404 会直接走「创建 Release」分支
|
||||||
|
- **Windows 上 npx 缓存里的 dsh 无法启动**:npm 在 `.npm/_npx/*/node_modules/.bin` 同时生成无扩展名的 POSIX 脚本 `dsh` 与 `dsh.cmd`,此前可能选中无扩展名脚本导致启动失败;现在 Windows 优先使用 `dsh.cmd`
|
||||||
|
|
||||||
|
## 变更 / Changes
|
||||||
|
- 非 Flatpak 安装包的运行时要求从「本机已安装 dsh」改为「本机有 Node.js/npm 用于首次自动安装」(README 同步更新)
|
||||||
|
|
||||||
|
## 安装与更新 / Install & Update
|
||||||
|
- 安装包:Windows NSIS/MSI、macOS DMG(Apple Silicon 与 Intel)、Linux deb/rpm、Flatpak
|
||||||
|
- 非 Flatpak:首次启动会自动安装 dsh 到内置目录;也可设置 `DSH_DESKTOP_DSH_BIN` 指定 dsh 路径
|
||||||
|
- macOS 包未公证:首次打开需在「系统设置 → 隐私与安全性」中允许
|
||||||
|
- 更新提示出现在启动页:点击「下载并安装」即可(走 Gitea 镜像,签名校验后生效)
|
||||||
@@ -57,7 +57,10 @@ else
|
|||||||
release_body="大陆镜像安装包;文件与 GitHub Release 同源。应用内更新包由 Tauri 签名校验。"
|
release_body="大陆镜像安装包;文件与 GitHub Release 同源。应用内更新包由 Tauri 签名校验。"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
release_json=$(curl_retry curl --fail --silent --show-error -H "$AUTH" "$API/releases/tags/$RELEASE_TAG")
|
# 404 is expected when the mirror has not yet created a release for the tag;
|
||||||
|
# the create branch below handles it. Only retry transient failures, so the
|
||||||
|
# lookup itself must not go through curl_retry.
|
||||||
|
release_json=$(curl --fail --silent --show-error -H "$AUTH" "$API/releases/tags/$RELEASE_TAG" || true)
|
||||||
release_id=$(printf '%s' "$release_json" | jq -r '.id // empty')
|
release_id=$(printf '%s' "$release_json" | jq -r '.id // empty')
|
||||||
if [[ -z "$release_id" ]]; then
|
if [[ -z "$release_id" ]]; then
|
||||||
release_payload=$(jq -n \
|
release_payload=$(jq -n \
|
||||||
|
|||||||
@@ -357,14 +357,38 @@ fn boot(app: AppHandle) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let launcher = DshLauncher::new(state.args.dsh.clone(), state.args.cwd.clone());
|
let launcher = DshLauncher::new(state.args.dsh.clone(), state.args.cwd.clone());
|
||||||
|
let missing = match launcher.resolve() {
|
||||||
|
Ok(_) => !launcher.has_installed_dsh().unwrap_or(true),
|
||||||
|
Err(error) => error.is_missing(),
|
||||||
|
};
|
||||||
|
let mut bootstrap_message: Option<String> = None;
|
||||||
|
if missing && !state.args.no_update {
|
||||||
|
let _ = app.emit(
|
||||||
|
"status",
|
||||||
|
StatusPayload {
|
||||||
|
message: "未检测到 dsh,正在下载并安装内置 dsh…".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let result = update_dsh(true);
|
||||||
|
mark_update_checked();
|
||||||
|
log::info!(
|
||||||
|
"dsh not found; bootstrap install: {} ({})",
|
||||||
|
result.status,
|
||||||
|
result.message
|
||||||
|
);
|
||||||
|
bootstrap_message = Some(result.message);
|
||||||
|
}
|
||||||
let process = match launcher.start() {
|
let process = match launcher.start() {
|
||||||
Ok(p) => Arc::new(p),
|
Ok(p) => Arc::new(p),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
let detail = bootstrap_message
|
||||||
|
.map(|message| format!("{message}\n{err}"))
|
||||||
|
.unwrap_or_default();
|
||||||
let _ = app.emit(
|
let _ = app.emit(
|
||||||
"error",
|
"error",
|
||||||
ErrorPayload {
|
ErrorPayload {
|
||||||
message: err.to_string(),
|
message: err.to_string(),
|
||||||
detail: String::new(),
|
detail,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user