@@ -305,7 +683,7 @@ function DualReportWindow() {
if (emojiUrl) {
return (
-

{
+

{
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.removeAttribute('style');
}} />
@@ -356,7 +734,7 @@ function DualReportWindow() {
if (avatarUrl) {
return (
-

+
)
}
@@ -419,9 +797,99 @@ function DualReportWindow() {
+
+
+
+
+
+
+
+ {isExporting && (
+
+
+
+
正在导出
+
{exportProgress}
+
+
+ )}
+
+ {showExportModal && (
+
setShowExportModal(false)}>
+
e.stopPropagation()}>
+
+
{exportMode === 'long' ? '自定义导出长图' : '选择要导出的板块'}
+
+
+
+ {getAvailableSections().map((section) => (
+
toggleSection(section.id)}
+ >
+
+ {selectedSections.has(section.id) && }
+
+
{section.name}
+
+ ))}
+
+
+
+
+
+
+
+ )}
+
-
-
+
+
WEFLOW · DUAL REPORT
{yearTitle}
双人聊天报告
@@ -433,7 +901,7 @@ function DualReportWindow() {
每一次对话都值得被珍藏
-
+
首次聊天
故事的开始
{firstChat ? (
@@ -457,7 +925,7 @@ function DualReportWindow() {
{yearFirstChat && (!firstChat || yearFirstChat.createTime !== firstChat.createTime) ? (
-
+
第一段对话
{reportData.year === 0 ? '你们的第一段对话' : `${reportData.year}年的第一段对话`}
@@ -473,7 +941,7 @@ function DualReportWindow() {
) : null}
{reportData.heatmap && (
-
+
聊天习惯
作息规律
{mostActive && (
@@ -486,14 +954,14 @@ function DualReportWindow() {
)}
{reportData.initiative && (
-
+
主动性
情感的天平
- {reportData.selfAvatarUrl ?

: '我'}
+ {reportData.selfAvatarUrl ?

: '我'}
{reportData.initiative.initiated}次
{initiatedPercent.toFixed(1)}%
@@ -507,7 +975,7 @@ function DualReportWindow() {
- {reportData.friendAvatarUrl ?

: reportData.friendName.substring(0, 1)}
+ {reportData.friendAvatarUrl ?

: reportData.friendName.substring(0, 1)}
{reportData.initiative.received}次
{receivedPercent.toFixed(1)}%
@@ -521,7 +989,7 @@ function DualReportWindow() {
)}
{reportData.response && (
-
+
回应速度
你说,我在
@@ -558,7 +1026,7 @@ function DualReportWindow() {
)}
{reportData.streak && (
-
+
聊天火花
最长连续聊天
@@ -596,7 +1064,7 @@ function DualReportWindow() {
)}
-
+
常用语
{yearTitle}常用语
@@ -640,7 +1108,7 @@ function DualReportWindow() {
-
+
年度统计
{yearTitle}数据概览
@@ -664,7 +1132,7 @@ function DualReportWindow() {
我常用的表情
{myEmojiUrl ? (
-

{
+

{
(e.target as HTMLImageElement).nextElementSibling?.removeAttribute('style');
(e.target as HTMLImageElement).style.display = 'none';
}} />
@@ -677,7 +1145,7 @@ function DualReportWindow() {
{reportData.friendName}常用的表情
{friendEmojiUrl ? (
-

{
+

{
(e.target as HTMLImageElement).nextElementSibling?.removeAttribute('style');
(e.target as HTMLImageElement).style.display = 'none';
}} />
@@ -690,7 +1158,7 @@ function DualReportWindow() {
-
+
尾声
谢谢你一直在
愿我们继续把故事写下去
diff --git a/src/utils/reportExport.ts b/src/utils/reportExport.ts
new file mode 100644
index 0000000..224b99e
--- /dev/null
+++ b/src/utils/reportExport.ts
@@ -0,0 +1,36 @@
+const PATTERN_LIGHT_SVG = ``
+
+const PATTERN_DARK_SVG = ``
+
+export const drawPatternBackground = async (
+ ctx: CanvasRenderingContext2D,
+ width: number,
+ height: number,
+ bgColor: string,
+ isDark: boolean
+) => {
+ ctx.fillStyle = bgColor
+ ctx.fillRect(0, 0, width, height)
+
+ const svgString = isDark ? PATTERN_DARK_SVG : PATTERN_LIGHT_SVG
+ const blob = new Blob([svgString], { type: 'image/svg+xml' })
+ const url = URL.createObjectURL(blob)
+
+ return new Promise((resolve) => {
+ const img = new window.Image()
+ img.onload = () => {
+ const pattern = ctx.createPattern(img, 'repeat')
+ if (pattern) {
+ ctx.fillStyle = pattern
+ ctx.fillRect(0, 0, width, height)
+ }
+ URL.revokeObjectURL(url)
+ resolve()
+ }
+ img.onerror = () => {
+ URL.revokeObjectURL(url)
+ resolve()
+ }
+ img.src = url
+ })
+}
From 758de9949b34b344db65545cd08c247c0c0b24ee Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Fri, 3 Apr 2026 21:08:05 +0800
Subject: [PATCH 2/2] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BC=80=E6=9C=BA?=
=?UTF-8?q?=E8=87=AA=E5=90=AF=E5=8A=A8=20[Enhancement]:=20=E5=B8=8C?=
=?UTF-8?q?=E6=9C=9B=E8=83=BD=E5=A4=9F=E6=94=AF=E6=8C=81=E9=9D=99=E9=BB=98?=
=?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=92=8C=E5=BC=80=E6=9C=BA=E8=87=AA=E5=90=AF?=
=?UTF-8?q?=E5=8A=A8=20Fixes=20#516?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
electron/main.ts | 134 +++++++++++++++++++++++++++++++++++-
electron/preload.ts | 2 +
electron/services/config.ts | 1 +
src/pages/SettingsPage.tsx | 64 +++++++++++++++++
src/services/config.ts | 13 ++++
src/types/electron.d.ts | 8 +++
6 files changed, 221 insertions(+), 1 deletion(-)
diff --git a/electron/main.ts b/electron/main.ts
index 352651d..b2a93e5 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -171,6 +171,118 @@ const AUTO_UPDATE_ENABLED =
process.env.AUTO_UPDATE_ENABLED === '1' ||
(process.env.AUTO_UPDATE_ENABLED == null && !process.env.VITE_DEV_SERVER_URL)
+const getLaunchAtStartupUnsupportedReason = (): string | null => {
+ if (process.platform !== 'win32' && process.platform !== 'darwin') {
+ return '当前平台暂不支持开机自启动'
+ }
+ if (!app.isPackaged) {
+ return '仅安装后的 Windows / macOS 版本支持开机自启动'
+ }
+ return null
+}
+
+const isLaunchAtStartupSupported = (): boolean => getLaunchAtStartupUnsupportedReason() == null
+
+const getStoredLaunchAtStartupPreference = (): boolean | undefined => {
+ const value = configService?.get('launchAtStartup')
+ return typeof value === 'boolean' ? value : undefined
+}
+
+const getSystemLaunchAtStartup = (): boolean => {
+ if (!isLaunchAtStartupSupported()) return false
+ try {
+ return app.getLoginItemSettings().openAtLogin === true
+ } catch (error) {
+ console.error('[WeFlow] 读取开机自启动状态失败:', error)
+ return false
+ }
+}
+
+const buildLaunchAtStartupSettings = (enabled: boolean): Parameters[0] =>
+ process.platform === 'win32'
+ ? { openAtLogin: enabled, path: process.execPath }
+ : { openAtLogin: enabled }
+
+const setSystemLaunchAtStartup = (enabled: boolean): { success: boolean; enabled: boolean; error?: string } => {
+ try {
+ app.setLoginItemSettings(buildLaunchAtStartupSettings(enabled))
+ const effectiveEnabled = app.getLoginItemSettings().openAtLogin === true
+ if (effectiveEnabled !== enabled) {
+ return {
+ success: false,
+ enabled: effectiveEnabled,
+ error: '系统未接受该开机自启动设置'
+ }
+ }
+ return { success: true, enabled: effectiveEnabled }
+ } catch (error) {
+ return {
+ success: false,
+ enabled: getSystemLaunchAtStartup(),
+ error: `设置开机自启动失败: ${String((error as Error)?.message || error)}`
+ }
+ }
+}
+
+const getLaunchAtStartupStatus = (): { enabled: boolean; supported: boolean; reason?: string } => {
+ const unsupportedReason = getLaunchAtStartupUnsupportedReason()
+ if (unsupportedReason) {
+ return {
+ enabled: getStoredLaunchAtStartupPreference() === true,
+ supported: false,
+ reason: unsupportedReason
+ }
+ }
+ return {
+ enabled: getSystemLaunchAtStartup(),
+ supported: true
+ }
+}
+
+const applyLaunchAtStartupPreference = (
+ enabled: boolean
+): { success: boolean; enabled: boolean; supported: boolean; reason?: string; error?: string } => {
+ const unsupportedReason = getLaunchAtStartupUnsupportedReason()
+ if (unsupportedReason) {
+ return {
+ success: false,
+ enabled: getStoredLaunchAtStartupPreference() === true,
+ supported: false,
+ reason: unsupportedReason
+ }
+ }
+
+ const result = setSystemLaunchAtStartup(enabled)
+ configService?.set('launchAtStartup', result.enabled)
+ return {
+ ...result,
+ supported: true
+ }
+}
+
+const syncLaunchAtStartupPreference = () => {
+ if (!configService) return
+
+ const unsupportedReason = getLaunchAtStartupUnsupportedReason()
+ if (unsupportedReason) return
+
+ const storedPreference = getStoredLaunchAtStartupPreference()
+ const systemEnabled = getSystemLaunchAtStartup()
+
+ if (typeof storedPreference !== 'boolean') {
+ configService.set('launchAtStartup', systemEnabled)
+ return
+ }
+
+ if (storedPreference === systemEnabled) return
+
+ const result = setSystemLaunchAtStartup(storedPreference)
+ configService.set('launchAtStartup', result.enabled)
+ if (!result.success && result.error) {
+ console.error('[WeFlow] 同步开机自启动设置失败:', result.error)
+ }
+}
+
// 使用白名单过滤 PATH,避免被第三方目录中的旧版 VC++ 运行库劫持。
// 仅保留系统目录(Windows/System32/SysWOW64)和应用自身目录(可执行目录、resources)。
function sanitizePathEnv() {
@@ -1250,7 +1362,12 @@ function registerIpcHandlers() {
})
ipcMain.handle('config:set', async (_, key: string, value: any) => {
- const result = configService?.set(key as any, value)
+ let result: unknown
+ if (key === 'launchAtStartup') {
+ result = applyLaunchAtStartupPreference(value === true)
+ } else {
+ result = configService?.set(key as any, value)
+ }
if (key === 'updateChannel') {
applyAutoUpdateChannel('settings')
}
@@ -1259,6 +1376,12 @@ function registerIpcHandlers() {
})
ipcMain.handle('config:clear', async () => {
+ if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
+ const result = setSystemLaunchAtStartup(false)
+ if (!result.success && result.error) {
+ console.error('[WeFlow] 清空配置时关闭开机自启动失败:', result.error)
+ }
+ }
configService?.clear()
messagePushService.handleConfigCleared()
return true
@@ -1301,6 +1424,14 @@ function registerIpcHandlers() {
return app.getVersion()
})
+ ipcMain.handle('app:getLaunchAtStartupStatus', async () => {
+ return getLaunchAtStartupStatus()
+ })
+
+ ipcMain.handle('app:setLaunchAtStartup', async (_, enabled: boolean) => {
+ return applyLaunchAtStartupPreference(enabled === true)
+ })
+
ipcMain.handle('app:checkWayland', async () => {
if (process.platform !== 'linux') return false;
@@ -2881,6 +3012,7 @@ app.whenReady().then(async () => {
updateSplashProgress(5, '正在加载配置...')
configService = new ConfigService()
applyAutoUpdateChannel('startup')
+ syncLaunchAtStartupPreference()
// 将用户主题配置推送给 Splash 窗口
if (splashWindow && !splashWindow.isDestroyed()) {
diff --git a/electron/preload.ts b/electron/preload.ts
index 38e722f..db103ef 100644
--- a/electron/preload.ts
+++ b/electron/preload.ts
@@ -53,6 +53,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
app: {
getDownloadsPath: () => ipcRenderer.invoke('app:getDownloadsPath'),
getVersion: () => ipcRenderer.invoke('app:getVersion'),
+ getLaunchAtStartupStatus: () => ipcRenderer.invoke('app:getLaunchAtStartupStatus'),
+ setLaunchAtStartup: (enabled: boolean) => ipcRenderer.invoke('app:setLaunchAtStartup', enabled),
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'),
ignoreUpdate: (version: string) => ipcRenderer.invoke('app:ignoreUpdate', version),
diff --git a/electron/services/config.ts b/electron/services/config.ts
index 7e3b1e1..3039412 100644
--- a/electron/services/config.ts
+++ b/electron/services/config.ts
@@ -27,6 +27,7 @@ interface ConfigSchema {
themeId: string
language: string
logEnabled: boolean
+ launchAtStartup?: boolean
llmModelPath: string
whisperModelName: string
whisperModelDir: string
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx
index 98fe8b3..808a601 100644
--- a/src/pages/SettingsPage.tsx
+++ b/src/pages/SettingsPage.tsx
@@ -138,6 +138,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'>('top-right')
const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all')
const [notificationFilterList, setNotificationFilterList] = useState([])
+ const [launchAtStartup, setLaunchAtStartup] = useState(false)
+ const [launchAtStartupSupported, setLaunchAtStartupSupported] = useState(isWindows || isMac)
+ const [launchAtStartupReason, setLaunchAtStartupReason] = useState('')
const [windowCloseBehavior, setWindowCloseBehavior] = useState('ask')
const [quoteLayout, setQuoteLayout] = useState('quote-top')
const [updateChannel, setUpdateChannel] = useState('stable')
@@ -162,6 +165,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const [isFetchingDbKey, setIsFetchingDbKey] = useState(false)
const [isFetchingImageKey, setIsFetchingImageKey] = useState(false)
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false)
+ const [isUpdatingLaunchAtStartup, setIsUpdatingLaunchAtStartup] = useState(false)
const [appVersion, setAppVersion] = useState('')
const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null)
const [showDecryptKey, setShowDecryptKey] = useState(false)
@@ -337,6 +341,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const savedNotificationFilterMode = await configService.getNotificationFilterMode()
const savedNotificationFilterList = await configService.getNotificationFilterList()
const savedMessagePushEnabled = await configService.getMessagePushEnabled()
+ const savedLaunchAtStartupStatus = await window.electronAPI.app.getLaunchAtStartupStatus()
const savedWindowCloseBehavior = await configService.getWindowCloseBehavior()
const savedQuoteLayout = await configService.getQuoteLayout()
const savedUpdateChannel = await configService.getUpdateChannel()
@@ -386,6 +391,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
setNotificationFilterMode(savedNotificationFilterMode)
setNotificationFilterList(savedNotificationFilterList)
setMessagePushEnabled(savedMessagePushEnabled)
+ setLaunchAtStartup(savedLaunchAtStartupStatus.enabled)
+ setLaunchAtStartupSupported(savedLaunchAtStartupStatus.supported)
+ setLaunchAtStartupReason(savedLaunchAtStartupStatus.reason || '')
setWindowCloseBehavior(savedWindowCloseBehavior)
setQuoteLayout(savedQuoteLayout)
if (savedUpdateChannel) {
@@ -428,6 +436,29 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
+ const handleLaunchAtStartupChange = async (enabled: boolean) => {
+ if (isUpdatingLaunchAtStartup) return
+
+ try {
+ setIsUpdatingLaunchAtStartup(true)
+ const result = await window.electronAPI.app.setLaunchAtStartup(enabled)
+ setLaunchAtStartup(result.enabled)
+ setLaunchAtStartupSupported(result.supported)
+ setLaunchAtStartupReason(result.reason || '')
+
+ if (result.success) {
+ showMessage(enabled ? '已开启开机自启动' : '已关闭开机自启动', true)
+ return
+ }
+
+ showMessage(result.error || result.reason || '设置开机自启动失败', false)
+ } catch (e: any) {
+ showMessage(`设置开机自启动失败: ${e?.message || String(e)}`, false)
+ } finally {
+ setIsUpdatingLaunchAtStartup(false)
+ }
+ }
+
const refreshWhisperStatus = async (modelDirValue = whisperModelDir) => {
try {
const result = await window.electronAPI.whisper?.getModelStatus()
@@ -1199,6 +1230,39 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
+
+
+
+ {launchAtStartupSupported
+ ? '开启后,登录系统时会自动启动 WeFlow。'
+ : launchAtStartupReason || '当前环境暂不支持开机自启动。'}
+
+
+
+ {isUpdatingLaunchAtStartup
+ ? '保存中...'
+ : launchAtStartupSupported
+ ? (launchAtStartup ? '已开启' : '已关闭')
+ : '当前不可用'}
+
+
+
+
+
+
+
设置点击关闭按钮后的默认行为;选择“每次询问”时会弹出关闭确认。
diff --git a/src/services/config.ts b/src/services/config.ts
index 59e8afa..1f687e7 100644
--- a/src/services/config.ts
+++ b/src/services/config.ts
@@ -13,6 +13,7 @@ export const CONFIG_KEYS = {
LAST_SESSION: 'lastSession',
WINDOW_BOUNDS: 'windowBounds',
CACHE_PATH: 'cachePath',
+ LAUNCH_AT_STARTUP: 'launchAtStartup',
EXPORT_PATH: 'exportPath',
AGREEMENT_ACCEPTED: 'agreementAccepted',
@@ -258,6 +259,18 @@ export async function setLogEnabled(enabled: boolean): Promise {
await config.set(CONFIG_KEYS.LOG_ENABLED, enabled)
}
+// 获取开机自启动偏好
+export async function getLaunchAtStartup(): Promise {
+ const value = await config.get(CONFIG_KEYS.LAUNCH_AT_STARTUP)
+ if (typeof value === 'boolean') return value
+ return null
+}
+
+// 设置开机自启动偏好
+export async function setLaunchAtStartup(enabled: boolean): Promise {
+ await config.set(CONFIG_KEYS.LAUNCH_AT_STARTUP, enabled)
+}
+
// 获取 LLM 模型路径
export async function getLlmModelPath(): Promise {
const value = await config.get(CONFIG_KEYS.LLM_MODEL_PATH)
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts
index c174983..19f33a5 100644
--- a/src/types/electron.d.ts
+++ b/src/types/electron.d.ts
@@ -56,6 +56,14 @@ export interface ElectronAPI {
app: {
getDownloadsPath: () => Promise
getVersion: () => Promise
+ getLaunchAtStartupStatus: () => Promise<{ enabled: boolean; supported: boolean; reason?: string }>
+ setLaunchAtStartup: (enabled: boolean) => Promise<{
+ success: boolean
+ enabled: boolean
+ supported: boolean
+ reason?: string
+ error?: string
+ }>
checkForUpdates: () => Promise<{ hasUpdate: boolean; version?: string; releaseNotes?: string }>
downloadAndInstall: () => Promise
ignoreUpdate: (version: string) => Promise<{ success: boolean }>