120 lines
4.8 KiB
TypeScript
120 lines
4.8 KiB
TypeScript
/**
|
||
* Core.invoke:工具调用的唯一出口(架构设计 4 / 术语表规则 3)。
|
||
*
|
||
* 职责:解析能力 → 判幂等 → 建执行记录 → 按截止时间调用 Provider → 归一化错误与回执。
|
||
* A4 会把策略判定与凭据代理插在「建执行记录」与「调用 Provider」之间;现在留出接缝。
|
||
*/
|
||
import { randomUUID } from 'node:crypto';
|
||
|
||
import type { ExecutionReceipt } from '@aps/contracts';
|
||
import { isErrorKind, ProviderError, type ErrorKind, type ExecutionContext } from '@aps/provider-sdk';
|
||
import type { CapabilityRegistry } from '../registry/registry.js';
|
||
import { digestParams, ExecutionStore, type ExecutionRecord } from './store.js';
|
||
|
||
export interface InvokeRequest {
|
||
/** 调用者身份(A4 起由身份上下文补齐,现在只需要用户与租户) */
|
||
subject: { userId: string; tenantId: string };
|
||
sessionId: string;
|
||
projectId?: string | null;
|
||
capabilityId: string;
|
||
params: unknown;
|
||
idempotencyKey?: string;
|
||
/** 覆盖默认超时(毫秒) */
|
||
timeoutMs?: number;
|
||
}
|
||
|
||
export interface CoreOptions {
|
||
registry: CapabilityRegistry;
|
||
executions?: ExecutionStore;
|
||
defaultTimeoutMs?: number;
|
||
}
|
||
|
||
export interface Core {
|
||
invoke(request: InvokeRequest): Promise<ExecutionReceipt>;
|
||
executions: ExecutionStore;
|
||
registry: CapabilityRegistry;
|
||
}
|
||
|
||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||
|
||
function failed(executionId: string, errorKind: ErrorKind, message: string): ExecutionReceipt {
|
||
return { executionId, status: 'failed', errorKind, result: { message } };
|
||
}
|
||
|
||
/** 把任意异常归一化成六分类之一(实施设计 5) */
|
||
function normalizeError(error: unknown): { kind: ErrorKind; message: string } {
|
||
if (error instanceof ProviderError) return { kind: error.kind, message: error.message };
|
||
if (error instanceof Error) return { kind: 'unknown_result', message: error.message };
|
||
return { kind: 'unknown_result', message: 'Provider 抛出未知错误' };
|
||
}
|
||
|
||
export function createCore(options: CoreOptions): Core {
|
||
const executions = options.executions ?? new ExecutionStore();
|
||
const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||
|
||
async function invoke(request: InvokeRequest): Promise<ExecutionReceipt> {
|
||
const capabilityId = request.capabilityId;
|
||
if (typeof capabilityId !== 'string' || capabilityId.trim() === '') {
|
||
return failed(`exec-${randomUUID()}`, 'invalid_argument', 'capabilityId 不能为空');
|
||
}
|
||
|
||
// 幂等:同一键的重试返回原回执,不重复执行
|
||
if (request.idempotencyKey) {
|
||
const existing = executions.findByIdempotencyKey(request.idempotencyKey);
|
||
if (existing) return existing.receipt;
|
||
}
|
||
|
||
const resolved = options.registry.resolve(capabilityId);
|
||
const executionId = `exec-${randomUUID()}`;
|
||
if (!resolved) {
|
||
return failed(executionId, 'invalid_argument', `能力 ${capabilityId} 未注册`);
|
||
}
|
||
|
||
const timeoutMs = request.timeoutMs ?? defaultTimeoutMs;
|
||
const context: ExecutionContext = {
|
||
executionId,
|
||
sessionId: request.sessionId,
|
||
projectId: request.projectId ?? null,
|
||
subject: { userId: request.subject.userId, tenantId: request.subject.tenantId },
|
||
...(request.idempotencyKey === undefined ? {} : { idempotencyKey: request.idempotencyKey }),
|
||
deadlineAt: Date.now() + timeoutMs,
|
||
};
|
||
|
||
executions.create({
|
||
executionId,
|
||
capabilityId,
|
||
providerId: resolved.providerId,
|
||
sessionId: request.sessionId,
|
||
projectId: request.projectId ?? null,
|
||
subjectUserId: request.subject.userId,
|
||
paramsDigest: digestParams(request.params),
|
||
...(request.idempotencyKey === undefined ? {} : { idempotencyKey: request.idempotencyKey }),
|
||
receipt: { executionId, status: 'accepted' },
|
||
});
|
||
|
||
// 超时判定:超时不等于成功也不等于失败——结果未知(实施设计 5)
|
||
const timeout = new Promise<ExecutionReceipt>((resolve) => {
|
||
setTimeout(() => resolve(failed(executionId, 'unknown_result', `调用超过 ${timeoutMs}ms,结果未知`)), timeoutMs).unref();
|
||
});
|
||
|
||
let receipt: ExecutionReceipt;
|
||
try {
|
||
receipt = await Promise.race([resolved.provider.invoke(context, { capabilityId, params: request.params }), timeout]);
|
||
} catch (error) {
|
||
const normalized = normalizeError(error);
|
||
receipt = failed(executionId, normalized.kind, normalized.message);
|
||
}
|
||
|
||
// Provider 返回的分类非法时归一为 unknown_result,避免脏值进记录
|
||
if (receipt.status === 'failed' && receipt.errorKind !== undefined && !isErrorKind(receipt.errorKind)) {
|
||
receipt = failed(executionId, 'unknown_result', `Provider 返回了未知错误分类 ${String(receipt.errorKind)}`);
|
||
}
|
||
|
||
executions.complete(executionId, receipt);
|
||
return receipt;
|
||
}
|
||
|
||
return { invoke, executions, registry: options.registry };
|
||
}
|
||
|
||
export type { ExecutionRecord }; |