id); if ($existing && !$forceRefresh && $existing->status === 'success') { return $this->formatSummary($existing); } $sourceText = trim((string) $note->content); if ($sourceText === '') { $sourceText = trim((string) $note->transcript_text); } if ($sourceText === '') { throw new \Exception('笔记内容为空,无法生成总结', 400); } $generated = $this->generateSummaryPayload($sourceText, $summaryType); $now = time(); $summary = $existing ?: new NoteAiSummary(); if (!$existing) { $summary->note_id = (int) $note->id; $summary->created_at = $now; } $summary->summary_type = $summaryType; $summary->summary_text = $generated['summary_text']; $summary->todo_list = json_encode($generated['todo_list'], JSON_UNESCAPED_UNICODE); $summary->keywords = json_encode($generated['keywords'], JSON_UNESCAPED_UNICODE); $summary->status = 'success'; $summary->error_message = ''; $summary->updated_at = $now; $summary->save(); $note->summary_status = 'success'; $note->updated_at = $now; $note->save(); return $this->formatSummary($summary); } /** * 读取总结 * * @param int $noteId * @return array * @throws \Exception */ public function getSummary(int $noteId): array { $summary = NoteAiSummary::findLatestByNoteId($noteId); if (!$summary) { throw new \Exception('该笔记暂无总结结果', 404); } return $this->formatSummary($summary); } /** * 生成规则版总结 * * @param string $text * @param string $summaryType * @return array */ private function generateSummaryPayload(string $text, string $summaryType): array { $sentences = $this->splitSentences($text); $summarySentences = array_slice($sentences, 0, $summaryType === 'outline' ? 5 : 3); $summaryText = implode("\n", array_map(function ($item, $index) use ($summaryType) { if ($summaryType === 'outline') { return sprintf('%d. %s', $index + 1, $item); } return $item; }, $summarySentences, array_keys($summarySentences))); $todoList = $this->extractTodoList($text); $keywords = $this->extractKeywords($text); if ($summaryType === 'todo' && !empty($todoList)) { $summaryText = "待办事项:\n" . implode("\n", array_map(function ($item, $index) { return sprintf('%d. %s', $index + 1, $item); }, $todoList, array_keys($todoList))); } return [ 'summary_text' => $summaryText !== '' ? $summaryText : mb_substr($text, 0, 200), 'todo_list' => $todoList, 'keywords' => $keywords, ]; } /** * 切分句子 * * @param string $text * @return array */ private function splitSentences(string $text): array { $normalized = preg_replace('/\s+/', ' ', trim($text)); $parts = preg_split('/[。!?!?;;\n\r]+/u', (string) $normalized); $parts = array_values(array_filter(array_map('trim', $parts), function ($item) { return $item !== ''; })); return empty($parts) ? [mb_substr($normalized, 0, 200)] : $parts; } /** * 提取待办 * * @param string $text * @return array */ private function extractTodoList(string $text): array { $lines = preg_split('/[\n\r]+/u', $text); $todoList = []; foreach ($lines as $line) { $line = trim($line); if ($line === '') { continue; } if (preg_match('/^(待办|todo|todo:|TODO|TODO:|需要|安排|跟进|完成|处理)/u', $line)) { $todoList[] = $line; } } if (!empty($todoList)) { return array_slice(array_values(array_unique($todoList)), 0, 10); } $sentences = $this->splitSentences($text); $fallback = []; foreach ($sentences as $sentence) { if (preg_match('/(需要|安排|跟进|完成|处理|确认|整理|联系)/u', $sentence)) { $fallback[] = $sentence; } } return array_slice(array_values(array_unique($fallback)), 0, 10); } /** * 提取关键词 * * @param string $text * @return array */ private function extractKeywords(string $text): array { preg_match_all('/[\x{4e00}-\x{9fa5}A-Za-z0-9]{2,20}/u', $text, $matches); $words = $matches[0] ?? []; $stopWords = ['我们', '你们', '他们', '这个', '那个', '然后', '以及', '因为', '所以', '可以', '进行', '一个', '没有', '已经', '需要', '今天', '目前', '如果', '就是']; $countMap = []; foreach ($words as $word) { if (in_array($word, $stopWords, true)) { continue; } $countMap[$word] = ($countMap[$word] ?? 0) + 1; } arsort($countMap); return array_slice(array_keys($countMap), 0, 10); } /** * 格式化总结结果 * * @param NoteAiSummary $summary * @return array */ private function formatSummary(NoteAiSummary $summary): array { return [ 'summary_id' => (int) $summary->id, 'note_id' => (int) $summary->note_id, 'summary_type' => (string) $summary->summary_type, 'summary_text' => (string) $summary->summary_text, 'todo_list' => $this->decodeJsonList((string) $summary->todo_list), 'keywords' => $this->decodeJsonList((string) $summary->keywords), 'status' => (string) $summary->status, 'error_message' => (string) $summary->error_message, 'created_at' => (int) $summary->created_at, 'updated_at' => (int) $summary->updated_at, ]; } /** * 解析 JSON 列表 * * @param string $value * @return array */ private function decodeJsonList(string $value): array { $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } }