码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
index.js19.8 KB
/**
 * 插件:技能系统(Skill)
 * --------------------------------------------------
 * 允许每个机器人安装技能包,技能包提供:
 *   1. 系统提示扩展 — 注入额外的指令/角色设定
 *   2. 自定义工具 — 技能包可声明 OpenAI function schema 与执行函数
 *   3. 预设对话模板 — 快速调起特定对话场景
 *
 * 内置技能市场(skill registry)包含一些实用技能,
 * 也支持用户自定义技能(通过控制台创建/编辑/删除)。
 *
 * 使用方式:
 *   发送「技能」或「skill」查看已安装技能列表
 *   发送「技能 安装 名称」安装技能
 *   发送「技能 卸载 名称」卸载技能
 *   在控制台「技能配置」中管理安装的技能 / 创建自定义技能
 */
const db = require('../../lib/db');

// ==================== 内置技能注册表 ====================

/**
 * 技能定义:
 *   id          - 唯一标识
 *   name        - 显示名称
 *   icon        - 图标
 *   description - 简介
 *   prompt      - 注入到系统提示的指令(可选,null 表示无)
 *   tools       - AI 工具定义(可选,OpenAI function schema 数组)
 *   handler     - 工具执行函数(toolName, args) => string(可选)
 */
const SKILL_REGISTRY = {
  'dev-helper': {
    id: 'dev-helper',
    name: '开发助手',
    icon: 'DEV',
    description: '提供代码解读、Bug 分析、API 文档查询等开发辅助能力',
    prompt: '你是一位资深软件工程师。回答编程问题时,提供代码示例和技术细节,并在合适时给出最佳实践建议。回答简洁专业,避免无关讨论。',
  },

  'translator': {
    id: 'translator',
    name: '翻译官',
    icon: 'WEB',
    description: '精通多语种翻译,支持中英日韩法德等多种语言互译',
    prompt: '你是一位专业翻译。当用户要求翻译时,只返回翻译结果,格式如下:\n原文:<原文>\n译文:<翻译>\n若用户未指定目标语言,默认译为中文。支持中英日韩法德西俄互译。',
  },

  'writing-pro': {
    id: 'writing-pro',
    name: '写作助手',
    icon: 'WRT',
    description: '文案润色、改写、扩写、缩写、公文写作等',
    prompt: '你是一位专业文字编辑和写手。帮助用户润色文案时,保持原意的基础上优化表达,去除冗余,增强感染力。回复时先给出优化后的版本,再附简要修改说明。',
  },

  'data-analyst': {
    id: 'data-analyst',
    name: '数据分析师',
    icon: 'DT',
    description: '解读数据、生成图表描述、统计分析',
    prompt: '你是一位数据分析师。面对用户提供的数据,先分析结构和质量,再给出洞察和建议。使用表格总结关键指标,用通俗语言解释统计含义。',
    tools: [
      {
        type: 'function',
        function: {
          name: 'analyze_numbers',
          description: '对一串数字进行基本统计分析(求和、平均值、中位数、最大、最小、标准差)',
          parameters: {
            type: 'object',
            properties: {
              numbers: { type: 'string', description: '逗号或空格分隔的数字序列,如 "1,2,3,4,5"' },
            },
            required: ['numbers'],
          },
        },
      },
    ],
    handler(toolName, args) {
      if (toolName === 'analyze_numbers') {
        const nums = (args.numbers || '')
          .split(/[,\s]+/)
          .map(Number)
          .filter(n => !isNaN(n));
        if (nums.length === 0) return '未提供有效数字。';
        const sum = nums.reduce((a, b) => a + b, 0);
        const avg = sum / nums.length;
        const sorted = [...nums].sort((a, b) => a - b);
        const median = nums.length % 2 === 0
          ? (sorted[nums.length / 2 - 1] + sorted[nums.length / 2]) / 2
          : sorted[Math.floor(nums.length / 2)];
        const variance = nums.reduce((s, n) => s + (n - avg) ** 2, 0) / nums.length;
        const std = Math.sqrt(variance);
        return [
          `[Stat] 统计分析(${nums.length} 个数字)`,
          `总和:${sum}`,
          `平均:${avg.toFixed(2)}`,
          `中位数:${median}`,
          `最大:${Math.max(...nums)},最小:${Math.min(...nums)}`,
          `标准差:${std.toFixed(2)}`,
        ].join('\n');
      }
      return `未知工具: ${toolName}`;
    },
  },

  'schedule-bot': {
    id: 'schedule-bot',
    name: '日程管家',
    icon: 'CL',
    description: '帮助用户管理日程、日期计算、时间提醒',
    prompt: '你是日程管理助手。帮助用户规划时间、计算日期、设置提醒。使用 reminder 工具来设置提醒。回答时先确认用户的时间意图,再给出建议。',
  },

  'food-explorer': {
    id: 'food-explorer',
    name: '美食探索',
    icon: 'FD',
    description: '推荐菜谱、解读食材、搭配建议',
    prompt: '你是一位美食家和营养顾问。根据用户需求推荐菜谱,解读食材功效,提供搭配建议。回答有趣又实用,可以附上简单步骤。',
  },
};

