diff --git a/.github/workflows/build-mac.yml b/.github/workflows/build-mac.yml index 8652ac2..3307bc7 100644 --- a/.github/workflows/build-mac.yml +++ b/.github/workflows/build-mac.yml @@ -103,8 +103,8 @@ jobs: id: release_meta run: | VERSION=${GITHUB_REF#refs/tags/v} - node scripts/extract-release-notes.mjs $VERSION > /tmp/release-notes.md - cat /tmp/release-notes.md + node scripts/extract-release-notes.mjs $VERSION > "$RUNNER_TEMP/release-notes.md" + cat "$RUNNER_TEMP/release-notes.md" # 主版本号为 0 或包含 - 的版本视为测试版 if [[ "$VERSION" =~ ^0\. ]] || [[ "$VERSION" =~ - ]]; then echo "is_prerelease=true" >> $GITHUB_OUTPUT @@ -119,7 +119,7 @@ jobs: files: | dist/*.dmg dist/latest-mac.yml - body_path: /tmp/release-notes.md + body_path: ${{ runner.temp }}/release-notes.md draft: false prerelease: ${{ steps.release_meta.outputs.is_prerelease }} env: diff --git a/.github/workflows/build-win.yml b/.github/workflows/build-win.yml index 32fa4b3..3bfdb89 100644 --- a/.github/workflows/build-win.yml +++ b/.github/workflows/build-win.yml @@ -85,8 +85,8 @@ jobs: shell: bash run: | VERSION=${GITHUB_REF#refs/tags/v} - node scripts/extract-release-notes.mjs $VERSION > /tmp/release-notes.md - cat /tmp/release-notes.md + node scripts/extract-release-notes.mjs $VERSION > "$RUNNER_TEMP/release-notes.md" + cat "$RUNNER_TEMP/release-notes.md" # 主版本号为 0 或包含 - 的版本视为测试版 if [[ "$VERSION" =~ ^0\. ]] || [[ "$VERSION" =~ - ]]; then echo "is_prerelease=true" >> $GITHUB_OUTPUT @@ -101,7 +101,7 @@ jobs: files: | dist/*.exe dist/latest.yml - body_path: /tmp/release-notes.md + body_path: ${{ runner.temp }}/release-notes.md draft: false prerelease: ${{ steps.release_meta.outputs.is_prerelease }} env: diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index 424a5a5..9b19e9b 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -16,7 +16,7 @@ */ import type { IpcMain, BrowserWindow } from 'electron' -import { app } from 'electron' +import { app, shell } from 'electron' import { autoUpdater } from 'electron-updater' import { logger } from './lib/logger' @@ -122,4 +122,15 @@ export function registerAppUpdaterHandlers(ipcMain: IpcMain): void { ipcMain.handle('app:install-update', () => { autoUpdater.quitAndInstall() }) + + ipcMain.handle('app:paths', () => ({ + appPath: app.isPackaged ? process.resourcesPath : app.getAppPath(), + userData: app.getPath('userData'), + logs: app.getPath('logs'), + })) + + ipcMain.handle('app:open-path', async (_event, targetPath: string) => { + const err = await shell.openPath(targetPath) + if (err) logger.warn(`[AppPaths] shell.openPath failed: ${err}`) + }) } diff --git a/src/preload/index.ts b/src/preload/index.ts index cc651eb..26ca7e1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -224,6 +224,10 @@ const ipcApi = { appUpdateCallbacks.add(callback) return () => appUpdateCallbacks.delete(callback) }, + appPaths: (): Promise<{ appPath: string; userData: string; logs: string }> => + ipcRenderer.invoke('app:paths'), + appOpenPath: (targetPath: string): Promise => + ipcRenderer.invoke('app:open-path', targetPath), // ── Window controls (frameless) ─────────────────────────────────────────── windowMinimize: () => ipcRenderer.send('window:minimize'), diff --git a/src/renderer/src/components/settings/settings-dialog.tsx b/src/renderer/src/components/settings/settings-dialog.tsx index d000c51..93a5ebc 100644 --- a/src/renderer/src/components/settings/settings-dialog.tsx +++ b/src/renderer/src/components/settings/settings-dialog.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react" -import { ArrowLeft, Brain, Check, Languages, Info, RefreshCw, Download, CheckCircle2, Loader2 } from "lucide-react" +import { ArrowLeft, Brain, Check, Languages, Info, RefreshCw, Download, CheckCircle2, Loader2, FolderOpen, Copy, CheckCheck } from "lucide-react" import { Dialog, DialogContent, @@ -174,11 +174,20 @@ type UpdateState = function AboutPanel() { const [updateState, setUpdateState] = useState({ status: 'idle' }) const [appVersion, setAppVersion] = useState('…') + const [paths, setPaths] = useState<{ appPath: string; userData: string; logs: string } | null>(null) + const [copied, setCopied] = useState(null) useEffect(() => { window.ipc.appVersion().then(setAppVersion) + window.ipc.appPaths().then(setPaths) }, []) + const handleCopy = (key: string, value: string) => { + navigator.clipboard.writeText(value) + setCopied(key) + setTimeout(() => setCopied(null), 2000) + } + const handleCheckUpdate = async () => { setUpdateState({ status: 'checking' }) const unsubscribe = window.ipc.onAppUpdateStatus((s) => { @@ -211,7 +220,47 @@ function AboutPanel() { - {/* Update section */} + {/* File paths */} +
+

文件目录

+
+ {[ + { key: 'appPath', label: '应用文件夹', value: paths?.appPath }, + { key: 'userData', label: '数据文件夹', value: paths?.userData }, + { key: 'logs', label: '日志文件夹', value: paths?.logs }, + ].map(({ key, label, value }) => ( +
+
+ {label} +
+ + +
+
+

+ {value ?? 加载中…} +

+
+ ))} +
+

软件更新