Files
2026-08-05 15:56:08 +08:00

253 lines
9.1 KiB
PHP

<?php
namespace app\controller\api;
use app\model\AiModel;
use app\model\Conversation as ConversationModel;
use app\model\Message;
use app\service\ComfyUIService;
use app\service\GuestAccessService;
use app\service\PermissionService;
use think\exception\HttpResponseException;
class Conversation extends BaseApi
{
public function index()
{
$user = $this->authUser();
if (GuestAccessService::isGuest($user)) {
$guestModelId = GuestAccessService::modelId();
$reset = ['model_id' => $guestModelId, 'external_conversation_id' => null];
ConversationModel::where('user_id', $user['id'])->whereNull('deleted_at')->whereNull('model_id')->update($reset);
ConversationModel::where('user_id', $user['id'])->whereNull('deleted_at')->where('model_id', '<>', $guestModelId)->update($reset);
}
$page = max(1, (int) $this->request->get('page', 1));
$limit = min(50, max(1, (int) $this->request->get('limit', 20)));
$query = ConversationModel::alias('c')
->leftJoin('ai_models m', 'c.model_id = m.id')
->where('c.user_id', $user['id'])
->whereNull('c.deleted_at')
->field('c.*,m.name as model_name,m.provider as model_provider')
->order(['c.is_pinned' => 'desc', 'c.updated_at' => 'desc']);
$total = (clone $query)->count();
$list = $query->page($page, $limit)->select();
return $this->success([
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit,
]);
}
public function create()
{
$user = $this->authUser();
PermissionService::checkConversationLimit($user);
$input = $this->request->post();
$modelId = $input['model_id'] ?? null;
if ($modelId !== null && $modelId !== '') {
$modelId = (int) $modelId;
if ($modelId <= 0) {
$modelId = null;
}
} else {
$modelId = null;
}
if (GuestAccessService::isGuest($user)) {
$modelId = GuestAccessService::assertModelAllowed($user, $modelId);
} elseif ($modelId && !AiModel::where('id', $modelId)->where('enabled', 1)->find()) {
return $this->error('所选模型不存在或已停用', 422);
}
$conversation = ConversationModel::create([
'user_id' => $user['id'],
'title' => trim($input['title'] ?? '新对话'),
'model_id' => $modelId,
]);
return $this->success($this->findConversation((int) $conversation->id, (int) $user['id']), '创建成功');
}
public function show($id)
{
$user = $this->authUser();
return $this->success($this->findConversation((int) $id, (int) $user['id']));
}
public function update($id)
{
$user = $this->authUser();
$conversation = ConversationModel::where('id', $id)
->where('user_id', $user['id'])
->whereNull('deleted_at')
->find();
if (!$conversation) {
return $this->error('会话不存在', 404);
}
$input = $this->request->put();
$data = [];
if (isset($input['title'])) {
$data['title'] = trim($input['title']);
}
if (isset($input['is_pinned'])) {
$data['is_pinned'] = (int) $input['is_pinned'];
}
if (array_key_exists('model_id', $input)) {
$mid = $input['model_id'];
if ($mid === null || $mid === '') {
$data['model_id'] = null;
} else {
$data['model_id'] = (int) $mid;
}
if (GuestAccessService::isGuest($user)) {
$requestedModelId = $data['model_id'] ?: null;
$data['model_id'] = GuestAccessService::assertModelAllowed($user, $requestedModelId);
} elseif ($data['model_id'] && !AiModel::where('id', $data['model_id'])->where('enabled', 1)->find()) {
return $this->error('所选模型不存在或已停用', 422);
}
$currentModelId = $conversation->model_id === null
? null
: (int) $conversation->model_id;
if ($data['model_id'] !== $currentModelId) {
// Dify conversation_id 只属于创建它的应用/模型;切换模型后不可复用。
$data['external_conversation_id'] = null;
}
}
if (GuestAccessService::isGuest($user) && !array_key_exists('model_id', $input)) {
$guestModelId = GuestAccessService::modelId();
if ((int) $conversation->model_id !== $guestModelId) {
$data['model_id'] = $guestModelId;
$data['external_conversation_id'] = null;
}
}
if (empty($data)) {
return $this->error('无更新内容');
}
$conversation->save($data);
return $this->success($this->findConversation((int) $id, (int) $user['id']));
}
public function delete($id)
{
$user = $this->authUser();
$conversation = ConversationModel::where('id', $id)
->where('user_id', $user['id'])
->whereNull('deleted_at')
->find();
if (!$conversation) {
return $this->error('会话不存在', 404);
}
$conversation->save(['deleted_at' => date('Y-m-d H:i:s')]);
return $this->success(null, '删除成功');
}
public function messages($id)
{
$user = $this->authUser();
$this->findConversation((int) $id, (int) $user['id']);
$rows = Message::where('conversation_id', $id)
->field('id,role,content,content_type,attachments,created_at')
->order('created_at', 'asc')
->select();
$list = [];
foreach ($rows as $item) {
$attachments = $item->attachments;
if (is_string($attachments)) {
$attachments = json_decode($attachments, true) ?: [];
}
if (!is_array($attachments)) {
$attachments = [];
}
// 刷新后恢复未完成的 Comfy 生图任务
$stillPending = false;
$pendingJob = ComfyUIService::findPendingJob($attachments);
if ($pendingJob && $item->role === 'assistant') {
$modelId = (int) ($pendingJob['model_id'] ?? 0);
$model = $modelId > 0 ? AiModel::find($modelId) : null;
if ($model) {
$recovered = ComfyUIService::recoverPendingMessage([
'id' => (int) $item->id,
'content' => (string) $item->content,
'attachments' => $attachments,
], $model, (int) $user['id']);
$item->content = $recovered['content'];
$item->attachments = $recovered['attachments'];
$attachments = $recovered['attachments'];
$stillPending = empty($recovered['finished']);
Message::where('id', $item->id)->update([
'content' => $recovered['content'],
'attachments' => json_encode($recovered['attachments'], JSON_UNESCAPED_UNICODE),
]);
} else {
$stillPending = true;
}
}
if ($item->role === 'assistant' && trim((string) $item->content) === '') {
if (empty($attachments) || !ComfyUIService::hasImageAttachments($attachments)) {
continue;
}
}
// 前端只展示真实媒体附件;pending 任务信息已体现在 content 文案中
$visibleAttachments = array_values(array_filter(
$attachments,
fn ($a) => is_array($a) && ($a['type'] ?? '') !== ComfyUIService::JOB_TYPE
));
$list[] = [
'id' => $item->id,
'role' => $item->role,
'content' => $item->content,
'content_type' => $item->content_type,
'attachments' => $visibleAttachments,
'pending_job' => $stillPending,
'pending_image_count' => $stillPending ? (int) ($pendingJob['image_count'] ?? 0) : 0,
'created_at' => $item->created_at,
];
}
return $this->success($list);
}
private function findConversation(int $id, int $userId): array
{
$conversation = ConversationModel::alias('c')
->leftJoin('ai_models m', 'c.model_id = m.id')
->where('c.id', $id)
->where('c.user_id', $userId)
->whereNull('c.deleted_at')
->field('c.*,m.name as model_name,m.provider as model_provider')
->find();
if (!$conversation) {
throw new HttpResponseException(json([
'code' => 1,
'message' => '会话不存在',
'data' => null,
], 404));
}
return $conversation->toArray();
}
}