tool-registry.js 2.76 KB
/**
 * DevLoop Tool Registry — loadAllTools()
 *
 * 把 server/src/tools/devloop/ 下所有 *-tools.js 加载并按工具 name 聚合到 Map。
 * tool-manager / ClaudeCliProtocol 启动 Agent 时通过 toolPolicy 取这里的 ToolDef。
 *
 * 规格: docs/prd/newfunction/_spec/arch-tool-registry.md · §④
 */

const path = require('path');
const fs = require('fs');

// 工具模块清单(手动 require,避免动态 require 出错)
const TOOL_MODULES = [
  './persistence-tools',
  './doc-tools',
  './coder-tools',
  './tester-tools',
  './integrator-tools',
  './triager-tools',
];

let cached = null;

function loadAllTools() {
  if (cached) return cached;

  const registry = new Map();

  for (const mod of TOOL_MODULES) {
    const fullPath = path.join(__dirname, mod + '.js');
    if (!fs.existsSync(fullPath)) continue;
    const tools = require(mod);
    for (const key of Object.keys(tools)) {
      const tool = tools[key];
      if (tool && typeof tool === 'object' && tool.name && typeof tool.execute === 'function') {
        if (registry.has(tool.name)) {
          throw new Error(`Duplicate tool name: ${tool.name} (in ${mod})`);
        }
        registry.set(tool.name, tool);
      }
    }
  }

  cached = registry;
  return registry;
}

/**
 * 列工具名(用于 _registry.json toolPolicy 校验)
 */
function listToolNames() {
  return Array.from(loadAllTools().keys()).sort();
}

/**
 * 按 toolPolicy 取一组工具(含 LemonBoss 既有工具是允许的,本函数只过滤 devloop 的)
 */
function getDevLoopTools(toolPolicy) {
  const all = loadAllTools();
  const out = [];
  for (const name of toolPolicy) {
    if (all.has(name)) out.push(all.get(name));
  }
  return out;
}

/**
 * 调度:根据 name 找工具,执行(中央调用入口,外部 ACP 调用方使用)。
 * - 校验工具存在
 * - 校验 input 符合 inputSchema(简化版:只校验 required + type)
 * - 调 tool.execute(input, ctx)
 * - 错误标准化
 */
async function dispatch(name, input, ctx) {
  const all = loadAllTools();
  const tool = all.get(name);
  if (!tool) {
    throw new Error(`Unknown tool: ${name}`);
  }
  _validateInput(input, tool.inputSchema, name);
  return tool.execute(input, ctx);
}

function _validateInput(input, schema, name) {
  if (!schema || typeof schema !== 'object') return;
  if (schema.required && Array.isArray(schema.required)) {
    for (const k of schema.required) {
      if (input == null || input[k] === undefined) {
        const e = new Error(`Tool ${name}: missing required field "${k}"`);
        e.code = 'TOOL_INPUT_INVALID';
        throw e;
      }
    }
  }
}

function _resetForTesting() {
  cached = null;
}

module.exports = {
  loadAllTools,
  listToolNames,
  getDevLoopTools,
  dispatch,
  _resetForTesting,
  TOOL_MODULES,
};