aps-agent/server/agent/pi/extension/aps-bridge.ts

91 lines
3.4 KiB
TypeScript
Raw Normal View History

/**
* 平台能力桥(任务 A3.11):把平台的能力目录注册成 Pi 的工具。
*
* 这个文件由平台以 `--extension <path>` 加载,运行在 Pi 子进程里(Pi 用 jiti 直接读 TS)。
* 它自己不判断权限、不碰凭据,只做一件事:把 Pi 的工具调用转成对平台内部回调端点的 HTTP 请求,
* 平台侧收口到 Core.invoke(架构设计 5.1「每次都过 Core」)。
*
* 配置来自环境变量 APS_BRIDGE_CONFIG 指向的 JSON 文件,由平台在建立会话时写入:
* { baseUrl, internalToken, sessionId, capabilities: [{ toolName, capabilityId, description, parameters }] }
*/
import { readFileSync } from 'node:fs';
interface BridgeCapability {
toolName: string;
capabilityId: string;
description: string;
parameters: Record<string, unknown>;
}
interface BridgeConfig {
baseUrl: string;
internalToken: string;
sessionId: string;
capabilities: BridgeCapability[];
}
interface PlatformReceipt {
executionId: string;
status: 'accepted' | 'pending-confirmation' | 'succeeded' | 'failed';
result?: unknown;
errorKind?: string;
}
/** Pi 扩展的注册接口(只声明用到的部分,避免扩展依赖 Pi 的类型包) */
interface PiExtensionApi {
registerTool(tool: {
name: string;
label: string;
description: string;
parameters: Record<string, unknown>;
execute: (toolCallId: string, params: unknown, signal?: AbortSignal) => Promise<{ content: Array<{ type: 'text'; text: string }>; details: unknown }>;
}): void;
}
export default function apsBridge(pi: PiExtensionApi): void {
const configPath = process.env.APS_BRIDGE_CONFIG ?? '';
if (configPath === '') return;
let config: BridgeConfig;
try {
config = JSON.parse(readFileSync(configPath, 'utf8')) as BridgeConfig;
} catch {
// 配置读不到就不注册任何工具:宁可让模型说做不到,也不要注册一个坏工具
return;
}
for (const capability of config.capabilities) {
pi.registerTool({
name: capability.toolName,
label: capability.capabilityId,
description: capability.description,
parameters: capability.parameters,
async execute(toolCallId: string, params: unknown, signal?: AbortSignal) {
const response = await fetch(`${config.baseUrl}/internal/tool-call`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-aps-internal-token': config.internalToken },
body: JSON.stringify({
sessionId: config.sessionId,
capabilityId: capability.capabilityId,
params,
idempotencyKey: `call-${toolCallId}`,
}),
...(signal === undefined ? {} : { signal }),
});
if (!response.ok) {
const detail = await response.text().catch(() => '');
throw new Error(`平台拒绝调用(HTTP ${response.status}):${detail.slice(0, 200)}`);
}
const receipt = (await response.json()) as PlatformReceipt;
if (receipt.status === 'failed') {
const detail = (receipt.result as { message?: string } | undefined)?.message ?? '未提供原因';
throw new Error(`能力 ${capability.capabilityId} 执行失败(${receipt.errorKind ?? 'unknown'}):${detail}`);
}
// 成功:把回执原样交给模型(含 executionId,便于它引用证据)
return { content: [{ type: 'text', text: JSON.stringify(receipt) }], details: receipt };
},
});
}
}