hasOne(WebSocketDialog::class, 'id', 'dialog_id'); } /** * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function user(): \Illuminate\Database\Eloquent\Relations\HasOne { return $this->hasOne(User::class, 'userid', 'userid'); } /** * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function extra(): \Illuminate\Database\Eloquent\Relations\HasOne { return $this->hasOne(WebSocketDialogMsgExtra::class, 'msg_id', 'id'); } /** * 阅读占比 * @return int|mixed */ public function getPercentageAttribute() { if (!isset($this->appendattrs['percentage'])) { if ($this->read > $this->send || empty($this->send)) { $this->appendattrs['percentage'] = 100; } else { $this->appendattrs['percentage'] = intval($this->read / $this->send * 100); } } return $this->appendattrs['percentage']; } /** * 消息格式化 * @param $value * @return array|mixed */ public function getMsgAttribute($value) { if (is_array($value)) { return $value; } $value = $this->formatDataMsg($this->type, $value); if (isset($value['reply_data'])) { $value['reply_data']['msg'] = $this->formatDataMsg($value['reply_data']['type'], $value['reply_data']['msg']); } return $value; } /** * emoji回复格式化 * @param $value * @return array|mixed */ public function getEmojiAttribute($value) { if (is_array($value)) { return $value; } return Base::json2array($value); } /** * 处理消息数据 * @param $type * @param $msg * @return mixed */ private function formatDataMsg($type, $msg) { if (!is_array($msg)) { $msg = Base::json2array($msg); } switch ($type) { case 'file': $msg['type'] = in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file'; $msg['path'] = Base::fillUrl($msg['path']); $msg['thumb'] = Base::fillUrl($msg['thumb'] ?: Base::extIcon($msg['ext'])); break; case 'record': $msg['path'] = Base::fillUrl($msg['path']); $textUserid = is_array($msg['text_userid']) ? $msg['text_userid'] : []; if (isset($msg['text_userid'])) { unset($msg['text_userid']); } if ($msg['text'] && !in_array(Doo::userId(), $textUserid)) { $msg['text'] = ""; } break; case 'location': $msg['thumb'] = Base::fillUrl($msg['thumb'] ?: "images/other/location.jpg"); break; case 'template': if ($msg['data']['thumb']) { $msg['data']['thumb']['url'] = Base::fillUrl($msg['data']['thumb']['url']); } break; } return $msg; } /** * 标记已送达 同时 告诉发送人已送达 * @param $userid * @return bool */ public function readSuccess($userid) { if (empty($userid)) { return false; } self::transaction(function() use ($userid) { $msgRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereUserid($userid)->lockForUpdate()->first(); if (empty($msgRead)) { $msgRead = WebSocketDialogMsgRead::createInstance([ 'dialog_id' => $this->dialog_id, 'msg_id' => $this->id, 'userid' => $userid, 'after' => 1, ]); if ($msgRead->saveOrIgnore()) { $this->send = WebSocketDialogMsgRead::whereMsgId($this->id)->count(); $this->save(); } else { return; } } if (!$msgRead->read_at) { $msgRead->read_at = Carbon::now(); $msgRead->save(); // $row = self::incrementRead($this->id); PushTask::push([ 'userid' => $row->userid, 'msg' => [ 'type' => 'dialog', 'mode' => 'readed', 'data' => [ 'id' => $row->id, 'read' => $row->read, 'percentage' => $row->percentage, ], ] ]); } }); return true; } /** * 增加已读数量 * @param $msgId * @return self */ private static function incrementRead($msgId) { return self::transaction(function () use ($msgId) { $model = WebSocketDialogMsg::lockForUpdate()->find($msgId); if (!$model) { throw new \Exception('记录不存在'); } $model->increment('read'); return WebSocketDialogMsg::find($msgId); }); } /** * emoji回复 * @param $symbol * @param int $sender 发送的会员ID * @return mixed */ public function emojiMsg($symbol, $sender) { $exist = false; $array = $this->emoji; foreach ($array as $index => &$item) { if ($item['symbol'] === $symbol) { if (in_array($sender, $item['userids'])) { // 已存在 去除 $item['userids'] = array_values(array_diff($item['userids'], [$sender])); if (empty($item['userids'])) { unset($array[$index]); $array = array_values($array); } } else { // 未存在 添加 array_unshift($item['userids'], $sender); } $exist = true; break; } } if (!$exist) { array_unshift($array, [ 'symbol' => $symbol, 'userids' => [$sender] ]); } // $this->emoji = Base::array2json($array); $this->save(); $resData = [ 'id' => $this->id, 'emoji' => $array, ]; // $dialog = WebSocketDialog::find($this->dialog_id); if ($dialog) { $dialog->pushMsg('update', $resData); WebSocketDialogUser::whereDialogId($dialog->id)->change([ 'updated_at' => Carbon::now()->toDateTimeString('millisecond'), ]); } // return Base::retSuccess('success', $resData); } /** * 是否完成所有待办 * @param bool $noCache 是否禁止缓存 * @return int 1=已完成 0=未完成 */ public function isTodoDone(?bool $noCache = false): int { if ($noCache) { Cache::forget('todo_done_' . $this->id); } if ($this->todo <= 0) { return 1; } return (int) Cache::remember('todo_done_' . $this->id, Carbon::now()->addDays(), function () { return WebSocketDialogMsgTodo::whereMsgId($this->id)->whereDoneAt(null)->exists() ? 0 : 1; }); } /** * 标注、取消标注 * @param int $sender 标注的会员ID * @return mixed */ public function toggleTagMsg($sender) { if (in_array($this->type, ['tag', 'todo', 'notice'])) { return Base::retError('此消息不支持标注'); } $before = $this->tag; $this->tag = $before ? 0 : $sender; $this->save(); $resData = [ 'id' => $this->id, 'tag' => $this->tag, ]; // $data = [ 'update' => $resData ]; $res = self::sendMsg(null, $this->dialog_id, 'tag', [ 'action' => $this->tag ? 'add' : 'remove', 'data' => [ 'id' => $this->id, 'type' => $this->type, 'msg' => $this->quoteTextMsg(), ] ], $sender); if (Base::isSuccess($res)) { $data['add'] = $res['data']; $dialog = WebSocketDialog::find($this->dialog_id); $dialog->pushMsg('update', $resData); } else { $this->tag = $before; $this->save(); } // return Base::retSuccess($this->tag ? '标注成功' : '取消成功', $data); } /** * 设待办、取消待办 * @param int $sender 设待办的会员ID * @param array $userids 设置给指定会员 * @return mixed */ public function toggleTodoMsg($sender, $userids = []) { if (in_array($this->type, ['tag', 'todo', 'notice'])) { return Base::retError('此消息不支持设待办'); } $dialog = WebSocketDialog::find($this->dialog_id); $current = WebSocketDialogMsgTodo::whereMsgId($this->id)->pluck('userid')->toArray(); $cancel = array_diff($current, $userids); $setup = array_diff($userids, $current); // $this->todo = $setup || count($current) > count($cancel) ? $sender : 0; $this->save(); // $addData = []; if ($cancel) { $res = self::sendMsg(null, $this->dialog_id, 'todo', [ 'action' => 'remove', 'data' => [ 'id' => $this->id, 'type' => $this->type, 'msg' => $this->quoteTextMsg(), 'userids' => implode(",", $cancel), ] ], $sender); if (Base::isSuccess($res)) { $addData[] = $res['data']; WebSocketDialogMsgTodo::whereMsgId($this->id)->whereIn('userid', $cancel)->delete(); } } if ($setup) { $res = self::sendMsg(null, $this->dialog_id, 'todo', [ 'action' => 'add', 'data' => [ 'id' => $this->id, 'type' => $this->type, 'msg' => $this->quoteTextMsg(), 'userids' => implode(",", $setup), ] ], $sender); if (Base::isSuccess($res)) { $addData[] = $res['data']; $useridList = $dialog->dialogUser->pluck('userid')->toArray(); foreach ($setup as $userid) { if (!in_array($userid, $useridList)) { continue; } WebSocketDialogMsgTodo::createInstance([ 'dialog_id' => $this->dialog_id, 'msg_id' => $this->id, 'userid' => $userid, ])->saveOrIgnore(); } } } // $upData = [ 'id' => $this->id, 'todo' => $this->todo, 'todo_done' => $this->isTodoDone(true), 'dialog_id' => $this->dialog_id, ]; $dialog->pushMsg('update', $upData); // return Base::retSuccess($this->todo ? '设置成功' : '取消成功', [ 'add' => $addData, 'update' => $upData, ]); } /** * 转发消息 * @param array|int $dialogids * @param array|int $userids * @param User $user 发送的会员 * @param int $showSource 是否显示原发送者信息 * @param string $leaveMessage 转发留言 * @return mixed */ public function forwardMsg($dialogids, $userids, $user, $showSource = 1, $leaveMessage = '') { return AbstractModel::transaction(function () use ($dialogids, $user, $userids, $showSource, $leaveMessage) { $msgData = Base::json2array($this->getRawOriginal('msg')); $forwardData = is_array($msgData['forward_data']) ? $msgData['forward_data'] : []; $forwardId = $forwardData['id'] ?: $this->id; $forwardUserid = $forwardData['userid'] ?: $this->userid; if ($forwardData['show'] === 0) { // 如果上一条消息不显示原发送者信息,则转发的消息原始数据为当前消息 $forwardId = $this->id; $forwardUserid = $this->userid; } $msgData['forward_data'] = [ 'id' => $forwardId, // 转发的消息ID(原始) 'userid' => $forwardUserid, // 转发的消息会员ID(原始) 'parent_id' => $this->id, // 转发的消息ID 'parent_userid' => $this->userid, // 转发的消息会员ID 'show' => $showSource, // 是否显示原发送者信息 'leave' => $leaveMessage ? 1 : 0, // 是否留言(用于判断是否发给AI) ]; $msgs = []; $dialogs = []; if ($userids) { if (!is_array($userids)) { $userids = [$userids]; } foreach ($userids as $userid) { if (!User::whereUserid($userid)->exists()) { continue; } $dialog = WebSocketDialog::checkUserDialog($user, $userid); if ($dialog) { $dialogs[$dialog->id] = $dialog; } } } if ($dialogids) { if (!is_array($dialogids)) { $dialogids = [$dialogids]; } foreach ($dialogids as $dialogid) { if (isset($dialogs[$dialogid])) { continue; } $dialog = WebSocketDialog::find($dialogid); if ($dialog) { $dialogs[$dialog->id] = $dialog; } } } foreach ($dialogs as $dialog) { $res = self::sendMsg('forward-' . $forwardId, $dialog->id, $this->type, $msgData, $user->userid); if (Base::isSuccess($res)) { $msgs[] = $res['data']; } if ($leaveMessage) { $action = $dialog->isAiDialog() ? "reply-{$res['data']['id']}" : null; $res = self::sendMsg($action, $dialog->id, 'text', ['text' => $leaveMessage], $user->userid); if (Base::isSuccess($res)) { $msgs[] = $res['data']; } } } if (count($msgs) > 0) { $this->increment('forward_num', count($msgs)); } return Base::retSuccess('转发成功', [ 'msgs' => $msgs ]); }); } /** * 删除消息 * @param array|int $ids * @return void */ public static function deleteMsgs($ids) { $ids = Base::arrayRetainInt(is_array($ids) ? $ids : [$ids], true); AbstractModel::transaction(function() use ($ids) { $dialogIds = WebSocketDialogMsg::select('dialog_id')->whereIn("id", $ids)->distinct()->get()->pluck('dialog_id'); $replyIds = WebSocketDialogMsg::select('reply_id')->whereIn("id", $ids)->distinct()->get()->pluck('reply_id'); // WebSocketDialogMsgRead::whereIn('msg_id', $ids)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可 WebSocketDialogMsgTodo::whereIn('msg_id', $ids)->delete(); self::whereIn('id', $ids)->delete(); // foreach ($dialogIds as $dialogId) { WebSocketDialogUser::updateMsgLastAt($dialogId); } foreach ($replyIds as $id) { self::whereId($id)->update(['reply_num' => self::whereReplyId($id)->count()]); } }); } /** * 撤回消息 * @return void */ public function withdrawMsg() { AbstractModel::transaction(function() { $deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可 $this->delete(); // if ($this->reply_id > 0) { self::whereId($this->reply_id)->decrement('reply_num'); } // $dialogData = $this->webSocketDialog; if ($dialogData) { foreach ($dialogData->dialogUser as $dialogUser) { $dialogUser->updated_at = Carbon::now(); $dialogUser->save(); } $userids = $dialogData->dialogUser->pluck('userid')->toArray(); PushTask::push([ 'userid' => $userids, 'msg' => [ 'type' => 'dialog', 'mode' => 'delete', 'data' => [ 'id' => $this->id, 'dialog_id' => $this->dialog_id, 'last_msg' => WebSocketDialogUser::updateMsgLastAt($this->dialog_id), 'update_read' => $deleteRead ? 1 : 0 ], ] ]); } // WebSocketDialogMsgTodo::whereMsgId($this->id)->delete(); }); } /** * 预览消息 * @param WebSocketDialogMsg|array $data 消息数据 * @param bool $preserveHtml 保留html格式 * @return string */ public static function previewMsg($data, $preserveHtml = false) { if ($data instanceof WebSocketDialogMsg) { $data = [ 'type' => $data->type, 'msg' => $data->msg, ]; } if (!is_array($data)) { return ''; } switch ($data['type']) { case 'text': return self::previewTextMsg($data['msg'], $preserveHtml); case 'longtext': return $data['msg']['desc'] ? Base::cutStr($data['msg']['desc'], 50) : ("[" . Doo::translate("长文本") . "]"); case 'vote': $action = Doo::translate("投票"); return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml); case 'word-chain': $action = Doo::translate("接龙"); return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml); case 'record': $action = Doo::translate("语音"); return "[{$action}]"; case 'location': $action = Doo::translate("位置"); return "[{$action}] " . Base::cutStr($data['msg']['title'], 50); case 'meeting': $action = Doo::translate("会议"); return "[{$action}] " . Base::cutStr($data['msg']['name'], 50); case 'file': return self::previewFileMsg($data['msg']); case 'tag': $action = Doo::translate($data['msg']['action'] === 'remove' ? '取消标注' : '标注'); return "[{$action}] " . self::previewMsg($data['msg']['data']); case 'top': $action = Doo::translate($data['msg']['action'] === 'remove' ? '取消置顶' : '置顶'); return "[{$action}] " . self::previewMsg($data['msg']['data']); case 'todo': $action = Doo::translate($data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办')); return "[{$action}] " . self::previewMsg($data['msg']['data']); case 'notice': $notice = $data['msg']['source'] === 'api' ? $data['msg']['notice'] : Doo::translate($data['msg']['notice']); return Base::cutStr($notice, 50); case 'template': return self::previewTemplateMsg($data['msg']); case 'preview': return $data['msg']['preview']; default: $action = Doo::translate("未知的消息"); return "[{$action}]"; } } /** * 返回文本预览消息 * @param array $msgData * @param bool $preserveHtml 保留html格式 * @return string|string[]|null */ public static function previewTextMsg($msgData, $preserveHtml = false) { $text = $msgData['text'] ?? ''; if (!$text) return ''; if ($msgData['type'] === 'md') { $text = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $text); if (preg_match('/:::\s*reasoning\s+/', $text)) { return Doo::translate('思考中...'); } $title = ''; if (preg_match('/^#{1,2}\s+(.+)/m', $text, $matches)) { $title = trim($matches[1]); } if ($title) { $text = $title; } else { $text = Base::markdown2html($text); $text = self::previewConvertTaskList($text); } } $text = preg_replace("/]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text); $text = preg_replace("/]*?>/", "[" . Doo::translate('动画表情') . "]", $text); $text = preg_replace("/]*?>/", "[" . Doo::translate('图片') . "]", $text); if (!$preserveHtml) { $text = str_replace("

