authUser(); PermissionService::checkDailyLimit($user); $input = $this->request->post(); $conversationId = (int) ($input['conversation_id'] ?? 0); $content = trim($input['content'] ?? ''); $attachments = $input['attachments'] ?? []; $stream = ($input['stream'] ?? true) !== false; if (!$conversationId) { return $this->error('缺少 conversation_id'); } if (!$content && empty($attachments)) { return $this->error('消息内容不能为空'); } $conversation = ConversationModel::where('id', $conversationId) ->where('user_id', $user['id']) ->whereNull('deleted_at') ->find(); if (!$conversation) { return $this->error('会话不存在', 404); } $contentType = SettingsService::isFeatureEnabled('markdown') ? 'markdown' : 'text'; if (!empty($attachments)) { $contentType = 'mixed'; } Message::create([ 'conversation_id' => $conversationId, 'role' => 'user', 'content' => $content, 'content_type' => $contentType, 'attachments' => $attachments, ]); PermissionService::incrementDailyCount((int) $user['id']); $history = Message::where('conversation_id', $conversationId) ->field('role,content,attachments') ->order('created_at', 'asc') ->limit(50) ->select() ->toArray(); $apiMessages = $this->buildApiMessages($history); $model = OpenAIService::getModel($conversation->model_id ? (int) $conversation->model_id : null); if ((int) $conversation->message_count === 0 && $content) { $conversation->save(['title' => mb_substr($content, 0, 30)]); } $conversation->inc('message_count')->update(['updated_at' => date('Y-m-d H:i:s')]); if ($stream) { $this->streamResponse($conversationId, $model, $apiMessages, $user); } return $this->syncResponse($conversationId, $model, $apiMessages, $user); } private function streamResponse(int $conversationId, $model, array $apiMessages, array $user): void { OpenAIService::sseHeaders(); $fullContent = ''; OpenAIService::streamChat($model, $apiMessages, function ($chunk) use (&$fullContent) { $delta = $chunk['choices'][0]['delta']['content'] ?? ''; if ($delta) { $fullContent .= $delta; OpenAIService::sseEvent('message', ['content' => $delta]); } }); Message::create([ 'conversation_id' => $conversationId, 'role' => 'assistant', 'content' => $fullContent, 'content_type' => 'markdown', ]); ConversationModel::where('id', $conversationId)->inc('message_count')->update([ 'updated_at' => date('Y-m-d H:i:s'), ]); PermissionService::incrementDailyCount((int) $user['id']); OpenAIService::sseEvent('done', ['content' => $fullContent]); exit; } private function syncResponse(int $conversationId, $model, array $apiMessages, array $user) { $result = OpenAIService::chat($model, $apiMessages); $content = $result['choices'][0]['message']['content'] ?? ''; $tokens = $result['usage']['total_tokens'] ?? 0; Message::create([ 'conversation_id' => $conversationId, 'role' => 'assistant', 'content' => $content, 'content_type' => 'markdown', 'tokens_used' => $tokens, ]); ConversationModel::where('id', $conversationId)->inc('message_count')->update([ 'updated_at' => date('Y-m-d H:i:s'), ]); PermissionService::incrementDailyCount((int) $user['id']); return $this->success(['content' => $content, 'tokens' => $tokens]); } private function buildApiMessages(array $history): array { $messages = []; foreach ($history as $msg) { $content = $msg['content']; $attachments = $msg['attachments'] ?? []; if (is_string($attachments)) { $attachments = json_decode($attachments, true) ?: []; } if (!empty($attachments) && SettingsService::isFeatureEnabled('image')) { $parts = []; if ($content) { $parts[] = ['type' => 'text', 'text' => $content]; } foreach ($attachments as $att) { if (($att['type'] ?? '') === 'image') { $parts[] = [ 'type' => 'image_url', 'image_url' => ['url' => $this->absoluteUrl($att['url'])], ]; } } $messages[] = ['role' => $msg['role'], 'content' => $parts]; } else { $messages[] = ['role' => $msg['role'], 'content' => $content]; } } return $messages; } private function absoluteUrl(string $url): string { if (str_starts_with($url, 'http')) { return $url; } $scheme = $this->request->scheme(); $host = $this->request->host(); return "{$scheme}://{$host}{$url}"; } }