chore(desktop): 构建改进与 UI 完善

- build-sidecars.ts macOS 编译后自动 ad-hoc codesign
- build-macos-arm64.sh 构建流程优化
- tauri.conf.json 配置更新
- i18n 中英文翻译补充
- Settings 页面与 uiStore 调整
This commit is contained in:
程序员阿江(Relakkes) 2026-04-15 14:18:55 +08:00
parent 651ab936b7
commit 0e60cadb13
9 changed files with 241 additions and 17 deletions

2
.gitignore vendored
View File

@ -24,6 +24,7 @@ e2e-*.png
# Desktop build output
desktop/dist/
desktop/build-artifacts/
desktop/src-tauri/binaries/
desktop/src-tauri/target/
desktop/src-tauri/gen/
@ -57,3 +58,4 @@ sidebar-snapshot.md
# VitePress
docs/.vitepress/dist
docs/.vitepress/cache
runtime/__pycache__

View File

@ -7,7 +7,10 @@ DESKTOP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
REPO_ROOT="$(cd "${DESKTOP_DIR}/.." && pwd)"
TARGET_TRIPLE="aarch64-apple-darwin"
OUTPUT_DIR="${DESKTOP_DIR}/src-tauri/target/release/bundle/dmg"
TAURI_TARGET_DIR="${DESKTOP_DIR}/src-tauri/target"
CANONICAL_OUTPUT_DIR="${DESKTOP_DIR}/build-artifacts/macos-arm64"
APP_BUNDLE_NAME="Claude Code Haha.app"
APP_BUNDLE_ID="com.claude-code-haha.desktop"
usage() {
cat <<'EOF'
@ -19,7 +22,7 @@ Usage:
Environment:
SKIP_INSTALL=1 Skip `bun install` in the repo root and desktop app.
SIGN_BUILD=1 Remove the default `--no-sign` flag and allow signed builds.
OPEN_OUTPUT=1 Open the DMG output directory in Finder after a successful build.
OPEN_OUTPUT=1 Open the canonical artifact output directory in Finder after a successful build.
Examples:
./desktop/scripts/build-macos-arm64.sh
@ -43,7 +46,7 @@ if [[ "$(uname -m)" != "arm64" ]]; then
exit 1
fi
for command in bun cargo rustc; do
for command in bun cargo rustc codesign hdiutil; do
if ! command -v "${command}" >/dev/null 2>&1; then
echo "[build-macos-arm64] Missing required command: ${command}" >&2
exit 1
@ -65,7 +68,7 @@ TAURI_ARGS=(
--target
"${TARGET_TRIPLE}"
--bundles
dmg
app,dmg
--ci
)
@ -84,16 +87,113 @@ echo "[build-macos-arm64] Building DMG for ${TARGET_TRIPLE}..."
"${TAURI_ARGS[@]}"
)
LATEST_DMG="$(find "${OUTPUT_DIR}" -maxdepth 1 -type f -name '*.dmg' | sort | tail -n 1 || true)"
TARGETED_DMG_DIR="${TAURI_TARGET_DIR}/${TARGET_TRIPLE}/release/bundle/dmg"
FALLBACK_DMG_DIR="${TAURI_TARGET_DIR}/release/bundle/dmg"
TARGETED_APP_DIR="${TAURI_TARGET_DIR}/${TARGET_TRIPLE}/release/bundle/macos"
FALLBACK_APP_DIR="${TAURI_TARGET_DIR}/release/bundle/macos"
LEGACY_BUNDLE_ROOT="${TAURI_TARGET_DIR}/release/bundle"
mkdir -p "${CANONICAL_OUTPUT_DIR}"
find "${CANONICAL_OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
find_latest_file() {
local search_dir="$1"
local pattern="$2"
if [[ -d "${search_dir}" ]]; then
find "${search_dir}" -maxdepth 1 -type f -name "${pattern}" | sort | tail -n 1
fi
}
find_latest_dir() {
local search_dir="$1"
local pattern="$2"
if [[ -d "${search_dir}" ]]; then
find "${search_dir}" -maxdepth 1 -type d -name "${pattern}" | sort | tail -n 1
fi
}
LATEST_DMG="$(find_latest_file "${TARGETED_DMG_DIR}" '*.dmg')"
if [[ -z "${LATEST_DMG}" ]]; then
LATEST_DMG="$(find_latest_file "${FALLBACK_DMG_DIR}" '*.dmg')"
fi
LATEST_APP="$(find_latest_dir "${TARGETED_APP_DIR}" '*.app')"
if [[ -z "${LATEST_APP}" ]]; then
LATEST_APP="$(find_latest_dir "${FALLBACK_APP_DIR}" '*.app')"
fi
sign_app_bundle() {
local app_bundle="$1"
local sidecar_path="${app_bundle}/Contents/MacOS/claude-sidecar"
if [[ -f "${sidecar_path}" ]]; then
codesign --force --sign - --identifier "${APP_BUNDLE_ID}.sidecar" --timestamp=none "${sidecar_path}"
fi
codesign --force --deep --sign - --identifier "${APP_BUNDLE_ID}" --timestamp=none "${app_bundle}"
}
build_canonical_dmg() {
local app_bundle="$1"
local dmg_output="$2"
local staging_dir
staging_dir="$(mktemp -d "${TMPDIR:-/tmp}/cc-haha-dmg.XXXXXX")"
cp -R "${app_bundle}" "${staging_dir}/"
ln -s /Applications "${staging_dir}/Applications"
hdiutil create \
-volname "Claude Code Haha" \
-srcfolder "${staging_dir}" \
-ov \
-format UDZO \
"${dmg_output}" >/dev/null
rm -rf "${staging_dir}"
}
if [[ -n "${LATEST_DMG}" ]]; then
cp -f "${LATEST_DMG}" "${CANONICAL_OUTPUT_DIR}/"
fi
if [[ -n "${LATEST_APP}" ]]; then
cp -R "${LATEST_APP}" "${CANONICAL_OUTPUT_DIR}/"
sign_app_bundle "${CANONICAL_OUTPUT_DIR}/${APP_BUNDLE_NAME}"
rm -f "${CANONICAL_OUTPUT_DIR}/"*.dmg
build_canonical_dmg \
"${CANONICAL_OUTPUT_DIR}/${APP_BUNDLE_NAME}" \
"${CANONICAL_OUTPUT_DIR}/$(basename "${LATEST_DMG:-Claude Code Haha_0.1.0_aarch64.dmg}")"
fi
cat > "${CANONICAL_OUTPUT_DIR}/BUILD_INFO.txt" <<EOF
Target triple: ${TARGET_TRIPLE}
Canonical output: ${CANONICAL_OUTPUT_DIR}
Source DMG: ${LATEST_DMG:-not found}
Source app: ${LATEST_APP:-not found}
Built at: $(date '+%Y-%m-%d %H:%M:%S %z')
EOF
if [[ -d "${LEGACY_BUNDLE_ROOT}" ]]; then
rm -rf "${LEGACY_BUNDLE_ROOT}"
fi
echo
echo "[build-macos-arm64] Build finished."
if [[ -n "${LATEST_DMG}" ]]; then
echo "[build-macos-arm64] DMG: ${LATEST_DMG}"
echo "[build-macos-arm64] DMG source: ${LATEST_DMG}"
else
echo "[build-macos-arm64] No DMG found in ${OUTPUT_DIR}" >&2
echo "[build-macos-arm64] No DMG found in ${TARGETED_DMG_DIR} or ${FALLBACK_DMG_DIR}" >&2
fi
if [[ "${OPEN_OUTPUT:-0}" == "1" ]]; then
open "${OUTPUT_DIR}"
if [[ -n "${LATEST_APP}" ]]; then
echo "[build-macos-arm64] App source: ${LATEST_APP}"
else
echo "[build-macos-arm64] No .app found in ${TARGETED_APP_DIR} or ${FALLBACK_APP_DIR}" >&2
fi
echo "[build-macos-arm64] Canonical output: ${CANONICAL_OUTPUT_DIR}"
echo "[build-macos-arm64] Removed legacy bundle dir: ${LEGACY_BUNDLE_ROOT}"
if [[ "${OPEN_OUTPUT:-0}" == "1" ]]; then
open "${CANONICAL_OUTPUT_DIR}"
fi

