mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
feat: 完成 TraceMemo v2.2.0 品牌升级并保留旧数据兼容
- 将用户可见品牌升级为 TraceMemo(迹忆) - 增加最早期 userData/sessionData 兼容路径选择 - 保留 WechatExplorer runtime identity 以兼容 safeStorage - 继续使用旧 Knowledge、Settings、API Token 和 Provider 配置 - 新日志写入 TraceMemo 目录并保留历史日志 - 保留旧 API、Skill、环境变量和导出目录兼容标识 - 更新相关文档、界面文案与自动化测试
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { app } from 'electron'
|
||||
import path from 'path'
|
||||
import {
|
||||
chooseUserDataRoot,
|
||||
getUserDataRoots,
|
||||
LEGACY_USER_DATA_NAME
|
||||
} from './app-data-paths'
|
||||
|
||||
// This module must remain the first main-process import. Static imports in
|
||||
// settings/cache services can otherwise resolve Electron paths before the
|
||||
// legacy runtime identity and selected userData are installed.
|
||||
app.setName(process.platform === 'win32' ? 'WeFlow' : LEGACY_USER_DATA_NAME)
|
||||
|
||||
const isolatedUserData = process.env['WXE_USER_DATA']
|
||||
const roots = getUserDataRoots(app.getPath('appData'))
|
||||
const selectedUserData = chooseUserDataRoot({
|
||||
...roots,
|
||||
isolated: isolatedUserData
|
||||
})
|
||||
|
||||
app.setPath('userData', selectedUserData)
|
||||
app.setPath('sessionData', selectedUserData)
|
||||
|
||||
// Logs are intentionally independent from userData. New TraceMemo logs go to
|
||||
// the new visible directory while historical WechatExplorer logs remain in
|
||||
// place and are never moved or renamed.
|
||||
if (process.platform === 'darwin') {
|
||||
app.setPath('logs', path.join(app.getPath('home'), 'Library', 'Logs', 'TraceMemo'))
|
||||
}
|
||||
|
||||
export { roots, selectedUserData }
|
||||
@@ -0,0 +1,104 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export const LEGACY_USER_DATA_NAME = 'WechatExplorer'
|
||||
export const CURRENT_USER_DATA_NAME = 'tracememo'
|
||||
|
||||
export interface UserDataRoots {
|
||||
legacy: string
|
||||
current: string
|
||||
}
|
||||
|
||||
export interface UserDataSelectionInput extends UserDataRoots {
|
||||
isolated?: string
|
||||
}
|
||||
|
||||
function isNonEmptyFile(filePath: string): boolean {
|
||||
try {
|
||||
const stat = fs.statSync(filePath)
|
||||
return stat.isFile() && stat.size > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function hasPersistentEntries(directoryPath: string): boolean {
|
||||
try {
|
||||
return fs.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
|
||||
if (entry.name === '.DS_Store') return false
|
||||
if (entry.name === 'LOCK' || entry.name === 'LOG' || entry.name === 'LOG.old') return false
|
||||
return entry.isFile() || entry.isDirectory()
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function hasDatabaseKey(directoryPath: string): boolean {
|
||||
try {
|
||||
return fs.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
|
||||
return entry.isFile() && entry.name.endsWith('.bin') && isNonEmptyFile(path.join(directoryPath, entry.name))
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function hasKnowledgeDatabase(root: string): boolean {
|
||||
const knowledgeRoot = path.join(root, 'knowledge')
|
||||
try {
|
||||
return fs.readdirSync(knowledgeRoot, { withFileTypes: true }).some((entry) => {
|
||||
if (!entry.isDirectory()) return false
|
||||
return isNonEmptyFile(path.join(knowledgeRoot, entry.name, 'knowledge.sqlite'))
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime-only Chromium files are deliberately excluded. A directory is a
|
||||
* valid data root only when it contains at least one user-owned marker.
|
||||
*/
|
||||
export function hasValidUserAssets(root: string): boolean {
|
||||
const markers = [
|
||||
'settings.json',
|
||||
'ai-providers.json',
|
||||
'ai-provider-keys.bin',
|
||||
'local-api-token.bin',
|
||||
'wechat-db-key.bin',
|
||||
'wechat-image-keys.bin',
|
||||
'image-insights.json',
|
||||
'wechat-share-service.bin'
|
||||
]
|
||||
if (markers.some((marker) => isNonEmptyFile(path.join(root, marker)))) return true
|
||||
if (hasKnowledgeDatabase(root)) return true
|
||||
if (hasDatabaseKey(path.join(root, 'database-keys'))) return true
|
||||
if (hasPersistentEntries(path.join(root, 'reports'))) return true
|
||||
if (hasPersistentEntries(path.join(root, 'recall-archive'))) return true
|
||||
if (hasPersistentEntries(path.join(root, 'digital-twin'))) return true
|
||||
if (hasPersistentEntries(path.join(root, 'group-exit-monitor'))) return true
|
||||
if (hasPersistentEntries(path.join(root, 'Local Storage', 'leveldb'))) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export function getUserDataRoots(appDataPath: string): UserDataRoots {
|
||||
return {
|
||||
legacy: path.join(appDataPath, LEGACY_USER_DATA_NAME),
|
||||
current: path.join(appDataPath, CURRENT_USER_DATA_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select exactly one root. This intentionally does not copy, merge, delete or
|
||||
* modify either directory. Legacy wins when both roots contain user assets so
|
||||
* a v2.1.9 upgrade remains deterministic and lossless.
|
||||
*/
|
||||
export function chooseUserDataRoot(input: UserDataSelectionInput): string {
|
||||
const isolated = input.isolated?.trim()
|
||||
if (isolated) return path.resolve(isolated)
|
||||
|
||||
if (hasValidUserAssets(input.legacy)) return input.legacy
|
||||
if (hasValidUserAssets(input.current)) return input.current
|
||||
return input.current
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class AppLogger {
|
||||
}
|
||||
|
||||
get logPath(): string {
|
||||
return path.join(this.logDir, 'wechatexplorer.log')
|
||||
return path.join(this.logDir, 'tracememo.log')
|
||||
}
|
||||
|
||||
private rotateIfNeeded(): void {
|
||||
|
||||
@@ -44,6 +44,18 @@ const exportStamp = (): string => {
|
||||
const pad = (value: number): string => String(value).padStart(2, '0')
|
||||
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
|
||||
}
|
||||
const defaultExportRoot = (): string => join(app.getPath('documents'), 'TraceMemo', '导出')
|
||||
const legacyExportRoot = (): string => join(app.getPath('documents'), 'WechatExplorer', '导出')
|
||||
const resolveDefaultExportRoot = async (outputFolder?: string): Promise<string> => {
|
||||
if (!outputFolder) return defaultExportRoot()
|
||||
try {
|
||||
await fs.access(join(legacyExportRoot(), outputFolder))
|
||||
// Continue incremental exports in the legacy folder when it already exists.
|
||||
return legacyExportRoot()
|
||||
} catch {
|
||||
return defaultExportRoot()
|
||||
}
|
||||
}
|
||||
const imageKeys = new ImageKeyConfigService()
|
||||
|
||||
export interface HtmlExportConversation {
|
||||
@@ -908,13 +920,13 @@ async function runSingleExport(
|
||||
total: messages.length,
|
||||
percent: request.format === 'html' ? 18 : 20
|
||||
})
|
||||
const root = options.outputRoot || join(app.getPath('documents'), 'WechatExplorer', '导出')
|
||||
await fs.mkdir(root, { recursive: true })
|
||||
const ext = request.format === 'markdown' ? 'md' : request.format
|
||||
const outputFolder =
|
||||
request.format === 'html'
|
||||
? options.outputFolderName || safeFilePart(request.outputName)
|
||||
: `${safeFilePart(request.outputName)}_${exportStamp()}`
|
||||
const root = options.outputRoot || (await resolveDefaultExportRoot(outputFolder))
|
||||
await fs.mkdir(root, { recursive: true })
|
||||
const outputDir = join(root, outputFolder)
|
||||
const outputPath =
|
||||
request.format === 'html'
|
||||
@@ -1506,8 +1518,8 @@ async function runAllExport(
|
||||
throw new Error('导出聊天不能重复')
|
||||
}
|
||||
|
||||
const exportRoot = join(app.getPath('documents'), 'WechatExplorer', '导出')
|
||||
const outputFolder = safeFilePart(request.outputName)
|
||||
const exportRoot = await resolveDefaultExportRoot(outputFolder)
|
||||
outputDir = join(exportRoot, outputFolder)
|
||||
const folderNames = conversationFolderNames(targets)
|
||||
let lastProgressAt = 0
|
||||
|
||||
@@ -75,7 +75,7 @@ const embedAvatar = async (source: string | undefined, name: string): Promise<st
|
||||
if (/^https?:\/\//i.test(source)) {
|
||||
const response = await fetch(source, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 WechatExplorer',
|
||||
'User-Agent': 'Mozilla/5.0 TraceMemo',
|
||||
Referer: 'https://weixin.qq.com/'
|
||||
},
|
||||
signal: AbortSignal.timeout(8000)
|
||||
|
||||
@@ -167,7 +167,7 @@ const routes: Record<string, RouteHandler> = {
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
ready: isReady(),
|
||||
service: 'WechatExplorer Reader',
|
||||
service: 'TraceMemo Reader',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
@@ -186,7 +186,7 @@ const routes: Record<string, RouteHandler> = {
|
||||
},
|
||||
|
||||
'/api/v1/contact': ({ res, url }) => {
|
||||
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
|
||||
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
|
||||
const filter = url.searchParams.get('filter') || undefined
|
||||
const type = url.searchParams.get('type') || undefined
|
||||
let contacts = listContacts(filter)
|
||||
@@ -197,7 +197,7 @@ const routes: Record<string, RouteHandler> = {
|
||||
},
|
||||
|
||||
'/api/v1/chatroom': ({ res, url }) => {
|
||||
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
|
||||
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
|
||||
const keyword = url.searchParams.get('keyword') || ''
|
||||
let groups = listContacts().filter((c) => c.type === 'group')
|
||||
if (keyword) {
|
||||
@@ -212,14 +212,14 @@ const routes: Record<string, RouteHandler> = {
|
||||
},
|
||||
|
||||
'/api/v1/recent_chat': ({ res, url }) => {
|
||||
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
|
||||
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
|
||||
const limit = parseNumeric(url.searchParams.get('limit'), 50)
|
||||
const items = listRecentChat(limit)
|
||||
sendJson(res, 200, { count: items.length, items })
|
||||
},
|
||||
|
||||
'/api/v1/chatlog': ({ res, url }) => {
|
||||
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
|
||||
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
|
||||
const talker = url.searchParams.get('talker')
|
||||
if (!talker) return sendError(res, 400, '缺少必要参数 talker')
|
||||
|
||||
@@ -257,7 +257,7 @@ const routes: Record<string, RouteHandler> = {
|
||||
},
|
||||
|
||||
'/api/v1/group_snapshot': ({ res, url }) => {
|
||||
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
|
||||
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
|
||||
const md5 = url.searchParams.get('md5')
|
||||
if (!md5) return sendError(res, 400, '缺少必要参数 md5')
|
||||
const snapshot = getGroupSnapshot(md5)
|
||||
@@ -266,7 +266,7 @@ const routes: Record<string, RouteHandler> = {
|
||||
},
|
||||
|
||||
'/api/v1/resolve': ({ res, url }) => {
|
||||
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
|
||||
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
|
||||
const q = url.searchParams.get('q')
|
||||
if (!q) return sendError(res, 400, '缺少必要参数 q')
|
||||
const contact = resolveMd5(q)
|
||||
@@ -276,7 +276,7 @@ const routes: Record<string, RouteHandler> = {
|
||||
|
||||
'/api/v1/report': async ({ req, res, body }) => {
|
||||
if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求')
|
||||
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
|
||||
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
|
||||
if (typeof body !== 'string' || !body.trim()) {
|
||||
return sendError(res, 400, '请求体为空,需 POST GroupReportExportRequest JSON')
|
||||
}
|
||||
@@ -300,7 +300,7 @@ const routes: Record<string, RouteHandler> = {
|
||||
|
||||
'/api/v1/agent/group-report': async ({ req, res, body }) => {
|
||||
if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求')
|
||||
if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化')
|
||||
if (!isReady()) return sendError(res, 503, 'TraceMemo 数据库未初始化')
|
||||
let request: { group?: string; range?: 'today' | 'yesterday' | '7days' }
|
||||
try {
|
||||
request = JSON.parse(typeof body === 'string' ? body : '{}')
|
||||
|
||||
@@ -562,7 +562,7 @@ export class ImageDecryptService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 md5 查找图片文件 (WechatExplorer 风格)
|
||||
* 根据 md5 查找图片文件
|
||||
*/
|
||||
findImageFile(md5?: string, imageDatName?: string, options?: ImageFindOptions): string | null {
|
||||
const allowThumbnail = options?.allowThumbnail !== false
|
||||
@@ -648,7 +648,7 @@ export class ImageDecryptService {
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试 WechatExplorer 的目录结构: msg/attach/{hash}/{YYYY-MM}/Img/
|
||||
// 尝试 TraceMemo 兼容的微信目录结构: msg/attach/{hash}/{YYYY-MM}/Img/
|
||||
if (!existsSync(attachDir)) {
|
||||
imageDecryptLog('[ImageDecrypt] attach dir not found:', attachDir)
|
||||
return rememberPath(
|
||||
|
||||
+6
-16
@@ -1,3 +1,4 @@
|
||||
import './app-data-bootstrap'
|
||||
import './preload-env'
|
||||
import {
|
||||
app,
|
||||
@@ -272,17 +273,6 @@ protocol.registerSchemesAsPrivileged([
|
||||
}
|
||||
])
|
||||
|
||||
// WCDB's Windows runtime checks the host application name during wcdb_init.
|
||||
// Mirroring WeFlow's name unblocks the -1006 init failure on Windows.
|
||||
app.setName(
|
||||
process.platform === 'win32'
|
||||
? 'WeFlow'
|
||||
: process.env['WXE_USER_DATA']
|
||||
? 'WechatExplorer Dev'
|
||||
: 'WechatExplorer'
|
||||
)
|
||||
const isolatedUserData = process.env['WXE_USER_DATA']
|
||||
if (isolatedUserData) app.setPath('userData', isolatedUserData)
|
||||
let dbInitInFlight: Promise<{ success: boolean; monitoring?: boolean; error?: string }> | null =
|
||||
null
|
||||
let appShutdownRequested = false
|
||||
@@ -435,7 +425,7 @@ function createWindow(): void {
|
||||
void dialog
|
||||
.showMessageBox(mainWindow, {
|
||||
type: 'question',
|
||||
title: '关闭 WechatExplorer',
|
||||
title: '关闭 TraceMemo',
|
||||
message: '请选择关闭方式',
|
||||
detail: '你可以将窗口隐藏到系统托盘,或退出整个应用进程。',
|
||||
buttons: ['最小化到系统托盘', '关闭进程', '取消'],
|
||||
@@ -516,11 +506,11 @@ app.whenReady().then(async () => {
|
||||
return new Response('Media unavailable', { status: 500 })
|
||||
}
|
||||
})
|
||||
console.log(`WechatExplorer main build: ${BUILD_MARK}`)
|
||||
console.log(`TraceMemo main build: ${BUILD_MARK}`)
|
||||
appLogger.write({
|
||||
level: 'info',
|
||||
scope: 'lifecycle',
|
||||
message: 'WechatExplorer 启动',
|
||||
message: 'TraceMemo 启动',
|
||||
details: { build: BUILD_MARK, platform: process.platform, version: app.getVersion() }
|
||||
})
|
||||
process.on('uncaughtException', (error) => {
|
||||
@@ -1695,7 +1685,7 @@ function buildTrayMenu(): Menu {
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '退出 WechatExplorer',
|
||||
label: '退出 TraceMemo',
|
||||
click: () => {
|
||||
tray?.destroy()
|
||||
tray = null
|
||||
@@ -1714,7 +1704,7 @@ function setupTray(): void {
|
||||
? nativeImage.createEmpty()
|
||||
: image.resize({ width: traySize, height: traySize, quality: 'best' })
|
||||
tray = new Tray(trayImage)
|
||||
tray.setToolTip('WechatExplorer')
|
||||
tray.setToolTip('TraceMemo')
|
||||
// macOS may show a Tray context menu on a primary click when it is set
|
||||
// directly on the Tray. Keep the menu for an explicit secondary click so
|
||||
// the primary click only restores the main window.
|
||||
|
||||
@@ -33,5 +33,5 @@ try {
|
||||
process.env.WEFLOW_PROJECT_NAME = process.env.WEFLOW_PROJECT_NAME || 'WeFlow'
|
||||
prependPath(dllDirs.filter((dir) => fs.existsSync(dir)))
|
||||
} catch (error) {
|
||||
console.error('[WechatExplorer] failed to enforce local DLL priority:', error)
|
||||
console.error('[TraceMemo] failed to enforce local DLL priority:', error)
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ class AgentHubService {
|
||||
|
||||
private async replyRecentChats(inbound: InboundMessage, limit: number): Promise<void> {
|
||||
if (!isReady()) {
|
||||
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
|
||||
await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
|
||||
return
|
||||
}
|
||||
const items = listRecentChat(limit)
|
||||
@@ -474,7 +474,7 @@ class AgentHubService {
|
||||
const result = await agentAIProvider.chat([
|
||||
{
|
||||
role: 'system',
|
||||
content: `你是 WechatExplorer 微信机器人的意图理解器。只能输出一行 JSON,不要 Markdown。
|
||||
content: `你是 TraceMemo 微信机器人的意图理解器。只能输出一行 JSON,不要 Markdown。
|
||||
支持的工具:
|
||||
1. recent:查看最近会话,参数 limit 为 1-20。
|
||||
2. contact:查看我与某个联系人的最近聊天,参数 contact 和 limit。
|
||||
@@ -544,7 +544,7 @@ class AgentHubService {
|
||||
intent: ContactChatIntent
|
||||
): Promise<void> {
|
||||
if (!isReady()) {
|
||||
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
|
||||
await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ class AgentHubService {
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!isReady()) {
|
||||
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
|
||||
await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
|
||||
return
|
||||
}
|
||||
const contact = resolveMd5(intent.contact)
|
||||
@@ -642,7 +642,7 @@ class AgentHubService {
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!isReady()) {
|
||||
await this.sendConnector(inbound, 'WechatExplorer 本地数据库尚未连接,请连接后再试。')
|
||||
await this.sendConnector(inbound, 'TraceMemo 本地数据库尚未连接,请连接后再试。')
|
||||
return
|
||||
}
|
||||
const group = this.resolveGroup(intent.group)
|
||||
|
||||
@@ -80,7 +80,7 @@ const agentSystemPrompt = (
|
||||
question: string,
|
||||
scopeLabel: string,
|
||||
rangeLabel: string
|
||||
): string => `你是 WechatExplorer 的受控本地聊天搜索代理,只负责决定下一步检索,不回答用户问题。
|
||||
): string => `你是 TraceMemo 的受控本地聊天搜索代理,只负责决定下一步检索,不回答用户问题。
|
||||
用户问题:${question}
|
||||
允许范围:${scopeLabel};时间范围:${rangeLabel}。
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function inspectImageDecryptionStatus(
|
||||
const imageDirectoryFound = hasImageDirectory(accountRoot)
|
||||
const stickerCacheFound =
|
||||
fs.existsSync(path.join(accountRoot, 'cache')) ||
|
||||
fs.existsSync(path.join(os.homedir(), 'Documents', 'TraceMemo', 'Emojis')) ||
|
||||
fs.existsSync(path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis'))
|
||||
const dbConnected = chat.isReady()
|
||||
const [wechatRunning, decoder] = await Promise.all([
|
||||
@@ -287,7 +288,7 @@ export function buildImageTestDiagnosticLog(input: {
|
||||
const rootIsDirectory = rootExists ? safeIsDirectory(root) : false
|
||||
const resultCode = input.result.success ? 'SUCCESS' : input.result.code || 'UNKNOWN'
|
||||
return [
|
||||
'WechatExplorer 图片解析测试日志(已脱敏)',
|
||||
'TraceMemo 图片解析测试日志(已脱敏)',
|
||||
`时间:${new Date().toISOString()}`,
|
||||
`应用版本:${safeAppVersion()}`,
|
||||
`运行环境:${process.platform} ${process.arch}`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/main/services/image-insight-service.ts
|
||||
// WechatExplorer AI 图片理解基础设施
|
||||
// TraceMemo AI 图片理解基础设施
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. base64 不走 IPC,只在 main 内部流转(renderer 只看到 ImageInsight 结构化结果)
|
||||
|
||||
@@ -90,7 +90,7 @@ export function resolveSkillResourceStatus(
|
||||
available: false,
|
||||
source,
|
||||
githubUrl: GITHUB_URL,
|
||||
error: `未找到 WechatExplorer Reader Skill 文件(已检查:${candidates.map((item) => item.path).join(';')})`
|
||||
error: `未找到 TraceMemo Reader Skill 文件(已检查:${candidates.map((item) => item.path).join(';')})`
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -19,9 +19,12 @@ const downloadCache = new Map<string, Promise<StickerResult>>()
|
||||
|
||||
export class StickerService {
|
||||
private readonly cacheDir: string
|
||||
private readonly legacyCacheDir: string
|
||||
|
||||
constructor(private readonly wcdb4Client?: Wcdb4Client | null) {
|
||||
this.cacheDir = path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis')
|
||||
this.cacheDir = path.join(os.homedir(), 'Documents', 'TraceMemo', 'Emojis')
|
||||
// Keep reading the former directory so existing sticker caches remain usable.
|
||||
this.legacyCacheDir = path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis')
|
||||
}
|
||||
|
||||
async resolveSticker(cdnUrl?: string, md5?: string): Promise<StickerResult> {
|
||||
@@ -69,7 +72,7 @@ export class StickerService {
|
||||
const extensions = ['.gif', '.png', '.webp', '.jpg', '.jpeg']
|
||||
const cacheDirs = [
|
||||
this.cacheDir,
|
||||
path.join(os.homedir(), 'Documents', 'WechatExplorer', 'Emojis')
|
||||
this.legacyCacheDir
|
||||
]
|
||||
for (const cacheDir of cacheDirs) {
|
||||
for (const ext of extensions) {
|
||||
@@ -128,7 +131,7 @@ export class StickerService {
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 MicroMessenger WechatExplorer',
|
||||
'User-Agent': 'Mozilla/5.0 MicroMessenger TraceMemo',
|
||||
Referer: 'https://weixin.qq.com/'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -261,6 +261,7 @@ export function resolveWindowsNativeAccountRoot(
|
||||
.find((candidate) => candidate && isAsciiPath(candidate))
|
||||
if (!publicRoot || !isAsciiPath(publicRoot)) return accountRoot
|
||||
|
||||
// Preserve the legacy ASCII bridge path so existing junctions remain reusable.
|
||||
const bridgeRoot = path.join(publicRoot, 'WechatExplorer', 'path-bridges')
|
||||
const bridgePath = path.join(
|
||||
bridgeRoot,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>WechatExplorer</title>
|
||||
<title>TraceMemo(迹忆)</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/brand-icon.svg" />
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
|
||||
@@ -805,7 +805,7 @@ function App(): React.ReactElement {
|
||||
})
|
||||
setStartupProgress({
|
||||
title: '正在加载账号信息...',
|
||||
subtitle: '即将进入 WechatExplorer',
|
||||
subtitle: '即将进入 TraceMemo',
|
||||
detail: '正在读取联系人和当前账号',
|
||||
percent: 70
|
||||
})
|
||||
@@ -1813,7 +1813,7 @@ function App(): React.ReactElement {
|
||||
? autoConnectSource === 'env'
|
||||
? '检测到环境变量中的密钥'
|
||||
: '使用上次安全保存的密钥'
|
||||
: 'WechatExplorer')
|
||||
: 'TraceMemo')
|
||||
return (
|
||||
<div className={`boot-splash ${appearanceSettings.showStartupProgress ? '' : 'is-quiet'}`}>
|
||||
<div className="boot-splash-spinner" aria-hidden />
|
||||
|
||||
@@ -141,12 +141,12 @@ export function DatabaseConnectionPage({
|
||||
|
||||
return (
|
||||
<main className="database-login-page">
|
||||
<section className="database-login-brand" aria-label="WechatExplorer 产品说明">
|
||||
<section className="database-login-brand" aria-label="TraceMemo(迹忆)产品说明">
|
||||
<div className="database-login-brand-content">
|
||||
<div className="database-login-logo" aria-hidden="true">
|
||||
<LineIcon name="database" />
|
||||
</div>
|
||||
<h1>WechatExplorer</h1>
|
||||
<h1>TraceMemo(迹忆)</h1>
|
||||
<p className="database-login-tagline">让 AI 读懂你的微信</p>
|
||||
<p className="database-login-description">
|
||||
连接成功后,你可以搜索聊天记录、生成群聊日报,并按需使用 AI 分析。
|
||||
@@ -261,7 +261,7 @@ export function DatabaseConnectionPage({
|
||||
'确认下方检测结果;没有找到目录时可以手动选择。',
|
||||
'请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。',
|
||||
'开始后请按页面提示完成系统授权。',
|
||||
'正在准备连接组件,请不要关闭微信或 WechatExplorer。',
|
||||
'正在准备连接组件,请不要关闭微信或 TraceMemo。',
|
||||
'请回到微信完成登录,登录成功后再回来验证。',
|
||||
'正在验证密钥和本地数据库,请稍候。'
|
||||
][guideStep - 1]}
|
||||
@@ -567,7 +567,7 @@ export function DatabaseConnectionPage({
|
||||
<button type="button" onClick={onClearKey}>
|
||||
清除已保存密钥
|
||||
</button>
|
||||
<span>WechatExplorer</span>
|
||||
<span>TraceMemo</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -478,7 +478,7 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({
|
||||
API 默认仅监听本机,并通过 Bearer Token 保护数据接口。Token 请在 API Center
|
||||
中显示或复制。关闭后 Claude / Codex 等客户端无法读取聊天数据。
|
||||
<br />
|
||||
配置文档:<code>docs/skill/wechatexplorer-reader/SKILL.md</code>
|
||||
配置文档(兼容路径):<code>docs/skill/wechatexplorer-reader/SKILL.md</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export function ExportTaskCenter({
|
||||
|
||||
const copyTaskLog = async (task: ExportTaskRecord): Promise<void> => {
|
||||
const log = [
|
||||
'WechatExplorer 导出任务日志',
|
||||
'TraceMemo 导出任务日志',
|
||||
`时间:${new Date(task.createdAt).toLocaleString('zh-CN')}`,
|
||||
`会话:${task.targetLabel}`,
|
||||
`格式:${task.format.toUpperCase()}`,
|
||||
|
||||
@@ -431,13 +431,13 @@ export function ExportWorkspace({
|
||||
|
||||
const targetPath = exportAll
|
||||
? format === 'html' && zip
|
||||
? `文稿/WechatExplorer/导出/${outputName}.zip`
|
||||
: `文稿/WechatExplorer/导出/${outputName}/`
|
||||
? `文稿/TraceMemo/导出/${outputName}.zip`
|
||||
: `文稿/TraceMemo/导出/${outputName}/`
|
||||
: format === 'html'
|
||||
? zip
|
||||
? `文稿/WechatExplorer/导出/${outputName}.zip`
|
||||
: `文稿/WechatExplorer/导出/${outputName}/`
|
||||
: `文稿/WechatExplorer/导出/${outputName}.${format === 'markdown' ? 'md' : format}`
|
||||
? `文稿/TraceMemo/导出/${outputName}.zip`
|
||||
: `文稿/TraceMemo/导出/${outputName}/`
|
||||
: `文稿/TraceMemo/导出/${outputName}.${format === 'markdown' ? 'md' : format}`
|
||||
|
||||
return (
|
||||
<div className="export-workspace">
|
||||
|
||||
@@ -26,7 +26,7 @@ interface AppShellProps {
|
||||
|
||||
function BrandLogo(): React.ReactElement {
|
||||
return (
|
||||
<div className="app-brand" title="WechatExplorer" aria-label="WechatExplorer">
|
||||
<div className="app-brand" title="TraceMemo(迹忆)" aria-label="TraceMemo(迹忆)">
|
||||
<img src={brandIcon} alt="" aria-hidden="true" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -194,7 +194,7 @@ export function AiReportWorkspace({
|
||||
<h3>隐私说明</h3>
|
||||
<p>微信数据库和聊天记录默认从本机读取。</p>
|
||||
<p>所选内容将发送至你配置的模型服务进行处理。</p>
|
||||
<p>WechatExplorer 本身不额外保存或转发内容。</p>
|
||||
<p>TraceMemo 本身不额外保存或转发内容。</p>
|
||||
</section>
|
||||
|
||||
{generatedImage && (
|
||||
|
||||
@@ -1094,7 +1094,7 @@ export function AISearchWorkspace({
|
||||
<div className="ai-search-workspace">
|
||||
<header className="ai-search-header">
|
||||
<div>
|
||||
<span className="ai-search-kicker">WechatExplorer · LOCAL INTELLIGENCE</span>
|
||||
<span className="ai-search-kicker">TraceMemo · LOCAL INTELLIGENCE</span>
|
||||
<h1>问问你的微信</h1>
|
||||
<p>在本地聊天记录中提炼主题、结论和可追溯证据</p>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ export function AgentHubWorkspace(): React.ReactElement {
|
||||
<div className="agent-hub-workspace">
|
||||
<header className="agent-hub-header">
|
||||
<div>
|
||||
<div className="agent-hub-eyebrow">WechatExplorer</div>
|
||||
<div className="agent-hub-eyebrow">TraceMemo</div>
|
||||
<h1>Agent Hub</h1>
|
||||
<p>让微信机器人安全调用聊天数据与 AI 能力。</p>
|
||||
</div>
|
||||
@@ -193,7 +193,7 @@ export function AgentHubWorkspace(): React.ReactElement {
|
||||
<aside className="agent-hub-card agent-hub-capability-card">
|
||||
<span className="agent-hub-card-kicker">已启用能力</span>
|
||||
<h2>微信数据助手</h2>
|
||||
<p>机器人通过本机 Agent Hub 调用 WechatExplorer,不向公网暴露数据库。</p>
|
||||
<p>机器人通过本机 Agent Hub 调用 TraceMemo,不向公网暴露数据库。</p>
|
||||
<div className="agent-hub-example">
|
||||
<span>支持自然语言,可以这样问</span>
|
||||
<strong>“最近 5 条消息是谁?”</strong>
|
||||
|
||||
@@ -195,7 +195,7 @@ export function ApiRuntimePanel({
|
||||
<h3>隐私说明</h3>
|
||||
<p>
|
||||
{localOnly
|
||||
? '本地 API 默认监听 127.0.0.1。WechatExplorer 不会通过该接口自动把聊天内容发送到云端。外部 Agent 是否调用第三方模型,取决于其自身配置。'
|
||||
? '本地 API 默认监听 127.0.0.1。TraceMemo 不会通过该接口自动把聊天内容发送到云端。外部 Agent 是否调用第三方模型,取决于其自身配置。'
|
||||
: '当前服务并非仅本机访问。请确认局域网环境可信;API Token 不等同于公网安全防护。'}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -37,13 +37,13 @@ export function ReaderSkillOverview({
|
||||
<div className="api-workspace-heading">
|
||||
<div>
|
||||
<div className="api-title-line">
|
||||
<h1>WechatExplorer Reader</h1>
|
||||
<h1>TraceMemo Reader</h1>
|
||||
<span className={`api-skill-status ${skill?.available ? 'ready' : 'error'}`}>
|
||||
{skill?.available ? '已安装' : '文件不可用'}
|
||||
</span>
|
||||
<span className="api-version">{skill?.version || 'v1.0'}</span>
|
||||
</div>
|
||||
<p>通过本地 HTTP API 读取 WechatExplorer 已解锁的微信聊天数据</p>
|
||||
<p>通过本地 HTTP API 读取 TraceMemo 已解锁的微信聊天数据</p>
|
||||
</div>
|
||||
<div className="api-header-actions">
|
||||
<button type="button" onClick={onPreview} disabled={!skill?.available}>
|
||||
@@ -79,9 +79,9 @@ export function ReaderSkillOverview({
|
||||
<div className="api-introduction">
|
||||
<h2>能力简介</h2>
|
||||
<p>
|
||||
WechatExplorer Reader 让本地 AI Agent
|
||||
TraceMemo Reader 让本地 AI Agent
|
||||
在用户授权和本地服务运行时,读取联系人、群聊、聊天记录和群成员信息,并调用内置模板导出群聊日报。聊天数据由
|
||||
WechatExplorer 本地服务提供,不会由该 API 自动上传到其他服务器。
|
||||
TraceMemo 本地服务提供,不会由该 API 自动上传到其他服务器。
|
||||
</p>
|
||||
<div className="api-flow">
|
||||
<span>AI Agent</span>
|
||||
|
||||
@@ -18,6 +18,10 @@ export function SkillDetails({
|
||||
<dl>
|
||||
<div>
|
||||
<dt>名称</dt>
|
||||
<dd>TraceMemo Reader</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>兼容标识</dt>
|
||||
<dd>wechatexplorer-reader</dd>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function SkillInstallFlow({
|
||||
<section className={service?.running && dbReady ? 'done' : 'active'}>
|
||||
<b>1</b>
|
||||
<div>
|
||||
<h3>确认 WechatExplorer 已就绪</h3>
|
||||
<h3>确认 TraceMemo 已就绪</h3>
|
||||
<p>
|
||||
本地 API:{service?.running ? '运行中' : '已停止'} · {address}
|
||||
</p>
|
||||
|
||||
@@ -16,12 +16,12 @@ export function SkillPreviewDialog({
|
||||
className="api-markdown-overlay"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="WechatExplorer Reader Skill 预览"
|
||||
aria-label="TraceMemo Reader Skill 预览"
|
||||
>
|
||||
<div>
|
||||
<header>
|
||||
<div>
|
||||
<strong>WechatExplorer Reader</strong>
|
||||
<strong>TraceMemo Reader</strong>
|
||||
<span>{version || 'v1.0'}</span>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const REPORT_REQUEST_PRESET = JSON.stringify(
|
||||
activeUsers: 0,
|
||||
timeSpan: '00:00-23:59',
|
||||
generatedAt: '2026-07-13 22:00',
|
||||
recordNote: '本日报由 WechatExplorer 自动生成',
|
||||
recordNote: '本日报由 TraceMemo 自动生成',
|
||||
footerNote: '',
|
||||
heroParticipants: [],
|
||||
avatars: {},
|
||||
@@ -37,7 +37,7 @@ export const AGENT_GROUP_REPORT_PRESET = JSON.stringify(
|
||||
)
|
||||
|
||||
export const AGENT_SEND_PRESET = JSON.stringify(
|
||||
{ to: '', text: 'WechatExplorer Agent Hub 发送测试' },
|
||||
{ to: '', text: 'TraceMemo Agent Hub 发送测试' },
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
@@ -7,13 +7,13 @@ function requestHost(host: string): string {
|
||||
function opening(target: AgentInstallTarget): string {
|
||||
switch (target) {
|
||||
case 'codex':
|
||||
return '请将本地目录中的 WechatExplorer Reader Skill 安装到当前 Codex 项目或用户 Skill 目录:'
|
||||
return '请将本地目录中的 TraceMemo Reader Skill 安装到当前 Codex 项目或用户 Skill 目录:'
|
||||
case 'claude-code':
|
||||
return '请安装以下本地 WechatExplorer Reader Skill,并按照 SKILL.md 调用本地 HTTP API:'
|
||||
return '请安装以下本地 TraceMemo Reader Skill,并按照 SKILL.md 调用本地 HTTP API:'
|
||||
case 'openclaw':
|
||||
return '请将以下本地目录作为 WechatExplorer Reader Skill 安装,并阅读其中的 SKILL.md:'
|
||||
return '请将以下本地目录作为 TraceMemo Reader Skill 安装,并阅读其中的 SKILL.md:'
|
||||
default:
|
||||
return '请读取并安装以下 WechatExplorer Reader Skill:'
|
||||
return '请读取并安装以下 TraceMemo Reader Skill:'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ export function buildSkillInstallInstruction({
|
||||
const sourceText =
|
||||
source.type === 'local'
|
||||
? `${opening(target)}\n\n${source.directoryPath}\n\n请先阅读该目录中的 SKILL.md,然后调用:`
|
||||
: `请从以下地址安装 WechatExplorer Reader Skill:\n\n${source.installUrl}\n\n阅读 SKILL.md 后,调用:`
|
||||
return `${sourceText}\n\n${healthUrl}\n\n先调用公开的 health 接口验证服务。然后请用户在 WechatExplorer → API Center → API Token 中点击“复制 Token”,并把 Token 配置为 Agent 本机环境变量 WECHATEXPLORER_API_TOKEN。读取联系人、会话或聊天记录时,必须发送 Authorization: Bearer $WECHATEXPLORER_API_TOKEN。此服务是 Local HTTP API,不是 MCP Server。安装完成后告诉我验证结果。`
|
||||
: `请从以下地址安装 TraceMemo Reader Skill:\n\n${source.installUrl}\n\n阅读 SKILL.md 后,调用:`
|
||||
return `${sourceText}\n\n${healthUrl}\n\n先调用公开的 health 接口验证服务。然后请用户在 TraceMemo → API Center → API Token 中点击“复制 Token”,并把 Token 配置为 Agent 本机环境变量 WECHATEXPLORER_API_TOKEN。读取联系人、会话或聊天记录时,必须发送 Authorization: Bearer $WECHATEXPLORER_API_TOKEN。此服务是 Local HTTP API,不是 MCP Server。安装完成后告诉我验证结果。`
|
||||
}
|
||||
|
||||
export function buildSkillVerificationPrompt(): string {
|
||||
return '请检查 WechatExplorer Reader 是否已连接,然后列出最近 5 个微信会话。'
|
||||
return '请检查 TraceMemo Reader 是否已连接,然后列出最近 5 个微信会话。'
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function AIImageUnderstandingTest({
|
||||
</label>
|
||||
|
||||
<p className="ai-vision-privacy">
|
||||
图片只会发送给你配置的 AI 服务,不会上传到 WechatExplorer 的其他服务器,也不会写入本地缓存。
|
||||
图片只会发送给你配置的 AI 服务,不会上传到 TraceMemo 的其他服务器,也不会写入本地缓存。
|
||||
</p>
|
||||
|
||||
{state.error ? <p className="ai-vision-error">{state.error}</p> : null}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function DatabaseKeyAutoDetect({
|
||||
<div className="database-key-auto-heading">
|
||||
<div>
|
||||
<strong>Windows 自动获取</strong>
|
||||
<p>WechatExplorer 可在微信桌面端正在运行时,通过本机内存扫描尝试获取数据库密钥。</p>
|
||||
<p>TraceMemo 可在微信桌面端正在运行时,通过本机内存扫描尝试获取数据库密钥。</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -63,7 +63,7 @@ export function DatabaseKeyDangerZone({
|
||||
>
|
||||
<h2 id="database-key-confirm-title">确认清除数据库密钥?</h2>
|
||||
<p>
|
||||
清除后 WechatExplorer
|
||||
清除后 TraceMemo
|
||||
将暂时无法读取聊天记录,需要重新输入或获取密钥。该操作不会删除微信原始数据。
|
||||
</p>
|
||||
<div>
|
||||
@@ -99,7 +99,7 @@ export function DatabaseKeyDangerZone({
|
||||
>
|
||||
<h2 id="database-key-return-confirm-title">返回登录界面?</h2>
|
||||
<p>
|
||||
WechatExplorer
|
||||
TraceMemo
|
||||
将断开当前数据库连接并回到密钥输入界面。已保存的数据库密钥和微信原始数据不会被删除。
|
||||
</p>
|
||||
<div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function buildDatabaseKeyDiagnostics(
|
||||
): string {
|
||||
const validation = state.validation
|
||||
return [
|
||||
'WechatExplorer 数据库密钥诊断',
|
||||
'TraceMemo 数据库密钥诊断',
|
||||
`已保存: ${state.saved ? '是' : '否'}`,
|
||||
`已验证: ${validation?.success ? '是' : '否'}`,
|
||||
`密钥长度合法: ${isDatabaseKeyFormatValid(input) ? '是' : '否'}`,
|
||||
|
||||
@@ -54,13 +54,13 @@ export function AboutPage({ onNotice }: { onNotice: (message: string) => void })
|
||||
<header className="settings-page-header">
|
||||
<div>
|
||||
<h1>关于</h1>
|
||||
<p>WechatExplorer 本地微信聊天记录工作台。</p>
|
||||
<p>TraceMemo(迹忆)本地优先、可追溯的 AI 微信知识与分析工作台。</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="settings-page-scroll">
|
||||
<div className="settings-page-content">
|
||||
<section className="settings-card about-identity-card">
|
||||
<div><span className="settings-card-kicker">当前版本</span><strong>WechatExplorer</strong><small>v{update.currentVersion}</small></div>
|
||||
<div><span className="settings-card-kicker">当前版本</span><strong>TraceMemo(迹忆)</strong><small>v{update.currentVersion}</small></div>
|
||||
<a href={REPOSITORY_URL} target="_blank" rel="noreferrer">GitHub 仓库</a>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export function DatabaseKeyPage({
|
||||
<div>
|
||||
<strong>密钥仅保存在本机</strong>
|
||||
<p>
|
||||
WechatExplorer
|
||||
TraceMemo
|
||||
使用数据库密钥读取本机微信数据库。密钥通过系统安全存储加密保存,不会写入普通日志,也不会上传到服务器。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function ImageDecryptionPage({
|
||||
</svg>
|
||||
<div>
|
||||
<strong>图片仅在本机解析</strong>
|
||||
<p>WechatExplorer 不会上传您的微信图片。所有图片解析和缓存处理均在本地完成。</p>
|
||||
<p>TraceMemo 不会上传您的微信图片。所有图片解析和缓存处理均在本地完成。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -658,7 +658,7 @@ export const buildGroupReportFacts = async (
|
||||
mediaMessageCount: imageCount + voiceCount + stickerCount,
|
||||
timeSpan,
|
||||
generatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
||||
recordNote: `基于 WechatExplorer 已加载的 ${transcriptRows.length} 条记录`,
|
||||
recordNote: `基于 TraceMemo 已加载的 ${transcriptRows.length} 条记录`,
|
||||
footerNote: '基于已读取聊天记录生成;图片、表情等未解析内容默认只按类型与上下文参与日报。',
|
||||
heroParticipants: topSpeakers.slice(0, 4).map((speaker) => speaker.name),
|
||||
avatars,
|
||||
|
||||
@@ -4,7 +4,7 @@ export function buildSafeDiagnosticSummary(
|
||||
environment: Omit<DatabaseKeyEnvironment, 'diagnosticSummary'>
|
||||
): string {
|
||||
return [
|
||||
`WechatExplorer: ${environment.appVersion}`,
|
||||
`TraceMemo: ${environment.appVersion}`,
|
||||
`操作系统: ${environment.osVersion}`,
|
||||
`微信客户端: ${environment.wechatVersion}`,
|
||||
`数据结构: ${environment.dataStructureVersion}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/shared/image-insight.ts
|
||||
// ImageInsight:微信图片的 AI 理解结果持久化数据结构
|
||||
// 与 WechatExplorer 整体 AI 知识平台定位一致 — 图片理解结果可索引、可缓存、可复用。
|
||||
// 与 TraceMemo 整体 AI 知识平台定位一致 — 图片理解结果可索引、可缓存、可复用。
|
||||
|
||||
export type ImageCategory =
|
||||
| 'screenshot' // 截图
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const WINDOWS_VC_RUNTIME_DOWNLOAD_URL = 'https://aka.ms/vc14/vc_redist.x64.exe'
|
||||
|
||||
export const WINDOWS_VC_RUNTIME_ERROR_MESSAGE = `当前 Windows 缺少 Microsoft Visual C++ 2015-2022 x64 运行库,无法加载微信数据库组件。请下载安装后重新启动 WechatExplorer:${WINDOWS_VC_RUNTIME_DOWNLOAD_URL}`
|
||||
export const WINDOWS_VC_RUNTIME_ERROR_MESSAGE = `当前 Windows 缺少 Microsoft Visual C++ 2015-2022 x64 运行库,无法加载微信数据库组件。请下载安装后重新启动 TraceMemo:${WINDOWS_VC_RUNTIME_DOWNLOAD_URL}`
|
||||
|
||||
const VC_RUNTIME_LIBRARY_PATTERN =
|
||||
/(?:vcruntime140(?:_1)?\.dll|msvcp140(?:_[12])?\.dll|concrt140\.dll|ucrtbase\.dll|api-ms-win-crt)/i
|
||||
|
||||
Reference in New Issue
Block a user