Files
chat/backend-tp8/app/controller/api/Chat.php
T
2026-07-22 10:18:59 +08:00

174 lines
5.7 KiB
PHP

<?php
namespace app\controller\api;
use app\model\Conversation as ConversationModel;
use app\model\Message;
use app\service\OpenAIService;
use app\service\PermissionService;
use app\service\SettingsService;
use think\exception\HttpResponseException;
class Chat extends BaseApi
{
public function completions()
{
$user = $this->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}";
}
}