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 };
|