更新
This commit is contained in:
@@ -24,6 +24,8 @@ use app\adminapi\service\TcmClinicalEvidenceIndex;
|
||||
use app\adminapi\service\TcmClinicalEvidenceContract;
|
||||
use app\adminapi\service\TcmClinicalReportStream;
|
||||
use app\adminapi\service\TcmLongReportContract;
|
||||
use app\adminapi\service\LiveConsultationContext;
|
||||
use app\adminapi\service\LiveConsultationResult;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
@@ -96,6 +98,12 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
|
||||
/** @var array<string,array{label:string,profile:string,instruction:string}> */
|
||||
private const ASSISTANT_TASKS = [
|
||||
'live_consultation' => [
|
||||
'label' => '本次通话辅助', 'profile' => 'qwen', 'instruction' => '依据已保存的本次通话分段整理原话与待核实问题。',
|
||||
],
|
||||
'live_review' => [
|
||||
'label' => '本次通话复盘', 'profile' => 'qwen', 'instruction' => '依据已结束转写的本次通话分段提供可核查的问诊复盘。',
|
||||
],
|
||||
'case_analysis' => [
|
||||
'label' => '分析病历',
|
||||
'profile' => 'qwen',
|
||||
@@ -340,6 +348,12 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
int $adminId,
|
||||
array $adminInfo
|
||||
): ?array {
|
||||
if (LiveConsultationContext::isTask(strtolower(trim($task)))) {
|
||||
self::$assistantErrorCode = 'LIVE_STREAM_REQUIRED';
|
||||
self::$assistantErrorMeta = [];
|
||||
self::setError('当前通话分析须通过带通话身份的流式接口发起');
|
||||
return null;
|
||||
}
|
||||
$prepared = self::prepareAssistant($diagnosisId, $task, $prompt, $adminId, $adminInfo);
|
||||
if ($prepared === null) {
|
||||
return null;
|
||||
@@ -434,12 +448,27 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
$adminId,
|
||||
$adminInfo,
|
||||
self::PERMISSION_ASSISTANT,
|
||||
'权限不足,无法使用诊单 AI 助手'
|
||||
'权限不足,无法使用诊单 AI 助手',
|
||||
$task === 'live_consultation'
|
||||
);
|
||||
if ($diagnosis === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// These checks precede on_authorized/SSE, request ownership and all model calls.
|
||||
$liveContext = null;
|
||||
if (LiveConsultationContext::isTask($task)) {
|
||||
try {
|
||||
$liveContext = LiveConsultationContext::load($diagnosisId, $adminId,
|
||||
$options['call_record_id'] ?? null, $options['transcription_session_id'] ?? null, $task);
|
||||
} catch (\DomainException $e) {
|
||||
self::$assistantErrorCode = 'LIVE_CONTEXT_UNAVAILABLE';
|
||||
self::$assistantErrorMeta = ['live_error_category' => 'context', 'live_partial_available' => false];
|
||||
self::setError($e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$prompt = self::cleanText($prompt, self::MAX_ASSISTANT_PROMPT_LENGTH, true);
|
||||
// Released desktop shortcuts inferred TCM from the incidental word
|
||||
// 证候. Keep that exact summary question usable before client reload.
|
||||
@@ -464,17 +493,32 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
$requestBudgetMs = max(1, min(90000, (int) ((config('prescription_ai') ?: [])['timeout'] ?? 90) * 1000,
|
||||
(int) ($options['timeout'] ?? 90) * 1000, (int) ($options['timeout_ms'] ?? 90000)));
|
||||
$options['request_deadline_ns'] = min((int) ($options['request_deadline_ns'] ?? PHP_INT_MAX),
|
||||
hrtime(true) + $requestBudgetMs * 1000000);
|
||||
($liveContext !== null ? $prepareStartedAt : hrtime(true)) + $requestBudgetMs * 1000000);
|
||||
if ($liveContext !== null && !self::assistantWithinDeadline($options)) return null;
|
||||
// This is the only early-start boundary: all identity, scope and task
|
||||
// checks have succeeded, while no aggregation or model work has begun.
|
||||
if (is_callable($options['on_authorized'] ?? null)
|
||||
&& ($options['on_authorized'])(['diagnosis_id' => $diagnosisId, 'task' => $task, 'model_key' => $profile,
|
||||
'required_sections' => []]) === false) {
|
||||
'required_sections' => []] + ($liveContext !== null
|
||||
? ['analysis_scope' => LiveConsultationContext::analysisScope($task)] : [])) === false) {
|
||||
self::setError('该请求标识已使用,请重新发起');
|
||||
return null;
|
||||
}
|
||||
if (self::assistantCancelled($options['should_abort'] ?? null)) return null;
|
||||
if (!self::assistantWithinDeadline($options)) return null;
|
||||
// Video in-call hints intentionally never enter chart aggregation, compaction, OCR
|
||||
// or attachment collection. The authorized immutable call snapshot is the full input.
|
||||
if ($task === 'live_consultation') {
|
||||
$prepared = self::prepareLiveAssistant($liveContext, [], $diagnosisId, $adminId, $task,
|
||||
$profile, $model, $modelLabel, $options);
|
||||
if (self::assistantCancelled($options['should_abort'] ?? null)
|
||||
|| !self::assistantWithinDeadline($options)) return null;
|
||||
$sourceBytes = strlen(LiveConsultationContext::encode($liveContext));
|
||||
$queryBytes = strlen($prepared['query']);
|
||||
$inputs = $prepared['inputs'];
|
||||
$status = 'ok';
|
||||
return $prepared;
|
||||
}
|
||||
self::assistantProgress($options, ['phase' => 'preparing', 'status' => 'aggregating', 'message' => '正在聚合本次授权范围内的患者资料']);
|
||||
$aggregateStartedAt = hrtime(true);
|
||||
$context = self::buildCaseContext($diagnosis, $adminId, $adminInfo);
|
||||
@@ -515,6 +559,15 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
|
||||
// Missing evidence limits individual conclusions, never changes the
|
||||
// requested model task into source selection or a local checklist.
|
||||
if ($liveContext !== null) {
|
||||
$prepared = self::prepareLiveAssistant($liveContext, $context, $diagnosisId, $adminId, $task,
|
||||
$profile, $model, $modelLabel, $options);
|
||||
$sourceBytes += strlen(LiveConsultationContext::encode($liveContext));
|
||||
$queryBytes = strlen($prepared['query']);
|
||||
$inputs = $prepared['inputs'];
|
||||
$status = 'ok';
|
||||
return $prepared;
|
||||
}
|
||||
$query = self::buildAssistantPrompt($context, $task, $prompt);
|
||||
$inputs = self::buildUpstreamInputs(
|
||||
$context,
|
||||
@@ -588,9 +641,16 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
$callerDeadline = (int) ($options['request_deadline_ns'] ?? 0);
|
||||
$options['request_deadline_ns'] = min((int) $prepared['request_deadline_ns'],
|
||||
$callerDeadline > 0 ? $callerDeadline : PHP_INT_MAX);
|
||||
if (!self::assistantWithinDeadline($options)) return null;
|
||||
if (!self::assistantWithinDeadline($options)) {
|
||||
if (LiveConsultationContext::isTask((string) ($prepared['task'] ?? '')))
|
||||
self::$assistantErrorMeta = ['live_error_category' => 'timeout', 'live_partial_available' => false];
|
||||
return null;
|
||||
}
|
||||
$leftMs = max(1, (int) floor(($options['request_deadline_ns'] - hrtime(true)) / 1000000));
|
||||
$options['timeout_ms'] = min((int) ($options['timeout_ms'] ?? $leftMs), $leftMs);
|
||||
}
|
||||
if (LiveConsultationContext::isTask((string) ($prepared['task'] ?? ''))) {
|
||||
return self::streamLiveAssistant($prepared, $shouldAbort, $timing, $options);
|
||||
}
|
||||
if (self::isExplicitLongReport($prepared)) {
|
||||
return self::streamLongReport($prepared, $onDelta, $shouldAbort, $timing, $options);
|
||||
@@ -877,6 +937,252 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Keep the immutable current-call sources separate from authorized case background/compaction. */
|
||||
private static function prepareLiveAssistant(array $snapshot, array $context, int $diagnosisId, int $adminId,
|
||||
string $task, string $profile, string $model, string $modelLabel, array $options): array
|
||||
{
|
||||
$dialogueOnly = $task === 'live_consultation';
|
||||
// Enforce at the builder boundary too: no caller may add chart inputs to this task.
|
||||
if ($dialogueOnly) $context = [];
|
||||
$analysisScope = LiveConsultationContext::analysisScope($task);
|
||||
$contextScope = $dialogueOnly ? 'current_video_call' : 'current_video_call_with_authorized_case';
|
||||
$summary = array_replace($context['source_summary'] ?? [], [
|
||||
'live_segment_count' => $snapshot['segment_count'], 'live_snapshot_complete' => true,
|
||||
'transcription_complete' => $snapshot['transcription_status'] === 'completed',
|
||||
'historical_context_included' => !$dialogueOnly]);
|
||||
$inputs = $dialogueOnly ? [
|
||||
// Keep deployed Dify input names while identifying this as dialogue, not a case report.
|
||||
'prescription_name' => '本次视频通话', 'formula_type' => '本次通话对话辅助',
|
||||
'prompt_version' => LiveConsultationContext::VERSION,
|
||||
'source_counts_json' => LiveConsultationContext::encode($summary),
|
||||
'source_diagnosis_count' => 0, 'attachment_count' => 0,
|
||||
] : self::buildUpstreamInputs($context, '本次通话辅助', LiveConsultationContext::VERSION);
|
||||
// One authoritative full copy lives in query; structured variables carry only a manifest.
|
||||
$inputs['herbs_json'] = LiveConsultationContext::encode([
|
||||
'source_location' => 'query_untrusted_data_blocks', 'call_record_id' => $snapshot['call_record_id'],
|
||||
'revision' => $snapshot['revision'], 'segment_count' => $snapshot['segment_count'],
|
||||
'analysis_scope' => $analysisScope] + ($dialogueOnly ? []
|
||||
: ['case_context_revision' => (string) ($context['fingerprint'] ?? '')]));
|
||||
$knownChart = $dialogueOnly ? []
|
||||
: ['demographics' => self::redactSensitiveIdentifiers((string) ($context['demographics'] ?? ''))];
|
||||
$inputs['analysis_scope'] = $analysisScope;
|
||||
$inputs['context_scope'] = $contextScope;
|
||||
$inputs['context_version'] = LiveConsultationContext::VERSION;
|
||||
self::assistantProgress($options, ['phase' => 'preparing', 'status' => 'ready',
|
||||
'message' => $dialogueOnly ? '本次完整对话已准备,正在整理可引用的草稿'
|
||||
: '本次通话分段及授权病历已准备,正在整理可引用的草稿',
|
||||
'analysis_scope' => $analysisScope,
|
||||
'source_summary' => $summary, 'live_context' => array_diff_key($snapshot, ['sources' => true])]);
|
||||
return ['diagnosis_id' => $diagnosisId, 'admin_id' => $adminId, 'task' => $task,
|
||||
'profile' => $profile, 'model_name' => $model, 'model_label' => $modelLabel,
|
||||
'inputs' => $inputs,
|
||||
'query' => LiveConsultationContext::prompt($snapshot, $task,
|
||||
self::redactSensitiveIdentifiers((string) ($context['case_text'] ?? '')), $knownChart),
|
||||
'known_chart' => $knownChart, 'analysis_scope' => $analysisScope,
|
||||
'live_context' => $snapshot, 'context_scope' => $contextScope,
|
||||
'context_version' => LiveConsultationContext::VERSION,
|
||||
'case_context_revision' => (string) ($context['fingerprint'] ?? ''), 'source_summary' => $summary,
|
||||
'source_diagnosis_ids' => $dialogueOnly ? [] : ($context['source_diagnosis_ids'] ?? [$diagnosisId]),
|
||||
'reference_scope' => 'consultation', 'knowledge_source' => null,
|
||||
'user' => 'admin-diagnosis-assistant-' . $adminId, 'request_deadline_ns' => $options['request_deadline_ns'],
|
||||
'upstream_timeout' => (int) ((config('prescription_ai') ?: [])['timeout'] ?? 90),
|
||||
'files' => $context['files'] ?? [], 'required_sections' => $context['required_sections'] ?? [],
|
||||
'source_form' => $dialogueOnly ? 'full_current_call'
|
||||
: (!empty($context['source_summary']['source_compacted']) ? 'evidence_summary' : 'full_context'),
|
||||
'evidence_cache_hit' => !empty($context['compaction_cache_hit'])];
|
||||
}
|
||||
|
||||
/** A bounded generation; all submission/task/terminal/cancel ownership callbacks pass through. */
|
||||
private static function streamLiveAssistant(array $prepared, ?callable $shouldAbort,
|
||||
?AssistantSseTiming $timing, array $options): ?array
|
||||
{
|
||||
self::$assistantErrorMeta = [];
|
||||
$snapshot = $prepared['live_context'] ?? null;
|
||||
if (!is_array($snapshot) || !LiveConsultationContext::validIdentity(
|
||||
$snapshot['call_record_id'] ?? null, $snapshot['transcription_session_id'] ?? null)
|
||||
|| empty($snapshot['revision']) || empty($snapshot['sources'])
|
||||
|| empty($prepared['inputs']) || empty($prepared['query']) || empty($prepared['user'])
|
||||
|| (int) ($prepared['admin_id'] ?? 0) <= 0 || (int) ($prepared['diagnosis_id'] ?? 0) <= 0) {
|
||||
self::$assistantErrorCode = 'LIVE_CONTEXT_UNAVAILABLE';
|
||||
self::$assistantErrorMeta = ['live_error_category' => 'context', 'live_partial_available' => false];
|
||||
self::setError('当前通话分析上下文无效');
|
||||
return null;
|
||||
}
|
||||
$startedAt = hrtime(true);
|
||||
$status = 'error';
|
||||
$bytes = 0;
|
||||
$oversized = false;
|
||||
$buffer = '';
|
||||
$displayedItems = 0;
|
||||
$updateCount = 0;
|
||||
$maxUpdates = $prepared['task'] === 'live_review' ? 15 : 12;
|
||||
$firstValidUpdateMs = null;
|
||||
$deliveryStopped = false;
|
||||
$budgetMs = max(1, min(90000, (int) ($prepared['upstream_timeout'] ?? 90) * 1000,
|
||||
(int) ($options['timeout'] ?? 90) * 1000, (int) ($options['timeout_ms'] ?? 90000)));
|
||||
$options['request_deadline_ns'] = min((int) ($options['request_deadline_ns'] ?? PHP_INT_MAX),
|
||||
$startedAt + $budgetMs * 1000000);
|
||||
$transportAbort = static function () use (&$oversized, &$deliveryStopped, $shouldAbort): bool {
|
||||
return $oversized || $deliveryStopped || ($shouldAbort !== null && $shouldAbort());
|
||||
};
|
||||
if (is_callable($options['on_cancel_task'] ?? null)) {
|
||||
$cancelTask = $options['on_cancel_task'];
|
||||
$options['on_cancel_task'] = static function (string $id, callable $stop, string $reason = 'cancelled')
|
||||
use ($cancelTask, &$oversized): array {
|
||||
return $cancelTask($id, $stop, $oversized ? 'validation' : $reason);
|
||||
};
|
||||
}
|
||||
if ($timing !== null) {
|
||||
$priorObserver = $options['on_upstream_timing'] ?? null;
|
||||
$options['on_upstream_timing'] = static function (\app\common\service\DifyStreamTiming $stages)
|
||||
use ($timing, $priorObserver): void {
|
||||
$timing->logUpstream($stages);
|
||||
if (is_callable($priorObserver)) $priorObserver($stages);
|
||||
};
|
||||
$priorAttachmentObserver = $options['on_attachment_timing'] ?? null;
|
||||
$options['on_attachment_timing'] = static function (array $metrics) use ($timing, $priorAttachmentObserver): void {
|
||||
$timing->log('attachment', $metrics);
|
||||
if (is_callable($priorAttachmentObserver)) $priorAttachmentObserver($metrics);
|
||||
};
|
||||
}
|
||||
try {
|
||||
if (self::assistantCancelled($shouldAbort) || !self::assistantWithinDeadline($options)
|
||||
|| !self::mayStartUpstreamAttempt($options)) return null;
|
||||
$remainingMs = max(1, (int) floor(($options['request_deadline_ns'] - hrtime(true)) / 1000000));
|
||||
$result = DifyChatService::streamChat($prepared['profile'], $prepared['inputs'], $prepared['query'],
|
||||
$prepared['user'], static function (string $delta) use (&$bytes, &$oversized, &$buffer,
|
||||
&$displayedItems, &$updateCount, &$firstValidUpdateMs, &$deliveryStopped, $startedAt,
|
||||
$shouldAbort, $snapshot, $prepared, $options, $maxUpdates): bool {
|
||||
$bytes += strlen($delta);
|
||||
$oversized = $bytes > LiveConsultationResult::MAX_BYTES;
|
||||
if ($oversized || self::assistantCancelled($shouldAbort) || !self::assistantWithinDeadline($options)) return false;
|
||||
$buffer .= $delta;
|
||||
// Only complete, legally delimited items cross the same validation gate.
|
||||
// The finite displayed-item count bounds frames; closing empty groups adds none.
|
||||
if ($updateCount < $maxUpdates && is_callable($options['on_live_update'] ?? null)
|
||||
&& strpbrk($delta, ',]') !== false) {
|
||||
$partial = LiveConsultationResult::progress($buffer, $snapshot, $prepared['task'], $prepared['known_chart'] ?? []);
|
||||
if ($partial !== null && $partial['displayed_items'] > $displayedItems) {
|
||||
$displayedItems = $partial['displayed_items'];
|
||||
$payload = self::liveAssistantPayload($prepared, $partial['assistance'], true);
|
||||
$payload['live_completed_groups'] = $partial['completed_groups'];
|
||||
if (($options['on_live_update'])($payload) === false) $deliveryStopped = true;
|
||||
else {
|
||||
$updateCount++;
|
||||
if ($firstValidUpdateMs === null) $firstValidUpdateMs = AssistantSseTiming::elapsedMs($startedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Never forward unvalidated JSON fragments to the clinical delta channel.
|
||||
return !$deliveryStopped;
|
||||
}, $transportAbort, $prepared['files'] ?? [], array_replace($options,
|
||||
['timeout' => (int) ceil($remainingMs / 1000), 'timeout_ms' => $remainingMs,
|
||||
'required_files' => $prepared['task'] === 'live_review',
|
||||
'analysis_scope' => LiveConsultationContext::analysisScope($prepared['task']),
|
||||
'required_sections' => $prepared['required_sections'] ?? [],
|
||||
'source_summary' => $prepared['source_summary'] ?? [],
|
||||
'source_form' => $prepared['source_form'] ?? 'full_context',
|
||||
'evidence_cache_hit' => !empty($prepared['evidence_cache_hit']),
|
||||
'reference_scope' => 'consultation', 'knowledge_mode' => 'clinical',
|
||||
'response_contract' => 'live_consultation',
|
||||
'should_abort' => $transportAbort]));
|
||||
if (self::assistantCancelled($deliveryStopped ? static fn (): bool => true : $shouldAbort)) { $status = 'cancelled'; return null; }
|
||||
if (!self::assistantWithinDeadline($options)) return null;
|
||||
if ($oversized) {
|
||||
self::$assistantErrorCode = 'LIVE_RESULT_INVALID';
|
||||
self::$assistantErrorMeta['live_validation_reason'] = 'LENGTH_LIMIT';
|
||||
self::setError('本次通话辅助结果超出校验容量,本次草稿未完成');
|
||||
return null;
|
||||
}
|
||||
if (empty($result['ok'])) {
|
||||
self::$assistantErrorMeta = self::safeAssistantErrorMeta($result);
|
||||
$code = (string) ($result['error_code'] ?? '');
|
||||
self::$assistantErrorCode = in_array($code, ['UPSTREAM_TIMEOUT', 'INCOMPLETE_RESPONSE', 'EMPTY_RESPONSE',
|
||||
'AI_ASSISTANT_CANCELLED', 'REQUIRED_FILES_INVALID', 'REQUIRED_FILES_REJECTED', 'REQUIRED_FILES_TOO_LARGE',
|
||||
'REQUIRED_FILES_UNSUPPORTED', 'REQUIRED_FILES_UPLOAD_FAILED'], true) ? $code : 'UPSTREAM_UNAVAILABLE';
|
||||
self::setError(match (self::$assistantErrorCode) {
|
||||
'UPSTREAM_TIMEOUT' => '本次通话辅助响应超时,草稿未完成,请稍后重试',
|
||||
'INCOMPLETE_RESPONSE', 'EMPTY_RESPONSE' => '本次通话辅助响应未完整返回,请重试',
|
||||
'AI_ASSISTANT_CANCELLED' => '本次请求已取消',
|
||||
default => '本次通话辅助服务或资料提交暂时不可用,请稍后重试',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
$validationFailure = null;
|
||||
$assistance = LiveConsultationResult::parse((string) ($result['content'] ?? ''), $snapshot,
|
||||
$prepared['task'], $prepared['known_chart'] ?? [], $validationFailure);
|
||||
if ($assistance === null) {
|
||||
self::$assistantErrorCode = 'LIVE_RESULT_INVALID';
|
||||
self::$assistantErrorMeta['live_validation_reason'] = $validationFailure;
|
||||
self::setError(match ($validationFailure) {
|
||||
'CITATION_INVALID', 'SOURCE_QUOTE_MISMATCH' => '本次结果引用未通过核对,草稿未完成,请重试',
|
||||
'SOURCE_ROLE_INVALID' => '本次结果混入非患者陈述,草稿未完成,请重试',
|
||||
'KNOWN_DEMOGRAPHIC_REASK' => '本次建议重复询问病历已有信息,草稿未完成,请重试',
|
||||
'SAFETY_REJECTED' => '本次建议超出问诊辅助范围,草稿未完成,请重试',
|
||||
default => '本次结果格式不完整,草稿未完成,请重试',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (self::assistantCancelled($shouldAbort) || !self::assistantWithinDeadline($options)) return null;
|
||||
$status = 'ok';
|
||||
$payload = self::liveAssistantPayload($prepared, $assistance, false);
|
||||
if (($result['transfer_manifest_final'] ?? false) === true) {
|
||||
if (is_array($result['source_summary'] ?? null))
|
||||
$payload['source_summary'] = array_replace($payload['source_summary'], $result['source_summary']);
|
||||
foreach (['required_sections', 'items', 'attachment_count', 'submitted_attachment_count', 'skipped_attachment_count',
|
||||
'rejected_attachment_count', 'attachment_mode', 'attachment_batch_count', 'attachment_transfer', 'transfer_manifest_final'] as $field)
|
||||
if (array_key_exists($field, $result)) $payload[$field] = $result[$field];
|
||||
}
|
||||
return $payload;
|
||||
} catch (\Throwable $e) {
|
||||
self::$assistantErrorCode = 'UPSTREAM_UNAVAILABLE';
|
||||
self::setError('本次通话辅助暂时不可用,请稍后重试');
|
||||
return null;
|
||||
} finally {
|
||||
if ($status !== 'ok') {
|
||||
$code = self::getAssistantErrorCode();
|
||||
$reason = self::$assistantErrorMeta['live_validation_reason'] ?? '';
|
||||
self::$assistantErrorMeta['live_error_category'] = match (true) {
|
||||
$code === 'AI_ASSISTANT_CANCELLED' => 'cancelled',
|
||||
$code === 'UPSTREAM_TIMEOUT' => 'timeout',
|
||||
$code === 'LIVE_CONTEXT_UNAVAILABLE' => 'context',
|
||||
in_array($reason, ['CITATION_INVALID', 'SOURCE_QUOTE_MISMATCH', 'SOURCE_ROLE_INVALID'], true) => 'citation',
|
||||
$reason === 'SAFETY_REJECTED' => 'safety',
|
||||
$reason === 'KNOWN_DEMOGRAPHIC_REASK' => 'relevance',
|
||||
$code === 'LIVE_RESULT_INVALID' => 'format',
|
||||
default => 'upstream',
|
||||
};
|
||||
self::$assistantErrorMeta['live_partial_available'] = $updateCount > 0;
|
||||
}
|
||||
if ($timing !== null) $timing->log('upstream_done', [
|
||||
'duration_ms' => AssistantSseTiming::elapsedMs($startedAt), 'delta_count' => 0, 'delta_bytes' => 0,
|
||||
'first_valid_update_ms' => $firstValidUpdateMs, 'live_update_count' => $updateCount,
|
||||
'live_error_category' => self::$assistantErrorMeta['live_error_category'] ?? null,
|
||||
'live_validation_reason' => self::$assistantErrorMeta['live_validation_reason'] ?? null,
|
||||
'retry_attempted' => false, 'status' => $status,
|
||||
'error_code' => $status === 'ok' ? '' : self::getAssistantErrorCode()]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Identical revision/citation binding for progressive drafts and the authoritative final result. */
|
||||
private static function liveAssistantPayload(array $prepared, array $assistance, bool $partial): array
|
||||
{
|
||||
$snapshot = $prepared['live_context'];
|
||||
return ['diagnosis_id' => $prepared['diagnosis_id'], 'task' => $prepared['task'],
|
||||
'model_key' => $prepared['profile'], 'model_label' => $prepared['model_label'],
|
||||
'model_name' => $prepared['model_name'], 'answer_mode' => 'live_assistance',
|
||||
'live_partial' => $partial,
|
||||
'analysis_scope' => LiveConsultationContext::analysisScope($prepared['task']),
|
||||
'answer' => LiveConsultationResult::answer($assistance, $snapshot),
|
||||
'live_assistance' => $assistance, 'live_context' => LiveConsultationResult::citedContext($snapshot, $assistance),
|
||||
'context_scope' => $prepared['context_scope'], 'context_version' => LiveConsultationContext::VERSION,
|
||||
'case_context_revision' => $prepared['case_context_revision'],
|
||||
'source_summary' => $prepared['source_summary'], 'source_diagnosis_ids' => $prepared['source_diagnosis_ids'],
|
||||
'knowledge_source' => null, 'ancient_book_sources' => [],
|
||||
'evidence_limitations' => $snapshot['transcription_status'] === 'completed'
|
||||
? [] : ['仅依据截至当前已保存的完成分段,不能代表完整通话']];
|
||||
}
|
||||
|
||||
/** One correction of an undelivered paragraph, under the original request deadline. */
|
||||
@@ -2476,7 +2782,8 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
int $adminId,
|
||||
array $adminInfo,
|
||||
string $permission,
|
||||
string $permissionError
|
||||
string $permissionError,
|
||||
bool $identityOnly = false
|
||||
): ?array {
|
||||
if ($id <= 0) {
|
||||
self::setError('诊单ID必须大于0');
|
||||
@@ -2492,6 +2799,10 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
return null;
|
||||
}
|
||||
|
||||
// DataScope already verified an active, nondeleted diagnosis. Live hints need only
|
||||
// that identity; detail() would aggregate notes, chart images and demographics.
|
||||
if ($identityOnly) return ['id' => $id];
|
||||
|
||||
$diagnosis = DiagnosisLogic::detail(['id' => $id], $adminInfo);
|
||||
if ($diagnosis === [] || empty($diagnosis['id'])) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
Reference in New Issue
Block a user