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 };
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|