skill-config-loader.js 4.26 KB
/**
 * skill-config-loader.js — 从 SQLite skills DB 生成统一的运行时配置
 *
 * 替代 agents.json,所有 agent/skill 注册统一由 skill-db.js (SQLite) 管理。
 * 生成与 agents.json 完全一致的 config 结构,下游代码零修改。
 *
 * config = {
 *   agents: { [name]: { type, protocol, transport, launcher, client, prompt, description } },
 *   agentGroups: { [name]: { name, description, agents, ... } },
 *   settings: { maxIterations, timeout, logsDir },
 *   clients: { ... }    // 由 backends.json 提供
 * }
 */

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

/**
 * 从文件系统动态加载 Skill 配置(无需 SQLite)
 * @param {string} skillsCodeDir - 例如 data/skills/
 * @param {object} [backendsConfig] - backends.json
 */
function loadConfigFromSkills(skillsCodeDir, backendsConfig = {}) {
    // 扫描所有 Agent 专属的技能挂载目录 (符合 Local File-System First)
    const dirsToScan = [];
    const agentsRoot = path.resolve(__dirname, '../../../data/system/agents');
    if (fs.existsSync(agentsRoot)) {
        const agentFolders = fs.readdirSync(agentsRoot, { withFileTypes: true });
        for (const f of agentFolders) {
            if (f.isDirectory() && !f.name.startsWith('.')) {
                const claudeSkillsPath = path.join(agentsRoot, f.name, '.claude', 'skills');
                if (fs.existsSync(claudeSkillsPath)) {
                    dirsToScan.push({ base: claudeSkillsPath, isCore: true });
                }
            }
        }
    }

    const agents = {};
    const groups = {};
    const settings = {};

    for (const scan of dirsToScan) {
        if (!fs.existsSync(scan.base)) continue;
        const entries = fs.readdirSync(scan.base, { withFileTypes: true });
        for (const entry of entries) {
            if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
            
            const name = entry.name;
            let type = 'executor';
            let description = '';
            const skillDir = path.join(scan.base, name);
            const promptPath = path.join(skillDir, 'SKILL.md');

            // 尝试读取 SKILL.md 或 metadata.json 获取基本信息
            
            let isOrchestator = false;
            if (fs.existsSync(promptPath)) {
                const content = fs.readFileSync(promptPath, 'utf-8');
                const descMatch = content.match(/description:\s*(?:>|\|)?\s*(.+)/);
                if (descMatch) description = descMatch[1].trim();
                
                // 简单的探查:如果技能本身是大脑编排器
                if (content.includes('layer: orchestrator') || content.includes('Orchestrator') || content.includes('Master')) {
                    isOrchestator = name === 'master' || name === 'innovator';
                }
            }

            if (isOrchestator) {
               agents[name] = { type: 'orchestrator', agentGroup: name, description };
            } else {
               agents[name] = {
                   type,
                   protocol: 'claude-cli',
                   transport: 'pipe',
                   launcher: 'claude-local',
                   client: 'claude-local',
                   prompt: promptPath,
                   description
               };
            }
        }
    }

    // --- 合并 backends 配置 ---
    const config = {
        ...backendsConfig,
        agents,
        agentGroups: groups,
        settings: {
            maxIterations: 10,
            timeout: 600000,
            logsDir: '.autodev-logs',
            ...settings,
        },
    };

    // 桥接 launchers → clients(与原 master-chat.js 逻辑一致)
    if (config.launchers && !config.clients) {
        config.clients = {};
        for (const [name, launcher] of Object.entries(config.launchers)) {
            config.clients[name] = {
                name,
                command: launcher.command,
                acpArgs: launcher.args || [],
                acpProtocol: launcher.protocol || 'acp',
            };
        }
    }

    // 桥接 launcher → client
    for (const agent of Object.values(config.agents)) {
        if (agent.launcher && !agent.client) {
            agent.client = agent.launcher;
        }
    }

    return config;
}

module.exports = { loadConfigFromSkills };