// ==================== 缓存系统 ====================

const skillPromptCache = new Map(); // botId -> combined prompt string
const skillToolsCache = new Map();  // botId -> {tools, handlers}

/** 用户自定义技能缓存(userId → { version, skills }) */
const userSkillsCache = new Map();
/** 用户技能版本号(userId → version,自定义技能变更时递增以失效缓存) */
const userSkillsVersion = new Map();

// 每 10 分钟清理已删除 bot/user 的缓存条目(防内存泄漏)
setInterval(async () => {
  try {
    const allBots = await db.rows('SELECT id FROM bots');
    const validBotIds = new Set(allBots.map(b => b.id));
    for (const botId of skillPromptCache.keys()) {
      if (!validBotIds.has(botId)) skillPromptCache.delete(botId);
    }
    for (const botId of skillToolsCache.keys()) {
      if (!validBotIds.has(botId)) skillToolsCache.delete(botId);
    }
    // userSkillsCache/Version 依赖 userId,user 删除场景极少,但一并清理
    const allUsers = await db.rows('SELECT id FROM users');
    const validUserIds = new Set(allUsers.map(u => u.id));
    for (const uid of userSkillsCache.keys()) {
      if (!validUserIds.has(uid)) { userSkillsCache.delete(uid); userSkillsVersion.delete(uid); }
    }
  } catch (_) { /* 静默 */ }
}, 10 * 60 * 1000).unref();

/** 加载用户的所有可用技能(内置 + 自定义),带版本缓存 */
async function loadUserSkills(userId) {
  if (!userId) return SKILL_REGISTRY;
  const version = userSkillsVersion.get(userId) || 0;
  if (userSkillsCache.has(userId)) {
    const cached = userSkillsCache.get(userId);
    if (cached.version === version) return cached.skills;
  }
  const skills = { ...SKILL_REGISTRY };
  try {
    const customs = await db.rows(
      'SELECT id, name, icon, description, prompt FROM custom_skills WHERE user_id = ? ORDER BY id',
      [userId]
    );
    for (const s of customs) {
      skills['custom_' + s.id] = {
        id: 'custom_' + s.id,
        name: s.name,
        icon: s.icon || '',
        description: s.description || '',
        prompt: s.prompt || '',
        isCustom: true,
        _dbId: s.id,
      };
    }
  } catch (_) { /* 表可能尚未创建 */ }
  userSkillsCache.set(userId, { version, skills });
  return skills;
}

/** 失效指定用户的技能缓存 */
function invalidateUserSkillCache(userId) {
  userSkillsCache.delete(userId);
  userSkillsVersion.set(userId, (userSkillsVersion.get(userId) || 0) + 1);
  // 同时失效该用户所有 bot 的 prompt/tools 缓存
  db.rows('SELECT id FROM bots WHERE user_id = ?', [userId]).then(bots => {
    for (const b of bots) {
      skillPromptCache.delete(b.id);
      skillToolsCache.delete(b.id);
    }
  }).catch(() => {});
}

// ==================== 自定义技能 CRUD ====================

