aps-agent/providers/aps/index.ts

182 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* APS Provider(架构设计第 6 节):把 APS 的能力翻译成平台契约。
*
* 只做适配:声明能力、转发调用、归一化回执。不判断权限(那归 APS)、不碰模型上下文。
*
* **能力的权威来源是 APS 自己的 `/api/manifest`**(A5.1):平台启动注册时拉一次,
* 不在平台侧手抄一份,避免两边漂移。APS 起不来时 `describe()` 直接失败——注册不上,
* 比"注册上了但能力是旧的"安全。
*/
import {
ProviderError,
isErrorKind,
type Capability,
type CapabilityCall,
type ExecutionContext,
type HealthStatus,
type Provider,
type ProviderManifest,
} from '@aps/provider-sdk';
import type { ExecutionReceipt } from '@aps/contracts';
export const APS_PROVIDER_ID = 'aps';
export interface ApsProviderOptions {
/** APS 服务地址,如 http://127.0.0.1:8000 */
baseUrl: string;
/** 服务间令牌(B5 的鉴权位;首版 APS 未强制) */
token?: string;
/** 单次调用超时,默认 30s;写能力应短于平台的 execution 超时 */
timeoutMs?: number;
/** 便于测试注入 */
fetchImpl?: typeof fetch;
}
interface ApsManifestBody {
id?: unknown;
displayName?: unknown;
version?: unknown;
platformVersion?: unknown;
requiresIdentity?: unknown;
auth?: unknown;
capabilities?: unknown;
}
const DEFAULT_TIMEOUT_MS = 30_000;
const RECEIPT_STATUSES = new Set(['accepted', 'pending-confirmation', 'succeeded', 'failed']);
function asString(value: unknown, fallback: string): string {
return typeof value === 'string' && value.trim() !== '' ? value : fallback;
}
/** APS 的能力声明与平台 provider-sdk 的 Capability 同形,这里只做字段校验与兜底 */
function toCapability(raw: unknown): Capability {
const item = (raw ?? {}) as Record<string, unknown>;
const id = asString(item.id, '');
if (id === '') throw new ProviderError('invalid_argument', 'APS manifest 里有能力缺 id');
const effect = item.effect === 'draft' || item.effect === 'write' ? item.effect : 'read';
const mode = item.mode === 'async' ? 'async' : 'sync';
const idempotency = item.idempotency === 'key' || item.idempotency === 'natural' ? item.idempotency : 'none';
const errors = Array.isArray(item.errors)
? item.errors.filter((kind): kind is Capability['errors'][number] => isErrorKind(kind))
: [];
return {
id,
description: asString(item.description, item.title === undefined ? id : String(item.title)),
effect,
mode,
inputSchema: (item.inputSchema as Record<string, unknown>) ?? { type: 'object' },
outputSchema: (item.outputSchema as Record<string, unknown>) ?? { type: 'object' },
idempotency,
errors: errors.length > 0 ? errors : ['unavailable', 'unknown_result'],
...(typeof item.title === 'string' ? { title: item.title } : {}),
...(typeof item.whenNotToUse === 'string' ? { whenNotToUse: item.whenNotToUse } : {}),
...(Array.isArray(item.synonyms) ? { synonyms: item.synonyms.filter((s): s is string => typeof s === 'string') } : {}),
};
}
export function createApsProvider(options: ApsProviderOptions): Provider {
const baseUrl = options.baseUrl.replace(/\/+$/, '');
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const doFetch = options.fetchImpl ?? fetch;
const headers = (): Record<string, string> => ({
'content-type': 'application/json',
...(options.token === undefined ? {} : { authorization: `Bearer ${options.token}` }),
});
let manifestCache: ProviderManifest | undefined;
async function describe(): Promise<ProviderManifest> {
{
let response: Response;
try {
response = await doFetch(`${baseUrl}/api/manifest`, { headers: headers(), signal: AbortSignal.timeout(timeoutMs) });
} catch (error) {
throw new ProviderError('unavailable', `连不上 APS:${error instanceof Error ? error.message : '未知错误'}`);
}
if (!response.ok) {
throw new ProviderError('unavailable', `APS manifest 返回 HTTP ${response.status}`);
}
const body = (await response.json()) as ApsManifestBody;
const raw = Array.isArray(body.capabilities) ? body.capabilities : [];
const described: ProviderManifest = {
id: asString(body.id, APS_PROVIDER_ID),
displayName: asString(body.displayName, 'APS 排产系统'),
version: asString(body.version, '0.0.0'),
platformVersion: typeof body.platformVersion === 'number' ? body.platformVersion : 1,
requiresIdentity: body.requiresIdentity !== false,
capabilities: raw.map(toCapability),
};
manifestCache = described;
return described;
}
}
async function invoke(context: ExecutionContext, call: CapabilityCall): Promise<ExecutionReceipt> {
{
// 能力白名单:APS 上不存在的能力直接报错,不把一个 404/兜底响应当成回执
const manifest = manifestCache ?? (await describe());
if (!manifest.capabilities.some((capability) => capability.id === call.capabilityId)) {
throw new ProviderError('invalid_argument', `APS 没有能力 ${call.capabilityId}`);
}
const payload = {
params: call.params ?? {},
executionId: context.executionId,
deadlineAt: context.deadlineAt,
...(context.identity === undefined ? {} : { identity: { accountId: context.identity.accountId } }),
...(context.idempotencyKey === undefined ? {} : { idempotencyKey: context.idempotencyKey }),
};
let response: Response;
try {
response = await doFetch(`${baseUrl}/api/capabilities/${encodeURIComponent(call.capabilityId)}`, {
method: 'POST',
headers: headers(),
body: JSON.stringify(payload),
signal: AbortSignal.timeout(Math.max(1_000, Math.min(timeoutMs, context.deadlineAt - Date.now()))),
});
} catch (error) {
throw new ProviderError('unavailable', `调用 APS 失败:${error instanceof Error ? error.message : '未知错误'}`);
}
if (!response.ok) {
// 5xx 与服务不可达归 unavailable;4xx 是平台把请求拼错了,归 invalid_argument
const kind = response.status >= 500 ? 'unavailable' : 'invalid_argument';
throw new ProviderError(kind, `APS 返回 HTTP ${response.status}`);
}
const body = (await response.json()) as Partial<ExecutionReceipt> & { errorKind?: unknown };
const status = typeof body.status === 'string' && RECEIPT_STATUSES.has(body.status) ? body.status : 'failed';
return {
executionId: asString(body.executionId, context.executionId),
status: status as ExecutionReceipt['status'],
...(body.result === undefined ? {} : { result: body.result }),
// 分类非法时交给 Core 归一,provider 不猜
...(typeof body.errorKind === 'string' ? { errorKind: body.errorKind } : {}),
...(typeof body.approvalId === 'string' ? { approvalId: body.approvalId } : {}),
};
}
}
async function health(): Promise<HealthStatus> {
{
try {
const response = await doFetch(`${baseUrl}/api/health`, { headers: headers(), signal: AbortSignal.timeout(3_000) });
if (!response.ok) return { status: 'down', detail: `HTTP ${response.status}`, checkedAt: Date.now() };
const body = (await response.json()) as { status?: unknown; detail?: unknown };
const status = body.status === 'degraded' || body.status === 'down' || body.status === 'ok' ? body.status : 'ok';
return {
status,
...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
checkedAt: Date.now(),
};
} catch (error) {
return { status: 'down', detail: error instanceof Error ? error.message : '健康探测失败', checkedAt: Date.now() };
}
}
}
return { describe, invoke, health };
}
/** 供平台按配置加载:模块导出统一的 createProvider,参数来自配置 */
export function createProvider(options: { baseUrl: string; token?: string; timeoutMs?: number }): Provider {
return createApsProvider(options);
}