Merge pull request #514 from H3CoF6/dev

linux版本增加wayland说明
优化一点点页面显示
---

我发现appimage可以用,之前觉得FUSE导致难以操控微信进程的
重新支持appimage,放弃对deb的打包(等appimage的-1006报错修好后彻底放弃)
This commit is contained in:
H3CoF6
2026-03-21 03:45:12 +08:00
committed by GitHub
8 changed files with 202 additions and 41 deletions

View File

@@ -151,6 +151,7 @@ jobs:
MAC_ASSET="$(pick_asset "\\.dmg$")" MAC_ASSET="$(pick_asset "\\.dmg$")"
LINUX_DEB_ASSET="$(pick_asset "\\.deb$")" LINUX_DEB_ASSET="$(pick_asset "\\.deb$")"
LINUX_TAR_ASSET="$(pick_asset "\\.tar\\.gz$")" LINUX_TAR_ASSET="$(pick_asset "\\.tar\\.gz$")"
LINUX_APPIMAGE_ASSET="$(pick_asset "\\.AppImage$")"
build_link() { build_link() {
local name="$1" local name="$1"
@@ -163,6 +164,7 @@ jobs:
MAC_URL="$(build_link "$MAC_ASSET")" MAC_URL="$(build_link "$MAC_ASSET")"
LINUX_DEB_URL="$(build_link "$LINUX_DEB_ASSET")" LINUX_DEB_URL="$(build_link "$LINUX_DEB_ASSET")"
LINUX_TAR_URL="$(build_link "$LINUX_TAR_ASSET")" LINUX_TAR_URL="$(build_link "$LINUX_TAR_ASSET")"
LINUX_APPIMAGE_URL="$(build_link "$LINUX_APPIMAGE_ASSET")"
cat > release_notes.md <<EOF cat > release_notes.md <<EOF
## 更新日志 ## 更新日志
@@ -174,8 +176,9 @@ jobs:
## 下载 ## 下载
- Windows Win10+: ${WINDOWS_URL:-$RELEASE_PAGE} - Windows Win10+: ${WINDOWS_URL:-$RELEASE_PAGE}
- macOSM系列芯片: ${MAC_URL:-$RELEASE_PAGE} - macOSM系列芯片: ${MAC_URL:-$RELEASE_PAGE}
- Linux (.deb): ${LINUX_DEB_URL:-$RELEASE_PAGE} - Linux (.deb) (即将废弃): ${LINUX_DEB_URL:-$RELEASE_PAGE}
- Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE} - Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE}
- linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE}
> 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE > 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE
EOF EOF

View File

@@ -1043,6 +1043,13 @@ function registerIpcHandlers() {
return app.getVersion() return app.getVersion()
}) })
ipcMain.handle('app:checkWayland', async () => {
if (process.platform !== 'linux') return false;
const sessionType = process.env.XDG_SESSION_TYPE?.toLowerCase();
return Boolean(process.env.WAYLAND_DISPLAY || sessionType === 'wayland');
})
ipcMain.handle('log:getPath', async () => { ipcMain.handle('log:getPath', async () => {
return join(app.getPath('userData'), 'logs', 'wcdb.log') return join(app.getPath('userData'), 'logs', 'wcdb.log')
}) })

View File

@@ -63,7 +63,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => { onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => {
ipcRenderer.on('app:updateAvailable', (_, info) => callback(info)) ipcRenderer.on('app:updateAvailable', (_, info) => callback(info))
return () => ipcRenderer.removeAllListeners('app:updateAvailable') return () => ipcRenderer.removeAllListeners('app:updateAvailable')
} },
checkWayland: () => ipcRenderer.invoke('app:checkWayland'),
}, },
// 日志 // 日志

View File

