Files
zyt/server/app/adminapi/service/AssistantClinicalRejectDiagnostic.php
T
2026-10-27 14:03:35 +08:00

79 lines
4.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\adminapi\service;
use think\facade\Log;
/** Request-local lexical diagnostics; never a clinical validation decision. */
final class AssistantClinicalRejectDiagnostic
{
private const FORMS = ['pulse_row', 'missing_pulse_statement', 'workflow_rule_narration', 'pulse_feature', 'other'];
private const MAX_PROBE_CHARS = 100000;
private const FEATURES = '无力|有力|浮|沉|迟|数|虚|实|滑|涩|弦|细|洪|弱|缓|紧|濡|微|促|结|代|正常|无异常';
private const PULSE = '脉(?:象|诊)?(?!率|压|搏|络|管)|舌脉|\bpulse\b(?![\s_-]*(?:rate|pressure)\b)';
/** @return list<string> Fixed lexical forms only; unrecognized pulse wording stays other. */
public static function forms(string $unit): array
{
if (!mb_check_encoding($unit, 'UTF-8')) return ['other'];
$found = [];
$probe = mb_substr($unit, 0, self::MAX_PROBE_CHARS, 'UTF-8');
// Only a complete, narrow workflow-rule clause is masked here. A nearby
// request to collect pulse remains visible to the other form checks.
$probe = preg_replace_callback('/[^,,。;;!!??\r\n]+/u', static function (array $match) use (&$found): string {
$clause = trim($match[0], " \t|*");
$workflow = '(?:本流程|当前流程|本工作流|当前工作流|本次问诊|此次问诊|本次评估|当前评估|问诊流程|咨询流程|线上问诊|远程问诊)';
$noun = '(?:脉象|脉诊|切脉|把脉|诊脉|舌脉)';
if (preg_match('/^(?:' . $workflow . '(?:中|下)?[::\s]*(?:不采集|不使用|不依据|不纳入|不进行|禁止采集|无需采集|无需补采|无需提供)'
. $noun . '(?:信息|资料|评估|辨证)?|'
. $noun . '(?:不纳入|不用于|不作为)' . $workflow . '(?:的)?(?:评估|依据|辨证)?)$/uD', $clause) === 1) {
$found['workflow_rule_narration'] = true;
return ' ';
}
return $match[0];
}, $probe) ?? $probe;
$row = '/^[ \t]*(?:\|[ \t]*)?(?:(?:[-*+]|\d+[.)、])[ \t]+)?(?:\*\*)?(?:脉象|脉诊|舌脉|脉)(?:\*\*)?[ \t]*(?:[::]|\|)/mu';
if (preg_match($row, $probe) === 1) $found['pulse_row'] = true;
$missing = '/(?:(?:脉象|脉诊|脉(?!率|压|搏|络|管))[\s::|*]*(?:未记录|未采集|缺失|缺少|缺乏|未提供|未见记录|未获取|暂无|不详|待补|资料不足|信息不足)'
. '|(?:缺|缺少|缺乏|未采集|未记录|未提供|未获取)(?:的)?(?:脉象|脉诊|脉(?!率|压|搏|络|管))'
. '|\bpulse\b(?![\s_-]*(?:rate|pressure)\b)[\s:]*(?:(?:is|was)[\s]+)?(?:not[\s]+(?:recorded|collected|provided)|missing|unavailable))/iu';
$feature = '/(?:脉(?:象|诊)?[::\s|*]*(?:(?:呈|为|见|可见|表现为|按之)[::\s]*)?(?:不|未|无)?(?:' . self::FEATURES . ')'
. '|(?:' . self::FEATURES . ')脉(?!络|管|压|率))/u';
foreach (['missing_pulse_statement' => $missing, 'pulse_feature' => $feature] as $form => $pattern) {
if (preg_match($pattern, $probe) !== 1) continue;
$found[$form] = true;
$probe = preg_replace($pattern, ' ', $probe) ?? $probe;
}
// A row label is already represented, but any unclassified pulse wording
// (including active solicitation) gets other, even in a mixed paragraph.
$probe = preg_replace($row, ' ', $probe) ?? $probe;
if ($found === [] || preg_match('/(?:' . self::PULSE . ')/iu', $probe) === 1) $found['other'] = true;
return array_values(array_filter(self::FORMS, static fn (string $form): bool => isset($found[$form])));
}
/** @return array{trace_id:string,pulse_forms:list<string>,clinical_issue_codes:list<string>,unit_chars:int} */
public static function event(AssistantSseTiming $timing, string $unit, array $issues): array
{
$codes = [];
foreach ($issues as $issue) {
if (is_array($issue) && is_string($issue['code'] ?? null)) $codes[] = $issue['code'];
}
// Reuse the existing exact issue allowlist and its sixteen-code bound.
$safe = $timing->event('upstream_done', ['clinical_issue_codes' => $codes]);
return [
'trace_id' => $timing->id(),
'pulse_forms' => self::forms($unit),
'clinical_issue_codes' => $safe['clinical_issue_codes'],
'unit_chars' => mb_strlen($unit, 'UTF-8'),
];
}
public static function log(AssistantSseTiming $timing, string $unit, array $issues): void
{
Log::write('diagnosis ai clinical rejection ' . json_encode(self::event($timing, $unit, $issues)), 'info');
}
}