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: [],
|
||
};
|
||
};
|