mirror of
https://github.com/TommyFang2077/dsh-desktop.git
synced 2026-08-18 05:16:35 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cbcca32a2b | |||
| 67f684d988 | |||
| 601077468c | |||
| 761c97213f | |||
| 272d9320c9 | |||
| fb841ec683 | |||
| d9f81cc7af | |||
| fe3e9586ec | |||
| b15292dd03 | |||
| 5e9ac59c59 |
@@ -11,15 +11,27 @@ jobs:
|
||||
publish:
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
steps:
|
||||
- name: Download publish scripts
|
||||
run: |
|
||||
BASE=https://raw.githubusercontent.com/TommyFang2077/dsh-easy-desktop/main
|
||||
curl -fsSL -o build-gitea-update.py "$BASE/scripts/build-gitea-update.py"
|
||||
curl -fsSL -o publish-gitea-release.sh "$BASE/scripts/publish-gitea-release.sh"
|
||||
curl -fsSL -o publish-gitea-actions.sh "$BASE/scripts/publish-gitea-actions.sh"
|
||||
- name: Publish to Gitea
|
||||
run: bash publish-gitea-actions.sh
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apk add --no-cache curl jq python3 >/dev/null 2>&1 || true
|
||||
for i in $(seq 1 40); do
|
||||
rm -rf repo
|
||||
git clone -q --depth 1 http://192.168.30.33:3000/TomHanck4/dsh-easy-desktop.git repo 2>/dev/null || { sleep 5; continue; }
|
||||
if grep -qF 'export GITEA_OWNER="$REPO_OWNER"' repo/scripts/publish-gitea-actions.sh 2>/dev/null; then
|
||||
echo "publish scripts fresh (attempt ${i})"
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
grep -qF 'export GITEA_OWNER="$REPO_OWNER"' repo/scripts/publish-gitea-actions.sh || {
|
||||
echo "mirror still stale after ~6 minutes" >&2
|
||||
exit 1
|
||||
}
|
||||
cd repo
|
||||
bash scripts/publish-gitea-actions.sh
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
GITEA_BASE_URL: http://192.168.30.33:3000
|
||||
RELEASE_TAG: ${{ github.event.inputs.tag }}
|
||||
working-directory: /tmp
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -949,7 +949,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dsh-core"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
@@ -966,7 +966,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dsh-desktop"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"dsh-core",
|
||||
|
||||
@@ -3,7 +3,7 @@ members = ["crates/dsh-core", "src-tauri"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
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` |
|
||||
| macOS | Apple Silicon / Intel `.dmg` | 未公证,首次打开需在「隐私与安全性」允许 + 本机 `dsh` |
|
||||
| Linux | `.deb` / `.rpm` | WebKitGTK 4.1 + 本机 `dsh` |
|
||||
| Windows | NSIS `.exe` / `.msi` | [WebView2](https://developer.microsoft.com/microsoft-edge/webview2/)(安装器可引导下载)+ Node.js/npm(首次启动自动安装 `dsh` 到内置目录) |
|
||||
| macOS | Apple Silicon / Intel `.dmg` | 未公证,首次打开需在「隐私与安全性」允许 + Node.js/npm(首次启动自动安装 `dsh` 到内置目录) |
|
||||
| Linux | `.deb` / `.rpm` | WebKitGTK 4.1 + Node.js/npm(首次启动自动安装 `dsh` 到内置目录) |
|
||||
| Linux Flatpak | `.flatpak` | **零依赖**:自带 Node.js 24 与 `@deepseek-ai/dsh` |
|
||||
|
||||
除 Flatpak 外,需要先安装 `dsh`:
|
||||
除 Flatpak 外,需要本机有 Node.js/npm,用于首次启动自动下载并安装 `dsh`。也可以提前安装或指定路径:
|
||||
|
||||
```bash
|
||||
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 {
|
||||
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 {
|
||||
@@ -59,6 +68,18 @@ pub fn find_npx_dsh_bins() -> Vec<PathBuf> {
|
||||
if let Some(globbed) = pattern.to_str() {
|
||||
if let Ok(paths) = glob_simple(globbed) {
|
||||
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) {
|
||||
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();
|
||||
|
||||
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(|| {
|
||||
if self.in_flatpak {
|
||||
DshNotFound::new(
|
||||
@@ -170,13 +196,22 @@ impl DshLauncher {
|
||||
)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let mut argv = self.resolve()?;
|
||||
argv.extend([
|
||||
@@ -406,6 +441,15 @@ mod tests {
|
||||
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]
|
||||
fn invalid_env_override_raises() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
|
||||
@@ -41,6 +41,15 @@
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<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">
|
||||
<description>
|
||||
<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 镜像,签名校验后生效)
|
||||
@@ -7,6 +7,8 @@ set -euo pipefail
|
||||
GITHUB_REPO="${GITHUB_REPO:-TommyFang2077/dsh-easy-desktop}"
|
||||
REPO_OWNER="${GITEA_OWNER:-TomHanck4}"
|
||||
REPO_NAME="${GITEA_REPO:-dsh-easy-desktop}"
|
||||
export GITEA_OWNER="$REPO_OWNER"
|
||||
export GITEA_REPO="$REPO_NAME"
|
||||
GITEA_BASE_URL="${GITEA_BASE_URL:-http://192.168.30.33:3000}"
|
||||
PACKAGE_BASE_URL="${PACKAGE_BASE_URL:-https://git.fangsiyuan.top/api/packages/TomHanck4/generic/dsh-easy-desktop-updater}"
|
||||
RELEASE_TAG="${RELEASE_TAG:-latest}"
|
||||
@@ -20,12 +22,14 @@ fi
|
||||
exit 1
|
||||
}
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
export RELEASE_VERSION="$VERSION"
|
||||
echo "target: $RELEASE_TAG ($VERSION)"
|
||||
echo "gitea: $GITEA_BASE_URL"
|
||||
|
||||
if curl -fsS -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_BASE_URL/api/packages/$REPO_OWNER/generic/dsh-easy-desktop-updater/$VERSION/dsh-easy-desktop_${VERSION}_darwin_aarch64.app.tar.gz" >/dev/null 2>&1; then
|
||||
echo "version $VERSION already published on the Gitea package feed; nothing to do"
|
||||
"$GITEA_BASE_URL/api/packages/$REPO_OWNER/generic/dsh-easy-desktop-updater/latest/latest.json" -o /tmp/latest.json 2>/dev/null \
|
||||
&& [ "$(jq -r '.version' /tmp/latest.json)" = "$VERSION" ]; then
|
||||
echo "version $VERSION already published on the Gitea feed; nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -33,18 +37,24 @@ rm -rf artifacts staged
|
||||
mkdir -p artifacts
|
||||
|
||||
curl -fsS "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$RELEASE_TAG" -o /tmp/release.json
|
||||
jq -r '.assets[].browser_download_url' /tmp/release.json | while read -r url; do
|
||||
jq -r '.assets[].browser_download_url' /tmp/release.json > /tmp/asset-urls.txt
|
||||
while read -r url; do
|
||||
curl -fsSL --retry 3 --retry-delay 5 -o "artifacts/$(basename "$url")" "$url" &
|
||||
done
|
||||
done < /tmp/asset-urls.txt
|
||||
wait
|
||||
echo "downloaded $(find artifacts -type f | wc -l) assets"
|
||||
COUNT=$(find artifacts -type f | wc -l)
|
||||
echo "downloaded $COUNT assets"
|
||||
[ "$COUNT" -ge 15 ] || {
|
||||
echo "expected at least 15 release assets, got $COUNT" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
python3 build-gitea-update.py \
|
||||
python3 scripts/build-gitea-update.py \
|
||||
--flat-artifacts artifacts \
|
||||
--output staged \
|
||||
--version "$VERSION" \
|
||||
--pub-date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--package-base-url "$PACKAGE_BASE_URL"
|
||||
|
||||
bash publish-gitea-release.sh staged
|
||||
bash scripts/publish-gitea-release.sh staged
|
||||
echo "published $RELEASE_TAG to Gitea"
|
||||
@@ -57,7 +57,10 @@ else
|
||||
release_body="大陆镜像安装包;文件与 GitHub Release 同源。应用内更新包由 Tauri 签名校验。"
|
||||
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')
|
||||
if [[ -z "$release_id" ]]; then
|
||||
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 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() {
|
||||
Ok(p) => Arc::new(p),
|
||||
Err(err) => {
|
||||
let detail = bootstrap_message
|
||||
.map(|message| format!("{message}\n{err}"))
|
||||
.unwrap_or_default();
|
||||
let _ = app.emit(
|
||||
"error",
|
||||
ErrorPayload {
|
||||
message: err.to_string(),
|
||||
detail: String::new(),
|
||||
detail,
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user