feat: API-Runtime 版本兼容协议 (API-AI-072)
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 46s

- pollJobs: 记录/更新 RuntimeInstance (capabilities + heartbeat)
- submitResult: 校验 schemaVersion 匹配 job.outputSchemaVersion
- heartbeat: 首次调用设置 startedAt
- 错误码: RESULT_SCHEMA_UNSUPPORTED + RUNTIME_VERSION_INCOMPATIBLE

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-11 21:42:02 +08:00
parent 00ac32a103
commit 012e26b950

View File

@ -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) // Post-filter by snapshotVersion if needed (requires snapshot join)
if (supSnapshot.length > 0) { if (supSnapshot.length > 0) {
const snapshotIds = [...new Set(jobs.map(j => j.snapshotId).filter(Boolean))]; const snapshotIds = [...new Set(jobs.map(j => j.snapshotId).filter(Boolean))];
@ -92,14 +110,14 @@ export class RuntimeInternalService {
const now = new Date(); const now = new Date();
const lockUntil = new Date(now.getTime() + 60_000); 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({ const result = await this.prisma.aiRuntimeJob.updateMany({
where: { where: {
id: jobId, id: jobId,
lockedBy: runtimeInstanceId, lockedBy: runtimeInstanceId,
status: { in: ['locked', 'running'] }, status: { in: ['locked', 'running'] },
}, },
data: { lockUntil, status: 'running' }, data: { lockUntil, status: 'running', startedAt: new Date() },
}); });
if (result.count === 0) { 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` }); 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 ?? ''}`; const resultIdempotencyKey = `${jobId}:${dto.attemptNo}:${dto.outputHash ?? ''}`;
// Check duplicate // Check duplicate