success('中医诊单控制器可以访问', [ 'controller' => 'TcmDiagnosisController', 'namespace' => __NAMESPACE__, 'class' => __CLASS__, 'method' => __METHOD__ ]); } /** * @notes 诊单列表 * @return \think\response\Json */ public function lists() { return $this->dataLists(new DiagnosisLists()); } /** * @notes 医助理诊单统计(按部门、按人) * @return \think\response\Json */ public function assistantDiagnosisStats() { $params = $this->request->get(); $result = DiagnosisLogic::assistantDiagnosisStats($params, (int) $this->adminId, $this->adminInfo); return $this->data($result); } /** * @notes 添加诊单 * @return \think\response\Json */ public function add() { $params = (new DiagnosisValidate())->post()->goCheck('add'); // 诊单创建人(需表 zyt_tcm_diagnosis 已增加 admin_id 列,见 sql 脚本) $params['admin_id'] = (int) $this->adminId; $result = DiagnosisLogic::add($params); if ($result) { return $this->success('添加成功', ['id' => $result], 1, 1); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 编辑诊单 * @return \think\response\Json */ public function edit() { $params = (new DiagnosisValidate())->post()->goCheck('edit'); $result = DiagnosisLogic::edit($params, $this->adminInfo); if ($result) { return $this->success('编辑成功', [], 1, 1); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 设置复诊接诊率统计起始偏移(业务订单 tab) */ public function setRevisitSlotStartOffset() { $params = (new DiagnosisValidate())->post()->goCheck('setRevisitSlotStartOffset'); $ok = DiagnosisLogic::setRevisitSlotStartOffset( (int) $params['id'], (int) $params['revisit_slot_start_offset'], $this->adminInfo ); if (!$ok) { return $this->fail(DiagnosisLogic::getError()); } return $this->success('保存成功'); } /** * @notes 删除诊单 * @return \think\response\Json */ public function delete() { $params = (new DiagnosisValidate())->post()->goCheck('id'); $result = DiagnosisLogic::delete($params); if ($result) { return $this->success('删除成功', [], 1, 1); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 诊单详情 * @return \think\response\Json */ public function detail() { $params = (new DiagnosisValidate())->goCheck('id'); $result = DiagnosisLogic::detail($params, $this->adminInfo); DiagnosisLogic::markAssignRead((int) ($params['id'] ?? 0), $this->adminId); return $this->data($result); } /** * @notes 二诊只读病例详情(患者信息 + 病例 + 跟踪记录) * * 路由:GET /tcm.diagnosis/readonlyDetail?id=:diagnosisId * 权限:tcm.diagnosis/readonlyDetail * * @return \think\response\Json */ public function readonlyDetail() { $params = (new DiagnosisValidate())->goCheck('readonlyDetail'); $result = DiagnosisLogic::readonlyDetail($params, $this->adminId, $this->adminInfo); if (empty($result)) { return $this->fail(DiagnosisLogic::getError() ?: '诊单不存在或无权访问'); } DiagnosisLogic::markAssignRead((int) ($params['id'] ?? 0), $this->adminId); return $this->data($result); } /** * @notes 跟踪信息(血糖血压 / 饮食 / 运动)—— 按日期区间 lazy load * * 路由:GET /tcm.diagnosis/trackingWindow?id=:diagnosisId&start_date=&end_date= * 权限:tcm.diagnosis/readonlyDetail(与只读病例页同档;接诊台亦复用) * * @return \think\response\Json */ public function trackingWindow() { $params = (new DiagnosisValidate())->goCheck('trackingWindow'); if (!DiagnosisLogic::canViewReadonlyDiagnosis((int) $params['id'], $this->adminId, $this->adminInfo)) { return $this->fail(DiagnosisLogic::getError() ?: '诊单不存在或无权访问'); } $result = DiagnosisLogic::fetchTrackingWindow( (int) $params['id'], (string) ($params['start_date'] ?? ''), (string) ($params['end_date'] ?? '') ); return $this->data($result); } /** * @notes 新增跟踪备注(按天合并追加,仅文字) * * 路由:POST /tcm.diagnosis/addTrackingNote * 权限:tcm.diagnosis/addTrackingNote * * @return \think\response\Json */ public function addTrackingNote() { $params = (new DiagnosisValidate())->post()->goCheck('addTrackingNote'); $ok = TrackingNoteLogic::addOrAppend([ 'diagnosis_id' => (int) $params['diagnosis_id'], 'admin_id' => (int) $this->adminId, 'content' => (string) $params['tracking_content'], ]); if ($ok === false) { return $this->fail(TrackingNoteLogic::getError() ?: '保存失败'); } return $this->success('保存成功'); } /** * @notes 拉取跟踪备注列表(note_date DESC) * * 路由:GET /tcm.diagnosis/trackingNotes?diagnosis_id=:diagnosisId * 权限:tcm.diagnosis/trackingNotes * * @return \think\response\Json */ public function trackingNotes() { $params = (new DiagnosisValidate())->goCheck('trackingNotes'); return $this->data(TrackingNoteLogic::getByDiagnosis((int) $params['diagnosis_id'])); } /** * @notes 诊单挂号 / 取消挂号 操作日志(谁在何时操作) */ public function guahaoLogList() { $params = (new DiagnosisValidate())->get()->goCheck('guahaoLogList'); return $this->data(DiagnosisLogic::guahaoLogList((int) $params['id'])); } /** * @notes 诊单详情(患者端) * @return \think\response\Json */ public function diagnosisDetail() { $params = $this->request->get(); if (empty($params['id'])) { return $this->fail('诊单ID不能为空'); } if (empty($params['user_id'])) { return $this->fail('用户ID不能为空'); } $result = DiagnosisLogic::diagnosisDetail($params); if ($result) { return $this->data($result); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 检查手机号是否重复 * @return \think\response\Json */ public function checkPhone() { $params = $this->request->post(); $result = DiagnosisLogic::checkPhone($params); return $this->data($result); } /** * @notes 检查身份证号是否重复 * @return \think\response\Json */ public function checkIdCard() { $params = $this->request->post(); $result = DiagnosisLogic::checkIdCard($params); return $this->data($result); } /** * @notes 补全身份证号(自动计算年龄并更新) * @return \think\response\Json */ public function fillIdCard() { $params = (new DiagnosisValidate())->post()->goCheck('fillIdCard'); $result = DiagnosisLogic::fillIdCard($params); if ($result) { return $this->success('补全成功,年龄已自动更新', [], 1, 1); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 指派医助 * @return \think\response\Json */ public function assign() { $params = $this->request->post(); // 验证参数 if (empty($params['id'])) { return $this->fail('诊单ID不能为空'); } if (!array_key_exists('assistant_id', $params)) { return $this->fail('请选择医助或取消指派'); } $result = DiagnosisLogic::assign($params); if ($result) { $msg = (int) ($params['assistant_id'] ?? -1) === 0 ? '已取消指派' : '指派成功'; return $this->success($msg, [], 1, 1); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 指派医助操作记录 * @return \think\response\Json */ public function assignLogList() { $params = (new DiagnosisValidate())->goCheck('id'); $result = DiagnosisLogic::assignLogList((int) $params['id']); return $this->data($result); } /** * @notes 获取通话签名 * @return \think\response\Json */ public function getCallSignature() { $params = (new DiagnosisValidate())->post()->goCheck('callIdentity'); $params['admin_id'] = (int) $this->adminId; $result = DiagnosisLogic::getCallSignature($params, $this->adminInfo); if ($result) { return $this->data($result); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 发起通话 * @return \think\response\Json */ public function startCall() { $params = $this->request->post(); if (empty($params['diagnosis_id'])) { return $this->fail('诊单ID不能为空'); } // 传递当前管理员ID $params['admin_id'] = $this->adminId; $result = DiagnosisLogic::startCall($params, $this->adminInfo); if ($result !== false) { return $this->data($result); } return $this->fail(DiagnosisLogic::getError()); } /** @notes 为当前医生的指定通话记录启动实时录音转写 */ public function startCallTranscription() { $params = $this->request->post(); $params['admin_id'] = (int)$this->adminId; $result = DiagnosisLogic::startCallTranscription($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** @notes 幂等写入当前通话的已完成转写分段 */ public function upsertCallTranscriptSegments() { $params = $this->request->post(); $params['admin_id'] = (int)$this->adminId; $result = DiagnosisLogic::upsertCallTranscriptSegments($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** @notes 完成当前通话转写并固化对话文字 */ public function finishCallTranscription() { $params = $this->request->post(); $params['admin_id'] = (int)$this->adminId; $result = DiagnosisLogic::finishCallTranscription($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 结束通话 * @return \think\response\Json */ public function endCall() { $params = $this->request->post(); if (empty($params['diagnosis_id'])) { return $this->fail('诊单ID不能为空'); } // 传递当前管理员ID $params['admin_id'] = $this->adminId; $result = DiagnosisLogic::endCall($params); if ($result) { return $this->success('', [], 1, 1); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 获取通话记录 * @return \think\response\Json */ public function getCallRecords() { $params = $this->request->get(); if (empty($params['diagnosis_id'])) { return $this->fail('诊单ID不能为空'); } $result = DiagnosisLogic::getCallRecords($params); return $this->data($result); } /** * @notes 获取诊单关联的腾讯云 IM 单聊记录(admin_getroammsg) */ public function getImChatMessages() { // 增加执行时间限制到 120 秒 set_time_limit(120); $diagnosisId = (int)$this->request->get('diagnosis_id', 0); if ($diagnosisId <= 0) { return $this->fail('诊单ID不能为空'); } if (!DiagnosisLogic::canViewReadonlyDiagnosis( $diagnosisId, (int) $this->adminId, $this->adminInfo )) { return $this->fail(DiagnosisLogic::getError() ?: '诊单不存在或无权访问'); } $onlyArchived = (int)$this->request->get('only_archived', 0) === 1; $result = DiagnosisLogic::getImChatMessagesForDiagnosis($diagnosisId, $onlyArchived); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 触发后台异步同步当前诊单的腾讯云 IM 聊天记录到本地归档表 * 请求即返回,真正的同步逻辑在 fastcgi_finish_request 之后执行 */ public function triggerImChatSync() { $diagnosisId = (int)$this->request->post('diagnosis_id', 0); if ($diagnosisId <= 0) { $diagnosisId = (int)$this->request->get('diagnosis_id', 0); } if ($diagnosisId <= 0) { return $this->fail('诊单ID不能为空'); } if (!DiagnosisLogic::canViewReadonlyDiagnosis( $diagnosisId, (int) $this->adminId, $this->adminInfo )) { return $this->fail(DiagnosisLogic::getError() ?: '诊单不存在或无权访问'); } register_shutdown_function(function () use ($diagnosisId) { try { @set_time_limit(300); ignore_user_abort(true); DiagnosisLogic::syncImChatArchiveForDiagnosis($diagnosisId); } catch (\Throwable $e) { \think\facade\Log::warning('triggerImChatSync failed', [ 'diagnosis_id' => $diagnosisId, 'err' => $e->getMessage(), ]); } }); return $this->success('已发起后台同步,几秒后请重新加载查看', ['queued' => true]); } /** * @notes 绑定 TRTC 房间号到当前诊单通话记录(便于云端录制回调关联) * @return \think\response\Json */ public function bindCallRoom() { $params = $this->request->post(); if (empty($params['diagnosis_id'])) { return $this->fail('诊单ID不能为空'); } if (empty($params['room_id'])) { return $this->fail('房间号不能为空'); } $params['admin_id'] = (int)$this->adminId; $result = DiagnosisLogic::bindCallRoom($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->success('', is_array($result) ? $result : [], 1, 0); } /** * @notes 接通后发起腾讯云云端混流录制(需配置 CAM 与云点播) * @return \think\response\Json */ public function startCloudRecording() { $params = $this->request->post(); if (empty($params['diagnosis_id'])) { return $this->fail('诊单ID不能为空'); } $params['admin_id'] = (int)$this->adminId; $result = DiagnosisLogic::startCloudRecording($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 分片上传本机通话录音或手动视频回放 * @return \think\response\Json */ public function uploadCallRecording() { $params = $this->request->post(); if (empty($params['diagnosis_id'])) { return $this->fail('诊单ID不能为空'); } $params['admin_id'] = (int)$this->adminId; $result = DiagnosisLogic::uploadCallRecording($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 浏览器本地上传通话录制后,关联到通话记录(合并 recording_urls) * @return \think\response\Json */ public function attachLocalCallRecording() { $params = $this->request->post(); if (empty($params['diagnosis_id'])) { return $this->fail('诊单ID不能为空'); } if (empty($params['file_url'])) { return $this->fail('文件地址不能为空'); } $params['admin_id'] = (int)$this->adminId; $result = DiagnosisLogic::attachLocalCallRecording($params); if ($result) { return $this->success('已关联录制', [], 1, 1); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 手动上传视频前,先创建一条模拟通话记录 * @return \think\response\Json */ public function createManualCallRecord() { $params = (new DiagnosisValidate())->post()->goCheck('trackingNotes'); $result = DiagnosisLogic::createManualCallRecord([ 'diagnosis_id' => (int)$params['diagnosis_id'], 'admin_id' => (int)$this->adminId, ]); if (!$result) { return $this->fail(DiagnosisLogic::getError()); } return $this->success('创建成功', $result); } /** * @notes 医助旁观诊单当前视频通话(TRTC 进房参数,仅拉流) * @return \think\response\Json */ public function watchCall() { $diagnosisId = (int)$this->request->get('diagnosis_id', 0); if ($diagnosisId <= 0) { return $this->fail('诊单ID不能为空'); } $result = DiagnosisLogic::getAssistantWatchRoomParams([ 'diagnosis_id' => $diagnosisId, 'admin_id' => (int)$this->adminId, ]); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 获取患者通话签名(用于测试) * @return \think\response\Json */ public function getDoctorSignature() { $params = $this->request->get(); if (empty($params['patient_id'])) { return $this->fail('医助理ID不能为空'); } $result = DiagnosisLogic::getDoctorSignature((int)$params['patient_id']); if ($result) { return $this->data($result); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 获取患者通话签名(用于测试) * @return \think\response\Json */ public function getPatientSignature() { $params = $this->request->get(); if (empty($params['patient_id'])) { return $this->fail('患者ID不能为空'); } $result = DiagnosisLogic::getPatientSignature((int)$params['patient_id']); if ($result) { return $this->data($result); } return $this->fail(DiagnosisLogic::getError()); } /** * @notes 获取医助列表 * @return \think\response\Json */ public function getAssistants() { $result = DiagnosisLogic::getAssistants((int) $this->adminId, $this->adminInfo); return $this->data($result); } /** * @notes 获取医生列表 * @return \think\response\Json */ public function getDoctors() { $result = DiagnosisLogic::getDoctors(); return $this->data($result); } /** * @notes 生成小程序码 * @return \think\response\Json */ public function generateMiniProgramQrcode() { $params = (new DiagnosisValidate())->post()->goCheck('generateQrcode'); $result = DiagnosisLogic::generateMiniProgramQrcode($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 生成订单小程序码 * @return \think\response\Json */ public function generateOrderQrcode() { $params = $this->request->post(); $params['share_user_id'] = $this->adminId; $result = DiagnosisLogic::generateOrderQrcode($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } $orderNo = (string)($params['order_no'] ?? ''); if ($orderNo !== '') { $po = Order::where('order_no', $orderNo)->find(); if ($po) { OrderActionLogLogic::record( (int)$po->id, (int)$this->adminId, $this->adminInfo, 'wx_qrcode', '生成订单小程序码' ); } } return $this->data($result); } /** * @notes 获取企业微信聊天记录 */ public function getWechatChatRecords() { $diagnosisId = $this->request->get('diagnosis_id', 0); $patientId = $this->request->get('patient_id', 0); $pageNo = $this->request->get('page_no', 1); $pageSize = $this->request->get('page_size', 20); if (empty($diagnosisId) && empty($patientId)) { return $this->fail('诊单ID或患者ID不能都为空'); } $query = WechatChatRecord::order('chat_time', 'desc'); if ($diagnosisId) { $query->where('diagnosis_id', $diagnosisId); } if ($patientId) { $query->where('patient_id', $patientId); } $count = (clone $query)->count(); $lists = $query->page($pageNo, $pageSize)->select()->toArray(); return $this->data([ 'lists' => $lists, 'count' => $count, 'page_no' => $pageNo, 'page_size' => $pageSize ]); } /** * @notes 添加企业微信聊天记录(手动录入/同步写入) */ public function addWechatChatRecord() { $params = $this->request->post(); if (empty($params['diagnosis_id']) && empty($params['patient_id'])) { return $this->fail('诊单ID或患者ID不能都为空'); } if (empty($params['content'])) { return $this->fail('内容不能为空'); } $adminInfo = $this->adminInfo; $data = [ 'diagnosis_id' => $params['diagnosis_id'] ?? 0, 'patient_id' => $params['patient_id'] ?? 0, 'staff_userid' => $adminInfo['work_wechat_userid'] ?? ($params['staff_userid'] ?? ''), 'staff_name' => $params['staff_name'] ?? ($adminInfo['name'] ?? ''), 'external_userid' => $params['external_userid'] ?? '', 'external_name' => $params['external_name'] ?? '', 'msg_type' => $params['msg_type'] ?? 'note', 'content' => $params['content'], 'media_url' => $params['media_url'] ?? '', 'chat_time' => $params['chat_time'] ?? time(), 'direction' => $params['direction'] ?? 0, 'create_time' => time(), ]; $record = WechatChatRecord::create($data); return $this->success('添加成功', ['id' => $record->id]); } /** * @notes 删除企业微信聊天记录 */ public function deleteWechatChatRecord() { $id = $this->request->post('id', 0); if (empty($id)) { return $this->fail('记录ID不能为空'); } WechatChatRecord::destroy($id); return $this->success('删除成功'); } /** * @notes 同步企业微信外部联系人信息(查找该患者在企业微信中的跟进人) */ public function getWechatExternalContact() { $patientId = $this->request->get('patient_id', 0); if (empty($patientId)) { return $this->fail('患者ID不能为空'); } $result = DiagnosisLogic::getWechatExternalContact($patientId); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 获取会话内容存档开启的成员列表 */ public function getMsgAuditPermitUsers() { $result = DiagnosisLogic::getMsgAuditPermitUsers(); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 搜索患者(用于创建订单等场景) * @return \think\response\Json */ public function searchPatient() { $keyword = $this->request->get('keyword', ''); $page_no = $this->request->get('page_no', 1); $page_size = $this->request->get('page_size', 10); if (empty($keyword)) { return $this->success('', ['lists' => [], 'count' => 0]); } $offset = ($page_no - 1) * $page_size; $lists = \app\common\model\tcm\Diagnosis::where('patient_name|phone|id_card', 'like', '%' . $keyword . '%') ->field(['id', 'patient_name', 'phone', 'id_card', 'gender', 'age']) ->limit($offset, $page_size) ->order('id desc') ->select() ->toArray(); $count = \app\common\model\tcm\Diagnosis::where('patient_name|phone|id_card', 'like', '%' . $keyword . '%') ->count(); return $this->success('', [ 'lists' => $lists, 'count' => $count, 'page_no' => $page_no, 'page_size' => $page_size ]); } /** * @notes AI 助手患者诊单选择,仅返回当前数据域内的脱敏最小 DTO */ public function aiPatientOptions() { $result = DiagnosisAiLogic::patientOptions( $this->request->get(), (int) $this->adminId, $this->adminInfo ); if ($result === null) { return $this->fail(DiagnosisAiLogic::getError()); } return $this->data($result); } /** * @notes 读取已保存的双模型诊单 AI 报告,不触发上游调用 */ public function aiReports() { $params = (new DiagnosisValidate())->get()->goCheck('aiReports'); $reports = DiagnosisAiLogic::getSavedReports( (int) $params['id'], $this->adminId, $this->adminInfo ); if ($reports === null) { return $this->fail(DiagnosisAiLogic::getError()); } return $this->data($reports); } /** * @notes 基于当前授权诊单向 AI 助手提问,不接收客户端上游配置 */ public function aiAssistant() { $params = (new DiagnosisValidate())->post()->goCheck('aiAssistant'); $result = DiagnosisAiLogic::assistant( (int) $params['id'], (string) $params['task'], (string) ($params['prompt'] ?? ''), $this->adminId, $this->adminInfo ); if ($result === null) { return $this->fail(DiagnosisAiLogic::getError()); } return $this->data($result); } /** * @notes 基于当前授权诊单向 AI 助手提问(SSE 真流式) * * 路由:POST tcm.diagnosis/aiAssistantStream body: id, task, prompt */ public function aiAssistantStream() { // 登录由全局中间件完成;请求校验、旧助手权限与 DataScope 必须全部 // 在任何 SSE header / start 事件之前完成,失败时仍返回标准 JSON。 $params = (new DiagnosisValidate())->post()->goCheck('aiAssistant'); $prepared = DiagnosisAiLogic::prepareAssistant( (int) $params['id'], (string) $params['task'], (string) ($params['prompt'] ?? ''), $this->adminId, $this->adminInfo ); if ($prepared === null) { return $this->fail(DiagnosisAiLogic::getError()); } $this->runAssistantSse($prepared); } /** @param array $prepared */ private function runAssistantSse(array $prepared): void { while (ob_get_level() > 0) { ob_end_clean(); } @ini_set('output_buffering', 'off'); @ini_set('zlib.output_compression', '0'); ignore_user_abort(true); if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); } header('Content-Type: text/event-stream; charset=utf-8'); header('Cache-Control: no-cache, no-transform'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); header('Content-Encoding: none'); echo ':' . str_repeat(' ', 2048) . "\n\n"; $this->flushSseOutput(); $protocol = new AssistantSseProtocol(); $emit = function (string $event, array $payload) use ($protocol): bool { if ($protocol->isTerminal() || connection_aborted()) { return false; } $encoded = $protocol->encode($event, $payload); if ($encoded === null) { return false; } echo $encoded; $this->flushSseOutput(); return !connection_aborted(); }; $emit('start', [ 'task' => (string) ($prepared['task'] ?? ''), 'model_key' => (string) ($prepared['profile'] ?? ''), 'diagnosis_id' => (int) ($prepared['diagnosis_id'] ?? 0), 'context_scope' => (string) ($prepared['context_scope'] ?? ''), 'context_version' => (string) ($prepared['context_version'] ?? ''), 'source_summary' => is_array($prepared['source_summary'] ?? null) ? $prepared['source_summary'] : [], 'message' => '已连接,正在生成…', ]); try { $result = DiagnosisAiLogic::streamPreparedAssistant( $prepared, static fn (string $delta): bool => $emit('delta', ['text' => $delta]), static fn (): bool => connection_aborted() === 1 ); if (connection_aborted()) { exit; } if ($result === null) { $emit('error', [ 'code' => DiagnosisAiLogic::getAssistantErrorCode(), 'message' => DiagnosisAiLogic::getError(), ]); } else { $emit('done', $result); } } catch (\Throwable $e) { $emit('error', [ 'code' => 'AI_ASSISTANT_FAILED', 'message' => 'AI 助手暂时不可用,请稍后重试', ]); } exit; } private function flushSseOutput(): void { if (function_exists('ob_flush')) { @ob_flush(); } flush(); } /** * @notes 对当前授权诊单生成一次结构化 AI 智能分析,仅接受 qwen/openai 模型键 */ public function aiAnalysis() { $params = (new DiagnosisValidate())->post()->goCheck('aiAnalysis'); $result = DiagnosisAiLogic::analysis( (int) $params['id'], $this->adminId, $this->adminInfo, (string) ($params['model'] ?? 'qwen') ); if ($result === null) { return $this->fail(DiagnosisAiLogic::getError()); } return $this->data($result); } /** * @notes 查询患者级 AI 诊断报告全部历史及各模型最新版本,不触发模型调用 */ public function patientAiReports() { $params = (new DiagnosisValidate())->get()->goCheck('patientAiReports'); $result = PatientAiReportLogic::reports( (int) $params['patient_id'], $this->adminId, $this->adminInfo ); if ($result === null) { return $this->fail(PatientAiReportLogic::getError()); } return $this->data($result); } /** * @notes 聚合当前数据域内患者纵向资料,调用指定固定模型并新增一份不可变报告快照 */ public function generatePatientAiReport() { $params = (new DiagnosisValidate())->post()->goCheck('generatePatientAiReport'); $result = PatientAiReportLogic::generate( (int) $params['patient_id'], (string) $params['model'], $this->adminId, $this->adminInfo ); if ($result === null) { return $this->fail(PatientAiReportLogic::getError()); } return $this->data($result); } /** * @notes 整份重新生成两个固定模型的诊单 AI 报告并保存成功项 */ public function generateAiReports() { $params = (new DiagnosisValidate())->post()->goCheck('generateAiReports'); $reports = DiagnosisAiLogic::generateAll( (int) $params['id'], $this->adminId, $this->adminInfo ); if ($reports === null) { return $this->fail(DiagnosisAiLogic::getError()); } return $this->data($reports); } /** * @notes 编辑一份已保存的诊单 AI 报告 */ public function editAiReport() { $params = (new DiagnosisValidate())->post()->goCheck('editAiReport'); $report = DiagnosisAiLogic::editReport( (int) $params['id'], (int) $params['report_id'], $params['content'] ?? null, $this->adminId, $this->adminInfo ); if ($report === null) { return $this->fail(DiagnosisAiLogic::getError()); } return $this->data($report); } }