feat: add mainland-reachable desktop distribution

This commit is contained in:
2026-08-16 23:00:54 +08:00
parent e8eeb4ac72
commit a3e1f11e85
42 changed files with 3661 additions and 191 deletions

135
scripts/build-gitea-update.py Executable file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Normalize Tauri artifacts and build the static updater manifest."""
from __future__ import annotations
import argparse
import json
import shutil
from pathlib import Path
MATRIX_TARGETS = {
"gitea-macos-arm64": ("darwin", "aarch64"),
"gitea-macos-x64": ("darwin", "x86_64"),
"gitea-linux": ("linux", "x86_64"),
"gitea-windows": ("windows", "x86_64"),
"gitea-flatpak": ("linux", "x86_64"),
}
DELIVERABLE_SUFFIXES = (
".app.tar.gz.sig",
".app.tar.gz",
".AppImage.sig",
".AppImage",
".flatpak",
".dmg",
".deb",
".rpm",
".exe.sig",
".exe",
".msi.sig",
".msi",
)
UPDATER_SUFFIX = {
"darwin": ".app.tar.gz",
"linux": ".AppImage",
"windows": ".exe",
}
INSTALLER_KIND = {
"darwin": "app",
"linux": "appimage",
"windows": "nsis",
}
def artifact_target(path: Path, root: Path) -> tuple[str, str] | None:
relative = path.relative_to(root)
for part in relative.parts:
if part in MATRIX_TARGETS:
return MATRIX_TARGETS[part]
return None
def deliverable_suffix(name: str) -> str | None:
return next((suffix for suffix in DELIVERABLE_SUFFIXES if name.endswith(suffix)), None)
def stage_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.rglob("*")):
if not path.is_file():
continue
target = artifact_target(path, source)
suffix = deliverable_suffix(path.name)
if target is None or suffix is None:
continue
os_name, arch = target
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,
package_base_url: str,
notes: str,
pub_date: str | None,
) -> dict[str, object]:
platforms: dict[str, dict[str, str]] = {}
for (os_name, arch, suffix), artifact in staged.items():
if suffix != UPDATER_SUFFIX.get(os_name):
continue
signature = staged.get((os_name, arch, suffix + ".sig"))
if signature is None:
raise ValueError(f"missing signature for {artifact.name}")
entry = {
"url": f"{package_base_url.rstrip('/')}/{version}/{artifact.name}",
"signature": signature.read_text(encoding="utf-8").strip(),
}
platforms[f"{os_name}-{arch}-{INSTALLER_KIND[os_name]}"] = entry
platforms[f"{os_name}-{arch}"] = entry
required = {"darwin-aarch64", "darwin-x86_64", "linux-x86_64", "windows-x86_64"}
missing = required.difference(platforms)
if missing:
raise ValueError(f"missing updater targets: {', '.join(sorted(missing))}")
manifest: dict[str, object] = {
"version": version,
"notes": notes,
"platforms": platforms,
}
if pub_date:
manifest["pub_date"] = pub_date
return manifest
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--artifacts", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--version", required=True)
parser.add_argument("--package-base-url", required=True)
parser.add_argument("--notes", default="DeepSeek Harness Desktop update")
parser.add_argument("--pub-date")
args = parser.parse_args()
staged = stage_artifacts(args.artifacts, args.output, args.version)
manifest = build_manifest(
staged,
args.version,
args.package_base_url,
args.notes,
args.pub_date,
)
(args.output / "latest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Apply the downstream mainland-connectivity warning to vendored dshmarket."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MARKET = ROOT / "vendor" / "dshmarket" / "node_modules" / "dshmarket"
def insert_after(path: Path, anchor: str, addition: str) -> None:
text = path.read_text(encoding="utf-8")
if addition.strip() in text:
return
if text.count(anchor) != 1:
raise RuntimeError(f"expected one patch anchor in {path}: {anchor!r}")
path.write_text(text.replace(anchor, anchor + addition), encoding="utf-8")
def insert_before(path: Path, anchor: str, addition: str) -> None:
text = path.read_text(encoding="utf-8")
if addition.strip() in text:
return
if text.count(anchor) != 1:
raise RuntimeError(f"expected one patch anchor in {path}: {anchor!r}")
path.write_text(text.replace(anchor, addition + anchor), encoding="utf-8")
def main() -> None:
locales = MARKET / "src" / "client" / "locales.ts"
insert_after(
locales,
" terminalWarn: '这看起来是终端/命令行插件:装进网页版可能无效,甚至导致 DeepSeek Harness 无法启动。建议先看它的使用说明,按说明装进对应的 profile。',\n",
" githubInstallWarn: '此插件没有 npm 安装包,安装时必须从 github.com 下载源码;中国大陆网络通常无法直连。请仅在当前网络能访问 GitHub 时继续。',\n",
)
insert_after(
locales,
" terminalWarn: 'This looks like a terminal/CLI plugin: installing it into the web profile may do nothing, or even break DeepSeek Harness startup. Read its README and install it into the profile it targets.',\n",
" githubInstallWarn: 'This plugin has no npm package. Installation must download its source from github.com and will fail where GitHub is unreachable. Continue only if this network can access GitHub.',\n",
)
section = MARKET / "src" / "client" / "MarketSection.tsx"
source_anchor = """ <p className={css.modalNote}><IconWarningOutline16 size={14} className={css.bannerIcon} />{' ' + t('confirmWarn')}</p>\n"""
source_addition = """ {typeof confirming.npm !== 'string' && (\n <p className={css.warnLine}>\n <IconWarningOutline16 size={14} className={css.bannerIcon} />\n {' ' + t('githubInstallWarn')}\n </p>\n )}\n"""
insert_before(section, source_anchor, source_addition)
client = MARKET / "client" / "client.js"
insert_after(
client,
'\t\t\tterminalWarn: "这看起来是终端/命令行插件:装进网页版可能无效,甚至导致 DeepSeek Harness 无法启动。建议先看它的使用说明,按说明装进对应的 profile。",\n',
'\t\t\tgithubInstallWarn: "此插件没有 npm 安装包,安装时必须从 github.com 下载源码;中国大陆网络通常无法直连。请仅在当前网络能访问 GitHub 时继续。",\n',
)
insert_after(
client,
'\t\t\tterminalWarn: "This looks like a terminal/CLI plugin: installing it into the web profile may do nothing, or even break DeepSeek Harness startup. Read its README and install it into the profile it targets.",\n',
'\t\t\tgithubInstallWarn: "This plugin has no npm package. Installation must download its source from github.com and will fail where GitHub is unreachable. Continue only if this network can access GitHub.",\n',
)
built_anchor = """\t\t\t\t\t\t\t/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(\"p\", {\n\t\t\t\t\t\t\t\tclassName: Market_module_css_default.modalNote,\n"""
built_addition = """\t\t\t\t\t\t\ttypeof confirming.npm !== \"string\" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(\"p\", {\n\t\t\t\t\t\t\t\tclassName: Market_module_css_default.warnLine,\n\t\t\t\t\t\t\t\tchildren: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {\n\t\t\t\t\t\t\t\t\tsize: 14,\n\t\t\t\t\t\t\t\t\tclassName: Market_module_css_default.bannerIcon\n\t\t\t\t\t\t\t\t}), \" \" + t(\"githubInstallWarn\")]\n\t\t\t\t\t\t\t}),\n"""
insert_before(client, built_anchor, built_addition)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Publish normalized installers and signed updater artifacts to Gitea.
set -euo pipefail
STAGING_DIR="${1:?usage: publish-gitea-release.sh STAGING_DIR}"
: "${GITEA_TOKEN:?GITEA_TOKEN is required}"
: "${GITEA_BASE_URL:?GITEA_BASE_URL is required}"
: "${GITEA_OWNER:?GITEA_OWNER is required}"
: "${GITEA_REPO:?GITEA_REPO is required}"
: "${RELEASE_TAG:?RELEASE_TAG is required}"
: "${RELEASE_VERSION:?RELEASE_VERSION is required}"
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
if curl --fail --silent --show-error -H "$AUTH" "$API/tags/$RELEASE_TAG" >/dev/null 2>&1; then
break
fi
sleep 10
done
curl --fail --silent --show-error -H "$AUTH" "$API/tags/$RELEASE_TAG" >/dev/null
release_json=$(curl --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 签名校验。" \
'{tag_name:$tag,target_commitish:$tag,name:$name,body:$body,draft:false,prerelease:false}')
release_json=$(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')
fi
# Versioned generic package: immutable URLs consumed by latest.json.
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" \
"$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" \
"$PACKAGE_BASE/latest/latest.json" >/dev/null
assets=$(curl --fail --silent --show-error -H "$AUTH" "$API/releases/$release_id/assets")
for file in "$STAGING_DIR"/*; do
[[ -f "$file" ]] || continue
name=$(basename "$file")
case "$name" in
*.dmg|*.deb|*.rpm|*.exe|*.msi|*.AppImage|*.flatpak) ;;
*) continue ;;
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" \
"$API/releases/$release_id/assets/$old_id" >/dev/null
fi
curl --fail --silent --show-error -H "$AUTH" \
-F "attachment=@$file" "$API/releases/$release_id/assets?name=$name" >/dev/null
done

93
scripts/setup-gitea-release.sh Executable file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# Interactive one-time setup for Gitea publishing and Tauri update signing.
set -euo pipefail
BOLD='\033[1m'
DIM='\033[2m'
RED='\033[31m'
GREEN='\033[32m'
RESET='\033[0m'
GITEA_BASE_URL='https://git.fangsiyuan.top'
GITEA_OWNER='TomHanck4'
GITEA_REPO='dsh-easy-desktop'
step() {
printf '\n%b%s%b\n' "$BOLD" "$1" "$RESET"
printf '%b%s%b\n\n' "$DIM" "$2" "$RESET"
}
confirm() {
local answer
read -r -p "$1 [y/N] " answer
[[ "$answer" =~ ^[Yy]$ ]]
}
need() {
command -v "$1" >/dev/null 2>&1 || {
printf '%b缺少命令%s%b\n' "$RED" "$1" "$RESET" >&2
exit 1
}
}
need curl
need gh
need jq
printf '%bGitea 大陆发行通道设置%b\n' "$BOLD" "$RESET"
printf '共 3 步:验证 Gitea → 配置签名 → 写入 GitHub Secrets\n'
step '1/3 验证 Gitea' '需要一个可写入 TomHanck4/dsh-easy-desktop 发行版与 Generic Package 的 Gitea token。'
printf '%bHTTPS 已启用:%btoken 与发行包传输均受 TLS 保护。\n' "$GREEN" "$RESET"
read -r -s -p '粘贴 Gitea token: ' GITEA_TOKEN
printf '\n'
[[ -n "$GITEA_TOKEN" ]] || { printf '%btoken 不能为空%b\n' "$RED" "$RESET" >&2; exit 1; }
AUTH="Authorization: token $GITEA_TOKEN"
user=$(curl --fail --silent --show-error -H "$AUTH" "$GITEA_BASE_URL/api/v1/user")
repo=$(curl --fail --silent --show-error -H "$AUTH" "$GITEA_BASE_URL/api/v1/repos/$GITEA_OWNER/$GITEA_REPO")
printf '已认证:%s仓库%s\n' "$(printf '%s' "$user" | jq -r .login)" "$(printf '%s' "$repo" | jq -r .full_name)"
case "$(printf '%s' "$repo" | jq -r .html_url)" in
https://*) ;;
*)
printf '%bGitea API 仍生成 HTTP 链接。请先把 app.ini 的 [server] ROOT_URL 改为 https://git.fangsiyuan.top/ 并重启 Gitea。%b\n' "$RED" "$RESET" >&2
exit 1
;;
esac
[[ "$(printf '%s' "$repo" | jq -r .mirror)" == 'true' ]] || {
printf '%b仓库不是 pull mirror发布脚本无法同步 GitHub tag。%b\n' "$RED" "$RESET" >&2
exit 1
}
step '2/3 配置 Tauri 签名' '已有密钥可复用;否则向导通过 Tauri CLI 在 ~/.tauri 生成。私钥不会写入仓库。'
if confirm '已有 Tauri updater 私钥吗?'; then
read -r -e -p '私钥路径: ' PRIVATE_KEY_PATH
read -r -e -p '公钥路径: ' PUBLIC_KEY_PATH
read -r -s -p '私钥密码(没有则留空): ' SIGNING_PASSWORD
printf '\n'
else
need npx
PRIVATE_KEY_PATH="$HOME/.tauri/dsh-easy-desktop.key"
PUBLIC_KEY_PATH="$PRIVATE_KEY_PATH.pub"
mkdir -p "$(dirname "$PRIVATE_KEY_PATH")"
read -r -s -p '设置私钥密码(可留空): ' SIGNING_PASSWORD
printf '\n正在生成签名密钥…\n'
npm_config_registry='https://registry.npmmirror.com' \
npx --yes @tauri-apps/cli@2 signer generate --ci --password "$SIGNING_PASSWORD" -w "$PRIVATE_KEY_PATH"
fi
[[ -f "$PRIVATE_KEY_PATH" ]] || { printf '%b找不到私钥%s%b\n' "$RED" "$PRIVATE_KEY_PATH" "$RESET" >&2; exit 1; }
[[ -f "$PUBLIC_KEY_PATH" ]] || { printf '%b找不到公钥%s%b\n' "$RED" "$PUBLIC_KEY_PATH" "$RESET" >&2; exit 1; }
chmod 600 "$PRIVATE_KEY_PATH"
printf '私钥:%s\n公钥%s\n' "$PRIVATE_KEY_PATH" "$PUBLIC_KEY_PATH"
step '3/3 写入 GitHub Secrets' 'gh 会把四项机密直接写到当前仓库;终端不会打印 secret 内容。'
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
if [[ -n "$SIGNING_PASSWORD" ]]; then
printf '%s' "$SIGNING_PASSWORD" | gh secret set TAURI_SIGNING_PRIVATE_KEY_PASSWORD
fi
unset GITEA_TOKEN SIGNING_PASSWORD AUTH
printf '\n%b设置完成。%b 下次推送 v* tag 时Release 工作流会发布 GitHub 与 Gitea 两套安装包。\n' "$GREEN" "$RESET"
printf '首个版本发布后检查:%s/%s/%s/releases/latest\n' "$GITEA_BASE_URL" "$GITEA_OWNER" "$GITEA_REPO"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Fetch ModLens + Anchored Standard into vendor/ for Tauri resource bundling.
# Fetch ModLens, dshmarket, and Anchored Standard into vendor/ for Tauri resources.
# Does not vendor @deepseek-ai/dsh (that is Flatpak-only; see `make vendor`).
set -euo pipefail
@@ -7,6 +7,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
MODLENS_VERSION="${MODLENS_VERSION:-3.16.6}"
MARKET_VERSION="${MARKET_VERSION:-1.9.0}"
ANCHORED_COMMIT="${ANCHORED_COMMIT:-ffb845c5480adc953392a6db6f8a98ede621174b}"
ANCHORED_REPO="${ANCHORED_REPO:-https://github.com/xiaobright/dsh-anchored-standard.git}"
if [ -z "${PYTHON:-}" ]; then
@@ -20,6 +21,7 @@ fi
ANCHORED_DIR="vendor/anchored-standard"
ZERO_DIR="vendor/zero-anchored-standard"
MODLENS_DIR="vendor/modlens"
MARKET_DIR="vendor/dshmarket"
rm -rf vendor/.anchored-src "$ANCHORED_DIR" "$ZERO_DIR"
mkdir -p vendor/.anchored-src
@@ -42,7 +44,13 @@ rm -rf "$MODLENS_DIR"
mkdir -p "$MODLENS_DIR"
npm install --prefix "$MODLENS_DIR" --prefer-offline --no-audit --no-fund "@liustack/modlens@${MODLENS_VERSION}"
rm -rf "$MARKET_DIR"
mkdir -p "$MARKET_DIR"
npm install --prefix "$MARKET_DIR" --prefer-offline --no-audit --no-fund "dshmarket@${MARKET_VERSION}"
"$PYTHON" scripts/patch-dshmarket-mainland.py
test -f "$ANCHORED_DIR/preset.yml"
test -f "$ZERO_DIR/preset.yml"
test -d "$MODLENS_DIR/node_modules/@liustack/modlens"
echo "vendored ModLens ${MODLENS_VERSION} and Anchored Standard ${ANCHORED_COMMIT}"
test -d "$MARKET_DIR/node_modules/dshmarket"
echo "vendored ModLens ${MODLENS_VERSION}, dshmarket ${MARKET_VERSION}, and Anchored Standard ${ANCHORED_COMMIT}"