fix: 容错修复群聊日报 JSON, 图片样式问题

This commit is contained in:
Wxw-Gu
2026-07-23 16:20:45 +08:00
parent 62f729d281
commit d0ceeceb68
7 changed files with 135 additions and 22 deletions
+1
View File
@@ -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",
+7
View File
@@ -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'}
@@ -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<typeof parseGroupDailyReport> =>
parseGroupDailyReport(
raw,
input.topSpeakers,
input.activeTimeline,
input.voiceLeaderboard,
input.metadata,
input.media
)
let report: ReturnType<typeof parseGroupDailyReport>
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 || '总结图片生成失败' }
+3 -3
View File
@@ -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);
@@ -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('日报图片加载失败')
@@ -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<ReportGenerationMetadata['tokenUsage']>,
second: NonNullable<ReportGenerationMetadata['tokenUsage']>
): NonNullable<ReportGenerationMetadata['tokenUsage']> => ({
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<typeof parseGroupDailyReport>
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')
+17 -1
View File
@@ -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 | null | undefined>): string =>