This commit is contained in:
hicccc77
2026-03-14 18:58:55 +08:00
11 changed files with 114 additions and 51 deletions

View File

@@ -8,20 +8,23 @@ on:
permissions: permissions:
contents: write contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs: jobs:
release-mac-arm64: release-mac-arm64:
runs-on: macos-14 runs-on: macos-14
steps: steps:
- name: Check out git repository - name: Check out git repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v5
with: with:
node-version: 22.12 node-version: 24
cache: "npm" cache: "npm"
- name: Install Dependencies - name: Install Dependencies
@@ -66,14 +69,14 @@ jobs:
steps: steps:
- name: Check out git repository - name: Check out git repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v5
with: with:
node-version: 22.12 node-version: 24
cache: 'npm' cache: 'npm'
- name: Install Dependencies - name: Install Dependencies

View File

@@ -988,6 +988,17 @@ function registerIpcHandlers() {
} }
}) })
ipcMain.handle('log:clear', async () => {
try {
const logPath = join(app.getPath('userData'), 'logs', 'wcdb.log')
await mkdir(dirname(logPath), { recursive: true })
await writeFile(logPath, '', 'utf8')
return { success: true }
} catch (e) {
return { success: false, error: String(e) }
}
})
ipcMain.handle('diagnostics:getExportCardLogs', async (_, options?: { limit?: number }) => { ipcMain.handle('diagnostics:getExportCardLogs', async (_, options?: { limit?: number }) => {
return exportCardDiagnosticsService.snapshot(options?.limit) return exportCardDiagnosticsService.snapshot(options?.limit)
}) })

View File

@@ -70,6 +70,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
log: { log: {
getPath: () => ipcRenderer.invoke('log:getPath'), getPath: () => ipcRenderer.invoke('log:getPath'),
read: () => ipcRenderer.invoke('log:read'), read: () => ipcRenderer.invoke('log:read'),
clear: () => ipcRenderer.invoke('log:clear'),
debug: (data: any) => ipcRenderer.send('log:debug', data) debug: (data: any) => ipcRenderer.send('log:debug', data)
}, },

View File