async function createCustomSkill(userId, { name, icon, description, prompt }) {
  if (!name || !name.trim()) return { ok: false, msg: '技能名称不能为空' };
  if (!prompt || !prompt.trim()) return { ok: false, msg: '技能提示词不能为空' };
  try {
    await db.exec(
      'INSERT INTO custom_skills (user_id, name, icon, description, prompt, created_at) VALUES (?,?,?,?,?,?)',
      [userId, name.trim(), icon || '', (description || '').trim(), prompt.trim(), Math.floor(Date.now() / 1000)]
    );
    const id = await db.lastInsertId();
    invalidateUserSkillCache(userId);
    return { ok: true, id };
  } catch (e) {
    return { ok: false, msg: e.message };
  }
}

async function updateCustomSkill(userId, id, { name, icon, description, prompt }) {
  if (!name || !name.trim()) return { ok: false, msg: '技能名称不能为空' };
  if (!prompt || !prompt.trim()) return { ok: false, msg: '技能提示词不能为空' };
  try {
    const r = await db.exec(
      'UPDATE custom_skills SET name=?, icon=?, description=?, prompt=?, updated_at=? WHERE id=? AND user_id=?',
      [name.trim(), icon || '', (description || '').trim(), prompt.trim(), Math.floor(Date.now() / 1000), id, userId]
    );
    if (!r.changes) return { ok: false, msg: '技能不存在或无权操作' };
    invalidateUserSkillCache(userId);
    return { ok: true };
  } catch (e) {
    return { ok: false, msg: e.message };
  }
}

async function deleteCustomSkill(userId, id) {
  try {
    // 先查出受影响的行,用于清理已安装列表
    const skill = await db.row('SELECT id FROM custom_skills WHERE id=? AND user_id=?', [id, userId]);
    if (!skill) return { ok: false, msg: '技能不存在或无权操作' };
    await db.exec('DELETE FROM custom_skills WHERE id=?', [id]);

    // 把该技能从所有 bots 的已安装列表中移除
    const sid = 'custom_' + id;
    const bots = await db.rows('SELECT id, user_id FROM bots WHERE user_id = ?', [userId]);
    for (const bot of bots) {
      const installed = await getInstalledSkills(bot.id);
      if (installed.includes(sid)) {
        await saveInstalledSkills(bot.id, installed.filter(s => s !== sid));
      }
    }
    invalidateUserSkillCache(userId);
    return { ok: true };
  } catch (e) {
    return { ok: false, msg: e.message };
  }
}

async function listCustomSkills(userId) {
  try {
    return await db.rows('SELECT * FROM custom_skills WHERE user_id = ? ORDER BY id', [userId]);
  } catch (_) {
    return [];
  }
}

// ==================== 技能列表/安装管理 ====================

/** 获取某机器人已安装的技能 ID 列表 */
async function getInstalledSkills(botId) {
  try {
    const row = await db.row(
      'SELECT config_value FROM plugin_settings WHERE bot_id=? AND plugin_id=? AND config_key=?',
      [botId, 'skill', 'installed']
    );
    if (row && row.config_value) {
      try {
        const list = JSON.parse(row.config_value);
        return Array.isArray(list) ? list : [];
      } catch (_) {
        return [];
      }
    }
  } catch (e) {
    console.error('[skill] getInstalledSkills error:', e.message);
  }
  return [];
}

/** 保存某机器人已安装的技能 */
async function saveInstalledSkills(botId, skillIds) {
  await db.exec(
    'DELETE FROM plugin_settings WHERE bot_id=? AND plugin_id=? AND config_key=?',
    [botId, 'skill', 'installed']
  );
  if (skillIds.length) {
    await db.exec(
      'INSERT INTO plugin_settings (bot_id, plugin_id, config_key, config_value) VALUES (?,?,?,?)',
      [botId, 'skill', 'installed', JSON.stringify(skillIds)]
    );
  }
  // 刷新缓存
  skillPromptCache.delete(botId);
  skillToolsCache.delete(botId);
}

