mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-09 00:16:32 +08:00
fix: 日报截图窗口隐藏化、搜索缓存兼容读取与定时日报单测等待窗口
This commit is contained in:
@@ -906,6 +906,8 @@ const captureFullPage = async (
|
|||||||
const reportSessionPartition = `report-template-${crypto.randomUUID()}`
|
const reportSessionPartition = `report-template-${crypto.randomUUID()}`
|
||||||
const reportWindow = new BrowserWindow({
|
const reportWindow = new BrowserWindow({
|
||||||
show: false,
|
show: false,
|
||||||
|
opacity: 0,
|
||||||
|
skipTaskbar: true,
|
||||||
width: captureWidth,
|
width: captureWidth,
|
||||||
height: 800,
|
height: 800,
|
||||||
frame: false,
|
frame: false,
|
||||||
@@ -948,7 +950,9 @@ const captureFullPage = async (
|
|||||||
reportWindow.webContents.on('will-navigate', (event) => event.preventDefault())
|
reportWindow.webContents.on('will-navigate', (event) => event.preventDefault())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const readyToShow = new Promise<void>((resolve) => reportWindow.once('ready-to-show', resolve))
|
||||||
await reportWindow.loadFile(htmlPath)
|
await reportWindow.loadFile(htmlPath)
|
||||||
|
await readyToShow
|
||||||
console.log('[GroupReport] capture loaded html')
|
console.log('[GroupReport] capture loaded html')
|
||||||
await reportWindow.webContents.executeJavaScript(`Promise.all([
|
await reportWindow.webContents.executeJavaScript(`Promise.all([
|
||||||
document.fonts.ready,
|
document.fonts.ready,
|
||||||
@@ -964,10 +968,21 @@ const captureFullPage = async (
|
|||||||
})`)) as { width: number; height: number }
|
})`)) as { width: number; height: number }
|
||||||
const width = Math.max(captureWidth, Math.min(maxCaptureWidth, Math.ceil(metrics.width)))
|
const width = Math.max(captureWidth, Math.min(maxCaptureWidth, Math.ceil(metrics.width)))
|
||||||
const height = Math.max(800, Math.min(maxCaptureHeight, Math.ceil(metrics.height)))
|
const height = Math.max(800, Math.min(maxCaptureHeight, Math.ceil(metrics.height)))
|
||||||
|
const [currentWidth, currentHeight] = reportWindow.getContentSize()
|
||||||
|
if (currentWidth !== width || currentHeight !== height) {
|
||||||
|
const resized = new Promise<void>((resolve) => reportWindow.once('resize', resolve))
|
||||||
reportWindow.setContentSize(width, height)
|
reportWindow.setContentSize(width, height)
|
||||||
|
await resized
|
||||||
|
}
|
||||||
|
await reportWindow.webContents.executeJavaScript(
|
||||||
|
'new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))'
|
||||||
|
)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
console.log(`[GroupReport] capture native page width=${width} height=${height}`)
|
console.log(`[GroupReport] capture native page width=${width} height=${height}`)
|
||||||
const image = await reportWindow.webContents.capturePage({ x: 0, y: 0, width, height })
|
const image = await reportWindow.webContents.capturePage(
|
||||||
|
{ x: 0, y: 0, width, height },
|
||||||
|
{ stayHidden: true }
|
||||||
|
)
|
||||||
const png = image.toPNG()
|
const png = image.toPNG()
|
||||||
if (png.length < 1000) throw new Error('生成的日报图片为空')
|
if (png.length < 1000) throw new Error('生成的日报图片为空')
|
||||||
await fs.writeFile(pngPath, png)
|
await fs.writeFile(pngPath, png)
|
||||||
|
|||||||
@@ -217,6 +217,33 @@ export function useSearchHistory({
|
|||||||
onNotice('已恢复这条历史问题的最近结果')
|
onNotice('已恢复这条历史问题的最近结果')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const readCachedResult = (cacheKey: string): AISearchCacheRecord | null => {
|
||||||
|
const exact = readSearchCache(cacheKey)
|
||||||
|
if (exact) return exact
|
||||||
|
try {
|
||||||
|
const requested = parseSearchCacheKey(cacheKey)
|
||||||
|
if (!requested) return null
|
||||||
|
const records = JSON.parse(localStorage.getItem(SEARCH_CACHE_KEY) || '[]') as AISearchCacheRecord[]
|
||||||
|
return records
|
||||||
|
.filter((record) => {
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(record.key) as unknown[]
|
||||||
|
const location = parseSearchCacheKey(record.key)
|
||||||
|
// Only legacy four-part keys may use compatibility lookup, and
|
||||||
|
// all base dimensions must still match the active request.
|
||||||
|
return raw.length === 4 && location?.scope === requested.scope &&
|
||||||
|
location.contactMd5 === requested.contactMd5 && location.range === requested.range &&
|
||||||
|
location.query === requested.query
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.createdAt - a.createdAt)[0] || null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const persistSearchResult = (input: PersistSearchResultInput): AISearchCacheRecord => {
|
const persistSearchResult = (input: PersistSearchResultInput): AISearchCacheRecord => {
|
||||||
const record = createSearchCacheRecord({
|
const record = createSearchCacheRecord({
|
||||||
...input,
|
...input,
|
||||||
@@ -288,7 +315,7 @@ export function useSearchHistory({
|
|||||||
removeHistoryQuery,
|
removeHistoryQuery,
|
||||||
restoreHistoryQuery,
|
restoreHistoryQuery,
|
||||||
applyCachedResult,
|
applyCachedResult,
|
||||||
readCachedResult: readSearchCache,
|
readCachedResult,
|
||||||
persistSearchResult,
|
persistSearchResult,
|
||||||
clearActiveResult,
|
clearActiveResult,
|
||||||
skipNextCache,
|
skipNextCache,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
SEARCH_CACHE_KEY,
|
SEARCH_CACHE_KEY,
|
||||||
buildSearchCacheKey
|
buildSearchCacheKey
|
||||||
} from '../../src/renderer/src/components/search/searchUtils'
|
} from '../../src/renderer/src/components/search/searchUtils'
|
||||||
import { makeSearchResult } from './support/ai-search-fixtures'
|
import { makePipelineEvidence, makeSearchResult } from './support/ai-search-fixtures'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
getSettings: vi.fn(),
|
getSettings: vi.fn(),
|
||||||
@@ -224,7 +224,7 @@ describe('AISearchWorkspace cache privacy boundary', () => {
|
|||||||
value: scrollIntoView
|
value: scrollIntoView
|
||||||
})
|
})
|
||||||
api.getAiSearchProviderStatus.mockResolvedValue({ configured: true, requiresConsent: false })
|
api.getAiSearchProviderStatus.mockResolvedValue({ configured: true, requiresConsent: false })
|
||||||
api.runAiSearch.mockResolvedValue({
|
api.runAiSearch.mockResolvedValue(makeSearchResult({
|
||||||
requestId: 'evidence-navigation',
|
requestId: 'evidence-navigation',
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
answer: '请查看这条证据 [E7]。',
|
answer: '请查看这条证据 [E7]。',
|
||||||
@@ -264,7 +264,7 @@ describe('AISearchWorkspace cache privacy boundary', () => {
|
|||||||
agent: { mode: 'agent', toolCalls: 1, trace: [] },
|
agent: { mode: 'agent', toolCalls: 1, trace: [] },
|
||||||
timings: {},
|
timings: {},
|
||||||
elapsedMs: 1
|
elapsedMs: 1
|
||||||
} as never)
|
}) as never)
|
||||||
render(
|
render(
|
||||||
<AISearchWorkspace
|
<AISearchWorkspace
|
||||||
contacts={[]}
|
contacts={[]}
|
||||||
@@ -300,7 +300,7 @@ describe('AISearchWorkspace cache privacy boundary', () => {
|
|||||||
|
|
||||||
it('keeps the submitted result title stable while drafting a new question and clears it from 新问题', async () => {
|
it('keeps the submitted result title stable while drafting a new question and clears it from 新问题', async () => {
|
||||||
api.getAiSearchProviderStatus.mockResolvedValue({ configured: true, requiresConsent: false })
|
api.getAiSearchProviderStatus.mockResolvedValue({ configured: true, requiresConsent: false })
|
||||||
api.runAiSearch.mockResolvedValue({
|
api.runAiSearch.mockResolvedValue(makeSearchResult({
|
||||||
requestId: 'new-question',
|
requestId: 'new-question',
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
answer: 'first answer',
|
answer: 'first answer',
|
||||||
@@ -319,7 +319,7 @@ describe('AISearchWorkspace cache privacy boundary', () => {
|
|||||||
agent: { mode: 'agent', toolCalls: 1, trace: [] },
|
agent: { mode: 'agent', toolCalls: 1, trace: [] },
|
||||||
timings: {},
|
timings: {},
|
||||||
elapsedMs: 1
|
elapsedMs: 1
|
||||||
} as never)
|
}) as never)
|
||||||
render(
|
render(
|
||||||
<AISearchWorkspace
|
<AISearchWorkspace
|
||||||
contacts={[]}
|
contacts={[]}
|
||||||
|
|||||||
@@ -73,19 +73,38 @@ async function enableNotifications(storageDir: string): Promise<void> {
|
|||||||
await writeFile(join(storageDir, 'settings.json'), JSON.stringify({ enabled: true }))
|
await writeFile(join(storageDir, 'settings.json'), JSON.stringify({ enabled: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tick() 为保证调度不阻塞而 fire-and-forget 启动执行,测试只能轮询执行记录来等待结束。
|
||||||
|
// 该路径耗时几乎全部是临时目录的 JSON 落盘:本地约 16ms,Windows CI(含杀毒扫描)实测
|
||||||
|
// 50~120ms,最慢一次耗尽旧的 200ms 预算后误报“未结束”。窗口按最坏观测值留足余量,
|
||||||
|
// 但仍在 5s 的 vitest 用例超时之内,保留有界失败而不是无限等待。
|
||||||
|
const EXECUTION_WAIT_TIMEOUT_MS = 4_000
|
||||||
|
const EXECUTION_WAIT_POLL_INTERVAL_MS = 10
|
||||||
|
|
||||||
async function runScheduled(
|
async function runScheduled(
|
||||||
service: ScheduledReportService,
|
service: ScheduledReportService,
|
||||||
taskId: string
|
taskId: string
|
||||||
): Promise<ScheduledReportExecution> {
|
): Promise<ScheduledReportExecution> {
|
||||||
const task = (await service.listTasks()).find((item) => item.id === taskId)
|
const task = (await service.listTasks()).find((item) => item.id === taskId)
|
||||||
if (!task) throw new Error('scheduled task not found')
|
if (!task) throw new Error('scheduled task not found')
|
||||||
await service.tick(new Date(Date.parse(task.nextRunAt) + 1_000))
|
const scheduledSlot = task.nextRunAt
|
||||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
await service.tick(new Date(Date.parse(scheduledSlot) + 1_000))
|
||||||
const execution = (await service.listExecutions(taskId))[0]
|
const deadline = Date.now() + EXECUTION_WAIT_TIMEOUT_MS
|
||||||
if (execution && execution.status !== 'running') return execution
|
let lastStatus = 'no execution recorded'
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
for (;;) {
|
||||||
|
// 取最新一次执行,而不是数组首项(executions.json 按追加顺序保存)。
|
||||||
|
const latest = (await service.listExecutions(taskId))
|
||||||
|
.slice()
|
||||||
|
.sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt))[0]
|
||||||
|
if (latest && latest.status !== 'running') return latest
|
||||||
|
lastStatus = latest?.status ?? 'no execution recorded'
|
||||||
|
if (Date.now() >= deadline) {
|
||||||
|
throw new Error(
|
||||||
|
`scheduled execution did not finish within ${EXECUTION_WAIT_TIMEOUT_MS}ms ` +
|
||||||
|
`(slot ${scheduledSlot}, last status: ${lastStatus})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, EXECUTION_WAIT_POLL_INTERVAL_MS))
|
||||||
}
|
}
|
||||||
throw new Error('scheduled execution did not finish')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onlineAgentHubStatus = (): AgentHubStatus => ({
|
const onlineAgentHubStatus = (): AgentHubStatus => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user