@@ -359,16 +359,15 @@ class ChatService {
// 这种方式更高效,且不占用 JS 线程,并能直接监听 session/message 目录变更 // 这种方式更高效,且不占用 JS 线程,并能直接监听 session/message 目录变更
wcdbService.setMonitor((type, json) => { wcdbService.setMonitor((type, json) => {
this.handleSessionStatsMonitorChange(type, json) this.handleSessionStatsMonitorChange(type, json)
const windows = BrowserWindow.getAllWindows()
// 广播给所有渲染进程窗口 // 广播给所有渲染进程窗口
BrowserWindow.getAllWindows().forEach((win) => { windows.forEach((win) => {
if (!win.isDestroyed()) { if (!win.isDestroyed()) {
win.webContents.send('wcdb-change', { type, json }) win.webContents.send('wcdb-change', { type, json })
} }
}) })
}) })
} }
})
}
/** /**
* 预热 media 数据库列表缓存(后台异步执行) * 预热 media 数据库列表缓存(后台异步执行)

View File

@@ -173,7 +173,6 @@ export class WcdbCore {
} }
} catch {} } catch {}
} }
this.connectMonitorPipe(pipePath) this.connectMonitorPipe(pipePath)
return true return true
} catch (e) { } catch (e) {
@@ -194,8 +193,14 @@ export class WcdbCore {
let buffer = '' let buffer = ''
this.monitorPipeClient.on('data', (data: Buffer) => { this.monitorPipeClient.on('data', (data: Buffer) => {
buffer += data.toString('utf8') const rawChunk = data.toString('utf8')
const lines = buffer.split('\n') // macOS 侧可能使用 '\0' 或无换行分隔,统一归一化并兜底拆包
const normalizedChunk = rawChunk
.replace(/\u0000/g, '\n')
.replace(/}\s*{/g, '}\n{')
buffer += normalizedChunk
const lines = buffer.split(/\r?\n/)
buffer = lines.pop() || '' buffer = lines.pop() || ''
for (const line of lines) { for (const line of lines) {
if (line.trim()) { if (line.trim()) {
@@ -207,9 +212,23 @@ export class WcdbCore {
} }
} }
} }
// 兜底:如果没有分隔符但已形成完整 JSON则直接上报
const tail = buffer.trim()
if (tail.startsWith('{') && tail.endsWith('}')) {
try {
const parsed = JSON.parse(tail)
this.monitorCallback?.(parsed.action || 'update', tail)
buffer = ''
} catch {
// 不可解析则继续等待下一块数据
}
}
}) })
this.monitorPipeClient.on('error', () => {}) this.monitorPipeClient.on('error', () => {
// 保持静默,与现有错误处理策略一致
})
this.monitorPipeClient.on('close', () => { this.monitorPipeClient.on('close', () => {
this.monitorPipeClient = null this.monitorPipeClient = null

View File

@@ -136,7 +136,7 @@ export class WcdbService {
*/ */
setMonitor(callback: (type: string, json: string) => void): void { setMonitor(callback: (type: string, json: string) => void): void {
this.monitorListener = callback; this.monitorListener = callback;
this.callWorker('setMonitor').catch(() => { }); this.callWorker<{ success?: boolean }>('setMonitor').catch(() => { });
} }
/** /**

View File

@@ -20,15 +20,17 @@ if (parentPort) {
result = { success: true } result = { success: true }
break break
case 'setMonitor': case 'setMonitor':
core.setMonitor((type, json) => { {
const monitorOk = core.setMonitor((type, json) => {
parentPort!.postMessage({ parentPort!.postMessage({
id: -1, id: -1,
type: 'monitor', type: 'monitor',
payload: { type, json } payload: { type, json }
}) })
}) })
result = { success: true } result = { success: monitorOk }
break break
}
case 'testConnection': case 'testConnection':
result = await core.testConnection(payload.dbPath, payload.hexKey, payload.wxid) result = await core.testConnection(payload.dbPath, payload.hexKey, payload.wxid)
break break

View File

@@ -14,7 +14,7 @@
.update-dialog { .update-dialog {
width: 680px; width: 680px;
background: #f5f5f5; background: var(--bg-secondary, #f5f5f5);
border-radius: 24px; border-radius: 24px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
overflow: hidden; overflow: hidden;
@@ -25,7 +25,7 @@
/* Top Section (White/Gradient) */ /* Top Section (White/Gradient) */
.dialog-header { .dialog-header {
background: #ffffff; background: var(--bg-primary, #ffffff);
padding: 40px 20px 30px; padding: 40px 20px 30px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -41,14 +41,14 @@
left: -50px; left: -50px;
width: 200px; width: 200px;
height: 200px; height: 200px;
background: radial-gradient(circle, rgba(255, 235, 220, 0.4) 0%, rgba(255, 255, 255, 0) 70%); background: radial-gradient(circle, rgba(255, 235, 220, 0.15) 0%, rgba(255, 255, 255, 0) 70%);
opacity: 0.8; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
.version-tag { .version-tag {
background: #f0eee9; background: var(--bg-tertiary, #f0eee9);
color: #8c7b6e; color: var(--text-tertiary, #8c7b6e);
padding: 4px 16px; padding: 4px 16px;
border-radius: 12px; border-radius: 12px;
font-size: 13px; font-size: 13px;
@@ -60,21 +60,21 @@
h2 { h2 {
font-size: 32px; font-size: 32px;
font-weight: 800; font-weight: 800;
color: #333333; color: var(--text-primary, #333333);
margin: 0 0 12px; margin: 0 0 12px;
letter-spacing: -0.5px; letter-spacing: -0.5px;
} }
.subtitle { .subtitle {
font-size: 15px; font-size: 15px;
color: #999999; color: var(--text-secondary, #999999);
font-weight: 400; font-weight: 400;
} }
} }
/* Content Section (Light Gray) */ /* Content Section (Light Gray) */
.dialog-content { .dialog-content {
background: #f2f2f2; background: var(--bg-tertiary, #f2f2f2);
padding: 24px 40px 40px; padding: 24px 40px 40px;
flex: 1; flex: 1;
display: flex; display: flex;
@@ -87,7 +87,7 @@
margin-bottom: 30px; margin-bottom: 30px;
.icon-box { .icon-box {
background: #fbfbfb; // Beige-ish white background: var(--bg-primary, #fbfbfb);
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 16px; border-radius: 16px;
@@ -96,7 +96,7 @@
justify-content: center; justify-content: center;
margin-right: 20px; margin-right: 20px;
flex-shrink: 0; flex-shrink: 0;
color: #8c7b6e; color: var(--text-tertiary, #8c7b6e);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
svg { svg {
@@ -107,27 +107,38 @@
.text-box { .text-box {
flex: 1; flex: 1;
h3 { h1, h2, h3, h4, h5, h6 {
font-size: 18px; color: var(--text-primary, #333333);
font-weight: 700; font-weight: 700;
color: #333333; margin: 16px 0 8px;
margin: 0 0 8px;
&:first-child {
margin-top: 0;
}
}
h2 {
font-size: 16px;
}
h3 {
font-size: 15px;
} }
p { p {
font-size: 14px; font-size: 14px;
color: #666666; color: var(--text-secondary, #666666);
line-height: 1.6; line-height: 1.6;
margin: 0; margin: 4px 0;
} }
ul { ul {
margin: 8px 0 0 18px; margin: 4px 0 0 18px;
padding: 0; padding: 0;
li { li {
font-size: 14px; font-size: 14px;
color: #666666; color: var(--text-secondary, #666666);
line-height: 1.6; line-height: 1.6;
} }
} }
@@ -142,19 +153,19 @@
justify-content: space-between; justify-content: space-between;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 12px; font-size: 12px;
color: #888; color: var(--text-secondary, #888);
font-weight: 500; font-weight: 500;
} }
.progress-bar-bg { .progress-bar-bg {
height: 6px; height: 6px;
background: #e0e0e0; background: var(--border-color, #e0e0e0);
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
.progress-bar-fill { .progress-bar-fill {
height: 100%; height: 100%;
background: #000000; background: var(--text-primary, #000000);
border-radius: 3px; border-radius: 3px;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
@@ -164,7 +175,7 @@
text-align: center; text-align: center;
margin-top: 12px; margin-top: 12px;
font-size: 13px; font-size: 13px;
color: #666; color: var(--text-secondary, #666);
} }
} }
@@ -175,8 +186,8 @@
.btn-ignore { .btn-ignore {
background: transparent; background: transparent;
color: #666666; color: var(--text-secondary, #666666);
border: 1px solid #d0d0d0; border: 1px solid var(--border-color, #d0d0d0);
padding: 16px 32px; padding: 16px 32px;
border-radius: 20px; border-radius: 20px;
font-size: 16px; font-size: 16px;
@@ -185,9 +196,9 @@
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
background: #f5f5f5; background: var(--bg-hover, #f5f5f5);
border-color: #999999; border-color: var(--text-secondary, #999999);
color: #333333; color: var(--text-primary, #333333);
} }
&:active { &:active {
@@ -196,11 +207,11 @@
} }
.btn-update { .btn-update {
background: #000000; background: var(--text-primary, #000000);
color: #ffffff; color: var(--bg-primary, #ffffff);
border: none; border: none;
padding: 16px 48px; padding: 16px 48px;
border-radius: 20px; // Pill shape border-radius: 20px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
@@ -231,7 +242,7 @@
right: 16px; right: 16px;
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
border: none; border: none;
color: #999; color: var(--text-secondary, #999);
cursor: pointer; cursor: pointer;
width: 32px; width: 32px;
height: 32px; height: 32px;
@@ -244,7 +255,7 @@
&:hover { &:hover {
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
color: #333; color: var(--text-primary, #333);
transform: rotate(90deg); transform: rotate(90deg);
} }
} }

View File

@@ -89,7 +89,6 @@ const UpdateDialog: React.FC<UpdateDialogProps> = ({
<Quote size={20} /> <Quote size={20} />
</div> </div>
<div className="text-box"> <div className="text-box">
<h3></h3>
{updateInfo.releaseNotes ? ( {updateInfo.releaseNotes ? (
<div dangerouslySetInnerHTML={{ __html: updateInfo.releaseNotes }} /> <div dangerouslySetInnerHTML={{ __html: updateInfo.releaseNotes }} />
) : ( ) : (

View File

@@ -897,6 +897,21 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
} }
} }
const handleClearLog = async () => {
const confirmed = window.confirm('确定清空 wcdb.log 吗?')
if (!confirmed) return
try {
const result = await window.electronAPI.log.clear()
if (!result.success) {
showMessage(result.error || '清空日志失败', false)
return
}
showMessage('日志已清空', true)
} catch (e: any) {
showMessage(`清空日志失败: ${e}`, false)
}
}
const handleClearAnalyticsCache = async () => { const handleClearAnalyticsCache = async () => {
if (isClearingCache) return if (isClearingCache) return
setIsClearingAnalyticsCache(true) setIsClearingAnalyticsCache(true)
@@ -1427,6 +1442,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<button className="btn btn-secondary" onClick={handleCopyLog}> <button className="btn btn-secondary" onClick={handleCopyLog}>
<Copy size={16} /> <Copy size={16} />
</button> </button>
<button className="btn btn-secondary" onClick={handleClearLog}>
<Trash2 size={16} />
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -2043,7 +2061,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<RefreshCw size={16} className={isCheckingUpdate ? 'spin' : ''} /> <RefreshCw size={16} className={isCheckingUpdate ? 'spin' : ''} />
{isCheckingUpdate ? '检查中...' : '检查更新'} {isCheckingUpdate ? '检查中...' : '检查更新'}
</button> </button>
</div> </div>
)} )}
</div> </div>

View File

@@ -69,6 +69,7 @@ export interface ElectronAPI {
log: { log: {
getPath: () => Promise<string> getPath: () => Promise<string>
read: () => Promise<{ success: boolean; content?: string; error?: string }> read: () => Promise<{ success: boolean; content?: string; error?: string }>
clear: () => Promise<{ success: boolean; error?: string }>
debug: (data: any) => void debug: (data: any) => void
} }
diagnostics: { diagnostics: {