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

91 lines
3.4 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.

/**
* 平台能力桥(任务 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 };
},
});
}
}