main: 增强 Agent Run 调度可靠性与幂等性

- 默认切换 AgentProvider 为 HttpAgentProvider,增强网络请求的容错和重试机制
- 优化 Run 逻辑,支持多场景去重与并发保护
- 添加 Redis 发布失败的日志记录以提升问题排查效率
- 扩展 OpenAPI 规范,新增 Error 和 Run 状态相关模型
- 增强测试覆盖,验证调度策略和重复请求的幂等性
- 增加数据库索引以优化查询性能
- 更新所有相关文档和配置文件
This commit is contained in:
2025-12-18 17:41:42 +08:00
parent 2ad101c297
commit 6d934f4e34
16 changed files with 634 additions and 118 deletions

View File

@@ -2,6 +2,9 @@
namespace App\Jobs;
use App\Models\Message;
use App\Services\Agent\ProviderException;
use App\Services\CancelChecker;
use App\Services\OutputSink;
use App\Services\RunLoop;
use Illuminate\Bus\Queueable;
@@ -14,22 +17,61 @@ class AgentRunJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries;
public int $timeout;
public int $backoff;
public function __construct(public string $sessionId, public string $runId)
{
$this->tries = (int) config('agent.job.tries', 1);
$this->timeout = (int) config('agent.job.timeout_seconds', 120);
$this->backoff = (int) config('agent.job.backoff_seconds', 5);
}
public function handle(RunLoop $loop, OutputSink $sink): void
public function handle(RunLoop $loop, OutputSink $sink, CancelChecker $cancelChecker): void
{
try {
logger("Running run {$this->runId} for session {$this->sessionId}");
$loop->run($this->sessionId, $this->runId);
} catch (\Throwable $e) {
$sink->appendError($this->sessionId, $this->runId, 'run.failed', $e->getMessage());
logger("Running error {$this->runId} for session {$this->sessionId}");
logger("error message:",[$e->getMessage(),$e->getTraceAsString()]);
$errorCode = $e instanceof ProviderException ? $e->errorCode : 'run.failed';
$dedupeKey = $e instanceof ProviderException
? "run:{$this->runId}:error:provider"
: "run:{$this->runId}:error:job";
$sink->appendError($this->sessionId, $this->runId, $errorCode, $e->getMessage(), [], $dedupeKey);
$sink->appendRunStatus($this->sessionId, $this->runId, 'FAILED', [
'error' => $e->getMessage(),
'dedupe_key' => "run:{$this->runId}:status:FAILED",
]);
throw $e;
} finally {
$latestStatus = Message::query()
->where('session_id', $this->sessionId)
->where('type', 'run.status')
->whereRaw("payload->>'run_id' = ?", [$this->runId])
->orderByDesc('seq')
->first();
$status = $latestStatus ? ($latestStatus->payload['status'] ?? null) : null;
if ($status === 'RUNNING' || ! $status) {
if ($cancelChecker->isCanceled($this->sessionId, $this->runId)) {
$sink->appendRunStatus($this->sessionId, $this->runId, 'CANCELED', [
'dedupe_key' => "run:{$this->runId}:status:CANCELED",
]);
} else {
$sink->appendRunStatus($this->sessionId, $this->runId, 'FAILED', [
'error' => 'JOB_ENDED_UNEXPECTEDLY',
'dedupe_key' => "run:{$this->runId}:status:FAILED",
]);
}
}
}
}
}