307 lines
24 KiB
PHP
307 lines
24 KiB
PHP
<?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']],
|
||
];
|
||
}
|
||
// Independent literal answers are local transport fixtures, not generated
|
||
// medical advice. They exercise distinct useful analyses and byte retention.
|
||
$answers = [
|
||
'case_analysis' => "主要问题与病程变化\n2026-10-25记录空腹血糖8.3 mmol/L,2026-10-26记录7.9 mmol/L。两次点位可供本次复核,不能据此评价整个病程或确诊并发症。\n\n诊断支持与局限\n2020年外院糖尿病诊断只保留历史身份;结合近期血糖和治疗记录复核当前控制情况,其他病因或辨证方向仍需相应证据。",
|
||
'glucose_trend' => "可比较记录\n2026-10-25空腹血糖8.3 mmol/L,2026-10-26为7.9 mmol/L,两次相差0.4 mmol/L;这只是两个记录点,不能代表长期达标或总体稳定。\n\n关联与监测\n日常记录有晚餐主食较多及饭后步行信息,尚不能确立与次日数值的因果;需核实采样条件和记录时间,再结合更多同类数据评价变化。",
|
||
'medication_review' => "当前与历史用药\n2026-10-26当前用药记录为“二甲双胍500mg,每日2次。”;历史格列美脲已停用,不对旧药安排再次停用。\n\n安全复核\n现有记录可核对药名和用法;肾功能与过敏史的资料尚不完整,不能据此认定适用性或安全性,需由医师结合相应检查和实际服用情况复核,不擅自调药。",
|
||
'complication_risk' => "风险依据与范围\n历史糖尿病诊断及近期空腹血糖记录提示需要持续评价相关风险;目前提供的血糖点位和单次血压不能确诊、排除或分级肾脏、眼底等并发症。\n\n筛查与优先级\n肾功能、尿白蛋白及眼底检查的评价范围尚未明确,需核实既有结果与日期后决定后续筛查;资料未提供不代表检查正常或风险为零。",
|
||
'summary' => "问诊核心结论\n本次可交接的重点为近期空腹血糖记录及当前用药复核:2026-10-25为8.3 mmol/L,2026-10-26为7.9 mmol/L。\n\n治疗和后续重点\n记录二甲双胍500mg每日2次,历史格列美脲已停用;复核服用情况、检查时点与资料缺口,保持历史记录和本次信息的区分,不把有限点位写成长程疗效结论。",
|
||
'lifestyle_advice' => "与本例相关的优先建议\n日常记录提到“晚餐主食较多,晚饭后步行20分钟。”,可先记录餐量、进餐时间和同类血糖点位,帮助复核饮食与血糖的关联;目前不能断定该次餐量造成次日血糖变化。\n\n可行步骤与限制\n保留可持续的活动记录,结合实际耐受和治疗安排由医师细化饮食、活动目标;年龄及少量血糖记录不足以确定个体化热量或运动强度。",
|
||
];
|
||
$tasks = array_keys($answers);
|
||
$original = new \ReflectionClass(DiagnosisAiLogic::class);
|
||
r37Check($original->getConstant('MODEL_KEYS') === ['qwen', 'openai'], 'Original profile whitelist is retained.');
|
||
foreach (['PROMPT_VERSION', 'ASSISTANT_PROMPT_VERSION', 'ANALYSIS_PROMPT_VERSION'] as $version) {
|
||
r37Check(str_ends_with($original->getConstant($version), '-r37'), $version . ' invalidates R36 provenance/cache.');
|
||
}
|
||
foreach (['MAX_REPORT_LENGTH' => 12000, 'MAX_PROMPT_SOURCE_BYTES' => 120000, 'MAX_ANALYSIS_ADVICE_LENGTH' => 1200] as $key => $value) {
|
||
r37Check($original->getConstant($key) === $value, 'Original finite budget retained: ' . $key);
|
||
}
|
||
r37Check(count(array_unique(array_map('sha1', $answers))) === 6, 'Six independently written task results are distinct.');
|
||
|
||
$invocations = 0;
|
||
foreach (['both_missing', 'tongue_missing', 'symptoms_missing'] as $variant) {
|
||
r37Fixture($variant);
|
||
$rawBefore = serialize([$r37Current, $r37Context]);
|
||
foreach ($answers as $task => $answer) {
|
||
$question = 'R37医师原问题:请完成' . $task . ',结合现有记录说明依据及局限。';
|
||
$prepared = DiagnosisAiLogic::prepareAssistant(37, $task, $question, 7, []);
|
||
r37Check(is_array($prepared), $variant . '/' . $task . ': partial evidence prepares successfully.');
|
||
if ($prepared === null) continue;
|
||
$guard = $prepared['tcm_evidence_guard'];
|
||
r37Check($guard['restricted'] && $guard['analysis_allowed'] && !in_array('脉象', $guard['missing'], true),
|
||
$variant . '/' . $task . ': completeness marker never includes excluded pulse or disables analysis.');
|
||
r37Check(str_contains($prepared['query'], $question) && str_contains($prepared['query'], '输出安排:')
|
||
&& !str_contains($prepared['query'], '只能从下方事实索引选id') && !str_contains($prepared['query'], '只输出fact_ids'),
|
||
$variant . '/' . $task . ': original question and task contract replace old source-selection protocol.');
|
||
$coverage = true;
|
||
foreach (array_keys(AiRequiredSource::LABELS) as $key) {
|
||
$coverage = $coverage && str_contains($prepared['query'], 'R37_FULL_SOURCE_' . $key)
|
||
&& str_contains($prepared['inputs']['herbs_json'], 'R37_FULL_SOURCE_' . $key);
|
||
}
|
||
r37Check($coverage && count($prepared['required_sections']) === 13
|
||
&& $prepared['files'] === $r37Context['files'], $variant . '/' . $task . ': all 13 raw source categories and attachments retained.');
|
||
$missing = implode('、', $guard['missing']);
|
||
$modelText = $answer . "\n\n本次资料限制\n本次尚缺可核验的" . $missing . ';依赖这些资料的辨证只作待核实方向,其余记录仍可供相应任务分析。需执业医师复核。';
|
||
foreach (['chat', 'stream'] as $mode) {
|
||
r37Reset([['ok' => true, 'content' => $modelText]]);
|
||
$visible = ''; $duringTransport = [];
|
||
$result = $mode === 'chat'
|
||
? DiagnosisAiLogic::assistant(37, $task, $question, 7, [])
|
||
: DiagnosisAiLogic::streamPreparedAssistant($prepared,
|
||
static function (string $text) use (&$visible, &$duringTransport): bool {
|
||
$visible .= $text; $duringTransport[] = DifyChatService::$streaming; return true;
|
||
}, null, null, ['knowledge_mode' => 'evidence']);
|
||
$invocations += count(DifyChatService::$calls);
|
||
$call = DifyChatService::$calls[0] ?? [];
|
||
$prefix = $variant . '/' . $task . '/' . $mode;
|
||
r37Check(count(DifyChatService::$calls) === 1 && DifyChatService::$responses === [], $prefix . ': public endpoint invokes the real local transport boundary exactly once.');
|
||
r37Check(is_array($result) && $result['answer'] === $modelText, $prefix . ': original useful model answer is returned without any local replacement.');
|
||
r37Check(($result['answer_mode'] ?? '') === 'clinical_analysis'
|
||
&& ($result['clinical_fact_state_version'] ?? '') === TcmClinicalFactStateGuard::VERSION
|
||
&& ($result['evidence_limitations'] ?? []) === $guard['missing'], $prefix . ': model analysis and limitations are reported honestly.');
|
||
r37Check(($call['profile'] ?? '') === 'qwen' && ($result['model_name'] ?? '') === 'r37-local-qwen'
|
||
&& ($call['options']['knowledge_mode'] ?? '') === 'clinical'
|
||
&& ($call['options']['reference_scope'] ?? '') === 'consultation', $prefix . ': original Qwen route and clinical consultation scope retained.');
|
||
r37Check(($call['query'] ?? '') === $prepared['query'] && ($call['inputs'] ?? []) === $prepared['inputs']
|
||
&& ($call['files'] ?? []) === $prepared['files'] && ($call['options']['required_files'] ?? false), $prefix . ': prompt, inputs, original files and required-source flag reach transport.');
|
||
r37Check(($call['options']['timeout_ms'] ?? 0) > 0 && $call['options']['timeout_ms'] <= 90000
|
||
&& ($call['options']['request_deadline_ns'] ?? 0) > 0, $prefix . ': existing deadline and finite model budget retained.');
|
||
if ($mode === 'stream') r37Check($visible === $modelText && in_array(true, $duringTransport, true),
|
||
$prefix . ': validated original model paragraphs arrive while generation is running.');
|
||
}
|
||
}
|
||
r37Check(serialize([$r37Current, $r37Context]) === $rawBefore, $variant . ': full source snapshot is never mutated.');
|
||
}
|
||
|
||
r37Fixture('both_missing');
|
||
$structured = ['diagnosis_advice' => '近期记录有两次空腹血糖点位,可供当前控制情况复核;历史诊断保留日期,症状病史与舌象资料尚待核实,不能据此确定证候。',
|
||
'risk_assessment' => [['label' => '既有病史和近期血糖记录需要持续风险评价;并发症检查资料尚不完整。', 'level' => 'medium']],
|
||
'treatment_advice' => '复核实际用药、血糖测量条件及已有肾功能等检查日期,再决定监测与随访;当前不形成新增方药或调药结论。'];
|
||
foreach (['qwen', 'openai'] as $profile) {
|
||
r37Reset([['ok' => true, 'content' => json_encode($structured, JSON_UNESCAPED_UNICODE)]]);
|
||
$result = DiagnosisAiLogic::analysis(37, 7, [], $profile);
|
||
$invocations += count(DifyChatService::$calls);
|
||
r37Check(is_array($result) && array_intersect_key($result, $structured) === $structured, $profile . ': structured analysis retains actual parsed model findings under partial evidence.');
|
||
r37Check(count(DifyChatService::$calls) === 1 && DifyChatService::$calls[0]['profile'] === $profile
|
||
&& DifyChatService::$calls[0]['options']['knowledge_mode'] === 'clinical'
|
||
&& ($result['model_key'] ?? '') === $profile && ($result['answer_mode'] ?? '') === 'clinical_analysis',
|
||
$profile . ': selected original channel performs structured clinical analysis.');
|
||
}
|
||
foreach (['请补采脉象。', '本次舌红,苔黄腻。', '本次无过敏史。'] as $unsafe) {
|
||
$invalid = $structured;
|
||
$invalid['treatment_advice'] = $unsafe;
|
||
r37Reset([['ok' => true, 'content' => json_encode($invalid, JSON_UNESCAPED_UNICODE)]]);
|
||
r37Check(DiagnosisAiLogic::analysis(37, 7, [], 'openai') === null && count(DifyChatService::$calls) === 1,
|
||
'Partial structured analysis rejects unsafe model field without local substitution: ' . $unsafe);
|
||
}
|
||
r37Reset([['ok' => true, 'content' => '{"fact_ids":["f1"]}'], ['ok' => true, 'content' => '{"fact_ids":["f1"]}']]);
|
||
r37Check(DiagnosisAiLogic::analysis(37, 7, [], 'openai') === null && count(DifyChatService::$calls) === 2,
|
||
'Structured obsolete protocol gets only the existing finite format repair, then an honest error.');
|
||
|
||
// Independent missing-data cases still request task-specific model analysis.
|
||
$r37Current['fasting_blood_sugar'] = ''; $r37Current['current_medications'] = '未记录';
|
||
foreach ($r37Context['snapshot']['required_content'] as &$section) {
|
||
if ($section['key'] === 'diagnosis_info') $section['value'] = [$r37Current];
|
||
if (in_array($section['key'], ['daily_tracking', 'prescriptions'], true)) $section['value'] = [];
|
||
}
|
||
unset($section);
|
||
foreach (['glucose_trend' => '当前没有可比较的血糖数值,不能计算趋势;可依据既往诊断记录建立同一时段的测量表,补充空腹或餐后身份及测量条件后再评价。',
|
||
'medication_review' => '当前用药资料未记录,不能判断具体药物相互作用或剂量;可结合历史诊断核实药名、实际服用状态及监测结果,再作相应安全评价。'] as $task => $answer) {
|
||
r37Reset([['ok' => true, 'content' => $answer]]);
|
||
$result = DiagnosisAiLogic::assistant(37, $task, '按本例资料解释局限及能做的分析', 7, []);
|
||
$invocations += count(DifyChatService::$calls);
|
||
r37Check(count(DifyChatService::$calls) === 1 && ($result['answer'] ?? '') === $answer,
|
||
$task . ': absent task measurements produce an actual model explanation, not a bypass or local checklist.');
|
||
}
|
||
|
||
r37Fixture('both_missing');
|
||
foreach (['请补采脉象。', '本次舌红,苔黄腻。', '本次无过敏史。'] as $unsafe) {
|
||
r37Reset([['ok' => true, 'content' => $unsafe]]);
|
||
r37Check(DiagnosisAiLogic::assistant(37, 'case_analysis', '', 7, []) === null
|
||
&& count(DifyChatService::$calls) === 1, 'Partial evidence still rejects forbidden or fabricated fact: ' . $unsafe);
|
||
$prepared = DiagnosisAiLogic::prepareAssistant(37, 'case_analysis', '', 7, []);
|
||
r37Reset([['ok' => true, 'content' => $unsafe . "\n\n"]]);
|
||
$visible = '';
|
||
$result = DiagnosisAiLogic::streamPreparedAssistant($prepared,
|
||
static function (string $text) use (&$visible): bool { $visible .= $text; return true; });
|
||
r37Check($result === null && $visible === '', 'Partial stream rejects unsafe model paragraph before delivery: ' . $unsafe);
|
||
}
|
||
r37Reset([['ok' => false, 'error_code' => 'UPSTREAM_TIMEOUT', 'error' => 'Local stub timed out.']]);
|
||
r37Check(DiagnosisAiLogic::assistant(37, 'summary', '', 7, []) === null
|
||
&& DiagnosisAiLogic::getAssistantErrorCode() === 'UPSTREAM_TIMEOUT'
|
||
&& count(DifyChatService::$calls) === 1, 'Model timeout is an honest error, with no fabricated source report.');
|
||
$prepared = DiagnosisAiLogic::prepareAssistant(37, 'summary', '', 7, []);
|
||
foreach (['UPSTREAM_TIMEOUT' => 1, 'UPSTREAM_REJECTED' => 2] as $code => $attempts) {
|
||
r37Reset(array_fill(0, $attempts, ['ok' => false, 'error_code' => $code, 'error' => 'Local stub unavailable.']));
|
||
$visible = '';
|
||
$result = DiagnosisAiLogic::streamPreparedAssistant($prepared,
|
||
static function (string $text) use (&$visible): bool { $visible .= $text; return true; });
|
||
r37Check($result === null && $visible === '' && count(DifyChatService::$calls) === $attempts
|
||
&& DiagnosisAiLogic::getAssistantErrorCode() === $code,
|
||
$code . ': stream keeps bounded existing retry policy and reports failure without a replacement answer.');
|
||
}
|
||
r37Reset([['ok' => true, 'content' => '{"fact_ids":["f1"]}']]);
|
||
r37Check(DiagnosisAiLogic::assistant(37, 'summary', '', 7, []) === null
|
||
&& DiagnosisAiLogic::getAssistantErrorCode() === 'TASK_ANALYSIS_MISSING', 'Obsolete source-ID-only model answer is rejected rather than expanded locally.');
|
||
foreach (['{"fact_ids":["f1"]}', "```json\n{\"fact_ids\":[\"f1\"]}\n```\n\n"] as $obsolete) {
|
||
$prepared = DiagnosisAiLogic::prepareAssistant(37, 'summary', '', 7, []);
|
||
r37Reset([['ok' => true, 'content' => $obsolete]]);
|
||
$visible = '';
|
||
$result = DiagnosisAiLogic::streamPreparedAssistant($prepared,
|
||
static function (string $text) use (&$visible): bool { $visible .= $text; return true; });
|
||
r37Check($result === null && $visible === '' && DiagnosisAiLogic::getAssistantErrorCode() === 'TASK_ANALYSIS_MISSING',
|
||
'Stream rejects obsolete source selection before delivery, without reporting doctor cancellation.');
|
||
}
|
||
$prepared = DiagnosisAiLogic::prepareAssistant(37, 'summary', '', 7, []);
|
||
r37Reset([]);
|
||
r37Check(DiagnosisAiLogic::streamPreparedAssistant($prepared, static fn (): bool => true, static fn (): bool => true) === null
|
||
&& DifyChatService::$calls === [], 'Cancellation before submission prevents all upstream calls.');
|
||
$prepared['request_deadline_ns'] = hrtime(true) - 1;
|
||
r37Check(DiagnosisAiLogic::streamPreparedAssistant($prepared, static fn (): bool => true) === null
|
||
&& DifyChatService::$calls === [] && DiagnosisAiLogic::getAssistantErrorCode() === 'UPSTREAM_TIMEOUT', 'Expired budget prevents all upstream calls.');
|
||
r37Check(DiagnosisAiLogic::prepareAssistant(37, 'prescription_generate', '', 7, []) === null
|
||
&& DiagnosisAiLogic::getAssistantErrorCode() === 'TCM_EVIDENCE_INCOMPLETE' && DifyChatService::$calls === [],
|
||
'Separate prescription draft gate is explicit and never disguised as a model response.');
|
||
r37Reset([]);
|
||
$legacyQuestion = '请根据当前病历总结患者病情、证候与风险。';
|
||
$authorizedTask = null;
|
||
$legacy = DiagnosisAiLogic::prepareAssistant(37, 'tcm_pattern', $legacyQuestion, 7, [], null,
|
||
['on_authorized' => static function (array $meta) use (&$authorizedTask): bool {
|
||
$authorizedTask = $meta['task']; return true;
|
||
}]);
|
||
r37Check(is_array($legacy) && $legacy['task'] === 'summary' && $authorizedTask === 'summary',
|
||
'Released summary shortcut uses summary in both authorization metadata and prepared task.');
|
||
r37Check(is_array($legacy) && str_contains($legacy['query'], $legacyQuestion)
|
||
&& str_contains($legacy['query'], '问诊核心结论→病程和关键记录'),
|
||
'Legacy summary keeps the exact question and receives the summary output contract.');
|
||
$explicit = DiagnosisAiLogic::prepareAssistant(37, 'tcm_pattern', $legacyQuestion . '请生成完整中医辨证报告。', 7, []);
|
||
r37Check(is_array($explicit) && $explicit['task'] === 'tcm_pattern' && DifyChatService::$calls === [],
|
||
'Distinct explicit TCM request is not remapped and preparation makes no model call.');
|
||
$failed = array_values(array_filter($checks, static fn (array $check): bool => !$check['passed']));
|
||
echo json_encode(['suite' => 'DiagnosisAiTaskSpecificPartialR37IntegrationTest', 'checks' => count($checks),
|
||
'passed' => count($checks) - count($failed), 'failed' => count($failed), 'core_transport_calls' => $invocations,
|
||
'local_transport_calls' => DifyChatService::$totalCalls,
|
||
'failures' => $failed], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
|
||
exit($failed === [] ? 0 : 1);
|
||
}
|