release: v0.1.2

- bundle dshmarket 1.10.1; drop the duplicate vision settings section (modlens card is the single config surface)
- embed the updater signing public key in every build (Makefile / CI / Flatpak); in-app updates now actually run
- structured release notes per version: docs/release-notes/v<version>.md feeds GitHub, Gitea and the in-app update panel
- gate releases on the notes file; Gitea publish reads/patches the same body
This commit is contained in:
2026-08-17 00:48:38 +08:00
parent c71a87c92a
commit 97e5ae50b5
22 changed files with 204 additions and 513 deletions

View File

@@ -41,6 +41,10 @@ jobs:
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1) VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$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 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 echo "Git tag ${GITHUB_REF_NAME} must match Cargo.toml version v${VERSION}" >&2
exit 1 exit 1
@@ -125,6 +129,9 @@ jobs:
shell: bash shell: bash
run: bash scripts/vendor-native.sh 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 - uses: tauri-apps/tauri-action@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -207,6 +214,22 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 1 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: publish-gitea:
name: Publish Gitea mirror name: Publish Gitea mirror
needs: [version, package, flatpak] needs: [version, package, flatpak]
@@ -228,6 +251,7 @@ jobs:
--output staged \ --output staged \
--version "$VERSION" \ --version "$VERSION" \
--pub-date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --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" --package-base-url "https://git.fangsiyuan.top/api/packages/TomHanck4/generic/dsh-easy-desktop-updater"
- name: Publish Gitea release and updater feed - name: Publish Gitea release and updater feed

4
Cargo.lock generated
View File

