Files
zyt/artifacts/private_ai_runtime_synthetic_20261027.php
2026-10-27 14:03:35 +08:00

125 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
// Fresh, bounded synthetic-only lifecycle entry. No route, DB, auth, or patient fixture.
ini_set('display_errors', '0');
ini_set('log_errors', '0');
header('Content-Type: application/json');
if (PHP_SAPI !== 'cgi-fcgi' || ($_SERVER['REMOTE_ADDR'] ?? '') !== '127.0.0.1'
|| ($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST' || (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 256) {
http_response_code(403); echo '{"ok":false,"error_code":"PRIVATE_FASTCGI_POST_REQUIRED"}'; exit;
}
$control = null;
try {
$input = json_decode((string) file_get_contents('php://input'), true, 8, JSON_THROW_ON_ERROR);
if (!is_array($input) || count($input) !== 2 || !isset($input['profile'], $input['mode'])
|| !in_array($input['profile'], ['qwen', 'openai'], true)
|| !in_array($input['mode'], ['complete', 'cancel-before', 'cancel-after', 'timeout'], true)) {
echo '{"ok":false,"error_code":"SYNTHETIC_INPUT_INVALID"}'; exit;
}
$profile = $input['profile']; $mode = $input['mode']; unset($input);
$root = dirname(__DIR__) . '/server';
require $root . '/vendor/autoload.php';
$app = new \think\App($root . '/');
require_once $root . '/vendor/topthink/framework/src/helper.php';
$app->env->load($root . '/.env');
$app->config->set(require $root . '/config/prescription_ai.php', 'prescription_ai');
$app->config->set(['default' => 'probe', 'channels' => ['probe' => ['type' => 'File',
'path' => $root . '/runtime/ai-synthetic-runtime-20261027/logs/', 'realtime_write' => false]]], 'log');
$random = bin2hex(random_bytes(16));
$requestId = substr($random, 0, 8) . '-' . substr($random, 8, 4) . '-4' . substr($random, 13, 3)
. '-8' . substr($random, 17, 3) . '-' . substr($random, 20, 12);
$user = 'synthetic-runtime-20261027-' . $random;
$directory = $root . '/runtime/ai-synthetic-runtime-20261027/controls';
$control = new \app\adminapi\service\AssistantRequestControl(2147483001, 2147483002, $requestId, $directory);
if (!$control->begin()) throw new \RuntimeException('begin');
$started = hrtime(true);
$first = null; $deltaCount = 0; $deltaBytes = 0; $submissions = 0;
$streamHash = hash_init('sha256');
$taskSeen = false; $runSeen = false; $closeCount = 0; $lookupCount = 0;
$lookups = [];
$lookup = static function (array $identity, int $timeoutMs) use (&$lookupCount, &$lookups): array {
++$lookupCount;
$result = \app\common\service\DifyWorkflowRunStatus::lookup($identity, $timeoutMs);
$lookups[] = ($result['ok'] ?? false) === true ? $result['status'] : ($result['error_code'] ?? 'RUN_STATUS_UNCONFIRMED');
return $result;
};
$options = [
'knowledge_mode' => 'evidence', 'timeout' => $mode === 'timeout' ? 1 : 30,
'request_deadline_ns' => $started + ($mode === 'timeout' ? 1 : 30) * 1000000000,
'can_start_attempt' => static fn (): bool => $control->canStartAttempt(),
'on_submission' => static function () use ($control, $profile, $user, &$submissions): string {
// Exactly one owned synthetic generation per invocation; no model fallback.
if ($submissions !== 0) throw new \RuntimeException('one_generation_limit');
$token = $control->beginSubmission($profile, $user); ++$submissions; return $token;
},
'on_task' => static function (string $taskId, string $runId = '', string $submissionId = '')
use ($control, $profile, $user, $mode, &$taskSeen, &$runSeen): void {
$control->bindTask($profile, $user, $taskId, $runId, $submissionId);
$taskSeen = true; $runSeen = $runSeen || $runId !== '';
if ($mode === 'cancel-before') $control->signalCancel();
},
'on_stop' => static function (bool $accepted, string $taskId = '') use ($control): void {
if ($taskId !== '') $control->markStop($accepted, $taskId);
},
'on_timeout' => static function () use ($control): void { $control->signalTimeout(); },
'on_cancel_task' => static function (string $taskId, callable $performStop, string $reason = 'cancelled') use ($control): array {
if ($control->hasHandoffTask($taskId)) return $control->stopBoundTask($performStop, $taskId);
if ($reason === 'timeout') $control->signalTimeout();
elseif ($reason === 'validation') $control->signalValidationFailure();
else $control->signalCancel();
return $control->stopBoundTask($performStop, $taskId);
},
'on_terminal' => static function (string $taskId, string $status, string $runId = '') use ($control): void {
$control->markTerminal($taskId, $status, $runId);
},
'on_transfer_end' => static function (string $taskId = '', string $submissionId = '', string $disposition = 'unconfirmed')
use ($control, $lookup, &$closeCount): void {
++$closeCount;
if ($submissionId !== '') $control->closeSubmittedGeneration($submissionId, $taskId, $disposition, $lookup);
elseif ($taskId !== '') $control->closeGeneration($taskId, $lookup);
},
];
$query = $mode === 'complete'
? '这是纯软件测试,不对应真实患者,无病历或附件。请只用中文简短说明检查表单完整性需核对哪些字段,80字以内,不作诊断或用药建议。'
: '这是纯软件取消或超时测试,不对应真实患者,无病历或附件。请分20段说明如何检查普通软件表单数据完整性,每段80字,不作诊断或用药建议。';
$result = \app\common\service\DifyChatService::streamChat($profile, [], $query, $user,
static function (string $delta) use ($control, $mode, $streamHash, &$first, &$deltaCount, &$deltaBytes): bool {
if ($control->shouldAbort()) return false;
$first ??= hrtime(true); ++$deltaCount; $deltaBytes += strlen($delta); hash_update($streamHash, $delta);
if ($mode === 'cancel-after') { $control->signalCancel(); return false; }
return true;
}, static fn (): bool => $control->shouldAbort(), [], $options);
$control->finish($control->shouldAbort() ? 'cancelled' : (!empty($result['ok']) ? 'done' : 'error'));
$recovery = $control->reconcileClosedTasks($lookup);
$state = $control->publicState();
$state = array_intersect_key($state, array_flip([
'status', 'cancel_requested', 'timeout_reached', 'transport_closed', 'upstream_stop_confirmed',
'upstream_terminal_confirmed', 'upstream_terminal_status', 'upstream_pending_task_count',
'producer_terminal_received', 'producer_terminal_status', 'persisted_run_status',
'generation_transfer_closed', 'generation_verification_elapsed_ms', 'lifecycle_conflict',
'submitted_attempt_count', 'submitted_transfer_closed_count', 'submitted_pending_attempt_count',
'submitted_without_identity_count', 'pre_execution_closed_attempt_count',
'not_submitted_attempt_count', 'rejected_before_execution_attempt_count',
]));
$streamMatchesFinal = hash_final($streamHash) === hash('sha256', (string) ($result['content'] ?? ''));
echo json_encode([
'ok' => !empty($result['ok']), 'synthetic_only' => true, 'profile' => $profile, 'mode' => $mode,
'error_code' => (string) ($result['error_code'] ?? ''),
'first_delta_ms' => $first === null ? null : (int) floor(($first - $started) / 1e6),
'total_ms' => (int) floor((hrtime(true) - $started) / 1e6),
'delta_count' => $deltaCount, 'delta_bytes' => $deltaBytes,
'answer_bytes' => strlen((string) ($result['content'] ?? '')), 'stream_matches_final' => $streamMatchesFinal,
'task_captured' => $taskSeen, 'run_captured' => $runSeen,
'generation_submission_count' => $submissions, 'transfer_close_callback_count' => $closeCount,
'status_lookup_count' => $lookupCount, 'status_observations' => $lookups,
'state' => $state, 'final_recovery' => $recovery,
'full_answer_pass' => $mode === 'complete' && !empty($result['ok']) && $deltaBytes > 0 && $streamMatchesFinal,
'independent_terminal_pass' => !empty($state['upstream_terminal_confirmed']) && ($state['upstream_pending_task_count'] ?? 1) === 0,
], JSON_THROW_ON_ERROR);
} catch (\Throwable $ignored) {
if ($control !== null) {
try { $control->signalCancel(); $control->finish('error'); } catch (\Throwable $ignoredAgain) {}
}
echo '{"ok":false,"synthetic_only":true,"error_code":"SYNTHETIC_RUNTIME_DIAGNOSTIC_FAILED"}';
}