diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1cdce0d..084828f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -84,7 +84,7 @@ jobs:
- name: linux
platform: ubuntu-22.04
rust_targets: ""
- args: --bundles deb,rpm
+ args: --bundles deb,rpm,appimage
- name: windows
platform: windows-latest
rust_targets: ""
@@ -128,6 +128,9 @@ jobs:
- 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__
@@ -139,12 +142,32 @@ jobs:
- Windows / macOS / deb / rpm 需要本机已安装 `dsh`(`npm i -g @deepseek-ai/dsh`),或设置 `DSH_DESKTOP_DSH_BIN`。
- Flatpak 自带 Node.js 与 `dsh`。
- macOS 包未公证,需在「系统设置 → 隐私与安全性」中允许打开。
+ - 中国大陆用户可从 Gitea 镜像下载安装包;壳更新使用同一镜像并在安装前校验 Tauri 签名。
releaseDraft: false
prerelease: false
uploadUpdaterJson: false
- uploadUpdaterSignatures: false
+ uploadUpdaterSignatures: true
args: ${{ matrix.args }}
+ - name: Collect Gitea release artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: gitea-${{ matrix.name }}
+ path: |
+ target/**/release/bundle/**/*.dmg
+ target/**/release/bundle/**/*.app.tar.gz
+ target/**/release/bundle/**/*.app.tar.gz.sig
+ target/**/release/bundle/**/*.deb
+ target/**/release/bundle/**/*.rpm
+ target/**/release/bundle/**/*.AppImage
+ target/**/release/bundle/**/*.AppImage.sig
+ target/**/release/bundle/**/*.exe
+ target/**/release/bundle/**/*.exe.sig
+ target/**/release/bundle/**/*.msi
+ target/**/release/bundle/**/*.msi.sig
+ if-no-files-found: error
+ retention-days: 1
+
flatpak:
name: Flatpak
needs: [test, version, vendor]
@@ -176,3 +199,44 @@ jobs:
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Collect Gitea Flatpak artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: gitea-flatpak
+ path: io.github.tommyfang.DshDesktop.flatpak
+ if-no-files-found: error
+ retention-days: 1
+
+ publish-gitea:
+ name: Publish Gitea mirror
+ needs: [version, package, flatpak]
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: gitea-*
+ path: artifacts
+
+ - name: Normalize artifacts and build latest.json
+ env:
+ VERSION: ${{ needs.version.outputs.version }}
+ run: |
+ python3 scripts/build-gitea-update.py \
+ --artifacts artifacts \
+ --output staged \
+ --version "$VERSION" \
+ --pub-date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+ --package-base-url "https://git.fangsiyuan.top/api/packages/TomHanck4/generic/dsh-easy-desktop-updater"
+
+ - name: Publish Gitea release and updater feed
+ env:
+ GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
+ GITEA_BASE_URL: https://git.fangsiyuan.top
+ GITEA_OWNER: TomHanck4
+ GITEA_REPO: dsh-easy-desktop
+ RELEASE_TAG: ${{ needs.version.outputs.tag }}
+ RELEASE_VERSION: ${{ needs.version.outputs.version }}
+ run: bash scripts/publish-gitea-release.sh staged
diff --git a/Cargo.lock b/Cargo.lock
index a962542..b1969ee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -97,6 +97,15 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
[[package]]
name = "arboard"
version = "3.6.1"
@@ -799,6 +808,17 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -935,6 +955,7 @@ dependencies = [
"dirs",
"libc",
"regex",
+ "semver",
"serde",
"serde_json",
"shlex 1.3.0",
@@ -961,7 +982,9 @@ dependencies = [
"tauri-build",
"tauri-plugin-opener",
"tauri-plugin-single-instance",
+ "tauri-plugin-updater",
"url",
+ "webkit2gtk",
]
[[package]]
@@ -1160,6 +1183,16 @@ dependencies = [
"rustc_version",
]
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
@@ -1746,6 +1779,21 @@ dependencies = [
"want",
]
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -2088,6 +2136,36 @@ dependencies = [
"windows-sys 0.45.0",
]
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.20",
+ "walkdir",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -2277,6 +2355,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+[[package]]
+name = "minisign-verify"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2529,6 +2613,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.1",
"block2",
+ "libc",
"objc2",
"objc2-core-foundation",
]
@@ -2544,6 +2629,18 @@ dependencies = [
"objc2-core-foundation",
]
+[[package]]
+name = "objc2-osa-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+]
+
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -2624,6 +2721,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -2647,7 +2750,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
- "windows-sys 0.45.0",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "osakit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+ "objc2-osa-kit",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.20",
]
[[package]]
@@ -3079,15 +3196,20 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
+ "hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
+ "tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -3099,6 +3221,20 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "rustc-hash"
version = "2.1.3"
@@ -3127,6 +3263,79 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni 0.22.4",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.23"
@@ -3142,6 +3351,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "schemars"
version = "0.8.22"
@@ -3199,6 +3417,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "selectors"
version = "0.36.1"
@@ -3426,6 +3667,22 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -3538,6 +3795,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -3632,7 +3895,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
- "jni",
+ "jni 0.21.1",
"libc",
"log",
"ndk",
@@ -3665,6 +3928,17 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -3688,7 +3962,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
- "jni",
+ "jni 0.21.1",
"libc",
"log",
"mime",
@@ -3838,6 +4112,39 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "tauri-plugin-updater"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
+dependencies = [
+ "base64 0.22.1",
+ "dirs",
+ "flate2",
+ "futures-util",
+ "http",
+ "infer",
+ "log",
+ "minisign-verify",
+ "osakit",
+ "percent-encoding",
+ "reqwest",
+ "rustls",
+ "semver",
+ "serde",
+ "serde_json",
+ "tar",
+ "tauri",
+ "tauri-plugin",
+ "tempfile",
+ "thiserror 2.0.20",
+ "time",
+ "tokio",
+ "url",
+ "windows-sys 0.60.2",
+ "zip",
+]
+
[[package]]
name = "tauri-runtime"
version = "2.11.3"
@@ -3848,7 +4155,7 @@ dependencies = [
"dpi",
"gtk",
"http",
- "jni",
+ "jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@@ -3871,7 +4178,7 @@ checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
dependencies = [
"gtk",
"http",
- "jni",
+ "jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@@ -4083,6 +4390,16 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
[[package]]
name = "tokio-util"
version = "0.7.19"
@@ -4407,6 +4724,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.8"
@@ -4726,6 +5049,15 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "webview2-com"
version = "0.38.2"
@@ -4971,6 +5303,15 @@ dependencies = [
"windows-targets 0.42.2",
]
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -5284,7 +5625,7 @@ dependencies = [
"gtk",
"http",
"javascriptcore-rs",
- "jni",
+ "jni 0.21.1",
"libc",
"ndk",
"objc2",
@@ -5348,6 +5689,16 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
[[package]]
name = "yoke"
version = "0.8.3"
@@ -5482,6 +5833,12 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
[[package]]
name = "zerotrie"
version = "0.2.5"
@@ -5515,6 +5872,18 @@ dependencies = [
"syn 3.0.3",
]
+[[package]]
+name = "zip"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "indexmap 2.14.0",
+ "memchr",
+]
+
[[package]]
name = "zmij"
version = "1.0.23"
diff --git a/Makefile b/Makefile
index 051d673..e67dbf1 100644
--- a/Makefile
+++ b/Makefile
@@ -4,10 +4,12 @@ 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
ANCHORED_COMMIT := ffb845c5480adc953392a6db6f8a98ede621174b
ANCHORED_REPO := https://github.com/xiaobright/dsh-anchored-standard.git
VENDOR_DIR := vendor/dsh-prefix
MODLENS_DIR := vendor/modlens
+MARKET_DIR := vendor/dshmarket
ANCHORED_DIR := vendor/anchored-standard
ZERO_DIR := vendor/zero-anchored-standard
FLATPAK ?= flatpak
@@ -35,16 +37,19 @@ build:
test:
$(CARGO) test -p dsh-core
$(PYTHON) -m unittest discover -s tests -v
+ node --test tests/*.test.mjs
vendor:
mkdir -p vendor
- rm -rf $(VENDOR_DIR) $(MODLENS_DIR)
+ rm -rf $(VENDOR_DIR) $(MODLENS_DIR) $(MARKET_DIR)
npm install --prefix=$(CURDIR)/$(VENDOR_DIR) --global --prefer-offline --no-audit --no-fund @deepseek-ai/dsh@$(DSH_VERSION)
npm install --prefix=$(CURDIR)/$(MODLENS_DIR) --prefer-offline --no-audit --no-fund @liustack/modlens@$(MODLENS_VERSION)
+ npm install --prefix=$(CURDIR)/$(MARKET_DIR) --prefer-offline --no-audit --no-fund dshmarket@$(MARKET_VERSION)
+ $(PYTHON) scripts/patch-dshmarket-mainland.py
$(MAKE) vendor-anchored
vendor-native:
- MODLENS_VERSION=$(MODLENS_VERSION) ANCHORED_COMMIT=$(ANCHORED_COMMIT) ANCHORED_REPO=$(ANCHORED_REPO) bash scripts/vendor-native.sh
+ MODLENS_VERSION=$(MODLENS_VERSION) MARKET_VERSION=$(MARKET_VERSION) ANCHORED_COMMIT=$(ANCHORED_COMMIT) ANCHORED_REPO=$(ANCHORED_REPO) bash scripts/vendor-native.sh
vendor-anchored:
rm -rf vendor/.anchored-src $(ANCHORED_DIR) $(ZERO_DIR)
@@ -79,11 +84,21 @@ install: build
mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \
cp -R $(MODLENS_DIR) $(DESTDIR)$(PREFIX)/share/dsh-desktop/modlens; \
fi
+ if [ -d $(MARKET_DIR)/node_modules/dshmarket ]; then \
+ rm -rf $(DESTDIR)$(PREFIX)/share/dsh-desktop/market; \
+ 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; \
+ cp -R plugins/dsh-desktop-voice $(DESTDIR)$(PREFIX)/share/dsh-desktop/voice; \
+ fi
if [ -f $(ANCHORED_DIR)/preset.yml ]; then \
rm -rf $(DESTDIR)$(PREFIX)/share/dsh-desktop/anchored-standard; \
mkdir -p $(DESTDIR)$(PREFIX)/share/dsh-desktop; \
@@ -105,7 +120,7 @@ uninstall:
rm -f "$(DESTDIR)$(PREFIX)/share/icons/hicolor/$${size}x$${size}/apps/$(APP_ID).png"; \
done
-flatpak-build: $(VENDOR_DIR)/bin/dsh $(MODLENS_DIR)/node_modules/@liustack/modlens $(ANCHORED_DIR)/preset.yml $(ZERO_DIR)/preset.yml
+flatpak-build: $(VENDOR_DIR)/bin/dsh $(MODLENS_DIR)/node_modules/@liustack/modlens $(MARKET_DIR)/node_modules/dshmarket $(ANCHORED_DIR)/preset.yml $(ZERO_DIR)/preset.yml
$(BUILDER) --user --force-clean --install-deps-from=flathub $(BUILD_DIR) $(MANIFEST)
$(VENDOR_DIR)/bin/dsh:
@@ -114,6 +129,9 @@ $(VENDOR_DIR)/bin/dsh:
$(MODLENS_DIR)/node_modules/@liustack/modlens:
$(MAKE) vendor
+
+$(MARKET_DIR)/node_modules/dshmarket:
+ $(MAKE) vendor
$(ANCHORED_DIR)/preset.yml $(ZERO_DIR)/preset.yml:
$(MAKE) vendor-anchored
diff --git a/README.md b/README.md
index 7dc4f01..4b973e0 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
DeepSeek Harness Desktop
@@ -9,13 +9,14 @@
- 👁️ 给 DeepSeek 带上眼睛 —— 粘贴图片直接识别 · ⚓ 锚定模式实测比官方 Standard 约 +8%
+ 离线语音输入 · 内置插件市场 · 可视化配置视觉模型
- ⬇ 下载 ·
+ 下载 ·
+ 大陆镜像 ·
+ 核心体验 ·
三十秒上手 ·
- 内置插件 ·
从源码运行
@@ -27,140 +28,91 @@
-dsh 只能开在浏览器标签页里?这个仓库把它变成一个真正的桌面应用:Tauri 2 原生窗口 + 系统 WebView,Linux / Windows / macOS 通用。打开就是官方 WebUI——会话、工作区、插件、技能一个不少,`dsh` 升级后界面跟着升级,永远不用重打包前端。
+dsh 原本运行在浏览器标签页中;本项目用 Tauri 2 和系统 WebView 把官方 WebUI 变成原生桌面窗口。会话、工作区、插件和技能全部保留,`dsh` 更新后界面也会随之更新,不需要重新打包前端。
-这是作者自用的第三方壳,**不包含** DeepSeek Harness 源码,会持续跟着 dsh 和内置组件更新;踩到坑欢迎 [提 issue](https://github.com/TommyFang2077/dsh-desktop/issues)。仓库按官方 [贡献指南](https://github.com/deepseek-ai/deepseek-harness/blob/master/CONTRIBUTING.md) 挂了 [`dsh-plugin`](https://github.com/topics/dsh-plugin) topic。
+这是作者维护的第三方壳,**不包含** DeepSeek Harness 源码。重点补齐官方 WebUI 在桌面端缺少的输入和扩展体验:直接说话、直接安装插件、直接配置视觉模型。
-## 为什么值得一试
+## 核心体验
-### 👁️ 给 DeepSeek 带上眼睛
+### 内置语音:SenseVoice 本机离线听写
-DeepSeek 主力对话模型是纯文本的——你贴一张截图,它两眼一抹黑。本应用内置 [ModLens](https://github.com/liustack/modlens)(全网第一个 dsh 视觉插件):外挂视觉引擎后,**图片直接粘贴进对话框就能识别**,不用先存盘再填路径。配套的 **设置 → 视觉模型** 页面把 OpenAI / Gemini / Anthropic / 本机 CLI 引擎全配齐,免费的 Gemini key 就能跑。
+对话框旁直接提供麦克风按钮;按 `Ctrl+E`(macOS 为 `⌘E`)即可开始或结束听写,也可切换为按住说话。默认引擎是本机离线 **SenseVoiceSmall**,支持中文、粤语、英语、日语和韩语,识别结果直接写入当前输入框。
-
+模型和 sherpa-onnx WASM 运行时**不塞进安装包**。首次点击麦克风时会明确提示下载约 245 MB,底部状态条持续显示下载与校验进度;安装完成后保存在系统缓存目录,录音不离开本机。需要云端识别时,也可切换到 OpenAI 兼容的 `/v1/audio/transcriptions` 接口。
-### ⚓ 锚定模式:比官方 Standard 约 +8%
+
-DeepSeek V4 Pro 会按「第一眼看到的工具表」选执行轨迹:Project2 评测里官方 Minimal 拿 **99**,Standard 只有 **91**——但常驻 Minimal 又缺工具。内置的 [锚定式标准](https://github.com/xiaobright/dsh-anchored-standard) 两头都要:**首轮用 Minimal 真工具对钉住高分轨迹,从第二轮起解锁完整 Standard 工具目录**,同配置实测 Ability **98 / 99**,相对 Standard 的 91 约 **+8% / +9%**。另附零工具锚定变体。新会话默认就是它,开箱即用。
+### 内置插件市场:发现、安装和更新社区插件
-
+无需记包名或离开应用。在 **设置 → Plugin Market** 中可以浏览目录、搜索分类、查看已安装插件,并直接安装、更新、备份或恢复社区插件。ModLens 等默认组件也能从这里正常更新;桌面启动不会再把用户更新的版本降回内置基线。
-*百分比按预设作者在 Project2 / DeepSeek V4 Pro 上的 Ability 计算:`(98−91)/91 ≈ 8%`。同配置可复现;社区实验预设,不是官方出品,不代表所有任务都涨。*
+
-### 🪟 像个真正的 Mac / Linux / Windows 应用
+### 视觉模型配置:给 DeepSeek 带上眼睛
-36px 苹果风薄标题栏,不占一排后退/前进/刷新;左侧 `•••` 菜单可重启 dsh 或跳回浏览器。关窗口自动停掉 `dsh web`,崩溃一键拉起。凭据、权限、会话全部还在 `~/.dsh`,卸载壳不丢任何东西。
+纯文本 DeepSeek 配合视觉桥后,可以直接粘贴截图识别内容。内置的 **设置 → 视觉模型** 页面集中配置 OpenAI 兼容接口、Gemini API、Anthropic API、Antigravity CLI 和 Claude Code 登录;只展示当前引擎需要的字段,密钥保存在本机配置中,相关外链由系统浏览器打开。
-
+
-
+### 锚定模式与原生窗口
+
+内置的锚定式标准预设首轮使用 Minimal 工具表固定执行轨迹,从第二轮起恢复完整 Standard 工具目录。Project2 / DeepSeek V4 Pro 同配置 Ability 为 **98 / 99**,相对官方 Standard 的 91 约 **+8% / +9%**。这是社区实验预设,不代表所有任务都会提升。
+
+36px 薄标题栏保留更多对话空间;左侧 `•••` 菜单可重新启动 dsh 或在浏览器中打开。关闭窗口会停止对应的 `dsh web` 进程,凭据、权限和会话仍保存在 `~/.dsh`。
+
+
## 三十秒上手
-去 [GitHub Releases](https://github.com/TommyFang2077/dsh-desktop/releases/latest) 下载对应平台的安装包:
+从 [GitHub Releases](https://github.com/TommyFang2077/dsh-desktop/releases/latest) 下载对应平台的安装包;中国大陆网络可改用 [Gitea 发行版镜像](https://git.fangsiyuan.top/TomHanck4/dsh-easy-desktop/releases/latest)。壳会在启动时从该镜像检查自身更新,下载完成后先校验 Tauri 签名再安装。
| 平台 | 产物 | 运行时要求 |
| --- | --- | --- |
-| 🪟 Windows | NSIS `.exe` / `.msi` | [WebView2](https://developer.microsoft.com/microsoft-edge/webview2/)(安装器可引导下载)+ 本机 `dsh` |
-| 🍎 macOS | Apple Silicon / Intel `.dmg` | 未公证,首次打开需在「隐私与安全性」允许 + 本机 `dsh` |
-| 🐧 Linux | `.deb` / `.rpm` | WebKitGTK 4.1 + 本机 `dsh` |
-| 📦 Linux Flatpak | `.flatpak` | **零依赖**:自带 Node.js 24 与 `@deepseek-ai/dsh` |
+| Windows | NSIS `.exe` / `.msi` | [WebView2](https://developer.microsoft.com/microsoft-edge/webview2/)(安装器可引导下载)+ 本机 `dsh` |
+| macOS | Apple Silicon / Intel `.dmg` | 未公证,首次打开需在「隐私与安全性」允许 + 本机 `dsh` |
+| Linux | `.deb` / `.rpm` | WebKitGTK 4.1 + 本机 `dsh` |
+| Linux Flatpak | `.flatpak` | **零依赖**:自带 Node.js 24 与 `@deepseek-ai/dsh` |
-除 Flatpak 外需要本机有 `dsh`:
+除 Flatpak 外,需要先安装 `dsh`:
```bash
npm install -g @deepseek-ai/dsh
```
-装好后启动,等启动页转完就是官方 WebUI。
-
-
+安装后直接启动,等待启动页完成即可进入官方 WebUI。
## 功能一览
| 能力 | 说明 |
| --- | --- |
-| 原生窗口 | 启动 `dsh web --host 127.0.0.1 --port 0`,解析随机端口后用系统 WebView 加载 |
-| 零重写 | 官方会话、工作区、插件、技能全部保留;dsh 升级即界面升级 |
-| 内置视图 | 👁️ 纯文本 DeepSeek 也能粘贴识图(ModLens + 设置页) |
-| 内置锚定 | ⚓ 相对官方 Standard 约 +8%(Project2 Ability 91 → 98/99) |
-| 薄标题栏 | `•••` 菜单(重新启动 / 在浏览器中打开)+ 右侧最小化 · 缩放 · 关闭 |
-| 生命周期 | 关窗口停掉 `dsh web`;崩溃可从标题栏一键重启 |
+| 离线语音 | 对话框麦克风、`Ctrl+E` / `⌘E` 快捷键、SenseVoice 模型按需下载、本机识别 |
+| 插件市场 | 在设置内浏览、搜索、安装、更新、备份和恢复社区插件 |
+| 视觉模型 | 粘贴图片直接识别;用表单配置五类视觉引擎,不必手改 JSON |
+| 官方 WebUI | 会话、工作区、插件和技能原样保留;dsh 更新后界面同步更新 |
+| 锚定预设 | Project2 / DeepSeek V4 Pro 相对官方 Standard 约 +8% |
+| 原生生命周期 | 随机本地端口启动 `dsh web`;关闭窗口停止服务;崩溃可一键重启 |
-## 内置插件与预设
+## 内置能力与数据位置
-应用启动时把下面这些同步到用户目录。版本钉死在 [Makefile](Makefile);第三方原文许可证见 [docs/licenses/](docs/licenses/) 与 [THIRD_PARTY.md](THIRD_PARTY.md)。
+| 组件 | 当前基线 | 用途 | 本地位置 |
+| --- | --- | --- | --- |
+| 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` |
+| 锚定预设 | `ffb845c5480a` | 锚定式标准与零工具锚定式标准 | `~/.dsh/.agent-presets/` |
-### 1. DeepSeek Harness(`dsh`)
+应用启动时会同步桌面自带组件,但会保留用户从市场更新到更新版本的 ModLens。捆绑版本固定在 [Makefile](Makefile) 中。
-| | |
-| --- | --- |
-| 上游 | [@deepseek-ai/dsh](https://www.npmjs.com/package/@deepseek-ai/dsh) · [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) |
-| 当前版本 | `0.1.0-rc.6` |
-| 许可证 | MIT,Copyright (c) 2026 DeepSeek · [docs/licenses/deepseek-harness.LICENSE](docs/licenses/deepseek-harness.LICENSE) |
-| 本仓库 | **不 vendoring 源码**。Flatpak 构建时 `make vendor` 打进 Node 24 + npm 包;Windows / macOS / deb / rpm 运行时调用本机 `dsh` |
+`dsh` 的查找顺序:`DSH_DESKTOP_DSH_BIN` → `--dsh` → 桌面更新目录 → Flatpak 内置路径 → 宿主机常见路径 → `PATH` → `npx`。壳启动的 dsh、市场和语音运行时默认通过 `https://registry.npmmirror.com` 获取 npm 包;已有 `npm_config_registry` / `NPM_CONFIG_REGISTRY` 会保留,也可用 `DSH_DESKTOP_NPM_REGISTRY` 显式覆盖。设置 `DSH_DESKTOP_NO_UPDATE=1` 或传入 `--no-update` 可关闭启动时的 dsh 更新检查。
-解析顺序:
+## 安全与边界
-1. 环境变量 `DSH_DESKTOP_DSH_BIN`
-2. 命令行 `--dsh`
-3. 应用自己的更新目录(`$XDG_DATA_HOME/dsh-desktop/dsh-prefix/bin/dsh`)
-4. **Flatpak**:内置 `/app/bin/dsh`
-5. **宿主机**:`~/.local/bin/dsh` → `~/.npm/_npx` 缓存 → `PATH` → `npx --yes @deepseek-ai/dsh`
-
-设 `DSH_DESKTOP_NO_UPDATE=1` 或传 `--no-update` 可关掉启动时的 npm 更新检查。
-
-### 2. ModLens(`@liustack/modlens`)
-
-| | |
-| --- | --- |
-| 上游 | [liustack/modlens](https://github.com/liustack/modlens) · [npm @liustack/modlens](https://www.npmjs.com/package/@liustack/modlens) |
-| 当前版本 | `3.16.6` |
-| 作者 | Leon Liu / [liustack](https://github.com/liustack) |
-| 许可证 | MIT · [docs/licenses/modlens.LICENSE](docs/licenses/modlens.LICENSE) |
-| 亮点 | 给 DeepSeek 带上眼睛:纯文本模型粘贴即可读图 |
-| 安装位置 | 启动时复制到 `~/.dsh/profiles/web/node_modules/@liustack/modlens` |
-
-官方安装方式(本应用已内置,一般不必再跑):
-
-```bash
-npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.6
-```
-
-### 3. `dsh-desktop-vision`(本仓库)
-
-| | |
-| --- | --- |
-| 路径 | [`plugins/dsh-desktop-vision/`](plugins/dsh-desktop-vision/) |
-| 版本 | `0.1.4` |
-| 许可证 | 与本仓库相同(MIT) |
-| 作用 | 在官方 WebUI **设置 → 视觉模型** 增加表单,读写 `~/.modlens/config.json` |
-
-支持的引擎:
-
-| 引擎 | 默认接口 | 获取密钥 |
-| --- | --- | --- |
-| OpenAI 兼容 | `https://api.openai.com/v1` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
-| Gemini API | `https://generativelanguage.googleapis.com` | [aistudio.google.com/apikey](https://aistudio.google.com/apikey) |
-| Anthropic | `https://api.anthropic.com` | [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys) |
-| Antigravity CLI | 本机 CLI,无需填 URL | [antigravity.google](https://antigravity.google/) |
-| Claude CLI | 本机 CLI,无需填 URL | [code.claude.com](https://code.claude.com) |
-
-外链在系统浏览器中打开(Tauri `on_navigation`),密钥只写在本机 ModLens 配置里。
-
-### 4. Anchored Standard 预设
-
-| | |
-| --- | --- |
-| 上游 | [xiaobright/dsh-anchored-standard](https://github.com/xiaobright/dsh-anchored-standard) |
-| 钉选提交 | [`ffb845c5480adc953392a6db6f8a98ede621174b`](https://github.com/xiaobright/dsh-anchored-standard/commit/ffb845c5480adc953392a6db6f8a98ede621174b) |
-| 作者 | [xiaobright](https://github.com/xiaobright) |
-| 许可证 | MIT(含 DeepSeek 部分版权)· [LICENSE](docs/licenses/dsh-anchored-standard.LICENSE) · [NOTICE](docs/licenses/dsh-anchored-standard.NOTICE) |
-| 本仓库中的名称 | **锚定式标准(实验)**、**零工具锚定式标准(实验)**(`scripts/localize_preset.py` 本地化) |
-| 亮点 | 相对官方 Standard 约 **+8%**(Project2 Ability 91 → 98/99),同时拿回完整工具目录 |
-| 安装位置 | `~/.dsh/.agent-presets/anchored-standard` 与 `zero-anchored-standard` |
-
-NOTICE 写明:预设改编自 DeepSeek Harness Standard agent preset([deepseek-harness@47f9438](https://github.com/deepseek-ai/deepseek-harness))。这是社区实验 preset,**不是** DeepSeek 官方预设。若用户还没有默认 preset,桌面会把默认设为锚定式标准。
+- WebUI 只监听随机的 `127.0.0.1` 端口。
+- 语音模型按需下载;SenseVoice 识别在本机执行。
+- API 密钥只写入本机配置,不进入仓库或远端服务。
+- 卸载桌面壳不会删除 `~/.dsh` 中的会话、权限和工作区设置。
## 从源码运行
@@ -223,6 +175,7 @@ dsh-desktop/
├── src-tauri/ # Tauri 窗口、命令、deb/rpm/nsis/dmg
├── crates/dsh-core/ # 启动 / 更新 / ModLens / 预设 / 剪贴板
├── plugins/dsh-desktop-vision/ # 设置 → 视觉模型
+├── plugins/dsh-desktop-voice/ # 设置 → 语音输入 + 对话框麦克风
├── data/ # .desktop、图标、AppStream
├── flatpak/
├── docs/screenshots/ # README 截图
@@ -237,6 +190,19 @@ dsh-desktop/
自用项目,会持续更新。bug、想法、打包问题都欢迎开 [issue](https://github.com/TommyFang2077/dsh-desktop/issues)。
+## 上游项目与许可证
+
+引用与许可证集中列在这里,正文只介绍用户能直接使用的能力。完整版权说明见 [THIRD_PARTY.md](THIRD_PARTY.md),许可证副本见 [docs/licenses/](docs/licenses/)。
+
+| 组件 | 上游 / 固定版本 | 许可证 |
+| --- | --- | --- |
+| DeepSeek Harness | [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) · `0.1.0-rc.6` | [MIT](docs/licenses/deepseek-harness.LICENSE) |
+| ModLens | [liustack/modlens](https://github.com/liustack/modlens) · `3.16.6` | [MIT](docs/licenses/modlens.LICENSE) |
+| dshmarket | [dsh-market/dsh-market](https://github.com/dsh-market/dsh-market) · `1.9.0` | [MIT](docs/licenses/dshmarket.LICENSE) |
+| SenseVoiceSmall ONNX | [FunAudioLLM/SenseVoice](https://github.com/FunAudioLLM/SenseVoice) · 按需下载 | [MIT](docs/licenses/sensevoice.LICENSE) |
+| sherpa-onnx WASM | [k2-fsa/sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) · `1.13.5` · 按需下载 | Apache-2.0(许可证随运行时包提供) |
+| Anchored Standard | [xiaobright/dsh-anchored-standard](https://github.com/xiaobright/dsh-anchored-standard) · [`ffb845c5480a`](https://github.com/xiaobright/dsh-anchored-standard/commit/ffb845c5480adc953392a6db6f8a98ede621174b) | [MIT](docs/licenses/dsh-anchored-standard.LICENSE) · [NOTICE](docs/licenses/dsh-anchored-standard.NOTICE) |
+
## 图标与商标
应用图标使用 [Icons8 上的 DeepSeek 图标](https://icons8.com/icon/YWOidjGxCpFW/deepseek)。DeepSeek 名称与鲸鱼标志归 DeepSeek 所有。本项目是独立第三方桌面壳,与 DeepSeek、ModLens、Anchored Standard 的作者均无从属关系。
diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md
index a1de674..5367eac 100644
--- a/THIRD_PARTY.md
+++ b/THIRD_PARTY.md
@@ -15,6 +15,10 @@ DeepSeek, liustack, or xiaobright.
| 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`. |
+| 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. |
The Anchored Standard NOTICE records that the presets adapt the DeepSeek
Harness Standard agent preset from
diff --git a/crates/dsh-core/Cargo.toml b/crates/dsh-core/Cargo.toml
index 544822a..5166bc3 100644
--- a/crates/dsh-core/Cargo.toml
+++ b/crates/dsh-core/Cargo.toml
@@ -11,6 +11,7 @@ base64 = "0.22"
dirs = "6"
regex = "1"
serde = { version = "1", features = ["derive"] }
+semver = "1"
serde_json = "1"
shlex = "1"
thiserror = "2"
diff --git a/crates/dsh-core/src/clipboard.rs b/crates/dsh-core/src/clipboard.rs
index 10c0231..157a93b 100644
--- a/crates/dsh-core/src/clipboard.rs
+++ b/crates/dsh-core/src/clipboard.rs
@@ -24,7 +24,12 @@ pub struct ClipboardFile {
}
pub fn is_image_mime(mime: &str) -> bool {
- let mime = mime.split(';').next().unwrap_or(mime).trim().to_ascii_lowercase();
+ let mime = mime
+ .split(';')
+ .next()
+ .unwrap_or(mime)
+ .trim()
+ .to_ascii_lowercase();
let mime = if mime == "image/jpg" {
"image/jpeg"
} else {
@@ -34,7 +39,12 @@ pub fn is_image_mime(mime: &str) -> bool {
}
pub fn filename_for_mime(mime: &str, index: usize) -> String {
- let mime = mime.split(';').next().unwrap_or(mime).trim().to_ascii_lowercase();
+ let mime = mime
+ .split(';')
+ .next()
+ .unwrap_or(mime)
+ .trim()
+ .to_ascii_lowercase();
let ext = match mime.as_str() {
"image/png" => "png",
"image/jpeg" | "image/jpg" => "jpg",
@@ -84,9 +94,7 @@ fn url_parse_file(rest: &str) -> Result {
let decoded = percent_decode(&uri);
if let Some(idx) = decoded.find("://") {
let after = &decoded[idx + 3..];
- let path = after
- .strip_prefix("localhost")
- .unwrap_or(after);
+ let path = after.strip_prefix("localhost").unwrap_or(after);
return Ok(path.to_string());
}
if let Some(path) = decoded.strip_prefix("file:") {
@@ -101,7 +109,8 @@ fn percent_decode(input: &str) -> String {
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
- if let Ok(v) = u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16)
+ if let Ok(v) =
+ u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16)
{
out.push(v);
i += 3;
@@ -220,7 +229,10 @@ mod tests {
detect_image_mime(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0, 1]),
Some("image/png")
);
- assert_eq!(detect_image_mime(&[0xFF, 0xD8, 0xFF, 0xE0]), Some("image/jpeg"));
+ assert_eq!(
+ detect_image_mime(&[0xFF, 0xD8, 0xFF, 0xE0]),
+ Some("image/jpeg")
+ );
assert_eq!(detect_image_mime(b"not-an-image"), None);
}
diff --git a/crates/dsh-core/src/launcher.rs b/crates/dsh-core/src/launcher.rs
index 1d0eaca..08e0674 100644
--- a/crates/dsh-core/src/launcher.rs
+++ b/crates/dsh-core/src/launcher.rs
@@ -10,7 +10,7 @@ use regex::Regex;
use thiserror::Error;
use crate::paths::{home_dir, is_flatpak};
-use crate::updater::update_dsh_bin;
+use crate::updater::{configure_npm_registry, update_dsh_bin};
use crate::{ENV_BIN_OVERRIDE, ENV_CWD_OVERRIDE};
pub const DSH_DEFAULT_HOST: &str = "127.0.0.1";
@@ -117,8 +117,8 @@ impl DshLauncher {
}
}
if let Some(cli) = &self.dsh_bin_override {
- let parts = shlex::split(cli)
- .ok_or_else(|| DshNotFound::new("dsh 命令不是合法的命令"))?;
+ let parts =
+ shlex::split(cli).ok_or_else(|| DshNotFound::new("dsh 命令不是合法的命令"))?;
candidates.push(parts);
}
@@ -197,6 +197,7 @@ impl DshLauncher {
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.stdin(Stdio::null());
+ configure_npm_registry(&mut cmd);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
@@ -208,9 +209,9 @@ impl DshLauncher {
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
}
- let mut child = cmd.spawn().map_err(|exc| {
- DshNotFound::new(format!("无法启动 dsh web: {exc}"))
- })?;
+ let mut child = cmd
+ .spawn()
+ .map_err(|exc| DshNotFound::new(format!("无法启动 dsh web: {exc}")))?;
let stdout = child.stdout.take();
Ok(DshProcess::new(child, stdout, self.in_flatpak))
}
diff --git a/crates/dsh-core/src/lib.rs b/crates/dsh-core/src/lib.rs
index f020855..1c92040 100644
--- a/crates/dsh-core/src/lib.rs
+++ b/crates/dsh-core/src/lib.rs
@@ -19,3 +19,4 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const ENV_BIN_OVERRIDE: &str = "DSH_DESKTOP_DSH_BIN";
pub const ENV_CWD_OVERRIDE: &str = "DSH_DESKTOP_CWD";
pub const ENV_NO_UPDATE: &str = "DSH_DESKTOP_NO_UPDATE";
+pub const ENV_NPM_REGISTRY: &str = "DSH_DESKTOP_NPM_REGISTRY";
diff --git a/crates/dsh-core/src/modlens.rs b/crates/dsh-core/src/modlens.rs
index a4735c1..f5e11d2 100644
--- a/crates/dsh-core/src/modlens.rs
+++ b/crates/dsh-core/src/modlens.rs
@@ -8,7 +8,13 @@ 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 MANAGED_BUNDLES_DIR: &str = ".dsh-desktop/bundles";
pub const HIDE_PLAIN_TWINS_JS: &str = include_str!("../../../ui/inject/hide-twins.js");
pub const MANAGED_OVERLAY: &str = "\
@@ -48,12 +54,42 @@ pub fn read_modlens_version(prefix: &Path) -> Option {
.map(|s| s.to_string())
}
+fn bundle_should_replace(installed: Option<&str>, bundled: &str) -> bool {
+ let Some(installed) = installed else {
+ return true;
+ };
+ if installed == bundled {
+ return false;
+ }
+ match (
+ semver::Version::parse(installed),
+ semver::Version::parse(bundled),
+ ) {
+ (Ok(installed), Ok(bundled)) => installed < bundled,
+ _ => true,
+ }
+}
+
+fn bundled_plugin(paths: &BundledPaths, dirs: &[&str]) -> Option {
+ dirs.iter().find_map(|rel| {
+ paths
+ .find_dir(rel, "package.json")
+ .filter(|p| p.join("client.js").is_file() && p.join("index.js").is_file())
+ })
+}
+
pub fn bundled_vision_plugin(paths: &BundledPaths) -> Option {
- paths
- .find_dir("vision", "package.json")
- .or_else(|| paths.find_dir("dsh-desktop-vision", "package.json"))
- .or_else(|| paths.find_dir("plugins/dsh-desktop-vision", "package.json"))
- .filter(|p| p.join("client.js").is_file())
+ bundled_plugin(
+ paths,
+ &["vision", "dsh-desktop-vision", "plugins/dsh-desktop-vision"],
+ )
+}
+
+fn bundled_voice_plugin(paths: &BundledPaths) -> Option {
+ bundled_plugin(
+ paths,
+ &["voice", "dsh-desktop-voice", "plugins/dsh-desktop-voice"],
+ )
}
pub fn bundled_modlens_prefix(paths: &BundledPaths) -> Option {
@@ -62,6 +98,13 @@ pub fn bundled_modlens_prefix(paths: &BundledPaths) -> Option {
.or_else(|| paths.find_dir("vendor/modlens", "node_modules/@liustack/modlens"))
}
+pub fn bundled_market_prefix(paths: &BundledPaths) -> Option {
+ paths
+ .find_dir("market", "node_modules/dshmarket/package.json")
+ .or_else(|| paths.find_dir("dshmarket", "node_modules/dshmarket/package.json"))
+ .or_else(|| paths.find_dir("vendor/dshmarket", "node_modules/dshmarket/package.json"))
+}
+
fn install_into_profile(src_prefix: &Path, profile: &Path) -> std::io::Result<()> {
let dest_pkg = package_dir(profile);
copy_tree(&package_dir(src_prefix), &dest_pkg, true)?;
@@ -86,22 +129,92 @@ fn read_pkg_version(dir: &Path) -> Option {
.map(|s| s.to_string())
}
-fn install_vision_plugin(paths: &BundledPaths, profile: &Path) -> bool {
- let Some(src) = bundled_vision_plugin(paths) else {
+fn local_plugin_spec(pkg: &str) -> String {
+ format!("file:{MANAGED_BUNDLES_DIR}/{pkg}")
+}
+
+fn install_profile_plugin(
+ profile: &Path,
+ pkg: &str,
+ expected_version: &str,
+ src: Option,
+) -> bool {
+ let Some(src) = src else {
return false;
};
- let dest = profile.join("node_modules").join(VISION_PACKAGE);
- let up_to_date = dest.join("client.js").is_file()
- && dest.join("index.js").is_file()
- && read_pkg_version(&dest) == read_pkg_version(&src);
- if !up_to_date && copy_tree(&src, &dest, true).is_err() {
+ if read_pkg_version(&src).as_deref() != Some(expected_version) {
return false;
}
+ let managed = profile.join(MANAGED_BUNDLES_DIR).join(pkg);
+ let managed_current = managed.join("client.js").is_file()
+ && managed.join("index.js").is_file()
+ && read_pkg_version(&managed) == read_pkg_version(&src);
+ if !managed_current && copy_tree(&src, &managed, true).is_err() {
+ return false;
+ }
+ let dest = profile.join("node_modules").join(pkg);
+ let installed_current = dest.join("client.js").is_file()
+ && dest.join("index.js").is_file()
+ && read_pkg_version(&dest) == read_pkg_version(&managed);
+ if !installed_current && copy_tree(&managed, &dest, true).is_err() {
+ return false;
+ }
+ let fallback = dsh_home().join("profiles/node_modules").join(pkg);
+ let _ = replace_symlink(&fallback, &dest);
+ 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,
+ VOICE_PACKAGE,
+ VOICE_PLUGIN_VERSION,
+ bundled_voice_plugin(paths),
+ )
+}
+
+fn install_market_plugin(paths: &BundledPaths, profile: &Path) -> std::io::Result