125 lines
8.4 KiB
PHP
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"}';
|
|
}
|