更新
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\prescriptionai\PrescriptionAiStore;
|
||||
use app\common\service\prescriptionai\PrescriptionAiWorker;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
final class PrescriptionAiBackfill extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('prescription-ai:backfill')->setDescription('预览或分批补登记手工处方AI任务,不调用模型')
|
||||
->addOption('from', null, Option::VALUE_REQUIRED, '开始日期 YYYY-MM-DD')
|
||||
->addOption('to', null, Option::VALUE_REQUIRED, '结束日期 YYYY-MM-DD')
|
||||
->addOption('after-id', null, Option::VALUE_REQUIRED, '上次返回的游标', '0')
|
||||
->addOption('limit', null, Option::VALUE_REQUIRED, '每批最多200', '50')
|
||||
->addOption('apply', null, Option::VALUE_NONE, '确认将本批登记为后台任务');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
foreach (['from', 'to'] as $key) {
|
||||
$value = (string) $input->getOption($key);
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) || date('Y-m-d', strtotime($value)) !== $value) {
|
||||
$output->writeln('Explicit valid from/to dates are required');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if ($input->getOption('apply') && !PrescriptionAiStore::enabled()) {
|
||||
$output->writeln('Enable prescription_analysis before enqueueing');
|
||||
return 1;
|
||||
}
|
||||
$from = strtotime((string) $input->getOption('from'));
|
||||
$to = strtotime((string) $input->getOption('to') . ' 23:59:59');
|
||||
if ($from > $to) {
|
||||
$output->writeln('Invalid date range');
|
||||
return 1;
|
||||
}
|
||||
$result = (new PrescriptionAiWorker())->reconcile((int) $input->getOption('after-id'),
|
||||
(int) $input->getOption('limit'), $from, $to, (bool) $input->getOption('apply'));
|
||||
$output->writeln(json_encode(['dry_run' => !$input->getOption('apply')] + $result));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\prescriptionai\PrescriptionAiStore;
|
||||
use app\common\service\prescriptionai\PrescriptionAiWorker;
|
||||
use think\console\Command;
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
final class PrescriptionAiWork extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('prescription-ai:work')->setDescription('处方AI独立后台任务;分别运行prepare/qwen/openai')
|
||||
->addOption('lane', null, Option::VALUE_REQUIRED, 'prepare、qwen、openai', 'prepare')
|
||||
->addOption('once', null, Option::VALUE_NONE, '只处理一轮');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$lane = (string) $input->getOption('lane');
|
||||
if (!in_array($lane, ['prepare', 'qwen', 'openai'], true)) {
|
||||
$output->writeln('Invalid lane');
|
||||
return 1;
|
||||
}
|
||||
$running = true;
|
||||
if (function_exists('pcntl_async_signals')) {
|
||||
pcntl_async_signals(true);
|
||||
pcntl_signal(SIGTERM, static function () use (&$running): void { $running = false; });
|
||||
pcntl_signal(SIGINT, static function () use (&$running): void { $running = false; });
|
||||
}
|
||||
// One model task holds its database connection across several minutes of upstream calls,
|
||||
// which can outlive the server's wait_timeout. Without reconnecting, the first dropped
|
||||
// connection would wedge this consumer in a permanent error loop.
|
||||
$database = (array) config('database');
|
||||
$connection = (string) ($database['default'] ?? 'mysql');
|
||||
if (isset($database['connections'][$connection]) && is_array($database['connections'][$connection])) {
|
||||
$database['connections'][$connection]['break_reconnect'] = true;
|
||||
Config::set($database, 'database');
|
||||
}
|
||||
$worker = new PrescriptionAiWorker();
|
||||
$sweepAt = 0;
|
||||
$sourceCursor = 0;
|
||||
$rxCursor = 0;
|
||||
do {
|
||||
$worked = false;
|
||||
try {
|
||||
if (PrescriptionAiStore::enabled()) {
|
||||
$worked = $lane === 'prepare' ? $worker->prepareOne() : $worker->runOne($lane);
|
||||
if ($lane === 'prepare' && time() >= $sweepAt) {
|
||||
$sweep = $worker->refreshSources($sourceCursor);
|
||||
$sourceCursor = $sweep['selected'] > 0 ? $sweep['last_id'] : 0;
|
||||
$rx = $worker->reconcile($rxCursor);
|
||||
$rxCursor = $rx['selected'] > 0 ? $rx['last_id'] : 0;
|
||||
$sweepAt = time() + 60;
|
||||
}
|
||||
}
|
||||
if ($input->getOption('once') || $worked) {
|
||||
$output->writeln('PRESCRIPTION_AI ' . json_encode(['lane' => $lane, 'enabled' => PrescriptionAiStore::enabled(), 'processed' => $worked]));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Class, location and SQLSTATE only: an exception message can carry SQL values or clinical text.
|
||||
$detail = get_class($e) . '@' . basename($e->getFile()) . ':' . $e->getLine();
|
||||
if (preg_match('/SQLSTATE\[[A-Z0-9]{5}\](?:\s*\[\d+\])?/', $e->getMessage(), $sqlState) === 1) {
|
||||
$detail .= ' ' . $sqlState[0];
|
||||
}
|
||||
$output->writeln('PRESCRIPTION_AI storage_or_configuration_error ' . $detail);
|
||||
// Drop a possibly dead connection so the next round reconnects instead of looping.
|
||||
try {
|
||||
Db::connect()->close();
|
||||
} catch (\Throwable $ignored) {
|
||||
}
|
||||
if ($input->getOption('once')) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (!$input->getOption('once') && $running) {
|
||||
usleep($worked ? 100000 : 1000000);
|
||||
}
|
||||
} while (!$input->getOption('once') && $running);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user