191 lines
5.7 KiB
TypeScript
191 lines
5.7 KiB
TypeScript
|
|
import type { MeetingVO } from "../../api/business/meeting";
|
|||
|
|
|
|||
|
|
export type AnalysisChapter = {
|
|||
|
|
time?: string;
|
|||
|
|
title: string;
|
|||
|
|
summary: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export type AnalysisSpeakerSummary = {
|
|||
|
|
speaker: string;
|
|||
|
|
summary: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export type AnalysisKeyPoint = {
|
|||
|
|
title: string;
|
|||
|
|
summary: string;
|
|||
|
|
speaker?: string;
|
|||
|
|
time?: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export type MeetingAnalysis = {
|
|||
|
|
overview: string;
|
|||
|
|
keywords: string[];
|
|||
|
|
chapters: AnalysisChapter[];
|
|||
|
|
speakerSummaries: AnalysisSpeakerSummary[];
|
|||
|
|
keyPoints: AnalysisKeyPoint[];
|
|||
|
|
todos: string[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const ANALYSIS_EMPTY: MeetingAnalysis = {
|
|||
|
|
overview: "",
|
|||
|
|
keywords: [],
|
|||
|
|
chapters: [],
|
|||
|
|
speakerSummaries: [],
|
|||
|
|
keyPoints: [],
|
|||
|
|
todos: [],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const splitLines = (value?: string | null) =>
|
|||
|
|
(value || "")
|
|||
|
|
.split(/\r?\n/)
|
|||
|
|
.map((line) => line.trim())
|
|||
|
|
.filter(Boolean);
|
|||
|
|
|
|||
|
|
const parseLooseJson = (raw?: string | null) => {
|
|||
|
|
const input = (raw || "").trim();
|
|||
|
|
if (!input) return null;
|
|||
|
|
|
|||
|
|
const tryParse = (text: string) => {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(text);
|
|||
|
|
} catch {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const direct = tryParse(input);
|
|||
|
|
if (direct && typeof direct === "object") return direct;
|
|||
|
|
|
|||
|
|
const fenced = input.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
|
|||
|
|
if (fenced) {
|
|||
|
|
const fencedParsed = tryParse(fenced);
|
|||
|
|
if (fencedParsed && typeof fencedParsed === "object") return fencedParsed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const start = input.indexOf("{");
|
|||
|
|
const end = input.lastIndexOf("}");
|
|||
|
|
if (start >= 0 && end > start) {
|
|||
|
|
const wrapped = tryParse(input.slice(start, end + 1));
|
|||
|
|
if (wrapped && typeof wrapped === "object") return wrapped;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const extractSection = (markdown: string, aliases: string[]) => {
|
|||
|
|
const lines = markdown.split(/\r?\n/);
|
|||
|
|
const lowerAliases = aliases.map((item) => item.toLowerCase());
|
|||
|
|
const cleanHeading = (line: string) => line.replace(/^#{1,6}\s*/, "").trim().toLowerCase();
|
|||
|
|
|
|||
|
|
let start = -1;
|
|||
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|||
|
|
const line = lines[index].trim();
|
|||
|
|
if (!line.startsWith("#")) continue;
|
|||
|
|
const heading = cleanHeading(line);
|
|||
|
|
if (lowerAliases.some((alias) => heading.includes(alias))) {
|
|||
|
|
start = index + 1;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (start < 0) return "";
|
|||
|
|
|
|||
|
|
const buffer: string[] = [];
|
|||
|
|
for (let index = start; index < lines.length; index += 1) {
|
|||
|
|
const line = lines[index];
|
|||
|
|
if (line.trim().startsWith("#")) break;
|
|||
|
|
buffer.push(line);
|
|||
|
|
}
|
|||
|
|
return buffer.join("\n").trim();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const parseBulletList = (content?: string | null) =>
|
|||
|
|
splitLines(content)
|
|||
|
|
.map((line) => line.replace(/^[-*•\s]+/, "").replace(/^\d+[.)]\s*/, "").trim())
|
|||
|
|
.filter(Boolean);
|
|||
|
|
|
|||
|
|
const parseOverviewSection = (markdown: string) =>
|
|||
|
|
extractSection(markdown, ["全文概要", "概要", "摘要", "概览"]) || markdown.replace(/^---[\s\S]*?---/, "").trim();
|
|||
|
|
|
|||
|
|
const parseKeywordsSection = (markdown: string, tags: string) => {
|
|||
|
|
const section = extractSection(markdown, ["关键词", "关键字", "标签"]);
|
|||
|
|
const fromSection = parseBulletList(section)
|
|||
|
|
.flatMap((line) => line.split(/[,,、/]/))
|
|||
|
|
.map((item) => item.trim())
|
|||
|
|
.filter(Boolean);
|
|||
|
|
|
|||
|
|
if (fromSection.length) {
|
|||
|
|
return Array.from(new Set(fromSection)).slice(0, 12);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Array.from(new Set((tags || "").split(",").map((item) => item.trim()).filter(Boolean))).slice(0, 12);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const buildMeetingAnalysis = (
|
|||
|
|
sourceAnalysis: MeetingVO["analysis"] | undefined,
|
|||
|
|
summaryContent: string | undefined,
|
|||
|
|
tags: string,
|
|||
|
|
): MeetingAnalysis => {
|
|||
|
|
const parseStructured = (parsed: Record<string, any>): MeetingAnalysis => {
|
|||
|
|
const chapters = Array.isArray(parsed.chapters) ? parsed.chapters : [];
|
|||
|
|
const speakerSummaries = Array.isArray(parsed.speakerSummaries) ? parsed.speakerSummaries : [];
|
|||
|
|
const keyPoints = Array.isArray(parsed.keyPoints) ? parsed.keyPoints : [];
|
|||
|
|
const todos = Array.isArray(parsed.todos)
|
|||
|
|
? parsed.todos
|
|||
|
|
: Array.isArray(parsed.actionItems)
|
|||
|
|
? parsed.actionItems
|
|||
|
|
: [];
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
overview: String(parsed.overview || "").trim(),
|
|||
|
|
keywords: Array.from(
|
|||
|
|
new Set((Array.isArray(parsed.keywords) ? parsed.keywords : []).map((item) => String(item).trim()).filter(Boolean)),
|
|||
|
|
).slice(0, 12),
|
|||
|
|
chapters: chapters
|
|||
|
|
.map((item: any) => ({
|
|||
|
|
time: item?.time ? String(item.time).trim() : undefined,
|
|||
|
|
title: String(item?.title || "").trim(),
|
|||
|
|
summary: String(item?.summary || "").trim(),
|
|||
|
|
}))
|
|||
|
|
.filter((item: AnalysisChapter) => item.title || item.summary),
|
|||
|
|
speakerSummaries: speakerSummaries
|
|||
|
|
.map((item: any) => ({
|
|||
|
|
speaker: String(item?.speaker || "").trim(),
|
|||
|
|
summary: String(item?.summary || "").trim(),
|
|||
|
|
}))
|
|||
|
|
.filter((item: AnalysisSpeakerSummary) => item.speaker || item.summary),
|
|||
|
|
keyPoints: keyPoints
|
|||
|
|
.map((item: any) => ({
|
|||
|
|
title: String(item?.title || "").trim(),
|
|||
|
|
summary: String(item?.summary || "").trim(),
|
|||
|
|
speaker: item?.speaker ? String(item.speaker).trim() : undefined,
|
|||
|
|
time: item?.time ? String(item.time).trim() : undefined,
|
|||
|
|
}))
|
|||
|
|
.filter((item: AnalysisKeyPoint) => item.title || item.summary),
|
|||
|
|
todos: todos.map((item: any) => String(item).trim()).filter(Boolean).slice(0, 10),
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (sourceAnalysis) {
|
|||
|
|
return parseStructured(sourceAnalysis as Record<string, any>);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const raw = (summaryContent || "").trim();
|
|||
|
|
if (!raw && !tags) return ANALYSIS_EMPTY;
|
|||
|
|
|
|||
|
|
const loose = parseLooseJson(raw);
|
|||
|
|
if (loose) {
|
|||
|
|
return parseStructured(loose);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
overview: parseOverviewSection(raw),
|
|||
|
|
keywords: parseKeywordsSection(raw, tags),
|
|||
|
|
chapters: [],
|
|||
|
|
speakerSummaries: [],
|
|||
|
|
keyPoints: [],
|
|||
|
|
todos: [],
|
|||
|
|
};
|
|||
|
|
};
|