View File

@ -155,6 +155,29 @@ async function compileExecutable({
throw new Error(`[build-sidecars] Failed to compile ${productName}:\n${logs}`)
}
const outputPath = result.outputs[0]?.path
console.log(`[build-sidecars] ${productName} -> ${outputPath ?? outfileBase}`)
const outputPath = result.outputs[0]?.path ?? outfileBase
console.log(`[build-sidecars] ${productName} -> ${outputPath}`)
// macOS Apple System Policy (ASP) requires valid code signatures on all
// executables. Bun-compiled binaries ship with an invalid/empty signature
// that causes "load code signature error 4" and SIGKILL at launch.
// Fix: strip the broken signature, then ad-hoc sign.
if (process.platform === 'darwin') {
console.log(`[build-sidecars] ad-hoc signing ${outputPath} for macOS ...`)
const strip = Bun.spawn(['codesign', '--remove-signature', outputPath], {
stdout: 'inherit',
stderr: 'inherit',
})
await strip.exited
const sign = Bun.spawn(
['codesign', '--sign', '-', '--force', '--timestamp=none', outputPath],
{ stdout: 'inherit', stderr: 'inherit' },
)
const signExit = await sign.exited
if (signExit !== 0) {
throw new Error(`[build-sidecars] ad-hoc codesign failed for ${outputPath} (exit ${signExit})`)
}
console.log(`[build-sidecars] ad-hoc signed ${outputPath}`)
}
}