", "

", $text); $text = strip_tags($text); $text = str_replace([" ", """, "&", "<", ">"], [" ", '"', "&", "<", ">"], $text); $text = preg_replace("/\s+/", " ", $text); $text = Base::cutStr($text, 50); } return $text; } /** * 转换任务列表 * @param $text * @return array|string|string[]|null */ private static function previewConvertTaskList($text) { $pattern = '/:::\s*(create-task-list|create-subtask-list)(.*?):::/s'; $replacement = function($matches) { $content = $matches[2]; $lines = explode("\n", trim($content)); $result = []; $currentTitle = ''; foreach ($lines as $line) { $line = trim($line); if (empty($line)) continue; if (preg_match('/^title:\s*(.+)$/', $line, $titleMatch)) { $currentTitle = $titleMatch[1]; $result[] = $currentTitle; } elseif (preg_match('/^desc:\s*(.+)$/', $line, $descMatch)) { if (!empty($currentTitle)) { $result[] = $descMatch[1]; } } } return implode("\n", $result); }; return preg_replace_callback($pattern, $replacement, $text); } /** * 预览文件消息 * @param $msg * @return string */ private static function previewFileMsg($msg) { if ($msg['type'] == 'img') { $action = Doo::translate("图片"); return "[{$action}]"; } elseif ($msg['ext'] == 'mp4') { $action = Doo::translate("视频"); return "[{$action}]"; } $action = Doo::translate("文件"); return "[{$action}] " . Base::cutStr($msg['name'], 50); } /** * 预览模板消息 * @param $msg * @return string */ private static function previewTemplateMsg($msg) { if (!empty($msg['title_raw'])) { return $msg['title_raw']; } if ($msg['type'] === 'task_list' && count($msg['list']) === 1) { $title = $msg['source'] === 'api' ? $msg['title'] : Doo::translate($msg['title']); return $title . ": " . Base::cutStr($msg['list'][0]['name'], 50); } if (!empty($msg['title'])) { return $msg['source'] === 'api' ? $msg['title'] : Doo::translate($msg['title']); } if ($msg['type'] === 'content' && is_string($msg['content']) && $msg['content'] !== '') { $content = $msg['source'] === 'api' ? $msg['content'] : Doo::translate($msg['content']); return Base::cutStr($content, 50); } return Doo::translate('未知的消息'); } /** * 生成关键词并保存 * @param string $key * @return void */ public function generateKeyAndSave($key = ''): void { if (empty($key)) { $key = ''; switch ($this->type) { case 'text': if (preg_match("/]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/i", $this->msg['text'])) { break; } $key = $this->msg['text']; if ($this->msg['type'] === 'md') { $key = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $key); $key = Base::markdown2html($key); } $key = strip_tags($key); break; case 'vote': case 'word-chain': $key = strip_tags($this->msg['text']); break; case 'file': $key = $this->msg['name']; $key = preg_replace("/^(image|\d+)\.(png|jpg|jpeg|webp|gif)$/i", "", $key); $key = preg_replace("/^LongText-(.*?)/i", "", $key); break; case 'meeting': $key = $this->msg['name']; break; } } $this->key = self::filterEscape($key); $this->save(); } /** * 过滤转义 * @param $content * @return string */ public static function filterEscape($content) { $content = str_replace([""", "&", "<", ">"], "", $content); $content = str_replace(["\r", "\n", "\t", " "], " ", $content); $content = preg_replace("/^\/[A-Za-z]+/", " ", $content); $content = preg_replace("/\s+/", " ", $content); return trim($content); } /** * 返回引用消息(如果是文本只取预览) * @return array|mixed */ public function quoteTextMsg() { $msg = $this->msg; if ($this->type === 'text') { $msg['text'] = self::previewTextMsg($msg); } return $msg; } /** * 提取消息内容 * 根据消息类型(文件、文本等)提取相应的内容文本 * * @param int $maxLength 最大长度,超过则截取,0表示不限制 * @return string 提取出的消息文本内容 */ public function extractMessageContent(int $maxLength = 0): string { $reserves = []; switch ($this->type) { case "file": // 提取文件消息 $msgData = Base::json2array($this->getRawOriginal('msg')); $result = $this->convertMentionFormat("path", $msgData['path'], $msgData['name'], $reserves); break; case "text": // 提取文本消息 $result = $this->msg['text'] ?: ''; if (empty($result)) { return ''; } // 提取快捷键 if (preg_match("/]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $result, $match)) { $command = $match[2] ?? ''; $command = preg_replace("/^%3A\.?/", ":", $command); $command = trim($command); if ($command) { return $command; } } // 提及任务、文件、报告 $result = preg_replace_callback_array([ // 用户 "/(.*?)<\/span>/" => function () { return ""; }, // 任务 "/#?(.*?)<\/span>/" => function ($match) use (&$reserves) { return $this->convertMentionFormat("task", $match[1], $match[2], $reserves); }, // 文件 "/]*?>~?(.*?)<\/a>/" => function ($match) use (&$reserves) { if (preg_match("/single\/file\/(.*?)$/", $match[1], $subMatch)) { return $this->convertMentionFormat("file", $subMatch[1], $match[2], $reserves); } return ""; }, // 报告 "/]*?>%?(.*?)<\/a>/" => function ($match) use (&$reserves) { if (preg_match("/single\/report\/detail\/(.*?)$/", $match[1], $subMatch)) { return $this->convertMentionFormat("report", $subMatch[1], $match[2], $reserves); } return ""; }, ], $result); // 转成 markdown if ($this->msg['type'] !== 'md') { $result = Base::html2markdown($result); } break; default: // 其他类型消息不处理 return ''; } // 处理 reserves foreach ($reserves as $rand => $mention) { $result = str_replace($rand, $mention, $result); } // 截取最大长度 if ($maxLength > 0 && mb_strlen($result) > $maxLength) { $result = mb_substr($result, 0, $maxLength); } return $result; } /** * 转换提及消息格式 * 将提及的任务、文件、报告等转换为统一的格式 [type#key#name] * * @param string $type 提及类型(task、file、report、path) * @param string $key 提及对象的唯一标识 * @param string $name 提及对象的显示名称 * @return string 格式化后的提及字符串 */ private function convertMentionFormat($type, $key, $name, &$reserves) { $key = str_replace(['#', '-->'], '', $key); $name = str_replace(['#', '-->'], '', $name); $rand = Base::generatePassword(12); $reserves[$rand] = ""; return $rand; } /** * 处理文本消息内容,用于发送前 * @param $text * @param $dialog_id * @return mixed|string|string[] */ public static function formatMsg($text, $dialog_id) { @ini_set("pcre.backtrack_limit", 999999999); // 基础处理 $text = preg_replace("/<(\/[a-zA-Z]+)\s*>/s", "<$1>", $text); // 图片 [:IMAGE:className:width:height:src:alt:] preg_match_all("/(<\/img>)*/s", $text, $matchs); foreach ($matchs[2] as $key => $base64) { $imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/"; Base::makeDir(public_path($imagePath)); $imagePath .= md5s($base64) . "." . $matchs[1][$key]; if (Base::saveContentImage(public_path($imagePath), base64_decode($base64))) { $imageSize = getimagesize(public_path($imagePath)); if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) { $imagePath .= "_thumb.{$extension}"; } $text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text); } } // 表情图片 preg_match_all("//s", $text, $matchs); foreach ($matchs[1] as $key => $str) { preg_match("/data-asset=\"(.*?)\"/", $str, $matchAsset); preg_match("/data-name=\"(.*?)\"/", $str, $matchName); $imageSize = null; $imagePath = ""; $imageName = ""; if ($matchAsset[1] === "emosearch") { preg_match("/src=\"(.*?)\"/", $str, $matchSrc); if ($matchSrc) { $srcMd5 = md5($matchSrc[1]); $imagePath = "uploads/emosearch/" . substr($srcMd5, 0, 2) . "/" . substr($srcMd5, 32 - 2) . "/"; Base::makeDir(public_path($imagePath)); $imagePath .= md5s($matchSrc[1]); if (file_exists(public_path($imagePath))) { $imageSize = getimagesize(public_path($imagePath)); } else { $image = file_get_contents($matchSrc[1]); if ($image && file_put_contents(public_path($imagePath), $image)) { $imageSize = getimagesize(public_path($imagePath)); // 添加后缀 if ($imageSize && !str_contains($imagePath, '.')) { preg_match("/^image\/(png|jpg|jpeg|webp|gif)$/", $imageSize['mime'], $matchMine); if ($matchMine) { $imageNewPath = $imagePath . "." . $matchMine[1]; if (rename(public_path($imagePath), public_path($imageNewPath))) { $imagePath = $imageNewPath; } } } } } } } elseif (file_exists(public_path($matchAsset[1]))) { $imagePath = $matchAsset[1]; $imageName = $matchName[1]; $imageSize = getimagesize(public_path($matchAsset[1])); } if ($imageSize) { $text = str_replace($matchs[0][$key], "[:IMAGE:emoticon:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}:{$imageName}:]", $text); } else { $text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text); } } // 其他网络图片 $imageSaveLocal = Base::settingFind("system", "image_save_local"); preg_match_all("/]*?src=([\"'])(.*?(png|jpg|jpeg|webp|gif).*?)\\1[^>]*?>/is", $text, $matchs); foreach ($matchs[2] as $key => $str) { $parsed = parse_url($str); if (str_starts_with($parsed['path'], "/uploads/")) { $relativePath = ltrim($parsed['path'], "/"); $relativePath = Base::thumbRestore($relativePath); if (file_exists(public_path($relativePath))) { $str = "{{RemoteURL}}{$relativePath}"; } } if ($imageSaveLocal === 'close') { $imageSize = @getimagesize($str); if ($imageSize === false) { $imageSize = ["auto", "auto"]; } $imagePath = "base64-" . base64_encode($str); $text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text); continue; } if (str_starts_with($str, "{{RemoteURL}}")) { $imagePath = Base::leftDelete($str, "{{RemoteURL}}"); $imagePath = Base::thumbRestore($imagePath); } else { $imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/"; Base::makeDir(public_path($imagePath)); $imagePath .= md5s($str) . "." . $matchs[3][$key]; } if (file_exists(public_path($imagePath))) { $imageSize = getimagesize(public_path($imagePath)); if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) { $imagePath .= "_thumb.{$extension}"; } $text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text); } else { $image = file_get_contents($str); if (empty($image)) { $text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text); } else if (Base::saveContentImage(public_path($imagePath), $image)) { $imageSize = getimagesize(public_path($imagePath)); if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) { $imagePath .= "_thumb.{$extension}"; } $text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text); } } } // @成员、#任务、~文件、%报告 preg_match_all("/.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs); foreach ($matchs[1] as $key => $str) { preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar); preg_match("/data-id=\"(.*?)\"/", $str, $matchId); preg_match("/data-value=\"(.*?)\"/s", $str, $matchValye); $keyId = $matchId[1]; if ($matchChar[1] === "~") { // 文件特殊处理 if (Base::isNumber($keyId)) { $file = File::permissionFind($keyId, User::auth()); if ($file->type == 'folder') { throw new ApiException('文件夹不支持分享'); } $fileLink = $file->getShareLink(User::userid()); $keyId = $fileLink['code']; } else { preg_match("/\/single\/file\/(.*?)$/i", $keyId, $match); if ($match && strlen($match[1]) >= 8) { $keyId = $match[1]; } else { throw new ApiException('文件分享错误'); } } } elseif ($matchChar[1] === "%") { // 报告特殊处理 if (Base::isNumber($keyId)) { $reportLink = ReportLink::generateLink($keyId, User::userid()); $keyId = $reportLink['code']; } else { preg_match("/\/single\/report\/detail\/(.*?)$/i", $keyId, $match); if ($match && strlen($match[1]) >= 8) { $keyId = $match[1]; } else { throw new ApiException('报告分享错误'); } } } $text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text); } // 处理快捷消息 preg_match_all("/]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $text, $matchs); foreach ($matchs[0] as $key => $str) { $quickKey = $matchs[2][$key]; $quickLabel = $matchs[3][$key]; if ($quickKey && $quickLabel) { $quickKey = str_replace(":", "", $quickKey); $quickLabel = str_replace(":", "", $quickLabel); $text = str_replace($str, "[:QUICK:{$quickKey}:{$quickLabel}:]", $text); } } // 处理 li 标签 preg_match_all("/]*?>/i", $text, $matchs); foreach ($matchs[0] as $str) { if (preg_match("/data-list=['\"](bullet|ordered|checked|unchecked)['\"]/i", $str, $match)) { $text = str_replace($str, '

  • ', $text); } else { $text = str_replace($str, '
  • ', $text); } } // 处理链接标签 preg_match_all("/]*?href=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/a>/is", $text, $matchs); foreach ($matchs[0] as $key => $str) { $herf = $matchs[2][$key]; $title = $matchs[3][$key] ?: $herf; if (self::formatLink($str, strip_tags($title), $text)) { continue; } $herf = base64_encode($herf); $title = base64_encode($title); $text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text); } // 分享链接 preg_match_all("/(https?:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs); if ($matchs) { foreach ($matchs[0] as $str) { self::formatLink($str, $str, $text); } } // 过滤标签 $text = strip_tags($text, '