90]; } function hrtime(bool $number = false): int|array { $v = $GLOBALS['checkpointClock']; return $number ? $v : [intdiv($v, 1000000000), $v % 1000000000]; } } namespace app\adminapi\service { function hrtime(bool $number = false): int|array { return \app\adminapi\logic\tcm\hrtime($number); } } namespace app\common\service { final class DifyChatService { public static array $calls = [], $queue = []; public static function streamChat($profile, $inputs, $query, $user, $onDelta, $abort, $files = [], $options = []): array { self::$calls[] = compact('profile', 'inputs', 'query', 'user', 'files', 'options'); if (self::$queue === []) throw new \RuntimeException('Unexpected additional generation.'); $r = array_shift(self::$queue); $id = 'checkpoint-' . count(self::$calls); if (isset($options['on_task'])) ($options['on_task'])($id); $GLOBALS['checkpointClock'] += ($r['advance_ms'] ?? 0) * 1000000; foreach ($r['chunks'] ?? [] as $chunk) { if ($onDelta($chunk) !== false) continue; if (isset($options['on_cancel_task'])) ($options['on_cancel_task'])($id, static fn (): bool => true, 'cancelled'); $GLOBALS['checkpointClock'] += ($r['cleanup_ms'] ?? 0) * 1000000; if (!empty($r['terminal']) && isset($options['on_terminal'])) ($options['on_terminal'])($id, 'stopped'); return ['ok' => false, 'error_code' => 'AI_ASSISTANT_CANCELLED']; } if (!empty($r['disconnect_after_chunks'])) $GLOBALS['checkpointDisconnect'] = true; $GLOBALS['checkpointClock'] += ($r['finish_advance_ms'] ?? 0) * 1000000; if (isset($options['on_terminal'])) ($options['on_terminal'])($id, 'succeeded'); return $r; } public static function chat(...$args): array { throw new \RuntimeException('Blocking replacement is forbidden.'); } } } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; use app\adminapi\logic\tcm\DiagnosisAiLogic as Logic; use app\adminapi\service\AssistantRequestControl; use app\adminapi\service\TcmEvidenceGuard; use app\adminapi\service\TcmLongReportContract; use app\common\service\DifyChatService as Transport; $checks = 0; $checkpointClock = 1000000000; $checkpointDisconnect = false; function recoveryExpect(bool $ok, string $why): void { ++$GLOBALS['checks']; if (!$ok) throw new RuntimeException($why); } $chief = '乏力、活动后气短与白天自汗持续两周;患者自述活动后症状较明显,休息后可缓解,具体起止和日常活动关系保留本次访谈记录。'; $current = ['id' => 1, 'diagnosis_date' => '2026-10-23', 'chief_complaint' => $chief, 'present_illness' => '口干;未记录固定刺痛。', 'tongue' => '舌淡红苔少', 'pulse' => '脉细弱']; $guard = TcmEvidenceGuard::assess($current); $query = 'ALL_13_ORIGINAL_BYTES生成1800至2200字完整中医辨证长报告。'; $prepared = ['task' => 'custom', 'profile' => 'openai', 'admin_id' => 1, 'query' => $query, 'inputs' => ['source' => 'ALL_13_ORIGINAL_BYTES', 'opaque' => "original\r\nbytes"], 'files' => [['type' => 'image', 'url' => 'https://synthetic.invalid/unchanged.png']], 'user' => 'checkpoint-user', 'tcm_evidence_guard' => $guard, 'upstream_timeout' => 90]; $body = '本段为离线工程机制核验文字,记录输出次序与字节边界,临床事实和方药审核由独立测试负责。'; $prefix = "一、四诊\n\n望诊:未记录。\n\n闻诊:未记录。\n\n问诊:2026-10-23“" . $chief . "”(s1);2026-10-23“口干;未记录固定刺痛。”(s2)。\n\n切诊:其他触诊未记录,脉象见独立脉象项。\n\n舌象:2026-10-23“舌淡红苔少”(s3)。\n\n脉象:2026-10-23“脉细弱”(s4)。\n\n"; foreach (['二、八纲', '三、气血津液与脏腑', '四、证名', '五、治法'] as $title) $prefix .= $title . "\n\n" . str_repeat($body, 3) . "\n\n"; $makeTail = static function (string $p, int $length, string $intro = '主方方向:暂不列方,继续复核资料及监测。') use ($body): string { $t = "六、中药方药及复核监测\n\n" . $intro . "\n\n" . str_repeat($body, 3); return $t . str_repeat('核', max(0, $length - mb_strlen($p . $t, 'UTF-8'))); }; $tail = $makeTail($prefix, 2000); $whole = $prefix . $tail; recoveryExpect(TcmLongReportContract::validate($whole, $query)['valid'], 'The transport fixture retains the actual original range and all six substantive bodies.'); $badTail = "六、中药方药及复核监测\n\n主方方向:甲乙散方向,仅限医师审核。\n\n局限:核心方证证据不足。\n\n"; $root = sys_get_temp_dir() . '/tcm-final-checkpoint-' . bin2hex(random_bytes(6)); mkdir($root, 0700); function recoveryRun(array $prepared, array $queue, array $behavior = []): array { $GLOBALS['checkpointClock'] = 1000000000; $GLOBALS['checkpointDisconnect'] = false; Transport::$calls = []; Transport::$queue = $queue; $control = new AssistantRequestControl(1, 1, '12345678-1234-4234-8234-' . bin2hex(random_bytes(6)), $GLOBALS['root']); $control->begin(); $visible = ''; $reasons = []; $options = ['can_start_attempt' => static fn (): bool => $control->canStartAttempt(), 'on_task' => static fn ($id) => $control->bindTask($prepared['profile'], $prepared['user'], $id), 'on_terminal' => static fn ($id, $status) => $control->markTerminal($id, $status), 'on_cancel_task' => static function ($id, $stop, $why) use ($control, &$reasons, $behavior): array { $reasons[] = $why; if ($why === 'validation') $control->signalValidationFailure(); else $control->signalCancel(); if (!empty($behavior['cancel_cleanup'])) $control->signalCancel(); return $control->stopBoundTask($stop, $id); }]; if (!empty($behavior['without_gate'])) unset($options['can_start_attempt']); $result = Logic::streamPreparedAssistant($prepared, static function ($delta) use (&$visible, $behavior): bool { if (!empty($behavior['refuse_tail']) && str_starts_with($delta, '六、')) return false; $visible .= $delta; return true; }, static fn (): bool => $control->shouldAbort() || $GLOBALS['checkpointDisconnect'], null, $options); return ['out' => $result, 'visible' => $visible, 'calls' => Transport::$calls, 'error' => Logic::getAssistantErrorCode(), 'meta' => Logic::getAssistantErrorMeta(), 'state' => $control->publicState(), 'reasons' => $reasons]; } try { foreach (['qwen', 'openai'] as $profile) { $p = $prepared; $p['profile'] = $profile; $failure = ['ok' => false, 'chunks' => [$prefix, $badTail, 'NEVER_FORWARD'], 'terminal' => true, 'advance_ms' => 45000, 'cleanup_ms' => 1500]; $success = ['ok' => true, 'content' => $tail, 'chunks' => mb_str_split($tail, 7, 'UTF-8')]; $r = recoveryRun($p, [['ok' => true, 'content' => $whole, 'chunks' => mb_str_split($whole, 7, 'UTF-8')]]); recoveryExpect($r['out'] !== null && $r['visible'] === $whole && $r['out']['answer'] === $whole && count($r['calls']) === 1 && $r['out']['clinical_repair_count'] === 0, 'A valid original single-attempt report retains exact bytes and no unnecessary repair.'); $earlyPrefix = substr($prefix, 0, strpos($prefix, '三、')); $remaining = substr($whole, strlen($earlyPrefix)); $r = recoveryRun($p, [['ok' => true, 'content' => $earlyPrefix, 'chunks' => [$earlyPrefix], 'advance_ms' => 45000], ['ok' => true, 'content' => $remaining, 'chunks' => mb_str_split($remaining, 7, 'UTF-8')]]); recoveryExpect($r['out'] !== null && $r['visible'] === $whole && $r['out']['answer'] === $whole && count($r['calls']) === 2 && $r['out']['clinical_repair_count'] === 1, 'Natural truncation after sections1,2 uses the sole model suffix for sections3–6, preserving exact raw identity.'); preg_match('~\s*(.*?)\s*~su', $r['calls'][1]['query'], $m); $earlyData = json_decode($m[1], true); recoveryExpect($earlyData['delivered_prefix'] === $earlyPrefix && $earlyData['already_delivered_section_headings'] === [1, 2] && $earlyData['resume_existing_section'] === 2 && $earlyData['restart_undelivered_section'] === null && !str_contains($r['calls'][1]['query'], '当前必须从第六节标题开始') && !str_contains($r['calls'][1]['query'], '必须从“六、中药方药及复核监测”标题重新生成'), 'Generic early truncation resumes the existing section or missing following sections, without forcing a sixth-section restart.'); recoveryExpect(TcmLongReportContract::validate($r['visible'], $query)['section_order'] === [1, 2, 3, 4, 5, 6] && $earlyData['remaining_suffix_character_range'] === [1800 - mb_strlen($earlyPrefix, 'UTF-8'), 2200 - mb_strlen($earlyPrefix, 'UTF-8')] && $r['calls'][1]['options']['timeout_ms'] === 45000, 'Sections3–6 appear once within the original remaining character and absolute time budget.'); foreach ($r['calls'] as $call) recoveryExpect($call['profile'] === $profile && $call['inputs'] === $p['inputs'] && $call['files'] === $p['files'] && str_starts_with($call['query'], $p['query']), 'Early-truncation recovery preserves the original channel and complete original source payload.'); $factFailure = array_replace($failure, ['chunks' => [$prefix, "六、方药\n\n当前无固定刺痛。\n\n"]]); $quotedFailure = array_replace($failure, ['chunks' => [$prefix, "六、方药\n\n主方方向:“甲乙散”(s1)。核心方证证据不足。\n\n"]]); foreach ([$failure, $factFailure, $quotedFailure, ['ok' => true, 'content' => $prefix . $makeTail($prefix, 2221), 'chunks' => [$prefix . $makeTail($prefix, 2221)], 'advance_ms' => 45000]] as $first) { $r = recoveryRun($p, [$first, $success]); recoveryExpect($r['out'] !== null && $r['visible'] === $whole && $r['out']['answer'] === $whole, 'A private invalid final tail is replaced only by an original new model tail with exact final identity.'); recoveryExpect(count($r['calls']) === 2 && $r['out']['clinical_repair_count'] === 1 && $r['out']['report_stage_count'] === 2, 'Only the existing sole continuation is used.'); recoveryExpect(!str_contains($r['visible'], '甲乙散') && !str_contains($r['visible'], 'NEVER_FORWARD') && !str_contains($r['visible'], '当前无固定刺痛'), 'No rejected sixth-section draft byte is exposed.'); foreach ($r['calls'] as $call) recoveryExpect($call['profile'] === $profile && $call['inputs'] === $p['inputs'] && $call['files'] === $p['files'] && $call['user'] === $p['user'] && str_starts_with($call['query'], $p['query']) && substr_count($call['query'], '') === 1, 'Original channel, source inputs, files, user and question survive both calls.'); preg_match('~\s*(.*?)\s*~su', $r['calls'][1]['query'], $m); $data = json_decode($m[1], true); recoveryExpect($data['delivered_prefix'] === $prefix && $data['restart_undelivered_section'] === 6 && $data['resume_existing_section'] === 6, 'Continuation explicitly starts the never-delivered sixth heading after the exact first-five prefix.'); if ($first === $quotedFailure) recoveryExpect(in_array('HERBAL_PREMISE_SELF_CONFLICT', array_column($data['issues'], 'code'), true), 'The unrelated s1 citation counterfeit is specifically rejected by the premise checkpoint against the actual source ledger.'); recoveryExpect($data['original_whole_report_range'] === [1800, 2200] && $data['remaining_suffix_character_range'] === [1800 - mb_strlen($prefix, 'UTF-8'), 2200 - mb_strlen($prefix, 'UTF-8')] && str_contains($r['calls'][1]['query'], '第六节不列具体方名、药味'), 'Correction explains omitted proposals when key evidence remains missing and retains the exact remaining whole-report character budget.'); recoveryExpect($r['calls'][1]['options']['timeout_ms'] === (!empty($first['cleanup_ms']) ? 43500 : 45000), 'Generation and confirmed-stop cleanup consume the same90-second deadline.'); } $r = recoveryRun($p, [array_replace($failure, ['terminal' => false])]); recoveryExpect($r['out'] === null && count($r['calls']) === 1 && $r['error'] === 'UPSTREAM_CLEANUP_PENDING' && $r['visible'] === $prefix, 'Accepted stop without actual terminal blocks recovery.'); $r = recoveryRun($p, [$failure], ['cancel_cleanup' => true]); recoveryExpect($r['out'] === null && count($r['calls']) === 1 && $r['error'] === 'AI_ASSISTANT_CANCELLED', 'Cancellation during cleanup never starts recovery.'); $r = recoveryRun($p, [array_replace($failure, ['advance_ms' => 89000, 'cleanup_ms' => 1000])]); recoveryExpect($r['out'] === null && count($r['calls']) === 1 && $r['error'] === 'UPSTREAM_TIMEOUT', 'No repair gets an extra deadline after cleanup consumes90seconds.'); $r = recoveryRun($p, [$failure, ['ok' => false, 'chunks' => [$badTail], 'terminal' => true]]); recoveryExpect($r['out'] === null && count($r['calls']) === 2 && $r['visible'] === $prefix, 'A second invalid tail cannot create a third generation.'); $r = recoveryRun($p, [$failure], ['without_gate' => true]); recoveryExpect($r['out'] === null && count($r['calls']) === 1, 'Missing actual-terminal readiness cannot authorize recovery.'); $r = recoveryRun($p, [['ok' => true, 'content' => $whole, 'chunks' => [$whole], 'disconnect_after_chunks' => true]]); recoveryExpect($r['out'] === null && $r['visible'] === $prefix && count($r['calls']) === 1, 'Disconnect while the final tail is buffered never exposes it or starts another call.'); $r = recoveryRun($p, [['ok' => true, 'content' => $whole, 'chunks' => [$whole]]], ['refuse_tail' => true]); recoveryExpect($r['out'] === null && $r['visible'] === $prefix && $r['error'] === 'AI_ASSISTANT_CANCELLED' && count($r['calls']) === 1, 'Callback false on final flush remains cancellation.'); $r = recoveryRun($p, [['ok' => true, 'content' => $whole, 'chunks' => [$whole], 'finish_advance_ms' => 90000]]); recoveryExpect($r['out'] === null && $r['visible'] === $prefix && $r['error'] === 'UPSTREAM_TIMEOUT' && count($r['calls']) === 1, 'Expired absolute deadline before final flush cannot release the tail.'); $tightPrefix = $prefix . str_repeat('核', 2148 - mb_strlen($prefix, 'UTF-8')) . "\n\n"; $tightDraft = $tightPrefix . $makeTail($tightPrefix, 2300); $r = recoveryRun($p, [['ok' => true, 'content' => $tightDraft, 'chunks' => [$tightDraft]]]); recoveryExpect($r['out'] === null && count($r['calls']) === 1 && $r['visible'] === $tightPrefix, 'No remaining character room for a complete sixth section prevents a repair attempt.'); } } finally { foreach (glob($root . '/*') ?: [] as $f) if (is_file($f)) unlink($f); if (is_file($root . '/.gc.lock')) unlink($root . '/.gc.lock'); rmdir($root); } echo "TCM final-section sole-recovery integration: {$checks} assertions passed (offline transport; original acceptance unchanged).\n"; }