This commit is contained in:
Your Name
2026-10-27 14:03:35 +08:00
parent 7986e4baf5
commit f9cbd42113
417 changed files with 71876 additions and 5344 deletions
@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
/** Offline public-endpoint integration. No network, database, provider or patient data. */
namespace think\facade {
class Log {
public static function warning(string $message, array $context = []): void {}
public static function write(string $message, string $level = ''): void {}
}
}
namespace app\common\cache {
class AdminAuthCache {
public function __construct(int $id) {}
public function getAdminUri(): array {
return ['tcm.diagnosis/aiassistant', 'tcm.diagnosis/aianalysis', 'tcm.diagnosis/aigenerateprescription'];
}
}
}
namespace app\adminapi\logic\firstvisit {
class MyPatientLogic {
public static function canAccessDiagnosis($id, $adminId, $adminInfo): bool { return true; }
}
}
namespace app\adminapi\logic\tcm {
function config(string $key): array {
return ['timeout' => 90, 'models' => [
'qwen' => ['name' => 'r37-local-qwen', 'label' => 'R37 local Qwen'],
'openai' => ['name' => 'r37-local-openai', 'label' => 'R37 local OpenAI'],
]];
}
class DiagnosisLogic {
public static function detail($params, $adminInfo): array { return $GLOBALS['r37Current']; }
}
class PatientAiReportLogic {
public static function contextForAuthorizedDiagnosis($diagnosis, $adminId, $adminInfo): array {
return $GLOBALS['r37Context'];
}
public static function compactSourceForPrompt(...$args): array {
throw new \RuntimeException('Unexpected compaction; finite source is below the existing budget.');
}
}
}
namespace app\common\service {
class DifyChatService {
public static array $calls = [];
public static array $responses = [];
public static int $totalCalls = 0;
public static bool $streaming = false;
private static function take(): array {
if (self::$responses === []) throw new \RuntimeException('Unexpected extra local transport invocation.');
return array_shift(self::$responses);
}
public static function chat($profile, $inputs, $query, $user, $files = [], $options = []): array {
++self::$totalCalls;
self::$calls[] = compact('profile', 'inputs', 'query', 'user', 'files', 'options') + ['transport' => 'chat'];
return self::take();
}
public static function streamChat($profile, $inputs, $query, $user, $delta, $abort, $files = [], $options = []): array {
++self::$totalCalls;
self::$calls[] = compact('profile', 'inputs', 'query', 'user', 'files', 'options') + ['transport' => 'stream'];
$result = self::take();
if (empty($result['ok'])) return $result;
self::$streaming = true;
try {
foreach (mb_str_split($result['content'], 19, 'UTF-8') as $chunk) {
if (($abort !== null && $abort()) || $delta($chunk) === false) {
return ['ok' => false, 'error_code' => 'AI_ASSISTANT_CANCELLED'];
}
}
} finally { self::$streaming = false; }
return $result;
}
}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\logic\tcm\DiagnosisAiLogic;
use app\adminapi\service\AiRequiredSource;
use app\adminapi\service\TcmEvidenceGuard;
use app\adminapi\service\TcmClinicalFactStateGuard;
use app\common\service\DifyChatService;
$checks = [];
function r37Check(bool $passed, string $name): void { $GLOBALS['checks'][] = compact('passed', 'name'); }
function r37Reset(array $responses): void {
DifyChatService::$calls = [];
DifyChatService::$responses = $responses;
}
function r37Fixture(string $variant): void {
$GLOBALS['r37Current'] = [
'id' => 37, 'patient_id' => 999, 'diagnosis_date' => '2026-10-26', 'age' => 46,
'chief_complaint' => $variant === 'tongue_missing' ? '口干两周。' : '未记录',
'tongue' => $variant === 'symptoms_missing' ? '舌淡红,苔薄白。' : '未记录',
'pulse' => '脉细', 'allergy_history' => '不详',
'past_history' => '2020年外院糖尿病诊断记录。',
'current_medications' => '二甲双胍500mg,每日2次。',
'fasting_blood_sugar' => 7.9, 'systolic_pressure' => 132, 'diastolic_pressure' => 82,
];
$sections = []; $metadata = [];
foreach (AiRequiredSource::LABELS as $key => $label) {
$value = [['marker' => 'R37_FULL_SOURCE_' . $key]];
if ($key === 'diagnosis_info') $value[] = $GLOBALS['r37Current'];
if ($key === 'prescriptions') $value[] = ['prescription_date' => '2020-02-01',
'prescription_name' => '历史用药记录', 'usage' => '格列美脲已停用。'];
if ($key === 'daily_tracking') $value[] = ['record_date' => '2026-10-25',
'fasting_blood_sugar' => 8.3, 'note' => '晚餐主食较多,晚饭后步行20分钟。'];
if ($key === 'chat') $value[] = ['msg_time' => '2020-02-01', 'text' => '历史自述口渴,非本次采集。'];
$sections[] = compact('key', 'label', 'value');
$metadata[] = ['key' => $key, 'label' => $label, 'availability' => 'present', 'count' => count($value), 'status' => 'prepared'];
}
$GLOBALS['r37Context'] = [
'snapshot' => ['required_content' => $sections],
'source_summary' => ['required_sections' => $metadata, 'snapshot_complete' => true],
'source_diagnosis_ids' => [12, 37],
'files' => [['type' => 'image', 'url' => '/offline-r37/attachment.png']],
];
}
// Namespace transport/auth/context helpers reused from the independently saved
// source_ids_repro.php. New content-contract expectations below are root-authored.
$checks = []; $responses = [];
$ids = ['f1', 'f1, f2', "f1\nf2", 'f1、f2;f3', '["f1","f2"]',
"```json\n[\"f1\",\"f2\"]\n```", "```text\nf1, f2\n```", "\xEF\xBB\xBFf1 f2"];
foreach ($ids as $text) {
r37Check(TcmEvidenceGuard::isSourceIdListOnly($text), 'closed ID-only shape recognized: '.json_encode($text));
}
r37Fixture('both_missing');
foreach (['case_analysis','glucose_trend','medication_review','complication_risk','summary','lifestyle_advice'] as $task) {
foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>'f1, f2']]); $visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,$task,'请分析已有资料及其局限。',7,[]);
$result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,$task,'请分析已有资料及其局限。',7,[])
: DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;});
r37Check($result===null && $visible==='' && DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING'
&& count(DifyChatService::$calls)===1, "$task/$mode rejects exact independent counterexample honestly");
}
}
foreach ($ids as $text) foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有血糖记录及局限。',7,[]);
$result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有血糖记录及局限。',7,[])
: DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;});
r37Check($result===null && $visible==='' && DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING',
"$mode ID-only variant rejected before output: ".json_encode($text));
$guard=TcmEvidenceGuard::assess($r37Current,$r37Context['snapshot']);
$review=TcmEvidenceGuard::review($guard,$text);
r37Check(!$review['valid'] && $review['answer']==='' && in_array('TASK_ANALYSIS_MISSING',array_column($review['issues'],'code'),true),
'compatibility review does not manufacture ID-only analysis');
}
foreach (['当前分析引用资料编号f1和f2;既往记录与本次判断保留时点限制。',
'f1, f2仅为引文编号,已有血糖记录的采样条件仍待核实。'] as $text) foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有资料及局限。',7,[]);
$result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有资料及局限。',7,[])
: DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;});
r37Check(is_array($result) && $result['answer']===$text && ($mode==='ordinary'||$visible===$text),
"$mode actual prose containing source IDs remains byte-identical");
}
$analysis='可比较记录\n2026-10-25空腹血糖8.3 mmol/L,2026-10-26为7.9 mmol/L;两个记录点不能代表长期趋势。';
$analysis=str_replace('\\n',"\n",$analysis);
foreach (['f11, f23', '{"fact_ids":["f11","f23"]}', '["f11","f23"]'] as $citation) {
foreach ([$citation."\n\n".$analysis,$analysis."\n\n".$citation] as $text) {
foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]);
$result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[])
:DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;});
r37Check(is_array($result)&&$result['answer']===$text&&($mode==='ordinary'||$visible===$text),
"$mode complete analysis with leading/trailing citation paragraph preserved");
}
}
}
foreach (["f11, f23\n\nf24, f25", "{\"fact_ids\":[\"f11\"]}\n\n[\"f23\"]"] as $text) {
foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]);
$result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[])
:DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;});
r37Check($result===null&&$visible===''&&DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING',
"$mode source-only multi-paragraph response not delivered");
}
}
foreach (['模型拒绝后只返回{"fact_ids":[]}。','资料处理仅输出{"fact_ids":["f1"]}。'] as $text) foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]);
$result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[])
:DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;});
r37Check($result===null&&$visible===''&&DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING',
"$mode exclusive protocol-only wrapper is not analysis");
}
echo json_encode(['suite'=>'RootR37SourceIdAnalysisContent','passed'=>count(array_filter($checks,fn($c)=>$c['passed'])),
'total'=>count($checks),'checks'=>$checks,'transport_calls'=>DifyChatService::$totalCalls,
'live_model_calls'=>0,'network_calls'=>0],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)."\n";
exit(in_array(false,array_column($checks,'passed'),true)?1:0);
}