码桶
发现社区成员的开源项目
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,
};