diff --git a/.gitignore b/.gitignore
index ab96dff..6f6c3aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,8 @@ coverage/
dist/
.env
.env.local
+.env.signing
+apps/*/.env.signing
apps/*/dist/
apps/*/out/
apps/*/release/
diff --git a/apps/desktop-client/DESIGN.md b/apps/desktop-client/DESIGN.md
index 3a70171..b774243 100644
--- a/apps/desktop-client/DESIGN.md
+++ b/apps/desktop-client/DESIGN.md
@@ -365,3 +365,5 @@ The color system is almost entirely achromatic — dark backgrounds with white/g
5. Brand indigo (`#5e6ad2` / `#7170ff`) is the only chromatic color — everything else is grayscale
6. Borders are always semi-transparent white, never solid dark colors on dark backgrounds
7. Berkeley Mono for any code or technical content, Inter Variable for everything else
+
+rnzy-benw-ynai-ftbl
\ No newline at end of file
diff --git a/apps/desktop-client/build/entitlements.mac.inherit.plist b/apps/desktop-client/build/entitlements.mac.inherit.plist
new file mode 100644
index 0000000..667e0db
--- /dev/null
+++ b/apps/desktop-client/build/entitlements.mac.inherit.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+ com.apple.security.cs.allow-unsigned-executable-memory
+ com.apple.security.cs.allow-dyld-environment-variables
+ com.apple.security.cs.disable-library-validation
+ com.apple.security.inherit
+
+
diff --git a/apps/desktop-client/build/entitlements.mac.plist b/apps/desktop-client/build/entitlements.mac.plist
new file mode 100644
index 0000000..65aab31
--- /dev/null
+++ b/apps/desktop-client/build/entitlements.mac.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+ com.apple.security.cs.allow-unsigned-executable-memory
+ com.apple.security.cs.allow-dyld-environment-variables
+ com.apple.security.cs.disable-library-validation
+ com.apple.security.device.audio-input
+ com.apple.security.device.camera
+ com.apple.security.network.client
+ com.apple.security.network.server
+ com.apple.security.files.user-selected.read-write
+ com.apple.security.automation.apple-events
+
+
diff --git a/apps/desktop-client/electron-builder.yml b/apps/desktop-client/electron-builder.yml
index d52ec47..709bec2 100644
--- a/apps/desktop-client/electron-builder.yml
+++ b/apps/desktop-client/electron-builder.yml
@@ -35,18 +35,25 @@ files:
- "!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
mac:
category: public.app-category.productivity
- identity: null
- notarize: false
- hardenedRuntime: false
+ # identity 不写死:让 electron-builder 自动从 CSC_NAME 环境变量或钥匙串里挑
+ # 注意:写 `identity: null` 是"明确不签名",不要这么写
+ notarize: false # 我们用 notarytool 自己跑,比 electron-builder 内置更稳
+ hardenedRuntime: true
gatekeeperAssess: false
+ entitlements: build/entitlements.mac.plist
+ entitlementsInherit: build/entitlements.mac.inherit.plist
electronLanguages:
- en
- zh_CN
+ extendInfo:
+ NSCameraUsageDescription: 用于视频会议和录屏功能
+ NSMicrophoneUsageDescription: 用于音频采集
+ NSAppleEventsUsageDescription: 用于自动化控制其他应用
target:
- target: dmg
- arch: [arm64]
- - target: zip
- arch: [arm64]
+ arch: [arm64, universal]
+ # - target: zip
+ # arch: [arm64, universal]
dmg:
format: ULFO
contents:
diff --git a/apps/desktop-client/package.json b/apps/desktop-client/package.json
index deaedfa..6c996db 100644
--- a/apps/desktop-client/package.json
+++ b/apps/desktop-client/package.json
@@ -14,6 +14,8 @@
"dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev --inspect=9229",
"build": "electron-vite build",
"package:mac": "electron-builder --mac --arm64",
+ "sign:setup": "bash scripts/sign-setup.sh",
+ "sign:mac": "bash scripts/sign-mac.sh",
"test": "vitest run",
"typecheck": "vue-tsc --noEmit"
},
@@ -46,81 +48,5 @@
"vue": "^3.5.31",
"vue-router": "^4.5.1",
"vue-tsc": "^3.2.6"
- },
- "build": {
- "appId": "com.geo.rankly.desktop",
- "productName": "省心推",
- "copyright": "Copyright © 2026 shengxintui.com. All Rights Reserved.",
- "directories": {
- "output": "release",
- "buildResources": "build"
- },
- "compression": "maximum",
- "removePackageScripts": true,
- "removePackageKeywords": true,
- "asar": true,
- "asarUnpack": [
- "**/node_modules/better-sqlite3/build/Release/**",
- "**/node_modules/better-sqlite3/lib/**",
- "**/node_modules/bindings/**",
- "**/node_modules/file-uri-to-path/**",
- "**/node_modules/playwright-core/lib/**",
- "**/node_modules/playwright-core/bin/**"
- ],
- "files": [
- "out/**/*",
- "package.json",
- "!**/*.map",
- "!**/*.ts",
- "!**/*.tsx",
- "!**/*.md",
- "!**/*.markdown",
- "!**/{test,tests,__tests__,__mocks__,example,examples,docs,doc,demo,samples,man}/**",
- "!**/{LICENSE,LICENSE.*,license,CHANGELOG,CHANGELOG.*,README,README.*,HISTORY,HISTORY.*,AUTHORS,CONTRIBUTING}*",
- "!**/.*",
- "!**/node_modules/.bin/**",
- "!**/node_modules/*/{Makefile,Gulpfile.js,Gruntfile.js,CMakeLists.txt,*.coffee,*.flow,*.c,*.cc,*.cpp,*.h,*.hpp,*.gyp,*.gypi,*.tgz,binding.gyp}",
- "!**/node_modules/**/{*.d.ts,tsconfig*.json,jest.config*,rollup.config*,webpack.config*,vite.config*}",
- "!**/node_modules/better-sqlite3/{deps,src,docs,benchmark}/**",
- "!**/node_modules/better-sqlite3/{test_extension.c,download.sh,*.gyp,*.gypi}",
- "!**/node_modules/playwright-core/{types,ThirdPartyNotices.txt}/**",
- "!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
- ],
- "mac": {
- "category": "public.app-category.productivity",
- "identity": null,
- "electronLanguages": ["en", "zh_CN"],
- "target": [
- { "target": "dmg", "arch": ["arm64"] },
- { "target": "zip", "arch": ["arm64"] }
- ],
- "notarize": false,
- "hardenedRuntime": false,
- "gatekeeperAssess": false
- },
- "dmg": {
- "format": "ULFO",
- "contents": [
- { "x": 130, "y": 220 },
- { "x": 410, "y": 220, "type": "link", "path": "/Applications" }
- ]
- },
- "win": {
- "target": [
- { "target": "nsis", "arch": ["x64"] }
- ]
- },
- "nsis": {
- "oneClick": false,
- "perMachine": false,
- "allowToChangeInstallationDirectory": true
- },
- "linux": {
- "target": [
- { "target": "AppImage", "arch": ["x64"] }
- ],
- "category": "Office"
- },
- "publish": null
}
}
diff --git a/apps/desktop-client/scripts/ci-import-cert.sh b/apps/desktop-client/scripts/ci-import-cert.sh
new file mode 100755
index 0000000..10a4105
--- /dev/null
+++ b/apps/desktop-client/scripts/ci-import-cert.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+# CI 专用:把 base64 编码的 .p12 证书导入到一个临时钥匙串
+# 仅在 CI 环境调用,本地不要跑(会污染你的登录钥匙串体验)
+#
+# 必需环境变量:
+# CSC_LINK - base64 编码的 .p12 文件内容
+# CSC_KEY_PASSWORD - .p12 文件的密码
+# KEYCHAIN_PASSWORD - 临时钥匙串自身的密码(任意字符串,例如 secrets.GITHUB_TOKEN 截断都行)
+#
+# 副作用:创建 ~/Library/Keychains/build.keychain-db,已加入搜索路径并解锁
+# 销毁:脚本注册了 trap,进程退出时自动 delete-keychain
+set -euo pipefail
+
+: "${CSC_LINK:?CI 必须提供 CSC_LINK (base64 .p12)}"
+: "${CSC_KEY_PASSWORD:?CI 必须提供 CSC_KEY_PASSWORD}"
+: "${KEYCHAIN_PASSWORD:?CI 必须提供 KEYCHAIN_PASSWORD(用于临时钥匙串自身)}"
+
+KEYCHAIN_NAME="build.keychain"
+CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
+[ -z "${RUNNER_TEMP:-}" ] && CERT_PATH="/tmp/build_certificate.p12"
+
+# 1. 解码证书到临时文件
+echo "$CSC_LINK" | base64 --decode > "$CERT_PATH"
+
+# 2. 创建临时钥匙串
+security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
+security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME" # 6 小时不锁
+security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
+
+# 3. 导入 .p12(允许 codesign 和 productsign 不弹密码框)
+security import "$CERT_PATH" \
+ -P "$CSC_KEY_PASSWORD" \
+ -A -t cert -f pkcs12 \
+ -k "$KEYCHAIN_NAME"
+
+# 4. 关键:允许 codesign 不交互访问私钥
+security set-key-partition-list \
+ -S apple-tool:,apple:,codesign: \
+ -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" >/dev/null
+
+# 5. 把临时钥匙串加进搜索路径(放第一位,登录钥匙串保留作 fallback)
+security list-keychain -d user -s "$KEYCHAIN_NAME" $(security list-keychain -d user | tr -d '"')
+
+# 6. 立即清理证书文件
+rm -f "$CERT_PATH"
+
+# 7. 输出实际可用的 identity 名(让 sign-mac.sh 拿到精确的 CSC_NAME)
+IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_NAME" \
+ | grep "Developer ID Application" | head -n1 \
+ | sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/')
+
+if [ -z "$IDENTITY" ]; then
+ echo "✗ 导入后未在临时钥匙串里找到 Developer ID Application" >&2
+ exit 1
+fi
+echo "✓ 已导入证书到临时钥匙串: $KEYCHAIN_NAME"
+echo "✓ Identity: $IDENTITY"
+
+# 在 GitHub Actions 里把 CSC_NAME 写入后续步骤的 env
+if [ -n "${GITHUB_ENV:-}" ]; then
+ echo "CSC_NAME=$IDENTITY" >> "$GITHUB_ENV"
+ echo "✓ 已写入 \$GITHUB_ENV:CSC_NAME"
+fi
diff --git a/apps/desktop-client/scripts/sign-mac.sh b/apps/desktop-client/scripts/sign-mac.sh
new file mode 100755
index 0000000..8ce1474
--- /dev/null
+++ b/apps/desktop-client/scripts/sign-mac.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+# 一键:构建 → 签名 → 公证 → 装订 → 验证
+# 用法: pnpm sign:mac
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
+ok() { echo " ${G}✓${N} $1"; }
+fail() { echo " ${R}✗${N} $1"; }
+step() { echo ""; echo "${B}── $1 ──${N}"; }
+
+# ── 前置检查(本地/CI 双模式)─────────────────────────────────────
+# 本地模式:读 .env.signing,公证走钥匙串 profile
+# CI 模式:靠环境变量 CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID
+NOTARY_ARGS=()
+if [ -f .env.signing ] && [ -z "${CI:-}" ]; then
+ # shellcheck source=/dev/null
+ set -a; source .env.signing; set +a
+ : "${CSC_NAME:?.env.signing 缺少 CSC_NAME}"
+ : "${APPLE_TEAM_ID:?.env.signing 缺少 APPLE_TEAM_ID}"
+ : "${NOTARY_KEYCHAIN_PROFILE:?.env.signing 缺少 NOTARY_KEYCHAIN_PROFILE}"
+ NOTARY_ARGS=(--keychain-profile "$NOTARY_KEYCHAIN_PROFILE")
+ ok "模式: 本地(钥匙串 profile)"
+elif [ -n "${CSC_NAME:-}" ] && [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
+ NOTARY_ARGS=(--apple-id "$APPLE_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --team-id "$APPLE_TEAM_ID")
+ ok "模式: CI(环境变量)"
+else
+ fail "未找到签名配置"
+ echo " 本地: 运行 ${B}pnpm sign:setup${N}"
+ echo " CI: 设置 ${B}CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID${N} 环境变量"
+ exit 1
+fi
+
+ok "签名身份: $CSC_NAME"
+ok "Team ID: $APPLE_TEAM_ID"
+
+# ── 1. 构建 ──────────────────────────────────────────────────────
+step "[1/5] 构建 Electron 产物"
+pnpm build
+ok "构建完成"
+
+# ── 2. 打包 + 签名 ───────────────────────────────────────────────
+step "[2/5] 打包 + 签名 (electron-builder)"
+# 清理旧产物,避免 spctl 把上次未公证的 dmg 认成本次产物
+rm -f release/*.dmg release/*-mac.zip release/*.blockmap 2>/dev/null || true
+pnpm exec electron-builder --mac --arm64
+ok "打包完成"
+
+DMG=$(ls -t release/*.dmg 2>/dev/null | head -n1 || true)
+ZIP=$(ls -t release/*-mac.zip 2>/dev/null | head -n1 || true)
+APP=$(ls -td release/mac*/*.app 2>/dev/null | head -n1 || true)
+
+[ -z "$DMG" ] && { fail "未找到 .dmg 产物"; exit 1; }
+ok "DMG: $DMG"
+[ -n "$ZIP" ] && ok "ZIP: $ZIP"
+[ -n "$APP" ] && ok "APP: $APP"
+
+# 验证签名是否真的盖上了
+if [ -n "$APP" ]; then
+ if ! codesign --verify --deep --strict --verbose=2 "$APP" >/dev/null 2>&1; then
+ fail ".app 签名验证失败,停止公证"
+ codesign --verify --deep --strict --verbose=2 "$APP" || true
+ exit 1
+ fi
+ ok ".app 签名校验通过"
+fi
+
+# ── 3. 公证 ──────────────────────────────────────────────────────
+step "[3/5] 提交公证 (notarytool, 通常 1-5 分钟)"
+echo " 正在提交 DMG..."
+xcrun notarytool submit "$DMG" \
+ "${NOTARY_ARGS[@]}" \
+ --wait
+ok "DMG 公证通过"
+
+if [ -n "$ZIP" ]; then
+ echo " 正在提交 ZIP(用于自动更新)..."
+ xcrun notarytool submit "$ZIP" \
+ --keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \
+ --wait
+ ok "ZIP 公证通过"
+fi
+
+# ── 4. 装订 ticket ───────────────────────────────────────────────
+step "[4/5] 装订 ticket (stapler)"
+xcrun stapler staple "$DMG"
+ok "DMG ticket 已装订(断网也能验证)"
+# zip 不能 staple,但用户解压后里面的 .app 已经被 staple 过
+
+# ── 5. 验证 ──────────────────────────────────────────────────────
+step "[5/5] 最终验证 (Gatekeeper)"
+if spctl -a -vv -t install "$DMG" 2>&1 | tee /tmp/spctl.log | grep -q "accepted"; then
+ ok "Gatekeeper 验证通过"
+ grep -E "source=|origin=" /tmp/spctl.log | sed 's/^/ /'
+else
+ fail "Gatekeeper 拒绝了产物"
+ cat /tmp/spctl.log
+ exit 1
+fi
+
+# ── 完成 ────────────────────────────────────────────────────────
+SIZE=$(du -h "$DMG" | awk '{print $1}')
+echo ""
+echo "${G}${B}━━━━━━━━━━ 全部完成 ━━━━━━━━━━${N}"
+echo " 📦 $DMG (${SIZE})"
+[ -n "$ZIP" ] && echo " 📦 $ZIP"
+echo ""
+echo " 这份 DMG 可以直接发给任何 macOS 用户,"
+echo " 双击安装,首次打开${B}不会${N}弹\"无法验证开发者\"或被 Gatekeeper 拦截。"
diff --git a/apps/desktop-client/scripts/sign-setup.sh b/apps/desktop-client/scripts/sign-setup.sh
new file mode 100755
index 0000000..8d2abe7
--- /dev/null
+++ b/apps/desktop-client/scripts/sign-setup.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+# 一次性签名环境配置:检查证书 + 录入 Apple ID 凭证 + 写入 .env.signing
+# 用法: pnpm sign:setup
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+KEYCHAIN_PROFILE="GeoRanklySign"
+ENV_FILE=".env.signing"
+
+R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
+ok() { echo " ${G}✓${N} $1"; }
+fail() { echo " ${R}✗${N} $1"; }
+warn() { echo " ${Y}!${N} $1"; }
+hr() { echo ""; echo "${B}── $1 ──${N}"; }
+
+# ── 1. Xcode CLT ────────────────────────────────────────────────
+hr "1/3 检查 Xcode Command Line Tools"
+if xcode-select -p >/dev/null 2>&1; then
+ ok "已安装:$(xcode-select -p)"
+else
+ fail "未安装"
+ echo " 请在终端运行 ${B}xcode-select --install${N},弹窗装好后再来"
+ exit 1
+fi
+
+# ── 2. Developer ID 证书 ────────────────────────────────────────
+hr "2/3 检查 Developer ID Application 证书"
+IDENTITIES=$(security find-identity -v -p codesigning | grep "Developer ID Application" || true)
+
+if [ -z "$IDENTITIES" ]; then
+ fail "钥匙串里没找到 Developer ID Application 证书"
+ cat </dev/null 2>&1; then
+ ok "钥匙串里已有 profile:$KEYCHAIN_PROFILE"
+ read -r -p " 是否重新录入?(y/N): " RECONFIG
+ [[ "${RECONFIG:-n}" =~ ^[Yy]$ ]] || NEED_CREDENTIALS=0
+fi
+
+if [ "$NEED_CREDENTIALS" = "1" ]; then
+ echo ""
+ echo " 需要 ${B}Apple ID 邮箱${N} + ${B}App 专用密码${N}(不是登录密码!)"
+ echo " 生成 App 专用密码: https://appleid.apple.com → 登录与安全 → App 专用密码 → +"
+ echo ""
+ read -r -p " Apple ID 邮箱: " APPLE_ID
+ read -r -s -p " App 专用密码 (xxxx-xxxx-xxxx-xxxx): " APPLE_PWD
+ echo ""
+
+ if ! xcrun notarytool store-credentials "$KEYCHAIN_PROFILE" \
+ --apple-id "$APPLE_ID" \
+ --team-id "$TEAM_ID" \
+ --password "$APPLE_PWD" >/dev/null; then
+ fail "凭证录入失败,常见原因:密码格式错(需带短横线),或 Team ID 不匹配"
+ exit 1
+ fi
+ ok "凭证已存入钥匙串(profile: ${KEYCHAIN_PROFILE})"
+fi
+
+# ── 写入 .env.signing ───────────────────────────────────────────
+cat > "$ENV_FILE" <