This commit is contained in:
cc
2026-03-21 14:50:43 +08:00
10 changed files with 292 additions and 47 deletions

View File

@@ -157,6 +157,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"
@@ -169,6 +170,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
## 更新日志 ## 更新日志
@@ -180,8 +182,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,7 +1,7 @@
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';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
@@ -45,33 +45,104 @@ 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))
onStatus?.('正在尝试拉起微信...', 0) onStatus?.('正在尝试拉起微信...', 0)
const startCmds = [
'nohup wechat >/dev/null 2>&1 &', const cleanEnv = { ...process.env };
'nohup wechat-bin >/dev/null 2>&1 &', delete cleanEnv.ELECTRON_RUN_AS_NODE;
'nohup xwechat >/dev/null 2>&1 &' delete cleanEnv.ELECTRON_NO_ATTACH_CONSOLE;
delete cleanEnv.APPDIR;
delete cleanEnv.APPIMAGE;
const wechatBins = [
'wechat',
'wechat-bin',
'xwechat',
'/opt/wechat/wechat',
'/usr/bin/wechat',
'/opt/apps/com.tencent.wechat/files/wechat'
] ]
for (const cmd of startCmds) execAsync(cmd).catch(() => {})
for (const binName of wechatBins) {
try {
const child = spawn(binName, [], {
detached: true,
stdio: 'ignore',
env: cleanEnv
});
child.on('error', (err) => {
console.log(`[Debug] 拉起 ${binName} 失败:`, err.message);
});
child.unref();
console.log(`[Debug] 尝试拉起 ${binName} 完毕`);
} catch (e: any) {
console.log(`[Debug] 尝试拉起 ${binName} 发生异常:`, e.message);
}
}
onStatus?.('等待微信进程出现...', 0) onStatus?.('等待微信进程出现...', 0)
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 {
const { stdout } = await execAsync('pidof wechat wechat-bin xwechat', { env: envWithPath });
const pids = stdout.trim().split(/\s+/).filter(p => p);
if (pids.length > 0) { if (pids.length > 0) {
pid = parseInt(pids[0], 10) pid = parseInt(pids[0], 10);
break 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 }
} }
@@ -82,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

@@ -273,8 +273,20 @@ export class VoiceTranscribeService {
}) })
worker.on('error', (err: Error) => resolve({ success: false, error: String(err) })) worker.on('error', (err: Error) => resolve({ success: false, error: String(err) }))
worker.on('exit', (code: number) => { worker.on('exit', (code: number | null, signal: string | null) => {
if (code !== 0) resolve({ success: false, error: `Worker exited with code ${code}` }) if (code === null || signal === 'SIGSEGV') {
console.error(`[VoiceTranscribe] Worker 异常崩溃,信号: ${signal}。可能是由于底层 C++ 运行库在当前系统上发生段错误。`);
resolve({
success: false,
error: 'SEGFAULT_ERROR'
});
return;
}
if (code !== 0) {
resolve({ success: false, error: `Worker exited with code ${code}` });
}
}) })
} catch (error) { } catch (error) {

View File

@@ -96,6 +96,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

@@ -7611,6 +7611,12 @@ function MessageBubble({
const [voiceWaveform, setVoiceWaveform] = useState<number[]>([]) const [voiceWaveform, setVoiceWaveform] = useState<number[]>([])
const voiceAutoDecryptTriggered = useRef(false) const voiceAutoDecryptTriggered = useRef(false)
const [systemAlert, setSystemAlert] = useState<{
title: string;
message: React.ReactNode;
} | null>(null)
// 转账消息双方名称 // 转账消息双方名称
const [transferPayerName, setTransferPayerName] = useState<string | undefined>(undefined) const [transferPayerName, setTransferPayerName] = useState<string | undefined>(undefined)
const [transferReceiverName, setTransferReceiverName] = useState<string | undefined>(undefined) const [transferReceiverName, setTransferReceiverName] = useState<string | undefined>(undefined)
@@ -8300,6 +8306,21 @@ function MessageBubble({
voiceTranscriptCache.set(voiceTranscriptCacheKey, transcriptText) voiceTranscriptCache.set(voiceTranscriptCacheKey, transcriptText)
setVoiceTranscript(transcriptText) setVoiceTranscript(transcriptText)
} else { } else {
if (result.error === 'SEGFAULT_ERROR') {
console.warn('[ChatPage] 捕获到语音引擎底层段错误');
setSystemAlert({
title: '引擎崩溃提示',
message: (
<>
(Segmentation Fault)<br /><br />
使 Linux <code>sherpa-onnx</code> ( glibc )
</>
)
});
}
setVoiceTranscriptError(true) setVoiceTranscriptError(true)
voiceTranscriptRequestedRef.current = false voiceTranscriptRequestedRef.current = false
} }
@@ -9699,6 +9720,31 @@ function MessageBubble({
{isSelected && <Check size={14} strokeWidth={3} />} {isSelected && <Check size={14} strokeWidth={3} />}
</div> </div>
)} )}
{systemAlert && createPortal(
<div className="modal-overlay" onClick={() => setSystemAlert(null)} style={{ zIndex: 99999 }}>
<div className="delete-confirm-card" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '400px' }}>
<div className="confirm-icon">
<AlertCircle size={32} color="var(--danger)" />
</div>
<div className="confirm-content">
<h3>{systemAlert.title}</h3>
<p style={{ marginTop: '12px', lineHeight: '1.6', fontSize: '14px', color: 'var(--text-secondary)' }}>
{systemAlert.message}
</p>
</div>
<div className="confirm-actions" style={{ justifyContent: 'center', marginTop: '24px' }}>
<button
className="btn-primary"
onClick={() => setSystemAlert(null)}
style={{ padding: '8px 32px' }}
>
</button>
</div>
</div>
</div>,
document.body
)}
</div> </div>
</> </>
) )

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' : ''}`}
@@ -1667,7 +1687,22 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
scheduleConfigSave('cachePath', () => configService.setCachePath(value)) scheduleConfigSave('cachePath', () => configService.setCachePath(value))
}} }}
/> />
<div className="btn-row">
<div style={{ marginTop: '8px', fontSize: '13px', color: 'var(--text-secondary)' }}>
<code style={{
background: 'var(--bg-secondary)',
padding: '3px 6px',
borderRadius: '4px',
userSelect: 'all',
wordBreak: 'break-all',
marginLeft: '4px'
}}>
{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={handleSelectCachePath}><FolderOpen size={16} /> </button>
<button <button
className="btn btn-secondary" className="btn btn-secondary"

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>