erp-backend/app/Models/AIMessage.php
2026-04-01 17:07:04 +08:00

399 lines
11 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
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) . '分钟';
}
}
}