mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-20 14:39:25 +08:00
merge: resolve upstream/dev conflicts in export workflow branch
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -57,8 +57,8 @@ Thumbs.db
|
||||
|
||||
wcdb/
|
||||
xkey/
|
||||
server/
|
||||
*info
|
||||
概述.md
|
||||
chatlab-format.md
|
||||
*.bak
|
||||
AGENTS.md
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
68
electron/services/cloudControlService.ts
Normal file
68
electron/services/cloudControlService.ts
Normal file
@@ -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<string> = 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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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', {})
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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}` }
|
||||
}
|
||||
|
||||
Binary file not shown.
74
src/App.tsx
74
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() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 数据收集同意弹窗 */}
|
||||
{showAnalyticsConsent && !agreementLoading && (
|
||||
<div className="agreement-overlay">
|
||||
<div className="agreement-modal">
|
||||
<div className="agreement-header">
|
||||
<Shield size={32} />
|
||||
<h2>使用数据收集说明</h2>
|
||||
</div>
|
||||
<div className="agreement-content">
|
||||
<div className="agreement-text">
|
||||
<p>为了持续改进 WeFlow 并提供更好的用户体验,我们希望收集一些匿名的使用数据。</p>
|
||||
|
||||
<h4>我们会收集什么?</h4>
|
||||
<p>• 功能使用情况(如哪些功能被使用、使用频率)</p>
|
||||
<p>• 应用性能数据(如加载时间、错误日志)</p>
|
||||
<p>• 设备基本信息(如操作系统版本、应用版本)</p>
|
||||
|
||||
<h4>我们不会收集什么?</h4>
|
||||
<p>• 你的聊天记录内容</p>
|
||||
<p>• 个人身份信息</p>
|
||||
<p>• 联系人信息</p>
|
||||
<p>• 任何可以识别你身份的数据</p>
|
||||
<p>• 一切你担心会涉及隐藏的数据</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="agreement-footer">
|
||||
<div className="agreement-actions">
|
||||
<button className="btn btn-secondary" onClick={handleAnalyticsDeny}>不允许</button>
|
||||
<button className="btn btn-primary" onClick={handleAnalyticsAllow}>允许</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 更新提示对话框 */}
|
||||
<UpdateDialog
|
||||
open={showUpdateDialog}
|
||||
|
||||
9
src/services/cloudControl.ts
Normal file
9
src/services/cloudControl.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// 数据收集服务前端接口
|
||||
|
||||
export async function initCloudControl() {
|
||||
return window.electronAPI.cloud.init()
|
||||
}
|
||||
|
||||
export function recordPage(pageName: string) {
|
||||
window.electronAPI.cloud.recordPage(pageName)
|
||||
}
|
||||
@@ -61,7 +61,10 @@ export const CONFIG_KEYS = {
|
||||
NOTIFICATION_FILTER_LIST: 'notificationFilterList',
|
||||
|
||||
// 词云
|
||||
WORD_CLOUD_EXCLUDE_WORDS: 'wordCloudExcludeWords'
|
||||
WORD_CLOUD_EXCLUDE_WORDS: 'wordCloudExcludeWords',
|
||||
|
||||
// 数据收集
|
||||
ANALYTICS_CONSENT: 'analyticsConsent'
|
||||
} as const
|
||||
|
||||
export interface WxidConfig {
|
||||
@@ -1083,3 +1086,15 @@ export async function getWordCloudExcludeWords(): Promise<string[]> {
|
||||
export async function setWordCloudExcludeWords(words: string[]): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.WORD_CLOUD_EXCLUDE_WORDS, words)
|
||||
}
|
||||
|
||||
// 获取数据收集同意状态
|
||||
export async function getAnalyticsConsent(): Promise<boolean | null> {
|
||||
const value = await config.get(CONFIG_KEYS.ANALYTICS_CONSENT)
|
||||
if (typeof value === 'boolean') return value
|
||||
return null
|
||||
}
|
||||
|
||||
// 设置数据收集同意状态
|
||||
export async function setAnalyticsConsent(consent: boolean): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.ANALYTICS_CONSENT, consent)
|
||||
}
|
||||
|
||||
5
src/types/electron.d.ts
vendored
5
src/types/electron.d.ts
vendored
@@ -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<void>
|
||||
recordPage: (pageName: string) => Promise<void>
|
||||
getLogs: () => Promise<string[]>
|
||||
}
|
||||
http: {
|
||||
start: (port?: number) => Promise<{ success: boolean; port?: number; error?: string }>
|
||||
stop: () => Promise<{ success: boolean }>
|
||||
|
||||
Reference in New Issue
Block a user