Files
WechatExplorer/tests/e2e/support/electron-main.cjs
T

1612 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */
const { app, BrowserWindow, ipcMain } = require('electron')
const fs = require('fs')
const path = require('path')
const root = path.resolve(__dirname, '../../..')
const fixture = structuredClone(require(path.join(root, 'tests/fixtures/chat-data.json')))
const userData = process.env.WXE_E2E_USER_DATA
if (!userData) throw new Error('WXE_E2E_USER_DATA is required')
const backgroundE2E = process.env.WXE_E2E_HEADLESS === '1'
app.setPath('userData', userData)
app.setPath('logs', path.join(userData, 'logs'))
app.commandLine.appendSwitch('disable-gpu')
const VALID_KEY = 'a'.repeat(64)
const imageData = `data:image/png;base64,${fs.readFileSync(path.join(root, 'resources/icon.png')).toString('base64')}`
const voiceData = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='
const configuredNow = Number(process.env.WXE_E2E_NOW_MS)
const updateSimulation = process.env.WXE_E2E_UPDATE_SIMULATION === '1'
const personalWechatSupported = process.platform === 'darwin' || process.platform === 'win32'
const unsignedMacUpdate = process.env.WXE_E2E_UNSIGNED_MAC_UPDATE === '1'
const fixtureNowMs =
Number.isFinite(configuredNow) && configuredNow > 0 ? configuredNow : Date.now()
const formatFixtureDateTime = (timestampSeconds) => {
const date = new Date(timestampSeconds * 1000)
const pad = (value) => 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 allFixtureMessages = Object.values(fixture.messages).flat()
const latestFixtureTime = Math.max(...allFixtureMessages.map((message) => message.createTime || 0))
const fixtureTimeOffset = Math.floor(fixtureNowMs / 1000) - 3600 - latestFixtureTime
for (const message of allFixtureMessages) {
message.createTime = (message.createTime || latestFixtureTime) + fixtureTimeOffset
message.datetime = formatFixtureDateTime(message.createTime)
}
const emptyTimings = () => ({
queryUnderstandingMs: 0,
contactResolutionMs: 0,
knowledgeSearchMs: 0,
workerIpcMs: 0,
workerBootMs: 0,
dispatchMs: 0,
workerSqlMs: 0,
responseSerializeMs: 0,
responseTransferMs: 0,
workerQueueMs: 0,
workerExecutionMs: 0,
globalCountMs: 0,
voiceCoverageMs: 0,
wcdbQueueMs: 0,
wcdbExecutionMs: 0,
senderEnrichmentMs: 0,
ipcMs: 0,
serializationMs: 0,
otherMs: 0,
ftsMs: 0,
chunkExpandMs: 0,
messageLoadMs: 0,
rankingMs: 0,
candidateRankingMs: 0,
evidenceBuildMs: 0,
aggregationMs: 0,
contextPreparationMs: 0,
agentDecisionMs: 0,
agentToolMs: 0,
aiGenerationMs: 0,
totalMs: 1
})
const aiSearchResult = (request) => {
const failure = process.env.WXE_E2E_AI_FAILURE
const evidence = [
{
id: 'E1',
chunkId: 'fixture-chunk',
conversationId: 'group-regular-md5',
conversationName: '产品测试群',
conversationType: 'group',
startTime: fixture.messages['group-regular-md5'][0].createTime * 1000,
endTime: fixture.messages['group-regular-md5'][0].createTime * 1000,
messageId: 'msg-text',
senderId: 'wxid_fixture_member',
sender: '测试成员',
timestamp: fixture.messages['group-regular-md5'][0].createTime * 1000,
messageIds: ['msg-text'],
sourceKind: 'text',
text: '这是一条脱敏测试消息',
score: 1
}
]
return {
requestId: request.requestId,
status: failure ? 'ai_failed' : 'completed',
plan: {
intent: 'general',
keywords: ['测试'],
variants: [],
source: 'local',
scopeLabel: '全局搜索',
rangeLabel: '近 30 天',
timeRange: {
startTime: Math.floor(Date.now() / 1000) - 30 * 86400,
endTime: Math.floor(Date.now() / 1000),
label: '近 30 天',
reason: 'E2E fixture',
source: 'ui'
},
contactNames: []
},
knowledge: {
source: 'knowledge',
state: 'ready',
indexedMessageCount: allFixtureMessages.length,
indexedChunkCount: 1,
totalMessages: allFixtureMessages.length,
voiceCoverage: {
voiceMessageCount: 1,
transcribedVoiceCount: 1,
failedVoiceCount: 0,
voiceCoverageComplete: true
}
},
candidateEvidenceCount: 1,
retrieval: {
intent: 'general',
timeRange: {
startTime: Math.floor(Date.now() / 1000) - 30 * 86400,
endTime: Math.floor(Date.now() / 1000),
label: '近 30 天',
reason: 'E2E fixture',
source: 'ui'
},
retrievalMode: 'global_fts',
candidateCount: 1,
uniqueCandidateCount: 1,
sourceMessageCount: allFixtureMessages.length,
sourceCoverage: 'complete',
isComplete: true,
fallbackUsed: false,
suspicious: false
},
evidence,
contextEvidenceCount: 1,
aggregation: {
messageCount: 1,
peopleCount: 1,
conversationCount: 1,
people: [],
conversations: []
},
agent: { mode: 'fallback', toolCalls: 0, trace: [], fallbackReason: 'E2E fixture' },
citationValidation: { status: 'valid', invalidCitationIds: [] },
timings: emptyTimings(),
answer: failure ? undefined : '固定假回答:测试数据中的核心流程正常。',
ai: {
providerName: '本地假服务',
modelName: '固定响应模型',
inputTokens: 10,
inputTokensEstimated: false
},
error: failure ? `本地假服务错误 ${failure}` : undefined,
errorStage: failure ? 'ai_generating' : undefined,
elapsedMs: 1
}
}
const reportJson = JSON.stringify({
overview: '固定脱敏日报',
hero: {
headline: '产品测试群日报',
summary: '测试消息已完成自动整理。',
keyTakeaway: '核心流程可用',
pendingNote: '',
statusLine: '今日形成 1 个结论'
},
topics: [
{
title: '自动化测试',
timeRange: '10:00-10:02',
heat: '中',
participants: ['测试成员'],
summary: '讨论了脱敏自动化测试。',
conclusions: [{ text: '核心流程可用', sourceMessageIds: ['msg-text'] }],
keywords: ['测试'],
sourceMessageIds: ['msg-text']
}
],
resources: [],
importantMessages: [],
quotes: [],
qa: [],
todos: [],
unresolved: [],
storylines: [],
reversals: [],
participantChains: [],
keywords: ['测试']
})
let connected = process.env.WXE_E2E_MODE !== 'disconnected'
let savedKey = connected ? VALID_KEY : ''
let settings = {
dbRoot: 'fixture-account',
apiEnabled: false,
apiHost: '127.0.0.1',
apiPort: 5031,
imageKeyRoot: 'fixture-account',
ffmpegPath: '',
recallProtectionEnabled: false,
debugEnabled: false,
autoLogin: connected,
autoLoginPreferenceSet: true,
appearanceTheme: process.env.WXE_E2E_APPEARANCE_THEME === 'dark' ? 'dark' : 'light',
compactMode: false,
showStartupProgress: false,
imageXorKey: '0x40',
imageAesKey: '0123456789abcdef'
}
const extraContacts = Number(process.env.WXE_E2E_LARGE_CONTACTS || 0)
const contacts = [...fixture.contacts]
for (let index = 0; index < extraContacts; index += 1) {
contacts.push({
m_nsUsrName: `fixture_${index}`,
m_nsNickName: `性能样本 ${index}`,
md5: `fixture-contact-${index}`,
type: index % 5 === 0 ? 'group' : 'user'
})
}
if (process.env.WXE_E2E_REALTIME_REORDER === '1') {
contacts.push(
{
m_nsUsrName: 'realtime_group_b@chatroom',
m_nsNickName: '自动刷新 B',
md5: 'realtime-group-b-md5',
type: 'group'
},
{
m_nsUsrName: 'realtime_group_a@chatroom',
m_nsNickName: '自动刷新 A',
md5: 'realtime-group-a-md5',
type: 'group'
},
{
m_nsUsrName: 'realtime_direct_d',
m_nsNickName: '自动刷新 D',
md5: 'realtime-direct-d-md5',
type: 'user'
},
{
m_nsUsrName: 'realtime_direct_c',
m_nsNickName: '自动刷新 C',
md5: 'realtime-direct-c-md5',
type: 'user'
}
)
}
const handlers = new Map()
const handle = (channel, fn) => {
handlers.set(channel, fn)
ipcMain.handle(channel, async (event, ...args) => fn(...args))
}
let contactReadCount = 0
const groupExitEvent = {
id: 'fixture-exit-event',
contactId: 'group-regular-md5',
roomId: 'group_regular@chatroom',
groupName: '产品测试群',
memberWxid: 'wxid_fixture_member',
memberName: '测试成员',
wechatName: '测试成员微信名',
groupRemark: '测试成员群备注',
contactRemark: '测试成员通讯录备注',
previousCount: 240,
currentCount: 239,
delta: -1,
message: '测试成员退出了产品测试群',
detectedAt: fixtureNowMs - 60 * 1000
}
let groupExitMonitorState = {
enabled: connected,
events: [groupExitEvent],
running: connected,
nativeMonitorActive: connected,
monitoredGroupCount: 2,
monitorSelectionConfigured: true,
monitoredRoomIds: ['group_regular@chatroom', 'group_folded@chatroom'],
lastCheckedAt: fixtureNowMs - 30 * 1000,
lastReadAt: 0,
unreadCount: 1
}
const cloneGroupExitMonitorState = () => ({
...groupExitMonitorState,
events: groupExitMonitorState.events.map((event) => ({ ...event })),
unreadCount: groupExitMonitorState.events.filter(
(event) => event.detectedAt > groupExitMonitorState.lastReadAt
).length
})
handle('group-exit-monitor:getState', () => cloneGroupExitMonitorState())
// 只保存**监控范围**:通知配置已迁到自动化规则。
handle('group-exit-monitor:setGroups', (roomIds) => {
const selected = Array.isArray(roomIds)
? roomIds.filter((roomId) => typeof roomId === 'string' && roomId.endsWith('@chatroom'))
: []
groupExitMonitorState = {
...groupExitMonitorState,
monitorSelectionConfigured: true,
monitoredRoomIds: [...new Set(selected)],
monitoredGroupCount: selected.length,
lastCheckedAt: fixtureNowMs
}
return cloneGroupExitMonitorState()
})
handle('group-exit-monitor:checkNow', () => {
groupExitMonitorState = {
...groupExitMonitorState,
lastCheckedAt: fixtureNowMs
}
return cloneGroupExitMonitorState()
})
handle('group-exit-monitor:clearEvents', () => {
groupExitMonitorState = {
...groupExitMonitorState,
events: [],
lastReadAt: fixtureNowMs
}
return cloneGroupExitMonitorState()
})
handle('group-exit-monitor:markRead', (readAt) => {
const requestedAt = Number(readAt)
groupExitMonitorState = {
...groupExitMonitorState,
lastReadAt: Math.max(
groupExitMonitorState.lastReadAt,
Number.isFinite(requestedAt) && requestedAt > 0 ? requestedAt : fixtureNowMs
)
}
return cloneGroupExitMonitorState()
})
// ---- Automation v1(@我生成日报)----
//
// 这组桩只服务 UI / 视觉评审:让自动化首页拿到一份确定性的规则与群列表,
// 而不是整页挂着「自动化接口尚未就绪」。
// 它**不**参与任何真实发送,也**不**代表自动化功能已迁移。
const automationGroupOptions = () =>
contacts
.filter(
(contact) =>
contact.type === 'group' || String(contact.m_nsUsrName || '').endsWith('@chatroom')
)
.map((contact) => ({
id: contact.m_nsUsrName,
name: contact.m_nsNickName || contact.m_nsUsrName
}))
const builtinDailyReportRule = () => ({
id: 'builtin-mention-me-daily-report',
name: '@我生成日报',
enabled: true,
ruleType: 'daily_report',
trigger: 'message',
scope: 'group',
conditions: {
requireMentionMe: true,
keyword: '日报',
keywordMatchMode: 'contains',
conversationIds: [],
ignoreSelf: true
},
actions: [
{ type: 'replyText', enabled: true, text: '收到,正在生成今日日报' },
{ type: 'generateReport', enabled: true },
{ type: 'sendReportImage', enabled: true }
],
cooldownSeconds: 60,
replyDelaySeconds: 2,
createdAt: fixtureNowMs,
updatedAt: fixtureNowMs
})
/*
* ⚠️ 这里的默认值必须与 `src/shared/automation.ts` 的
* `createDefaultLeaveNotificationRule()` **逐字一致**。
*
* 它扮演"一个全新安装的 main 进程":一旦漂移,E2E 与截图验证的就不是真实默认行为。
* 由 `tests/integration/automation-default-contract.test.ts` 锁住。
*/
let leaveNotificationRule = {
id: 'builtin-leave-notification',
name: '退群通知',
enabled: true,
ruleType: 'leave_notification',
trigger: 'message',
scope: 'group',
conditions: {
requireMentionMe: false,
keyword: '',
keywordMatchMode: 'contains',
conversationIds: [],
ignoreSelf: true
},
actions: [],
cooldownSeconds: 0,
replyDelaySeconds: 2,
leaveNotification: {
// 默认目标 = 当前群聊(旧退群监控通知的原始行为)。
target: { type: 'source_chat' },
template:
'[退群监测]\n\n群聊: {groupName}\n\n用户: {user}\n\n群备注: {groupRemark}\n\n微信号: {wxid}\n\n人数: {previousCount} -> {currentCount}\n\n退群时间: {time}'
},
createdAt: fixtureNowMs,
updatedAt: fixtureNowMs
}
/** 「指定好友」候选:个人联系人,排除群 / 公众号 / 文件传输助手 / 自己。 */
const sendableContactOptions = () =>
contacts
.filter(
(contact) =>
contact.type !== 'group' &&
!String(contact.m_nsUsrName || '').endsWith('@chatroom') &&
!String(contact.m_nsUsrName || '').startsWith('gh_') &&
contact.m_nsUsrName !== 'filehelper'
)
.map((contact) => ({
id: contact.m_nsUsrName,
name: contact.m_nsNickName || contact.m_nsUsrName
}))
handle('automation:getStatus', () => ({
listening: connected,
listeningDegraded: false,
todayExecutions: connected ? 3 : 0,
todaySuccesses: connected ? 3 : 0,
sendCapability: {
supported: personalWechatSupported,
ready: connected,
canSendText: connected,
canSendImage: connected,
message: connected ? '个人微信发送能力已就绪' : '尚未绑定个人微信发送能力'
}
}))
/*
* 定时日报是 `ruleType === 'scheduled_report'` 的**真规则**:列表由
* `automation:listRules` 过滤得到,创建 / 更新走 `automation:createRule` /
* `automation:updateRule`。fixture 必须实现这组通道,否则保存后列表读不回来。
*/
const scheduledReportRules = []
let automationRuleSequence = 0
// 「微信异常通知」是全局开关,新旧两套通道共用同一份状态。
let scheduledReportNotificationEnabled = false
handle('automation:listRules', () => [
builtinDailyReportRule(),
structuredClone(leaveNotificationRule),
...scheduledReportRules.map((rule) => structuredClone(rule))
])
handle('automation:createRule', (draft) => {
automationRuleSequence += 1
const rule = {
...draft,
id: `fixture-scheduled-rule-${automationRuleSequence}`,
createdAt: fixtureNowMs,
updatedAt: fixtureNowMs
}
scheduledReportRules.push(rule)
return structuredClone(rule)
})
// `updateRule` / `setRuleEnabled` 收的是**单个对象**(`{ id, draft }` / `{ id, enabled }`),
// 与 `createRule(draft)` / `deleteRule(id)` 的位置参数不同 —— preload 就是这么拼的。
handle('automation:updateRule', (input) => {
const { id, draft } = input || {}
const index = scheduledReportRules.findIndex((rule) => rule.id === id)
if (index < 0) return null
scheduledReportRules[index] = {
...scheduledReportRules[index],
...(draft || {}),
id,
updatedAt: fixtureNowMs
}
return structuredClone(scheduledReportRules[index])
})
handle('automation:deleteRule', (id) => {
const index = scheduledReportRules.findIndex((rule) => rule.id === id)
if (index < 0) return false
scheduledReportRules.splice(index, 1)
return true
})
handle('automation:setRuleEnabled', (input) => {
const { id, enabled } = input || {}
const index = scheduledReportRules.findIndex((rule) => rule.id === id)
if (index < 0) return null
scheduledReportRules[index] = {
...scheduledReportRules[index],
enabled: enabled === true,
updatedAt: fixtureNowMs
}
return structuredClone(scheduledReportRules[index])
})
/*
* 微信异常通知(定时日报的全局能力,不是单条规则的配置)。
*
* 能力判定与 `ScheduledReportService#checkNotificationCapability` 一致:**Agent Hub 离线
* 就是不可用**,且要带上那句可操作的原因。fixture 里 Hub 是 offline,所以这里如实回
* 不可用 —— 不为了让开关点得动而假装就绪。
*
* 这四个通道生产环境都有;不注册会被渲染层的降级兜底吞掉,页面看起来"正常",
* 但测的就不是真实文案了。
*/
handle('automation:getScheduledReportNotificationSettings', () => ({
enabled: scheduledReportNotificationEnabled
}))
handle('automation:getScheduledReportNotificationCapability', () =>
agentHubStatus().hub === 'online'
? { ready: true, recipient: 'wxid_fixture_self' }
: {
ready: false,
reason: 'agent_hub_offline',
error: '需要先连接 Agent Hub 微信机器人,才能接收异常通知。'
}
)
handle('automation:setScheduledReportNotificationEnabled', (enabled) => {
if (enabled && agentHubStatus().hub !== 'online') {
scheduledReportNotificationEnabled = false
return {
success: false,
data: { enabled: false },
reason: 'agent_hub_offline',
error: '需要先连接 Agent Hub 微信机器人,才能接收异常通知。'
}
}
scheduledReportNotificationEnabled = Boolean(enabled)
return { success: true, data: { enabled: scheduledReportNotificationEnabled } }
})
handle('automation:testScheduledReportErrorNotification', (ruleId) =>
scheduledReportRules.some((rule) => rule.id === ruleId)
? {
success: true,
data: { ruleId, notificationId: 'fixture-notification', sentAt: fixtureNowMs }
}
: { success: false, error: '未找到这条定时日报规则' }
)
handle('automation:listGroups', () => automationGroupOptions())
handle('automation:listExecutions', () => [])
handle('automation:clearExecutions', () => true)
handle('automation:listSendableContacts', () => sendableContactOptions())
handle('automation:saveLeaveNotificationRule', (draft) => {
leaveNotificationRule = {
...leaveNotificationRule,
...(draft && typeof draft === 'object' ? draft : {}),
id: 'builtin-leave-notification',
ruleType: 'leave_notification',
updatedAt: fixtureNowMs
}
if (leaveNotificationRule.leaveNotification) {
const { targetNeedsReview, ...rest } = leaveNotificationRule.leaveNotification
void targetNeedsReview
leaveNotificationRule.leaveNotification = rest
}
return structuredClone(leaveNotificationRule)
})
/*
* 定时日报任务。
*
* 默认**为空**(既有 E2E 依赖这个初始状态)。
* `WXE_E2E_SCHEDULED_FIXTURE=1` 启动时注入三条任务,
* 让「自动化 → 定时日报」的列表与卡片走**真实读取路径**,而不是在渲染层塞假数据。
*/
const scheduledReportTasks =
process.env.WXE_E2E_SCHEDULED_FIXTURE === '1'
? [
{
id: 'fixture-scheduled-1',
name: 'TraceMemo 每日晚报',
group: 'TraceMemo 交流群',
scheduleTime: '18:21',
reportRange: 'today',
target: 'TraceMemo 管理群',
enabled: true,
createdAt: new Date(fixtureNowMs - 86_400_000).toISOString(),
updatedAt: new Date(fixtureNowMs - 86_400_000).toISOString(),
lastRunAt: new Date(fixtureNowMs - 86_400_000).toISOString(),
nextRunAt: new Date(fixtureNowMs + 3_600_000).toISOString()
},
{
id: 'fixture-scheduled-2',
name: '技术交流群日报',
group: '技术交流群',
scheduleTime: '09:00',
reportRange: 'yesterday',
target: '技术交流群',
enabled: true,
createdAt: new Date(fixtureNowMs - 172_800_000).toISOString(),
updatedAt: new Date(fixtureNowMs - 172_800_000).toISOString(),
nextRunAt: new Date(fixtureNowMs + 64_800_000).toISOString()
},
{
id: 'fixture-scheduled-3',
name: 'AI 编程讨论群日报',
group: 'AI 编程讨论群',
scheduleTime: '21:30',
reportRange: 'recent24h',
target: 'TechMemo 归档群',
enabled: false,
createdAt: new Date(fixtureNowMs - 259_200_000).toISOString(),
updatedAt: new Date(fixtureNowMs - 259_200_000).toISOString(),
nextRunAt: new Date(fixtureNowMs + 86_400_000).toISOString()
}
]
: []
const scheduledReportExecutions = []
const generatedReports = []
const scheduledReportNextRun = (scheduleTime) => {
const [hours, minutes] = String(scheduleTime || '09:00')
.split(':')
.map(Number)
const next = new Date(fixtureNowMs)
next.setHours(Number.isFinite(hours) ? hours : 9, Number.isFinite(minutes) ? minutes : 0, 0, 0)
if (next.getTime() <= fixtureNowMs) next.setDate(next.getDate() + 1)
return next.toISOString()
}
const startupCache = () => ({
self: fixture.self,
contacts,
updatedAt: Date.now()
})
handle('settings:get', () => ({ settings, settingsPath: path.join(userData, 'settings.json') }))
handle('settings:set', (patch) => {
settings = { ...settings, ...patch }
return { settings, settingsPath: path.join(userData, 'settings.json') }
})
handle('tts:getSettings', () => ({
success: true,
settings: {
provider: 'fish-audio',
hasApiKey: false,
hasStoredApiKey: false,
hasEnvironmentApiKey: false,
keySource: 'missing',
encryptionAvailable: true,
selectedVoiceId: '',
outputFormat: 'mp3',
model: 's2.1-pro-free',
phase: 'ready'
},
voices: []
}))
handle('wechat-personal:getStatus', () => ({
state: 'online',
platform: process.platform,
arch: process.arch,
sipDisabled: true,
wechatRunning: true,
wechatPid: 4668,
boundWechatPid: 4668,
endpoint: '127.0.0.1:4290',
endpointReady: true,
wechatVersion: '4.1.11.53',
runtimeReady: true,
attachReady: true,
baseAddress: '0x114ef8000',
baseAddressReady: true,
textHookInstalled: true,
textHookReady: true,
imageHookInstalled: true,
imageHookReady: true,
messageListenerReady: true,
canSend: true,
canSendText: true,
canSendImage: true,
canSendVoice: true,
message: '个人微信已绑定'
}))
handle('wechat-personal:getSendCapability', () => ({
supported: personalWechatSupported,
ready: personalWechatSupported,
status: personalWechatSupported ? 'ready' : 'unsupported',
capabilities: {
text: personalWechatSupported,
image: personalWechatSupported,
voice: personalWechatSupported
},
senderStatus: {
state: personalWechatSupported ? 'online' : 'unsupported_platform',
platform: process.platform,
arch: process.arch,
sipDisabled: true,
wechatRunning: true,
boundWechatPid: 4668,
endpoint: '127.0.0.1:4290',
endpointReady: true,
runtimeReady: true,
attachReady: true,
baseAddressReady: true,
textHookInstalled: true,
textHookReady: true,
imageHookInstalled: true,
imageHookReady: true,
messageListenerReady: true,
canSend: personalWechatSupported,
canSendText: personalWechatSupported,
canSendImage: personalWechatSupported,
canSendVoice: personalWechatSupported,
message: '个人微信已绑定'
},
message: personalWechatSupported
? '个人微信已准备好发送日报'
: '微信消息发送目前仅支持 macOS 和 Windows'
}))
handle('scheduled-report:list', () => [...scheduledReportTasks])
handle('scheduled-report:listExecutions', (taskId) =>
taskId
? scheduledReportExecutions.filter((execution) => execution.taskId === taskId)
: [...scheduledReportExecutions]
)
handle('scheduled-report:getNotificationSettings', () => ({
enabled: scheduledReportNotificationEnabled
}))
handle('scheduled-report:setNotificationEnabled', (enabled) => {
if (enabled) {
return {
success: false,
data: { enabled: false },
reason: 'agent_hub_offline',
error: '需要先连接 Agent Hub 微信机器人,才能接收异常通知。'
}
}
scheduledReportNotificationEnabled = false
return { success: true, data: { enabled: false } }
})
handle('scheduled-report:create', (request) => {
const now = new Date(fixtureNowMs).toISOString()
const task = {
id: `fixture-scheduled-report-${scheduledReportTasks.length + 1}`,
name: request.name,
group: request.group,
scheduleTime: request.scheduleTime,
reportRange: request.reportRange || 'yesterday',
messageTypes: request.messageTypes,
templateId: request.templateId,
memberNameMode: request.memberNameMode,
timeoutSeconds: request.timeoutSeconds,
target: request.target || request.group,
enabled: request.enabled !== false,
createdAt: now,
updatedAt: now,
nextRunAt: scheduledReportNextRun(request.scheduleTime)
}
scheduledReportTasks.push(task)
return { success: true, data: task }
})
handle('scheduled-report:update', (taskId, request) => {
const index = scheduledReportTasks.findIndex((task) => task.id === taskId)
if (index < 0) return { success: false, error: '任务不存在' }
const current = scheduledReportTasks[index]
const next = { ...current, ...request, updatedAt: new Date(fixtureNowMs).toISOString() }
if (request.scheduleTime) next.nextRunAt = scheduledReportNextRun(request.scheduleTime)
scheduledReportTasks[index] = next
return { success: true, data: next }
})
handle('scheduled-report:delete', (taskId) => {
const index = scheduledReportTasks.findIndex((task) => task.id === taskId)
if (index < 0) return { success: false, error: '任务不存在' }
scheduledReportTasks.splice(index, 1)
return { success: true, data: { deletedId: taskId } }
})
handle('scheduled-report:setEnabled', (taskId, enabled) => {
const task = scheduledReportTasks.find((item) => item.id === taskId)
if (!task) return { success: false, error: '任务不存在' }
task.enabled = Boolean(enabled)
task.updatedAt = new Date(fixtureNowMs).toISOString()
task.nextRunAt = task.enabled ? scheduledReportNextRun(task.scheduleTime) : task.nextRunAt
return { success: true, data: task }
})
handle('scheduled-report:runNow', (taskId) => {
const task = scheduledReportTasks.find((item) => item.id === taskId)
if (!task) return { success: false, error: '任务不存在' }
const pngPath = path.join(
userData,
`fixture-scheduled-report-${scheduledReportExecutions.length + 1}.png`
)
fs.writeFileSync(pngPath, Buffer.from(imageData.split(',')[1], 'base64'))
const execution = {
id: `fixture-execution-${scheduledReportExecutions.length + 1}`,
taskId,
triggerType: 'manual',
startedAt: new Date(fixtureNowMs).toISOString(),
finishedAt: new Date(fixtureNowMs).toISOString(),
status: 'success',
currentStage: 'send',
scheduledSlot: new Date(fixtureNowMs).toISOString().slice(0, 10),
retryCount: 0,
reportId: `fixture-report-record-${scheduledReportExecutions.length + 1}`,
pngPath,
sendTarget: task.target,
sendStatus: 'success',
notificationStatus: 'not_needed',
message: '日报生成成功,微信发送成功'
}
scheduledReportExecutions.push(execution)
task.lastRunAt = execution.finishedAt
return { success: true, data: execution }
})
handle('scheduled-report:retrySend', (executionId) => {
const execution = scheduledReportExecutions.find((item) => item.id === executionId)
if (!execution) return { success: false, error: '执行记录不存在' }
execution.status = 'success'
execution.currentStage = 'send'
execution.finishedAt = new Date(fixtureNowMs).toISOString()
execution.retryCount = Number(execution.retryCount || 0) + 1
execution.sendStatus = 'success'
execution.message = '日报生成成功,微信发送成功'
return { success: true, data: execution }
})
handle('scheduled-report:testErrorNotification', (taskId) => {
const task = scheduledReportTasks.find((item) => item.id === taskId)
if (!task) return { success: false, error: '任务不存在' }
const now = new Date(fixtureNowMs).toISOString()
const execution = {
id: `fixture-debug-execution-${scheduledReportExecutions.length + 1}`,
taskId,
triggerType: 'scheduled',
startedAt: now,
finishedAt: now,
status: 'failed',
currentStage: 'notify',
failedStage: 'report',
errorCode: 'DEBUG_TEST_NOTIFICATION',
technicalMessage: '这是调试用的模拟错误信息;本次没有调用 AI。',
userTitle: '定时日报错误通知测试',
userMessage: '这是一条调试用的模拟错误通知,用于验证 Agent Hub 推送链路。',
suggestedAction: '确认微信中是否收到这条测试通知。',
retryable: false,
retryCount: 0,
sendStatus: 'unavailable',
notificationStatus: 'sent'
}
scheduledReportExecutions.unshift(execution)
return { success: true, data: execution }
})
handle('wechat-share:getConfig', () => ({
success: true,
configured: true,
serviceUrl: 'https://share.example.test'
}))
handle('key:getSavedDbKey', () => ({
success: true,
key: savedKey || undefined,
saved: Boolean(savedKey),
encryptionAvailable: true
}))
handle('key:saveDbKey', (_accountRoot, key) => {
savedKey = String(key || '')
return { success: true, key: savedKey, saved: true, encryptionAvailable: true }
})
handle('key:clearSavedDbKey', () => {
savedKey = ''
return { success: true }
})
handle('key:getEnvironment', () => ({
platform: process.platform,
osVersion: process.platform === 'win32' ? 'Windows fixture' : 'macOS fixture',
appVersion: 'v2.2.0',
wechatVersion: '4.1.9.57',
dataStructureVersion: settings.dbRoot === 'fixture-account' ? '微信 4.x(WCDB)' : '未检测到',
dataDirectoryDetected: settings.dbRoot === 'fixture-account',
diagnosticSummary: 'TraceMemo: v2.2.0\n数据目录: 已检测到',
autoDetectSupported: true,
wechatRunning: true,
accountIdentified: connected,
dbConnected: connected,
encryptionAvailable: true
}))
handle('key:readClipboardDbKey', () => ({ success: true, value: VALID_KEY }))
handle('key:pasteAndSaveDbKey', () => ({ success: true, key: VALID_KEY }))
handle('key:autoGetDbKey', () => ({ success: true, key: VALID_KEY, saved: false }))
handle('key:autoGetImageKey', () => ({
success: true,
xorKey: 64,
aesKey: '0123456789abcdef',
verified: true
}))
handle('db:init', (key, accountRoot) => {
if (settings.dbRoot === 'Z:\\missing-wechat-data') {
connected = false
return {
success: false,
code: 'ROOT_UNAVAILABLE',
error: '微信数据目录不存在,请重新选择目录',
monitoring: false
}
}
if (key !== VALID_KEY) {
connected = false
return { success: false, error: '数据库密钥无效', monitoring: false }
}
connected = true
settings.dbRoot = accountRoot || settings.dbRoot
return { success: true, monitoring: true }
})
handle('db:testConnection', (key) =>
key === VALID_KEY
? { success: true, wxid: fixture.self.wxid, accountRoot: fixture.self.accountRoot }
: { success: false, code: 'DATABASE_OPEN_FAILED', error: '数据库密钥无效' }
)
handle('db:disconnect', () => {
connected = false
return { success: true }
})
handle('db:getStartupCache', () =>
process.env.WXE_E2E_CORRUPT_CACHE === '1' ? null : startupCache()
)
handle('db:getBootstrapCache', () =>
process.env.WXE_E2E_CORRUPT_CACHE === '1' ? null : startupCache()
)
handle('db:getContacts', (filter) => {
contactReadCount += 1
const query = String(filter || '').toLowerCase()
return query
? contacts.filter((contact) => contact.m_nsNickName.toLowerCase().includes(query))
: contacts
})
handle('test:getContactReadCount', () => contactReadCount)
handle('db:getContactAvatars', (usernames) =>
Object.fromEntries(
contacts
.filter((contact) => usernames.includes(contact.m_nsUsrName) && contact.avatar)
.map((contact) => [contact.m_nsUsrName, contact.avatar])
)
)
handle('wechat-action-log:list', () => [
{
id: 'fixture-action-log',
category: 'wechat_send',
source: 'scheduled_report',
purpose: 'scheduled_report',
triggerType: 'automation',
timestamp: new Date(fixtureNowMs - 60_000).toISOString(),
recipientType: 'group',
recipientId: 'group_regular@chatroom',
recipientName: '产品测试群',
contentType: 'image',
contentPreview: 'fixture-report.png',
status: 'sent',
executionId: 'fixture-execution',
idempotencyKey: 'scheduled_report:fixture-execution'
}
])
handle('settings:getSelf', () => ({ ready: true, info: fixture.self }))
handle('db:getCachedMessages', (md5) => fixture.messages[md5] || [])
handle('db:getCachedMessagePage', (md5) => ({
hit: true,
messages: fixture.messages[md5] || [],
groupSnapshot: null
}))
handle('db:getMessages', (md5, startTime, endTime, options) => {
let messages = fixture.messages[md5] || []
if (startTime) messages = messages.filter((message) => (message.createTime || 0) >= startTime)
if (endTime) messages = messages.filter((message) => (message.createTime || 0) <= endTime)
if (options && options.limit) messages = messages.slice(-options.limit)
return messages
})
handle('test:messageChange', (payload = {}) => {
const md5 = String(payload.md5 || '')
const message = payload.message
if (md5 && message && Array.isArray(fixture.messages[md5])) {
fixture.messages[md5].push(message)
}
const reorderContactMd5 = String(payload.reorderContactMd5 || '')
if (reorderContactMd5) {
const index = contacts.findIndex((contact) => contact.md5 === reorderContactMd5)
if (index >= 0) {
const [contact] = contacts.splice(index, 1)
contacts.unshift(contact)
}
}
const event = payload.event || { db: 'message_0.db', table: 'message', action: 'update' }
const json = typeof event === 'string' ? event : JSON.stringify(event)
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type: 'update', json })
}
return { success: true }
})
handle('db:getGroupSnapshot', (md5) =>
md5.startsWith('group-')
? {
roomId: md5,
memberCount: 1,
members: [
{
wxid: 'wxid_fixture_member',
nickname: '测试成员',
groupNickname: '测试成员',
wechatNickname: '测试成员',
remark: '',
avatar: ''
}
]
}
: null
)
handle('db:getImage', (md5, datName, sessionId, options) =>
md5 === 'unsupported'
? { success: false, error: '不支持的 DAT 版本' }
: {
success: true,
data: imageData,
isThumb: !options?.force,
filePath: path.join(userData, options?.force ? 'original.png' : 'thumbnail.png')
}
)
handle('db:getVoiceData', () => ({ success: true, data: voiceData }))
const voiceModelStatus = (state = 'missing') => ({
modelId: 'sensevoice-small-int8',
version: '2024-07-17',
state,
downloadedBytes: state === 'ready' ? 239549735 : 0,
totalBytes: 239549735,
progress: state === 'ready' ? 1 : 0,
platform: process.platform,
architecture: process.arch,
supported: process.platform === 'win32' || process.platform === 'darwin'
})
handle('voice:getModelStatus', () => voiceModelStatus())
handle('voice:downloadModel', () => ({ success: true, status: voiceModelStatus('ready') }))
handle('voice:cancelModelDownload', () => ({ success: true }))
handle('voice:removeModel', () => voiceModelStatus())
handle('voice:openModelDirectory', () => ({ success: true }))
handle('voice:getBatchProgress', () => ({
accountIdentity: 'fixture-account',
state: 'idle',
total: 0,
processed: 0,
cached: 0,
succeeded: 0,
failed: 0,
elapsedMs: 0,
estimatedRemainingMs: null
}))
handle('voice:getBatchConversationSummaries', (request) =>
request.conversationIds.map((conversationId, index) => ({
conversationId,
voiceMessageCount: index + 3
}))
)
handle('voice:getBatchPreflight', (request) => ({
accountIdentity: 'fixture-account',
conversationCount: request.conversationIds.length,
voiceMessageCount: request.conversationIds.length * 3,
cachedCount: 0,
pendingCount: request.conversationIds.length * 3,
failedCount: 0,
estimatedDurationMs: null,
modelReady: false
}))
handle('voice:recognize', () => ({ success: true, transcript: '固定脱敏转写文本', language: 'zh' }))
handle('voice:cancelRecognition', () => ({ success: true }))
handle('db:getSticker', (url) =>
String(url || '').includes('403')
? {
success: false,
error: '表情链接已失效或需要微信授权',
failureCode: 'access_denied',
httpStatus: 403
}
: { success: true, data: imageData }
)
handle('db:parseMessage', (content, messageType) =>
messageType === 1
? { type: 'text', content: String(content) }
: { type: 'unknown', raw: String(content), messageType }
)
handle('ai:getRuntimeConfig', () => ({
providerId: 'fixture-provider',
providerName: '本地假服务',
model: 'fixture-model',
modelName: '固定响应模型',
configured: true,
status: 'connected',
timeoutMs: 5000
}))
handle('ai:getVisionRuntimeConfig', () => ({
providerId: 'fixture-vision-provider',
providerName: '本地图片假服务',
model: 'fixture-vision-model',
modelName: '固定图片识别模型',
configured: true,
status: 'connected',
timeoutMs: 5000,
source: 'vision-capability'
}))
handle('ai:listProviders', () => ({
success: true,
providers: [
{
id: 'fixture-provider',
name: '本地假服务',
type: 'openai-compatible',
baseUrl: 'http://127.0.0.1:1/v1',
auth: { type: 'none' },
models: [
{
id: 'fixture-model',
name: '固定响应模型',
capabilities: { chat: true, vision: false, ocr: false, longContext: true }
},
{
id: 'fixture-vision-model',
name: '固定图片识别模型',
capabilities: { chat: true, vision: true, ocr: true, longContext: true }
}
],
defaultModel: 'fixture-model',
advanced: { timeoutMs: 5000, extraHeaders: {} },
hasApiKey: true,
isDefault: true,
status: 'connected'
}
],
defaultProviderId: 'fixture-provider'
}))
handle('ai:migrateLegacy', () => ({ success: true, providers: [] }))
handle('ai:chat', (messages) => {
const failure = process.env.WXE_E2E_AI_FAILURE
if (failure) return { success: false, error: `本地假服务错误 ${failure}` }
const system = String(messages?.[0]?.content || '')
if (system.includes('本地聊天检索规划器')) {
return {
success: true,
data: '{"intent":"general","keywords":["测试"],"variants":[]}'
}
}
if (system.includes('微信群聊日报编辑') || system.includes('JSON 格式修复器')) {
return { success: true, data: reportJson, usage: { input: 10, output: 20, total: 30 } }
}
return { success: true, data: '固定假回答:测试数据中的核心流程正常。' }
})
handle('knowledge:getStatus', () => ({
accountId: fixture.self.wxid,
state: 'ready',
indexedMessageCount: allFixtureMessages.length,
indexedChunkCount: 1,
sourceMessageCount: allFixtureMessages.length,
processedMessages: allFixtureMessages.length,
totalMessages: allFixtureMessages.length,
estimatedRemainingMs: 0,
databaseBytes: 1024,
walBytes: 0,
shmBytes: 0
}))
handle('knowledge:startIndex', () => ({ success: true }))
handle('knowledge:search', () => ({
source: 'knowledge',
state: 'ready',
evidence: [],
indexedMessageCount: allFixtureMessages.length,
indexedChunkCount: 1,
totalMessages: allFixtureMessages.length,
timings: {
workerIpcMs: 0,
workerBootMs: 0,
dispatchMs: 0,
workerSqlMs: 0,
responseTransferMs: 0,
responseSerializeMs: 0,
ftsMs: 0,
messageLoadMs: 0,
chunkExpandMs: 0,
rankingMs: 0,
totalMs: 0
}
}))
handle('ai-search:getProviderStatus', () => ({ configured: true, requiresConsent: false }))
handle('ai-search:authorizeExternalProvider', () => ({ success: true }))
handle('ai-search:run', (request) => aiSearchResult(request))
handle('ai-search:cancel', () => ({ cancelled: true }))
handle('report:export', () => {
const htmlPath = path.join(userData, 'fixture-report.html')
const pngPath = path.join(userData, 'fixture-report.png')
fs.writeFileSync(htmlPath, '<!doctype html><h1>固定脱敏日报</h1>', 'utf8')
fs.writeFileSync(pngPath, Buffer.from(imageData.split(',')[1], 'base64'))
return { success: true, imageDataUrl: imageData, htmlPath, pngPath }
})
handle('report:exportSnapshot', () => {
const htmlPath = path.join(userData, 'fixture-report-snapshot.html')
const pngPath = path.join(userData, 'fixture-report-snapshot.png')
fs.writeFileSync(htmlPath, '<!doctype html><h1>固定脱敏模板快照日报</h1>', 'utf8')
fs.writeFileSync(pngPath, Buffer.from(imageData.split(',')[1], 'base64'))
return { success: true, imageDataUrl: imageData, htmlPath, pngPath }
})
handle('report:prepareTemplateSwitch', () => ({
success: true,
snapshot: {
groupName: '固定脱敏群',
reportDate: '2026-08-12',
values: { REPORT_TITLE: '固定脱敏群日报' }
}
}))
handle('report:listGenerated', () => ({ success: true, reports: [...generatedReports] }))
handle('report:saveGenerated', (request) => {
const record = {
id: 'fixture-report-record',
...request,
htmlStatus: request.htmlPath ? 'ready' : 'missing',
pngStatus: request.pngPath ? 'ready' : 'missing'
}
const index = generatedReports.findIndex((report) => report.id === record.id)
if (index >= 0) generatedReports[index] = record
else generatedReports.unshift(record)
return { success: true, record }
})
handle('report:updateGeneratedTemplate', (request) => ({
success: true,
record: { id: request.reportId, templateId: request.templateId }
}))
handle('report:deleteGenerated', () => ({ success: true }))
handle('report:reveal', () => ({ success: true }))
handle('copy-image', () => ({ success: true }))
// 社区模板市场:默认关闭(返回空目录),测试用例可通过 WXE_E2E_TEMPLATE_MARKET=fixture 打开合成目录。
const templateMarketEnabled = process.env.WXE_E2E_TEMPLATE_MARKET === 'fixture'
const templateMarketEntry = {
id: 'community.github.example.neonboard',
version: '1.0.2',
interfaceVersion: '1',
name: '霓光指挥日报',
description: '霓光主题的桌面指挥日报,突出实时讨论、重点消息与行动项。',
author: 'TraceMemo 社区',
platform: 'desktop',
tags: ['desktop'],
license: 'MIT',
minAppVersion: null,
download: 'https://example.com/packages/community.github.example.neonboard/1.0.2/template.zip',
sizeBytes: 2048,
sha256: 'b'.repeat(64),
status: 'published'
}
const templateMarketInstalled = []
handle('report-template:list', () =>
templateMarketInstalled.map((entry) => ({
id: entry.id,
version: entry.version,
interfaceVersion: entry.interfaceVersion,
source: 'installed',
name: entry.name,
author: entry.author,
entryPath: path.join(userData, 'report-templates', entry.id, 'template.html'),
capture: { width: 1280, maxWidth: 1280, maxHeight: 4000 },
license: { spdx: entry.license },
installedAt: '2026-09-15T00:00:00.000Z'
}))
)
handle('report-template-market:list', () =>
templateMarketEnabled
? {
success: true,
catalog: {
schemaVersion: '1',
generatedAt: '2026-09-15T00:00:00.000Z',
status: 'published',
templates: [templateMarketEntry]
}
}
: { success: true, catalog: { schemaVersion: '1', status: 'published', templates: [] } }
)
handle('report-template-market:install', (id, version) => {
if (
!templateMarketEnabled ||
String(id) !== templateMarketEntry.id ||
String(version) !== templateMarketEntry.version
) {
return { success: false, error: '模板不存在', code: 'unknown_template' }
}
if (!templateMarketInstalled.some((entry) => entry.id === id && entry.version === version)) {
templateMarketInstalled.push(templateMarketEntry)
}
return { success: true, catalogEntry: templateMarketEntry }
})
handle('report-template:uninstall', (id, version) => {
const index = templateMarketInstalled.findIndex(
(entry) => entry.id === String(id) && entry.version === String(version)
)
if (index >= 0) templateMarketInstalled.splice(index, 1)
return { success: true }
})
handle('api:copyText', () => ({ success: true }))
handle('app-log:write', (entry) => {
const safe = JSON.stringify(entry)
.replace(/\b(?:0x)?[a-f0-9]{64}\b/gi, '***')
.replace(/\bsk-[a-z0-9_-]{8,}\b/gi, '***')
fs.mkdirSync(path.join(userData, 'logs'), { recursive: true })
fs.appendFileSync(path.join(userData, 'logs', 'e2e.log'), `${safe}\n`, 'utf8')
})
handle('app-log:getPath', () => path.join(userData, 'logs', 'e2e.log'))
handle('app-log:reveal', () => undefined)
const cacheSummary = () => ({
items: [
{
id: 'bootstrap',
label: '启动缓存',
description: '联系人和会话的本地启动快照',
sizeBytes: 4096,
fileCount: 2
},
{
id: 'electron',
label: '界面缓存',
description: 'Electron 页面资源和网络缓存',
sizeBytes: 2 * 1024 * 1024,
fileCount: 12
},
{
id: 'knowledge',
label: '本地知识库索引',
description: '问问微信使用的本地检索索引',
sizeBytes: 12 * 1024 * 1024,
fileCount: 8
}
],
totalBytes: 14 * 1024 * 1024 + 4096,
updatedAt: fixtureNowMs
})
handle('cache:getSummary', cacheSummary)
handle('cache:clear', cacheSummary)
handle('api:getStatus', () => ({ running: false, host: settings.apiHost, port: settings.apiPort }))
handle('api:tokenStatus', () => ({
success: true,
available: true,
hasToken: true,
maskedToken: '••••••••••••••••'
}))
handle('api:revealToken', () => ({
available: true,
hasToken: true,
maskedToken: '••••••••••••••••',
token: 'fixture-api-token'
}))
handle('api:copyToken', () => ({
success: true,
available: true,
hasToken: true,
maskedToken: '••••••••••••••••'
}))
handle('api:rotateToken', () => ({
success: true,
available: true,
hasToken: true,
maskedToken: '••••••••••••••••'
}))
handle('api:copyCurl', () => ({ success: true }))
handle('api:skillStatus', () => ({
available: true,
version: 'v1.2',
filePath: '/fixture/tracememo-reader/SKILL.md',
directoryPath: '/fixture/tracememo-reader',
source: 'development',
githubUrl: 'https://example.test/tracememo-reader'
}))
handle('api:readSkill', () => ({
success: true,
content:
'# TraceMemo Reader\n\n## 能力\n- 读取本地聊天记录\n- 导出群聊日报\n\n仅在用户授权后访问。'
}))
handle('api:start', () => ({ running: true, host: settings.apiHost, port: settings.apiPort }))
handle('api:stop', () => ({ running: false, host: settings.apiHost, port: settings.apiPort }))
handle('api:toggle', (enabled) => ({
running: enabled,
host: settings.apiHost,
port: settings.apiPort
}))
const agentHubStatus = () => ({
hub: 'offline',
connector: 'disconnected',
updatedAt: fixtureNowMs,
dataApi: 'online',
databaseReady: true
})
handle('agent-hub:getStatus', agentHubStatus)
handle('agent-hub:getLogs', () => [])
handle('agent-hub:clearLogs', () => ({ success: true }))
handle('agent-hub:startLogin', () => ({ status: agentHubStatus() }))
handle('agent-hub:cancelLogin', () => ({ status: agentHubStatus() }))
handle('agent-hub:disconnect', () => ({ status: agentHubStatus() }))
/*
* Agent Hub 对话记录(本机 conversations.json 的只读回看)。
*
* fixture 里 Hub 是 offline、也没有任何收发记录,所以恒为空 —— 与
* `agent-hub:getStatus` 的 offline 状态保持一致。
*
* 通道**必须**注册:会话面板挂载即调用,缺 handler 会被渲染层记成 pageerror。
*/
handle('agent-hub:getConversations', () => [])
handle('agent-hub:getConversation', () => null)
handle('agent-hub:clearConversations', () => ({ success: true }))
handle('image:getConfig', () => ({
success: true,
configured: true,
saved: true,
encryptionAvailable: true,
source: 'secure-storage',
resourceRoot: settings.imageKeyRoot,
xorKey: settings.imageXorKey,
aesKey: settings.imageAesKey
}))
handle('image:saveConfig', (request) => ({
success: true,
configured: true,
saved: true,
encryptionAvailable: true,
source: 'secure-storage',
...request
}))
handle('image:testConfig', () => ({
success: true,
fileFound: true,
decrypted: true,
readable: true,
diagnosticLog: 'TraceMemo 图片解析测试日志(已脱敏)\n测试结果:成功(SUCCESS)'
}))
handle('image:clearConfig', () => ({ success: true }))
handle('image:getDecoderStatus', () => ({
installed: true,
available: true,
source: 'system',
selected: false
}))
handle('image:getStatus', () => ({
configured: true,
saved: true,
encryptionAvailable: true,
source: 'secure-storage',
resourceRoot: settings.imageKeyRoot,
platform: process.platform,
autoDetectSupported: true,
wechatRunning: true,
accountIdentified: true,
cacheState: 'normal',
decoder: { installed: true, available: true, source: 'system', selected: false },
resources: Object.fromEntries(
['imageIndex', 'imageDirectory', 'thumbnail', 'original', 'sticker', 'video'].map((name) => [
name,
{ state: 'available', detail: 'fixture' }
])
)
}))
handle('settings:selectDbRoot', () => ({ canceled: false, path: 'fixture-account' }))
handle('accounts:discover', (inputPath) =>
inputPath === 'Z:\\missing-wechat-data'
? { success: false, accounts: [], error: '微信数据目录不存在,请重新选择目录' }
: {
success: true,
inputKind: 'account',
preselectedAccountId: 'fixture-account-id',
accounts: [
{
id: 'fixture-account-id',
accountRoot: inputPath || 'fixture-account',
directoryName: 'fixture-account',
wxid: fixture.self.wxid,
nickname: fixture.self.nickname,
avatar: fixture.self.avatar,
hasSavedDbKey: Boolean(savedKey),
loginStatus: connected ? 'current' : 'unknown',
selectedByInput: true
}
]
}
)
let appUpdateState = updateSimulation
? {
status: 'available',
currentVersion: '1.9.0',
delivery: 'automatic',
version: '2.0.0',
source: 'startup',
isSimulation: true
}
: unsignedMacUpdate
? {
status: 'available',
currentVersion: '2.2.2',
delivery: 'release-page',
version: '2.2.3',
source: 'startup'
}
: { status: 'idle', currentVersion: '2.2.0', delivery: 'automatic' }
let openedUpdateDownloadUrl = ''
handle('app-update:getState', () => appUpdateState)
handle('app-update:openDownloadPage', () => {
openedUpdateDownloadUrl = 'https://github.com/Wxw-Gu/TraceMemo/releases/latest'
return { success: true }
})
handle('app-update:getOpenedDownloadUrl', () => openedUpdateDownloadUrl)
handle('app-update:download', async () => {
if (!updateSimulation) return { success: true, state: appUpdateState }
const total = 60 * 1024 * 1024
const steps = [0, 5, 12, 21, 33, 46, 58, 69, 78, 86, 93, 97, 100]
let previousTransferred = 0
appUpdateState = {
...appUpdateState,
status: 'downloading',
percent: 0,
transferred: 0,
total,
bytesPerSecond: 0
}
for (const percent of steps.slice(1)) {
await new Promise((resolve) => setTimeout(resolve, 150))
const transferred = Math.round((total * percent) / 100)
const bytesPerSecond = Math.round((transferred - previousTransferred) / 0.15)
previousTransferred = transferred
appUpdateState = {
...appUpdateState,
status: 'downloading',
percent,
transferred,
total,
bytesPerSecond
}
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed()) window.webContents.send('app-update:state', appUpdateState)
}
}
appUpdateState = {
...appUpdateState,
status: 'downloaded',
percent: 100,
transferred: total,
total,
bytesPerSecond: undefined
}
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed()) window.webContents.send('app-update:state', appUpdateState)
}
return { success: true, state: appUpdateState }
})
handle('app-update:install', async () =>
updateSimulation
? {
success: true,
simulated: true,
message: '开发模拟模式:更新安装动作已模拟,未实际退出应用。'
}
: { success: true }
)
for (const channel of [
'export:start',
'export:cancel',
'export:reveal',
'settings:openAccountRoot',
'db:reopenWithRoot',
'api:skillStatus',
'api:readSkill',
'api:revealSkill',
'api:openSkillGithub',
'api:testLocalRequest',
'image:selectDecoder',
'image:openDecoderDownload',
'app-update:check',
'agent-hub:clearLogs',
'agent-hub:startLogin',
'agent-hub:cancelLogin',
'agent-hub:reconnect',
'agent-hub:disconnect',
'agent-hub:selectTestImage',
'image:listCandidates',
'image:analyze',
'image:getInsight',
'image:listInsights',
'db:search',
'db:getVideo'
]) {
if (!handlers.has(channel))
handle(channel, () => ({ success: true, candidates: [], insights: [] }))
}
app.whenReady().then(() => {
const window = new BrowserWindow({
/*
* E2E 窗口**刻意固定** 1400×800,不再跟随 `createWindow()`
* (后者已改为按屏幕工作区 60% 计算初始尺寸):visual 基线是按这个尺寸
* 评审入库的,若跟随真实逻辑,截图尺寸会随 runner 屏幕浮动、基线永远对不上。
*/
width: 1400,
height: 800,
show: false,
backgroundColor: '#ffffff',
webPreferences: {
preload: path.join(root, 'out/preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false
}
})
if (!backgroundE2E) window.once('ready-to-show', () => window.show())
window.loadFile(path.join(root, 'out/renderer/index.html'))
})
app.on('window-all-closed', () => app.quit())