View File

@ -53,6 +53,22 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
],
"macOS": {
"dmg": {
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"width": 660,
"height": 400
}
}
}
}
}

View File

@ -24,14 +24,14 @@ export class ApiError extends Error {
}
}
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
async function request<T>(method: string, path: string, body?: unknown, options?: { timeout?: number }): Promise<T> {
const url = `${baseUrl}${path}`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 30_000)
const timeout = setTimeout(() => controller.abort(), options?.timeout ?? 30_000)
try {
const res = await fetch(url, {
method,
@ -55,8 +55,8 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
}
export const api = {
get: <T>(path: string) => request<T>('GET', path),
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
get: <T>(path: string, options?: { timeout?: number }) => request<T>('GET', path, undefined, options),
post: <T>(path: string, body?: unknown, options?: { timeout?: number }) => request<T>('POST', path, body, options),
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body),
delete: <T>(path: string) => request<T>('DELETE', path),

View File

@ -233,6 +233,46 @@ export const en = {
'settings.about.author': 'Author',
'settings.about.socialMedia': 'Social Media',
// Settings > Computer Use
'settings.tab.computerUse': 'Computer Use',
'settings.computerUse.title': 'Computer Use',
'settings.computerUse.description': 'Allow Claude to take screenshots, click, type, and control your computer. Requires Python 3 and macOS accessibility permissions.',
'settings.computerUse.notSupported': 'Computer Use is only supported on macOS.',
'settings.computerUse.python': 'Python 3',
'settings.computerUse.pythonNotFound': 'Not installed. Please install Python 3 first.',
'settings.computerUse.pythonFound': 'Installed',
'settings.computerUse.venv': 'Virtual Environment',
'settings.computerUse.venvReady': 'Ready',
'settings.computerUse.venvNotReady': 'Not created',
'settings.computerUse.deps': 'Dependencies',
'settings.computerUse.depsReady': 'Installed',
'settings.computerUse.depsNotReady': 'Not installed',
'settings.computerUse.accessibility': 'Accessibility Permission',
'settings.computerUse.screenRecording': 'Screen Recording Permission',
'settings.computerUse.permGranted': 'Granted',
'settings.computerUse.permDenied': 'Denied — open System Settings > Privacy & Security to grant',
'settings.computerUse.permUnknown': 'Setup environment first to check',
'settings.computerUse.permScreenRecordingUnknownSoft': 'Auto-detection can be unreliable here; if System Settings already shows it enabled, Computer Use can usually proceed',
'settings.computerUse.permScreenRecordingManual': 'Cannot auto-detect — please verify in System Settings',
'settings.computerUse.setupBtn': 'Install Environment',
'settings.computerUse.setupRunning': 'Installing...',
'settings.computerUse.setupSuccess': 'Environment setup complete!',
'settings.computerUse.setupFail': 'Setup failed',
'settings.computerUse.allReady': 'All checks passed. Computer Use is ready.',
'settings.computerUse.recheckBtn': 'Recheck Status',
'settings.computerUse.requirementsLabel': 'Required packages',
'settings.computerUse.appsTitle': 'Authorized Apps',
'settings.computerUse.appsDescription': 'Pre-authorize apps for Computer Use. Authorized apps can be controlled by Claude without runtime prompts.',
'settings.computerUse.appsLoading': 'Loading installed apps...',
'settings.computerUse.appsEmpty': 'No installed apps found. Please setup the environment first.',
'settings.computerUse.appsSearch': 'Search apps...',
'settings.computerUse.appsSaved': 'Saved',
'settings.computerUse.openAccessibility': 'Open Accessibility Settings',
'settings.computerUse.openScreenRecording': 'Open Screen Recording Settings',
'settings.computerUse.permRestartHint': 'After granting, restart the app for it to take effect.',
'settings.computerUse.flagClipboard': 'Clipboard Access',
'settings.computerUse.flagSystemKeys': 'System Key Combos',
// Settings > General
'settings.general.languageTitle': 'Language',
'settings.general.languageDescription': 'Choose the display language for the application.',

View File

@ -235,6 +235,46 @@ export const zh: Record<TranslationKey, string> = {
'settings.about.author': '作者',
'settings.about.socialMedia': '社交媒体',
// Settings > Computer Use
'settings.tab.computerUse': 'Computer Use',
'settings.computerUse.title': 'Computer Use',
'settings.computerUse.description': '允许 Claude 截屏、点击、打字并控制你的电脑。需要 Python 3 和 macOS 辅助功能权限。',
'settings.computerUse.notSupported': 'Computer Use 仅支持 macOS。',
'settings.computerUse.python': 'Python 3',
'settings.computerUse.pythonNotFound': '未安装,请先安装 Python 3。',
'settings.computerUse.pythonFound': '已安装',
'settings.computerUse.venv': '虚拟环境',
'settings.computerUse.venvReady': '已就绪',
'settings.computerUse.venvNotReady': '未创建',
'settings.computerUse.deps': '依赖包',
'settings.computerUse.depsReady': '已安装',
'settings.computerUse.depsNotReady': '未安装',
'settings.computerUse.accessibility': '辅助功能权限',
'settings.computerUse.screenRecording': '屏幕录制权限',
'settings.computerUse.permGranted': '已授权',
'settings.computerUse.permDenied': '未授权 — 请前往「系统设置 > 隐私与安全性」授权',
'settings.computerUse.permUnknown': '请先安装环境后检测',
'settings.computerUse.permScreenRecordingUnknownSoft': '自动检测结果不稳定;如果系统设置里已授权,通常可以直接使用',
'settings.computerUse.permScreenRecordingManual': '无法自动检测 — 请在系统设置中手动确认已授权',
'settings.computerUse.setupBtn': '安装环境',
'settings.computerUse.setupRunning': '安装中...',
'settings.computerUse.setupSuccess': '环境安装完成!',
'settings.computerUse.setupFail': '安装失败',
'settings.computerUse.allReady': '所有检查通过Computer Use 已就绪。',
'settings.computerUse.recheckBtn': '重新检测',
'settings.computerUse.requirementsLabel': '需要的 Python 包',
'settings.computerUse.appsTitle': '已授权应用',
'settings.computerUse.appsDescription': '预先授权 AppClaude 将可以直接控制这些 App无需运行时弹窗确认。',
'settings.computerUse.appsLoading': '加载已安装的 App...',
'settings.computerUse.appsEmpty': '未找到已安装的 App请先安装环境。',
'settings.computerUse.appsSearch': '搜索 App...',
'settings.computerUse.appsSaved': '已保存',
'settings.computerUse.openAccessibility': '打开辅助功能设置',
'settings.computerUse.openScreenRecording': '打开屏幕录制设置',
'settings.computerUse.permRestartHint': '授权后需重启 App 才能生效。',
'settings.computerUse.flagClipboard': '剪贴板访问',
'settings.computerUse.flagSystemKeys': '系统快捷键',
// Settings > General
'settings.general.languageTitle': '语言',
'settings.general.languageDescription': '选择应用程序的显示语言。',

View File

@ -18,6 +18,7 @@ import { MarkdownRenderer } from '../components/markdown/MarkdownRenderer'
import { useSkillStore } from '../stores/skillStore'
import { SkillList } from '../components/skills/SkillList'
import { SkillDetail } from '../components/skills/SkillDetail'
import { ComputerUseSettings } from './ComputerUseSettings'
import { useUIStore, type SettingsTab } from '../stores/uiStore'
export function Settings() {
@ -43,6 +44,7 @@ export function Settings() {
<TabButton icon="chat" label={t('settings.tab.adapters')} active={activeTab === 'adapters'} onClick={() => setActiveTab('adapters')} />
<TabButton icon="smart_toy" label={t('settings.tab.agents')} active={activeTab === 'agents'} onClick={() => setActiveTab('agents')} />
<TabButton icon="auto_awesome" label={t('settings.tab.skills')} active={activeTab === 'skills'} onClick={() => setActiveTab('skills')} />
<TabButton icon="mouse" label={t('settings.tab.computerUse')} active={activeTab === 'computerUse'} onClick={() => setActiveTab('computerUse')} />
</div>
<div className="border-t border-[var(--color-border)]/40 pt-1">
<TabButton icon="info" label={t('settings.tab.about')} active={activeTab === 'about'} onClick={() => setActiveTab('about')} />
@ -57,6 +59,7 @@ export function Settings() {
{activeTab === 'adapters' && <AdapterSettings />}
{activeTab === 'agents' && <AgentsSettings />}
{activeTab === 'skills' && <SkillSettings />}
{activeTab === 'computerUse' && <ComputerUseSettings />}
{activeTab === 'about' && <AboutSettings />}
</div>
</div>

View File

@ -7,7 +7,7 @@ export type Toast = {
duration?: number
}
export type SettingsTab = 'providers' | 'permissions' | 'general' | 'adapters' | 'agents' | 'skills' | 'about'
export type SettingsTab = 'providers' | 'permissions' | 'general' | 'adapters' | 'agents' | 'skills' | 'computerUse' | 'about'
type ActiveView = 'code' | 'scheduled' | 'terminal' | 'history' | 'settings'