/** * APS Provider(架构设计第 6 节):把 APS 的能力翻译成平台契约。 * * 只做适配:声明能力、转发调用、归一化回执。不判断权限(那归 APS)、不碰模型上下文。 * * **能力的权威来源是 APS 自己的 `/api/manifest`**(A5.1):平台启动注册时拉一次, * 不在平台侧手抄一份,避免两边漂移。APS 起不来时 `describe()` 直接失败——注册不上, * 比"注册上了但能力是旧的"安全。 */ import { ProviderError, isErrorKind, type AuthExchangeRequest, type AuthExchangeResult, type Capability, type CapabilityCall, type CredentialCheck, 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; } /** 登录方式声明:只认平台定义的三种,别的当没声明 */ function toAuthSpec(raw: unknown): { kind: 'oauth' | 'password-exchange' | 'apikey'; authorizationUrl?: string } | undefined { const item = (raw ?? {}) as Record; const kind = item.kind; if (kind !== 'oauth' && kind !== 'password-exchange' && kind !== 'apikey') return undefined; return { kind, ...(typeof item.authorizationUrl === 'string' ? { authorizationUrl: item.authorizationUrl } : {}), }; } /** APS 的能力声明与平台 provider-sdk 的 Capability 同形,这里只做字段校验与兜底 */ function toCapability(raw: unknown): Capability { const item = (raw ?? {}) as Record; 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) ?? { type: 'object' }, outputSchema: (item.outputSchema as Record) ?? { 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 => ({ 'content-type': 'application/json', ...(options.token === undefined ? {} : { authorization: `Bearer ${options.token}` }), }); let manifestCache: ProviderManifest | undefined; async function describe(): Promise { { 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, // 登录方式要透传:平台按它决定绑定流程(ADR-0012) ...(toAuthSpec(body.auth) === undefined ? {} : { auth: toAuthSpec(body.auth) as NonNullable }), capabilities: raw.map(toCapability), }; manifestCache = described; return described; } } async function invoke(context: ExecutionContext, call: CapabilityCall): Promise { { // 能力白名单: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 }), }; // 登录态只从一次性句柄里取(A4.3),不进参数、不进日志 const secret = context.credentials === undefined ? undefined : await context.credentials.read(); const authHeaders = secret?.token === undefined ? headers() : { ...headers(), authorization: `Bearer ${secret.token}` }; let response: Response; try { response = await doFetch(`${baseUrl}/api/capabilities/${encodeURIComponent(call.capabilityId)}`, { method: 'POST', headers: authHeaders, 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) { if (response.status === 401 || response.status === 403) { // 业务系统说"你没登录/没权限":平台不覆盖这个结论,交给策略层把绑定标成待重绑 throw new ProviderError('forbidden', `APS 拒绝了这次调用(HTTP ${response.status}):登录态已失效或权限不足,请重新绑定`); } const kind = response.status >= 500 ? 'unavailable' : 'invalid_argument'; throw new ProviderError(kind, `APS 返回 HTTP ${response.status}`); } const body = (await response.json()) as Partial & { 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 { { 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() }; } } } async function exchange(request: AuthExchangeRequest): Promise { if (request.password === undefined) { throw new ProviderError('invalid_argument', 'APS 走账号密码换取 token,绑定时要给一次密码'); } let response: Response; try { response = await doFetch(`${baseUrl}/api/auth/login`, { method: 'POST', headers: headers(), body: JSON.stringify({ accountId: request.accountId, password: request.password }), signal: AbortSignal.timeout(timeoutMs), }); } catch (error) { throw new ProviderError('unavailable', `连不上 APS:${error instanceof Error ? error.message : '未知错误'}`); } if (response.status === 401) { throw new ProviderError('forbidden', 'APS 拒绝了这组账号密码'); } if (!response.ok) { throw new ProviderError(response.status >= 500 ? 'unavailable' : 'invalid_argument', `APS 登录返回 HTTP ${response.status}`); } const body = (await response.json()) as { token?: unknown; accountId?: unknown; displayName?: unknown; expiresAt?: unknown }; if (typeof body.token !== 'string' || body.token === '') { throw new ProviderError('unknown_result', 'APS 登录成功但没返回 token'); } return { accountId: asString(body.accountId, request.accountId), ...(typeof body.displayName === 'string' ? { displayName: body.displayName } : {}), secret: { token: body.token }, ...(typeof body.expiresAt === 'number' ? { expiresAt: body.expiresAt } : {}), }; } async function checkCredential(secret: Record): Promise { if (secret.token === undefined) return { ok: false, reason: '凭据里没有 token' }; try { const response = await doFetch(`${baseUrl}/api/auth/whoami`, { headers: { ...headers(), authorization: `Bearer ${secret.token}` }, signal: AbortSignal.timeout(3_000), }); if (response.status === 401 || response.status === 403) return { ok: false, reason: '登录态已失效,请重新绑定' }; if (!response.ok) return { ok: false, reason: `APS 返回 HTTP ${response.status}` }; return { ok: true }; } catch (error) { // 连不上不等于登录态失效:报 ok=false 会误伤,这里给出原因让上层决定 return { ok: false, reason: error instanceof Error ? error.message : '凭据体检失败' }; } } return { describe, invoke, health, exchange, checkCredential }; } /** 供平台按配置加载:模块导出统一的 createProvider,参数来自配置 */ export function createProvider(options: { baseUrl: string; token?: string; timeoutMs?: number }): Provider { return createApsProvider(options); }