/** 获取指定 bot 所属用户的完整技能列表(内置 + 自定义),含安装状态 */
async function getUserSkillList(botId) {
  const bot = await db.row('SELECT user_id FROM bots WHERE id = ?', [botId]);
  if (!bot) return [];
  const allSkills = await loadUserSkills(bot.user_id);
  const installed = await getInstalledSkills(botId);
  const installedSet = new Set(installed);
  return Object.values(allSkills).map(s => ({
    id: s.id,
    name: s.name,
    icon: s.icon,
    description: s.description,
    isCustom: !!s.isCustom,
    installed: installedSet.has(s.id),
  }));
}

// ==================== AI 核心逻辑 ====================

/** 获取 bot 所属用户 ID */
async function getBotUserId(botId) {
  const bot = await db.row('SELECT user_id FROM bots WHERE id = ?', [botId]);
  return bot ? bot.user_id : null;
}

/** 获取所有技能的组合 prompt */
async function getCombinedPrompt(botId) {

  if (skillPromptCache.has(botId)) return skillPromptCache.get(botId);

  const installed = await getInstalledSkills(botId);
  if (!installed.length) {
    skillPromptCache.set(botId, '');
    return '';
  }

  const userId = await getBotUserId(botId);
  const allSkills = await loadUserSkills(userId);

  const parts = [];
  for (const sid of installed) {
    const skill = allSkills[sid];
    if (skill && skill.prompt) {
      parts.push(`[技能:${skill.name}]\n${skill.prompt}`);
    }
  }

  const result = parts.length
    ? '\n\n## 已启用的技能\n\n' + parts.join('\n\n')
    : '';

  skillPromptCache.set(botId, result);
  return result;
}

/** 获取所有技能的 AI 工具 */
async function getSkillTools(botId) {
  if (skillToolsCache.has(botId)) return skillToolsCache.get(botId);

  const userId = await getBotUserId(botId);
  const allSkills = userId ? await loadUserSkills(userId) : SKILL_REGISTRY;
  const installed = await getInstalledSkills(botId);
  const tools = [];
  const handlers = {};

  for (const sid of installed) {
    const skill = allSkills[sid];
    if (skill && Array.isArray(skill.tools)) {
      for (const t of skill.tools) {
        const fnName = t?.function?.name;
        if (!fnName) continue;
        tools.push(t);
        handlers[fnName] = (name, args) => skill.handler(name, args);
      }
    }
  }

  const result = { tools, handlers };
  skillToolsCache.set(botId, result);
  return result;
}

// ==================== 插件元数据 ====================

const meta = {
  id: 'skill',
  name: '技能系统',
  category: 'AI对话',
  description: '安装各种技能包,扩展机器人的专业能力(开发助手、翻译官、写作助手、数据分析师等)+ 支持自定义技能上传',
  usage: '发送「技能」查看已安装技能列表;\n发送「技能 市场」查看可用技能;\n发送「技能 安装 名称」安装技能;\n发送「技能 卸载 名称」卸载技能。\n\n安装后,智能助手自动获得对应技能的专业能力。',
  builtin: true,
  configurable: true,
  customConfig: 'skill',
};

// ==================== 消息处理 ====================

