92 lines
3.4 KiB
TypeScript
92 lines
3.4 KiB
TypeScript
// 探索工具 server 的框架扩展(阶段 D)。
|
||
//
|
||
// 边界合同中的工具层:给创作智能体暴露只读探索工具。本扩展不自带任何查询逻辑,
|
||
// 工具登记表与实现都在 read_tools.py(Python,只读连接、未知工具拒绝、结果有界)。
|
||
// 加载时执行 `read_tools.py --list` 取登记表,按登记表动态注册工具,保证单一事实源;
|
||
// 每次工具执行把参数原样转给 `read_tools.py <工具名> '<json>'`,stdout 即结果。
|
||
//
|
||
// 派发器经环境变量定位实现:MUSE_READ_TOOLS_PYTHON(解释器)、MUSE_READ_TOOLS_SCRIPT(脚本)。
|
||
// 未经派发器注入这两个变量时不注册任何工具(静默跳过),隔离会话不受影响。
|
||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||
import { Type } from "typebox";
|
||
|
||
interface ToolSpec {
|
||
description: string;
|
||
tables: string[];
|
||
args: Record<string, "int" | "str">;
|
||
}
|
||
|
||
function runReadTools(python: string, script: string, argv: string[]): string {
|
||
const bun = (globalThis as { Bun?: any }).Bun;
|
||
if (bun?.spawnSync) {
|
||
const res = bun.spawnSync([python, script, ...argv], {
|
||
stdout: "pipe",
|
||
stderr: "pipe",
|
||
});
|
||
const stdout = new TextDecoder().decode(res.stdout);
|
||
if (res.exitCode !== 0) {
|
||
const stderr = new TextDecoder().decode(res.stderr).trim();
|
||
throw new Error(stderr || `read_tools 退出码 ${res.exitCode}`);
|
||
}
|
||
return stdout;
|
||
}
|
||
// Node 运行时的回退路径。
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||
const { execFileSync } = require("node:child_process");
|
||
return execFileSync(python, [script, ...argv], {
|
||
encoding: "utf-8",
|
||
stdio: ["ignore", "pipe", "pipe"],
|
||
});
|
||
}
|
||
|
||
export default function (pi: ExtensionAPI) {
|
||
const python = process.env.MUSE_READ_TOOLS_PYTHON;
|
||
const script = process.env.MUSE_READ_TOOLS_SCRIPT;
|
||
if (!python || !script) {
|
||
return; // 非工具 server 派发:不注册任何工具。
|
||
}
|
||
|
||
let registry: Record<string, ToolSpec> = {};
|
||
try {
|
||
registry = JSON.parse(runReadTools(python, script, ["--list"]));
|
||
} catch {
|
||
return; // 登记表不可读时保持无工具,不放行裸查询。
|
||
}
|
||
|
||
for (const [name, spec] of Object.entries(registry)) {
|
||
const props: Record<string, any> = {};
|
||
for (const [rawName, argType] of Object.entries(spec.args ?? {})) {
|
||
const optional = rawName.endsWith("?");
|
||
const argName = optional ? rawName.slice(0, -1) : rawName;
|
||
const base =
|
||
argType === "int"
|
||
? Type.Integer({ description: argName })
|
||
: Type.String({ description: argName });
|
||
props[argName] = optional ? Type.Optional(base) : base;
|
||
}
|
||
pi.registerTool({
|
||
name,
|
||
label: name,
|
||
description: `${spec.description}(只读;读取 ${spec.tables.join("、")})`,
|
||
promptGuidelines: [
|
||
`Use ${name} only for read-only exploration; it never modifies data.`,
|
||
],
|
||
parameters: Type.Object(props),
|
||
async execute(_toolCallId, params) {
|
||
try {
|
||
const stdout = runReadTools(python, script, [
|
||
name,
|
||
JSON.stringify(params ?? {}),
|
||
]);
|
||
return { content: [{ type: "text", text: stdout }] };
|
||
} catch (error) {
|
||
// 抛出错误使框架把该工具调用标记为失败,模型可据此调整探索。
|
||
throw new Error(
|
||
error instanceof Error ? error.message : String(error)
|
||
);
|
||
}
|
||
},
|
||
});
|
||
}
|
||
}
|