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