mirror of
https://github.com/TommyFang2077/dsh-desktop.git
synced 2026-08-17 09:06:36 +08:00
Compare commits
10 Commits
16c80dfa45
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b15292dd03 | |||
| 5e9ac59c59 | |||
| 97e5ae50b5 | |||
| c71a87c92a | |||
| 303e64790b | |||
| c94374b62a | |||
| f1b41e989a | |||
| 33186b2ef9 | |||
| 6a3cd55f88 | |||
| 862382daf6 |
25
.gitea/workflows/publish.yml
Normal file
25
.gitea/workflows/publish.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
name: publish-gitea
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "GitHub release tag to publish (default: latest)"
|
||||
required: false
|
||||
default: latest
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
steps:
|
||||
- name: Download publish scripts
|
||||
run: |
|
||||
BASE=https://raw.githubusercontent.com/TommyFang2077/dsh-easy-desktop/$GITHUB_SHA
|
||||
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
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
GITEA_BASE_URL: http://192.168.30.33:3000
|
||||
RELEASE_TAG: ${{ github.event.inputs.tag }}
|
||||
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -41,6 +41,10 @@ jobs:
|
||||
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
||||
test -f "docs/release-notes/v${VERSION}.md" || {
|
||||
echo "docs/release-notes/v${VERSION}.md is required for a release (see docs/release-notes/v0.1.1.md)" >&2
|
||||
exit 1
|
||||
}
|
||||
if [ "${GITHUB_REF_TYPE}" = "tag" ] && [ "${GITHUB_REF_NAME}" != "v${VERSION}" ]; then
|
||||
echo "Git tag ${GITHUB_REF_NAME} must match Cargo.toml version v${VERSION}" >&2
|
||||
exit 1
|
||||
@@ -125,12 +129,14 @@ jobs:
|
||||
shell: bash
|
||||
run: bash scripts/vendor-native.sh
|
||||
|
||||
- name: Export embedded updater public key
|
||||
run: echo "DSH_DESKTOP_UPDATER_PUBKEY=$(jq -r '.plugins.updater.pubkey' src-tauri/tauri.conf.json)" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: tauri-apps/tauri-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
DSH_DESKTOP_UPDATER_PUBKEY: ${{ secrets.DSH_DESKTOP_UPDATER_PUBKEY }}
|
||||
with:
|
||||
projectPath: .
|
||||
tagName: v__VERSION__
|
||||
@@ -208,6 +214,22 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release-notes:
|
||||
name: Release notes
|
||||
needs: [version, package, flatpak]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set structured release body
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.version.outputs.tag }}
|
||||
name: DeepSeek Harness Desktop v${{ needs.version.outputs.version }}
|
||||
body_path: docs/release-notes/v${{ needs.version.outputs.version }}.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-gitea:
|
||||
name: Publish Gitea mirror
|
||||
needs: [version, package, flatpak]
|
||||
@@ -229,6 +251,7 @@ jobs:
|
||||
--output staged \
|
||||
--version "$VERSION" \
|
||||
--pub-date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--notes "$(python3 scripts/release-notes.py notes "docs/release-notes/v${VERSION}.md")" \
|
||||
--package-base-url "https://git.fangsiyuan.top/api/packages/TomHanck4/generic/dsh-easy-desktop-updater"
|
||||
|
||||
- name: Publish Gitea release and updater feed
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -949,7 +949,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dsh-core"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
@@ -966,7 +966,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dsh-desktop"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"dsh-core",
|
||||
|
||||
@@ -3,7 +3,7 @@ members = ["crates/dsh-core", "src-tauri"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["TommyFang2077"]
|
||||
|
||||
12
Makefile
12
Makefile
@@ -4,7 +4,7 @@ PREFIX ?= $(HOME)/.local
|
||||
APP_ID := io.github.tommyfang.DshDesktop
|
||||
DSH_VERSION := 0.1.0-rc.6
|
||||
MODLENS_VERSION := 3.16.6
|
||||
MARKET_VERSION := 1.9.0
|
||||
MARKET_VERSION := 1.10.1
|
||||
ANCHORED_COMMIT := ffb845c5480adc953392a6db6f8a98ede621174b
|
||||
ANCHORED_REPO := https://github.com/xiaobright/dsh-anchored-standard.git
|
||||
VENDOR_DIR := vendor/dsh-prefix
|
||||
@@ -21,6 +21,11 @@ VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
|
||||
BUNDLE := dist/$(APP_ID)-$(VERSION).flatpak
|
||||
BIN := target/release/dsh-desktop
|
||||
|
||||
# The shell updater gates the update check on an embedded public key
|
||||
# (option_env in shell_updater.rs). Single source of truth is the tauri
|
||||
# config; without it every build silently disables in-app updates.
|
||||
export DSH_DESKTOP_UPDATER_PUBKEY := $(shell jq -r '.plugins.updater.pubkey' src-tauri/tauri.conf.json)
|
||||
|
||||
.PHONY: all run dev test vendor vendor-native vendor-anchored build install uninstall flatpak-build flatpak-export flatpak-install flatpak-bundle flatpak-run clean
|
||||
|
||||
all: test
|
||||
@@ -89,11 +94,6 @@ install: build
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \
|
||||
cp -R $(MARKET_DIR) $(DESTDIR)$(PREFIX)/share/dsh-desktop/market; \
|
||||
fi
|
||||
if [ -f plugins/dsh-desktop-vision/package.json ]; then \
|
||||
rm -rf $(DESTDIR)$(PREFIX)/share/dsh-desktop/vision; \
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \
|
||||
cp -R plugins/dsh-desktop-vision $(DESTDIR)$(PREFIX)/share/dsh-desktop/vision; \
|
||||
fi
|
||||
if [ -f plugins/dsh-desktop-voice/package.json ]; then \
|
||||
rm -rf $(DESTDIR)$(PREFIX)/share/dsh-desktop/voice; \
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \
|
||||
|
||||
10
README.md
10
README.md
@@ -50,9 +50,9 @@ dsh 原本运行在浏览器标签页中;本项目用 Tauri 2 和系统 WebVie
|
||||
|
||||
### 视觉模型配置:给 DeepSeek 带上眼睛
|
||||
|
||||
纯文本 DeepSeek 配合视觉桥后,可以直接粘贴截图识别内容。内置的 **设置 → 视觉模型** 页面集中配置 OpenAI 兼容接口、Gemini API、Anthropic API、Antigravity CLI 和 Claude Code 登录;只展示当前引擎需要的字段,密钥保存在本机配置中,相关外链由系统浏览器打开。
|
||||
纯文本 DeepSeek 配合视觉桥后,可以直接粘贴截图识别内容。引擎配置在 **设置 → 插件 → 插件配置 → 视觉引擎(ModLens)**:支持 OpenAI 兼容接口、Gemini API、Anthropic API、Antigravity CLI 和 Claude Code 登录,只展示当前引擎需要的字段,密钥保存在本机 `~/.modlens/config.json` 中,相关外链由系统浏览器打开。
|
||||
|
||||

|
||||

|
||||
|
||||
### 锚定模式与原生窗口
|
||||
|
||||
@@ -98,9 +98,8 @@ npm install -g @deepseek-ai/dsh
|
||||
| --- | --- | --- | --- |
|
||||
| DeepSeek Harness | `0.1.0-rc.6` | 官方 WebUI;Flatpak 内置,其他安装包调用本机 `dsh` | `~/.dsh` |
|
||||
| 离线语音 | `dsh-desktop-voice 0.4.0` | 麦克风、快捷键、SenseVoice / OpenAI 兼容听写 | 配置 `~/.config/dsh-desktop/voice.json`;模型在系统缓存目录 |
|
||||
| 插件市场 | `dshmarket 1.9.0` | 社区插件的发现、安装和更新 | `~/.dsh/profiles/web` |
|
||||
| 视觉配置 | `dsh-desktop-vision 0.1.4` | 配置视觉桥所使用的引擎、接口和模型 | `~/.modlens/config.json` |
|
||||
| 视觉桥 | `ModLens 3.16.6` | 让纯文本模型读取粘贴的图片;可从市场更新 | `~/.dsh/profiles/web` |
|
||||
| 插件市场 | `dshmarket 1.10.1` | 社区插件的发现、安装和更新 | `~/.dsh/profiles/web` |
|
||||
| 视觉桥 | `ModLens 3.16.6` | 让纯文本模型读取粘贴的图片;可从市场更新;引擎配置在「设置 → 插件」 | `~/.dsh/profiles/web`;配置 `~/.modlens/config.json` |
|
||||
| 锚定预设 | `ffb845c5480a` | 锚定式标准与零工具锚定式标准 | `~/.dsh/.agent-presets/` |
|
||||
|
||||
应用启动时会同步桌面自带组件,但会保留用户从市场更新到更新版本的 ModLens。捆绑版本固定在 [Makefile](Makefile) 中。
|
||||
@@ -174,7 +173,6 @@ dsh-desktop/
|
||||
├── ui/ # 启动页 + 注入到 WebUI 的标题栏
|
||||
├── src-tauri/ # Tauri 窗口、命令、deb/rpm/nsis/dmg
|
||||
├── crates/dsh-core/ # 启动 / 更新 / ModLens / 预设 / 剪贴板
|
||||
├── plugins/dsh-desktop-vision/ # 设置 → 视觉模型
|
||||
├── plugins/dsh-desktop-voice/ # 设置 → 语音输入 + 对话框麦克风
|
||||
├── data/ # .desktop、图标、AppStream
|
||||
├── flatpak/
|
||||
|
||||
@@ -14,9 +14,8 @@ DeepSeek, liustack, or xiaobright.
|
||||
| DeepSeek Harness (`@deepseek-ai/dsh`) | [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) | `0.1.0-rc.6` | MIT, © 2026 DeepSeek | Official WebUI. Not shipped as source. Flatpak vendors the npm package; other packages call a host `dsh`. See `docs/licenses/deepseek-harness.LICENSE`. |
|
||||
| ModLens (`@liustack/modlens`) | [liustack/modlens](https://github.com/liustack/modlens) | `3.16.6` | MIT, © 2026 Leon Liu (liustack) | Copied into `~/.dsh/profiles/web` so text-only models can read images. See `docs/licenses/modlens.LICENSE`. |
|
||||
| Anchored Standard | [xiaobright/dsh-anchored-standard](https://github.com/xiaobright/dsh-anchored-standard) | commit `ffb845c5480adc953392a6db6f8a98ede621174b` | MIT, © 2026 xiaobright; portions © 2026 DeepSeek | Localized as **锚定式标准(实验)** and **零工具锚定式标准(实验)**, written to `~/.dsh/.agent-presets/`. See `docs/licenses/dsh-anchored-standard.LICENSE` and `.NOTICE`. |
|
||||
| `dsh-desktop-vision` | this repo `plugins/dsh-desktop-vision/` | `0.1.4` | MIT, © 2026 TommyFang2077 | Settings page **设置 → 视觉模型**; writes `~/.modlens/config.json`. |
|
||||
| `dsh-desktop-voice` | this repo `plugins/dsh-desktop-voice/` | `0.4.0` | MIT, © 2026 TommyFang2077 | Settings page **设置 → 语音输入** and composer mic; writes `~/.config/dsh-desktop/voice.json`. |
|
||||
| dshmarket | [dsh-market/dsh-market](https://github.com/dsh-market/dsh-market) | `1.9.0` | MIT, © 2026 fkysly and dsh-market contributors | Bundled WebUI market for browsing and installing Community plugins. See `docs/licenses/dshmarket.LICENSE`. |
|
||||
| dshmarket | [dsh-market/dsh-market](https://github.com/dsh-market/dsh-market) | `1.10.1` | MIT, © 2026 fkysly and dsh-market contributors | Bundled WebUI market for browsing and installing Community plugins. See `docs/licenses/dshmarket.LICENSE`. |
|
||||
| SenseVoiceSmall ONNX | [FunAudioLLM/SenseVoice](https://github.com/FunAudioLLM/SenseVoice) via [k2-fsa conversion](https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17) | commit `2365baeacb507f821a0c8120fcee3d484dba7a07` | MIT, © 2025 FunASR | Not bundled. Downloaded after the user confirms, then stored in the platform cache. See `docs/licenses/sensevoice.LICENSE`. |
|
||||
| sherpa-onnx WASM | [k2-fsa/sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) | `1.13.5` | Apache-2.0 | Not bundled. Installed with the model to run SenseVoice locally; the npm package carries its upstream license. |
|
||||
|
||||
|
||||
@@ -7,13 +7,11 @@ use serde_json::{json, Value};
|
||||
use crate::paths::{copy_tree, dsh_home, replace_symlink, BundledPaths};
|
||||
|
||||
pub const PACKAGE: &str = "@liustack/modlens";
|
||||
pub const VISION_PACKAGE: &str = "dsh-desktop-vision";
|
||||
pub const VOICE_PACKAGE: &str = "dsh-desktop-voice";
|
||||
pub const MARKET_PACKAGE: &str = "dshmarket";
|
||||
pub const MODLENS_VERSION: &str = "3.16.6";
|
||||
const VISION_PLUGIN_VERSION: &str = "0.1.4";
|
||||
const VOICE_PLUGIN_VERSION: &str = "0.4.0";
|
||||
const MARKET_PLUGIN_VERSION: &str = "1.9.0";
|
||||
const MARKET_PLUGIN_VERSION: &str = "1.10.1";
|
||||
const MANAGED_BUNDLES_DIR: &str = ".dsh-desktop/bundles";
|
||||
pub const HIDE_PLAIN_TWINS_JS: &str = include_str!("../../../ui/inject/hide-twins.js");
|
||||
|
||||
@@ -78,13 +76,6 @@ fn bundled_plugin(paths: &BundledPaths, dirs: &[&str]) -> Option<PathBuf> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bundled_vision_plugin(paths: &BundledPaths) -> Option<PathBuf> {
|
||||
bundled_plugin(
|
||||
paths,
|
||||
&["vision", "dsh-desktop-vision", "plugins/dsh-desktop-vision"],
|
||||
)
|
||||
}
|
||||
|
||||
fn bundled_voice_plugin(paths: &BundledPaths) -> Option<PathBuf> {
|
||||
bundled_plugin(
|
||||
paths,
|
||||
@@ -164,15 +155,6 @@ fn install_profile_plugin(
|
||||
true
|
||||
}
|
||||
|
||||
fn install_vision_plugin(paths: &BundledPaths, profile: &Path) -> bool {
|
||||
install_profile_plugin(
|
||||
profile,
|
||||
VISION_PACKAGE,
|
||||
VISION_PLUGIN_VERSION,
|
||||
bundled_vision_plugin(paths),
|
||||
)
|
||||
}
|
||||
|
||||
fn install_voice_plugin(paths: &BundledPaths, profile: &Path) -> bool {
|
||||
install_profile_plugin(
|
||||
profile,
|
||||
@@ -435,15 +417,8 @@ fn ensure_modlens_inner(
|
||||
version: &str,
|
||||
) -> std::io::Result<ModlensEnsureResult> {
|
||||
std::fs::create_dir_all(profile)?;
|
||||
let vision_ok = install_vision_plugin(paths, profile);
|
||||
let voice_ok = install_voice_plugin(paths, profile);
|
||||
let mut packages = BTreeMap::new();
|
||||
if vision_ok {
|
||||
packages.insert(
|
||||
VISION_PACKAGE.to_string(),
|
||||
local_plugin_spec(VISION_PACKAGE),
|
||||
);
|
||||
}
|
||||
if voice_ok {
|
||||
packages.insert(VOICE_PACKAGE.to_string(), local_plugin_spec(VOICE_PACKAGE));
|
||||
}
|
||||
@@ -613,7 +588,7 @@ ui-theme:
|
||||
std::fs::create_dir_all(&market).unwrap();
|
||||
std::fs::write(
|
||||
market.join("package.json"),
|
||||
r#"{"name":"dshmarket","version":"1.9.0"}"#,
|
||||
format!(r#"{{"name":"dshmarket","version":"{MARKET_PLUGIN_VERSION}"}}"#),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(market.join("index.js"), "export {}\n").unwrap();
|
||||
@@ -625,7 +600,10 @@ ui-theme:
|
||||
let manifest: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(profile.join("package.json")).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(manifest["dependencies"]["dshmarket"], "1.9.0");
|
||||
assert_eq!(
|
||||
manifest["dependencies"]["dshmarket"],
|
||||
MARKET_PLUGIN_VERSION
|
||||
);
|
||||
assert!(manifest["dsh"]["profile"]["bundles"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
@@ -639,10 +617,7 @@ ui-theme:
|
||||
#[test]
|
||||
fn bundled_local_plugins_use_file_dependencies() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
for (dir, name, version) in [
|
||||
("vision", VISION_PACKAGE, VISION_PLUGIN_VERSION),
|
||||
("voice", VOICE_PACKAGE, VOICE_PLUGIN_VERSION),
|
||||
] {
|
||||
for (dir, name, version) in [("voice", VOICE_PACKAGE, VOICE_PLUGIN_VERSION)] {
|
||||
let plugin = tmp.path().join(dir);
|
||||
std::fs::create_dir_all(&plugin).unwrap();
|
||||
std::fs::write(
|
||||
@@ -661,7 +636,7 @@ ui-theme:
|
||||
let manifest: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(profile.join("package.json")).unwrap())
|
||||
.unwrap();
|
||||
for name in [VISION_PACKAGE, VOICE_PACKAGE] {
|
||||
for name in [VOICE_PACKAGE] {
|
||||
assert_eq!(
|
||||
manifest["dependencies"][name],
|
||||
format!("file:.dsh-desktop/bundles/{name}")
|
||||
|
||||
@@ -41,6 +41,21 @@
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<releases>
|
||||
<release version="0.1.2" date="2026-08-17">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Bundled plugin market upgraded to dshmarket 1.10.1 (more resilient downloads, safer installs)</li>
|
||||
<li>In-app updater enabled: the signing public key is now embedded in every build</li>
|
||||
<li>Removed the duplicate 视觉模型 settings section — the modlens card under Settings → Plugins is the single config surface</li>
|
||||
<li>Structured release notes for every version</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.1" date="2026-08-16">
|
||||
<description>
|
||||
<p>Signed in-app updater from the mainland mirror; HTTPS Gitea distribution; Market install warnings for GitHub-only sources.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.0" date="2026-08-15">
|
||||
<description>
|
||||
<p>Tauri shell with an Apple-style title bar, bundled dsh, and on-launch updates.</p>
|
||||
|
||||
19
docs/release-notes/v0.1.1.md
Normal file
19
docs/release-notes/v0.1.1.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# DeepSeek Harness Desktop v0.1.1
|
||||
|
||||
**摘要 / Summary:** 大陆可直达的发行链路上线:Gitea 镜像安装包 + 应用内签名更新(HTTPS 分发、安装前校验 Tauri 签名)。
|
||||
|
||||
## 新功能 / Features
|
||||
- 大陆发行镜像:安装包与本应用更新走 Gitea HTTPS,无需额外网络工具即可下载
|
||||
- 应用内更新:发现新版本后一键「下载并安装」,安装包下载完成先校验 Tauri 签名再安装
|
||||
- Linux 剪贴板图片原生读取:直接粘贴截图即作为图片进输入框(此前部分桌面环境取不到)
|
||||
|
||||
## 修复 / Fixes
|
||||
- Gitea 发布经过慢速链路时自动重试,不再因网关超时半途失败
|
||||
- 壳更新公钥内嵌进打包配置,签名校验始终可用
|
||||
- 仓库改名后(dsh-easy-desktop)的镜像与文档地址保持一致
|
||||
|
||||
## 安装与更新 / Install & Update
|
||||
- 安装包:Windows NSIS/MSI、macOS DMG(Apple Silicon 与 Intel)、Linux deb/rpm、Flatpak
|
||||
- 除 Flatpak 外需本机已安装 `dsh`(`npm i -g @deepseek-ai/dsh`),或设置 `DSH_DESKTOP_DSH_BIN`
|
||||
- macOS 包未公证:首次打开需在「系统设置 → 隐私与安全性」中允许
|
||||
- 更新提示出现在启动页:点击「下载并安装」即可(走 Gitea 镜像,签名校验后生效)
|
||||
26
docs/release-notes/v0.1.2.md
Normal file
26
docs/release-notes/v0.1.2.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# DeepSeek Harness Desktop v0.1.2
|
||||
|
||||
**摘要 / Summary:** 内置插件市场升级至 dshmarket 1.10.1(下载更稳、安装更安全);应用内更新真正启用(签名公钥内嵌);移除重复的视觉模型设置区;发布说明改为结构化更新日志。
|
||||
|
||||
## 新功能 / Features
|
||||
- 内置插件市场 dshmarket 1.9.0 → 1.10.1:
|
||||
- pnpm 拉取超时自动加重试,弱网下安装/更新不再一次失败就放弃
|
||||
- 失败的安装会回收遗留的本地缓存目录,不会越积越多
|
||||
- 重复安装防护:同一插件(含别名条目)只保留一个加载项,杜绝下次启动冲突
|
||||
- 每个版本提供结构化更新日志(新功能 / 修复 / 变更 / 安装),GitHub 与 Gitea 的发布文案、应用内更新面板自动取同一份
|
||||
|
||||
## 修复 / Fixes
|
||||
- **应用内更新此前在所有构建中都静默关闭**:壳更新器要求编译期内嵌签名公钥,而构建流程从未注入(`public_key() → None → 不检查更新`)。现在本地、CI、Flatpak 构建都会从 `tauri.conf.json` 读取公钥注入,「发现壳更新 → 下载并安装」真正可用
|
||||
- 消除市场「有新版本」横幅与桌面内嵌市场版本不一致的死结:内嵌市场已同步到上游最新,横幅不再出现;此前该更新提示在桌面版中无法真正落地(每次启动会被内嵌版本还原)
|
||||
- 插件安装遇到 pnpm 默认拦截构建脚本(如 node-pty)时,放行与重试路径按上游最新逻辑工作,不再被 allowBuilds 占位符卡死
|
||||
- Gitea 镜像发布链路重构:由自托管 Actions runner 经局域网直连发布,慢速公网链路超时影响正式发布流程
|
||||
|
||||
## 变更 / Changes
|
||||
- 移除独立的「视觉模型」设置区:视觉引擎配置统一由 modlens 自带的「设置 → 插件 → 插件配置 → 视觉引擎(ModLens)」提供(同样读写 `~/.modlens/config.json`,不再有两处重复入口)
|
||||
- 构建产物不再附带 `plugins/dsh-desktop-vision`(license 列表与 README 同步更新)
|
||||
|
||||
## 安装与更新 / Install & Update
|
||||
- 安装包:Windows NSIS/MSI、macOS DMG(Apple Silicon 与 Intel)、Linux deb/rpm、Flatpak
|
||||
- 除 Flatpak 外需本机已安装 `dsh`(`npm i -g @deepseek-ai/dsh`),或设置 `DSH_DESKTOP_DSH_BIN`
|
||||
- macOS 包未公证:首次打开需在「系统设置 → 隐私与安全性」中允许
|
||||
- 更新提示出现在启动页:点击「下载并安装」即可(走 Gitea 镜像,签名校验后生效)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 1.5 MiB |
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN" class="dsh-desktop-offset">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>视觉模型</title>
|
||||
<title>视觉引擎(ModLens)</title>
|
||||
<link rel="stylesheet" href="chrome.css" />
|
||||
<style>
|
||||
:root {
|
||||
@@ -74,15 +74,15 @@
|
||||
<h2>设置</h2>
|
||||
<a class="muted" href="#">通用</a>
|
||||
<a class="muted" href="#">模型</a>
|
||||
<a class="active" href="#">视觉模型</a>
|
||||
<a class="muted" href="#">插件</a>
|
||||
<a class="muted" href="#">语音输入</a>
|
||||
<a class="active" href="#">插件</a>
|
||||
<a class="muted" href="#">技能</a>
|
||||
</nav>
|
||||
<section class="content">
|
||||
<h1>视觉模型</h1>
|
||||
<p class="lead">内置插件 dsh-desktop-vision · 写入 ~/.modlens/config.json</p>
|
||||
<h1>插件 · 插件配置</h1>
|
||||
<p class="lead">视觉引擎(ModLens)由 modlens 插件提供 · 写入 ~/.modlens/config.json</p>
|
||||
<div class="dshdv">
|
||||
<h3>ModLens 视觉模型</h3>
|
||||
<h3>视觉引擎(ModLens)</h3>
|
||||
<p>纯文本对话模型读图时使用这里的引擎。已声明视觉能力的模型(如 Qwen)不会走这条桥。</p>
|
||||
<label>
|
||||
引擎
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 36 KiB |
@@ -53,10 +53,9 @@ modules:
|
||||
- --share=network
|
||||
build-commands:
|
||||
- mkdir -p vendor/modlens vendor/dshmarket vendor/anchored-standard vendor/zero-anchored-standard
|
||||
- export DSH_DESKTOP_UPDATER_PUBKEY=$(python3 -c "import json;print(json.load(open('src-tauri/tauri.conf.json'))['plugins']['updater']['pubkey'])")
|
||||
- cargo build --release --locked --offline || cargo build --release
|
||||
- install -Dm755 target/release/dsh-desktop ${FLATPAK_DEST}/bin/dsh-desktop
|
||||
- mkdir -p ${FLATPAK_DEST}/share/dsh-desktop/vision
|
||||
- cp -a plugins/dsh-desktop-vision/. ${FLATPAK_DEST}/share/dsh-desktop/vision
|
||||
- mkdir -p ${FLATPAK_DEST}/share/dsh-desktop/voice
|
||||
- cp -a plugins/dsh-desktop-voice/. ${FLATPAK_DEST}/share/dsh-desktop/voice
|
||||
- install -Dm644 data/applications/io.github.tommyfang.DshDesktop.desktop ${FLATPAK_DEST}/share/applications/io.github.tommyfang.DshDesktop.desktop
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
window.__ModuleLoader__.load({
|
||||
id: 'dsh-desktop-vision',
|
||||
factory: (require) => {
|
||||
var module = { exports: {} }
|
||||
var exports = module.exports
|
||||
var React = require('react')
|
||||
var jsx = require('react/jsx-runtime')
|
||||
|
||||
var css = [
|
||||
'.dshdv{width:100%;max-width:640px;display:flex;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary)}',
|
||||
'.dshdv h3{margin:0;font-size:15px;font-weight:600;line-height:22px}',
|
||||
'.dshdv p{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}',
|
||||
'.dshdv label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
||||
'.dshdv .head{display:flex;align-items:center;justify-content:space-between;gap:12px}',
|
||||
'.dshdv a{color:var(--dsw-alias-state-business-primary);text-decoration:none;font-size:12px;line-height:18px}',
|
||||
'.dshdv a:hover{text-decoration:underline}',
|
||||
'.dshdv input,.dshdv select{height:36px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font:inherit}',
|
||||
'.dshdv input:focus,.dshdv select:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}',
|
||||
'.dshdv button{align-self:flex-start;height:32px;padding:0 14px;border:0;border-radius:8px;background:var(--dsw-alias-state-business-primary);color:#fff;font:inherit;cursor:pointer}',
|
||||
'.dshdv button:disabled{opacity:.55;cursor:default}',
|
||||
'.dshdv .ok{color:var(--dsw-alias-state-success-primary)}',
|
||||
'.dshdv .err{color:var(--dsw-alias-state-error-primary)}',
|
||||
].join('')
|
||||
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined') return
|
||||
if (document.querySelector('style[data-plugin-css="dsh-desktop-vision"]')) return
|
||||
var tag = document.createElement('style')
|
||||
tag.dataset.pluginCss = 'dsh-desktop-vision'
|
||||
tag.textContent = css
|
||||
document.head.appendChild(tag)
|
||||
}
|
||||
|
||||
function remote(provider) {
|
||||
var q = provider ? '?provider=' + encodeURIComponent(provider) : ''
|
||||
return fetch('/dsh-desktop/modlens' + q).then(function (res) {
|
||||
if (!res.ok) throw new Error('load failed ' + res.status)
|
||||
return res.json()
|
||||
})
|
||||
}
|
||||
|
||||
function VisionSettings() {
|
||||
ensureCss()
|
||||
var state = React.useState({ status: 'loading' })
|
||||
var snap = state[0]
|
||||
var setSnap = state[1]
|
||||
React.useEffect(function () {
|
||||
var live = true
|
||||
remote()
|
||||
.then(function (form) {
|
||||
if (live) setSnap({ status: 'ready', form: form, message: '' })
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (live) setSnap({ status: 'error', message: String(error.message || error) })
|
||||
})
|
||||
return function () {
|
||||
live = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
function patch(field, value) {
|
||||
setSnap(function (cur) {
|
||||
if (cur.status !== 'ready') return cur
|
||||
return { status: 'ready', form: Object.assign({}, cur.form, { [field]: value }), message: '' }
|
||||
})
|
||||
}
|
||||
|
||||
function onProvider(id) {
|
||||
remote(id)
|
||||
.then(function (form) {
|
||||
setSnap({ status: 'ready', form: form, message: '' })
|
||||
})
|
||||
.catch(function () {
|
||||
patch('provider', id)
|
||||
})
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (snap.status !== 'ready' || snap.saving) return
|
||||
setSnap(Object.assign({}, snap, { saving: true, message: '' }))
|
||||
fetch('/dsh-desktop/modlens', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(snap.form),
|
||||
})
|
||||
.then(function (res) {
|
||||
return res.json().then(function (body) {
|
||||
if (!res.ok) throw new Error(body.error || 'save failed')
|
||||
return body
|
||||
})
|
||||
})
|
||||
.then(function (form) {
|
||||
setSnap({ status: 'ready', form: form, message: '已保存', saving: false })
|
||||
})
|
||||
.catch(function (error) {
|
||||
setSnap(Object.assign({}, snap, { saving: false, message: String(error.message || error) }))
|
||||
})
|
||||
}
|
||||
|
||||
if (snap.status === 'loading') {
|
||||
return jsx.jsx('div', { className: 'dshdv', children: jsx.jsx('p', { children: '正在读取 ModLens 配置…' }) })
|
||||
}
|
||||
if (snap.status === 'error') {
|
||||
return jsx.jsx('div', { className: 'dshdv', children: jsx.jsx('p', { className: 'err', children: snap.message }) })
|
||||
}
|
||||
var form = snap.form
|
||||
var remoteEngine = form.provider === 'openai' || form.provider === 'gemini-api' || form.provider === 'anthropic'
|
||||
var options = form.options || []
|
||||
var current = options.filter(function (item) { return item.id === form.provider })[0]
|
||||
var apiUrl = (current && current.apiUrl) || form.apiUrl || ''
|
||||
var example = (current && current.example) || form.example || ''
|
||||
var getApi = apiUrl
|
||||
? jsx.jsx('a', { href: apiUrl, children: '获取 API' })
|
||||
: null
|
||||
return jsx.jsxs('div', {
|
||||
className: 'dshdv',
|
||||
children: [
|
||||
jsx.jsx('h3', { children: 'ModLens 视觉模型' }),
|
||||
jsx.jsx('p', {
|
||||
children:
|
||||
'纯文本对话模型读图时使用这里的引擎。已声明视觉能力的模型(如 Qwen)不会走这条桥。',
|
||||
}),
|
||||
jsx.jsxs('label', {
|
||||
children: [
|
||||
'引擎',
|
||||
jsx.jsx('select', {
|
||||
value: form.provider,
|
||||
onChange: function (event) {
|
||||
onProvider(event.target.value)
|
||||
},
|
||||
children: options.map(function (item) {
|
||||
return jsx.jsx('option', { value: item.id, children: item.label }, item.id)
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
jsx.jsxs('label', {
|
||||
children: [
|
||||
'接口地址',
|
||||
jsx.jsx('input', {
|
||||
value: form.baseUrl || '',
|
||||
disabled: !remoteEngine,
|
||||
placeholder: form.officialBaseUrl || '',
|
||||
onChange: function (event) {
|
||||
patch('baseUrl', event.target.value)
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
jsx.jsxs('label', {
|
||||
children: [
|
||||
jsx.jsxs('span', { className: 'head', children: ['API 密钥', getApi] }),
|
||||
jsx.jsx('input', {
|
||||
type: 'password',
|
||||
value: form.apiKey || '',
|
||||
disabled: !remoteEngine,
|
||||
onChange: function (event) {
|
||||
patch('apiKey', event.target.value)
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
jsx.jsxs('label', {
|
||||
children: [
|
||||
'视觉模型',
|
||||
jsx.jsx('input', {
|
||||
value: form.model || '',
|
||||
placeholder: example ? '例如 ' + example : '',
|
||||
onChange: function (event) {
|
||||
patch('model', event.target.value)
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
jsx.jsx('button', {
|
||||
type: 'button',
|
||||
disabled: !!snap.saving,
|
||||
onClick: save,
|
||||
children: snap.saving ? '保存中…' : '保存',
|
||||
}),
|
||||
snap.message
|
||||
? jsx.jsx('p', {
|
||||
className: /失败|failed|error/i.test(snap.message) ? 'err' : 'ok',
|
||||
children: snap.message,
|
||||
})
|
||||
: null,
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
function apply(ctx) {
|
||||
ctx.slots.inject('settings.section', function () {
|
||||
return ctx.slots.register(
|
||||
{
|
||||
name: 'settings.section',
|
||||
id: 'modlens-vision',
|
||||
order: 12,
|
||||
label: '视觉模型',
|
||||
},
|
||||
VisionSettings,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
exports.apply = apply
|
||||
exports.inject = ['slots']
|
||||
return module.exports
|
||||
},
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
# Settings page + host route for the ModLens vision engine.
|
||||
- insert:
|
||||
- id: dsh-desktop-vision
|
||||
name: dsh-desktop-vision
|
||||
@@ -1,181 +0,0 @@
|
||||
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
export const name = 'dsh-desktop-vision'
|
||||
export const inject = []
|
||||
|
||||
const PROVIDERS = [
|
||||
{
|
||||
id: 'openai',
|
||||
label: 'OpenAI 兼容',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
apiUrl: 'https://platform.openai.com/api-keys',
|
||||
example: 'gpt-4o',
|
||||
},
|
||||
{
|
||||
id: 'gemini-api',
|
||||
label: 'Gemini API',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
apiUrl: 'https://aistudio.google.com/apikey',
|
||||
example: 'gemini-3.6-flash',
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
label: 'Anthropic API',
|
||||
baseUrl: 'https://api.anthropic.com',
|
||||
apiUrl: 'https://console.anthropic.com/settings/keys',
|
||||
example: 'claude-haiku-4-5-20251001',
|
||||
},
|
||||
{
|
||||
id: 'antigravity-cli',
|
||||
label: 'Antigravity CLI(免费)',
|
||||
baseUrl: '',
|
||||
apiUrl: 'https://antigravity.google/',
|
||||
example: 'gemini-3.6-flash-low',
|
||||
},
|
||||
{
|
||||
id: 'claude-cli',
|
||||
label: 'Claude Code 登录',
|
||||
baseUrl: '',
|
||||
apiUrl: 'https://code.claude.com',
|
||||
example: 'haiku',
|
||||
},
|
||||
]
|
||||
const PROVIDER_IDS = new Set(PROVIDERS.map((item) => item.id))
|
||||
|
||||
function providerOf(id) {
|
||||
return PROVIDERS.find((entry) => entry.id === id)
|
||||
}
|
||||
|
||||
function officialBaseUrl(provider) {
|
||||
const item = providerOf(provider)
|
||||
return item ? item.baseUrl : ''
|
||||
}
|
||||
|
||||
function officialApiUrl(provider) {
|
||||
const item = providerOf(provider)
|
||||
return item ? item.apiUrl : ''
|
||||
}
|
||||
|
||||
function officialExample(provider) {
|
||||
const item = providerOf(provider)
|
||||
return item ? item.example : ''
|
||||
}
|
||||
const FORM_FIELDS = ['apiKey', 'baseUrl', 'model']
|
||||
|
||||
function configPath() {
|
||||
const home = process.env.MODLENS_HOME || join(homedir(), '.modlens')
|
||||
return join(home, 'config.json')
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(configPath(), 'utf8'))
|
||||
return data && typeof data === 'object' ? data : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function formOf(cfg, provider) {
|
||||
const id = PROVIDER_IDS.has(provider) ? provider : 'openai'
|
||||
const entry =
|
||||
cfg.providers && typeof cfg.providers === 'object' && typeof cfg.providers[id] === 'object'
|
||||
? cfg.providers[id]
|
||||
: {}
|
||||
return {
|
||||
provider: id,
|
||||
apiKey: String(entry.apiKey || ''),
|
||||
baseUrl: String(entry.baseUrl || officialBaseUrl(id)),
|
||||
officialBaseUrl: officialBaseUrl(id),
|
||||
apiUrl: officialApiUrl(id),
|
||||
example: officialExample(id),
|
||||
model: String(entry.model || ''),
|
||||
}
|
||||
}
|
||||
|
||||
function saveForm(values) {
|
||||
const provider = String(values.provider || 'openai').trim()
|
||||
if (!PROVIDER_IDS.has(provider)) {
|
||||
const error = new Error(`unknown provider: ${provider}`)
|
||||
error.status = 400
|
||||
throw error
|
||||
}
|
||||
const cfg = loadConfig()
|
||||
cfg.provider = provider
|
||||
if (!cfg.providers || typeof cfg.providers !== 'object') cfg.providers = {}
|
||||
if (!cfg.providers[provider] || typeof cfg.providers[provider] !== 'object') {
|
||||
cfg.providers[provider] = {}
|
||||
}
|
||||
const entry = cfg.providers[provider]
|
||||
for (const field of FORM_FIELDS) {
|
||||
const raw = String(values[field] || '').trim()
|
||||
if (raw) entry[field] = raw
|
||||
else delete entry[field]
|
||||
}
|
||||
const path = configPath()
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`, { encoding: 'utf8' })
|
||||
try {
|
||||
chmodSync(path, 0o600)
|
||||
} catch {
|
||||
// best-effort; some filesystems ignore mode
|
||||
}
|
||||
return formOf(cfg, provider)
|
||||
}
|
||||
|
||||
async function readJsonBody(req) {
|
||||
const chunks = []
|
||||
for await (const chunk of req) chunks.push(chunk)
|
||||
const raw = Buffer.concat(chunks).toString('utf8').trim()
|
||||
if (!raw) return {}
|
||||
return JSON.parse(raw)
|
||||
}
|
||||
|
||||
function json(res, status, body) {
|
||||
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
||||
res.end(JSON.stringify(body))
|
||||
}
|
||||
|
||||
export function apply(ctx) {
|
||||
if (typeof ctx.inject !== 'function') return
|
||||
ctx.inject(['webServer'], (scope) => {
|
||||
try {
|
||||
scope.webServer.register({
|
||||
name: 'dsh-desktop-modlens',
|
||||
kind: 'exact',
|
||||
path: '/dsh-desktop/modlens',
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
if (req.method === 'GET') {
|
||||
const cfg = loadConfig()
|
||||
const provider = new URL(req.url, 'http://localhost').searchParams.get('provider')
|
||||
json(res, 200, {
|
||||
...formOf(cfg, provider || cfg.provider || 'openai'),
|
||||
options: PROVIDERS.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
officialBaseUrl: item.baseUrl,
|
||||
apiUrl: item.apiUrl,
|
||||
example: item.example,
|
||||
})),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (req.method === 'PUT' || req.method === 'POST') {
|
||||
const body = await readJsonBody(req)
|
||||
json(res, 200, { ...saveForm(body), saved: true })
|
||||
return
|
||||
}
|
||||
res.writeHead(405).end()
|
||||
} catch (error) {
|
||||
json(res, error.status || 500, { error: String(error?.message || error) })
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`[dsh-desktop-vision] config route skipped: ${error}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "dsh-desktop-vision",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./client": "./client.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
},
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-ui-settings"
|
||||
],
|
||||
"platform": "web",
|
||||
"immediately": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
@@ -39,6 +40,27 @@ INSTALLER_KIND = {
|
||||
"linux": "appimage",
|
||||
"windows": "nsis",
|
||||
}
|
||||
# Asset-name patterns for GitHub Release downloads (flat directory): maps a
|
||||
# fileName to its canonical (os, arch, deliverable suffix).
|
||||
FLAT_PATTERNS: list[tuple[re.Pattern, str, str, str]] = [
|
||||
(re.compile(r"^.*_aarch64\.app\.tar\.gz\.sig$"), "darwin", "aarch64", ".app.tar.gz.sig"),
|
||||
(re.compile(r"^.*_aarch64\.app\.tar\.gz$"), "darwin", "aarch64", ".app.tar.gz"),
|
||||
(re.compile(r"^.*_aarch64\.dmg$"), "darwin", "aarch64", ".dmg"),
|
||||
(re.compile(r"^.*_x64\.app\.tar\.gz\.sig$"), "darwin", "x86_64", ".app.tar.gz.sig"),
|
||||
(re.compile(r"^.*_x64\.app\.tar\.gz$"), "darwin", "x86_64", ".app.tar.gz"),
|
||||
(re.compile(r"^.*_x64\.dmg$"), "darwin", "x86_64", ".dmg"),
|
||||
(re.compile(r"^.*_amd64\.deb\.sig$"), "linux", "x86_64", ".deb.sig"),
|
||||
(re.compile(r"^.*_amd64\.deb$"), "linux", "x86_64", ".deb"),
|
||||
(re.compile(r"^.*_amd64\.AppImage\.sig$"), "linux", "x86_64", ".AppImage.sig"),
|
||||
(re.compile(r"^.*_amd64\.AppImage$"), "linux", "x86_64", ".AppImage"),
|
||||
(re.compile(r"^.*?\.x86_64\.rpm\.sig$"), "linux", "x86_64", ".rpm.sig"),
|
||||
(re.compile(r"^.*?\.x86_64\.rpm$"), "linux", "x86_64", ".rpm"),
|
||||
(re.compile(r"^.*_x64-setup\.exe\.sig$"), "windows", "x86_64", ".exe.sig"),
|
||||
(re.compile(r"^.*_x64-setup\.exe$"), "windows", "x86_64", ".exe"),
|
||||
(re.compile(r"^.*_x64_en-US\.msi\.sig$"), "windows", "x86_64", ".msi.sig"),
|
||||
(re.compile(r"^.*_x64_en-US\.msi$"), "windows", "x86_64", ".msi"),
|
||||
(re.compile(r"^.*\.flatpak$"), "linux", "x86_64", ".flatpak"),
|
||||
]
|
||||
|
||||
|
||||
def artifact_target(path: Path, root: Path) -> tuple[str, str] | None:
|
||||
@@ -73,6 +95,28 @@ def stage_artifacts(source: Path, output: Path, version: str) -> dict[tuple[str,
|
||||
return staged
|
||||
|
||||
|
||||
def stage_flat_artifacts(source: Path, output: Path, version: str) -> dict[tuple[str, str, str], Path]:
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
staged: dict[tuple[str, str, str], Path] = {}
|
||||
for path in sorted(source.iterdir()):
|
||||
if not path.is_file():
|
||||
continue
|
||||
match = next(
|
||||
((pattern, os_name, arch, suffix) for pattern, os_name, arch, suffix in FLAT_PATTERNS if pattern.match(path.name)),
|
||||
None,
|
||||
)
|
||||
if match is None:
|
||||
continue
|
||||
_, os_name, arch, suffix = match
|
||||
key = (os_name, arch, suffix)
|
||||
if key in staged:
|
||||
raise ValueError(f"duplicate {os_name}-{arch}{suffix}: {staged[key]} and {path}")
|
||||
destination = output / f"dsh-easy-desktop_{version}_{os_name}_{arch}{suffix}"
|
||||
shutil.copy2(path, destination)
|
||||
staged[key] = destination
|
||||
return staged
|
||||
|
||||
|
||||
def build_manifest(
|
||||
staged: dict[tuple[str, str, str], Path],
|
||||
version: str,
|
||||
@@ -109,7 +153,8 @@ def build_manifest(
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--artifacts", required=True, type=Path)
|
||||
parser.add_argument("--artifacts", type=Path, help="matrix-layout artifact directory")
|
||||
parser.add_argument("--flat-artifacts", type=Path, help="flat GitHub Release asset directory")
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--version", required=True)
|
||||
parser.add_argument("--package-base-url", required=True)
|
||||
@@ -117,7 +162,12 @@ def main() -> None:
|
||||
parser.add_argument("--pub-date")
|
||||
args = parser.parse_args()
|
||||
|
||||
staged = stage_artifacts(args.artifacts, args.output, args.version)
|
||||
if (args.artifacts is None) == (args.flat_artifacts is None):
|
||||
raise SystemExit("exactly one of --artifacts or --flat-artifacts is required")
|
||||
if args.artifacts is not None:
|
||||
staged = stage_artifacts(args.artifacts, args.output, args.version)
|
||||
else:
|
||||
staged = stage_flat_artifacts(args.flat_artifacts, args.output, args.version)
|
||||
manifest = build_manifest(
|
||||
staged,
|
||||
args.version,
|
||||
|
||||
57
scripts/publish-gitea-actions.sh
Executable file
57
scripts/publish-gitea-actions.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Gitea Actions publisher: download a GitHub release's assets and publish
|
||||
# them to the local Gitea instance over the LAN interface (bypassing the
|
||||
# public reverse proxy, which times out on large uploads).
|
||||
set -euo pipefail
|
||||
|
||||
GITHUB_REPO="${GITHUB_REPO:-TommyFang2077/dsh-easy-desktop}"
|
||||
REPO_OWNER="${GITEA_OWNER:-TomHanck4}"
|
||||
REPO_NAME="${GITEA_REPO:-dsh-easy-desktop}"
|
||||
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}"
|
||||
: "${GITEA_TOKEN:?GITEA_TOKEN is required}"
|
||||
|
||||
if [[ "$RELEASE_TAG" == "latest" ]]; then
|
||||
RELEASE_TAG=$(curl -fsS "https://api.github.com/repos/$GITHUB_REPO/releases?per_page=1" | jq -r '.[0].tag_name')
|
||||
fi
|
||||
[[ -n "$RELEASE_TAG" && "$RELEASE_TAG" != "null" ]] || {
|
||||
echo "no GitHub release found" >&2
|
||||
exit 1
|
||||
}
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
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/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
|
||||
|
||||
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 > /tmp/asset-urls.txt
|
||||
while read -r url; do
|
||||
curl -fsSL --retry 3 --retry-delay 5 -o "artifacts/$(basename "$url")" "$url" &
|
||||
done < /tmp/asset-urls.txt
|
||||
wait
|
||||
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 \
|
||||
--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
|
||||
echo "published $RELEASE_TAG to Gitea"
|
||||
@@ -14,29 +14,68 @@ API="${GITEA_BASE_URL%/}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}"
|
||||
PACKAGE_BASE="${GITEA_BASE_URL%/}/api/packages/${GITEA_OWNER}/generic/dsh-easy-desktop-updater"
|
||||
AUTH="Authorization: token ${GITEA_TOKEN}"
|
||||
|
||||
# The Gitea repository is a pull mirror. Sync the GitHub tag before creating
|
||||
# the matching Gitea release, then wait until the tag is queryable.
|
||||
curl --fail --silent --show-error -X POST -H "$AUTH" "$API/mirror-sync" >/dev/null
|
||||
for _ in $(seq 1 30); do
|
||||
# Retry transient gateway errors (504) coming from the front proxy while the
|
||||
# NAS reaches github.com over a mainland link: requests can stall past the
|
||||
# proxy read timeout. 404s are NOT retried here (release/package state).
|
||||
curl_retry() {
|
||||
local attempts=0
|
||||
while ! "$@"; do
|
||||
attempts=$((attempts + 1))
|
||||
if (( attempts >= 4 )); then
|
||||
echo "curl failed after 4 attempts: $*" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "curl transient failure (attempt ${attempts}/3), retrying: $*" >&2
|
||||
sleep 10
|
||||
done
|
||||
}
|
||||
|
||||
# The Gitea repository is a pull mirror. Kick a background sync; the trigger
|
||||
# response itself is irrelevant (it can 504 while the sync runs on Gitea).
|
||||
# The tag poll below is the actual gate.
|
||||
curl --silent --show-error -X POST -H "$AUTH" "$API/mirror-sync" >/dev/null || true
|
||||
|
||||
# Wait (up to 10 min) until the tag has synced; then fail loudly if it never did.
|
||||
for _ in $(seq 1 60); do
|
||||
if curl --fail --silent --show-error -H "$AUTH" "$API/tags/$RELEASE_TAG" >/dev/null 2>&1; then
|
||||
synced=1
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
curl --fail --silent --show-error -H "$AUTH" "$API/tags/$RELEASE_TAG" >/dev/null
|
||||
[[ "${synced:-0}" == 1 ]] || {
|
||||
echo "tag $RELEASE_TAG did not appear on the Gitea mirror after 10 minutes" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
release_json=$(curl --silent --show-error -H "$AUTH" "$API/releases/tags/$RELEASE_TAG")
|
||||
# Structured release body from docs/release-notes/v<version>.md; legacy
|
||||
# fallback only when the file is missing (the version job normally gates it).
|
||||
RELEASE_NOTES="docs/release-notes/v${RELEASE_VERSION}.md"
|
||||
if [[ -f "$RELEASE_NOTES" ]]; then
|
||||
release_body=$(cat "$RELEASE_NOTES")
|
||||
else
|
||||
release_body="大陆镜像安装包;文件与 GitHub Release 同源。应用内更新包由 Tauri 签名校验。"
|
||||
fi
|
||||
|
||||
release_json=$(curl_retry curl --fail --silent --show-error -H "$AUTH" "$API/releases/tags/$RELEASE_TAG")
|
||||
release_id=$(printf '%s' "$release_json" | jq -r '.id // empty')
|
||||
if [[ -z "$release_id" ]]; then
|
||||
release_payload=$(jq -n \
|
||||
--arg tag "$RELEASE_TAG" \
|
||||
--arg name "DeepSeek Harness Desktop $RELEASE_TAG" \
|
||||
--arg body "大陆镜像安装包;文件与 GitHub Release 同源。应用内更新包由 Tauri 签名校验。" \
|
||||
--arg body "$release_body" \
|
||||
'{tag_name:$tag,target_commitish:$tag,name:$name,body:$body,draft:false,prerelease:false}')
|
||||
release_json=$(curl --fail --silent --show-error \
|
||||
release_json=$(curl_retry curl --fail --silent --show-error \
|
||||
-X POST -H "$AUTH" -H 'Content-Type: application/json' \
|
||||
--data "$release_payload" "$API/releases")
|
||||
release_id=$(printf '%s' "$release_json" | jq -r '.id')
|
||||
else
|
||||
# The mirror may have synced an older, terse GitHub body; keep the Gitea
|
||||
# copy identical to the structured notes file on every publish.
|
||||
curl_retry curl --fail --silent --show-error -X PATCH -H "$AUTH" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(jq -n --arg body "$release_body" '{body:$body}')" \
|
||||
"$API/releases/$release_id" >/dev/null
|
||||
fi
|
||||
|
||||
# Versioned generic package: immutable URLs consumed by latest.json.
|
||||
@@ -44,17 +83,17 @@ curl --silent --show-error -X DELETE -H "$AUTH" \
|
||||
"$PACKAGE_BASE/$RELEASE_VERSION" >/dev/null || true
|
||||
for file in "$STAGING_DIR"/*; do
|
||||
[[ -f "$file" && "$(basename "$file")" != "latest.json" ]] || continue
|
||||
curl --fail --silent --show-error -H "$AUTH" --upload-file "$file" \
|
||||
curl_retry curl --fail --silent --show-error -H "$AUTH" --upload-file "$file" \
|
||||
"$PACKAGE_BASE/$RELEASE_VERSION/$(basename "$file")" >/dev/null
|
||||
done
|
||||
|
||||
# Stable updater endpoint. Gitea generic packages are immutable, so replace
|
||||
# the synthetic "latest" version on each completed release.
|
||||
curl --silent --show-error -X DELETE -H "$AUTH" "$PACKAGE_BASE/latest" >/dev/null || true
|
||||
curl --fail --silent --show-error -H "$AUTH" --upload-file "$STAGING_DIR/latest.json" \
|
||||
curl_retry curl --fail --silent --show-error -H "$AUTH" --upload-file "$STAGING_DIR/latest.json" \
|
||||
"$PACKAGE_BASE/latest/latest.json" >/dev/null
|
||||
|
||||
assets=$(curl --fail --silent --show-error -H "$AUTH" "$API/releases/$release_id/assets")
|
||||
assets=$(curl_retry curl --fail --silent --show-error -H "$AUTH" "$API/releases/$release_id/assets")
|
||||
for file in "$STAGING_DIR"/*; do
|
||||
[[ -f "$file" ]] || continue
|
||||
name=$(basename "$file")
|
||||
@@ -64,9 +103,9 @@ for file in "$STAGING_DIR"/*; do
|
||||
esac
|
||||
old_id=$(printf '%s' "$assets" | jq -r --arg name "$name" '[.[] | select(.name == $name) | .id][0] // empty')
|
||||
if [[ -n "$old_id" ]]; then
|
||||
curl --fail --silent --show-error -X DELETE -H "$AUTH" \
|
||||
curl_retry curl --fail --silent --show-error -X DELETE -H "$AUTH" \
|
||||
"$API/releases/$release_id/assets/$old_id" >/dev/null
|
||||
fi
|
||||
curl --fail --silent --show-error -H "$AUTH" \
|
||||
curl_retry curl --fail --silent --show-error -H "$AUTH" \
|
||||
-F "attachment=@$file" "$API/releases/$release_id/assets?name=$name" >/dev/null
|
||||
done
|
||||
67
scripts/release-notes.py
Executable file
67
scripts/release-notes.py
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract release-copy fragments from a structured per-version notes file.
|
||||
|
||||
Notes live under docs/release-notes/v<version>.md in this layout:
|
||||
|
||||
# DeepSeek Harness Desktop v0.1.2
|
||||
|
||||
**摘要 / Summary:** one short bilingual paragraph …
|
||||
|
||||
## 新功能 / Features
|
||||
- …
|
||||
|
||||
## 修复 / Fixes
|
||||
- …
|
||||
|
||||
## 安装与更新 / Install & Update
|
||||
- …
|
||||
|
||||
Subcommands:
|
||||
body FILE the full file (GitHub / Gitea release body)
|
||||
notes FILE the 摘要 / Summary paragraph, one line (in-app update notes)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TITLE_RE = re.compile(r"^# .+$", re.M)
|
||||
SUMMARY_RE = re.compile(r"^\*\*摘要\s*/\s*Summary:\*\*\s*(.+)$", re.M)
|
||||
|
||||
|
||||
def load(path: Path) -> str:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if TITLE_RE.search(text) is None:
|
||||
raise SystemExit(f"{path}: expected a '# ' title line")
|
||||
return text
|
||||
|
||||
|
||||
def read_body(path: Path) -> str:
|
||||
text = load(path)
|
||||
if not text.endswith("\n"):
|
||||
text += "\n"
|
||||
return text
|
||||
|
||||
|
||||
def read_notes(path: Path) -> str:
|
||||
text = load(path)
|
||||
match = SUMMARY_RE.search(text)
|
||||
if match is None:
|
||||
raise SystemExit(f"{path}: missing '**摘要 / Summary:**' line")
|
||||
return match.group(1).strip()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("subcommand", choices=["body", "notes"])
|
||||
parser.add_argument("file")
|
||||
args = parser.parse_args()
|
||||
out = read_body(Path(args.file)) if args.subcommand == "body" else read_notes(Path(args.file))
|
||||
sys.stdout.write(out if out.endswith("\n") else out + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -76,7 +76,14 @@ gh auth status >/dev/null
|
||||
confirm '现在写入 GITEA_TOKEN 与 Tauri 签名 secrets 吗?' || exit 1
|
||||
printf '%s' "$GITEA_TOKEN" | gh secret set GITEA_TOKEN
|
||||
cat "$PRIVATE_KEY_PATH" | gh secret set TAURI_SIGNING_PRIVATE_KEY
|
||||
cat "$PUBLIC_KEY_PATH" | gh secret set DSH_DESKTOP_UPDATER_PUBKEY
|
||||
python3 - "$PUBLIC_KEY_PATH" <<'PY'
|
||||
import json, pathlib, sys
|
||||
conf = pathlib.Path("src-tauri/tauri.conf.json")
|
||||
data = json.loads(conf.read_text(encoding="utf-8"))
|
||||
data["plugins"]["updater"]["pubkey"] = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").strip()
|
||||
conf.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
PY
|
||||
echo "公钥已写入 src-tauri/tauri.conf.json(随仓库提交)"
|
||||
if [[ -n "$SIGNING_PASSWORD" ]]; then
|
||||
printf '%s' "$SIGNING_PASSWORD" | gh secret set TAURI_SIGNING_PRIVATE_KEY_PASSWORD
|
||||
fi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "DeepSeek Harness",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"identifier": "io.github.tommyfang.DshDesktop",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": ""
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM1OEI5NkQwMjJCNkFDODMKUldTRHJMWWkwSmFMTlcza3Vkd1UxclQydm0xOW9MTVNxU1JIckkreWRwc1dMTm1HOVVaMHI1SysK"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
@@ -41,7 +41,6 @@
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"../plugins/dsh-desktop-vision/",
|
||||
"../plugins/dsh-desktop-voice/",
|
||||
"../vendor/modlens/",
|
||||
"../vendor/dshmarket/",
|
||||
|
||||
@@ -7,39 +7,16 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class VisionPluginFilesTests(unittest.TestCase):
|
||||
def test_client_registers_system_settings_slots(self):
|
||||
client = (ROOT / "plugins" / "dsh-desktop-vision" / "client.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("settings.section", client)
|
||||
self.assertNotIn("settings.plugins.tab", client)
|
||||
self.assertIn("modlens-vision", client)
|
||||
client = (ROOT / "plugins" / "dsh-desktop-vision" / "client.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("settings.section", client)
|
||||
self.assertNotIn("settings.plugins.tab", client)
|
||||
self.assertIn("modlens-vision", client)
|
||||
host = (ROOT / "plugins" / "dsh-desktop-vision" / "index.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("/dsh-desktop/modlens", host)
|
||||
self.assertIn("'OpenAI 兼容'", host)
|
||||
self.assertNotIn("Qwen / 自建网关", host)
|
||||
self.assertIn("https://api.openai.com/v1", host)
|
||||
self.assertIn("https://generativelanguage.googleapis.com", host)
|
||||
self.assertIn("https://api.anthropic.com", host)
|
||||
self.assertIn("https://platform.openai.com/api-keys", host)
|
||||
self.assertIn("https://aistudio.google.com/apikey", host)
|
||||
self.assertIn("https://console.anthropic.com/settings/keys", host)
|
||||
self.assertIn("获取 API", client)
|
||||
self.assertNotIn("qwen-agent", client)
|
||||
self.assertIn("example: 'gpt-4o'", host)
|
||||
self.assertIn("example: 'gemini-3.6-flash'", host)
|
||||
self.assertIn("example: 'claude-haiku-4-5-20251001'", host)
|
||||
self.assertIn("example: 'gemini-3.6-flash-low'", host)
|
||||
self.assertIn("example: 'haiku'", host)
|
||||
class VisionSettingsDocsTests(unittest.TestCase):
|
||||
"""The vision engine has exactly one documented settings surface: the
|
||||
modlens card under 设置 → 插件 → 插件配置 (regression guard for the
|
||||
removed duplicate dsh-desktop-vision section)."""
|
||||
|
||||
def test_readme_points_at_the_modlens_config_card(self):
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
self.assertIn("设置 → 插件 → 插件配置", readme)
|
||||
self.assertIn("视觉引擎(ModLens)", readme)
|
||||
self.assertNotIn("设置 → 视觉模型", readme)
|
||||
|
||||
|
||||
class VoicePluginFilesTests(unittest.TestCase):
|
||||
@@ -106,10 +83,9 @@ class BundledAttributionTests(unittest.TestCase):
|
||||
self.assertIn("0.1.0-rc.6", text)
|
||||
self.assertIn("3.16.6", text)
|
||||
self.assertIn("ffb845c5480adc953392a6db6f8a98ede621174b", text)
|
||||
self.assertIn("dsh-desktop-vision", text)
|
||||
self.assertIn("dsh-desktop-voice", text)
|
||||
self.assertIn("https://github.com/dsh-market/dsh-market", text)
|
||||
self.assertIn("1.9.0", text)
|
||||
self.assertIn("1.10.1", text)
|
||||
self.assertIn("dsh-plugin", readme)
|
||||
self.assertIn("带上眼睛", readme)
|
||||
self.assertIn("+8%", readme)
|
||||
|
||||
@@ -68,5 +68,68 @@ class GiteaUpdateManifestTests(unittest.TestCase):
|
||||
self.assertTrue(windows["url"].endswith("/1.2.3/dsh-easy-desktop_1.2.3_windows_x86_64.exe"))
|
||||
|
||||
|
||||
def test_normalizes_flat_github_assets(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
artifacts = root / "artifacts"
|
||||
output = root / "staged"
|
||||
artifacts.mkdir()
|
||||
fixtures = {
|
||||
"DeepSeek.Harness_0.1.1_aarch64.app.tar.gz": "darwin-aarch64",
|
||||
"DeepSeek.Harness_0.1.1_aarch64.app.tar.gz.sig": "darwin-aarch64",
|
||||
"DeepSeek.Harness_0.1.1_aarch64.dmg": "darwin-aarch64",
|
||||
"DeepSeek.Harness_0.1.1_x64.app.tar.gz": "darwin-x86_64",
|
||||
"DeepSeek.Harness_0.1.1_x64.app.tar.gz.sig": "darwin-x86_64",
|
||||
"DeepSeek.Harness_0.1.1_x64.dmg": "darwin-x86_64",
|
||||
"DeepSeek.Harness_0.1.1_amd64.deb": "linux-x86_64",
|
||||
"DeepSeek.Harness_0.1.1_amd64.AppImage": "linux-x86_64",
|
||||
"DeepSeek.Harness_0.1.1_amd64.AppImage.sig": "linux-x86_64",
|
||||
"DeepSeek.Harness-0.1.1-1.x86_64.rpm": "linux-x86_64",
|
||||
"DeepSeek.Harness_0.1.1_x64-setup.exe": "windows-x86_64",
|
||||
"DeepSeek.Harness_0.1.1_x64-setup.exe.sig": "windows-x86_64",
|
||||
"DeepSeek.Harness_0.1.1_x64_en-US.msi": "windows-x86_64",
|
||||
"io.github.tommyfang.DshDesktop.flatpak": "linux-x86_64",
|
||||
}
|
||||
for name, marker in fixtures.items():
|
||||
(artifacts / name).write_bytes(marker.encode())
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"python3",
|
||||
str(SCRIPT),
|
||||
"--flat-artifacts",
|
||||
str(artifacts),
|
||||
"--output",
|
||||
str(output),
|
||||
"--version",
|
||||
"1.2.3",
|
||||
"--package-base-url",
|
||||
"http://gitea.example/api/packages/u/generic/app",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
manifest = json.loads((output / "latest.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(
|
||||
set(manifest["platforms"]),
|
||||
{
|
||||
"darwin-aarch64-app",
|
||||
"darwin-aarch64",
|
||||
"darwin-x86_64-app",
|
||||
"darwin-x86_64",
|
||||
"linux-x86_64-appimage",
|
||||
"linux-x86_64",
|
||||
"windows-x86_64-nsis",
|
||||
"windows-x86_64",
|
||||
},
|
||||
)
|
||||
# Each updater target embeds its own .sig content; fixed-name
|
||||
# assets (dmg/rpm/deb/flatpak) are deliverables only.
|
||||
self.assertEqual(
|
||||
manifest["platforms"]["linux-x86_64"]["signature"],
|
||||
"linux-x86_64",
|
||||
)
|
||||
self.assertNotIn("linux-x86_64-app", manifest["platforms"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user