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 a2d8d58..4ec00b7 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -22,6 +22,7 @@ import { snsService, isVideoUrl } from './services/snsService' import { contactExportService } from './services/contactExportService' import { windowsHelloService } from './services/windowsHelloService' import { exportCardDiagnosticsService } from './services/exportCardDiagnosticsService' +import { cloudControlService } from './services/cloudControlService' import { registerNotificationHandlers, showNotification } from './windows/notificationWindow' import { httpService } from './services/httpService' @@ -919,6 +920,19 @@ function registerIpcHandlers() { return exportCardDiagnosticsService.exportCombinedLogs(filePath, payload?.frontendLogs || []) }) + // 数据收集服务 + 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 8ac25a6..c173d10 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -357,6 +357,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 4063e8c..b77fa3a 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -4601,8 +4601,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 6a537e6..1e0be35 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -936,20 +936,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 80bdbc1..5153298 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 + } // 初始化 @@ -1875,8 +1898,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 a77378e..6aee8e9 100644 --- a/electron/services/wcdbService.ts +++ b/electron/services/wcdbService.ts @@ -483,6 +483,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 f2a4ddd..333527a 100644 --- a/electron/wcdbWorker.ts +++ b/electron/wcdbWorker.ts @@ -174,7 +174,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 4bbd1e9..9d040d2 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' @@ -77,6 +78,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 @@ -172,6 +176,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) @@ -182,16 +192,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 @@ -447,6 +485,42 @@ function App() { )} + {/* 数据收集同意弹窗 */} + {showAnalyticsConsent && !agreementLoading && ( +
+
+
+ +

使用数据收集说明

+
+
+
+

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

+ +

我们会收集什么?

+

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

+

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

+

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

+ +

我们不会收集什么?

+

• 你的聊天记录内容

+

• 个人身份信息

+

• 联系人信息

+

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

+

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

+ +
+
+
+
+ + +
+
+
+
+ )} + {/* 更新提示对话框 */} { export async function setWordCloudExcludeWords(words: string[]): Promise { await config.set(CONFIG_KEYS.WORD_CLOUD_EXCLUDE_WORDS, words) } + +// 获取数据收集同意状态 +export async function getAnalyticsConsent(): Promise { + const value = await config.get(CONFIG_KEYS.ANALYTICS_CONSENT) + if (typeof value === 'boolean') return value + return null +} + +// 设置数据收集同意状态 +export async function setAnalyticsConsent(consent: boolean): Promise { + await config.set(CONFIG_KEYS.ANALYTICS_CONSENT, consent) +} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 440244f..a3abc38 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -784,6 +784,11 @@ export interface ElectronAPI { deleteSnsPost: (postId: string) => Promise<{ success: boolean; error?: string }> downloadEmoji: (params: { url: string; encryptUrl?: string; aesKey?: string }) => Promise<{ success: boolean; localPath?: string; error?: string }> } + cloud: { + init: () => Promise + recordPage: (pageName: string) => Promise + getLogs: () => Promise + } http: { start: (port?: number) => Promise<{ success: boolean; port?: number; error?: string }> stop: () => Promise<{ success: boolean }>