async function onMessage(msg, ctx) {
  const text = (msg.text || msg.content || '').trim();
  const botId = ctx.bot.id;
  const userId = ctx.bot.user_id;

  const allSkills = await loadUserSkills(userId);

  // 命令:无参数「技能」/「skill」→ 列出已安装
  if (/^(技能|skill)$/i.test(text)) {
    const installed = await getInstalledSkills(botId);
    if (!installed.length) {
      await ctx.sendText('[Skill] 当前未安装任何技能。\n\n发送「技能 市场」查看可用技能列表。\n发送「技能 安装 名称」安装技能。');
      return true;
    }

    const lines = ['[Skill] 已安装的技能:', ''];
    for (const sid of installed) {
      const s = allSkills[sid];
      if (s) {
        const tag = s.isCustom ? ' [自定义]' : '';
        lines.push(`${s.icon} **${s.name}** — ${s.description}${tag}`);
      }
    }
    lines.push('');
    lines.push('安装的技能会自动增强智能助手的能力。');
    lines.push('发送「技能 市场」查看可安装的技能。');
    await ctx.sendText(lines.join('\n'));
    return true;
  }

  // 命令:技能 市场
  if (/^(技能|skill)\s*(市场|market|列表|list)/i.test(text)) {
    const installed = await getInstalledSkills(botId);
    const all = Object.values(allSkills).map(s => ({ id: s.id, name: s.name, icon: s.icon, description: s.description, isCustom: s.isCustom }));
    const lines = ['[Market] 技能市场:', ''];

    for (const s of all) {
      const isInstalled = installed.includes(s.id);
      const status = isInstalled ? '[Installed] 已安装' : '[Available] 可安装';
      const tag = s.isCustom ? ' [自定义]' : '';
      lines.push(`${s.icon} **${s.name}** (${s.id}) ${status}${tag}`);
      lines.push(`   ${s.description}`);
      lines.push('');
    }

    lines.push('发送「技能 安装 名称或ID」安装技能。');
    lines.push('发送「技能 卸载 名称或ID」卸载技能。');
    await ctx.sendText(lines.join('\n'));
    return true;
  }

  // 命令:技能 安装 <名称或ID>
  const installMatch = text.match(/^(技能|skill)\s*(安装|install|add)\s+(.+)/i);
  if (installMatch) {
    const target = installMatch[3].trim();
    let skill = allSkills[target];
    if (!skill) {
      // 按名称模糊搜索
      for (const s of Object.values(allSkills)) {
        if (s.name === target) {
          skill = s;
          break;
        }
      }
    }
    if (!skill) {
      const available = Object.values(allSkills).map(s => `${s.icon}${s.name} (${s.id})`).join('\n');
      await ctx.sendText(`[NG] 未找到技能「${target}」。\n\n可用技能:\n${available}\n\n发送「技能 市场」查看更多。`);
      return true;
    }

    const installed = await getInstalledSkills(botId);
    if (installed.includes(skill.id)) {
      await ctx.sendText(`[WARN] 技能「${skill.name}」已安装,无需重复安装。`);
      return true;
    }

    installed.push(skill.id);
    await saveInstalledSkills(botId, installed);
    await ctx.sendText(`[Installed] 已安装技能:${skill.icon} **${skill.name}**\n${skill.description}`);
    return true;
  }

  // 命令:技能 卸载 <名称或ID>
  const uninstallMatch = text.match(/^(技能|skill)\s*(卸载|uninstall|remove|del)\s+(.+)/i);
  if (uninstallMatch) {
    const target = uninstallMatch[3].trim();
    let skill = allSkills[target];
    if (!skill) {
      for (const s of Object.values(allSkills)) {
        if (s.name === target) {
          skill = s;
          break;
        }
      }
    }

    const installed = await getInstalledSkills(botId);
    const sid = skill ? skill.id : target;

    if (!installed.includes(sid)) {
      await ctx.sendText(`[WARN] 技能「${target}」尚未安装。`);
      return true;
    }

    const name = skill ? skill.name : target;
    const newList = installed.filter(s => s !== sid);
    await saveInstalledSkills(botId, newList);
    await ctx.sendText(`[Installed] 已卸载技能:${skill?.icon || '-'} ${name}`);
    return true;
  }

  return false;
}

// ==================== AI 工具集成 ====================

/** 返回当前机器人技能系统的 AI 工具 */
async function getAiTools(botId) {
  const { tools } = await getSkillTools(botId);
  return tools;
}

/** 执行技能工具 */
async function handleAiTool(toolName, args, ctx) {
  const { handlers } = await getSkillTools(ctx.bot.id);
  if (handlers[toolName]) {
    return handlers[toolName](toolName, args);
  }
  return `技能工具 "${toolName}" 未找到执行句柄。`;
}

// ==================== 导出 ====================

module.exports = {
  meta,
  getAiTools,
  handleAiTool,
  onMessage,
  SKILL_REGISTRY,
  getInstalledSkills,
  saveInstalledSkills,
  getCombinedPrompt,
  getSkillTools,
  loadUserSkills,
  getUserSkillList,
  createCustomSkill,
  updateCustomSkill,
  deleteCustomSkill,
  listCustomSkills,
};