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",
|
"@tanstack/react-virtual": "^3.14.6",
|
||||||
"fs-extra": "^11.3.2",
|
"fs-extra": "^11.3.2",
|
||||||
"fzstd": "^0.1.1",
|
"fzstd": "^0.1.1",
|
||||||
|
"jsonrepair": "^3.15.0",
|
||||||
"koffi": "^3.1.0",
|
"koffi": "^3.1.0",
|
||||||
"openai": "^6.10.0",
|
"openai": "^6.10.0",
|
||||||
"silk-wasm": "^3.7.1",
|
"silk-wasm": "^3.7.1",
|
||||||
|
|||||||
Generated
+7
@@ -27,6 +27,7 @@ specifiers:
|
|||||||
eslint-plugin-react-refresh: ^0.4.24
|
eslint-plugin-react-refresh: ^0.4.24
|
||||||
fs-extra: ^11.3.2
|
fs-extra: ^11.3.2
|
||||||
fzstd: ^0.1.1
|
fzstd: ^0.1.1
|
||||||
|
jsonrepair: ^3.15.0
|
||||||
koffi: ^3.1.0
|
koffi: ^3.1.0
|
||||||
openai: ^6.10.0
|
openai: ^6.10.0
|
||||||
prettier: ^3.7.4
|
prettier: ^3.7.4
|
||||||
@@ -44,6 +45,7 @@ dependencies:
|
|||||||
'@tanstack/react-virtual': 3.14.6_bokjwhiew3ov3ffvbmafuwoalq
|
'@tanstack/react-virtual': 3.14.6_bokjwhiew3ov3ffvbmafuwoalq
|
||||||
fs-extra: 11.3.2
|
fs-extra: 11.3.2
|
||||||
fzstd: 0.1.1
|
fzstd: 0.1.1
|
||||||
|
jsonrepair: 3.15.0
|
||||||
koffi: 3.1.0
|
koffi: 3.1.0
|
||||||
openai: 6.10.0
|
openai: 6.10.0
|
||||||
silk-wasm: 3.7.1
|
silk-wasm: 3.7.1
|
||||||
@@ -3431,6 +3433,11 @@ packages:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
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:
|
/jsx-ast-utils/3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AIProviderService } from './ai-provider-service'
|
|||||||
import {
|
import {
|
||||||
buildGroupReportInput,
|
buildGroupReportInput,
|
||||||
getSummaryDateRange,
|
getSummaryDateRange,
|
||||||
|
GROUP_REPORT_JSON_REPAIR_SYSTEM_PROMPT,
|
||||||
GROUP_REPORT_SYSTEM_PROMPT,
|
GROUP_REPORT_SYSTEM_PROMPT,
|
||||||
isInternalName,
|
isInternalName,
|
||||||
parseGroupDailyReport,
|
parseGroupDailyReport,
|
||||||
@@ -68,14 +69,39 @@ export async function generateAgentGroupReport(
|
|||||||
{ role: 'user', content: input.prompt }
|
{ role: 'user', content: input.prompt }
|
||||||
])
|
])
|
||||||
if (!ai.success || !ai.data) return { success: false, error: ai.error || 'AI 总结失败' }
|
if (!ai.success || !ai.data) return { success: false, error: ai.error || 'AI 总结失败' }
|
||||||
const report = parseGroupDailyReport(
|
const parseReport = (raw: string): ReturnType<typeof parseGroupDailyReport> =>
|
||||||
ai.data,
|
parseGroupDailyReport(
|
||||||
input.topSpeakers,
|
raw,
|
||||||
input.activeTimeline,
|
input.topSpeakers,
|
||||||
input.voiceLeaderboard,
|
input.activeTimeline,
|
||||||
input.metadata,
|
input.voiceLeaderboard,
|
||||||
input.media
|
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 })
|
const exported = await exportGroupReport({ report, metadata: input.metadata })
|
||||||
if (!exported.success || !exported.pngPath) {
|
if (!exported.success || !exported.pngPath) {
|
||||||
return { success: false, error: exported.error || '总结图片生成失败' }
|
return { success: false, error: exported.error || '总结图片生成失败' }
|
||||||
|
|||||||
@@ -7244,17 +7244,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.report-viewer-stage {
|
.report-viewer-stage {
|
||||||
display: flex;
|
display: block;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
justify-content: center;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-canvas {
|
.report-canvas {
|
||||||
align-self: flex-start;
|
width: max-content;
|
||||||
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 8px 24px rgba(32, 39, 36, 0.14);
|
box-shadow: 0 8px 24px rgba(32, 39, 36, 0.14);
|
||||||
|
|||||||
@@ -37,10 +37,7 @@ export function ReportViewer({
|
|||||||
return () => window.cancelAnimationFrame(frame)
|
return () => window.cancelAnimationFrame(frame)
|
||||||
}, [report?.id])
|
}, [report?.id])
|
||||||
|
|
||||||
const title = useMemo(
|
const title = useMemo(() => (report ? `${report.contactName} 群聊日报` : 'AI 日报'), [report])
|
||||||
() => (report ? `${report.contactName} 群聊日报` : 'AI 日报'),
|
|
||||||
[report]
|
|
||||||
)
|
|
||||||
|
|
||||||
const fitWidth = (): void => {
|
const fitWidth = (): void => {
|
||||||
const viewport = viewportRef.current
|
const viewport = viewportRef.current
|
||||||
@@ -108,10 +105,16 @@ export function ReportViewer({
|
|||||||
}}
|
}}
|
||||||
onLoad={(event) => {
|
onLoad={(event) => {
|
||||||
const image = event.currentTarget
|
const image = event.currentTarget
|
||||||
setNaturalSize({
|
const nextSize = {
|
||||||
width: image.naturalWidth,
|
width: image.naturalWidth,
|
||||||
height: image.naturalHeight
|
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={() => {
|
onError={() => {
|
||||||
setImageError('日报图片加载失败')
|
setImageError('日报图片加载失败')
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Contact, Message } from '../../../shared/types'
|
|||||||
import {
|
import {
|
||||||
buildGroupReportInput,
|
buildGroupReportInput,
|
||||||
getSummaryDateRange,
|
getSummaryDateRange,
|
||||||
|
GROUP_REPORT_JSON_REPAIR_SYSTEM_PROMPT,
|
||||||
GROUP_REPORT_SYSTEM_PROMPT,
|
GROUP_REPORT_SYSTEM_PROMPT,
|
||||||
isInternalName,
|
isInternalName,
|
||||||
parseGroupDailyReport,
|
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 (
|
const applyGroupMemberNames = async (
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
@@ -469,12 +480,12 @@ export function useGroupReportGeneration({
|
|||||||
usage: result.usage
|
usage: result.usage
|
||||||
})
|
})
|
||||||
|
|
||||||
const tokenUsage =
|
let tokenUsage =
|
||||||
result.usage && result.usage.total
|
result.usage && result.usage.total
|
||||||
? result.usage
|
? result.usage
|
||||||
: estimateTokenUsage(aiMessages, result.data)
|
: estimateTokenUsage(aiMessages, result.data)
|
||||||
|
|
||||||
let report
|
let report: ReturnType<typeof parseGroupDailyReport>
|
||||||
try {
|
try {
|
||||||
report = parseGroupDailyReport(
|
report = parseGroupDailyReport(
|
||||||
result.data,
|
result.data,
|
||||||
@@ -485,8 +496,57 @@ export function useGroupReportGeneration({
|
|||||||
input.media
|
input.media
|
||||||
)
|
)
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
writeReportLog('error', '日报 JSON 解析失败', jsonErrorContext(result.data, parseError))
|
writeReportLog('warn', '本地修复日报 JSON 失败,尝试由模型纠正', {
|
||||||
throw parseError
|
...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')
|
setPhase('exportingReport')
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ReportVoiceLeaderboardItem
|
ReportVoiceLeaderboardItem
|
||||||
} from '../../../shared/group-report'
|
} from '../../../shared/group-report'
|
||||||
import { Contact, Message } from '../../../shared/types'
|
import { Contact, Message } from '../../../shared/types'
|
||||||
|
import { jsonrepair } from 'jsonrepair'
|
||||||
import { buildGroupReportFacts } from './group-report-facts'
|
import { buildGroupReportFacts } from './group-report-facts'
|
||||||
|
|
||||||
export const GROUP_REPORT_SYSTEM_PROMPT = `你是微信群聊日报编辑。请仅根据用户提供的聊天记录生成结构化中文日报。
|
export const GROUP_REPORT_SYSTEM_PROMPT = `你是微信群聊日报编辑。请仅根据用户提供的聊天记录生成结构化中文日报。
|
||||||
@@ -73,6 +74,9 @@ JSON 结构必须为:
|
|||||||
- 完整版:可以保留更多候选项,但仍要去重和排序。
|
- 完整版:可以保留更多候选项,但仍要去重和排序。
|
||||||
- 如果某个字段不确定,请返回 null 或空数组,不要猜。`
|
- 如果某个字段不确定,请返回 null 或空数组,不要猜。`
|
||||||
|
|
||||||
|
export const GROUP_REPORT_JSON_REPAIR_SYSTEM_PROMPT =
|
||||||
|
'你是 JSON 格式修复器。只修复输入中的 JSON 语法,不改写、删减或新增任何业务内容。只输出一个可被 JSON.parse 解析的 JSON 对象,不要输出 Markdown 或解释。'
|
||||||
|
|
||||||
export interface GroupReportInput {
|
export interface GroupReportInput {
|
||||||
prompt: string
|
prompt: string
|
||||||
metadata: GroupReportMetadata
|
metadata: GroupReportMetadata
|
||||||
@@ -259,7 +263,19 @@ const extractJson = (raw: string): unknown => {
|
|||||||
const start = cleaned.indexOf('{')
|
const start = cleaned.indexOf('{')
|
||||||
const end = cleaned.lastIndexOf('}')
|
const end = cleaned.lastIndexOf('}')
|
||||||
if (start < 0 || end <= start) throw new Error('AI 未返回可解析的日报 JSON')
|
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 =>
|
const createSignature = (...parts: Array<string | null | undefined>): string =>
|
||||||
|
|||||||
Reference in New Issue
Block a user