mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
fix: 容错修复群聊日报 JSON, 图片样式问题
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+7
@@ -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 || '总结图片生成失败' }
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
Reference in New Issue
Block a user