diff --git a/.gitignore b/.gitignore index 8601fb0..a173bbb 100644 --- a/.gitignore +++ b/.gitignore @@ -57,8 +57,8 @@ Thumbs.db wcdb/ xkey/ +server/ *info -概述.md chatlab-format.md *.bak AGENTS.md diff --git a/electron/main.ts b/electron/main.ts index f686c4b..72a7bd3 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -21,6 +21,7 @@ import { videoService } from './services/videoService' import { snsService, isVideoUrl } from './services/snsService' import { contactExportService } from './services/contactExportService' import { windowsHelloService } from './services/windowsHelloService' +import { cloudControlService } from './services/cloudControlService' import { registerNotificationHandlers, showNotification } from './windows/notificationWindow' import { httpService } from './services/httpService' @@ -665,6 +666,19 @@ function registerIpcHandlers() { } }) + // 数据收集服务 + ipcMain.handle('cloud:init', async () => { + await cloudControlService.init() + }) + + ipcMain.handle('cloud:recordPage', (_, pageName: string) => { + cloudControlService.recordPage(pageName) + }) + + ipcMain.handle('cloud:getLogs', async () => { + return cloudControlService.getLogs() + }) + ipcMain.handle('app:checkForUpdates', async () => { if (!AUTO_UPDATE_ENABLED) { return { hasUpdate: false } diff --git a/electron/preload.ts b/electron/preload.ts index dd087bb..687dcdb 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -303,6 +303,14 @@ contextBridge.exposeInMainWorld('electronAPI', { downloadEmoji: (params: { url: string; encryptUrl?: string; aesKey?: string }) => ipcRenderer.invoke('sns:downloadEmoji', params) }, + + // 数据收集 + cloud: { + init: () => ipcRenderer.invoke('cloud:init'), + recordPage: (pageName: string) => ipcRenderer.invoke('cloud:recordPage', pageName), + getLogs: () => ipcRenderer.invoke('cloud:getLogs') + }, + // HTTP API 服务 http: { start: (port?: number) => ipcRenderer.invoke('http:start', port), diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index e188de8..63946c9 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -2871,8 +2871,8 @@ class ChatService { private shouldKeepSession(username: string): boolean { if (!username) return false const lowered = username.toLowerCase() - // placeholder_foldgroup 是折叠群入口,需要保留 - if (lowered.includes('@placeholder') && !lowered.includes('foldgroup')) return false + // 排除所有 placeholder 会话(包括折叠群) + if (lowered.includes('@placeholder')) return false if (username.startsWith('gh_')) return false const excludeList = [ diff --git a/electron/services/cloudControlService.ts b/electron/services/cloudControlService.ts new file mode 100644 index 0000000..f1a1c02 --- /dev/null +++ b/electron/services/cloudControlService.ts @@ -0,0 +1,68 @@ +import { app } from 'electron' +import { wcdbService } from './wcdbService' + +interface UsageStats { + appVersion: string + platform: string + deviceId: string + timestamp: number + online: boolean + pages: string[] +} + +class CloudControlService { + private deviceId: string = '' + private timer: NodeJS.Timeout | null = null + private pages: Set = new Set() + + async init() { + this.deviceId = this.getDeviceId() + await wcdbService.cloudInit(300) + await this.reportOnline() + + this.timer = setInterval(() => { + this.reportOnline() + }, 300000) + } + + private getDeviceId(): string { + const crypto = require('crypto') + const os = require('os') + const machineId = os.hostname() + os.platform() + os.arch() + return crypto.createHash('md5').update(machineId).digest('hex') + } + + private async reportOnline() { + const data: UsageStats = { + appVersion: app.getVersion(), + platform: process.platform, + deviceId: this.deviceId, + timestamp: Date.now(), + online: true, + pages: Array.from(this.pages) + } + + await wcdbService.cloudReport(JSON.stringify(data)) + this.pages.clear() + } + + recordPage(pageName: string) { + this.pages.add(pageName) + } + + stop() { + if (this.timer) { + clearInterval(this.timer) + this.timer = null + } + wcdbService.cloudStop() + } + + async getLogs() { + return wcdbService.getLogs() + } +} + +export const cloudControlService = new CloudControlService() + + diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index 835850f..6ade687 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -431,20 +431,6 @@ class SnsService { const result = await wcdbService.getSnsTimeline(limit, offset, usernames, keyword, startTime, endTime) if (!result.success || !result.timeline || result.timeline.length === 0) return result - // 诊断:测试 execQuery 查 content 字段 - try { - const testResult = await wcdbService.execQuery('sns', null, 'SELECT tid, CAST(content AS TEXT) as ct, typeof(content) as ctype FROM SnsTimeLine ORDER BY tid DESC LIMIT 1') - if (testResult.success && testResult.rows?.[0]) { - const r = testResult.rows[0] - console.log('[SnsService] execQuery 诊断: ctype=', r.ctype, 'ct长度=', r.ct?.length, 'ct前200=', r.ct?.substring(0, 200)) - console.log('[SnsService] ct包含CommentUserList:', r.ct?.includes('CommentUserList')) - } else { - console.log('[SnsService] execQuery 诊断失败:', testResult.error) - } - } catch (e) { - console.log('[SnsService] execQuery 诊断异常:', e) - } - const enrichedTimeline = result.timeline.map((post: any) => { const contact = this.contactCache.get(post.username) const isVideoPost = post.type === 15 diff --git a/electron/services/wcdbCore.ts b/electron/services/wcdbCore.ts index 1c47b4c..0e3a6e7 100644 --- a/electron/services/wcdbCore.ts +++ b/electron/services/wcdbCore.ts @@ -1,4 +1,4 @@ -import { join, dirname, basename } from 'path' +import { join, dirname, basename } from 'path' import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs' // DLL 初始化错误信息,用于帮助用户诊断问题 @@ -114,6 +114,9 @@ export class WcdbCore { private wcdbStartMonitorPipe: any = null private wcdbStopMonitorPipe: any = null private wcdbGetMonitorPipeName: any = null + private wcdbCloudInit: any = null + private wcdbCloudReport: any = null + private wcdbCloudStop: any = null private monitorPipeClient: any = null private monitorCallback: ((type: string, json: string) => void) | null = null @@ -702,6 +705,26 @@ export class WcdbCore { this.wcdbVerifyUser = null } + // wcdb_status wcdb_cloud_init(int32_t interval_seconds) + try { + this.wcdbCloudInit = this.lib.func('int32 wcdb_cloud_init(int32 intervalSeconds)') + } catch { + this.wcdbCloudInit = null + } + + // wcdb_status wcdb_cloud_report(const char* stats_json) + try { + this.wcdbCloudReport = this.lib.func('int32 wcdb_cloud_report(const char* statsJson)') + } catch { + this.wcdbCloudReport = null + } + + // void wcdb_cloud_stop() + try { + this.wcdbCloudStop = this.lib.func('void wcdb_cloud_stop()') + } catch { + this.wcdbCloudStop = null + } // 初始化 @@ -1841,8 +1864,57 @@ export class WcdbCore { } /** - * 验证 Windows Hello + * 数据收集初始化 */ + async cloudInit(intervalSeconds: number = 600): Promise<{ success: boolean; error?: string }> { + if (!this.initialized) { + const initOk = await this.initialize() + if (!initOk) return { success: false, error: 'WCDB init failed' } + } + if (!this.wcdbCloudInit) { + return { success: false, error: 'Cloud init API not supported by DLL' } + } + try { + const result = this.wcdbCloudInit(intervalSeconds) + if (result !== 0) { + return { success: false, error: `Cloud init failed: ${result}` } + } + return { success: true } + } catch (e) { + return { success: false, error: String(e) } + } + } + + async cloudReport(statsJson: string): Promise<{ success: boolean; error?: string }> { + if (!this.initialized) { + const initOk = await this.initialize() + if (!initOk) return { success: false, error: 'WCDB init failed' } + } + if (!this.wcdbCloudReport) { + return { success: false, error: 'Cloud report API not supported by DLL' } + } + try { + const result = this.wcdbCloudReport(statsJson || '') + if (result !== 0) { + return { success: false, error: `Cloud report failed: ${result}` } + } + return { success: true } + } catch (e) { + return { success: false, error: String(e) } + } + } + + cloudStop(): { success: boolean; error?: string } { + if (!this.wcdbCloudStop) { + return { success: false, error: 'Cloud stop API not supported by DLL' } + } + try { + this.wcdbCloudStop() + return { success: true } + } catch (e) { + return { success: false, error: String(e) } + } + } async verifyUser(message: string, hwnd?: string): Promise<{ success: boolean; error?: string }> { if (!this.initialized) { const initOk = await this.initialize() diff --git a/electron/services/wcdbService.ts b/electron/services/wcdbService.ts index b8834f6..bbf9ac9 100644 --- a/electron/services/wcdbService.ts +++ b/electron/services/wcdbService.ts @@ -479,6 +479,27 @@ export class WcdbService { return this.callWorker('deleteMessage', { sessionId, localId, createTime, dbPathHint }) } + /** + * 数据收集:初始化 + */ + async cloudInit(intervalSeconds: number): Promise<{ success: boolean; error?: string }> { + return this.callWorker('cloudInit', { intervalSeconds }) + } + + /** + * 数据收集:上报数据 + */ + async cloudReport(statsJson: string): Promise<{ success: boolean; error?: string }> { + return this.callWorker('cloudReport', { statsJson }) + } + + /** + * 数据收集:停止 + */ + cloudStop(): Promise<{ success: boolean; error?: string }> { + return this.callWorker('cloudStop', {}) + } + } diff --git a/electron/wcdbWorker.ts b/electron/wcdbWorker.ts index d95f5f6..2ccae78 100644 --- a/electron/wcdbWorker.ts +++ b/electron/wcdbWorker.ts @@ -171,7 +171,15 @@ if (parentPort) { case 'deleteMessage': result = await core.deleteMessage(payload.sessionId, payload.localId, payload.createTime, payload.dbPathHint) break - + case 'cloudInit': + result = await core.cloudInit(payload.intervalSeconds) + break + case 'cloudReport': + result = await core.cloudReport(payload.statsJson) + break + case 'cloudStop': + result = core.cloudStop() + break default: result = { success: false, error: `Unknown method: ${type}` } } diff --git a/resources/wcdb_api.dll b/resources/wcdb_api.dll index 4dcca7d..c03efbe 100644 Binary files a/resources/wcdb_api.dll and b/resources/wcdb_api.dll differ diff --git a/src/App.tsx b/src/App.tsx index c999a80..4561b90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import NotificationWindow from './pages/NotificationWindow' import { useAppStore } from './stores/appStore' import { themes, useThemeStore, type ThemeId, type ThemeMode } from './stores/themeStore' import * as configService from './services/config' +import * as cloudControl from './services/cloudControl' import { Download, X, Shield } from 'lucide-react' import './App.scss' @@ -75,6 +76,9 @@ function App() { const [agreementChecked, setAgreementChecked] = useState(false) const [agreementLoading, setAgreementLoading] = useState(true) + // 数据收集同意状态 + const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false) + useEffect(() => { const root = document.documentElement const body = document.body @@ -170,6 +174,12 @@ function App() { const agreed = await configService.getAgreementAccepted() if (!agreed) { setShowAgreement(true) + } else { + // 协议已同意,检查数据收集同意状态 + const consent = await configService.getAnalyticsConsent() + if (consent === null) { + setShowAnalyticsConsent(true) + } } } catch (e) { console.error('检查协议状态失败:', e) @@ -180,16 +190,44 @@ function App() { checkAgreement() }, []) + // 初始化数据收集 + useEffect(() => { + cloudControl.initCloudControl() + }, []) + + // 记录页面访问 + useEffect(() => { + const path = location.pathname + if (path && path !== '/') { + cloudControl.recordPage(path) + } + }, [location.pathname]) + const handleAgree = async () => { if (!agreementChecked) return await configService.setAgreementAccepted(true) setShowAgreement(false) + // 协议同意后,检查数据收集同意 + const consent = await configService.getAnalyticsConsent() + if (consent === null) { + setShowAnalyticsConsent(true) + } } const handleDisagree = () => { window.electronAPI.window.close() } + const handleAnalyticsAllow = async () => { + await configService.setAnalyticsConsent(true) + setShowAnalyticsConsent(false) + } + + const handleAnalyticsDeny = async () => { + await configService.setAnalyticsConsent(false) + window.electronAPI.window.close() + } + // 监听启动时的更新通知 useEffect(() => { if (isNotificationWindow) return // Skip updates in notification window @@ -439,6 +477,42 @@ function App() { )} + {/* 数据收集同意弹窗 */} + {showAnalyticsConsent && !agreementLoading && ( +
+
+
+ +

