2026-09-24 15:48:45 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 执行记录(架构设计 4.2:执行模块)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 平台侧的权威记录:executionId、能力、参数摘要、状态、回执、幂等键。
|
|
|
|
|
|
* A3.7 落库前的内存实现;幂等索引与参数摘要的语义不随存储变化。
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
|
|
|
|
|
|
|
|
import type { ExecutionReceipt } from '@aps/contracts';
|
|
|
|
|
|
|
|
|
|
|
|
export interface ExecutionRecord {
|
|
|
|
|
|
executionId: string;
|
|
|
|
|
|
capabilityId: string;
|
|
|
|
|
|
providerId: string;
|
|
|
|
|
|
sessionId: string;
|
|
|
|
|
|
projectId: string | null;
|
|
|
|
|
|
subjectUserId: string;
|
|
|
|
|
|
/** 参数摘要:只存摘要不存明文(架构设计 4.1「凭据不进日志」同理) */
|
|
|
|
|
|
paramsDigest: string;
|
|
|
|
|
|
idempotencyKey?: string;
|
|
|
|
|
|
status: ExecutionReceipt['status'];
|
|
|
|
|
|
receipt: ExecutionReceipt;
|
|
|
|
|
|
createdAt: number;
|
|
|
|
|
|
updatedAt: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function digestParams(params: unknown): string {
|
|
|
|
|
|
const canonical = JSON.stringify(params ?? null, Object.keys((params ?? {}) as Record<string, unknown>).sort());
|
|
|
|
|
|
return `sha256:${createHash('sha256').update(canonical).digest('hex').slice(0, 16)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class ExecutionStore {
|
|
|
|
|
|
private readonly records = new Map<string, ExecutionRecord>();
|
|
|
|
|
|
private readonly byIdempotencyKey = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
|
|
create(input: Omit<ExecutionRecord, 'status' | 'receipt' | 'createdAt' | 'updatedAt'> & { receipt: ExecutionReceipt }): ExecutionRecord {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
const record: ExecutionRecord = {
|
|
|
|
|
|
...input,
|
|
|
|
|
|
status: input.receipt.status,
|
|
|
|
|
|
createdAt: now,
|
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
|
};
|
|
|
|
|
|
this.records.set(record.executionId, record);
|
|
|
|
|
|
if (record.idempotencyKey) this.byIdempotencyKey.set(record.idempotencyKey, record.executionId);
|
|
|
|
|
|
return record;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
complete(executionId: string, receipt: ExecutionReceipt): ExecutionRecord | undefined {
|
|
|
|
|
|
const record = this.records.get(executionId);
|
|
|
|
|
|
if (!record) return undefined;
|
|
|
|
|
|
record.receipt = receipt;
|
|
|
|
|
|
record.status = receipt.status;
|
|
|
|
|
|
record.updatedAt = Date.now();
|
|
|
|
|
|
return record;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get(executionId: string): ExecutionRecord | undefined {
|
|
|
|
|
|
return this.records.get(executionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
findByIdempotencyKey(key: string): ExecutionRecord | undefined {
|
|
|
|
|
|
const executionId = this.byIdempotencyKey.get(key);
|
|
|
|
|
|
return executionId ? this.records.get(executionId) : undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-29 10:24:41 +08:00
|
|
|
|
/** 某个会话最近的执行记录(上下文摘要里引用工具结果用) */
|
|
|
|
|
|
listBySession(sessionId: string, limit = 10): ExecutionRecord[] {
|
|
|
|
|
|
return [...this.records.values()]
|
|
|
|
|
|
.filter((record) => record.sessionId === sessionId)
|
|
|
|
|
|
.sort((a, b) => b.createdAt - a.createdAt)
|
|
|
|
|
|
.slice(0, limit);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 15:48:45 +08:00
|
|
|
|
list(limit = 50): ExecutionRecord[] {
|
|
|
|
|
|
return [...this.records.values()].sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|