399 lines
11 KiB
PHP
399 lines
11 KiB
PHP
<?php
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
|
||
class AIMessage extends Model
|
||
{
|
||
use HasFactory;
|
||
|
||
protected $fillable = [
|
||
'conversation_id',
|
||
'role',
|
||
'content',
|
||
'tokens',
|
||
'metadata',
|
||
];
|
||
|
||
protected $casts = [
|
||
'tokens' => 'integer',
|
||
'metadata' => 'array',
|
||
'created_at' => 'datetime',
|
||
];
|
||
|
||
/**
|
||
* 关联对话
|
||
*/
|
||
public function conversation(): BelongsTo
|
||
{
|
||
return $this->belongsTo(AIConversation::class);
|
||
}
|
||
|
||
/**
|
||
* 获取消息摘要
|
||
*/
|
||
public function getSummaryAttribute(): string
|
||
{
|
||
$content = $this->content;
|
||
|
||
if (strlen($content) <= 100) {
|
||
return $content;
|
||
}
|
||
|
||
return substr($content, 0, 100) . '...';
|
||
}
|
||
|
||
/**
|
||
* 获取消息类型
|
||
*/
|
||
public function getTypeAttribute(): string
|
||
{
|
||
$types = [
|
||
'system' => '系统',
|
||
'user' => '用户',
|
||
'assistant' => 'AI助手',
|
||
];
|
||
|
||
return $types[$this->role] ?? '未知';
|
||
}
|
||
|
||
/**
|
||
* 获取消息图标
|
||
*/
|
||
public function getIconAttribute(): string
|
||
{
|
||
$icons = [
|
||
'system' => 'settings',
|
||
'user' => 'person',
|
||
'assistant' => 'smart_toy',
|
||
];
|
||
|
||
return $icons[$this->role] ?? 'chat';
|
||
}
|
||
|
||
/**
|
||
* 获取消息颜色
|
||
*/
|
||
public function getColorAttribute(): string
|
||
{
|
||
$colors = [
|
||
'system' => 'secondary',
|
||
'user' => 'primary',
|
||
'assistant' => 'success',
|
||
];
|
||
|
||
return $colors[$this->role] ?? 'default';
|
||
}
|
||
|
||
/**
|
||
* 检查是否为AI消息
|
||
*/
|
||
public function getIsAiAttribute(): bool
|
||
{
|
||
return $this->role === 'assistant';
|
||
}
|
||
|
||
/**
|
||
* 检查是否为用户消息
|
||
*/
|
||
public function getIsUserAttribute(): bool
|
||
{
|
||
return $this->role === 'user';
|
||
}
|
||
|
||
/**
|
||
* 检查是否为系统消息
|
||
*/
|
||
public function getIsSystemAttribute(): bool
|
||
{
|
||
return $this->role === 'system';
|
||
}
|
||
|
||
/**
|
||
* 获取消息时间格式
|
||
*/
|
||
public function getTimeFormattedAttribute(): string
|
||
{
|
||
$now = now();
|
||
$messageTime = $this->created_at;
|
||
|
||
if ($messageTime->isToday()) {
|
||
return $messageTime->format('H:i');
|
||
} elseif ($messageTime->isYesterday()) {
|
||
return '昨天 ' . $messageTime->format('H:i');
|
||
} elseif ($messageTime->diffInDays($now) < 7) {
|
||
return $messageTime->format('m-d H:i');
|
||
} else {
|
||
return $messageTime->format('Y-m-d H:i');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取消息相对时间
|
||
*/
|
||
public function getRelativeTimeAttribute(): string
|
||
{
|
||
$diff = $this->created_at->diff(now());
|
||
|
||
if ($diff->y > 0) {
|
||
return $diff->y . '年前';
|
||
} elseif ($diff->m > 0) {
|
||
return $diff->m . '个月前';
|
||
} elseif ($diff->d > 0) {
|
||
return $diff->d . '天前';
|
||
} elseif ($diff->h > 0) {
|
||
return $diff->h . '小时前';
|
||
} elseif ($diff->i > 0) {
|
||
return $diff->i . '分钟前';
|
||
} else {
|
||
return '刚刚';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取消息内容格式(HTML安全)
|
||
*/
|
||
public function getContentFormattedAttribute(): string
|
||
{
|
||
$content = htmlspecialchars($this->content, ENT_QUOTES, 'UTF-8');
|
||
|
||
// 将换行转换为<br>
|
||
$content = nl2br($content);
|
||
|
||
// 将URL转换为链接
|
||
$content = preg_replace(
|
||
'/(https?:\/\/[^\s]+)/',
|
||
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
|
||
$content
|
||
);
|
||
|
||
// 将代码块格式化
|
||
$content = preg_replace_callback(
|
||
'/```(\w+)?\n([\s\S]*?)```/',
|
||
function ($matches) {
|
||
$language = $matches[1] ?? '';
|
||
$code = htmlspecialchars($matches[2], ENT_QUOTES, 'UTF-8');
|
||
return '<pre><code class="language-' . $language . '">' . $code . '</code></pre>';
|
||
},
|
||
$content
|
||
);
|
||
|
||
// 将行内代码格式化
|
||
$content = preg_replace(
|
||
'/`([^`]+)`/',
|
||
'<code>$1</code>',
|
||
$content
|
||
);
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 获取消息中的代码块
|
||
*/
|
||
public function getCodeBlocksAttribute(): array
|
||
{
|
||
preg_match_all('/```(\w+)?\n([\s\S]*?)```/', $this->content, $matches, PREG_SET_ORDER);
|
||
|
||
$codeBlocks = [];
|
||
foreach ($matches as $match) {
|
||
$codeBlocks[] = [
|
||
'language' => $match[1] ?? '',
|
||
'code' => $match[2],
|
||
];
|
||
}
|
||
|
||
return $codeBlocks;
|
||
}
|
||
|
||
/**
|
||
* 获取消息中的链接
|
||
*/
|
||
public function getLinksAttribute(): array
|
||
{
|
||
preg_match_all('/https?:\/\/[^\s]+/', $this->content, $matches);
|
||
|
||
return $matches[0] ?? [];
|
||
}
|
||
|
||
/**
|
||
* 获取消息中的关键词
|
||
*/
|
||
public function getKeywordsAttribute(): array
|
||
{
|
||
// 简单关键词提取
|
||
$content = strtolower($this->content);
|
||
$words = preg_split('/\s+/', $content);
|
||
$wordCount = array_count_values($words);
|
||
arsort($wordCount);
|
||
|
||
// 过滤常见词
|
||
$commonWords = ['the', 'and', 'you', 'for', 'are', 'this', 'that', 'with', 'have', 'from'];
|
||
$keywords = array_diff(array_keys($wordCount), $commonWords);
|
||
|
||
return array_slice($keywords, 0, 10);
|
||
}
|
||
|
||
/**
|
||
* 获取消息情感分析(简单版)
|
||
*/
|
||
public function getSentimentAttribute(): string
|
||
{
|
||
$positiveWords = ['好', '优秀', '成功', '满意', '高兴', '喜欢', '感谢', '完美', '棒', '赞'];
|
||
$negativeWords = ['差', '失败', '不满意', '生气', '讨厌', '问题', '错误', '糟糕', '慢', '贵'];
|
||
|
||
$content = $this->content;
|
||
$positiveCount = 0;
|
||
$negativeCount = 0;
|
||
|
||
foreach ($positiveWords as $word) {
|
||
$positiveCount += substr_count($content, $word);
|
||
}
|
||
|
||
foreach ($negativeWords as $word) {
|
||
$negativeCount += substr_count($content, $word);
|
||
}
|
||
|
||
if ($positiveCount > $negativeCount) {
|
||
return 'positive';
|
||
} elseif ($negativeCount > $positiveCount) {
|
||
return 'negative';
|
||
} else {
|
||
return 'neutral';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取消息长度等级
|
||
*/
|
||
public function getLengthLevelAttribute(): string
|
||
{
|
||
$length = strlen($this->content);
|
||
|
||
if ($length < 50) {
|
||
return 'short';
|
||
} elseif ($length < 200) {
|
||
return 'medium';
|
||
} elseif ($length < 500) {
|
||
return 'long';
|
||
} else {
|
||
return 'very-long';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取消息复杂度
|
||
*/
|
||
public function getComplexityAttribute(): float
|
||
{
|
||
$content = $this->content;
|
||
|
||
// 计算平均句子长度
|
||
$sentences = preg_split('/[。!?.!?]/', $content);
|
||
$sentences = array_filter($sentences, function($sentence) {
|
||
return strlen(trim($sentence)) > 0;
|
||
});
|
||
|
||
if (empty($sentences)) {
|
||
return 0;
|
||
}
|
||
|
||
$totalWords = 0;
|
||
foreach ($sentences as $sentence) {
|
||
$words = preg_split('/\s+/', $sentence);
|
||
$totalWords += count($words);
|
||
}
|
||
|
||
$avgSentenceLength = $totalWords / count($sentences);
|
||
|
||
// 计算生词比例(简单版)
|
||
$commonWords = ['的', '了', '在', '是', '和', '与', '或', '等', '这个', '那个'];
|
||
$allWords = preg_split('/\s+/', $content);
|
||
$uniqueWords = array_unique($allWords);
|
||
$uncommonWords = array_diff($uniqueWords, $commonWords);
|
||
|
||
$uncommonRatio = count($uncommonWords) / max(1, count($uniqueWords));
|
||
|
||
// 综合复杂度
|
||
$complexity = ($avgSentenceLength * 0.6) + ($uncommonRatio * 40);
|
||
|
||
return min(100, max(0, $complexity));
|
||
}
|
||
|
||
/**
|
||
* 获取对话中的消息统计
|
||
*/
|
||
public static function getConversationStatistics($conversationId)
|
||
{
|
||
$messages = self::where('conversation_id', $conversationId)->get();
|
||
|
||
$totalMessages = $messages->count();
|
||
$userMessages = $messages->where('role', 'user')->count();
|
||
$aiMessages = $messages->where('role', 'assistant')->count();
|
||
|
||
$totalTokens = $messages->sum('tokens');
|
||
$userTokens = $messages->where('role', 'user')->sum('tokens');
|
||
$aiTokens = $messages->where('role', 'assistant')->sum('tokens');
|
||
|
||
$avgTokensPerMessage = $totalMessages > 0 ?
|
||
round($totalTokens / $totalMessages, 1) : 0;
|
||
|
||
$avgResponseTime = self::calculateAvgResponseTime($messages);
|
||
|
||
return [
|
||
'total_messages' => $totalMessages,
|
||
'user_messages' => $userMessages,
|
||
'ai_messages' => $aiMessages,
|
||
'message_ratio' => $userMessages > 0 ?
|
||
round($aiMessages / $userMessages, 2) : 0,
|
||
'total_tokens' => $totalTokens,
|
||
'user_tokens' => $userTokens,
|
||
'ai_tokens' => $aiTokens,
|
||
'avg_tokens_per_message' => $avgTokensPerMessage,
|
||
'avg_response_time' => $avgResponseTime,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 计算平均响应时间
|
||
*/
|
||
private static function calculateAvgResponseTime($messages)
|
||
{
|
||
$aiMessages = $messages->where('role', 'assistant');
|
||
|
||
if ($aiMessages->isEmpty()) {
|
||
return null;
|
||
}
|
||
|
||
$totalSeconds = 0;
|
||
$count = 0;
|
||
|
||
foreach ($aiMessages as $aiMessage) {
|
||
$previousMessage = $messages
|
||
->where('created_at', '<', $aiMessage->created_at)
|
||
->sortByDesc('created_at')
|
||
->first();
|
||
|
||
if ($previousMessage) {
|
||
$diff = $aiMessage->created_at->diffInSeconds($previousMessage->created_at);
|
||
$totalSeconds += $diff;
|
||
$count++;
|
||
}
|
||
}
|
||
|
||
if ($count === 0) {
|
||
return null;
|
||
}
|
||
|
||
$avgSeconds = $totalSeconds / $count;
|
||
|
||
if ($avgSeconds < 60) {
|
||
return round($avgSeconds) . '秒';
|
||
} else {
|
||
return round($avgSeconds / 60, 1) . '分钟';
|
||
}
|
||
}
|
||
} |