使用数据收集说明

+
+
+
+

为了持续改进 WeFlow 并提供更好的用户体验,我们希望收集一些匿名的使用数据。

+ +

我们会收集什么?

+

• 功能使用情况(如哪些功能被使用、使用频率)

+

• 应用性能数据(如加载时间、错误日志)

+

• 设备基本信息(如操作系统版本、应用版本)

+ +

我们不会收集什么?

+

• 你的聊天记录内容

+

• 个人身份信息

+

• 联系人信息

+

• 任何可以识别你身份的数据

+

• 一切你担心会涉及隐藏的数据

+ +
+
+
+
+ + +
+
+
+
+ )} + {/* 更新提示对话框 */} s.username !== 'floatbottle_folder_session') + setSessions(validSessions) + setFilteredSessions(validSessions) } } catch (e) { console.error('加载会话失败:', e) @@ -350,6 +353,7 @@ function ExportPage() { exportVideos: options.exportMedia && options.exportVideos, exportEmojis: options.exportMedia && options.exportEmojis, exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容 + exportQuotedContent: options.exportQuotedContent, excelCompactColumns: options.excelCompactColumns, txtColumns: options.txtColumns, displayNamePreference: options.displayNamePreference, @@ -774,6 +778,20 @@ function ExportPage() {
+ + +
+