mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-05-01 07:26:48 +00:00
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -350,6 +350,8 @@ jobs:
|
|||||||
updpkgsums: true
|
updpkgsums: true
|
||||||
assets: |
|
assets: |
|
||||||
resources/installer/linux/weflow.desktop
|
resources/installer/linux/weflow.desktop
|
||||||
|
resources/installer/linux/icon.png
|
||||||
|
resources/installer/linux/.gitignore
|
||||||
|
|
||||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
commit_username: H3CoF6
|
commit_username: H3CoF6
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { insightService } from './services/insightService'
|
|||||||
import { normalizeWeiboCookieInput, weiboService } from './services/social/weiboService'
|
import { normalizeWeiboCookieInput, weiboService } from './services/social/weiboService'
|
||||||
import { bizService } from './services/bizService'
|
import { bizService } from './services/bizService'
|
||||||
import { backupService } from './services/backupService'
|
import { backupService } from './services/backupService'
|
||||||
|
import { imageDownloadService } from './services/imageDownloadService'
|
||||||
|
|
||||||
// 配置自动更新
|
// 配置自动更新
|
||||||
autoUpdater.autoDownload = false
|
autoUpdater.autoDownload = false
|
||||||
@@ -3954,6 +3955,19 @@ function registerIpcHandlers() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 自动下载原图
|
||||||
|
ipcMain.handle('image:startAutoDownload', async (_, whitelist?: string[]) => {
|
||||||
|
return await imageDownloadService.startAutoDownload(whitelist || [])
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('image:stopAutoDownload', async () => {
|
||||||
|
await imageDownloadService.stopAutoDownload()
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('image:getAutoDownloadStatus', async () => {
|
||||||
|
return await imageDownloadService.getStatus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主窗口引用
|
// 主窗口引用
|
||||||
@@ -4081,6 +4095,13 @@ app.whenReady().then(async () => {
|
|||||||
// 注册 IPC 处理器
|
// 注册 IPC 处理器
|
||||||
updateSplashProgress(28, '正在初始化...')
|
updateSplashProgress(28, '正在初始化...')
|
||||||
registerIpcHandlers()
|
registerIpcHandlers()
|
||||||
|
if (configService.get('autoDownloadHighRes')) {
|
||||||
|
const whitelistArr = configService.get('autoDownloadWhitelist') || []
|
||||||
|
const whitelistStr = (Array.isArray(whitelistArr) && whitelistArr.length > 0)
|
||||||
|
? (whitelistArr.join('\0') + '\0\0')
|
||||||
|
: ''
|
||||||
|
imageDownloadService.startAutoDownload(whitelistStr)
|
||||||
|
}
|
||||||
chatService.addDbMonitorListener((type, json) => {
|
chatService.addDbMonitorListener((type, json) => {
|
||||||
messagePushService.handleDbMonitorChange(type, json)
|
messagePushService.handleDbMonitorChange(type, json)
|
||||||
insightService.handleDbMonitorChange(type, json)
|
insightService.handleDbMonitorChange(type, json)
|
||||||
@@ -4252,6 +4273,8 @@ const shutdownAppServices = async (): Promise<void> => {
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
forceExitTimer.unref()
|
forceExitTimer.unref()
|
||||||
try { await cloudControlService.stop() } catch {}
|
try { await cloudControlService.stop() } catch {}
|
||||||
|
// 停止自动下载服务
|
||||||
|
try { await imageDownloadService.stopAutoDownload() } catch {}
|
||||||
// 停止 chatService(内部会关闭 cursor 与 DB),避免退出阶段仍触发监控回调
|
// 停止 chatService(内部会关闭 cursor 与 DB),避免退出阶段仍触发监控回调
|
||||||
try { chatService.close() } catch {}
|
try { chatService.close() } catch {}
|
||||||
// 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出
|
// 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出
|
||||||
|
|||||||
@@ -365,7 +365,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
}) => callback(payload)
|
}) => callback(payload)
|
||||||
ipcRenderer.on('image:decryptProgress', listener)
|
ipcRenderer.on('image:decryptProgress', listener)
|
||||||
return () => ipcRenderer.removeListener('image:decryptProgress', listener)
|
return () => ipcRenderer.removeListener('image:decryptProgress', listener)
|
||||||
}
|
},
|
||||||
|
startAutoDownload: (whitelist: string[] | string) => ipcRenderer.invoke('image:startAutoDownload', whitelist),
|
||||||
|
stopAutoDownload: () => ipcRenderer.invoke('image:stopAutoDownload'),
|
||||||
|
getAutoDownloadStatus: () => ipcRenderer.invoke('image:getAutoDownloadStatus')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 视频
|
// 视频
|
||||||
@@ -374,6 +377,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
parseVideoMd5: (content: string) => ipcRenderer.invoke('video:parseVideoMd5', content)
|
parseVideoMd5: (content: string) => ipcRenderer.invoke('video:parseVideoMd5', content)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
process: {
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch
|
||||||
|
},
|
||||||
|
|
||||||
// 数据分析
|
// 数据分析
|
||||||
analytics: {
|
analytics: {
|
||||||
getOverallStatistics: (force?: boolean) => ipcRenderer.invoke('analytics:getOverallStatistics', force),
|
getOverallStatistics: (force?: boolean) => ipcRenderer.invoke('analytics:getOverallStatistics', force),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as https from 'https'
|
|||||||
import * as http from 'http'
|
import * as http from 'http'
|
||||||
import * as fzstd from 'fzstd'
|
import * as fzstd from 'fzstd'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
|
import { app, BrowserWindow, dialog } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { wcdbService } from './wcdbService'
|
import { wcdbService } from './wcdbService'
|
||||||
import { MessageCacheService } from './messageCacheService'
|
import { MessageCacheService } from './messageCacheService'
|
||||||
@@ -17,7 +18,6 @@ import { voiceTranscribeService } from './voiceTranscribeService'
|
|||||||
import { ImageDecryptService } from './imageDecryptService'
|
import { ImageDecryptService } from './imageDecryptService'
|
||||||
import { CONTACT_REGION_LOOKUP_DATA } from './contactRegionLookupData'
|
import { CONTACT_REGION_LOOKUP_DATA } from './contactRegionLookupData'
|
||||||
import { LRUCache } from '../utils/LRUCache.js'
|
import { LRUCache } from '../utils/LRUCache.js'
|
||||||
import { getAppPathFallback, getElectronBrowserWindow, getElectronDialog, getPathFallback, isElectronAppPackaged } from './electronRuntime'
|
|
||||||
|
|
||||||
export interface ChatSession {
|
export interface ChatSession {
|
||||||
username: string
|
username: string
|
||||||
@@ -498,7 +498,7 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async maybeShowInitFailureDialog(errorMessage: string): Promise<void> {
|
private async maybeShowInitFailureDialog(errorMessage: string): Promise<void> {
|
||||||
if (!isElectronAppPackaged()) return
|
if (!app.isPackaged) return
|
||||||
if (this.initFailureDialogShown) return
|
if (this.initFailureDialogShown) return
|
||||||
|
|
||||||
const code = this.extractErrorCode(errorMessage)
|
const code = this.extractErrorCode(errorMessage)
|
||||||
@@ -519,8 +519,6 @@ class ChatService {
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dialog = getElectronDialog()
|
|
||||||
if (!dialog?.showMessageBox) return
|
|
||||||
await dialog.showMessageBox({
|
await dialog.showMessageBox({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'WeFlow 启动失败',
|
title: 'WeFlow 启动失败',
|
||||||
@@ -602,7 +600,7 @@ class ChatService {
|
|||||||
console.error('[ChatService] 数据库监听回调失败:', error)
|
console.error('[ChatService] 数据库监听回调失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const windows = getElectronBrowserWindow()?.getAllWindows?.() || []
|
const windows = BrowserWindow.getAllWindows()
|
||||||
// 广播给所有渲染进程窗口
|
// 广播给所有渲染进程窗口
|
||||||
windows.forEach((win) => {
|
windows.forEach((win) => {
|
||||||
if (!win.isDestroyed()) {
|
if (!win.isDestroyed()) {
|
||||||
@@ -7182,7 +7180,7 @@ class ChatService {
|
|||||||
return join(cachePath, 'Voices')
|
return join(cachePath, 'Voices')
|
||||||
}
|
}
|
||||||
// 回退到默认目录
|
// 回退到默认目录
|
||||||
const documentsPath = getPathFallback('documents')
|
const documentsPath = app.getPath('documents')
|
||||||
return join(documentsPath, 'WeFlow', 'Voices')
|
return join(documentsPath, 'WeFlow', 'Voices')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7192,7 +7190,7 @@ class ChatService {
|
|||||||
return join(cachePath, 'Emojis')
|
return join(cachePath, 'Emojis')
|
||||||
}
|
}
|
||||||
// 回退到默认目录
|
// 回退到默认目录
|
||||||
const documentsPath = getPathFallback('documents')
|
const documentsPath = app.getPath('documents')
|
||||||
return join(documentsPath, 'WeFlow', 'Emojis')
|
return join(documentsPath, 'WeFlow', 'Emojis')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8437,13 +8435,13 @@ class ChatService {
|
|||||||
private async decodeSilkToPcm(silkData: Buffer, sampleRate: number): Promise<Buffer | null> {
|
private async decodeSilkToPcm(silkData: Buffer, sampleRate: number): Promise<Buffer | null> {
|
||||||
try {
|
try {
|
||||||
let wasmPath: string
|
let wasmPath: string
|
||||||
if (isElectronAppPackaged()) {
|
if (app.isPackaged) {
|
||||||
wasmPath = join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', 'silk-wasm', 'lib', 'silk.wasm')
|
wasmPath = join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', 'silk-wasm', 'lib', 'silk.wasm')
|
||||||
if (!existsSync(wasmPath)) {
|
if (!existsSync(wasmPath)) {
|
||||||
wasmPath = join(process.resourcesPath, 'node_modules', 'silk-wasm', 'lib', 'silk.wasm')
|
wasmPath = join(process.resourcesPath, 'node_modules', 'silk-wasm', 'lib', 'silk.wasm')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
wasmPath = join(getAppPathFallback(), 'node_modules', 'silk-wasm', 'lib', 'silk.wasm')
|
wasmPath = join(app.getAppPath(), 'node_modules', 'silk-wasm', 'lib', 'silk.wasm')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existsSync(wasmPath)) {
|
if (!existsSync(wasmPath)) {
|
||||||
@@ -8631,7 +8629,7 @@ class ChatService {
|
|||||||
/** 获取持久化转写缓存文件路径 */
|
/** 获取持久化转写缓存文件路径 */
|
||||||
private getTranscriptCachePath(): string {
|
private getTranscriptCachePath(): string {
|
||||||
const cachePath = this.configService.get('cachePath')
|
const cachePath = this.configService.get('cachePath')
|
||||||
const base = cachePath || join(getPathFallback('documents'), 'WeFlow')
|
const base = cachePath || join(app.getPath('documents'), 'WeFlow')
|
||||||
return join(base, 'Voices', 'transcripts.json')
|
return join(base, 'Voices', 'transcripts.json')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { dirname, join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { app, safeStorage } from 'electron'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
import Store from 'electron-store'
|
||||||
import { expandHomePath } from '../utils/pathUtils'
|
import { expandHomePath } from '../utils/pathUtils'
|
||||||
import { getElectronSafeStorage, getPathFallback, isWorkerRuntime } from './electronRuntime'
|
|
||||||
|
|
||||||
// 加密前缀标记
|
// 加密前缀标记
|
||||||
const SAFE_PREFIX = 'safe:' // safeStorage 加密(普通模式)
|
const SAFE_PREFIX = 'safe:' // safeStorage 加密(普通模式)
|
||||||
const isSafeStorageAvailable = (): boolean => {
|
const isSafeStorageAvailable = (): boolean => {
|
||||||
try {
|
try {
|
||||||
const safeStorage = getElectronSafeStorage()
|
|
||||||
return typeof safeStorage?.isEncryptionAvailable === 'function' && safeStorage.isEncryptionAvailable()
|
return typeof safeStorage?.isEncryptionAvailable === 'function' && safeStorage.isEncryptionAvailable()
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
@@ -86,7 +85,13 @@ interface ConfigSchema {
|
|||||||
aiInsightApiModel: string
|
aiInsightApiModel: string
|
||||||
aiInsightSilenceDays: number
|
aiInsightSilenceDays: number
|
||||||
aiInsightAllowContext: boolean
|
aiInsightAllowContext: boolean
|
||||||
|
aiInsightAllowMomentsContext: boolean
|
||||||
|
aiInsightMomentsContextCount: number
|
||||||
|
aiInsightMomentsBindings: Record<string, { enabled: boolean; updatedAt: number }>
|
||||||
aiInsightAllowSocialContext: boolean
|
aiInsightAllowSocialContext: boolean
|
||||||
|
aiInsightSocialContextCount: number
|
||||||
|
aiInsightWeiboCookie: string
|
||||||
|
aiInsightWeiboBindings: Record<string, { uid: string; screenName?: string; updatedAt: number }>
|
||||||
aiInsightFilterMode: 'whitelist' | 'blacklist'
|
aiInsightFilterMode: 'whitelist' | 'blacklist'
|
||||||
aiInsightFilterList: string[]
|
aiInsightFilterList: string[]
|
||||||
aiInsightWhitelistEnabled: boolean
|
aiInsightWhitelistEnabled: boolean
|
||||||
@@ -111,68 +116,8 @@ interface ConfigSchema {
|
|||||||
aiFootprintSystemPrompt: string
|
aiFootprintSystemPrompt: string
|
||||||
/** 是否将 AI 见解调试日志输出到桌面 */
|
/** 是否将 AI 见解调试日志输出到桌面 */
|
||||||
aiInsightDebugLogEnabled: boolean
|
aiInsightDebugLogEnabled: boolean
|
||||||
}
|
autoDownloadHighRes: boolean
|
||||||
|
autoDownloadWhitelist: string[]
|
||||||
interface ConfigStoreLike<T extends Record<string, any>> {
|
|
||||||
get<K extends keyof T>(key: K): T[K]
|
|
||||||
set<K extends keyof T>(key: K, value: T[K]): void
|
|
||||||
clear(): void
|
|
||||||
store: T
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneJson<T>(value: T): T {
|
|
||||||
return JSON.parse(JSON.stringify(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
class JsonConfigStore<T extends Record<string, any>> implements ConfigStoreLike<T> {
|
|
||||||
private readonly filePath: string
|
|
||||||
private readonly defaults: T
|
|
||||||
private data: T
|
|
||||||
|
|
||||||
constructor(options: { name: string; defaults: T; cwd?: string }) {
|
|
||||||
const baseDir = options.cwd || getPathFallback('userData')
|
|
||||||
mkdirSync(baseDir, { recursive: true })
|
|
||||||
this.filePath = join(baseDir, `${options.name}.json`)
|
|
||||||
this.defaults = cloneJson(options.defaults)
|
|
||||||
this.data = cloneJson(options.defaults)
|
|
||||||
this.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
get store(): T {
|
|
||||||
return this.data
|
|
||||||
}
|
|
||||||
|
|
||||||
private load(): void {
|
|
||||||
try {
|
|
||||||
if (!existsSync(this.filePath)) return
|
|
||||||
const raw = readFileSync(this.filePath, 'utf8')
|
|
||||||
const parsed = JSON.parse(raw)
|
|
||||||
if (parsed && typeof parsed === 'object') {
|
|
||||||
this.data = { ...cloneJson(this.defaults), ...parsed }
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
this.data = cloneJson(this.defaults)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private persist(): void {
|
|
||||||
mkdirSync(dirname(this.filePath), { recursive: true })
|
|
||||||
writeFileSync(this.filePath, JSON.stringify(this.data), 'utf8')
|
|
||||||
}
|
|
||||||
|
|
||||||
get<K extends keyof T>(key: K): T[K] {
|
|
||||||
return this.data[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
set<K extends keyof T>(key: K, value: T[K]): void {
|
|
||||||
this.data[key] = value
|
|
||||||
this.persist()
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.data = cloneJson(this.defaults)
|
|
||||||
this.persist()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 需要 safeStorage 加密的字段(普通模式)
|
// 需要 safeStorage 加密的字段(普通模式)
|
||||||
@@ -194,7 +139,7 @@ const LOCKABLE_NUMBER_KEYS: Set<string> = new Set(['imageXorKey'])
|
|||||||
|
|
||||||
export class ConfigService {
|
export class ConfigService {
|
||||||
private static instance: ConfigService
|
private static instance: ConfigService
|
||||||
private store!: ConfigStoreLike<ConfigSchema>
|
private store!: Store<ConfigSchema>
|
||||||
|
|
||||||
// 锁定模式运行时状态
|
// 锁定模式运行时状态
|
||||||
private unlockedKeys: Map<string, any> = new Map()
|
private unlockedKeys: Map<string, any> = new Map()
|
||||||
@@ -268,6 +213,9 @@ export class ConfigService {
|
|||||||
aiInsightApiModel: 'gpt-4o-mini',
|
aiInsightApiModel: 'gpt-4o-mini',
|
||||||
aiInsightSilenceDays: 3,
|
aiInsightSilenceDays: 3,
|
||||||
aiInsightAllowContext: false,
|
aiInsightAllowContext: false,
|
||||||
|
aiInsightAllowMomentsContext: false,
|
||||||
|
aiInsightMomentsContextCount: 5,
|
||||||
|
aiInsightMomentsBindings: {},
|
||||||
aiInsightAllowSocialContext: false,
|
aiInsightAllowSocialContext: false,
|
||||||
aiInsightFilterMode: 'whitelist',
|
aiInsightFilterMode: 'whitelist',
|
||||||
aiInsightFilterList: [],
|
aiInsightFilterList: [],
|
||||||
@@ -285,20 +233,41 @@ export class ConfigService {
|
|||||||
aiInsightWeiboBindings: {},
|
aiInsightWeiboBindings: {},
|
||||||
aiFootprintEnabled: false,
|
aiFootprintEnabled: false,
|
||||||
aiFootprintSystemPrompt: '',
|
aiFootprintSystemPrompt: '',
|
||||||
aiInsightDebugLogEnabled: false
|
aiInsightDebugLogEnabled: false,
|
||||||
|
autoDownloadHighRes: false,
|
||||||
|
autoDownloadWhitelist: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim()
|
const storeOptions: any = {
|
||||||
this.store = new JsonConfigStore<ConfigSchema>({
|
|
||||||
name: 'WeFlow-config',
|
name: 'WeFlow-config',
|
||||||
defaults,
|
defaults,
|
||||||
cwd: cwd || undefined
|
projectName: String(process.env.WEFLOW_PROJECT_NAME || 'WeFlow').trim() || 'WeFlow'
|
||||||
})
|
|
||||||
|
|
||||||
if (!isWorkerRuntime()) {
|
|
||||||
this.migrateAuthFields()
|
|
||||||
this.migrateAiConfig()
|
|
||||||
}
|
}
|
||||||
|
const runningInWorker = process.env.WEFLOW_WORKER === '1'
|
||||||
|
if (runningInWorker) {
|
||||||
|
const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim()
|
||||||
|
if (cwd) {
|
||||||
|
storeOptions.cwd = cwd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.store = new Store<ConfigSchema>(storeOptions)
|
||||||
|
} catch (error) {
|
||||||
|
const message = String((error as Error)?.message || error || '')
|
||||||
|
if (message.includes('projectName')) {
|
||||||
|
const fallbackOptions = {
|
||||||
|
...storeOptions,
|
||||||
|
projectName: 'WeFlow',
|
||||||
|
cwd: storeOptions.cwd || process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || process.cwd()
|
||||||
|
}
|
||||||
|
this.store = new Store<ConfigSchema>(fallbackOptions)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.migrateAuthFields()
|
||||||
|
this.migrateAiConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 状态查询 ===
|
// === 状态查询 ===
|
||||||
@@ -400,8 +369,6 @@ export class ConfigService {
|
|||||||
if (!plaintext) return ''
|
if (!plaintext) return ''
|
||||||
if (plaintext.startsWith(SAFE_PREFIX)) return plaintext
|
if (plaintext.startsWith(SAFE_PREFIX)) return plaintext
|
||||||
if (!isSafeStorageAvailable()) return plaintext
|
if (!isSafeStorageAvailable()) return plaintext
|
||||||
const safeStorage = getElectronSafeStorage()
|
|
||||||
if (!safeStorage) return plaintext
|
|
||||||
const encrypted = safeStorage.encryptString(plaintext)
|
const encrypted = safeStorage.encryptString(plaintext)
|
||||||
return SAFE_PREFIX + encrypted.toString('base64')
|
return SAFE_PREFIX + encrypted.toString('base64')
|
||||||
}
|
}
|
||||||
@@ -410,8 +377,6 @@ export class ConfigService {
|
|||||||
if (!stored) return ''
|
if (!stored) return ''
|
||||||
if (!stored.startsWith(SAFE_PREFIX)) return stored
|
if (!stored.startsWith(SAFE_PREFIX)) return stored
|
||||||
if (!isSafeStorageAvailable()) return ''
|
if (!isSafeStorageAvailable()) return ''
|
||||||
const safeStorage = getElectronSafeStorage()
|
|
||||||
if (!safeStorage) return ''
|
|
||||||
try {
|
try {
|
||||||
const buf = Buffer.from(stored.slice(SAFE_PREFIX.length), 'base64')
|
const buf = Buffer.from(stored.slice(SAFE_PREFIX.length), 'base64')
|
||||||
return safeStorage.decryptString(buf)
|
return safeStorage.decryptString(buf)
|
||||||
@@ -879,7 +844,7 @@ export class ConfigService {
|
|||||||
if (workerUserDataPath) {
|
if (workerUserDataPath) {
|
||||||
return workerUserDataPath
|
return workerUserDataPath
|
||||||
}
|
}
|
||||||
return getPathFallback('userData')
|
return app?.getPath?.('userData') || process.cwd()
|
||||||
}
|
}
|
||||||
|
|
||||||
getCacheBasePath(): string {
|
getCacheBasePath(): string {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { join, dirname } from 'path'
|
import { join, dirname } from 'path'
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
|
||||||
|
import { app } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
|
|
||||||
export interface ContactCacheEntry {
|
export interface ContactCacheEntry {
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import { homedir, tmpdir } from 'os'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
type RuntimeRequire = (id: string) => any
|
|
||||||
|
|
||||||
let cachedElectron: any | null | false = null
|
|
||||||
|
|
||||||
export function isWorkerRuntime(): boolean {
|
|
||||||
return process.env.WEFLOW_WORKER === '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getElectronModule(): any | null {
|
|
||||||
if (isWorkerRuntime()) return null
|
|
||||||
if (cachedElectron !== null) return cachedElectron || null
|
|
||||||
try {
|
|
||||||
const runtimeRequire = (0, eval)('require') as RuntimeRequire
|
|
||||||
cachedElectron = runtimeRequire('electron')
|
|
||||||
} catch {
|
|
||||||
cachedElectron = false
|
|
||||||
}
|
|
||||||
return cachedElectron || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getElectronApp(): any | null {
|
|
||||||
return getElectronModule()?.app || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getElectronBrowserWindow(): any | null {
|
|
||||||
return getElectronModule()?.BrowserWindow || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getElectronDialog(): any | null {
|
|
||||||
return getElectronModule()?.dialog || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getElectronSafeStorage(): any | null {
|
|
||||||
return getElectronModule()?.safeStorage || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getElectronPath(name: string): string | null {
|
|
||||||
try {
|
|
||||||
const getter = getElectronApp()?.getPath
|
|
||||||
if (typeof getter === 'function') {
|
|
||||||
return getter(name)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fall through to caller fallback
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAppPathFallback(): string {
|
|
||||||
try {
|
|
||||||
const getter = getElectronApp()?.getAppPath
|
|
||||||
if (typeof getter === 'function') {
|
|
||||||
return getter()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
return process.cwd()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPathFallback(name: string): string {
|
|
||||||
const fromElectron = getElectronPath(name)
|
|
||||||
if (fromElectron) return fromElectron
|
|
||||||
|
|
||||||
const home = homedir()
|
|
||||||
switch (name) {
|
|
||||||
case 'userData': {
|
|
||||||
const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim()
|
|
||||||
if (workerUserDataPath) return workerUserDataPath
|
|
||||||
if (process.platform === 'win32' && process.env.APPDATA) return join(process.env.APPDATA, 'WeFlow')
|
|
||||||
if (process.platform === 'darwin') return join(home, 'Library', 'Application Support', 'WeFlow')
|
|
||||||
return join(process.env.XDG_CONFIG_HOME || join(home, '.config'), 'WeFlow')
|
|
||||||
}
|
|
||||||
case 'documents':
|
|
||||||
return join(home, 'Documents')
|
|
||||||
case 'desktop':
|
|
||||||
return join(home, 'Desktop')
|
|
||||||
case 'downloads':
|
|
||||||
return join(home, 'Downloads')
|
|
||||||
case 'temp':
|
|
||||||
return tmpdir()
|
|
||||||
case 'appData':
|
|
||||||
return process.platform === 'win32' && process.env.APPDATA ? process.env.APPDATA : join(home, '.config')
|
|
||||||
default:
|
|
||||||
return process.cwd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isElectronAppPackaged(): boolean {
|
|
||||||
const app = getElectronApp()
|
|
||||||
if (typeof app?.isPackaged === 'boolean') return app.isPackaged
|
|
||||||
return Boolean((process as any).resourcesPath && process.env.NODE_ENV !== 'development')
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getPathFallback } from './electronRuntime'
|
|
||||||
|
|
||||||
export interface ExportRecord {
|
export interface ExportRecord {
|
||||||
exportTime: number
|
exportTime: number
|
||||||
@@ -20,7 +20,7 @@ class ExportRecordService {
|
|||||||
private resolveFilePath(): string {
|
private resolveFilePath(): string {
|
||||||
if (this.filePath) return this.filePath
|
if (this.filePath) return this.filePath
|
||||||
const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim()
|
const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim()
|
||||||
const userDataPath = workerUserDataPath || getPathFallback('userData')
|
const userDataPath = workerUserDataPath || app?.getPath?.('userData') || process.cwd()
|
||||||
fs.mkdirSync(userDataPath, { recursive: true })
|
fs.mkdirSync(userDataPath, { recursive: true })
|
||||||
this.filePath = path.join(userDataPath, 'weflow-export-records.json')
|
this.filePath = path.join(userDataPath, 'weflow-export-records.json')
|
||||||
return this.filePath
|
return this.filePath
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { app, BrowserWindow } from 'electron'
|
||||||
import { basename, dirname, extname, join } from 'path'
|
import { basename, dirname, extname, join } from 'path'
|
||||||
import { pathToFileURL } from 'url'
|
import { pathToFileURL } from 'url'
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, appendFileSync } from 'fs'
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, appendFileSync } from 'fs'
|
||||||
@@ -7,7 +8,6 @@ import crypto from 'crypto'
|
|||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { wcdbService } from './wcdbService'
|
import { wcdbService } from './wcdbService'
|
||||||
import { decryptDatViaNative, nativeAddonLocation } from './nativeImageDecrypt'
|
import { decryptDatViaNative, nativeAddonLocation } from './nativeImageDecrypt'
|
||||||
import { getElectronBrowserWindow, getPathFallback, isElectronAppPackaged } from './electronRuntime'
|
|
||||||
|
|
||||||
// 获取 ffmpeg-static 的路径
|
// 获取 ffmpeg-static 的路径
|
||||||
function getStaticFfmpegPath(): string | null {
|
function getStaticFfmpegPath(): string | null {
|
||||||
@@ -35,7 +35,7 @@ function getStaticFfmpegPath(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 方法3: 打包后的路径
|
// 方法3: 打包后的路径
|
||||||
if (isElectronAppPackaged()) {
|
if (app?.isPackaged) {
|
||||||
const resourcesPath = process.resourcesPath
|
const resourcesPath = process.resourcesPath
|
||||||
const packedPath = join(resourcesPath, 'app.asar.unpacked', 'node_modules', 'ffmpeg-static', 'ffmpeg.exe')
|
const packedPath = join(resourcesPath, 'app.asar.unpacked', 'node_modules', 'ffmpeg-static', 'ffmpeg.exe')
|
||||||
if (existsSync(packedPath)) {
|
if (existsSync(packedPath)) {
|
||||||
@@ -1475,7 +1475,7 @@ export class ImageDecryptService {
|
|||||||
|
|
||||||
private getActiveWindowsSafely(): Array<{ isDestroyed: () => boolean; webContents: { send: (channel: string, payload: unknown) => void } }> {
|
private getActiveWindowsSafely(): Array<{ isDestroyed: () => boolean; webContents: { send: (channel: string, payload: unknown) => void } }> {
|
||||||
try {
|
try {
|
||||||
const getter = (getElectronBrowserWindow() as { getAllWindows?: () => any[] } | undefined)?.getAllWindows
|
const getter = (BrowserWindow as unknown as { getAllWindows?: () => any[] } | undefined)?.getAllWindows
|
||||||
if (typeof getter !== 'function') return []
|
if (typeof getter !== 'function') return []
|
||||||
const windows = getter()
|
const windows = getter()
|
||||||
if (!Array.isArray(windows)) return []
|
if (!Array.isArray(windows)) return []
|
||||||
@@ -2191,7 +2191,14 @@ export class ImageDecryptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getElectronPath(name: 'userData' | 'documents' | 'temp'): string | null {
|
private getElectronPath(name: 'userData' | 'documents' | 'temp'): string | null {
|
||||||
return getPathFallback(name)
|
try {
|
||||||
|
const getter = (app as unknown as { getPath?: (n: string) => string } | undefined)?.getPath
|
||||||
|
if (typeof getter !== 'function') return null
|
||||||
|
const value = getter(name)
|
||||||
|
return typeof value === 'string' && value.trim() ? value : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUserDataPath(): string {
|
private getUserDataPath(): string {
|
||||||
|
|||||||
203
electron/services/imageDownloadService.ts
Normal file
203
electron/services/imageDownloadService.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { execFile } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
// import { ConfigService } from './config'
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
|
export class ImageDownloadService {
|
||||||
|
private static instance: ImageDownloadService
|
||||||
|
private koffi: any = null
|
||||||
|
private lib: any = null
|
||||||
|
private initialized = false
|
||||||
|
|
||||||
|
private initImgHelper: any = null
|
||||||
|
private uninstallImgHelper: any = null
|
||||||
|
private getImgHelperError: any = null
|
||||||
|
|
||||||
|
private currentPid: number | null = null
|
||||||
|
private pollTimer: NodeJS.Timeout | null = null
|
||||||
|
private isHooked = false
|
||||||
|
|
||||||
|
private lastWhitelist: string[] = []
|
||||||
|
|
||||||
|
static getInstance(): ImageDownloadService {
|
||||||
|
if (!ImageDownloadService.instance) {
|
||||||
|
ImageDownloadService.instance = new ImageDownloadService()
|
||||||
|
}
|
||||||
|
return ImageDownloadService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureInitialized(): Promise<boolean> {
|
||||||
|
if (this.initialized) return true
|
||||||
|
if (process.platform !== 'win32' || process.arch !== 'x64') return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.koffi = require('koffi')
|
||||||
|
const dllPath = this.getDllPath()
|
||||||
|
if (!existsSync(dllPath)) return false
|
||||||
|
|
||||||
|
this.lib = this.koffi.load(dllPath)
|
||||||
|
|
||||||
|
this.initImgHelper = this.lib.func('bool InitImgHelper(uint32, const char*)')
|
||||||
|
this.uninstallImgHelper = this.lib.func('void UninstallImgHelper()')
|
||||||
|
this.getImgHelperError = this.lib.func('const char* GetImgHelperError()')
|
||||||
|
|
||||||
|
this.initialized = true
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ImageDownloadService] failed to initialize:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDllPath(): string {
|
||||||
|
const isPackaged = app.isPackaged
|
||||||
|
const candidates: string[] = []
|
||||||
|
|
||||||
|
if (isPackaged) {
|
||||||
|
candidates.push(join(process.resourcesPath, 'resources', 'image', 'win32', 'x64', 'img_helper.dll'))
|
||||||
|
} else {
|
||||||
|
candidates.push(join(process.cwd(), 'resources', 'image', 'win32', 'x64', 'img_helper.dll'))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of candidates) {
|
||||||
|
if (existsSync(path)) return path
|
||||||
|
}
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findMainWeChatPid(): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const script = `
|
||||||
|
Get-CimInstance Win32_Process -Filter "Name = 'Weixin.exe'" |
|
||||||
|
Select-Object ProcessId, CommandLine |
|
||||||
|
ConvertTo-Json -Compress
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', script])
|
||||||
|
if (!stdout || !stdout.trim()) return null
|
||||||
|
|
||||||
|
let processes = JSON.parse(stdout.trim())
|
||||||
|
if (!Array.isArray(processes)) processes = [processes]
|
||||||
|
|
||||||
|
const target = processes
|
||||||
|
.filter((p: any) => p.CommandLine && p.CommandLine.toLowerCase().includes('weixin.exe'))
|
||||||
|
.sort((a: any, b: any) => a.CommandLine.length - b.CommandLine.length)[0]
|
||||||
|
|
||||||
|
return target ? target.ProcessId : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startAutoDownload(whitelist: string[] | string = []): Promise<{ success: boolean; error?: string }> {
|
||||||
|
if (!await this.ensureInitialized()) {
|
||||||
|
return { success: false, error: '核心组件初始化失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isHooked) {
|
||||||
|
await this.unhook()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastWhitelist = whitelist
|
||||||
|
|
||||||
|
if (!this.pollTimer) {
|
||||||
|
this.pollTimer = setInterval(() => this.checkAndHook(this.lastWhitelist, false), 30000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.checkAndHook(whitelist, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopAutoDownload() {
|
||||||
|
if (this.pollTimer) {
|
||||||
|
clearInterval(this.pollTimer)
|
||||||
|
this.pollTimer = null
|
||||||
|
}
|
||||||
|
await this.unhook()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkAndHook(whitelist: string[] | string = [], isManualStart = false): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const pid = await this.findMainWeChatPid()
|
||||||
|
|
||||||
|
if (!pid) {
|
||||||
|
if (this.isHooked) {
|
||||||
|
console.log('[ImageDownloadService] WeChat exited, unhooking')
|
||||||
|
await this.unhook()
|
||||||
|
}
|
||||||
|
return { success: true, error: '等待微信启动' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isHooked && this.currentPid === pid) {
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isHooked && this.currentPid !== pid) {
|
||||||
|
console.log('[ImageDownloadService] WeChat PID changed, re-hooking')
|
||||||
|
await this.unhook()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ImageDownloadService] attempting to hook PID: ${pid}`)
|
||||||
|
try {
|
||||||
|
let whitelistBuffer: Buffer | null = null;
|
||||||
|
if (typeof whitelist === 'string') {
|
||||||
|
if (whitelist.length > 0) {
|
||||||
|
whitelistBuffer = Buffer.from(whitelist, 'utf8');
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(whitelist) && whitelist.length > 0) {
|
||||||
|
whitelistBuffer = Buffer.from(whitelist.join('\0') + '\0\0', 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = this.initImgHelper(pid, whitelistBuffer)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this.isHooked = true
|
||||||
|
this.currentPid = pid
|
||||||
|
console.log('[ImageDownloadService] hook successful')
|
||||||
|
return { success: true }
|
||||||
|
} else {
|
||||||
|
const err = this.getImgHelperError()
|
||||||
|
console.error(`[ImageDownloadService] hook failed: ${err}`)
|
||||||
|
if (isManualStart && this.pollTimer) {
|
||||||
|
clearInterval(this.pollTimer)
|
||||||
|
this.pollTimer = null
|
||||||
|
}
|
||||||
|
return { success: false, error: err || 'Hook 失败' }
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[ImageDownloadService] InitImgHelper call crashed:', e)
|
||||||
|
if (isManualStart && this.pollTimer) {
|
||||||
|
clearInterval(this.pollTimer)
|
||||||
|
this.pollTimer = null
|
||||||
|
}
|
||||||
|
return { success: false, error: `调用异常: ${e.message || String(e)}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async unhook() {
|
||||||
|
if (this.isHooked && this.uninstallImgHelper) {
|
||||||
|
try {
|
||||||
|
this.uninstallImgHelper()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ImageDownloadService] uninstall failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isHooked = false
|
||||||
|
this.currentPid = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus() {
|
||||||
|
return {
|
||||||
|
isHooked: this.isHooked,
|
||||||
|
pid: this.currentPid,
|
||||||
|
supported: process.platform === 'win32' && process.arch === 'x64'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imageDownloadService = ImageDownloadService.getInstance()
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
* 设计原则:
|
* 设计原则:
|
||||||
* - 不引入任何额外 npm 依赖,使用 Node 原生 https 模块调用 OpenAI 兼容 API
|
* - 不引入任何额外 npm 依赖,使用 Node 原生 https 模块调用 OpenAI 兼容 API
|
||||||
* - 所有失败静默处理,不影响主流程
|
* - 所有失败静默处理,不影响主流程
|
||||||
* - 当日触发记录(sessionId + 时间列表)随 prompt 一起发送,让模型自行判断是否克制
|
* - 触发频率、冷却与名单过滤均在本地完成,不把调度统计塞进模型 prompt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
@@ -21,6 +21,7 @@ import { URL } from 'url'
|
|||||||
import { app, Notification } from 'electron'
|
import { app, Notification } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { chatService, ChatSession, Message } from './chatService'
|
import { chatService, ChatSession, Message } from './chatService'
|
||||||
|
import { snsService } from './snsService'
|
||||||
import { weiboService } from './social/weiboService'
|
import { weiboService } from './social/weiboService'
|
||||||
|
|
||||||
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
||||||
@@ -52,6 +53,9 @@ const INSIGHT_CONFIG_KEYS = new Set([
|
|||||||
'aiModelApiMaxTokens',
|
'aiModelApiMaxTokens',
|
||||||
'aiInsightFilterMode',
|
'aiInsightFilterMode',
|
||||||
'aiInsightFilterList',
|
'aiInsightFilterList',
|
||||||
|
'aiInsightAllowMomentsContext',
|
||||||
|
'aiInsightMomentsContextCount',
|
||||||
|
'aiInsightMomentsBindings',
|
||||||
'aiInsightAllowSocialContext',
|
'aiInsightAllowSocialContext',
|
||||||
'aiInsightSocialContextCount',
|
'aiInsightSocialContextCount',
|
||||||
'aiInsightWeiboCookie',
|
'aiInsightWeiboCookie',
|
||||||
@@ -445,7 +449,7 @@ class InsightService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
||||||
const requestMessages = [{ role: 'user', content: appendPromptCurrentTime('请回复"连接成功"四个字。') }]
|
const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }]
|
||||||
insightDebugSection(
|
insightDebugSection(
|
||||||
'INFO',
|
'INFO',
|
||||||
'AI 测试连接请求',
|
'AI 测试连接请求',
|
||||||
@@ -823,26 +827,13 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录触发并返回该会话今日所有触发时间(用于组装 prompt)。
|
* 记录成功推送的见解,用于设置页展示今日触发统计。
|
||||||
*/
|
*/
|
||||||
private recordTrigger(sessionId: string): string[] {
|
private recordTrigger(sessionId: string): void {
|
||||||
this.resetIfNewDay()
|
this.resetIfNewDay()
|
||||||
const existing = this.todayTriggers.get(sessionId) ?? { timestamps: [] }
|
const existing = this.todayTriggers.get(sessionId) ?? { timestamps: [] }
|
||||||
existing.timestamps.push(Date.now())
|
existing.timestamps.push(Date.now())
|
||||||
this.todayTriggers.set(sessionId, existing)
|
this.todayTriggers.set(sessionId, existing)
|
||||||
return existing.timestamps.map(formatTimestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取今日全局已触发次数(所有会话合计),用于 prompt 中告知模型全局上下文。
|
|
||||||
*/
|
|
||||||
private getTodayTotalTriggerCount(): number {
|
|
||||||
this.resetIfNewDay()
|
|
||||||
let total = 0
|
|
||||||
for (const record of this.todayTriggers.values()) {
|
|
||||||
total += record.timestamps.length
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatWeiboTimestamp(raw: string): string {
|
private formatWeiboTimestamp(raw: string): string {
|
||||||
@@ -853,12 +844,66 @@ ${topMentionText}
|
|||||||
return new Date(parsed).toLocaleString('zh-CN')
|
return new Date(parsed).toLocaleString('zh-CN')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatMomentsTimestamp(raw: unknown): string {
|
||||||
|
const numeric = Number(raw)
|
||||||
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const ms = numeric > 1_000_000_000_000 ? numeric : numeric * 1000
|
||||||
|
return new Date(ms).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractMomentReadableText(post: { contentDesc?: unknown; linkTitle?: unknown }): string {
|
||||||
|
const contentDesc = this.normalizeInsightText(String(post.contentDesc || '')).replace(/\s+/g, ' ').trim()
|
||||||
|
if (contentDesc) return contentDesc
|
||||||
|
|
||||||
|
const linkTitle = this.normalizeInsightText(String(post.linkTitle || '')).replace(/\s+/g, ' ').trim()
|
||||||
|
if (linkTitle) return `[链接] ${linkTitle}`
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getMomentsContextSection(sessionId: string): Promise<string> {
|
||||||
|
const allowMomentsContext = this.config.get('aiInsightAllowMomentsContext') === true
|
||||||
|
if (!allowMomentsContext) return ''
|
||||||
|
|
||||||
|
const bindings =
|
||||||
|
(this.config.get('aiInsightMomentsBindings') as Record<string, { enabled?: boolean }> | undefined) || {}
|
||||||
|
const isEnabledForSession = bindings[sessionId]?.enabled === true
|
||||||
|
if (!isEnabledForSession) return ''
|
||||||
|
|
||||||
|
const countRaw = Number(this.config.get('aiInsightMomentsContextCount') || 5)
|
||||||
|
const momentsCount = Math.max(1, Math.min(20, Math.floor(countRaw) || 5))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await snsService.getTimeline(momentsCount, 0, [sessionId])
|
||||||
|
const posts = result.success && Array.isArray(result.timeline) ? result.timeline : []
|
||||||
|
if (posts.length === 0) return ''
|
||||||
|
|
||||||
|
const lines = posts
|
||||||
|
.map((post) => {
|
||||||
|
const text = this.extractMomentReadableText(post as { contentDesc?: unknown; linkTitle?: unknown })
|
||||||
|
if (!text) return ''
|
||||||
|
const shortText = text.length > 180 ? `${text.slice(0, 180)}...` : text
|
||||||
|
const time = this.formatMomentsTimestamp((post as { createTime?: unknown }).createTime)
|
||||||
|
return time ? `[朋友圈 ${time}] ${shortText}` : `[朋友圈] ${shortText}`
|
||||||
|
})
|
||||||
|
.filter(Boolean) as string[]
|
||||||
|
|
||||||
|
if (lines.length === 0) return ''
|
||||||
|
insightLog('INFO', `已加载 ${lines.length} 条朋友圈内容 (sessionId=${sessionId})`)
|
||||||
|
return `近期朋友圈内容(最近 ${lines.length} 条):\n${lines.join('\n')}`
|
||||||
|
} catch (error) {
|
||||||
|
insightLog('WARN', `拉取朋友圈内容失败 (sessionId=${sessionId}): ${(error as Error).message}`)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getSocialContextSection(sessionId: string): Promise<string> {
|
private async getSocialContextSection(sessionId: string): Promise<string> {
|
||||||
const allowSocialContext = this.config.get('aiInsightAllowSocialContext') === true
|
const allowSocialContext = this.config.get('aiInsightAllowSocialContext') === true
|
||||||
if (!allowSocialContext) return ''
|
if (!allowSocialContext) return ''
|
||||||
|
|
||||||
const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim()
|
const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim()
|
||||||
const hasCookie = rawCookie.length > 0
|
|
||||||
|
|
||||||
const bindings =
|
const bindings =
|
||||||
(this.config.get('aiInsightWeiboBindings') as Record<string, { uid?: string; screenName?: string }> | undefined) || {}
|
(this.config.get('aiInsightWeiboBindings') as Record<string, { uid?: string; screenName?: string }> | undefined) || {}
|
||||||
@@ -879,10 +924,7 @@ ${topMentionText}
|
|||||||
return `[微博 ${time}] ${text}`
|
return `[微博 ${time}] ${text}`
|
||||||
})
|
})
|
||||||
insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`)
|
insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`)
|
||||||
const riskHint = hasCookie
|
return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}`
|
||||||
? ''
|
|
||||||
: '\n提示:未配置微博 Cookie,使用移动端公开接口抓取,可能因平台风控导致获取失败或内容较少。'
|
|
||||||
return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}${riskHint}`
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`)
|
insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`)
|
||||||
return ''
|
return ''
|
||||||
@@ -1118,10 +1160,6 @@ ${topMentionText}
|
|||||||
|
|
||||||
// ── 构建 prompt ────────────────────────────────────────────────────────────
|
// ── 构建 prompt ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// 今日触发统计(让模型具备时间与克制感)
|
|
||||||
const sessionTriggerTimes = this.recordTrigger(sessionId)
|
|
||||||
const totalTodayTriggers = this.getTodayTotalTriggerCount()
|
|
||||||
|
|
||||||
let contextSection = ''
|
let contextSection = ''
|
||||||
if (allowContext) {
|
if (allowContext) {
|
||||||
try {
|
try {
|
||||||
@@ -1136,6 +1174,7 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const momentsContextSection = await this.getMomentsContextSection(sessionId)
|
||||||
const socialContextSection = await this.getSocialContextSection(sessionId)
|
const socialContextSection = await this.getSocialContextSection(sessionId)
|
||||||
|
|
||||||
// ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)────
|
// ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)────
|
||||||
@@ -1151,25 +1190,12 @@ ${topMentionText}
|
|||||||
const customPrompt = (this.config.get('aiInsightSystemPrompt') as string) || ''
|
const customPrompt = (this.config.get('aiInsightSystemPrompt') as string) || ''
|
||||||
const systemPrompt = customPrompt.trim() || DEFAULT_SYSTEM_PROMPT
|
const systemPrompt = customPrompt.trim() || DEFAULT_SYSTEM_PROMPT
|
||||||
|
|
||||||
// 可变的上下文统计信息放在 user message 里,保持 system prompt 稳定不变
|
|
||||||
// 这样 provider 端(Anthropic/OpenAI)能最大化命中 prompt cache,降低费用
|
|
||||||
const triggerDesc =
|
|
||||||
triggerReason === 'silence'
|
|
||||||
? `你已经 ${silentDays} 天没有和「${resolvedDisplayName}」聊天了。`
|
|
||||||
: `你最近和「${resolvedDisplayName}」有新的聊天动态。`
|
|
||||||
|
|
||||||
const todayStatsDesc =
|
|
||||||
sessionTriggerTimes.length > 1
|
|
||||||
? `今天你已经针对「${resolvedDisplayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。`
|
|
||||||
: `今天你还没有针对「${resolvedDisplayName}」发出过见解。`
|
|
||||||
|
|
||||||
const globalStatsDesc = `今天全部联系人合计已触发 ${totalTodayTriggers} 条见解。`
|
|
||||||
|
|
||||||
const userPromptBase = [
|
const userPromptBase = [
|
||||||
`触发原因:${triggerDesc}`,
|
triggerReason === 'silence' && silentDays
|
||||||
`时间统计:${todayStatsDesc}`,
|
? `已 ${silentDays} 天未联系「${resolvedDisplayName}」。`
|
||||||
`全局统计:${globalStatsDesc}`,
|
: '',
|
||||||
contextSection,
|
contextSection,
|
||||||
|
momentsContextSection,
|
||||||
socialContextSection,
|
socialContextSection,
|
||||||
'请给出你的见解(≤80字):'
|
'请给出你的见解(≤80字):'
|
||||||
].filter(Boolean).join('\n\n')
|
].filter(Boolean).join('\n\n')
|
||||||
@@ -1189,7 +1215,7 @@ ${topMentionText}
|
|||||||
`接口地址:${endpoint}`,
|
`接口地址:${endpoint}`,
|
||||||
`模型:${model}`,
|
`模型:${model}`,
|
||||||
`Max Tokens:${maxTokens}`,
|
`Max Tokens:${maxTokens}`,
|
||||||
`触发原因:${triggerReason}`,
|
`触发类型:${triggerReason}`,
|
||||||
`上下文开关:${allowContext ? '开启' : '关闭'}`,
|
`上下文开关:${allowContext ? '开启' : '关闭'}`,
|
||||||
`上下文条数:${contextCount}`,
|
`上下文条数:${contextCount}`,
|
||||||
'',
|
'',
|
||||||
@@ -1253,6 +1279,7 @@ ${topMentionText}
|
|||||||
}
|
}
|
||||||
|
|
||||||
insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`)
|
insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`)
|
||||||
|
this.recordTrigger(sessionId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
insightDebugSection(
|
insightDebugSection(
|
||||||
'ERROR',
|
'ERROR',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { join, dirname } from 'path'
|
import { join, dirname } from 'path'
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
|
||||||
|
import { app } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
|
|
||||||
export interface SessionMessageCacheEntry {
|
export interface SessionMessageCacheEntry {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { existsSync, readdirSync, statSync, readFileSync, appendFileSync, mkdirSync } from 'fs'
|
import { existsSync, readdirSync, statSync, readFileSync, appendFileSync, mkdirSync } from 'fs'
|
||||||
import { pathToFileURL } from 'url'
|
import { pathToFileURL } from 'url'
|
||||||
|
import { app } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { wcdbService } from './wcdbService'
|
import { wcdbService } from './wcdbService'
|
||||||
import { getPathFallback } from './electronRuntime'
|
|
||||||
|
|
||||||
export interface VideoInfo {
|
export interface VideoInfo {
|
||||||
videoUrl?: string // 视频文件路径(用于 readFile)
|
videoUrl?: string // 视频文件路径(用于 readFile)
|
||||||
@@ -45,7 +45,7 @@ class VideoService {
|
|||||||
try {
|
try {
|
||||||
const timestamp = new Date().toISOString()
|
const timestamp = new Date().toISOString()
|
||||||
const metaStr = meta ? ` ${JSON.stringify(meta)}` : ''
|
const metaStr = meta ? ` ${JSON.stringify(meta)}` : ''
|
||||||
const logDir = join(getPathFallback('userData'), 'logs')
|
const logDir = join(app.getPath('userData'), 'logs')
|
||||||
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
|
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
|
||||||
appendFileSync(join(logDir, 'wcdb.log'), `[${timestamp}] [VideoService] ${message}${metaStr}\n`, 'utf8')
|
appendFileSync(join(logDir, 'wcdb.log'), `[${timestamp}] [VideoService] ${message}${metaStr}\n`, 'utf8')
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
import { existsSync, mkdirSync, statSync, unlinkSync, createWriteStream, openSync, writeSync, closeSync } from 'fs'
|
import { existsSync, mkdirSync, statSync, unlinkSync, createWriteStream, openSync, writeSync, closeSync } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import * as http from 'http'
|
import * as http from 'http'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { getPathFallback } from './electronRuntime'
|
|
||||||
|
|
||||||
// Sherpa-onnx 类型定义
|
// Sherpa-onnx 类型定义
|
||||||
type OfflineRecognizer = any
|
type OfflineRecognizer = any
|
||||||
@@ -91,7 +91,7 @@ export class VoiceTranscribeService {
|
|||||||
private resolveModelDir(): string {
|
private resolveModelDir(): string {
|
||||||
const configured = this.configService.get('whisperModelDir') as string | undefined
|
const configured = this.configService.get('whisperModelDir') as string | undefined
|
||||||
if (configured) return configured
|
if (configured) return configured
|
||||||
return join(getPathFallback('documents'), 'WeFlow', 'models', 'sensevoice')
|
return join(app.getPath('documents'), 'WeFlow', 'models', 'sensevoice')
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveModelPath(fileName: string): string {
|
private resolveModelPath(fileName: string): string {
|
||||||
|
|||||||
1
resources/image/README.md
Normal file
1
resources/image/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
> 目前只适配了x64 win32平台,其它平台同样原理,但是代码还没写(
|
||||||
BIN
resources/image/win32/x64/img_helper.dll
Normal file
BIN
resources/image/win32/x64/img_helper.dll
Normal file
Binary file not shown.
6
resources/installer/linux/.gitignore
vendored
Normal file
6
resources/installer/linux/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
*.tar.gz
|
||||||
|
*.tar.xz
|
||||||
|
*.zip
|
||||||
|
src/
|
||||||
|
pkg/
|
||||||
|
weflow-*/
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
.export-date-range-dialog {
|
.export-date-range-dialog {
|
||||||
width: min(480px, calc(100vw - 32px));
|
width: min(480px, calc(100vw - 32px));
|
||||||
max-height: calc(100vh - 64px);
|
max-height: calc(100vh - 80px);
|
||||||
overflow-y: auto;
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
background: var(--bg-secondary-solid, var(--bg-primary));
|
background: var(--bg-secondary-solid, var(--bg-primary));
|
||||||
@@ -21,12 +20,14 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.16);
|
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.16);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-date-range-dialog-header {
|
.export-date-range-dialog-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -35,6 +36,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-date-range-dialog-content {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding-right: 2px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.export-date-range-dialog-close-btn {
|
.export-date-range-dialog-close-btn {
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
@@ -439,6 +460,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-date-range-dialog-btn {
|
.export-date-range-dialog-btn {
|
||||||
|
|||||||
@@ -565,6 +565,7 @@ export function ExportDateRangeDialog({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="export-date-range-dialog-content">
|
||||||
<div className="export-date-range-preset-list">
|
<div className="export-date-range-preset-list">
|
||||||
{EXPORT_DATE_RANGE_PRESETS.map((preset) => {
|
{EXPORT_DATE_RANGE_PRESETS.map((preset) => {
|
||||||
const active = isPresetActive(preset.value)
|
const active = isPresetActive(preset.value)
|
||||||
@@ -728,6 +729,7 @@ export function ExportDateRangeDialog({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="export-date-range-dialog-actions">
|
<div className="export-date-range-dialog-actions">
|
||||||
<button type="button" className="export-date-range-dialog-btn secondary" onClick={onClose}>
|
<button type="button" className="export-date-range-dialog-btn secondary" onClick={onClose}>
|
||||||
|
|||||||
@@ -915,6 +915,31 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insight-collapsible-setting {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translate3d(0, -4px, 0);
|
||||||
|
contain: layout paint;
|
||||||
|
will-change: max-height, opacity, transform;
|
||||||
|
transition: max-height 0.2s ease, opacity 0.18s ease, transform 0.2s ease;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
max-height: 128px;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-collapsible-setting-inner {
|
||||||
|
padding-top: 2px;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* Premium Switch Style */
|
/* Premium Switch Style */
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -3616,17 +3641,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.insight-social-tab {
|
&.insight-social-tab {
|
||||||
|
--insight-moments-column-width: 76px;
|
||||||
|
--insight-social-column-width: minmax(220px, 300px);
|
||||||
|
--insight-status-column-width: 82px;
|
||||||
|
--insight-social-list-grid: minmax(0, 1fr) var(--insight-moments-column-width) var(--insight-social-column-width) var(--insight-status-column-width);
|
||||||
|
|
||||||
.anti-revoke-list-header {
|
.anti-revoke-list-header {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto;
|
grid-template-columns: var(--insight-social-list-grid);
|
||||||
|
gap: 14px;
|
||||||
|
|
||||||
|
.insight-moments-column-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
.insight-social-column-title {
|
.insight-social-column-title {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anti-revoke-status-column-title {
|
||||||
|
justify-self: end;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.anti-revoke-row {
|
.anti-revoke-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto;
|
grid-template-columns: var(--insight-social-list-grid);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
@@ -3635,6 +3678,67 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insight-moments-cell {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-moments-toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-indicator {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-color) 78%, var(--primary) 22%);
|
||||||
|
background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary) 14%);
|
||||||
|
color: var(--on-primary, #fff);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.16s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.75);
|
||||||
|
transition: opacity 0.16s ease, transform 0.16s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox']:checked + .check-indicator {
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox']:focus-visible + .check-indicator {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--primary) 42%, transparent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.insight-social-binding-cell {
|
.insight-social-binding-cell {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -3653,7 +3757,7 @@
|
|||||||
.binding-platform-chip {
|
.binding-platform-chip {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 2px 8px;
|
padding: 2px 7px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
|
||||||
@@ -3663,7 +3767,7 @@
|
|||||||
.insight-social-binding-input {
|
.insight-social-binding-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 30px;
|
height: 28px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary) 8%);
|
background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary) 8%);
|
||||||
@@ -3706,9 +3810,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.anti-revoke-row-status {
|
.anti-revoke-row-status {
|
||||||
justify-self: flex-end;
|
justify-self: end;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3752,6 +3857,7 @@
|
|||||||
.anti-revoke-list-header {
|
.anti-revoke-list-header {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
|
||||||
|
.insight-moments-column-title,
|
||||||
.insight-social-column-title {
|
.insight-social-column-title {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -3763,11 +3869,16 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insight-moments-cell,
|
||||||
.insight-social-binding-cell,
|
.insight-social-binding-cell,
|
||||||
.anti-revoke-row-status {
|
.anti-revoke-row-status {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insight-moments-cell {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.insight-social-binding-cell {
|
.insight-social-binding-cell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type SettingsTab =
|
|||||||
| 'aiCommon'
|
| 'aiCommon'
|
||||||
| 'insight'
|
| 'insight'
|
||||||
| 'aiFootprint'
|
| 'aiFootprint'
|
||||||
|
| 'autoDownload'
|
||||||
|
|
||||||
const tabs: { id: Exclude<SettingsTab, 'insight' | 'aiFootprint'>; label: string; icon: React.ElementType }[] = [
|
const tabs: { id: Exclude<SettingsTab, 'insight' | 'aiFootprint'>; label: string; icon: React.ElementType }[] = [
|
||||||
{ id: 'appearance', label: '外观', icon: Palette },
|
{ id: 'appearance', label: '外观', icon: Palette },
|
||||||
@@ -39,6 +40,7 @@ const tabs: { id: Exclude<SettingsTab, 'insight' | 'aiFootprint'>; label: string
|
|||||||
{ id: 'antiRevoke', label: '防撤回', icon: RotateCcw },
|
{ id: 'antiRevoke', label: '防撤回', icon: RotateCcw },
|
||||||
{ id: 'database', label: '数据库连接', icon: Database },
|
{ id: 'database', label: '数据库连接', icon: Database },
|
||||||
{ id: 'models', label: '模型管理', icon: Mic },
|
{ id: 'models', label: '模型管理', icon: Mic },
|
||||||
|
{ id: 'autoDownload', label: '自动下载', icon: Download },
|
||||||
{ id: 'cache', label: '缓存', icon: HardDrive },
|
{ id: 'cache', label: '缓存', icon: HardDrive },
|
||||||
{ id: 'api', label: 'API 服务', icon: Globe },
|
{ id: 'api', label: 'API 服务', icon: Globe },
|
||||||
{ id: 'analytics', label: '分析', icon: BarChart2 },
|
{ id: 'analytics', label: '分析', icon: BarChart2 },
|
||||||
@@ -47,6 +49,13 @@ const tabs: { id: Exclude<SettingsTab, 'insight' | 'aiFootprint'>; label: string
|
|||||||
{ id: 'about', label: '关于', icon: Info }
|
{ id: 'about', label: '关于', icon: Info }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const filteredTabs = tabs.filter(tab => {
|
||||||
|
if (tab.id === 'autoDownload') {
|
||||||
|
return (window as any).electronAPI.process.platform === 'win32' && (window as any).electronAPI.process.arch === 'x64'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
const aiTabs: Array<{ id: Extract<SettingsTab, 'aiCommon' | 'insight' | 'aiFootprint'>; label: string }> = [
|
const aiTabs: Array<{ id: Extract<SettingsTab, 'aiCommon' | 'insight' | 'aiFootprint'>; label: string }> = [
|
||||||
{ id: 'aiCommon', label: '基础配置' },
|
{ id: 'aiCommon', label: '基础配置' },
|
||||||
{ id: 'insight', label: 'AI 见解' },
|
{ id: 'insight', label: 'AI 见解' },
|
||||||
@@ -149,6 +158,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const [imageKeyPercent, setImageKeyPercent] = useState<number | null>(null)
|
const [imageKeyPercent, setImageKeyPercent] = useState<number | null>(null)
|
||||||
|
|
||||||
const [logEnabled, setLogEnabled] = useState(false)
|
const [logEnabled, setLogEnabled] = useState(false)
|
||||||
|
const [autoDownloadHighRes, setAutoDownloadHighRes] = useState(false)
|
||||||
const [whisperModelName, setWhisperModelName] = useState('base')
|
const [whisperModelName, setWhisperModelName] = useState('base')
|
||||||
const [whisperModelDir, setWhisperModelDir] = useState('')
|
const [whisperModelDir, setWhisperModelDir] = useState('')
|
||||||
const [isWhisperDownloading, setIsWhisperDownloading] = useState(false)
|
const [isWhisperDownloading, setIsWhisperDownloading] = useState(false)
|
||||||
@@ -284,6 +294,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const [aiModelApiMaxTokens, setAiModelApiMaxTokens] = useState(200)
|
const [aiModelApiMaxTokens, setAiModelApiMaxTokens] = useState(200)
|
||||||
const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3)
|
const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3)
|
||||||
const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false)
|
const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false)
|
||||||
|
const [aiInsightAllowMomentsContext, setAiInsightAllowMomentsContext] = useState(false)
|
||||||
|
const [aiInsightMomentsContextCount, setAiInsightMomentsContextCount] = useState(5)
|
||||||
|
const [aiInsightMomentsBindings, setAiInsightMomentsBindings] = useState<Record<string, configService.AiInsightMomentsBinding>>({})
|
||||||
const [isTestingInsight, setIsTestingInsight] = useState(false)
|
const [isTestingInsight, setIsTestingInsight] = useState(false)
|
||||||
const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||||
const [showInsightApiKey, setShowInsightApiKey] = useState(false)
|
const [showInsightApiKey, setShowInsightApiKey] = useState(false)
|
||||||
@@ -315,6 +328,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
|
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
|
||||||
const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false)
|
const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false)
|
||||||
|
|
||||||
|
// 自动下载图片
|
||||||
|
const [autoDownloadStatus, setAutoDownloadStatus] = useState<{ isHooked: boolean; pid: number | null; supported: boolean } | null>(null)
|
||||||
|
const [autoDownloadSelectedIds, setAutoDownloadSelectedIds] = useState<Set<string>>(new Set())
|
||||||
|
const [autoDownloadSearchKeyword, setAutoDownloadSearchKeyword] = useState('')
|
||||||
|
|
||||||
// 检查 Hello 可用性
|
// 检查 Hello 可用性
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHelloAvailable(isWindows)
|
setHelloAvailable(isWindows)
|
||||||
@@ -526,9 +544,12 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setWordCloudExcludeWords(savedExcludeWords)
|
setWordCloudExcludeWords(savedExcludeWords)
|
||||||
setExcludeWordsInput(savedExcludeWords.join('\n'))
|
setExcludeWordsInput(savedExcludeWords.join('\n'))
|
||||||
|
|
||||||
|
const savedAutoDownloadHighRes = await configService.getAutoDownloadHighRes()
|
||||||
|
const savedAutoDownloadWhitelist = await configService.getAutoDownloadWhitelist()
|
||||||
const savedAnalyticsConsent = await configService.getAnalyticsConsent()
|
const savedAnalyticsConsent = await configService.getAnalyticsConsent()
|
||||||
setAnalyticsConsent(savedAnalyticsConsent ?? false)
|
setAnalyticsConsent(savedAnalyticsConsent ?? false)
|
||||||
|
setAutoDownloadHighRes(savedAutoDownloadHighRes)
|
||||||
|
setAutoDownloadSelectedIds(new Set(savedAutoDownloadWhitelist))
|
||||||
|
|
||||||
|
|
||||||
// 如果语言列表为空,保存默认值
|
// 如果语言列表为空,保存默认值
|
||||||
@@ -549,6 +570,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const savedAiModelApiMaxTokens = await configService.getAiModelApiMaxTokens()
|
const savedAiModelApiMaxTokens = await configService.getAiModelApiMaxTokens()
|
||||||
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
|
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
|
||||||
const savedAiInsightAllowContext = await configService.getAiInsightAllowContext()
|
const savedAiInsightAllowContext = await configService.getAiInsightAllowContext()
|
||||||
|
const savedAiInsightAllowMomentsContext = await configService.getAiInsightAllowMomentsContext()
|
||||||
|
const savedAiInsightMomentsContextCount = await configService.getAiInsightMomentsContextCount()
|
||||||
|
const savedAiInsightMomentsBindings = await configService.getAiInsightMomentsBindings()
|
||||||
const savedAiInsightFilterMode = await configService.getAiInsightFilterMode()
|
const savedAiInsightFilterMode = await configService.getAiInsightFilterMode()
|
||||||
const savedAiInsightFilterList = await configService.getAiInsightFilterList()
|
const savedAiInsightFilterList = await configService.getAiInsightFilterList()
|
||||||
const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes()
|
const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes()
|
||||||
@@ -573,6 +597,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setAiModelApiMaxTokens(savedAiModelApiMaxTokens)
|
setAiModelApiMaxTokens(savedAiModelApiMaxTokens)
|
||||||
setAiInsightSilenceDays(savedAiInsightSilenceDays)
|
setAiInsightSilenceDays(savedAiInsightSilenceDays)
|
||||||
setAiInsightAllowContext(savedAiInsightAllowContext)
|
setAiInsightAllowContext(savedAiInsightAllowContext)
|
||||||
|
setAiInsightAllowMomentsContext(savedAiInsightAllowMomentsContext)
|
||||||
|
setAiInsightMomentsContextCount(savedAiInsightMomentsContextCount)
|
||||||
|
setAiInsightMomentsBindings(savedAiInsightMomentsBindings)
|
||||||
setAiInsightFilterMode(savedAiInsightFilterMode)
|
setAiInsightFilterMode(savedAiInsightFilterMode)
|
||||||
setAiInsightFilterList(new Set(savedAiInsightFilterList))
|
setAiInsightFilterList(new Set(savedAiInsightFilterList))
|
||||||
setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes)
|
setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes)
|
||||||
@@ -685,6 +712,21 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
void refreshWhisperStatus(whisperModelDir)
|
void refreshWhisperStatus(whisperModelDir)
|
||||||
}, [whisperModelDir])
|
}, [whisperModelDir])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 'autoDownload') {
|
||||||
|
fetchAutoDownloadStatus()
|
||||||
|
|
||||||
|
let interval: ReturnType<typeof setInterval> | undefined
|
||||||
|
if (autoDownloadHighRes) {
|
||||||
|
interval = setInterval(fetchAutoDownloadStatus, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (interval) clearInterval(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeTab, autoDownloadHighRes])
|
||||||
|
|
||||||
const getErrorMessage = (error: any): string => {
|
const getErrorMessage = (error: any): string => {
|
||||||
const raw = typeof error?.message === 'string' ? error.message : String(error ?? '')
|
const raw = typeof error?.message === 'string' ? error.message : String(error ?? '')
|
||||||
const normalized = raw.replace(/^Error:\s*/i, '').trim()
|
const normalized = raw.replace(/^Error:\s*/i, '').trim()
|
||||||
@@ -1013,11 +1055,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab !== 'antiRevoke' && activeTab !== 'insight') return
|
if (activeTab !== 'antiRevoke' && activeTab !== 'insight' && activeTab !== 'autoDownload') return
|
||||||
let canceled = false
|
let canceled = false
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
if (activeTab === 'antiRevoke') {
|
if (activeTab === 'antiRevoke' || activeTab === 'autoDownload') {
|
||||||
await ensureAntiRevokeSessionsLoaded()
|
await ensureAntiRevokeSessionsLoaded()
|
||||||
} else {
|
} else {
|
||||||
await ensureChatSessionsLoaded()
|
await ensureChatSessionsLoaded()
|
||||||
@@ -1579,6 +1621,15 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchAutoDownloadStatus = async () => {
|
||||||
|
try {
|
||||||
|
const status = await (window as any).electronAPI.image.getAutoDownloadStatus()
|
||||||
|
setAutoDownloadStatus(status)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取自动下载状态失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const renderAppearanceTab = () => (
|
const renderAppearanceTab = () => (
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
<div className="theme-mode-toggle">
|
<div className="theme-mode-toggle">
|
||||||
@@ -3081,6 +3132,24 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMomentsEnabledForSession = (sessionId: string): boolean => {
|
||||||
|
return aiInsightMomentsBindings[sessionId]?.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleMomentsBinding = async (sessionId: string, enabled: boolean) => {
|
||||||
|
const nextBindings = { ...aiInsightMomentsBindings }
|
||||||
|
if (enabled) {
|
||||||
|
nextBindings[sessionId] = {
|
||||||
|
enabled: true,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete nextBindings[sessionId]
|
||||||
|
}
|
||||||
|
setAiInsightMomentsBindings(nextBindings)
|
||||||
|
await configService.setAiInsightMomentsBindings(nextBindings)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveWeiboBinding = async (sessionId: string, displayName: string) => {
|
const handleSaveWeiboBinding = async (sessionId: string, displayName: string) => {
|
||||||
const draftUid = getWeiboBindingDraftValue(sessionId)
|
const draftUid = getWeiboBindingDraftValue(sessionId)
|
||||||
setWeiboBindingLoadingSessionId(sessionId)
|
setWeiboBindingLoadingSessionId(sessionId)
|
||||||
@@ -3274,7 +3343,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<span className="form-hint">
|
<span className="form-hint">
|
||||||
开启后,触发见解时会将该联系人最近 N 条聊天记录发送给 AI,分析质量显著提升。
|
开启后,触发见解时会将该联系人最近 N 条聊天记录发送给 AI,分析质量显著提升。
|
||||||
<br />
|
<br />
|
||||||
<strong>关闭时</strong>:AI 仅知道统计摘要(沉默天数等),输出质量较低。
|
<strong>关闭时</strong>:不会发送聊天原文,输出质量较低。
|
||||||
<br />
|
<br />
|
||||||
<strong>开启时</strong>:聊天文本内容(不含图片、语音)会通过你配置的 API 发送给模型提供商。请确认你信任该服务商。
|
<strong>开启时</strong>:聊天文本内容(不含图片、语音)会通过你配置的 API 发送给模型提供商。请确认你信任该服务商。
|
||||||
</span>
|
</span>
|
||||||
@@ -3295,27 +3364,79 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aiInsightAllowContext && (
|
<div className={`insight-collapsible-setting ${aiInsightAllowContext ? 'expanded' : 'collapsed'}`} aria-hidden={!aiInsightAllowContext}>
|
||||||
<div className="form-group">
|
<div className="insight-collapsible-setting-inner">
|
||||||
<label>发送近期对话条数</label>
|
<div className="form-group">
|
||||||
<span className="form-hint">
|
<label>发送近期对话条数</label>
|
||||||
发送给 AI 的聊天记录最大条数。条数越多分析越准确,token 消耗也越多。
|
<span className="form-hint">
|
||||||
</span>
|
发送给 AI 的聊天记录最大条数。条数越多分析越准确,token 消耗也越多。
|
||||||
<input
|
</span>
|
||||||
type="number"
|
<input
|
||||||
className="field-input"
|
type="number"
|
||||||
value={aiInsightContextCount}
|
className="field-input"
|
||||||
min={1}
|
value={aiInsightContextCount}
|
||||||
max={200}
|
min={1}
|
||||||
onChange={(e) => {
|
max={200}
|
||||||
const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40))
|
disabled={!aiInsightAllowContext}
|
||||||
setAiInsightContextCount(val)
|
onChange={(e) => {
|
||||||
scheduleConfigSave('aiInsightContextCount', () => configService.setAiInsightContextCount(val))
|
const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40))
|
||||||
}}
|
setAiInsightContextCount(val)
|
||||||
style={{ width: 100 }}
|
scheduleConfigSave('aiInsightContextCount', () => configService.setAiInsightContextCount(val))
|
||||||
/>
|
}}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
<div className="divider" />
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>允许发送近期朋友圈内容用于分析(实验性)</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
开启后,可在下方列表为私聊联系人单独允许朋友圈补充分析。程序只会在触发见解时按需读取,不会做后台持续扫描。
|
||||||
|
</span>
|
||||||
|
<div className="log-toggle-line">
|
||||||
|
<span className="log-status">{aiInsightAllowMomentsContext ? '已开启' : '已关闭'}</span>
|
||||||
|
<label className="switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aiInsightAllowMomentsContext}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const val = e.target.checked
|
||||||
|
setAiInsightAllowMomentsContext(val)
|
||||||
|
await configService.setAiInsightAllowMomentsContext(val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="switch-slider" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`insight-collapsible-setting ${aiInsightAllowMomentsContext ? 'expanded' : 'collapsed'}`} aria-hidden={!aiInsightAllowMomentsContext}>
|
||||||
|
<div className="insight-collapsible-setting-inner">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>发送近期朋友圈条数</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
发送给 AI 的朋友圈最大条数。条数越多上下文越充分,token 消耗也越多。
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="field-input"
|
||||||
|
value={aiInsightMomentsContextCount}
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
disabled={!aiInsightAllowMomentsContext}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = Math.max(1, Math.min(20, parseInt(e.target.value, 10) || 5))
|
||||||
|
setAiInsightMomentsContextCount(val)
|
||||||
|
scheduleConfigSave('aiInsightMomentsContextCount', () => configService.setAiInsightMomentsContextCount(val))
|
||||||
|
}}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
|
|
||||||
@@ -3354,29 +3475,32 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aiInsightAllowSocialContext && (
|
<div className={`insight-collapsible-setting ${aiInsightAllowSocialContext ? 'expanded' : 'collapsed'}`} aria-hidden={!aiInsightAllowSocialContext}>
|
||||||
<div className="form-group">
|
<div className="insight-collapsible-setting-inner">
|
||||||
<label>发送近期社交平台内容条数</label>
|
<div className="form-group">
|
||||||
<span className="form-hint">
|
<label>发送近期社交平台内容条数</label>
|
||||||
当前仅支持微博最近发帖。
|
<span className="form-hint">
|
||||||
<br />
|
当前仅支持微博最近发帖。
|
||||||
<strong>不建议超过 5,避免触发平台风控。</strong>
|
<br />
|
||||||
</span>
|
<strong>不建议超过 5,避免触发平台风控。</strong>
|
||||||
<input
|
</span>
|
||||||
type="number"
|
<input
|
||||||
className="field-input"
|
type="number"
|
||||||
value={aiInsightSocialContextCount}
|
className="field-input"
|
||||||
min={1}
|
value={aiInsightSocialContextCount}
|
||||||
max={5}
|
min={1}
|
||||||
onChange={(e) => {
|
max={5}
|
||||||
const val = Math.max(1, Math.min(5, parseInt(e.target.value, 10) || 3))
|
disabled={!aiInsightAllowSocialContext}
|
||||||
setAiInsightSocialContextCount(val)
|
onChange={(e) => {
|
||||||
scheduleConfigSave('aiInsightSocialContextCount', () => configService.setAiInsightSocialContextCount(val))
|
const val = Math.max(1, Math.min(5, parseInt(e.target.value, 10) || 3))
|
||||||
}}
|
setAiInsightSocialContextCount(val)
|
||||||
style={{ width: 100 }}
|
scheduleConfigSave('aiInsightSocialContextCount', () => configService.setAiInsightSocialContextCount(val))
|
||||||
/>
|
}}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
{/* 自定义 System Prompt */}
|
{/* 自定义 System Prompt */}
|
||||||
@@ -3652,11 +3776,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<>
|
<>
|
||||||
<div className="anti-revoke-list-header">
|
<div className="anti-revoke-list-header">
|
||||||
<span>对话({filteredSessions.length})</span>
|
<span>对话({filteredSessions.length})</span>
|
||||||
|
<span className="insight-moments-column-title">朋友圈</span>
|
||||||
<span className="insight-social-column-title">社交平台(微博)</span>
|
<span className="insight-social-column-title">社交平台(微博)</span>
|
||||||
<span>状态</span>
|
<span className="anti-revoke-status-column-title">状态</span>
|
||||||
</div>
|
</div>
|
||||||
{filteredSessions.map((session) => {
|
{filteredSessions.map((session) => {
|
||||||
const isSelected = aiInsightFilterList.has(session.username)
|
const isSelected = aiInsightFilterList.has(session.username)
|
||||||
|
const isPrivateSession = session.type === 'private'
|
||||||
|
const isMomentsEnabled = isMomentsEnabledForSession(session.username)
|
||||||
const weiboBinding = aiInsightWeiboBindings[session.username]
|
const weiboBinding = aiInsightWeiboBindings[session.username]
|
||||||
const weiboDraftValue = getWeiboBindingDraftValue(session.username)
|
const weiboDraftValue = getWeiboBindingDraftValue(session.username)
|
||||||
const isBindingLoading = weiboBindingLoadingSessionId === session.username
|
const isBindingLoading = weiboBindingLoadingSessionId === session.username
|
||||||
@@ -3695,8 +3822,24 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<span className="desc">{getSessionFilterTypeLabel(session.type)}</span>
|
<span className="desc">{getSessionFilterTypeLabel(session.type)}</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<div className="insight-moments-cell">
|
||||||
|
{isPrivateSession ? (
|
||||||
|
<label className="insight-moments-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isMomentsEnabled}
|
||||||
|
onChange={(e) => { void handleToggleMomentsBinding(session.username, e.target.checked) }}
|
||||||
|
/>
|
||||||
|
<span className="check-indicator" aria-hidden="true">
|
||||||
|
<Check size={12} />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<span className="binding-feedback muted">-</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="insight-social-binding-cell">
|
<div className="insight-social-binding-cell">
|
||||||
{session.type === 'private' ? (
|
{isPrivateSession ? (
|
||||||
<>
|
<>
|
||||||
<div className="insight-social-binding-input-wrap">
|
<div className="insight-social-binding-input-wrap">
|
||||||
<span className="binding-platform-chip">微博</span>
|
<span className="binding-platform-chip">微博</span>
|
||||||
@@ -3771,9 +3914,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<div className="api-docs">
|
<div className="api-docs">
|
||||||
<div className="api-item">
|
<div className="api-item">
|
||||||
<p className="api-desc" style={{ lineHeight: 1.7 }}>
|
<p className="api-desc" style={{ lineHeight: 1.7 }}>
|
||||||
<strong>触发方式一:活跃会话分析</strong> — 每当微信数据库变化(即你收到新消息)时,经过 500ms 防抖后,对符合黑白名单规则的活跃会话进行分析。<br />
|
<strong>触发方式一:活跃会话分析</strong> — 每当微信数据库变化(即你收到新消息)时,经过约 2 秒防抖后,对符合黑白名单规则的活跃会话进行分析。<br />
|
||||||
<strong>触发方式二:沉默扫描</strong> — 每 4 小时独立扫描一次,对超过阈值天数无消息的联系人发出提醒。<br />
|
<strong>触发方式二:沉默扫描</strong> — 每 4 小时独立扫描一次,对超过阈值天数无消息的联系人发出提醒。<br />
|
||||||
<strong>时间观念</strong> — 每次调用时,AI 会收到今天已向该联系人和全局发出过多少次见解,由 AI 自行决定是否需要克制。<br />
|
<strong>频率控制</strong> — 冷却期、沉默间隔、黑白名单均在本地判断,不额外发送给模型。<br />
|
||||||
<strong>隐私</strong> — 所有分析请求均直接从你的电脑发往你填写的 API 地址,不经过任何 WeFlow 服务器。
|
<strong>隐私</strong> — 所有分析请求均直接从你的电脑发往你填写的 API 地址,不经过任何 WeFlow 服务器。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -4557,6 +4700,203 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderAutoDownloadTab = () => {
|
||||||
|
const sortedSessions = [...antiRevokeSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0))
|
||||||
|
const keyword = autoDownloadSearchKeyword.trim().toLowerCase()
|
||||||
|
const filteredSessions = sortedSessions.filter((session) => {
|
||||||
|
if (!keyword) return true
|
||||||
|
const displayName = String(session.displayName || '').toLowerCase()
|
||||||
|
const username = String(session.username || '').toLowerCase()
|
||||||
|
return displayName.includes(keyword) || username.includes(keyword)
|
||||||
|
})
|
||||||
|
const filteredSessionIds = filteredSessions.map((session) => session.username)
|
||||||
|
const selectedCount = autoDownloadSelectedIds.size
|
||||||
|
const selectedInFilteredCount = filteredSessionIds.filter((id) => autoDownloadSelectedIds.has(id)).length
|
||||||
|
const allFilteredSelected = filteredSessionIds.length > 0 && selectedInFilteredCount === filteredSessionIds.length
|
||||||
|
const isHooked = autoDownloadStatus?.isHooked
|
||||||
|
|
||||||
|
const persistWhitelist = (ids: Set<string>) => {
|
||||||
|
const whitelistArr = Array.from(ids)
|
||||||
|
configService.setAutoDownloadWhitelist(whitelistArr)
|
||||||
|
if (autoDownloadHighRes) {
|
||||||
|
const whitelistStr = whitelistArr.length > 0 ? (whitelistArr.join('\0') + '\0\0') : '';
|
||||||
|
(window as any).electronAPI.image.startAutoDownload(whitelistStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelection = (id: string) => {
|
||||||
|
const next = new Set(autoDownloadSelectedIds)
|
||||||
|
if (next.has(id)) next.delete(id)
|
||||||
|
else next.add(id)
|
||||||
|
setAutoDownloadSelectedIds(next)
|
||||||
|
persistWhitelist(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAllFiltered = () => {
|
||||||
|
const next = new Set(autoDownloadSelectedIds)
|
||||||
|
filteredSessionIds.forEach(id => next.add(id))
|
||||||
|
setAutoDownloadSelectedIds(next)
|
||||||
|
persistWhitelist(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
const next = new Set<string>()
|
||||||
|
setAutoDownloadSelectedIds(next)
|
||||||
|
persistWhitelist(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tab-content anti-revoke-tab">
|
||||||
|
{/* 顶部 Hero 区域保持不变 */}
|
||||||
|
<div className="anti-revoke-hero" style={{ background: 'linear-gradient(110deg, var(--bg-primary) 0%, rgba(245, 158, 11, 0.1) 100%)', borderColor: 'rgba(245, 158, 11, 0.3)' }}>
|
||||||
|
<div className="anti-revoke-hero-main">
|
||||||
|
<span className="updates-chip" style={{ color: '#f59e0b', background: 'rgba(245, 158, 11, 0.15)', width: 'fit-content' }}>测试功能 (Test)</span>
|
||||||
|
<h2 style={{ marginTop: '8px' }}>自动下载原图</h2>
|
||||||
|
<p>强制微信在接收图片时下载高清原图。建议仅在必要会话中开启以节省流量和空间。</p>
|
||||||
|
</div>
|
||||||
|
<div className="anti-revoke-metrics">
|
||||||
|
<div className={`anti-revoke-metric ${isHooked ? 'is-installed' : 'is-pending'}`}>
|
||||||
|
<span className="label">服务状态</span>
|
||||||
|
<span className="value" style={{ fontSize: '14px' }}>
|
||||||
|
{isHooked ? '正在监控' : autoDownloadHighRes ? '等待连接' : '未启用'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="anti-revoke-metric">
|
||||||
|
<span className="label">已选会话</span>
|
||||||
|
<span className="value">{selectedCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="anti-revoke-control-card">
|
||||||
|
<div className="anti-revoke-toolbar">
|
||||||
|
<div className="filter-search-box anti-revoke-search">
|
||||||
|
<Search size={14} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索联系人或群聊..."
|
||||||
|
value={autoDownloadSearchKeyword}
|
||||||
|
onChange={(e) => setAutoDownloadSearchKeyword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="anti-revoke-toolbar-actions">
|
||||||
|
<div className="anti-revoke-btn-group">
|
||||||
|
<button className="btn btn-secondary btn-sm" onClick={selectAllFiltered} disabled={filteredSessionIds.length === 0 || allFilteredSelected}>
|
||||||
|
全选
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary btn-sm" onClick={clearSelection} disabled={selectedCount === 0}>
|
||||||
|
清空选择
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="anti-revoke-btn-group" style={{ marginLeft: '12px', paddingLeft: '12px', borderLeft: '1px solid var(--border-color)' }}>
|
||||||
|
<label className="switch switch-md">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoDownloadHighRes}
|
||||||
|
onChange={() => handleToggleAutoDownload(Array.from(autoDownloadSelectedIds))}
|
||||||
|
/>
|
||||||
|
<span className="switch-slider" />
|
||||||
|
</label>
|
||||||
|
<span style={{ fontSize: '12px', color: 'var(--text-secondary)', marginLeft: '8px' }}>
|
||||||
|
{autoDownloadHighRes ? '服务已开启' : '服务已关闭'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="anti-revoke-batch-actions">
|
||||||
|
<div className="anti-revoke-selected-count">
|
||||||
|
<span>已选 <strong>{selectedCount}</strong> 个目标会话</span>
|
||||||
|
<span style={{ opacity: 0.6 }}>(若不选则默认对所有聊天生效)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="anti-revoke-list">
|
||||||
|
<div className="anti-revoke-list-header">
|
||||||
|
<span>会话({filteredSessions.length})</span>
|
||||||
|
<span>状态</span>
|
||||||
|
</div>
|
||||||
|
{filteredSessions.length === 0 ? (
|
||||||
|
<div className="anti-revoke-empty">{autoDownloadSearchKeyword ? '没有匹配的会话' : '暂无会话'}</div>
|
||||||
|
) : (
|
||||||
|
filteredSessions.map((session) => {
|
||||||
|
const isSelected = autoDownloadSelectedIds.has(session.username)
|
||||||
|
return (
|
||||||
|
<div key={session.username} className={`anti-revoke-row ${isSelected ? 'selected' : ''}`}>
|
||||||
|
<label className="anti-revoke-row-main">
|
||||||
|
<span className="anti-revoke-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => toggleSelection(session.username)}
|
||||||
|
/>
|
||||||
|
<span className="check-indicator" aria-hidden="true">
|
||||||
|
<Check size={12} />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<Avatar src={session.avatarUrl} name={session.displayName} size={30} />
|
||||||
|
<div className="anti-revoke-row-text">
|
||||||
|
<span className="name">{session.displayName || session.username}</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div className="anti-revoke-row-status">
|
||||||
|
<span className={`status-badge ${isSelected ? 'installed' : 'not-installed'}`}>
|
||||||
|
<i className="status-dot" aria-hidden="true" />
|
||||||
|
{isSelected ? '已监控' : '未开启'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 风险提示部分保持不变 */}
|
||||||
|
<div className="api-warning-modal" style={{ width: '100%', border: '1px solid rgba(239, 68, 68, 0.2)', marginTop: '16px', background: 'rgba(239, 68, 68, 0.02)', animation: 'none', boxShadow: 'none', position: 'static' }}>
|
||||||
|
<div className="modal-header" style={{ border: 'none', padding: '12px 20px 0' }}>
|
||||||
|
<Lock size={16} color="#ef4444" />
|
||||||
|
<h3 style={{ fontSize: '13px', color: '#ef4444' }}>风险警告</h3>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body" style={{ fontSize: '12px', color: 'var(--text-secondary)', padding: '8px 20px 12px' }}>
|
||||||
|
此功能通过内存 Hook 修改微信行为,具有一定的风险。请尽量仅在白名单模式下针对必要会话开启。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleToggleAutoDownload = async (whitelist?: string[] | string) => {
|
||||||
|
const newVal = !autoDownloadHighRes
|
||||||
|
setAutoDownloadHighRes(newVal)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (newVal) {
|
||||||
|
let currentWhitelist: string[] | string = whitelist || Array.from(autoDownloadSelectedIds)
|
||||||
|
if (Array.isArray(currentWhitelist)) {
|
||||||
|
currentWhitelist = currentWhitelist.length > 0 ? (currentWhitelist.join('\0') + '\0\0') : ''
|
||||||
|
}
|
||||||
|
const result = await (window as any).electronAPI.image.startAutoDownload(currentWhitelist)
|
||||||
|
if (result && !result.success) {
|
||||||
|
// 如果底层明确返回了失败
|
||||||
|
throw new Error(result.error || '启动自动下载服务失败')
|
||||||
|
}
|
||||||
|
showMessage('自动下载已开启,正在尝试连接微信', true)
|
||||||
|
await fetchAutoDownloadStatus()
|
||||||
|
} else {
|
||||||
|
await (window as any).electronAPI.image.stopAutoDownload()
|
||||||
|
showMessage('自动下载已关闭', true)
|
||||||
|
setAutoDownloadStatus(null)
|
||||||
|
}
|
||||||
|
await configService.setAutoDownloadHighRes(newVal)
|
||||||
|
} catch (e: any) {
|
||||||
|
// 发生错误时,将开关拨回去
|
||||||
|
setAutoDownloadHighRes(!newVal)
|
||||||
|
showMessage(`操作失败: ${e.message || String(e)}`, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const renderUpdatesTab = () => {
|
const renderUpdatesTab = () => {
|
||||||
const downloadPercent = Math.max(0, Math.min(100, Number(downloadProgress?.percent || 0)))
|
const downloadPercent = Math.max(0, Math.min(100, Number(downloadProgress?.percent || 0)))
|
||||||
const channelCards: { id: configService.UpdateChannel; title: string; desc: string }[] = [
|
const channelCards: { id: configService.UpdateChannel; title: string; desc: string }[] = [
|
||||||
@@ -4691,7 +5031,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
|
|
||||||
<div className="settings-layout">
|
<div className="settings-layout">
|
||||||
<div className="settings-tabs" role="tablist" aria-label="设置项">
|
<div className="settings-tabs" role="tablist" aria-label="设置项">
|
||||||
{tabs.flatMap((tab) => {
|
{filteredTabs.flatMap((tab) => {
|
||||||
const row: React.ReactNode[] = [
|
const row: React.ReactNode[] = [
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@@ -4749,6 +5089,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
{activeTab === 'aiCommon' && renderAiCommonTab()}
|
{activeTab === 'aiCommon' && renderAiCommonTab()}
|
||||||
{activeTab === 'insight' && renderInsightTab()}
|
{activeTab === 'insight' && renderInsightTab()}
|
||||||
{activeTab === 'aiFootprint' && renderAiFootprintTab()}
|
{activeTab === 'aiFootprint' && renderAiFootprintTab()}
|
||||||
|
{activeTab === 'autoDownload' && renderAutoDownloadTab()}
|
||||||
{activeTab === 'updates' && renderUpdatesTab()}
|
{activeTab === 'updates' && renderUpdatesTab()}
|
||||||
{activeTab === 'analytics' && renderAnalyticsTab()}
|
{activeTab === 'analytics' && renderAnalyticsTab()}
|
||||||
{activeTab === 'security' && renderSecurityTab()}
|
{activeTab === 'security' && renderSecurityTab()}
|
||||||
|
|||||||
@@ -2015,6 +2015,7 @@
|
|||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
|
||||||
width: 480px;
|
width: 480px;
|
||||||
max-width: 92vw;
|
max-width: 92vw;
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -2062,6 +2063,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ export const CONFIG_KEYS = {
|
|||||||
AI_INSIGHT_API_MODEL: 'aiInsightApiModel',
|
AI_INSIGHT_API_MODEL: 'aiInsightApiModel',
|
||||||
AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays',
|
AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays',
|
||||||
AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext',
|
AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext',
|
||||||
|
AI_INSIGHT_ALLOW_MOMENTS_CONTEXT: 'aiInsightAllowMomentsContext',
|
||||||
|
AI_INSIGHT_MOMENTS_CONTEXT_COUNT: 'aiInsightMomentsContextCount',
|
||||||
|
AI_INSIGHT_MOMENTS_BINDINGS: 'aiInsightMomentsBindings',
|
||||||
AI_INSIGHT_ALLOW_SOCIAL_CONTEXT: 'aiInsightAllowSocialContext',
|
AI_INSIGHT_ALLOW_SOCIAL_CONTEXT: 'aiInsightAllowSocialContext',
|
||||||
AI_INSIGHT_FILTER_MODE: 'aiInsightFilterMode',
|
AI_INSIGHT_FILTER_MODE: 'aiInsightFilterMode',
|
||||||
AI_INSIGHT_FILTER_LIST: 'aiInsightFilterList',
|
AI_INSIGHT_FILTER_LIST: 'aiInsightFilterList',
|
||||||
@@ -116,7 +119,9 @@ export const CONFIG_KEYS = {
|
|||||||
// AI 足迹
|
// AI 足迹
|
||||||
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
||||||
AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt',
|
AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt',
|
||||||
AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled'
|
AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled',
|
||||||
|
AUTO_DOWNLOAD_HIGH_RES: 'autoDownloadHighRes',
|
||||||
|
AUTO_DOWNLOAD_WHITELIST: 'autoDownloadWhitelist'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export interface WxidConfig {
|
export interface WxidConfig {
|
||||||
@@ -132,6 +137,11 @@ export interface AiInsightWeiboBinding {
|
|||||||
updatedAt: number
|
updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AiInsightMomentsBinding {
|
||||||
|
enabled: boolean
|
||||||
|
updatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExportDefaultMediaConfig {
|
export interface ExportDefaultMediaConfig {
|
||||||
images: boolean
|
images: boolean
|
||||||
videos: boolean
|
videos: boolean
|
||||||
@@ -1922,6 +1932,24 @@ export async function setAiInsightAllowContext(allow: boolean): Promise<void> {
|
|||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_CONTEXT, allow)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_CONTEXT, allow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAiInsightAllowMomentsContext(): Promise<boolean> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_MOMENTS_CONTEXT)
|
||||||
|
return value === true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiInsightAllowMomentsContext(allow: boolean): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_MOMENTS_CONTEXT, allow)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAiInsightMomentsContextCount(): Promise<number> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_MOMENTS_CONTEXT_COUNT)
|
||||||
|
return typeof value === 'number' && value > 0 ? value : 5
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiInsightMomentsContextCount(count: number): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_INSIGHT_MOMENTS_CONTEXT_COUNT, count)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAiInsightAllowSocialContext(): Promise<boolean> {
|
export async function getAiInsightAllowSocialContext(): Promise<boolean> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_SOCIAL_CONTEXT)
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_SOCIAL_CONTEXT)
|
||||||
return value === true
|
return value === true
|
||||||
@@ -2067,6 +2095,33 @@ export async function setAiInsightWeiboBindings(bindings: Record<string, AiInsig
|
|||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_WEIBO_BINDINGS, bindings)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_WEIBO_BINDINGS, bindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeAiInsightMomentsBindings = (value: unknown): Record<string, AiInsightMomentsBinding> => {
|
||||||
|
if (!value || typeof value !== 'object') return {}
|
||||||
|
const result: Record<string, AiInsightMomentsBinding> = {}
|
||||||
|
for (const [sessionIdRaw, bindingRaw] of Object.entries(value as Record<string, unknown>)) {
|
||||||
|
const sessionId = String(sessionIdRaw || '').trim()
|
||||||
|
if (!sessionId) continue
|
||||||
|
if (!bindingRaw || typeof bindingRaw !== 'object') continue
|
||||||
|
const bindingObj = bindingRaw as { enabled?: unknown; updatedAt?: unknown }
|
||||||
|
if (bindingObj.enabled !== true) continue
|
||||||
|
const updatedAtRaw = Number(bindingObj.updatedAt)
|
||||||
|
result[sessionId] = {
|
||||||
|
enabled: true,
|
||||||
|
updatedAt: Number.isFinite(updatedAtRaw) && updatedAtRaw > 0 ? Math.floor(updatedAtRaw) : Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAiInsightMomentsBindings(): Promise<Record<string, AiInsightMomentsBinding>> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AI_INSIGHT_MOMENTS_BINDINGS)
|
||||||
|
return normalizeAiInsightMomentsBindings(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAiInsightMomentsBindings(bindings: Record<string, AiInsightMomentsBinding>): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AI_INSIGHT_MOMENTS_BINDINGS, normalizeAiInsightMomentsBindings(bindings))
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAiFootprintEnabled(): Promise<boolean> {
|
export async function getAiFootprintEnabled(): Promise<boolean> {
|
||||||
const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_ENABLED)
|
const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_ENABLED)
|
||||||
return value === true
|
return value === true
|
||||||
@@ -2094,3 +2149,22 @@ export async function setAiInsightDebugLogEnabled(enabled: boolean): Promise<voi
|
|||||||
await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled)
|
await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAutoDownloadHighRes(): Promise<boolean> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AUTO_DOWNLOAD_HIGH_RES)
|
||||||
|
return value === true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAutoDownloadHighRes(enabled: boolean): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.AUTO_DOWNLOAD_HIGH_RES, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAutoDownloadWhitelist(): Promise<string[]> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.AUTO_DOWNLOAD_WHITELIST)
|
||||||
|
return Array.isArray(value) ? value : []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAutoDownloadWhitelist(list: string[]): Promise<void> {
|
||||||
|
const normalized = Array.from(new Set((list || []).map(item => String(item || '').trim()).filter(Boolean)))
|
||||||
|
await config.set(CONFIG_KEYS.AUTO_DOWNLOAD_WHITELIST, normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,77 @@ const handleElectronOnStart = (options: { reload: () => void }) => {
|
|||||||
options.reload()
|
options.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportWorkerElectronShimPlugin = () => {
|
||||||
|
const virtualId = 'virtual:weflow-export-worker-electron'
|
||||||
|
const resolvedVirtualId = `\0${virtualId}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'weflow-export-worker-electron-shim',
|
||||||
|
enforce: 'pre' as const,
|
||||||
|
resolveId(id: string) {
|
||||||
|
if (id === virtualId) return resolvedVirtualId
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
load(id: string) {
|
||||||
|
if (id !== resolvedVirtualId) return null
|
||||||
|
return `
|
||||||
|
import { homedir, tmpdir } from 'os'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
const workerUserDataPath = () => String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim()
|
||||||
|
const appDataPath = () => {
|
||||||
|
if (process.platform === 'win32' && process.env.APPDATA) return process.env.APPDATA
|
||||||
|
if (process.platform === 'darwin') return join(homedir(), 'Library', 'Application Support')
|
||||||
|
return process.env.XDG_CONFIG_HOME || join(homedir(), '.config')
|
||||||
|
}
|
||||||
|
const getPath = (name) => {
|
||||||
|
if (name === 'userData') return workerUserDataPath() || join(appDataPath(), 'WeFlow')
|
||||||
|
if (name === 'documents') return join(homedir(), 'Documents')
|
||||||
|
if (name === 'desktop') return join(homedir(), 'Desktop')
|
||||||
|
if (name === 'downloads') return join(homedir(), 'Downloads')
|
||||||
|
if (name === 'temp') return tmpdir()
|
||||||
|
if (name === 'appData') return appDataPath()
|
||||||
|
return process.cwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const app = {
|
||||||
|
isPackaged: Boolean(process.resourcesPath && process.env.NODE_ENV !== 'development'),
|
||||||
|
getPath,
|
||||||
|
getAppPath: () => process.cwd(),
|
||||||
|
getName: () => 'WeFlow',
|
||||||
|
getVersion: () => process.env.npm_package_version || '0.0.0'
|
||||||
|
}
|
||||||
|
export const BrowserWindow = { getAllWindows: () => [] }
|
||||||
|
export const dialog = { showMessageBox: async () => ({ response: 0, checkboxChecked: false }) }
|
||||||
|
export const shell = { openExternal: async () => false, showItemInFolder: () => {} }
|
||||||
|
export const ipcMain = { on: () => {}, handle: () => {}, removeHandler: () => {} }
|
||||||
|
export const ipcRenderer = { sendSync: () => ({}) }
|
||||||
|
export const safeStorage = {
|
||||||
|
isEncryptionAvailable: () => false,
|
||||||
|
encryptString: (value) => Buffer.from(String(value || ''), 'utf8'),
|
||||||
|
decryptString: (value) => Buffer.isBuffer(value) ? value.toString('utf8') : Buffer.from(value).toString('utf8')
|
||||||
|
}
|
||||||
|
export const Notification = class {
|
||||||
|
static isSupported() { return false }
|
||||||
|
on() { return this }
|
||||||
|
show() {}
|
||||||
|
close() {}
|
||||||
|
}
|
||||||
|
export default { app, BrowserWindow, dialog, shell, ipcMain, ipcRenderer, safeStorage, Notification }
|
||||||
|
`
|
||||||
|
},
|
||||||
|
transform(code: string, id: string) {
|
||||||
|
if (!/\.[cm]?[jt]s$/.test(id)) return null
|
||||||
|
if (!code.includes("'electron'") && !code.includes('"electron"')) return null
|
||||||
|
const next = code
|
||||||
|
.replace(/from\s+(['"])electron\1/g, `from '${virtualId}'`)
|
||||||
|
.replace(/import\s*\(\s*(['"])electron\1\s*\)/g, `import('${virtualId}')`)
|
||||||
|
.replace(/require\s*\(\s*(['"])electron\1\s*\)/g, `require('${virtualId}')`)
|
||||||
|
return next === code ? null : { code: next, map: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './',
|
base: './',
|
||||||
server: {
|
server: {
|
||||||
@@ -142,6 +213,7 @@ export default defineConfig({
|
|||||||
entry: 'electron/exportWorker.ts',
|
entry: 'electron/exportWorker.ts',
|
||||||
onstart: handleElectronOnStart,
|
onstart: handleElectronOnStart,
|
||||||
vite: {
|
vite: {
|
||||||
|
plugins: [exportWorkerElectronShimPlugin()],
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user