mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-05 23:15:59 +00:00
4
.github/workflows/dev-daily-fixed.yml
vendored
4
.github/workflows/dev-daily-fixed.yml
vendored
@@ -81,6 +81,7 @@ jobs:
|
|||||||
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
|
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -131,6 +132,7 @@ jobs:
|
|||||||
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
|
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -178,6 +180,7 @@ jobs:
|
|||||||
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
|
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -225,6 +228,7 @@ jobs:
|
|||||||
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
|
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
|
|||||||
4
.github/workflows/preview-nightly-main.yml
vendored
4
.github/workflows/preview-nightly-main.yml
vendored
@@ -107,6 +107,7 @@ jobs:
|
|||||||
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
|
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -160,6 +161,7 @@ jobs:
|
|||||||
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
|
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -211,6 +213,7 @@ jobs:
|
|||||||
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
|
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -262,6 +265,7 @@ jobs:
|
|||||||
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
|
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -39,6 +39,7 @@ jobs:
|
|||||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
npm version $VERSION --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -95,6 +96,7 @@ jobs:
|
|||||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
npm version $VERSION --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -145,6 +147,7 @@ jobs:
|
|||||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
npm version $VERSION --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
@@ -195,6 +198,7 @@ jobs:
|
|||||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
npm version $VERSION --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Build Frontend & Type Check
|
- name: Build Frontend & Type Check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npx tsc
|
npx tsc
|
||||||
npx vite build
|
npx vite build
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { join, dirname } from 'path'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 强制将本地资源目录添加到 PATH 最前端,确保优先加载本地 DLL
|
* 强制将本地资源目录添加到 PATH 最前端,确保优先加载本地 DLL
|
||||||
* 解决系统中存在冲突版本的 DLL 导致的应用崩溃问题
|
* 解决系统中存在冲突版本的数据服务导致的应用崩溃问题
|
||||||
*/
|
*/
|
||||||
function enforceLocalDllPriority() {
|
function enforceLocalDllPriority() {
|
||||||
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
||||||
@@ -35,5 +35,5 @@ function enforceLocalDllPriority() {
|
|||||||
try {
|
try {
|
||||||
enforceLocalDllPriority()
|
enforceLocalDllPriority()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[WeFlow] Failed to enforce local DLL priority:', e)
|
console.error('[WeFlow] Failed to enforce local service priority:', e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ class ChatService {
|
|||||||
if (this.monitorSetup) return
|
if (this.monitorSetup) return
|
||||||
this.monitorSetup = true
|
this.monitorSetup = true
|
||||||
|
|
||||||
// 使用 C++ DLL 内部的文件监控 (ReadDirectoryChangesW)
|
// 使用 C++数据服务内部的文件监控 (ReadDirectoryChangesW)
|
||||||
// 这种方式更高效,且不占用 JS 线程,并能直接监听 session/message 目录变更
|
// 这种方式更高效,且不占用 JS 线程,并能直接监听 session/message 目录变更
|
||||||
wcdbService.setMonitor((type, json) => {
|
wcdbService.setMonitor((type, json) => {
|
||||||
this.handleSessionStatsMonitorChange(type, json)
|
this.handleSessionStatsMonitorChange(type, json)
|
||||||
@@ -5117,7 +5117,7 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//手动查找 media_*.db 文件(当 WCDB DLL 不支持 listMediaDbs 时的 fallback)
|
//手动查找 media_*.db 文件(当 WCDB数据服务不支持 listMediaDbs 时的 fallback)
|
||||||
private async findMediaDbsManually(): Promise<string[]> {
|
private async findMediaDbsManually(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const dbPath = this.configService.get('dbPath')
|
const dbPath = this.configService.get('dbPath')
|
||||||
@@ -5676,7 +5676,7 @@ class ChatService {
|
|||||||
if (!result.success || !result.contact) return null
|
if (!result.success || !result.contact) return null
|
||||||
const contact = result.contact as Record<string, any>
|
const contact = result.contact as Record<string, any>
|
||||||
let alias = String(contact.alias || contact.Alias || '')
|
let alias = String(contact.alias || contact.Alias || '')
|
||||||
// DLL 有时不返回 alias 字段,补一条直接 SQL 查询兜底
|
//数据服务有时不返回 alias 字段,补一条直接 SQL 查询兜底
|
||||||
if (!alias) {
|
if (!alias) {
|
||||||
try {
|
try {
|
||||||
const aliasResult = await wcdbService.getContactAliasMap([username])
|
const aliasResult = await wcdbService.getContactAliasMap([username])
|
||||||
|
|||||||
@@ -1423,7 +1423,7 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates)
|
return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getGroupNicknamesForRoom dll error:', e)
|
console.error('getGroupNicknamesForRoom service error:', e)
|
||||||
return new Map<string, string>()
|
return new Map<string, string>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ class GroupAnalyticsService {
|
|||||||
}
|
}
|
||||||
return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates)
|
return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getGroupNicknamesForRoom dll error:', e)
|
console.error('getGroupNicknamesForRoom service error:', e)
|
||||||
return new Map<string, string>()
|
return new Map<string, string>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -684,10 +684,7 @@ export class KeyService {
|
|||||||
return { success: false, error: '获取密钥超时', logs }
|
return { success: false, error: '获取密钥超时', logs }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Image Key (通过 DLL 从缓存目录获取 code,用前端 wxid 计算密钥) ---
|
|
||||||
|
|
||||||
private cleanWxid(wxid: string): string {
|
private cleanWxid(wxid: string): string {
|
||||||
// 截断到第二个下划线: wxid_g4pshorcc0r529_da6c → wxid_g4pshorcc0r529
|
|
||||||
const first = wxid.indexOf('_')
|
const first = wxid.indexOf('_')
|
||||||
if (first === -1) return wxid
|
if (first === -1) return wxid
|
||||||
const second = wxid.indexOf('_', first + 1)
|
const second = wxid.indexOf('_', first + 1)
|
||||||
|
|||||||
@@ -1062,14 +1062,14 @@ class SnsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 补全 DLL 返回的评论中缺失的 refNickname
|
* 补全数据服务返回的评论中缺失的 refNickname
|
||||||
* DLL 返回的 refCommentId 是被回复评论的 cmtid
|
*数据服务返回的 refCommentId 是被回复评论的 cmtid
|
||||||
* 评论按 cmtid 从小到大排列,cmtid 从 1 开始递增
|
* 评论按 cmtid 从小到大排列,cmtid 从 1 开始递增
|
||||||
*/
|
*/
|
||||||
private fixCommentRefs(comments: any[]): any[] {
|
private fixCommentRefs(comments: any[]): any[] {
|
||||||
if (!comments || comments.length === 0) return []
|
if (!comments || comments.length === 0) return []
|
||||||
|
|
||||||
// DLL 现在返回完整的评论数据(含 emojis、refNickname)
|
//数据服务现在返回完整的评论数据(含 emojis、refNickname)
|
||||||
// 此处做最终的格式化和兜底补全
|
// 此处做最终的格式化和兜底补全
|
||||||
const idToNickname = new Map<string, string>()
|
const idToNickname = new Map<string, string>()
|
||||||
comments.forEach((c, idx) => {
|
comments.forEach((c, idx) => {
|
||||||
@@ -1140,14 +1140,14 @@ class SnsService {
|
|||||||
} : undefined
|
} : undefined
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// DLL 已返回完整评论数据(含 emojis、refNickname)
|
//数据服务已返回完整评论数据(含 emojis、refNickname)
|
||||||
// 如果 DLL 评论缺少表情包信息,回退到从 rawXml 重新解析
|
// 如果数据服务评论缺少表情包信息,回退到从 rawXml 重新解析
|
||||||
const dllComments: any[] = post.comments || []
|
const dllComments: any[] = post.comments || []
|
||||||
const hasEmojisInDll = dllComments.some((c: any) => c.emojis && c.emojis.length > 0)
|
const hasEmojisInDll = dllComments.some((c: any) => c.emojis && c.emojis.length > 0)
|
||||||
|
|
||||||
let finalComments: any[]
|
let finalComments: any[]
|
||||||
if (dllComments.length > 0 && (hasEmojisInDll || !rawXml)) {
|
if (dllComments.length > 0 && (hasEmojisInDll || !rawXml)) {
|
||||||
// DLL 数据完整,直接使用
|
//数据服务数据完整,直接使用
|
||||||
finalComments = this.fixCommentRefs(dllComments)
|
finalComments = this.fixCommentRefs(dllComments)
|
||||||
} else if (rawXml) {
|
} else if (rawXml) {
|
||||||
// 回退:从 rawXml 重新解析(兼容旧版 DLL)
|
// 回退:从 rawXml 重新解析(兼容旧版 DLL)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export class VoiceTranscribeService {
|
|||||||
console.warn(`[VoiceTranscribe] 未找到 ${platformPkg} 目录,可能导致语音引擎加载失败`)
|
console.warn(`[VoiceTranscribe] 未找到 ${platformPkg} 目录,可能导致语音引擎加载失败`)
|
||||||
}
|
}
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
// Windows: 把 sherpa-onnx DLL 所在目录加到 PATH,否则 native module 找不到依赖
|
// Windows: 把 sherpa-onnx 所在目录加到 PATH,否则 native module 找不到依赖
|
||||||
const existing = env['PATH'] || ''
|
const existing = env['PATH'] || ''
|
||||||
const merged = [...candidates, ...existing.split(';').filter(Boolean)]
|
const merged = [...candidates, ...existing.split(';').filter(Boolean)]
|
||||||
env['PATH'] = Array.from(new Set(merged)).join(';')
|
env['PATH'] = Array.from(new Set(merged)).join(';')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { join, dirname, basename } from 'path'
|
|||||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs'
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs'
|
||||||
import { tmpdir } from 'os'
|
import { tmpdir } from 'os'
|
||||||
|
|
||||||
// DLL 初始化错误信息,用于帮助用户诊断问题
|
//数据服务初始化错误信息,用于帮助用户诊断问题
|
||||||
let lastDllInitError: string | null = null
|
let lastDllInitError: string | null = null
|
||||||
|
|
||||||
export function getLastDllInitError(): string | null {
|
export function getLastDllInitError(): string | null {
|
||||||
@@ -157,7 +157,7 @@ export class WcdbCore {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 DLL 获取动态管道名(含 PID)
|
// 从数据服务获取动态管道名(含 PID)
|
||||||
let pipePath = '\\\\.\\pipe\\weflow_monitor'
|
let pipePath = '\\\\.\\pipe\\weflow_monitor'
|
||||||
if (this.wcdbGetMonitorPipeName) {
|
if (this.wcdbGetMonitorPipeName) {
|
||||||
try {
|
try {
|
||||||
@@ -638,8 +638,8 @@ export class WcdbCore {
|
|||||||
this.writeLog(`[bootstrap] initialize platform=${process.platform} dllPath=${dllPath} resourcesPath=${this.resourcesPath || ''} userDataPath=${this.userDataPath || ''}`, true)
|
this.writeLog(`[bootstrap] initialize platform=${process.platform} dllPath=${dllPath} resourcesPath=${this.resourcesPath || ''} userDataPath=${this.userDataPath || ''}`, true)
|
||||||
|
|
||||||
if (!existsSync(dllPath)) {
|
if (!existsSync(dllPath)) {
|
||||||
console.error('WCDB DLL 不存在:', dllPath)
|
console.error('WCDB数据服务不存在:', dllPath)
|
||||||
this.writeLog(`[bootstrap] initialize failed: dll not found path=${dllPath}`, true)
|
this.writeLog(`[bootstrap] initialize failed:数据服务not found path=${dllPath}`, true)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,7 +694,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
// 尝试多个可能的资源路径
|
// 尝试多个可能的资源路径
|
||||||
const resourcePaths = [
|
const resourcePaths = [
|
||||||
dllDir, // DLL 所在目录
|
dllDir, //数据服务所在目录
|
||||||
dirname(dllDir), // 上级目录
|
dirname(dllDir), // 上级目录
|
||||||
process.resourcesPath, // 打包后 Contents/Resources
|
process.resourcesPath, // 打包后 Contents/Resources
|
||||||
process.resourcesPath ? join(process.resourcesPath as string, 'resources') : null, // Contents/Resources/resources
|
process.resourcesPath ? join(process.resourcesPath as string, 'resources') : null, // Contents/Resources/resources
|
||||||
@@ -1280,7 +1280,7 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打印 DLL 内部日志(仅在出错时调用)
|
* 打印数据服务内部日志(仅在出错时调用)
|
||||||
*/
|
*/
|
||||||
private async printLogs(force = false): Promise<void> {
|
private async printLogs(force = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -1603,7 +1603,7 @@ export class WcdbCore {
|
|||||||
const outPtr = [null as any]
|
const outPtr = [null as any]
|
||||||
const result = this.wcdbGetSessions(this.handle, outPtr)
|
const result = this.wcdbGetSessions(this.handle, outPtr)
|
||||||
|
|
||||||
// DLL 调用后再次让出控制权
|
//数据服务调用后再次让出控制权
|
||||||
await new Promise(resolve => setImmediate(resolve))
|
await new Promise(resolve => setImmediate(resolve))
|
||||||
|
|
||||||
if (result !== 0 || !outPtr[0]) {
|
if (result !== 0 || !outPtr[0]) {
|
||||||
@@ -1958,7 +1958,7 @@ export class WcdbCore {
|
|||||||
const outPtr = [null as any]
|
const outPtr = [null as any]
|
||||||
const result = this.wcdbGetDisplayNames(this.handle, JSON.stringify(usernames), outPtr)
|
const result = this.wcdbGetDisplayNames(this.handle, JSON.stringify(usernames), outPtr)
|
||||||
|
|
||||||
// DLL 调用后再次让出控制权
|
//数据服务调用后再次让出控制权
|
||||||
await new Promise(resolve => setImmediate(resolve))
|
await new Promise(resolve => setImmediate(resolve))
|
||||||
|
|
||||||
if (result !== 0 || !outPtr[0]) {
|
if (result !== 0 || !outPtr[0]) {
|
||||||
@@ -2043,7 +2043,7 @@ export class WcdbCore {
|
|||||||
const outPtr = [null as any]
|
const outPtr = [null as any]
|
||||||
const result = this.wcdbGetAvatarUrls(handle, JSON.stringify(toFetch), outPtr)
|
const result = this.wcdbGetAvatarUrls(handle, JSON.stringify(toFetch), outPtr)
|
||||||
|
|
||||||
// DLL 调用后再次让出控制权
|
//数据服务调用后再次让出控制权
|
||||||
await new Promise(resolve => setImmediate(resolve))
|
await new Promise(resolve => setImmediate(resolve))
|
||||||
|
|
||||||
if (result !== 0 || !outPtr[0]) {
|
if (result !== 0 || !outPtr[0]) {
|
||||||
@@ -2143,7 +2143,7 @@ export class WcdbCore {
|
|||||||
return { success: false, error: 'WCDB 未连接' }
|
return { success: false, error: 'WCDB 未连接' }
|
||||||
}
|
}
|
||||||
if (!this.wcdbGetGroupNicknames) {
|
if (!this.wcdbGetGroupNicknames) {
|
||||||
return { success: false, error: '当前 DLL 版本不支持获取群昵称接口' }
|
return { success: false, error: '当前数据服务版本不支持获取群昵称接口' }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const outPtr = [null as any]
|
const outPtr = [null as any]
|
||||||
@@ -2986,7 +2986,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
async getVoiceData(sessionId: string, createTime: number, candidates: string[], localId: number = 0, svrId: string | number = 0): Promise<{ success: boolean; hex?: string; error?: string }> {
|
async getVoiceData(sessionId: string, createTime: number, candidates: string[], localId: number = 0, svrId: string | number = 0): Promise<{ success: boolean; hex?: string; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbGetVoiceData) return { success: false, error: '当前 DLL 版本不支持获取语音数据' }
|
if (!this.wcdbGetVoiceData) return { success: false, error: '当前数据服务版本不支持获取语音数据' }
|
||||||
try {
|
try {
|
||||||
const outPtr = [null as any]
|
const outPtr = [null as any]
|
||||||
const result = this.wcdbGetVoiceData(this.handle, sessionId, createTime, localId, BigInt(svrId || 0), JSON.stringify(candidates), outPtr)
|
const result = this.wcdbGetVoiceData(this.handle, sessionId, createTime, localId, BigInt(svrId || 0), JSON.stringify(candidates), outPtr)
|
||||||
@@ -3400,7 +3400,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
async searchMessages(keyword: string, sessionId?: string, limit?: number, offset?: number, beginTimestamp?: number, endTimestamp?: number): Promise<{ success: boolean; messages?: any[]; error?: string }> {
|
async searchMessages(keyword: string, sessionId?: string, limit?: number, offset?: number, beginTimestamp?: number, endTimestamp?: number): Promise<{ success: boolean; messages?: any[]; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbSearchMessages) return { success: false, error: '当前 DLL 版本不支持搜索消息' }
|
if (!this.wcdbSearchMessages) return { success: false, error: '当前数据服务版本不支持搜索消息' }
|
||||||
try {
|
try {
|
||||||
const handle = this.handle
|
const handle = this.handle
|
||||||
await new Promise(resolve => setImmediate(resolve))
|
await new Promise(resolve => setImmediate(resolve))
|
||||||
@@ -3430,7 +3430,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
async getSnsTimeline(limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: any[]; error?: string }> {
|
async getSnsTimeline(limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: any[]; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbGetSnsTimeline) return { success: false, error: '当前 DLL 版本不支持获取朋友圈' }
|
if (!this.wcdbGetSnsTimeline) return { success: false, error: '当前数据服务版本不支持获取朋友圈' }
|
||||||
try {
|
try {
|
||||||
const outPtr = [null as any]
|
const outPtr = [null as any]
|
||||||
const usernamesJson = usernames && usernames.length > 0 ? JSON.stringify(usernames) : ''
|
const usernamesJson = usernames && usernames.length > 0 ? JSON.stringify(usernames) : ''
|
||||||
@@ -3522,7 +3522,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
async installMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> {
|
async installMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbInstallMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
if (!this.wcdbInstallMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
|
||||||
const normalizedSessionId = String(sessionId || '').trim()
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
||||||
try {
|
try {
|
||||||
@@ -3547,7 +3547,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
async uninstallMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; error?: string }> {
|
async uninstallMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbUninstallMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
if (!this.wcdbUninstallMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
|
||||||
const normalizedSessionId = String(sessionId || '').trim()
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
||||||
try {
|
try {
|
||||||
@@ -3569,7 +3569,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
async checkMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; installed?: boolean; error?: string }> {
|
async checkMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; installed?: boolean; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbCheckMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
if (!this.wcdbCheckMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
|
||||||
const normalizedSessionId = String(sessionId || '').trim()
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
||||||
try {
|
try {
|
||||||
@@ -3640,7 +3640,7 @@ export class WcdbCore {
|
|||||||
*/
|
*/
|
||||||
async installSnsBlockDeleteTrigger(): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> {
|
async installSnsBlockDeleteTrigger(): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbInstallSnsBlockDeleteTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
if (!this.wcdbInstallSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
|
||||||
try {
|
try {
|
||||||
const outPtr = [null]
|
const outPtr = [null]
|
||||||
const status = this.wcdbInstallSnsBlockDeleteTrigger(this.handle, outPtr)
|
const status = this.wcdbInstallSnsBlockDeleteTrigger(this.handle, outPtr)
|
||||||
@@ -3650,7 +3650,7 @@ export class WcdbCore {
|
|||||||
try { this.wcdbFreeString(outPtr[0]) } catch { }
|
try { this.wcdbFreeString(outPtr[0]) } catch { }
|
||||||
}
|
}
|
||||||
if (status === 1) {
|
if (status === 1) {
|
||||||
// DLL 返回 1 表示已安装
|
//数据服务返回 1 表示已安装
|
||||||
return { success: true, alreadyInstalled: true }
|
return { success: true, alreadyInstalled: true }
|
||||||
}
|
}
|
||||||
if (status !== 0) {
|
if (status !== 0) {
|
||||||
@@ -3667,7 +3667,7 @@ export class WcdbCore {
|
|||||||
*/
|
*/
|
||||||
async uninstallSnsBlockDeleteTrigger(): Promise<{ success: boolean; error?: string }> {
|
async uninstallSnsBlockDeleteTrigger(): Promise<{ success: boolean; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbUninstallSnsBlockDeleteTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
if (!this.wcdbUninstallSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
|
||||||
try {
|
try {
|
||||||
const outPtr = [null]
|
const outPtr = [null]
|
||||||
const status = this.wcdbUninstallSnsBlockDeleteTrigger(this.handle, outPtr)
|
const status = this.wcdbUninstallSnsBlockDeleteTrigger(this.handle, outPtr)
|
||||||
@@ -3690,7 +3690,7 @@ export class WcdbCore {
|
|||||||
*/
|
*/
|
||||||
async checkSnsBlockDeleteTrigger(): Promise<{ success: boolean; installed?: boolean; error?: string }> {
|
async checkSnsBlockDeleteTrigger(): Promise<{ success: boolean; installed?: boolean; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbCheckSnsBlockDeleteTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
if (!this.wcdbCheckSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
|
||||||
try {
|
try {
|
||||||
const outInstalled = [0]
|
const outInstalled = [0]
|
||||||
const status = this.wcdbCheckSnsBlockDeleteTrigger(this.handle, outInstalled)
|
const status = this.wcdbCheckSnsBlockDeleteTrigger(this.handle, outInstalled)
|
||||||
@@ -3705,7 +3705,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
async deleteSnsPost(postId: string): Promise<{ success: boolean; error?: string }> {
|
async deleteSnsPost(postId: string): Promise<{ success: boolean; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbDeleteSnsPost) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
if (!this.wcdbDeleteSnsPost) return { success: false, error: '当前数据服务版本不支持此功能' }
|
||||||
try {
|
try {
|
||||||
const outPtr = [null]
|
const outPtr = [null]
|
||||||
const status = this.wcdbDeleteSnsPost(this.handle, postId, outPtr)
|
const status = this.wcdbDeleteSnsPost(this.handle, postId, outPtr)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class WcdbService {
|
|||||||
// Worker 退出,需要 reject 所有 pending promises
|
// Worker 退出,需要 reject 所有 pending promises
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
console.error('WCDB Worker 异常退出,退出码:', code)
|
console.error('WCDB Worker 异常退出,退出码:', code)
|
||||||
const errorMsg = `Worker 异常退出 (退出码: ${code})。可能是 DLL 加载失败,请检查是否安装了 Visual C++ Redistributable。`
|
const errorMsg = `Worker 异常退出 (退出码: ${code})。可能是数据服务加载失败,请检查是否安装了 Visual C++ Redistributable。`
|
||||||
for (const [id, p] of this.pending) {
|
for (const [id, p] of this.pending) {
|
||||||
p.reject(new Error(errorMsg))
|
p.reject(new Error(errorMsg))
|
||||||
}
|
}
|
||||||
@@ -467,7 +467,7 @@ export class WcdbService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取表情包释义(严格 DLL 接口)
|
* 获取表情包释义(严格数据服务接口)
|
||||||
*/
|
*/
|
||||||
async getEmoticonCaptionStrict(md5: string): Promise<{ success: boolean; caption?: string; error?: string }> {
|
async getEmoticonCaptionStrict(md5: string): Promise<{ success: boolean; caption?: string; error?: string }> {
|
||||||
return this.callWorker('getEmoticonCaptionStrict', { md5 })
|
return this.callWorker('getEmoticonCaptionStrict', { md5 })
|
||||||
@@ -608,7 +608,7 @@ export class WcdbService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 DLL 内部日志
|
* 获取数据服务内部日志
|
||||||
*/
|
*/
|
||||||
async getLogs(): Promise<{ success: boolean; logs?: string[]; error?: string }> {
|
async getLogs(): Promise<{ success: boolean; logs?: string[]; error?: string }> {
|
||||||
return this.callWorker('getLogs')
|
return this.callWorker('getLogs')
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// 如果错误信息包含 VC++ 或 DLL 相关内容,不清除配置,只提示用户
|
// 如果错误信息包含 VC++ 或数据服务相关内容,不清除配置,只提示用户
|
||||||
// 其他错误可能需要重新配置
|
// 其他错误可能需要重新配置
|
||||||
const errorMsg = result.error || ''
|
const errorMsg = result.error || ''
|
||||||
if (errorMsg.includes('Visual C++') ||
|
if (errorMsg.includes('Visual C++') ||
|
||||||
|
|||||||
@@ -2712,43 +2712,76 @@
|
|||||||
|
|
||||||
// 会话详情面板
|
// 会话详情面板
|
||||||
.detail-panel {
|
.detail-panel {
|
||||||
width: 280px;
|
width: clamp(280px, 25vw, 360px);
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
background: var(--card-bg);
|
max-width: 360px;
|
||||||
border-left: 1px solid var(--border-color);
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
color-mix(in srgb, var(--card-bg) 94%, #fff 6%) 0%,
|
||||||
|
var(--card-bg) 100%
|
||||||
|
);
|
||||||
|
border-left: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
|
||||||
|
box-shadow: -14px 0 28px rgba(0, 0, 0, 0.07);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: slideInRight 0.2s ease;
|
animation: slideInRight 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
|
||||||
.detail-header {
|
.detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px;
|
gap: 8px;
|
||||||
|
padding: 14px 14px 12px;
|
||||||
|
background: color-mix(in srgb, var(--card-bg) 92%, #fff 8%);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
|
||||||
|
.detail-title-wrap {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-title-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 4px;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2780,69 +2813,135 @@
|
|||||||
.detail-content {
|
.detail-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: var(--text-tertiary);
|
background: color-mix(in srgb, var(--text-tertiary) 68%, transparent);
|
||||||
border-radius: 2px;
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-overview-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--bg-secondary) 84%, transparent);
|
||||||
|
animation: detailCardEnter 0.24s ease both;
|
||||||
|
|
||||||
|
.detail-overview-avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent);
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-overview-meta {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-overview-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-overview-sub {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section {
|
.detail-section {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
&:last-child {
|
border-radius: 12px;
|
||||||
margin-bottom: 0;
|
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||||
}
|
background: color-mix(in srgb, var(--bg-secondary) 86%, transparent);
|
||||||
|
animation: detailCardEnter 0.24s ease both;
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
text-transform: uppercase;
|
letter-spacing: 0.3px;
|
||||||
letter-spacing: 0.5px;
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
opacity: 0.7;
|
color: var(--primary);
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-stats-meta {
|
.detail-stats-meta {
|
||||||
margin-top: -6px;
|
margin-top: -2px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
|
background: color-mix(in srgb, var(--card-bg) 84%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-section:nth-child(2) {
|
||||||
|
animation-delay: 0.03s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section:nth-child(3) {
|
||||||
|
animation-delay: 0.06s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section:nth-child(4) {
|
||||||
|
animation-delay: 0.09s;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-item {
|
.detail-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px dashed color-mix(in srgb, var(--border-color) 76%, transparent);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
> svg {
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
width: 88px;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
@@ -2851,22 +2950,27 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
line-height: 1.35;
|
||||||
|
|
||||||
&.highlight {
|
&.highlight {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 21px;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-inline-btn {
|
.detail-inline-btn {
|
||||||
border: none;
|
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||||
background: var(--bg-secondary);
|
background: color-mix(in srgb, var(--card-bg) 90%, transparent);
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
border-radius: 6px;
|
border-radius: 999px;
|
||||||
padding: 4px 8px;
|
padding: 5px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.16s ease;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -2874,6 +2978,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2886,12 +2991,12 @@
|
|||||||
height: 22px;
|
height: 22px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0;
|
opacity: 0.2;
|
||||||
transition: opacity 0.15s, color 0.15s, background 0.15s;
|
transition: opacity 0.15s, color 0.15s, background 0.15s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -2907,18 +3012,27 @@
|
|||||||
&:hover .copy-btn {
|
&:hover .copy-btn {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within .copy-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-basic-section .label {
|
||||||
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-list {
|
.table-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-table-placeholder {
|
.detail-table-placeholder {
|
||||||
padding: 10px 12px;
|
padding: 11px 12px;
|
||||||
background: var(--bg-secondary);
|
background: color-mix(in srgb, var(--card-bg) 84%, transparent);
|
||||||
border-radius: 8px;
|
border: 1px dashed color-mix(in srgb, var(--border-color) 76%, transparent);
|
||||||
|
border-radius: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@@ -2928,18 +3042,64 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background: var(--bg-secondary);
|
background: color-mix(in srgb, var(--card-bg) 90%, transparent);
|
||||||
border-radius: 8px;
|
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||||
|
border-radius: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
transition: transform 0.16s ease, border-color 0.16s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: color-mix(in srgb, var(--primary) 26%, var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
.db-name {
|
.db-name {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
max-width: 62%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-count {
|
.table-count {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-detail-panel {
|
||||||
|
.detail-content {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-overview-card {
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.detail-overview-meta {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-overview-close-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--card-bg) 88%, transparent);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.16s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3140,6 +3300,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes detailCardEnter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 语音转文字按钮样式 */
|
/* 语音转文字按钮样式 */
|
||||||
.voice-transcribe-btn {
|
.voice-transcribe-btn {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
|
|||||||
@@ -68,6 +68,21 @@ const MESSAGE_LIST_SCROLL_IDLE_MS = 160
|
|||||||
const MESSAGE_TOP_WHEEL_LOAD_COOLDOWN_MS = 160
|
const MESSAGE_TOP_WHEEL_LOAD_COOLDOWN_MS = 160
|
||||||
const MESSAGE_EDGE_TRIGGER_DISTANCE_PX = 96
|
const MESSAGE_EDGE_TRIGGER_DISTANCE_PX = 96
|
||||||
|
|
||||||
|
type RequestIdleCallbackCompat = (callback: () => void, options?: { timeout?: number }) => number
|
||||||
|
|
||||||
|
function scheduleWhenIdle(task: () => void, options?: { timeout?: number; fallbackDelay?: number }): void {
|
||||||
|
const requestIdleCallbackFn = (
|
||||||
|
globalThis as typeof globalThis & { requestIdleCallback?: RequestIdleCallbackCompat }
|
||||||
|
).requestIdleCallback
|
||||||
|
|
||||||
|
if (typeof requestIdleCallbackFn === 'function') {
|
||||||
|
requestIdleCallbackFn(task, options?.timeout !== undefined ? { timeout: options.timeout } : undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(task, options?.fallbackDelay ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
function isGlobalMsgSearchCanceled(error: unknown): boolean {
|
function isGlobalMsgSearchCanceled(error: unknown): boolean {
|
||||||
return String(error || '') === GLOBAL_MSG_SEARCH_CANCELED_ERROR
|
return String(error || '') === GLOBAL_MSG_SEARCH_CANCELED_ERROR
|
||||||
}
|
}
|
||||||
@@ -2959,15 +2974,9 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
await loadContactInfoBatch(usernames)
|
await loadContactInfoBatch(usernames)
|
||||||
} else {
|
} else {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
if ('requestIdleCallback' in window) {
|
scheduleWhenIdle(() => {
|
||||||
window.requestIdleCallback(() => {
|
|
||||||
void loadContactInfoBatch(usernames).finally(resolve)
|
void loadContactInfoBatch(usernames).finally(resolve)
|
||||||
}, { timeout: 700 })
|
}, { timeout: 700, fallbackDelay: 80 })
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
void loadContactInfoBatch(usernames).finally(resolve)
|
|
||||||
}, 80)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
processedBatchCount += 1
|
processedBatchCount += 1
|
||||||
@@ -3066,7 +3075,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
const loadContactInfoBatch = async (usernames: string[]) => {
|
const loadContactInfoBatch = async (usernames: string[]) => {
|
||||||
const startTime = performance.now()
|
const startTime = performance.now()
|
||||||
try {
|
try {
|
||||||
// 在 DLL 调用前让出控制权(使用 setTimeout 0 代替 setImmediate)
|
// 在数据服务调用前让出控制权(使用 setTimeout 0 代替 setImmediate)
|
||||||
await new Promise(resolve => setTimeout(resolve, 0))
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
|
||||||
const dllStart = performance.now()
|
const dllStart = performance.now()
|
||||||
@@ -3077,7 +3086,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
}
|
}
|
||||||
const dllTime = performance.now() - dllStart
|
const dllTime = performance.now() - dllStart
|
||||||
|
|
||||||
// DLL 调用后再次让出控制权
|
//数据服务调用后再次让出控制权
|
||||||
await new Promise(resolve => setTimeout(resolve, 0))
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
|
||||||
const totalTime = performance.now() - startTime
|
const totalTime = performance.now() - startTime
|
||||||
@@ -3259,13 +3268,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (defer) {
|
if (defer) {
|
||||||
if ('requestIdleCallback' in window) {
|
scheduleWhenIdle(runWarmup, { timeout: 1200, fallbackDelay: 120 })
|
||||||
window.requestIdleCallback(() => {
|
|
||||||
runWarmup()
|
|
||||||
}, { timeout: 1200 })
|
|
||||||
} else {
|
|
||||||
globalThis.setTimeout(runWarmup, 120)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3288,11 +3291,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
run()
|
run()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('requestIdleCallback' in window) {
|
scheduleWhenIdle(runWhenIdle, { timeout: 1200, fallbackDelay: MESSAGE_LIST_SCROLL_IDLE_MS })
|
||||||
window.requestIdleCallback(runWhenIdle, { timeout: 1200 })
|
|
||||||
} else {
|
|
||||||
window.setTimeout(runWhenIdle, MESSAGE_LIST_SCROLL_IDLE_MS)
|
|
||||||
}
|
|
||||||
}, [warmupGroupSenderProfiles])
|
}, [warmupGroupSenderProfiles])
|
||||||
|
|
||||||
// 加载消息
|
// 加载消息
|
||||||
@@ -6796,13 +6795,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
|
|
||||||
{/* 会话详情面板 */}
|
{/* 会话详情面板 */}
|
||||||
{showDetailPanel && (
|
{showDetailPanel && (
|
||||||
<div className="detail-panel">
|
<div className="detail-panel session-detail-panel">
|
||||||
<div className="detail-header">
|
|
||||||
<h4>会话详情</h4>
|
|
||||||
<button className="close-btn" onClick={() => setShowDetailPanel(false)}>
|
|
||||||
<X size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{isLoadingDetail && !sessionDetail ? (
|
{isLoadingDetail && !sessionDetail ? (
|
||||||
<div className="detail-loading">
|
<div className="detail-loading">
|
||||||
<Loader2 size={20} className="spin" />
|
<Loader2 size={20} className="spin" />
|
||||||
@@ -6810,7 +6803,27 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : sessionDetail ? (
|
) : sessionDetail ? (
|
||||||
<div className="detail-content">
|
<div className="detail-content">
|
||||||
<div className="detail-section">
|
<div className="detail-overview-card">
|
||||||
|
<Avatar
|
||||||
|
src={currentSession?.avatarUrl}
|
||||||
|
name={sessionDetail.remark || sessionDetail.nickName || currentSession?.displayName || sessionDetail.wxid}
|
||||||
|
size={42}
|
||||||
|
className="detail-overview-avatar"
|
||||||
|
/>
|
||||||
|
<div className="detail-overview-meta">
|
||||||
|
<span className="detail-overview-name">
|
||||||
|
{sessionDetail.remark || sessionDetail.nickName || currentSession?.displayName || sessionDetail.alias || sessionDetail.wxid}
|
||||||
|
</span>
|
||||||
|
<span className="detail-overview-sub">
|
||||||
|
{sessionDetail.alias || sessionDetail.wxid}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button className="detail-overview-close-btn" onClick={() => setShowDetailPanel(false)} title="关闭详情">
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-section detail-basic-section">
|
||||||
<div className="detail-item">
|
<div className="detail-item">
|
||||||
<Hash size={14} />
|
<Hash size={14} />
|
||||||
<span className="label">微信ID</span>
|
<span className="label">微信ID</span>
|
||||||
@@ -6848,10 +6861,10 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="detail-section">
|
<div className="detail-section detail-stats-section">
|
||||||
<div className="section-title">
|
<div className="section-title">
|
||||||
<MessageSquare size={14} />
|
<MessageSquare size={14} />
|
||||||
<span>消息统计(导出口径)</span>
|
<span>消息统计</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-stats-meta">
|
<div className="detail-stats-meta">
|
||||||
{isRefreshingDetailStats
|
{isRefreshingDetailStats
|
||||||
@@ -7009,7 +7022,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="detail-section">
|
<div className="detail-section detail-db-section">
|
||||||
<div className="section-title">
|
<div className="section-title">
|
||||||
<Database size={14} />
|
<Database size={14} />
|
||||||
<span>数据库分布</span>
|
<span>数据库分布</span>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ import {
|
|||||||
Database,
|
Database,
|
||||||
Download,
|
Download,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
File as FileIcon,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Hash,
|
Hash,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
@@ -201,6 +202,7 @@ const contentTypeLabels: Record<ContentType, string> = {
|
|||||||
emoji: '表情包',
|
emoji: '表情包',
|
||||||
file: '文件'
|
file: '文件'
|
||||||
}
|
}
|
||||||
|
const FILE_SIZE_PRESETS_MB = [0, 100, 200, 500, 1024] as const
|
||||||
|
|
||||||
const backgroundTaskSourceLabels: Record<string, string> = {
|
const backgroundTaskSourceLabels: Record<string, string> = {
|
||||||
export: '导出页',
|
export: '导出页',
|
||||||
@@ -1210,16 +1212,18 @@ const WriteLayoutSelector = memo(function WriteLayoutSelector({
|
|||||||
const writeLayoutLabel = writeLayoutOptions.find(option => option.value === writeLayout)?.label || 'A(类型分目录)'
|
const writeLayoutLabel = writeLayoutOptions.find(option => option.value === writeLayout)?.label || 'A(类型分目录)'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="write-layout-control" ref={containerRef}>
|
<div className={`write-layout-control ${isOpen ? 'open' : ''}`} ref={containerRef}>
|
||||||
<span className="control-label">写入目录方式</span>
|
<span className="control-label">写入目录方式</span>
|
||||||
<button
|
<button
|
||||||
className={`layout-trigger ${isOpen ? 'active' : ''}`}
|
className={`layout-trigger ${isOpen ? 'active' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(prev => !prev)}
|
onClick={() => setIsOpen(prev => !prev)}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-haspopup="listbox"
|
||||||
>
|
>
|
||||||
{writeLayoutLabel}
|
{writeLayoutLabel}
|
||||||
</button>
|
</button>
|
||||||
<div className={`layout-dropdown ${isOpen ? 'open' : ''}`}>
|
<div className={`layout-dropdown ${isOpen ? 'open' : ''}`} role="listbox" aria-label="写入目录方式">
|
||||||
{writeLayoutOptions.map(option => (
|
{writeLayoutOptions.map(option => (
|
||||||
<button
|
<button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
@@ -1336,7 +1340,7 @@ const TaskCenterModal = memo(function TaskCenterModal({
|
|||||||
}: TaskCenterModalProps) {
|
}: TaskCenterModalProps) {
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className="task-center-modal-overlay"
|
className="task-center-modal-overlay"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -1533,7 +1537,8 @@ const TaskCenterModal = memo(function TaskCenterModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -6491,6 +6496,10 @@ function ExportPage() {
|
|||||||
const useCollapsedSessionFormatSelector = isSessionScopeDialog || isContentTextDialog
|
const useCollapsedSessionFormatSelector = isSessionScopeDialog || isContentTextDialog
|
||||||
const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog
|
const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog
|
||||||
const shouldShowMediaSection = !isContentScopeDialog
|
const shouldShowMediaSection = !isContentScopeDialog
|
||||||
|
const shouldRenderImageDeepSearchToggle = exportDialog.scope !== 'sns' && (
|
||||||
|
isSessionScopeDialog ||
|
||||||
|
(isContentScopeDialog && exportDialog.contentType === 'image')
|
||||||
|
)
|
||||||
const shouldShowImageDeepSearchToggle = exportDialog.scope !== 'sns' && (
|
const shouldShowImageDeepSearchToggle = exportDialog.scope !== 'sns' && (
|
||||||
(isSessionScopeDialog && options.exportImages) ||
|
(isSessionScopeDialog && options.exportImages) ||
|
||||||
(isContentScopeDialog && exportDialog.contentType === 'image')
|
(isContentScopeDialog && exportDialog.contentType === 'image')
|
||||||
@@ -6500,6 +6509,80 @@ function ExportPage() {
|
|||||||
const activeDialogFormatLabel = exportDialog.scope === 'sns'
|
const activeDialogFormatLabel = exportDialog.scope === 'sns'
|
||||||
? (snsFormatOptions.find(option => option.value === snsExportFormat)?.label ?? snsExportFormat)
|
? (snsFormatOptions.find(option => option.value === snsExportFormat)?.label ?? snsExportFormat)
|
||||||
: (formatOptions.find(option => option.value === options.format)?.label ?? options.format)
|
: (formatOptions.find(option => option.value === options.format)?.label ?? options.format)
|
||||||
|
const sessionMediaOptions = [
|
||||||
|
{
|
||||||
|
key: 'images',
|
||||||
|
label: '图片',
|
||||||
|
desc: '聊天图片与缩略图',
|
||||||
|
icon: ImageIcon,
|
||||||
|
checked: options.exportImages,
|
||||||
|
onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportImages: checked }))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'voices',
|
||||||
|
label: '语音',
|
||||||
|
desc: '语音消息文件',
|
||||||
|
icon: Mic,
|
||||||
|
checked: options.exportVoices,
|
||||||
|
onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportVoices: checked }))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'videos',
|
||||||
|
label: '视频',
|
||||||
|
desc: '聊天视频与封面',
|
||||||
|
icon: Video,
|
||||||
|
checked: options.exportVideos,
|
||||||
|
onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportVideos: checked }))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'emojis',
|
||||||
|
label: '表情包',
|
||||||
|
desc: '静态与动态表情',
|
||||||
|
icon: MessageSquare,
|
||||||
|
checked: options.exportEmojis,
|
||||||
|
onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportEmojis: checked }))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'files',
|
||||||
|
label: '文件',
|
||||||
|
desc: '文档与附件',
|
||||||
|
icon: FileIcon,
|
||||||
|
checked: options.exportFiles,
|
||||||
|
onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportFiles: checked }))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const snsMediaOptions = [
|
||||||
|
{
|
||||||
|
key: 'images',
|
||||||
|
label: '图片',
|
||||||
|
desc: '朋友圈图片',
|
||||||
|
icon: ImageIcon,
|
||||||
|
checked: snsExportImages,
|
||||||
|
onToggle: (checked: boolean) => setSnsExportImages(checked)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'live-photos',
|
||||||
|
label: '实况图',
|
||||||
|
desc: 'Live Photo',
|
||||||
|
icon: Aperture,
|
||||||
|
checked: snsExportLivePhotos,
|
||||||
|
onToggle: (checked: boolean) => setSnsExportLivePhotos(checked)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'videos',
|
||||||
|
label: '视频',
|
||||||
|
desc: '朋友圈视频',
|
||||||
|
icon: Video,
|
||||||
|
checked: snsExportVideos,
|
||||||
|
onToggle: (checked: boolean) => setSnsExportVideos(checked)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const dialogMediaOptions = exportDialog.scope === 'sns' ? snsMediaOptions : sessionMediaOptions
|
||||||
|
const mediaSelectionSummaryLabel = `已选择 ${dialogMediaOptions.filter(option => option.checked).length}/${dialogMediaOptions.length}`
|
||||||
|
const voiceAsTextStatusLabel = options.exportVoices
|
||||||
|
? '已勾选导出语音:会同时导出语音文件,并在文本中追加语音转写结果。'
|
||||||
|
: '未勾选导出语音时,仅在文本里追加语音转写结果,不导出语音文件。'
|
||||||
|
const fileSizeLimitLabel = options.maxFileSizeMb <= 0 ? '不限' : `${options.maxFileSizeMb} MB`
|
||||||
const shouldShowDisplayNameSection = !(
|
const shouldShowDisplayNameSection = !(
|
||||||
exportDialog.scope === 'sns' ||
|
exportDialog.scope === 'sns' ||
|
||||||
(
|
(
|
||||||
@@ -6518,8 +6601,9 @@ function ExportPage() {
|
|||||||
const taskQueuedCount = tasks.filter(task => task.status === 'queued').length
|
const taskQueuedCount = tasks.filter(task => task.status === 'queued').length
|
||||||
const taskCenterAlertCount = taskRunningCount + taskQueuedCount
|
const taskCenterAlertCount = taskRunningCount + taskQueuedCount
|
||||||
const hasFilteredContacts = filteredContacts.length > 0
|
const hasFilteredContacts = filteredContacts.length > 0
|
||||||
|
const CONTACTS_ACTION_STICKY_WIDTH = 184
|
||||||
const contactsTableMinWidth = useMemo(() => {
|
const contactsTableMinWidth = useMemo(() => {
|
||||||
const baseWidth = 24 + 34 + 44 + 280 + 120 + (4 * 72) + 140 + (8 * 12)
|
const baseWidth = 24 + 34 + 44 + 280 + 120 + (4 * 72) + CONTACTS_ACTION_STICKY_WIDTH + (8 * 12)
|
||||||
const snsWidth = shouldShowSnsColumn ? 72 + 12 : 0
|
const snsWidth = shouldShowSnsColumn ? 72 + 12 : 0
|
||||||
const mutualFriendsWidth = shouldShowMutualFriendsColumn ? 72 + 12 : 0
|
const mutualFriendsWidth = shouldShowMutualFriendsColumn ? 72 + 12 : 0
|
||||||
return baseWidth + snsWidth + mutualFriendsWidth
|
return baseWidth + snsWidth + mutualFriendsWidth
|
||||||
@@ -6710,7 +6794,7 @@ function ExportPage() {
|
|||||||
const toggleTaskPerfDetail = useCallback((taskId: string) => {
|
const toggleTaskPerfDetail = useCallback((taskId: string) => {
|
||||||
setExpandedPerfTaskId(prev => (prev === taskId ? null : taskId))
|
setExpandedPerfTaskId(prev => (prev === taskId ? null : taskId))
|
||||||
}, [])
|
}, [])
|
||||||
const renderContactRow = useCallback((_: number, contact: ContactInfo) => {
|
const renderContactRow = useCallback((index: number, contact: ContactInfo) => {
|
||||||
const matchedSession = sessionRowByUsername.get(contact.username)
|
const matchedSession = sessionRowByUsername.get(contact.username)
|
||||||
const canExport = Boolean(matchedSession?.hasSession)
|
const canExport = Boolean(matchedSession?.hasSession)
|
||||||
const isSessionBindingPending = !matchedSession && (isLoading || isSessionEnriching)
|
const isSessionBindingPending = !matchedSession && (isLoading || isSessionEnriching)
|
||||||
@@ -6776,8 +6860,20 @@ function ExportPage() {
|
|||||||
: contact.type === 'group'
|
: contact.type === 'group'
|
||||||
? '打开群聊'
|
? '打开群聊'
|
||||||
: '打开对话'
|
: '打开对话'
|
||||||
|
const previousContact = index > 0 ? filteredContacts[index - 1] : null
|
||||||
|
const nextContact = index < filteredContacts.length - 1 ? filteredContacts[index + 1] : null
|
||||||
|
const previousCanExport = Boolean(previousContact && sessionRowByUsername.get(previousContact.username)?.hasSession)
|
||||||
|
const nextCanExport = Boolean(nextContact && sessionRowByUsername.get(nextContact.username)?.hasSession)
|
||||||
|
const previousSelected = Boolean(previousContact && previousCanExport && selectedSessions.has(previousContact.username))
|
||||||
|
const nextSelected = Boolean(nextContact && nextCanExport && selectedSessions.has(nextContact.username))
|
||||||
|
const rowClassName = [
|
||||||
|
'contact-row',
|
||||||
|
checked ? 'selected' : '',
|
||||||
|
checked && previousSelected ? 'selected-contiguous-top' : '',
|
||||||
|
checked && nextSelected ? 'selected-contiguous-bottom' : ''
|
||||||
|
].filter(Boolean).join(' ')
|
||||||
return (
|
return (
|
||||||
<div className={`contact-row ${checked ? 'selected' : ''}`}>
|
<div className={rowClassName}>
|
||||||
<div className="contact-item">
|
<div className="contact-item">
|
||||||
<div className="row-left-sticky">
|
<div className="row-left-sticky">
|
||||||
<div className="row-select-cell">
|
<div className="row-select-cell">
|
||||||
@@ -6926,6 +7022,7 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
|
filteredContacts,
|
||||||
lastExportBySession,
|
lastExportBySession,
|
||||||
navigate,
|
navigate,
|
||||||
nowTick,
|
nowTick,
|
||||||
@@ -7095,7 +7192,7 @@ function ExportPage() {
|
|||||||
onTogglePerfTask={toggleTaskPerfDetail}
|
onTogglePerfTask={toggleTaskPerfDetail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isExportDefaultsModalOpen && (
|
{isExportDefaultsModalOpen && createPortal(
|
||||||
<div
|
<div
|
||||||
className="export-defaults-modal-overlay"
|
className="export-defaults-modal-overlay"
|
||||||
onClick={() => setIsExportDefaultsModalOpen(false)}
|
onClick={() => setIsExportDefaultsModalOpen(false)}
|
||||||
@@ -7133,7 +7230,8 @@ function ExportPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="export-section-title-row">
|
<div className="export-section-title-row">
|
||||||
@@ -7218,7 +7316,7 @@ function ExportPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className={`session-load-detail-entry ${isSessionLoadDetailActive ? 'active' : ''}`}
|
className={`session-load-detail-entry ${showSessionLoadDetailModal ? 'open' : ''} ${isSessionLoadDetailActive && !showSessionLoadDetailModal ? 'active' : ''}`.trim()}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowSessionLoadDetailModal(true)}
|
onClick={() => setShowSessionLoadDetailModal(true)}
|
||||||
>
|
>
|
||||||
@@ -7428,7 +7526,7 @@ function ExportPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showSessionLoadDetailModal && (
|
{showSessionLoadDetailModal && createPortal(
|
||||||
<div
|
<div
|
||||||
className="session-load-detail-overlay"
|
className="session-load-detail-overlay"
|
||||||
onClick={() => setShowSessionLoadDetailModal(false)}
|
onClick={() => setShowSessionLoadDetailModal(false)}
|
||||||
@@ -7663,10 +7761,11 @@ function ExportPage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sessionMutualFriendsDialogTarget && sessionMutualFriendsDialogMetric && (
|
{sessionMutualFriendsDialogTarget && sessionMutualFriendsDialogMetric && createPortal(
|
||||||
<div
|
<div
|
||||||
className="session-mutual-friends-overlay"
|
className="session-mutual-friends-overlay"
|
||||||
onClick={closeSessionMutualFriendsDialog}
|
onClick={closeSessionMutualFriendsDialog}
|
||||||
@@ -7749,10 +7848,11 @@ function ExportPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showSessionDetailPanel && (
|
{showSessionDetailPanel && createPortal(
|
||||||
<div
|
<div
|
||||||
className="export-session-detail-overlay"
|
className="export-session-detail-overlay"
|
||||||
onClick={closeSessionDetailPanel}
|
onClick={closeSessionDetailPanel}
|
||||||
@@ -7854,19 +7954,15 @@ function ExportPage() {
|
|||||||
<div className="detail-record-list">
|
<div className="detail-record-list">
|
||||||
{currentSessionExportRecords.map((record, index) => (
|
{currentSessionExportRecords.map((record, index) => (
|
||||||
<div className="detail-record-item" key={`${record.exportTime}-${record.content}-${index}`}>
|
<div className="detail-record-item" key={`${record.exportTime}-${record.content}-${index}`}>
|
||||||
<div className="record-row">
|
<div className="detail-record-head">
|
||||||
<span className="label">导出时间</span>
|
<span className="record-export-time">{formatYmdHmDateTime(record.exportTime)}</span>
|
||||||
<span className="value">{formatYmdHmDateTime(record.exportTime)}</span>
|
<span className="record-content-pill" title={record.content}>{record.content}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="record-row">
|
<div className="detail-record-path-row">
|
||||||
<span className="label">导出内容</span>
|
<span className="path-label">导出目录</span>
|
||||||
<span className="value">{record.content}</span>
|
<span className="path-value" title={record.outputDir}>{formatPathBrief(record.outputDir)}</span>
|
||||||
</div>
|
|
||||||
<div className="record-row">
|
|
||||||
<span className="label">导出目录</span>
|
|
||||||
<span className="value path" title={record.outputDir}>{formatPathBrief(record.outputDir)}</span>
|
|
||||||
<button
|
<button
|
||||||
className="detail-inline-btn"
|
className="detail-inline-btn detail-record-open-btn"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void window.electronAPI.shell.openPath(record.outputDir)}
|
onClick={() => void window.electronAPI.shell.openPath(record.outputDir)}
|
||||||
>
|
>
|
||||||
@@ -7882,7 +7978,7 @@ function ExportPage() {
|
|||||||
<div className="detail-section">
|
<div className="detail-section">
|
||||||
<div className="section-title">
|
<div className="section-title">
|
||||||
<MessageSquare size={14} />
|
<MessageSquare size={14} />
|
||||||
<span>消息统计(导出口径)</span>
|
<span>消息统计</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-stats-meta">
|
<div className="detail-stats-meta">
|
||||||
{isRefreshingSessionDetailStats
|
{isRefreshingSessionDetailStats
|
||||||
@@ -8065,7 +8161,8 @@ function ExportPage() {
|
|||||||
<div className="detail-empty">暂无详情</div>
|
<div className="detail-empty">暂无详情</div>
|
||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ContactSnsTimelineDialog
|
<ContactSnsTimelineDialog
|
||||||
@@ -8192,34 +8289,63 @@ function ExportPage() {
|
|||||||
|
|
||||||
{shouldShowMediaSection && (
|
{shouldShowMediaSection && (
|
||||||
<div className="dialog-section">
|
<div className="dialog-section">
|
||||||
|
<div className="section-header-action media-section-header">
|
||||||
<h4>{exportDialog.scope === 'sns' ? '媒体文件(可多选)' : '媒体内容'}</h4>
|
<h4>{exportDialog.scope === 'sns' ? '媒体文件(可多选)' : '媒体内容'}</h4>
|
||||||
<div className="media-check-grid">
|
<span className="media-selection-pill">{mediaSelectionSummaryLabel}</span>
|
||||||
{exportDialog.scope === 'sns' ? (
|
|
||||||
<>
|
|
||||||
<label><input type="checkbox" checked={snsExportImages} onChange={event => setSnsExportImages(event.target.checked)} /> 图片</label>
|
|
||||||
<label><input type="checkbox" checked={snsExportLivePhotos} onChange={event => setSnsExportLivePhotos(event.target.checked)} /> 实况图</label>
|
|
||||||
<label><input type="checkbox" checked={snsExportVideos} onChange={event => setSnsExportVideos(event.target.checked)} /> 视频</label>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<label><input type="checkbox" checked={options.exportImages} onChange={event => setOptions(prev => ({ ...prev, exportImages: event.target.checked }))} /> 图片</label>
|
|
||||||
<label><input type="checkbox" checked={options.exportVoices} onChange={event => setOptions(prev => ({ ...prev, exportVoices: event.target.checked }))} /> 语音</label>
|
|
||||||
<label><input type="checkbox" checked={options.exportVideos} onChange={event => setOptions(prev => ({ ...prev, exportVideos: event.target.checked }))} /> 视频</label>
|
|
||||||
<label><input type="checkbox" checked={options.exportEmojis} onChange={event => setOptions(prev => ({ ...prev, exportEmojis: event.target.checked }))} /> 表情包</label>
|
|
||||||
<label><input type="checkbox" checked={options.exportFiles} onChange={event => setOptions(prev => ({ ...prev, exportFiles: event.target.checked }))} /> 文件</label>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{exportDialog.scope !== 'sns' && options.exportFiles && (
|
<div className="media-option-grid">
|
||||||
<div className="format-note">文件导出会优先使用消息里的 MD5 做校验;若设置了大小上限,则仅导出不超过该值的文件。</div>
|
{dialogMediaOptions.map(option => {
|
||||||
)}
|
const Icon = option.icon
|
||||||
|
return (
|
||||||
|
<label key={option.key} className={`media-option-card ${option.checked ? 'active' : ''}`}>
|
||||||
|
<input
|
||||||
|
className="media-option-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={option.checked}
|
||||||
|
onChange={event => option.onToggle(event.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="media-option-main">
|
||||||
|
<span className="media-option-icon">
|
||||||
|
<Icon size={16} />
|
||||||
|
</span>
|
||||||
|
<span className="media-option-text">
|
||||||
|
<span className="media-option-label">{option.label}</span>
|
||||||
|
<span className="media-option-desc">{option.desc}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className={`media-option-check ${option.checked ? 'active' : ''}`}>
|
||||||
|
<Check size={12} />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{exportDialog.scope !== 'sns' && (
|
||||||
|
<div
|
||||||
|
className={`dialog-collapse-slot ${options.exportFiles ? 'open' : ''}`}
|
||||||
|
aria-hidden={!options.exportFiles}
|
||||||
|
>
|
||||||
|
<div className="dialog-collapse-inner">
|
||||||
|
<div className="file-size-subsection">
|
||||||
|
<div className="file-size-subsection-header">
|
||||||
|
<div className="file-size-heading">文件大小上限</div>
|
||||||
|
<div className="file-size-current">{fileSizeLimitLabel}</div>
|
||||||
|
</div>
|
||||||
|
<div className="file-size-note">
|
||||||
|
文件导出优先使用消息中的 MD5 做校验;设置上限后,只导出不超过该值的文件。
|
||||||
|
</div>
|
||||||
|
<div className="file-size-preset-row">
|
||||||
|
{FILE_SIZE_PRESETS_MB.map(preset => (
|
||||||
|
<button
|
||||||
|
key={preset}
|
||||||
|
type="button"
|
||||||
|
className={`file-size-preset-btn ${options.maxFileSizeMb === preset ? 'active' : ''}`}
|
||||||
|
onClick={() => setOptions(prev => ({ ...prev, maxFileSizeMb: preset }))}
|
||||||
|
>
|
||||||
|
{preset === 0 ? '不限' : `${preset}MB`}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{shouldShowMediaSection && exportDialog.scope !== 'sns' && options.exportFiles && (
|
|
||||||
<div className="dialog-section">
|
|
||||||
<h4>文件大小上限</h4>
|
|
||||||
<div className="format-note">仅导出不超过该大小的文件,0 表示不限制。</div>
|
|
||||||
<div className="dialog-input-row">
|
<div className="dialog-input-row">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -8234,9 +8360,15 @@ function ExportPage() {
|
|||||||
<span>MB</span>
|
<span>MB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowImageDeepSearchToggle && (
|
{shouldRenderImageDeepSearchToggle && (
|
||||||
|
<div className={`dialog-collapse-slot ${shouldShowImageDeepSearchToggle ? 'open' : ''}`} aria-hidden={!shouldShowImageDeepSearchToggle}>
|
||||||
|
<div className="dialog-collapse-inner">
|
||||||
<div className="dialog-section">
|
<div className="dialog-section">
|
||||||
<div className="dialog-switch-row">
|
<div className="dialog-switch-row">
|
||||||
<div className="dialog-switch-copy">
|
<div className="dialog-switch-copy">
|
||||||
@@ -8254,6 +8386,8 @@ function ExportPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isSessionScopeDialog && (
|
{isSessionScopeDialog && (
|
||||||
@@ -8262,6 +8396,7 @@ function ExportPage() {
|
|||||||
<div className="dialog-switch-copy">
|
<div className="dialog-switch-copy">
|
||||||
<h4>语音转文字</h4>
|
<h4>语音转文字</h4>
|
||||||
<div className="format-note">默认状态跟随更多导出设置中的语音转文字开关。</div>
|
<div className="format-note">默认状态跟随更多导出设置中的语音转文字开关。</div>
|
||||||
|
<div className="format-note">{voiceAsTextStatusLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1867,8 +1867,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<div className="tab-content anti-revoke-tab">
|
<div className="tab-content anti-revoke-tab">
|
||||||
<div className="anti-revoke-hero">
|
<div className="anti-revoke-hero">
|
||||||
<div className="anti-revoke-hero-main">
|
<div className="anti-revoke-hero-main">
|
||||||
<h3>会话级防撤回触发器</h3>
|
<h3>防撤回</h3>
|
||||||
<p>仅针对勾选会话执行批量安装或卸载,状态可随时刷新。</p>
|
<p>你可以根据会话进行防撤回部署,安装后无需保持 WeFlow 运行即可实现防撤回</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="anti-revoke-metrics">
|
<div className="anti-revoke-metrics">
|
||||||
<div className="anti-revoke-metric is-total">
|
<div className="anti-revoke-metric is-total">
|
||||||
|
|||||||
Reference in New Issue
Block a user