码桶
发现社区成员的开源项目
index.js9.8 KB
/**
* 内置插件:OpenClaw AI Agent 对话
* --------------------------------------------------
* 通过 OpenClaw Gateway 的 OpenAI 兼容端点进行 AI Agent 交互。
* Token 与 AI 对话共享额度。
*
* 配置优先级(通过 resolveClawConfig 解析):
* 1. 机器人自定义 API(plugin_settings: use_custom_claw=1 + claw_api_base + claw_api_key)
* 2. 管理员全局配置(settings 表: claw_api_base / claw_api_key / claw_model …)
*
* 每机器人用户配置(trigger、session_key、以及自定义 API)存在 plugin_settings 表。
*/
const axios = require('axios');
const db = require('../../lib/db');
const settings = require('../../lib/settings');
/** 读取全局 OpenClaw 配置(管理员后台设置) */
async function loadGlobalClawConfig() {
return {
api_base: (await settings.getSetting('claw_api_base', '')).replace(/\/+$/, '').replace(/\/v1\/?$/, ''),
api_key: await settings.getSetting('claw_api_key', ''),
model: await settings.getSetting('claw_model', 'openclaw/default'),
backend_model: await settings.getSetting('claw_backend_model', ''),
system_prompt: await settings.getSetting('claw_system_prompt', ''),
max_tokens: parseInt(await settings.getSetting('claw_max_tokens', '2000'), 10) || 2000,
temperature: parseFloat(await settings.getSetting('claw_temperature', '0.7')) || 0.7,
};
}
/** 读取该机器人的用户配置(trigger、session_key、自定义 API 等) */
async function loadUserConfig(botId) {
const rows = await db.rows(
'SELECT config_key, config_value FROM plugin_settings WHERE bot_id=? AND plugin_id=?',
[botId, 'openclaw']
);
const cfg = {};
rows.forEach(r => { cfg[r.config_key] = r.config_value; });
return cfg;
}
/**
* 解析 OpenClaw 的 API 接口来源:
* - 若机器人配置了「自定义接口」(use_custom_claw=1 且基地址/密钥齐全) → 使用自定义接口
* - 否则回退到管理员在后台统一配置的全局 claw_* 设置
*/
async function resolveClawConfig(botId) {
const global = await loadGlobalClawConfig();
const bot = await loadUserConfig(botId);
if (bot.use_custom_claw === '1' && bot.claw_api_base && bot.claw_api_key) {
return {
custom: true,
api_base: bot.claw_api_base.replace(/\/+$/, '').replace(/\/v1\/?$/, ''),
api_key: bot.claw_api_key,
model: (bot.claw_model || 'openclaw/default').trim(),
backend_model: (bot.claw_backend_model || '').trim(),
system_prompt: bot.claw_system_prompt || '',
max_tokens: parseInt(bot.claw_max_tokens, 10) || 2000,
temperature: parseFloat(bot.claw_temperature) || 0.7,
};
}
return { ...global, custom: false };
}
/** 检查触发词 */
function matchTrigger(text, trigger) {
if (!trigger && trigger !== '') trigger = 'claw';
if (!trigger) return text;
const prefixes = [trigger + ' ', trigger + ',', '/' + trigger + ' '];
for (const p of prefixes) {
if (text.startsWith(p)) return text.slice(p.length).trim();
}
return null;
}
/** 获取用户 Token 用量(开源版:不限额) */
async function getUserTokenInfo(userId) {
return { used: 0, limit: Number.MAX_SAFE_INTEGER };
}
/** 增加用户 Token 用量(同步写入每日用量热力图表) */
async function addUserTokens(userId, tokens) {
await db.exec(
'UPDATE users SET ai_tokens_used = ai_tokens_used + ? WHERE id = ?',
[tokens, userId]
);
const d = new Date();
const day = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
try {
await db.exec(
'INSERT INTO token_usage(user_id, day, tokens) VALUES(?, ?, ?) ON CONFLICT(user_id, day) DO UPDATE SET tokens = tokens + excluded.tokens',
[userId, day, tokens]
);
} catch (e) { /* 热力图统计为非关键路径,失败忽略 */ }
}
module.exports = {
meta: {
id: 'openclaw',
name: 'OpenClaw Agent',
version: '2.0.0',
author: '奶狗',
category: 'AI对话',
description: '对接 OpenClaw AI Agent 框架。每个机器人可单独设置触发词、Gateway 接口与模型;不设置则使用管理员全局配置。Token 与 AI 对话共享额度。',
entry: 'openclaw/index.js',
builtin: true,
// 字段化配置:每个机器人各存一份(plugin_settings 按 bot_id + plugin_id 隔离)
settingsSchema: [
{ key: 'trigger', label: '触发词', type: 'text', placeholder: 'claw', help: '发送「触发词 你的问题」即调用。留空默认 claw。' },
{ key: 'use_custom_claw', label: '使用自定义接口', type: 'switch', help: '开启后使用下方自定义 Gateway;关闭则用管理员全局配置。' },
{ key: 'claw_api_base', label: 'Gateway 地址', type: 'text', placeholder: 'https://your-gateway.com', help: '自定义 OpenClaw Gateway 基地址(无需带 /v1)。' },
{ key: 'claw_api_key', label: '网关令牌', type: 'password', placeholder: 'Bearer 令牌', help: '自定义接口的 API Key。' },
{ key: 'claw_model', label: '模型', type: 'text', placeholder: 'openclaw/default', help: '请求使用的模型名。' },
{ key: 'claw_backend_model', label: '后端模型', type: 'text', placeholder: '可选', help: '透传 x-openclaw-model 头,留空则不发送。' },
{ key: 'session_key', label: '会话密钥', type: 'text', placeholder: '可选', help: '透传 x-openclaw-session-key 头,用于会话隔离。' },
{ key: 'claw_system_prompt', label: '系统提示词', type: 'textarea', placeholder: '可选', help: '每次对话前置的 system 消息。' },
{ key: 'claw_max_tokens', label: '最大 Token', type: 'number', placeholder: '2000', help: '单次回复最大 Token 数,默认 2000。' },
{ key: 'claw_temperature', label: '温度', type: 'number', placeholder: '0.7', help: '采样温度 0~2,默认 0.7。' },
],
},
async onMessage(msg, ctx) {
const botId = ctx.bot.id;
const userId = ctx.bot.user_id;
const text = (msg.content || '').trim();
if (!text) return false;
const ucfg = await loadUserConfig(botId);
const trigger = ucfg.trigger !== undefined ? ucfg.trigger : 'claw';
const session_key = ucfg.session_key || '';
const prompt = matchTrigger(text, trigger);
if (prompt === null) return false;
// 解析 API 配置:优先使用机器人自定义接口,否则用管理员全局配置
const cfg = await resolveClawConfig(botId);
if (!cfg.api_base) {
await ctx.sendText('[OpenClaw] 未配置 Gateway 地址,请先在网页端设置。');
return true;
}
if (!cfg.api_key) {
await ctx.sendText('[OpenClaw] 未配置网关令牌,请先在网页端设置。');
return true;
}
if (!prompt) {
await ctx.sendText('[OpenClaw] 请输入你的问题,例如:' + trigger + ' 帮我总结今天的任务');
return true;
}
// 检查 Token 额度
const tokenInfo = await getUserTokenInfo(userId);
if (tokenInfo.used >= tokenInfo.limit) {
const usedK = Math.round(tokenInfo.used / 1000);
const limitK = Math.round(tokenInfo.limit / 1000);
await ctx.sendText('[OpenClaw] Token 额度已用完(' + usedK + 'K / ' + limitK + 'K),请联系管理员提升额度。');
return true;
}
try {
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + cfg.api_key,
};
if (cfg.backend_model) {
headers['x-openclaw-model'] = cfg.backend_model;
}
if (session_key) {
headers['x-openclaw-session-key'] = session_key;
}
const body = {
model: cfg.model,
messages: [
...(cfg.system_prompt ? [{ role: 'system', content: cfg.system_prompt }] : []),
{ role: 'user', content: prompt }
],
max_tokens: cfg.max_tokens,
temperature: cfg.temperature,
};
const resp = await axios.post(
cfg.api_base + '/v1/chat/completions',
body,
{ headers, timeout: 90000 }
);
const reply = resp.data?.choices?.[0]?.message?.content;
const toolCalls = resp.data?.choices?.[0]?.message?.tool_calls;
const usageTokens = resp.data?.usage?.total_tokens || 0;
if (usageTokens > 0) {
await addUserTokens(userId, usageTokens);
}
if (reply) {
const newUsed = tokenInfo.used + usageTokens;
const remain = Math.max(0, tokenInfo.limit - newUsed);
const footer = remain > 0 ? '\n\n— 剩余约 ' + Math.round(remain / 1000) + 'K Token' : '';
await ctx.sendText(reply.trim() + footer);
} else if (toolCalls && toolCalls.length > 0) {
const toolNames = toolCalls.map(t => t.function?.name || 'unknown').join(', ');
await ctx.sendText('[OpenClaw] Agent 正在执行工具调用: ' + toolNames);
} else {
await ctx.sendText('[OpenClaw] Agent 未返回有效内容');
}
} catch (err) {
console.error('[openclaw] API调用失败:', err.message);
let errMsg = err.message;
if (err.response?.data?.error?.message) {
errMsg = err.response.data.error.message;
} else if (err.response?.status === 401 || err.response?.status === 403) {
errMsg = '网关认证失败,请检查令牌';
} else if (err.response?.status === 404) {
errMsg = '端点未找到,请确认 Gateway 地址正确';
} else if (err.code === 'ECONNABORTED') {
errMsg = '请求超时,请检查 Gateway 服务状态';
} else if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED') {
errMsg = '无法连接到 Gateway 地址';
}
await ctx.sendText('[OpenClaw] 调用失败: ' + errMsg);
}
return true;
},
};