Files
mini_tp/app/note/service/AiService.php
T
2026-04-17 07:48:44 +00:00

229 lines
7.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\note\service;
use app\note\model\NoteAiSummary;
use app\note\model\NoteItem;
/**
* note 模块 AI 总结服务
*
* 当前先提供规则版总结,保证接口可用。
* 后续如需接入大模型,可在本服务内部替换实现,不影响控制器接口。
*/
class AiService
{
/**
* 生成或刷新总结
*
* @param NoteItem $note
* @param string $summaryType
* @param bool $forceRefresh
* @return array
*/
public function createSummary(NoteItem $note, string $summaryType = 'brief', bool $forceRefresh = false): array
{
$summaryType = in_array($summaryType, ['brief', 'outline', 'todo'], true) ? $summaryType : 'brief';
$existing = NoteAiSummary::findLatestByNoteId((int) $note->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 : [];
}
}