@@ -1,10 +1,9 @@
import { app } from 'electron' import { app } from 'electron'
import { join } from 'path' import { join } from 'path'
import { existsSync, readdirSync, statSync, readFileSync } from 'fs' import { existsSync, readdirSync, statSync, readFileSync } from 'fs'
import { execFile, exec } from 'child_process' import { execFile, exec, spawn } from 'child_process'
import { promisify } from 'util' import { promisify } from 'util'
import { createRequire } from 'module'; import { createRequire } from 'module';
import { spawn } from 'child_process'
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const execFileAsync = promisify(execFile) const execFileAsync = promisify(execFile)
@@ -46,8 +45,32 @@ export class KeyServiceLinux {
onStatus?: (message: string, level: number) => void onStatus?: (message: string, level: number) => void
): Promise<DbKeyResult> { ): Promise<DbKeyResult> {
try { try {
// 1. 构造一个包含常用系统命令路径的环境变量,防止打包后找不到命令
const envWithPath = {
...process.env,
PATH: `${process.env.PATH || ''}:/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin`
};
onStatus?.('正在尝试结束当前微信进程...', 0) onStatus?.('正在尝试结束当前微信进程...', 0)
await execAsync('killall -9 wechat wechat-bin xwechat').catch(() => {}) console.log('[Debug] 开始执行进程清理逻辑...');
try {
const { stdout, stderr } = await execAsync('killall -9 wechat wechat-bin xwechat', { env: envWithPath });
console.log(`[Debug] killall 成功退出. stdout: ${stdout}, stderr: ${stderr}`);
} catch (err: any) {
// 命令如果没找到进程通常会返回 code 1这也是正常的但我们需要记录下来
console.log(`[Debug] killall 报错或未找到进程: ${err.message}`);
// Fallback: 尝试使用 pkill 兜底
try {
console.log('[Debug] 尝试使用备用命令 pkill...');
await execAsync('pkill -9 -x "wechat|wechat-bin|xwechat"', { env: envWithPath });
console.log('[Debug] pkill 执行完成');
} catch (e: any) {
console.log(`[Debug] pkill 报错或未找到进程: ${e.message}`);
}
}
// 稍微等待进程完全退出 // 稍微等待进程完全退出
await new Promise(r => setTimeout(r, 1000)) await new Promise(r => setTimeout(r, 1000))
@@ -76,11 +99,14 @@ export class KeyServiceLinux {
env: cleanEnv env: cleanEnv
}); });
child.on('error', () => {}); child.on('error', (err) => {
console.log(`[Debug] 拉起 ${binName} 失败:`, err.message);
});
child.unref(); child.unref();
} catch (e) { console.log(`[Debug] 尝试拉起 ${binName} 完毕`);
} catch (e: any) {
console.log(`[Debug] 尝试拉起 ${binName} 发生异常:`, e.message);
} }
} }
@@ -88,16 +114,35 @@ export class KeyServiceLinux {
let pid = 0 let pid = 0
for (let i = 0; i < 15; i++) { // 最多等 15 秒 for (let i = 0; i < 15; i++) { // 最多等 15 秒
await new Promise(r => setTimeout(r, 1000)) await new Promise(r => setTimeout(r, 1000))
const { stdout } = await execAsync('pidof wechat wechat-bin xwechat').catch(() => ({ stdout: '' }))
const pids = stdout.trim().split(/\s+/).filter(p => p) try {
if (pids.length > 0) { const { stdout } = await execAsync('pidof wechat wechat-bin xwechat', { env: envWithPath });
pid = parseInt(pids[0], 10) const pids = stdout.trim().split(/\s+/).filter(p => p);
break if (pids.length > 0) {
pid = parseInt(pids[0], 10);
console.log(`[Debug] 第 ${i + 1} 秒,通过 pidof 成功获取 PID: ${pid}`);
break;
}
} catch (err: any) {
console.log(`[Debug] 第 ${i + 1}pidof 失败: ${err.message.split('\n')[0]}`);
// Fallback: 使用 pgrep 兜底
try {
const { stdout: pgrepOut } = await execAsync('pgrep -x "wechat|wechat-bin|xwechat"', { env: envWithPath });
const pids = pgrepOut.trim().split(/\s+/).filter(p => p);
if (pids.length > 0) {
pid = parseInt(pids[0], 10);
console.log(`[Debug] 第 ${i + 1} 秒,通过 pgrep 成功获取 PID: ${pid}`);
break;
}
} catch (e: any) {
console.log(`[Debug] 第 ${i + 1}pgrep 也失败: ${e.message.split('\n')[0]}`);
}
} }
} }
if (!pid) { if (!pid) {
const err = '未能自动启动微信,手动启动并登录。' const err = '未能自动启动微信,或获取PID失败请查看控制台日志或手动启动并登录。'
onStatus?.(err, 2) onStatus?.(err, 2)
return { success: false, error: err } return { success: false, error: err }
} }
@@ -108,6 +153,7 @@ export class KeyServiceLinux {
return await this.getDbKey(pid, onStatus) return await this.getDbKey(pid, onStatus)
} catch (err: any) { } catch (err: any) {
console.error('[Debug] 自动获取流程彻底崩溃:', err);
const errMsg = '自动获取微信 PID 失败: ' + err.message const errMsg = '自动获取微信 PID 失败: ' + err.message
onStatus?.(errMsg, 2) onStatus?.(errMsg, 2)
return { success: false, error: errMsg } return { success: false, error: errMsg }

View File

@@ -95,6 +95,7 @@
"linux": { "linux": {
"icon": "public/icon.png", "icon": "public/icon.png",
"target": [ "target": [
"appimage",
"deb", "deb",
"tar.gz" "tar.gz"
], ],

View File

@@ -104,6 +104,44 @@ function App() {
// 数据收集同意状态 // 数据收集同意状态
const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false) const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false)
const [showWaylandWarning, setShowWaylandWarning] = useState(false)
useEffect(() => {
const checkWaylandStatus = async () => {
try {
// 防止在非客户端环境报错,先检查 API 是否存在
if (!window.electronAPI?.app?.checkWayland) return
// 通过 configService 检查是否已经弹过窗
const hasWarned = await window.electronAPI.config.get('waylandWarningShown')
if (!hasWarned) {
const isWayland = await window.electronAPI.app.checkWayland()
if (isWayland) {
setShowWaylandWarning(true)
}
}
} catch (e) {
console.error('检查 Wayland 状态失败:', e)
}
}
// 只有在协议同意之后并且已经进入主应用流程才检查
if (!isAgreementWindow && !isOnboardingWindow && !agreementLoading) {
checkWaylandStatus()
}
}, [isAgreementWindow, isOnboardingWindow, agreementLoading])
const handleDismissWaylandWarning = async () => {
try {
// 记录到本地配置中,下次不再提示
await window.electronAPI.config.set('waylandWarningShown', true)
} catch (e) {
console.error('保存 Wayland 提示状态失败:', e)
}
setShowWaylandWarning(false)
}
useEffect(() => { useEffect(() => {
if (location.pathname !== '/settings') { if (location.pathname !== '/settings') {
settingsBackgroundRef.current = location settingsBackgroundRef.current = location
@@ -432,6 +470,8 @@ function App() {
checkLock() checkLock()
}, [isAgreementWindow, isOnboardingWindow, isVideoPlayerWindow]) }, [isAgreementWindow, isOnboardingWindow, isVideoPlayerWindow])
// 独立协议窗口 // 独立协议窗口
if (isAgreementWindow) { if (isAgreementWindow) {
return <AgreementPage /> return <AgreementPage />
@@ -614,6 +654,33 @@ function App() {
</div> </div>
)} )}
{showWaylandWarning && (
<div className="agreement-overlay">
<div className="agreement-modal">
<div className="agreement-header">
<Shield size={32} />
<h2> (Wayland)</h2>
</div>
<div className="agreement-content">
<div className="agreement-text">
<p>使 <strong>Wayland</strong> </p>
<p> Wayland <strong></strong></p>
<p></p>
<br />
<p>使</p>
<p>1. <strong>X11 (Xorg)</strong> </p>
<p>2. (WM/DE) </p>
</div>
</div>
<div className="agreement-footer">
<div className="agreement-actions">
<button className="btn btn-primary" onClick={handleDismissWaylandWarning}></button>
</div>
</div>
</div>
</div>
)}
{/* 更新提示对话框 */} {/* 更新提示对话框 */}
<UpdateDialog <UpdateDialog
open={showUpdateDialog} open={showUpdateDialog}

View File

@@ -175,6 +175,21 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const isClearingCache = isClearingAnalyticsCache || isClearingImageCache || isClearingAllCache const isClearingCache = isClearingAnalyticsCache || isClearingImageCache || isClearingAllCache
const [isWayland, setIsWayland] = useState(false)
useEffect(() => {
const checkWaylandStatus = async () => {
if (window.electronAPI?.app?.checkWayland) {
try {
const wayland = await window.electronAPI.app.checkWayland()
setIsWayland(wayland)
} catch (e) {
console.error('检查 Wayland 状态失败:', e)
}
}
}
checkWaylandStatus()
}, [])
// 检查 Hello 可用性 // 检查 Hello 可用性
useEffect(() => { useEffect(() => {
if (window.PublicKeyCredential) { if (window.PublicKeyCredential) {
@@ -1169,6 +1184,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="form-group"> <div className="form-group">
<label></label> <label></label>
<span className="form-hint"></span> <span className="form-hint"></span>
{isWayland && (
<span className="form-hint" style={{ color: '#ff4d4f', marginTop: '4px', display: 'block' }}>
Wayland
</span>
)}
<div className="custom-select"> <div className="custom-select">
<div <div
className={`custom-select-trigger ${positionDropdownOpen ? 'open' : ''}`} className={`custom-select-trigger ${positionDropdownOpen ? 'open' : ''}`}
@@ -1652,34 +1672,49 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
) )
const renderCacheTab = () => ( const renderCacheTab = () => (
<div className="tab-content"> <div className="tab-content">
<p className="section-desc"></p> <p className="section-desc"></p>
<div className="form-group"> <div className="form-group">
<label> <span className="optional">()</span></label> <label> <span className="optional">()</span></label>
<span className="form-hint">使</span> <span className="form-hint">使</span>
<input <input
type="text" type="text"
placeholder="留空使用默认目录" placeholder="留空使用默认目录"
value={cachePath} value={cachePath}
onChange={(e) => { onChange={(e) => {
const value = e.target.value const value = e.target.value
setCachePath(value) setCachePath(value)
scheduleConfigSave('cachePath', () => configService.setCachePath(value)) scheduleConfigSave('cachePath', () => configService.setCachePath(value))
}} }}
/> />
<div className="btn-row">
<button className="btn btn-secondary" onClick={handleSelectCachePath}><FolderOpen size={16} /> </button> <div style={{ marginTop: '8px', fontSize: '13px', color: 'var(--text-secondary)' }}>
<button
className="btn btn-secondary" <code style={{
onClick={async () => { background: 'var(--bg-secondary)',
setCachePath('') padding: '3px 6px',
await configService.setCachePath('') borderRadius: '4px',
}} userSelect: 'all',
> wordBreak: 'break-all',
<RotateCcw size={16} /> marginLeft: '4px'
</button> }}>
{cachePath || (isMac ? '~/Documents/WeFlow' : isLinux ? '~/Documents/WeFlow' : '系统 文档\\WeFlow 目录')}
</code>
</div>
<div className="btn-row" style={{ marginTop: '12px' }}>
<button className="btn btn-secondary" onClick={handleSelectCachePath}><FolderOpen size={16} /> </button>
<button
className="btn btn-secondary"
onClick={async () => {
setCachePath('')
await configService.setCachePath('')
}}
>
<RotateCcw size={16} />
</button>
</div>
</div> </div>
</div>
<div className="btn-row"> <div className="btn-row">
<button className="btn btn-secondary" onClick={handleClearAnalyticsCache} disabled={isClearingCache}> <button className="btn btn-secondary" onClick={handleClearAnalyticsCache} disabled={isClearingCache}>

View File

@@ -61,6 +61,7 @@ export interface ElectronAPI {
ignoreUpdate: (version: string) => Promise<{ success: boolean }> ignoreUpdate: (version: string) => Promise<{ success: boolean }>
onDownloadProgress: (callback: (progress: number) => void) => () => void onDownloadProgress: (callback: (progress: number) => void) => () => void
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => () => void onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => () => void
checkWayland: () => Promise<boolean>
} }
notification: { notification: {
show: (data: { title: string; content: string; avatarUrl?: string; sessionId: string }) => Promise<{ success?: boolean; error?: string } | void> show: (data: { title: string; content: string; avatarUrl?: string; sessionId: string }) => Promise<{ success?: boolean; error?: string } | void>