From 012e26b9505b026307c676746cbfe87fd3e154a2 Mon Sep 17 00:00:00 2001 From: wangdl Date: Thu, 11 Jun 2026 21:42:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20API-Runtime=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E5=8D=8F=E8=AE=AE=20(API-AI-072)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pollJobs: 记录/更新 RuntimeInstance (capabilities + heartbeat) - submitResult: 校验 schemaVersion 匹配 job.outputSchemaVersion - heartbeat: 首次调用设置 startedAt - 错误码: RESULT_SCHEMA_UNSUPPORTED + RUNTIME_VERSION_INCOMPATIBLE Co-Authored-By: Claude Opus 4.7 --- .../internal/runtime-internal.service.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/modules/ai-runtime/internal/runtime-internal.service.ts b/src/modules/ai-runtime/internal/runtime-internal.service.ts index 75f83cd..759dec9 100644 --- a/src/modules/ai-runtime/internal/runtime-internal.service.ts +++ b/src/modules/ai-runtime/internal/runtime-internal.service.ts @@ -35,6 +35,24 @@ export class RuntimeInternalService { }, }); + // Register / update RuntimeInstance with capabilities + if (supSnapshot.length > 0 || supOutput.length > 0) { + await this.prisma.runtimeInstance.upsert({ + where: { runtimeInstanceId }, + create: { + runtimeInstanceId, + status: 'active', + lastHeartbeatAt: new Date(), + capabilities: capabilities as any, + }, + update: { + status: 'active', + lastHeartbeatAt: new Date(), + capabilities: capabilities as any, + }, + }); + } + // Post-filter by snapshotVersion if needed (requires snapshot join) if (supSnapshot.length > 0) { const snapshotIds = [...new Set(jobs.map(j => j.snapshotId).filter(Boolean))]; @@ -92,14 +110,14 @@ export class RuntimeInternalService { const now = new Date(); const lockUntil = new Date(now.getTime() + 60_000); - // First heartbeat: locked → running; subsequent heartbeats: extend lockUntil + // First heartbeat: locked → running + set startedAt; subsequent: extend lockUntil const result = await this.prisma.aiRuntimeJob.updateMany({ where: { id: jobId, lockedBy: runtimeInstanceId, status: { in: ['locked', 'running'] }, }, - data: { lockUntil, status: 'running' }, + data: { lockUntil, status: 'running', startedAt: new Date() }, }); if (result.count === 0) { @@ -176,6 +194,14 @@ export class RuntimeInternalService { throw new ConflictException({ errorCode: 'JOB_NOT_ACTIVE', message: `Job is ${job.status}, cannot accept result` }); } + // Validate schema version compatibility + if (job.outputSchemaVersion && dto.schemaVersion !== job.outputSchemaVersion) { + throw new BadRequestException({ + errorCode: 'RESULT_SCHEMA_UNSUPPORTED', + message: `Result schemaVersion ${dto.schemaVersion} does not match job outputSchemaVersion ${job.outputSchemaVersion}`, + }); + } + const resultIdempotencyKey = `${jobId}:${dto.attemptNo}:${dto.outputHash ?? ''}`; // Check duplicate