From d0ceeceb6808b632729f983a212e0870ab9ca6e2 Mon Sep 17 00:00:00 2001 From: Wxw-Gu Date: Thu, 23 Jul 2026 16:20:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=B9=E9=94=99=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BE=A4=E8=81=8A=E6=97=A5=E6=8A=A5=20JSON,=20=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=A0=B7=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 7 ++ .../services/agent-group-report-service.ts | 42 +++++++++--- src/renderer/src/assets/main.css | 6 +- .../src/components/reports/ReportViewer.tsx | 15 ++-- .../src/hooks/useGroupReportGeneration.ts | 68 +++++++++++++++++-- src/renderer/src/utils/group-report.ts | 18 ++++- 7 files changed, 135 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index e1cf2d3..381e3db 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@tanstack/react-virtual": "^3.14.6", "fs-extra": "^11.3.2", "fzstd": "^0.1.1", + "jsonrepair": "^3.15.0", "koffi": "^3.1.0", "openai": "^6.10.0", "silk-wasm": "^3.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 636a825..2d61088 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,7 @@ specifiers: eslint-plugin-react-refresh: ^0.4.24 fs-extra: ^11.3.2 fzstd: ^0.1.1 + jsonrepair: ^3.15.0 koffi: ^3.1.0 openai: ^6.10.0 prettier: ^3.7.4 @@ -44,6 +45,7 @@ dependencies: '@tanstack/react-virtual': 3.14.6_bokjwhiew3ov3ffvbmafuwoalq fs-extra: 11.3.2 fzstd: 0.1.1 + jsonrepair: 3.15.0 koffi: 3.1.0 openai: 6.10.0 silk-wasm: 3.7.1 @@ -3431,6 +3433,11 @@ packages: optionalDependencies: graceful-fs: 4.2.11 + /jsonrepair/3.15.0: + resolution: {integrity: sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==} + hasBin: true + dev: false + /jsx-ast-utils/3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} diff --git a/src/main/services/agent-group-report-service.ts b/src/main/services/agent-group-report-service.ts index 2b42852..63bfe0b 100644 --- a/src/main/services/agent-group-report-service.ts +++ b/src/main/services/agent-group-report-service.ts @@ -5,6 +5,7 @@ import { AIProviderService } from './ai-provider-service' import { buildGroupReportInput, getSummaryDateRange, + GROUP_REPORT_JSON_REPAIR_SYSTEM_PROMPT, GROUP_REPORT_SYSTEM_PROMPT, isInternalName, parseGroupDailyReport, @@ -68,14 +69,39 @@ export async function generateAgentGroupReport( { role: 'user', content: input.prompt } ]) if (!ai.success || !ai.data) return { success: false, error: ai.error || 'AI 总结失败' } - const report = parseGroupDailyReport( - ai.data, - input.topSpeakers, - input.activeTimeline, - input.voiceLeaderboard, - input.metadata, - input.media - ) + const parseReport = (raw: string): ReturnType => + parseGroupDailyReport( + raw, + input.topSpeakers, + input.activeTimeline, + input.voiceLeaderboard, + input.metadata, + input.media + ) + let report: ReturnType + try { + report = parseReport(ai.data) + } catch (parseError) { + const repaired = await aiProvider.chat([ + { role: 'system', content: GROUP_REPORT_JSON_REPAIR_SYSTEM_PROMPT }, + { role: 'user', content: ai.data } + ]) + if (!repaired.success || !repaired.data) { + const cause = parseError instanceof Error ? parseError.message : String(parseError) + return { + success: false, + error: `${repaired.error || 'AI 修复日报 JSON 失败'}(原始错误:${cause})` + } + } + try { + report = parseReport(repaired.data) + } catch (repairError) { + return { + success: false, + error: repairError instanceof Error ? repairError.message : String(repairError) + } + } + } const exported = await exportGroupReport({ report, metadata: input.metadata }) if (!exported.success || !exported.pngPath) { return { success: false, error: exported.error || '总结图片生成失败' } diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 5d8e5d2..393d89f 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -7244,17 +7244,17 @@ body { } .report-viewer-stage { - display: flex; + display: block; flex: 1 1 auto; min-height: 0; min-width: 0; - justify-content: center; overflow: auto; padding: 22px; } .report-canvas { - align-self: flex-start; + width: max-content; + margin: 0 auto; padding: 0; background: #fff; box-shadow: 0 8px 24px rgba(32, 39, 36, 0.14); diff --git a/src/renderer/src/components/reports/ReportViewer.tsx b/src/renderer/src/components/reports/ReportViewer.tsx index 916a7fd..ab4c4b4 100644 --- a/src/renderer/src/components/reports/ReportViewer.tsx +++ b/src/renderer/src/components/reports/ReportViewer.tsx @@ -37,10 +37,7 @@ export function ReportViewer({ return () => window.cancelAnimationFrame(frame) }, [report?.id]) - const title = useMemo( - () => (report ? `${report.contactName} 群聊日报` : 'AI 日报'), - [report] - ) + const title = useMemo(() => (report ? `${report.contactName} 群聊日报` : 'AI 日报'), [report]) const fitWidth = (): void => { const viewport = viewportRef.current @@ -108,10 +105,16 @@ export function ReportViewer({ }} onLoad={(event) => { const image = event.currentTarget - setNaturalSize({ + const nextSize = { width: image.naturalWidth, height: image.naturalHeight - }) + } + setNaturalSize(nextSize) + const viewport = viewportRef.current + if (viewport && nextSize.width > viewport.clientWidth - 48) { + const fittedZoom = Math.max(0.25, (viewport.clientWidth - 48) / nextSize.width) + setZoom(Number(fittedZoom.toFixed(2))) + } }} onError={() => { setImageError('日报图片加载失败') diff --git a/src/renderer/src/hooks/useGroupReportGeneration.ts b/src/renderer/src/hooks/useGroupReportGeneration.ts index ae582f2..90048f6 100644 --- a/src/renderer/src/hooks/useGroupReportGeneration.ts +++ b/src/renderer/src/hooks/useGroupReportGeneration.ts @@ -3,6 +3,7 @@ import { Contact, Message } from '../../../shared/types' import { buildGroupReportInput, getSummaryDateRange, + GROUP_REPORT_JSON_REPAIR_SYSTEM_PROMPT, GROUP_REPORT_SYSTEM_PROMPT, isInternalName, parseGroupDailyReport, @@ -161,6 +162,16 @@ const estimateTokenUsage = ( } } +const mergeTokenUsage = ( + first: NonNullable, + second: NonNullable +): NonNullable => ({ + input: (first.input || 0) + (second.input || 0), + output: (first.output || 0) + (second.output || 0), + total: (first.total || 0) + (second.total || 0), + estimated: Boolean(first.estimated || second.estimated) +}) + const applyGroupMemberNames = async ( contact: Contact, messages: Message[], @@ -469,12 +480,12 @@ export function useGroupReportGeneration({ usage: result.usage }) - const tokenUsage = + let tokenUsage = result.usage && result.usage.total ? result.usage : estimateTokenUsage(aiMessages, result.data) - let report + let report: ReturnType try { report = parseGroupDailyReport( result.data, @@ -485,8 +496,57 @@ export function useGroupReportGeneration({ input.media ) } catch (parseError) { - writeReportLog('error', '日报 JSON 解析失败', jsonErrorContext(result.data, parseError)) - throw parseError + writeReportLog('warn', '本地修复日报 JSON 失败,尝试由模型纠正', { + ...jsonErrorContext(result.data, parseError), + retry: 1 + }) + const repairMessages = [ + { + role: 'system', + content: GROUP_REPORT_JSON_REPAIR_SYSTEM_PROMPT + }, + { role: 'user', content: result.data } + ] + const repairResult = await trackStep('AI 修复 JSON', () => + withTimeout( + window.api.aiChat(repairMessages, { + providerId: modelConfig.providerId, + modelId: modelConfig.model, + timeoutMs: reportTimeoutSeconds * 1000 + }), + 'AI 修复日报 JSON', + reportTimeoutSeconds * 1000 + REPORT_MODEL_TIMEOUT_BUFFER_MS + ) + ) + if (!repairResult.success || !repairResult.data) { + throw new Error(repairResult.error || 'AI 修复日报 JSON 失败', { cause: parseError }) + } + const repairUsage = + repairResult.usage && repairResult.usage.total + ? repairResult.usage + : estimateTokenUsage(repairMessages, repairResult.data) + tokenUsage = mergeTokenUsage(tokenUsage, repairUsage) + try { + report = parseGroupDailyReport( + repairResult.data, + input.topSpeakers, + input.activeTimeline, + input.voiceLeaderboard || [], + input.metadata, + input.media + ) + writeReportLog('info', '模型已纠正日报 JSON', { + retry: 1, + outputLength: repairResult.data.length + }) + } catch (retryParseError) { + writeReportLog( + 'error', + '日报 JSON 重试后仍解析失败', + jsonErrorContext(repairResult.data, retryParseError) + ) + throw retryParseError + } } setPhase('exportingReport') diff --git a/src/renderer/src/utils/group-report.ts b/src/renderer/src/utils/group-report.ts index ae3b94b..b80127f 100644 --- a/src/renderer/src/utils/group-report.ts +++ b/src/renderer/src/utils/group-report.ts @@ -19,6 +19,7 @@ import { ReportVoiceLeaderboardItem } from '../../../shared/group-report' import { Contact, Message } from '../../../shared/types' +import { jsonrepair } from 'jsonrepair' import { buildGroupReportFacts } from './group-report-facts' export const GROUP_REPORT_SYSTEM_PROMPT = `你是微信群聊日报编辑。请仅根据用户提供的聊天记录生成结构化中文日报。 @@ -73,6 +74,9 @@ JSON 结构必须为: - 完整版:可以保留更多候选项,但仍要去重和排序。 - 如果某个字段不确定,请返回 null 或空数组,不要猜。` +export const GROUP_REPORT_JSON_REPAIR_SYSTEM_PROMPT = + '你是 JSON 格式修复器。只修复输入中的 JSON 语法,不改写、删减或新增任何业务内容。只输出一个可被 JSON.parse 解析的 JSON 对象,不要输出 Markdown 或解释。' + export interface GroupReportInput { prompt: string metadata: GroupReportMetadata @@ -259,7 +263,19 @@ const extractJson = (raw: string): unknown => { const start = cleaned.indexOf('{') const end = cleaned.lastIndexOf('}') if (start < 0 || end <= start) throw new Error('AI 未返回可解析的日报 JSON') - return JSON.parse(cleaned.slice(start, end + 1)) + const json = cleaned.slice(start, end + 1) + try { + return JSON.parse(json) + } catch (strictError) { + try { + return JSON.parse(jsonrepair(json)) + } catch (repairError) { + throw new Error( + `日报 JSON 自动修复失败:${repairError instanceof Error ? repairError.message : String(repairError)}`, + { cause: strictError } + ) + } + } } const createSignature = (...parts: Array): string =>