@@ -949,7 +949,7 @@ dependencies = [
[[package]] [[package]]
name = "dsh-core" name = "dsh-core"
version = "0.1.1" version = "0.1.2"
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.1" version = "0.1.2"
dependencies = [ dependencies = [
"arboard", "arboard",
"dsh-core", "dsh-core",

View File

@@ -3,7 +3,7 @@ members = ["crates/dsh-core", "src-tauri"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.1.1" version = "0.1.2"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["TommyFang2077"] authors = ["TommyFang2077"]

View File

@@ -4,7 +4,7 @@ PREFIX ?= $(HOME)/.local
APP_ID := io.github.tommyfang.DshDesktop APP_ID := io.github.tommyfang.DshDesktop
DSH_VERSION := 0.1.0-rc.6 DSH_VERSION := 0.1.0-rc.6
MODLENS_VERSION := 3.16.6 MODLENS_VERSION := 3.16.6
MARKET_VERSION := 1.9.0 MARKET_VERSION := 1.10.1
ANCHORED_COMMIT := ffb845c5480adc953392a6db6f8a98ede621174b ANCHORED_COMMIT := ffb845c5480adc953392a6db6f8a98ede621174b
ANCHORED_REPO := https://github.com/xiaobright/dsh-anchored-standard.git ANCHORED_REPO := https://github.com/xiaobright/dsh-anchored-standard.git
VENDOR_DIR := vendor/dsh-prefix 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 BUNDLE := dist/$(APP_ID)-$(VERSION).flatpak
BIN := target/release/dsh-desktop 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 .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 all: test
@@ -89,11 +94,6 @@ install: build
mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \ mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \
cp -R $(MARKET_DIR) $(DESTDIR)$(PREFIX)/share/dsh-desktop/market; \ cp -R $(MARKET_DIR) $(DESTDIR)$(PREFIX)/share/dsh-desktop/market; \
fi 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 \ if [ -f plugins/dsh-desktop-voice/package.json ]; then \
rm -rf $(DESTDIR)$(PREFIX)/share/dsh-desktop/voice; \ rm -rf $(DESTDIR)$(PREFIX)/share/dsh-desktop/voice; \
mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \ mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \

View File

@@ -50,9 +50,9 @@ dsh 原本运行在浏览器标签页中;本项目用 Tauri 2 和系统 WebVie
### 视觉模型配置:给 DeepSeek 带上眼睛 ### 视觉模型配置:给 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` 中,相关外链由系统浏览器打开。
![设置 → 视觉模型:配置 OpenAI 兼容视觉引擎](docs/screenshots/vision.webp) ![设置 → 插件 → 插件配置视觉引擎ModLens](docs/screenshots/vision.webp)
### 锚定模式与原生窗口 ### 锚定模式与原生窗口
@@ -98,9 +98,8 @@ npm install -g @deepseek-ai/dsh
| --- | --- | --- | --- | | --- | --- | --- | --- |
| DeepSeek Harness | `0.1.0-rc.6` | 官方 WebUIFlatpak 内置,其他安装包调用本机 `dsh` | `~/.dsh` | | DeepSeek Harness | `0.1.0-rc.6` | 官方 WebUIFlatpak 内置,其他安装包调用本机 `dsh` | `~/.dsh` |
| 离线语音 | `dsh-desktop-voice 0.4.0` | 麦克风、快捷键、SenseVoice / OpenAI 兼容听写 | 配置 `~/.config/dsh-desktop/voice.json`;模型在系统缓存目录 | | 离线语音 | `dsh-desktop-voice 0.4.0` | 麦克风、快捷键、SenseVoice / OpenAI 兼容听写 | 配置 `~/.config/dsh-desktop/voice.json`;模型在系统缓存目录 |
| 插件市场 | `dshmarket 1.9.0` | 社区插件的发现、安装和更新 | `~/.dsh/profiles/web` | | 插件市场 | `dshmarket 1.10.1` | 社区插件的发现、安装和更新 | `~/.dsh/profiles/web` |
| 视觉配置 | `dsh-desktop-vision 0.1.4` | 配置视觉桥所使用的引擎、接口和模型 | `~/.modlens/config.json` | | 视觉 | `ModLens 3.16.6` | 让纯文本模型读取粘贴的图片;可从市场更新;引擎配置在「设置 → 插件」 | `~/.dsh/profiles/web`;配置 `~/.modlens/config.json` |
| 视觉桥 | `ModLens 3.16.6` | 让纯文本模型读取粘贴的图片;可从市场更新 | `~/.dsh/profiles/web` |
| 锚定预设 | `ffb845c5480a` | 锚定式标准与零工具锚定式标准 | `~/.dsh/.agent-presets/` | | 锚定预设 | `ffb845c5480a` | 锚定式标准与零工具锚定式标准 | `~/.dsh/.agent-presets/` |
应用启动时会同步桌面自带组件,但会保留用户从市场更新到更新版本的 ModLens。捆绑版本固定在 [Makefile](Makefile) 中。 应用启动时会同步桌面自带组件,但会保留用户从市场更新到更新版本的 ModLens。捆绑版本固定在 [Makefile](Makefile) 中。
@@ -174,7 +173,6 @@ dsh-desktop/
├── ui/ # 启动页 + 注入到 WebUI 的标题栏 ├── ui/ # 启动页 + 注入到 WebUI 的标题栏
├── src-tauri/ # Tauri 窗口、命令、deb/rpm/nsis/dmg ├── src-tauri/ # Tauri 窗口、命令、deb/rpm/nsis/dmg
├── crates/dsh-core/ # 启动 / 更新 / ModLens / 预设 / 剪贴板 ├── crates/dsh-core/ # 启动 / 更新 / ModLens / 预设 / 剪贴板
├── plugins/dsh-desktop-vision/ # 设置 → 视觉模型
├── plugins/dsh-desktop-voice/ # 设置 → 语音输入 + 对话框麦克风 ├── plugins/dsh-desktop-voice/ # 设置 → 语音输入 + 对话框麦克风
├── data/ # .desktop、图标、AppStream ├── data/ # .desktop、图标、AppStream
├── flatpak/ ├── flatpak/

View File

@@ -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`. | | 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`. | | 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`. | | 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`. | | `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`. | | 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. | | 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. |

View File

@@ -7,13 +7,11 @@ use serde_json::{json, Value};
use crate::paths::{copy_tree, dsh_home, replace_symlink, BundledPaths}; use crate::paths::{copy_tree, dsh_home, replace_symlink, BundledPaths};
pub const PACKAGE: &str = "@liustack/modlens"; pub const PACKAGE: &str = "@liustack/modlens";
pub const VISION_PACKAGE: &str = "dsh-desktop-vision";
pub const VOICE_PACKAGE: &str = "dsh-desktop-voice"; pub const VOICE_PACKAGE: &str = "dsh-desktop-voice";
pub const MARKET_PACKAGE: &str = "dshmarket"; pub const MARKET_PACKAGE: &str = "dshmarket";
pub const MODLENS_VERSION: &str = "3.16.6"; 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 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"; const MANAGED_BUNDLES_DIR: &str = ".dsh-desktop/bundles";
pub const HIDE_PLAIN_TWINS_JS: &str = include_str!("../../../ui/inject/hide-twins.js"); 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> { fn bundled_voice_plugin(paths: &BundledPaths) -> Option<PathBuf> {
bundled_plugin( bundled_plugin(
paths, paths,
@@ -164,15 +155,6 @@ fn install_profile_plugin(
true 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 { fn install_voice_plugin(paths: &BundledPaths, profile: &Path) -> bool {
install_profile_plugin( install_profile_plugin(
profile, profile,
@@ -435,15 +417,8 @@ fn ensure_modlens_inner(
version: &str, version: &str,
) -> std::io::Result<ModlensEnsureResult> { ) -> std::io::Result<ModlensEnsureResult> {
std::fs::create_dir_all(profile)?; std::fs::create_dir_all(profile)?;
let vision_ok = install_vision_plugin(paths, profile);
let voice_ok = install_voice_plugin(paths, profile); let voice_ok = install_voice_plugin(paths, profile);
let mut packages = BTreeMap::new(); let mut packages = BTreeMap::new();
if vision_ok {
packages.insert(
VISION_PACKAGE.to_string(),
local_plugin_spec(VISION_PACKAGE),
);
}
if voice_ok { if voice_ok {
packages.insert(VOICE_PACKAGE.to_string(), local_plugin_spec(VOICE_PACKAGE)); 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::create_dir_all(&market).unwrap();
std::fs::write( std::fs::write(
market.join("package.json"), market.join("package.json"),
r#"{"name":"dshmarket","version":"1.9.0"}"#, format!(r#"{{"name":"dshmarket","version":"{MARKET_PLUGIN_VERSION}"}}"#),
) )
.unwrap(); .unwrap();
std::fs::write(market.join("index.js"), "export {}\n").unwrap(); std::fs::write(market.join("index.js"), "export {}\n").unwrap();
@@ -625,7 +600,10 @@ ui-theme:
let manifest: Value = let manifest: Value =
serde_json::from_str(&std::fs::read_to_string(profile.join("package.json")).unwrap()) serde_json::from_str(&std::fs::read_to_string(profile.join("package.json")).unwrap())
.unwrap(); .unwrap();
assert_eq!(manifest["dependencies"]["dshmarket"], "1.9.0"); assert_eq!(
manifest["dependencies"]["dshmarket"],
MARKET_PLUGIN_VERSION
);
assert!(manifest["dsh"]["profile"]["bundles"] assert!(manifest["dsh"]["profile"]["bundles"]
.as_array() .as_array()
.unwrap() .unwrap()
@@ -639,10 +617,7 @@ ui-theme:
#[test] #[test]
fn bundled_local_plugins_use_file_dependencies() { fn bundled_local_plugins_use_file_dependencies() {
let tmp = tempfile::TempDir::new().unwrap(); let tmp = tempfile::TempDir::new().unwrap();
for (dir, name, version) in [ for (dir, name, version) in [("voice", VOICE_PACKAGE, VOICE_PLUGIN_VERSION)] {
("vision", VISION_PACKAGE, VISION_PLUGIN_VERSION),
("voice", VOICE_PACKAGE, VOICE_PLUGIN_VERSION),
] {
let plugin = tmp.path().join(dir); let plugin = tmp.path().join(dir);
std::fs::create_dir_all(&plugin).unwrap(); std::fs::create_dir_all(&plugin).unwrap();
std::fs::write( std::fs::write(
@@ -661,7 +636,7 @@ ui-theme:
let manifest: Value = let manifest: Value =
serde_json::from_str(&std::fs::read_to_string(profile.join("package.json")).unwrap()) serde_json::from_str(&std::fs::read_to_string(profile.join("package.json")).unwrap())
.unwrap(); .unwrap();
for name in [VISION_PACKAGE, VOICE_PACKAGE] { for name in [VOICE_PACKAGE] {
assert_eq!( assert_eq!(
manifest["dependencies"][name], manifest["dependencies"][name],
format!("file:.dsh-desktop/bundles/{name}") format!("file:.dsh-desktop/bundles/{name}")

View File

@@ -41,6 +41,16 @@
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <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"> <release version="0.1.1" date="2026-08-16">
<description> <description>
<p>Signed in-app updater from the mainland mirror; HTTPS Gitea distribution; Market install warnings for GitHub-only sources.</p> <p>Signed in-app updater from the mainland mirror; HTTPS Gitea distribution; Market install warnings for GitHub-only sources.</p>

View 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 DMGApple Silicon 与 Intel、Linux deb/rpm、Flatpak
- 除 Flatpak 外需本机已安装 `dsh``npm i -g @deepseek-ai/dsh`),或设置 `DSH_DESKTOP_DSH_BIN`
- macOS 包未公证:首次打开需在「系统设置 → 隐私与安全性」中允许
- 更新提示出现在启动页:点击「下载并安装」即可(走 Gitea 镜像,签名校验后生效)

View 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 DMGApple Silicon 与 Intel、Linux deb/rpm、Flatpak
- 除 Flatpak 外需本机已安装 `dsh``npm i -g @deepseek-ai/dsh`),或设置 `DSH_DESKTOP_DSH_BIN`
- macOS 包未公证:首次打开需在「系统设置 → 隐私与安全性」中允许
- 更新提示出现在启动页:点击「下载并安装」即可(走 Gitea 镜像,签名校验后生效)

View File

@@ -2,7 +2,7 @@
<html lang="zh-CN" class="dsh-desktop-offset"> <html lang="zh-CN" class="dsh-desktop-offset">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>视觉模型</title> <title>视觉引擎ModLens</title>
<link rel="stylesheet" href="chrome.css" /> <link rel="stylesheet" href="chrome.css" />
<style> <style>
:root { :root {
@@ -74,15 +74,15 @@
<h2>设置</h2> <h2>设置</h2>
<a class="muted" href="#">通用</a> <a class="muted" href="#">通用</a>
<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> <a class="muted" href="#">技能</a>
</nav> </nav>
<section class="content"> <section class="content">
<h1>视觉模型</h1> <h1>插件 · 插件配置</h1>
<p class="lead">内置插件 dsh-desktop-vision · 写入 ~/.modlens/config.json</p> <p class="lead">视觉引擎ModLens由 modlens 插件提供 · 写入 ~/.modlens/config.json</p>
<div class="dshdv"> <div class="dshdv">
<h3>ModLens 视觉模型</h3> <h3>视觉引擎(ModLens</h3>
<p>纯文本对话模型读图时使用这里的引擎。已声明视觉能力的模型(如 Qwen不会走这条桥。</p> <p>纯文本对话模型读图时使用这里的引擎。已声明视觉能力的模型(如 Qwen不会走这条桥。</p>
<label> <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

View File

@@ -53,10 +53,9 @@ modules:
- --share=network - --share=network
build-commands: build-commands:
- mkdir -p vendor/modlens vendor/dshmarket vendor/anchored-standard vendor/zero-anchored-standard - 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 - cargo build --release --locked --offline || cargo build --release
- install -Dm755 target/release/dsh-desktop ${FLATPAK_DEST}/bin/dsh-desktop - 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 - mkdir -p ${FLATPAK_DEST}/share/dsh-desktop/voice
- cp -a plugins/dsh-desktop-voice/. ${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 - install -Dm644 data/applications/io.github.tommyfang.DshDesktop.desktop ${FLATPAK_DEST}/share/applications/io.github.tommyfang.DshDesktop.desktop

View File

@@ -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
},
})

View File

@@ -1,4 +0,0 @@
# Settings page + host route for the ModLens vision engine.
- insert:
- id: dsh-desktop-vision
name: dsh-desktop-vision

View File

@@ -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}`)
}
})
}

View File

@@ -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
}
}
}

View File

@@ -48,18 +48,34 @@ done
exit 1 exit 1
} }
# 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_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') 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 \
--arg tag "$RELEASE_TAG" \ --arg tag "$RELEASE_TAG" \
--arg name "DeepSeek Harness Desktop $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}') '{tag_name:$tag,target_commitish:$tag,name:$name,body:$body,draft:false,prerelease:false}')
release_json=$(curl_retry curl --fail --silent --show-error \ release_json=$(curl_retry curl --fail --silent --show-error \
-X POST -H "$AUTH" -H 'Content-Type: application/json' \ -X POST -H "$AUTH" -H 'Content-Type: application/json' \
--data "$release_payload" "$API/releases") --data "$release_payload" "$API/releases")
release_id=$(printf '%s' "$release_json" | jq -r '.id') 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 fi
# Versioned generic package: immutable URLs consumed by latest.json. # Versioned generic package: immutable URLs consumed by latest.json.

67
scripts/release-notes.py Executable file
View 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()

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "DeepSeek Harness", "productName": "DeepSeek Harness",
"version": "0.1.1", "version": "0.1.2",
"identifier": "io.github.tommyfang.DshDesktop", "identifier": "io.github.tommyfang.DshDesktop",
"build": { "build": {
"frontendDist": "../ui" "frontendDist": "../ui"
@@ -41,7 +41,6 @@
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": [ "resources": [
"../plugins/dsh-desktop-vision/",
"../plugins/dsh-desktop-voice/", "../plugins/dsh-desktop-voice/",
"../vendor/modlens/", "../vendor/modlens/",
"../vendor/dshmarket/", "../vendor/dshmarket/",

View File

@@ -7,39 +7,16 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
class VisionPluginFilesTests(unittest.TestCase): class VisionSettingsDocsTests(unittest.TestCase):
def test_client_registers_system_settings_slots(self): """The vision engine has exactly one documented settings surface: the
client = (ROOT / "plugins" / "dsh-desktop-vision" / "client.js").read_text( modlens card under 设置 → 插件 → 插件配置 (regression guard for the
encoding="utf-8" removed duplicate dsh-desktop-vision section)."""
)
self.assertIn("settings.section", client) def test_readme_points_at_the_modlens_config_card(self):
self.assertNotIn("settings.plugins.tab", client) readme = (ROOT / "README.md").read_text(encoding="utf-8")
self.assertIn("modlens-vision", client) self.assertIn("设置 → 插件 → 插件配置", readme)
client = (ROOT / "plugins" / "dsh-desktop-vision" / "client.js").read_text( self.assertIn("视觉引擎ModLens", readme)
encoding="utf-8" self.assertNotIn("设置 → 视觉模型", readme)
)
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 VoicePluginFilesTests(unittest.TestCase): class VoicePluginFilesTests(unittest.TestCase):
@@ -106,10 +83,9 @@ class BundledAttributionTests(unittest.TestCase):
self.assertIn("0.1.0-rc.6", text) self.assertIn("0.1.0-rc.6", text)
self.assertIn("3.16.6", text) self.assertIn("3.16.6", text)
self.assertIn("ffb845c5480adc953392a6db6f8a98ede621174b", text) self.assertIn("ffb845c5480adc953392a6db6f8a98ede621174b", text)
self.assertIn("dsh-desktop-vision", text)
self.assertIn("dsh-desktop-voice", text) self.assertIn("dsh-desktop-voice", text)
self.assertIn("https://github.com/dsh-market/dsh-market", 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("dsh-plugin", readme)
self.assertIn("带上眼睛", readme) self.assertIn("带上眼睛", readme)
self.assertIn("+8%", readme) self.assertIn("+8%", readme)