平台概述

BotFox 是一个多平台智能机器人管理平台,支持微信、飞书、企业微信、钉钉、Telegram 五大平台。用户在控制台绑定 Bot 后,通过安装插件来扩展 Bot 的能力。

作为插件开发者,你编写的代码会在用户的 Bot 收到消息时被调用。平台通过统一的 Adapter 架构屏蔽了平台差异,让你专注于业务逻辑 — 同一个插件可以在所有平台上运行。

支持的平台

平台绑定方式消息类型
📱 微信扫码登录文字/图片/语音/视频/文件
🐦 飞书App ID + Secret文字/图片/文件
💼 企业微信CorpID + AgentID + Secret文字/图片/文件
🔷 钉钉AppKey + AppSecret + RobotCode文字/图片/文件
✈️ TelegramBot Token文字/图片/语音/视频/文件

插件能做什么

  • 接收并处理文字、图片、语音、视频、文件消息
  • 回复文字、发送图片/视频/文件
  • 调用外部 API(AI 对话、天气查询、翻译等)
  • 持久化存储数据(游戏存档、对话历史、用户偏好)
  • 处理引用消息,获取语音转文字结果
  • 控制打字状态("对方正在输入")

能力边界

开发插件前请先了解平台的能力边界。

✅ 插件可以做的

接收所有类型消息文字、图片、语音、视频、文件,平台自动下载媒体
语音转文字SDK 自动识别,通过 msg.voice.text 获取
AI 看图收到图片时可获取 mediaBuffer,转 base64 发给视觉模型
引用消息通过 quotedMessagemsg.textWithQuote 获取引用内容
回复文字bot.reply(msg, text)
发送图片/视频/文件bot.sendImage/sendVideo/sendFile
打字状态bot.sendTyping() / bot.cancelTyping()
调用外部 API可以使用 fetch(仅 HTTPS)
持久化存储每用户 5MB,storage.get/set/delete

❌ 插件不能做的

访问文件系统沙箱禁止 fsprocessrequirechild_process
群聊Bot 只支持 1 对 1 单聊
获取好友/群列表SDK 不提供通讯录接口
发送名片/位置/小程序仅支持文字、图片、语音、视频、文件
消息撤回/已读SDK 不支持

5 分钟上手

一个最简单的插件:

export async function handler({ bot, msg }) {
  if (msg.text === '你好') {
    await bot.reply(msg, '你好呀~ 😊');
    return { handled: true };  // 阻止后续插件执行
  }
  return { handled: false };   // 不处理,交给下一个插件
}

插件元信息 (meta)

推荐在插件文件顶部声明 meta,包含触发词声明:

export const meta = {
  slug: 'my-plugin',
  name: '🎯 我的插件',
  description: '一句话描述',
  icon: '🎯',
  category: 'utility',   // message | utility | fun | game | content
  version: '1.0.0',
  triggers: ['触发词1', '触发词2'],  // 声明触发词,用于冲突检测
  config_schema: [],
};

triggers 字段声明插件的触发词。系统启动时会自动检测跨插件的触发词冲突, 并在日志中告警。管理员可通过 GET /api/admin/trigger-conflicts 查看所有冲突。

提交流程

  1. 在插件库页面点击「提交插件」
  2. 填写名称、描述、代码
  3. 提交后等待管理员审核
  4. 审核通过后,所有用户都能在插件库看到并安装

插件上下文

插件 handler 函数接收一个对象,包含所有你需要的工具:

export async function handler({
  bot,            // BotAdapter 实例 — 发消息、下载媒体等(统一接口,适配所有平台)
  msg,            // ParsedMessage — 收到的消息
  config,         // object — 用户在控制台配置的参数
  botDbId,        // number — Bot 数据库 ID
  userId,         // number — 用户 ID
  storage,        // StorageHelper — 持久化存储
  mediaBuffer,    // Buffer | null — 平台已下载的媒体数据
  quotedMessage,  // object | null — 引用消息 { title, item, text }
  messageId,      // number | null — SDK 原始消息 ID
  aiService,      // AIService — 统一 AI 调用服务(见 AI Service 章节)
  searchService,   // SearchService — 联网搜索服务(见 Search Service 章节)
  getPlatformCreds, // async (platform) => object|null — 获取用户的平台凭证(飞书/企微/钉钉等)
  // 内置插件额外可用:pool (pg连接池)
}) {
  // 你的业务逻辑
  return { handled: true };   // 已处理
  // 或
  return { handled: false };  // 未处理,继续下一个插件
}

消息对象 (msg)

字段类型说明
typestringtext | image | voice | file | video
textstring文本内容。语音消息时为语音转文字结果
textWithQuotestring?带引用上下文:[引用: xxx]\n原文
fromstring发送者用户 ID
tostring接收者 ID(Bot 自身)
timestampnumber消息时间戳(毫秒)
contextTokenstring回复用的上下文 token
messageIdnumberSDK 原始消息 ID
imageImageItem?{ thumb_width, thumb_height, mid_size, media }
voiceVoiceItem?{ playtime, text, media } — playtime 毫秒,text 语音转文字
fileFileItem?{ file_name, len, md5, media }
videoVideoItem?{ play_length, video_size, thumb_width, thumb_height, media }
quotedMessageobject?{ title, item, text } — text 是 SDK 提取的引用文本
rawobjectSDK 原始消息对象

Bot 方法

所有平台共享统一的 Adapter 接口。同一套方法在微信/飞书/企微/钉钉/Telegram 上都能用,平台差异由 Adapter 内部处理。

方法说明
bot.reply(msg, text)回复文字消息(最常用)
bot.sendText(userId, text, contextToken)主动发送文字
bot.sendImage(userId, buffer)发送图片
bot.sendVideo(userId, buffer)发送视频
bot.sendFile(userId, buffer, filename)发送文件
bot.sendTyping(userId)显示"正在输入"
bot.cancelTyping(userId)取消"正在输入"
bot.downloadImage(imageItem)下载图片 → Buffer
bot.downloadVoice(voiceItem)下载语音 → Buffer
bot.downloadFile(fileItem)下载文件 → Buffer
bot.downloadVideo(videoItem)下载视频 → Buffer
bot.downloadRaw(encryptQueryParam)下载原始 CDN 数据(不解密)

💡 平台自动下载媒体并通过 mediaBuffer 传入,通常不需要手动调用 download。

💡 SDK 提供 markdownToPlainText(text),可将 Markdown 转为纯文本(微信/飞书等不支持 Markdown 渲染的平台会自动调用)。

💡 可通过 bot.platform 获取当前平台标识(wechat/feishu/wecom/dingtalk/telegram),用于平台特定逻辑。

配置 Schema

通过 config_schema 定义插件的可配置项,用户在控制台可视化配置:

[
  { "key": "api_key", "label": "API Key", "type": "password", "placeholder": "输入你的 API Key" },
  { "key": "enabled", "label": "启用功能", "type": "boolean", "default": true },
  { "key": "mode", "label": "模式", "type": "select",
    "options": [{ "value": "auto", "label": "自动" }, { "value": "manual", "label": "手动" }],
    "default": "auto" },
  { "key": "prompt", "label": "系统提示词", "type": "textarea", "default": "" }
]

支持的 type:textpasswordnumberbooleantextareaselect

用户配置的值通过 config 参数传入 handler。password 类型的值会加密存储。

优先级体系

插件按 priority 升序执行(数字越小越先执行)。返回 { handled: true } 可中断后续插件。

插件类型默认优先级说明
bot-menu(菜单)5最先执行,处理"菜单"命令
message-filter(过滤)10过滤敏感词等
keyword-reply(关键词)30关键词自动回复
auto-greeting(问候)40自动问候新用户
webhook-forward(转发)50转发消息到 Webhook
游戏类插件80大富翁等互动游戏
ai-chat(AI 聊天)100兜底,其他插件不处理时由 AI 回复

💡 你的插件优先级建议设在 60-90 之间(在 Webhook 之后、AI 之前)。

代码安全规范

  • 插件在沙箱中运行,禁止访问 fsprocessrequirechild_process
  • 可以使用 fetch 调用外部 API
  • 禁止修改全局变量或原型链
  • 不得收集或传输用户隐私数据
  • 代码中不得包含混淆或加密内容
  • 违规插件将被下架并封禁开发者账号

🧠 AI Service

平台提供统一的 AI 调用服务 aiService,所有插件通过它调用 AI 模型。自动处理模型路由、扣费、日志记录。

callModel — 调用 AI 模型

const result = await aiService.callModel({
  model: 'gpt-4o',           // 模型名称
  messages: [                 // OpenAI 格式消息数组
    { role: 'user', content: '你好' }
  ],
  systemPrompt: '你是助手',   // 可选 system prompt
  maxTokens: 1024,            // 可选,默认 1024
});

// 返回值
{
  text: 'AI 回复内容',        // 文字回复
  imageUrl: null,             // 生图模型返回图片 URL
  usage: { prompt_tokens, completion_tokens },
  credits_deducted: 0,        // 扣除的积分(免费模型为 0)
  free_use: true,             // 是否免费使用
  error: null,                // 错误信息
}

callModel — 生图模型

const result = await aiService.callModel({
  model: 'dall-e-3',
  imagePrompt: '一只可爱的猫咪',
  imageSize: '1024x1024',
});
if (result.imageUrl) {
  // 下载图片并发送
  const resp = await fetch(result.imageUrl);
  const buf = Buffer.from(await resp.arrayBuffer());
  await bot.sendImage(msg.from, buf);
}

getModelsByCategory — 按分类获取模型

// 获取所有聊天模型
const chatModels = await aiService.getModelsByCategory('chat');
// 获取所有视觉模型(能理解图片)
const visionModels = await aiService.getModelsByCategory('vision');
// 获取所有生图模型
const imageModels = await aiService.getModelsByCategory('image');

// 返回: [{ model_name, display_name, brand, input_price, output_price, category }]

getModelInfo — 获取模型详情

const info = await aiService.getModelInfo('gpt-4o');
// { model_name, display_name, category, brand, input_price, output_price, cost_multiplier }

其他工具方法

方法说明
aiService.isFreeModel(name)判断是否免费平台模型
aiService.getImageUsageToday()获取当前用户今日生图次数
aiService.getFreeImageQuota()获取免费生图配额 { dailyPerUser }

📊 消耗与计费

计费规则

  • 免费模型:不扣积分,有频率限制(每用户每小时 200 次)
  • 付费模型:按实际 token 消耗 × 倍率扣积分
  • 生图模型:每日免费次数(管理员可配),超出后按次扣积分
  • 积分换算:100 积分 = $1 USD

扣费公式

// 聊天/视觉模型
积分 = ceil((input_tokens × input_price + output_tokens × output_price) / 1M × multiplier × 100)

// 生图模型(超出免费额度后)
积分 = ceil(input_price / 1000 × multiplier × 100)

插件中检查余额

// callModel 自动处理扣费,余额不足时返回 error
const result = await aiService.callModel({ model: 'gpt-4o', messages: [...] });
if (result.error) {
  await bot.reply(msg, result.error);  // "积分不足,请充值或使用免费模型"
  return { handled: true };
}
// result.credits_deducted = 扣除的积分数

🔍 搜索服务 (Search Service)

平台提供统一的联网搜索服务 searchService,所有插件通过它搜索互联网获取实时信息。自动处理搜索路由、缓存、限流、日志记录。

search — 联网搜索

const result = await searchService.search('搜索关键词', {
  maxResults: 5,    // 可选,1-20,默认 5
  region: 'wt-wt',  // 可选,搜索区域,默认全球
});

// 返回值
{
  results: [
    { title: '标题', url: 'https://...', snippet: '摘要文本' },
    ...
  ],
  source: 'ddg',     // 搜索来源
  count: 5,          // 结果数量
  cached: false,     // 是否缓存命中
}

完整示例 — 搜索 + AI 总结

const result = await searchService.search('2026年春节放假安排', { maxResults: 5 });

if (result.count === 0) {
  await bot.reply(msg, '没有找到相关信息');
  return { handled: true };
}

// 用 AI 总结搜索结果
const context = result.results.map((r, i) =>
  `[${i+1}] ${r.title}\n${r.snippet}`
).join('\n\n');

const summary = await aiService.callModel({
  model: 'gpt-4o-mini',
  messages: [
    { role: 'system', content: '根据搜索结果回答用户问题,引用来源用[1][2]标注。' },
    { role: 'user', content: `问题:2026年春节放假安排\n\n搜索结果:\n${context}` },
  ],
  maxTokens: 500,
});

await bot.reply(msg, summary.text);

错误处理

try {
  const result = await searchService.search(query);
} catch (err) {
  // err.code: INVALID_QUERY | USER_RATE_LIMIT | PLUGIN_RATE_LIMIT | API_ERROR | NETWORK_ERROR
  await bot.reply(msg, `搜索失败: ${err.message}`);
}

限制与配额

限制项
用户限流30 次/分钟
插件限流60 次/分钟(所有用户合计)
查询长度最大 200 字符
结果数量1-20 条
缓存时间5 分钟(相同查询自动缓存)
请求超时30 秒

搜索用量管理 (管理员)

GET /api/admin/search/stats

查看搜索用量统计(需管理员 Token)。返回今日总量、按插件统计、按用户统计。

GET /api/admin/search/logs?limit=20

查看最近搜索日志(需管理员 Token)。返回最近 N 条搜索记录。

触发词冲突检测 (管理员)

GET /api/admin/trigger-conflicts

查看所有已发布插件的触发词冲突(需管理员 Token)。返回精确冲突和前缀冲突。

// 响应示例
{
  "exact": [                    // 精确冲突:两个插件注册了相同触发词
    { "trigger": "百科", "plugins": ["horoscope", "info-hub"] }
  ],
  "prefix": [                   // 前缀冲突:短触发词是长触发词的前缀
    { "trigger": "\"写\" ⊂ \"写日记\"", "plugins": ["ai-writer", "voice-diary"] }
  ],
  "total_plugins": 29,          // 注册了触发词的插件数
  "total_triggers": 125         // 触发词总数
}

🌐 多平台开发

BotFox 通过统一的 Adapter 架构支持 5 个平台。插件代码无需修改即可在所有平台运行。

平台判断

export async function handler({ bot, msg }) {
  // 获取当前平台
  const platform = bot.platform; // 'wechat' | 'feishu' | 'wecom' | 'dingtalk' | 'telegram'

  // 通用逻辑 — 所有平台都能用
  await bot.reply(msg, '你好!');

  // 平台特定逻辑
  if (platform === 'wechat') {
    // 微信特有:语音发送不可用
  } else if (platform === 'feishu') {
    // 飞书特有:支持富文本卡片(需通过平台凭证调飞书 API)
  }

  return { handled: true };
}

各平台能力差异

能力微信飞书企微钉钉Telegram
文字消息
图片消息
语音消息✅ 接收
视频消息
文件消息
打字状态
引用消息
群聊

💡 不支持的能力会被 Adapter 静默忽略,不会报错。比如在飞书上调 bot.sendTyping() 不会有任何效果。

🔑 平台凭证

插件可以通过 getPlatformCreds 获取用户配置的第三方平台凭证(如飞书 App ID/Secret),用于调用平台原生 API。

获取凭证

export async function handler({ bot, msg, getPlatformCreds }) {
  // 获取用户的飞书凭证
  const creds = await getPlatformCreds('feishu');
  if (!creds) {
    await bot.reply(msg, '请先在控制台「平台凭证」中配置飞书凭证');
    return { handled: true };
  }

  // creds 包含用户配置的所有字段(已解密)
  const { app_id, app_secret } = creds;

  // 用凭证调飞书 API
  // ...

  return { handled: true };
}

支持的平台凭证

平台凭证字段获取方式
飞书app_id, app_secret飞书开放平台 → 创建应用
企业微信corp_id, agent_id, secret企微管理后台 → 应用管理
钉钉app_key, app_secret, robot_code钉钉开放平台 → 应用开发

💡 凭证在数据库中使用 AES-256-GCM 加密存储,getPlatformCreds 返回的是解密后的明文对象。

💡 用户在控制台「🔑 平台凭证」页面管理自己的凭证,每个平台可以配置多组。

🎤 语音能力

📥 接收语音 + 语音转文字

  1. 用户发送语音,SDK 自动识别内容,转写结果存入 msg.voice.text
  2. 平台自动下载语音文件,通过 mediaBuffer 传给插件
  3. 语音时长通过 msg.voice.playtime 获取(毫秒)
export async function handler({ bot, msg, mediaBuffer }) {
  if (msg.type === 'voice') {
    const text = msg.voice?.text;       // SDK 自动转写
    const duration = msg.voice?.playtime; // 毫秒
    if (text) {
      await bot.reply(msg, `🎤 你说了: "${text}" (${Math.round(duration/1000)}秒)`);
    } else {
      await bot.reply(msg, '🎤 语音识别失败');
    }
    return { handled: true };
  }
  return { handled: false };
}
⚠️ 注意:目前仅支持接收语音消息和语音转文字,不支持发送语音消息(微信 iLink 接口限制)。

👁️ AI 看图

用户发送图片时,mediaBuffer 包含已下载的图片数据。你可以转 base64 发给视觉模型:

export async function handler({ bot, msg, config, mediaBuffer }) {
  if (msg.type === 'image' && mediaBuffer) {
    const b64 = mediaBuffer.toString('base64');
    // 调用视觉模型(OpenAI 格式)
    const resp = await fetch(config.api_url + '/chat/completions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.api_key}` },
      body: JSON.stringify({
        model: 'gpt-4o-mini',
        messages: [{
          role: 'user',
          content: [
            { type: 'image_url', image_url: { url: `data:image/jpeg;base64,${b64}` } },
            { type: 'text', text: msg.text || '描述这张图片' }
          ]
        }]
      })
    });
    const data = await resp.json();
    await bot.reply(msg, data.choices[0].message.content);
    return { handled: true };
  }
  return { handled: false };
}

💡 内置 AI 聊天插件已自动实现看图功能,需要支持视觉的模型(如 gpt-4o-mini)。

💬 引用消息

用户引用(长按回复)某条消息时:

// quotedMessage 结构
{
  title: "被引用消息的发送者名称",
  item: { /* 原始消息项 */ },
  text: "发送者 | 被引用的文字内容"  // SDK v1.1.0 提取
}

// msg.textWithQuote — SDK 自动合并
"[引用: 被引用内容]\n用户实际发送的文字"
export async function handler({ bot, msg, quotedMessage }) {
  if (quotedMessage) {
    const quoted = quotedMessage.text || quotedMessage.title || '';
    await bot.reply(msg, `你引用了: "${quoted}"\n你说: ${msg.text}`);
    return { handled: true };
  }
  return { handled: false };
}

📁 媒体处理

平台自动下载所有媒体消息,你可以直接使用:

export async function handler({ bot, msg, mediaBuffer }) {
  switch (msg.type) {
    case 'image':
      if (mediaBuffer) {
        const sizeKB = Math.round(mediaBuffer.length / 1024);
        const w = msg.image?.thumb_width;
        const h = msg.image?.thumb_height;
        await bot.reply(msg, `📸 图片 ${w}x${h}, ${sizeKB}KB`);
      }
      return { handled: true };

    case 'voice':
      await bot.reply(msg, `🎤 语音 ${Math.round(msg.voice?.playtime/1000)}秒: ${msg.voice?.text || '(无法识别)'}`);
      return { handled: true };

    case 'file':
      await bot.reply(msg, `📎 文件: ${msg.file?.file_name} (${msg.file?.len} bytes)`);
      return { handled: true };

    case 'video':
      await bot.reply(msg, `🎬 视频 ${msg.video?.play_length}秒`);
      return { handled: true };
  }
  return { handled: false };
}

插件存储

每个用户 5MB 存储空间。每个插件的数据按 plugin_slug 隔离。

  • Key-Value 存储,Value 支持字符串或 JSON 对象(自动序列化)
  • 单个 Value 最大 1MB
  • 卸载插件不会自动删除数据

Storage API

// 读取(不存在返回 null)
const data = await storage.get('game_state');

// 写入(对象自动 JSON 序列化)
await storage.set('game_state', { level: 3, score: 1500 });

// 删除
await storage.delete('game_state');

// 列出所有 key
const keys = await storage.keys();

// 清空该插件的所有数据
await storage.clear();

示例:游戏存档

export async function handler({ bot, msg, storage }) {
  if (msg.text === '开始游戏') {
    let save = await storage.get('save');
    if (save) {
      await bot.reply(msg, `欢迎回来!等级: ${save.level}, 金币: ${save.gold}`);
    } else {
      save = { level: 1, gold: 100 };
      await storage.set('save', save);
      await bot.reply(msg, '新游戏开始!初始金币: 100');
    }
    return { handled: true };
  }
  return { handled: false };
}

示例:AI 对话历史

export async function handler({ bot, msg, storage }) {
  let history = await storage.get('chat_history') || [];
  history.push({ role: 'user', content: msg.text });

  // 调用 AI...
  const reply = await callAI(history.slice(-20));

  history.push({ role: 'assistant', content: reply });
  if (history.length > 50) history = history.slice(-50);
  await storage.set('chat_history', history);

  await bot.reply(msg, reply);
  return { handled: true };
}

提交插件

在插件库页面点击「提交插件」,或通过 API 提交:

POST /api/plugins/submit

需要登录。提交后需管理员审核通过才会在插件库显示。

字段类型必填说明
slugstring唯一标识,小写字母/数字/连字符
namestring插件名称
descriptionstring简短描述
iconstringEmoji 图标,默认 🔌
categorystringmessage / command / scheduled / utility
versionstring版本号,默认 1.0.0
config_schemaarray配置项定义(见上方)
triggersarray触发词列表,用于冲突检测。如 ["帮我选", "抛硬币"]
codestring插件代码
readmestring详细说明(Markdown)
repository_urlstring源码仓库地址

升级插件

PUT /api/plugins/my/:id

更新自己提交的插件。两种模式:

  • 修改描述/图标/README/源码地址 — 不影响发布状态
  • 修改 code 或 version — 重置为待审核,需重新审核
GET /api/plugins/my

查看自己提交的所有插件(含审核中和已发布)。

免费模型

GET /api/plugins/free-models

获取平台提供的免费 AI 模型列表。无需认证。插件开发者可用此接口在配置中提供模型选择。

// 响应
[
  { "id": "gpt-4o-mini", "provider": "AI7Bot" },
  { "id": "claude-haiku-4-5", "provider": "AI7Bot" },
  ...
]
POST /api/plugins/ai-models

用自己的 API Key 获取可用模型列表(需登录)。

{ "api_base_url": "https://api.example.com/v1", "api_key": "sk-xxx" }
// 响应: { "models": ["gpt-4o", "gpt-4o-mini", ...] }

🤖 AI 代理

平台提供免费 AI 代理接口,插件可以直接调用,无需自备 API Key。

POST /api/plugins/ai-proxy

无需认证。兼容 OpenAI Chat Completions 格式。

请求参数

字段类型必填说明
modelstring模型名称,默认 gpt-4o-mini。可通过 /api/plugins/free-models 查看可用模型
messagesarray消息数组,格式 [{ role: "system"|"user"|"assistant", content: "..." }]。最多保留最近 10 条
max_tokensnumber最大生成 token 数,默认 800,上限 2000
temperaturenumber温度参数,默认 0.7,范围 0-1.5

system role 处理

支持发送 system role 消息。由于部分免费模型不原生支持 system role,代理会自动将其合并到第一条 user 消息中,确保兼容性。你只需正常传 system 消息,无需手动处理。

限制

  • 每 IP 每小时 60 次请求
  • 所有消息总长度不超过 4000 字符
  • 最多 10 条消息(超出自动截取最近 10 条)
  • max_tokens 上限 2000
  • 请求超时 30 秒
  • 超限返回 429 状态码

响应格式

// 成功
{
  "choices": [{
    "message": { "role": "assistant", "content": "AI 回复内容" }
  }]
}

// 错误
{ "error": "错误描述" }

示例:基础调用

// 简单调用(直接 user 消息)
async function askAI(prompt) {
  const resp = await fetch('https://botfox.ai/api/plugins/ai-proxy', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: prompt }],
      max_tokens: 500,
    }),
  });
  if (!resp.ok) return null;
  const data = await resp.json();
  return data.choices?.[0]?.message?.content || null;
}

示例:带 system 角色设定 + 多轮对话

// 带角色设定和对话历史(适合游戏/角色扮演类插件)
async function callAI(systemPrompt, userMsg, history) {
  const messages = [{ role: 'system', content: systemPrompt }];
  // 添加对话历史(assistant/user 交替)
  if (history?.length) {
    for (let i = 0; i < history.length; i++) {
      messages.push({
        role: i % 2 === 0 ? 'assistant' : 'user',
        content: history[i]
      });
    }
  }
  messages.push({ role: 'user', content: userMsg });

  const resp = await fetch('https://botfox.ai/api/plugins/ai-proxy', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      messages,
      max_tokens: 800,
      temperature: 0.85,
    }),
  });
  if (!resp.ok) return null;
  const data = await resp.json();
  return data.choices?.[0]?.message?.content || null;
}

// 使用示例
const reply = await callAI(
  '你是一个海盗船长,用海盗语气说话',
  '你好啊船长',
  ['哈哈,欢迎上船!', '这船要去哪?']
);

💡 建议:AI 调用需要 2-10 秒,可以先 bot.sendTyping() 显示"正在输入",完成后 bot.cancelTyping()

📝 消息排版

微信聊天窗口不支持 Markdown 渲染。为了保证用户体验,平台在 bot.reply() 底层内置了自动排版处理。

自动处理(无需手动调用)

所有通过 bot.reply(msg, text) 发送的消息会自动经过以下处理:

Markdown 格式转换结果
**加粗** / __加粗__纯文本(去除标记)
*斜体* / _斜体_纯文本
# 标题纯文本(去除 #)
```代码块```纯代码内容
`行内代码`纯文本
[链接](url)链接文字
![图片](url)移除
--- 分隔线移除
1. 2. 3. 编号列表①②③
- / * 无序列表• 圆点
> 引用纯文本
3 个以上连续空行压缩为 1 个空行
Markdown 表格空格分隔的纯文本

开发者建议

虽然平台会自动处理排版,但以下最佳实践能让回复效果更好:

  • AI 调用时在 system prompt 中加入格式约束(如「禁止 Markdown、每段不超过 4 行、300 字以内」)
  • 固定文本回复直接用纯文本拼接,不需要额外处理
  • 如需发送原始文本(如代码片段),使用 bot.sendText(userId, text) 绕过自动处理

示例:AI 插件的 system prompt

const result = await aiService.callModel({
  messages: [{ role: 'user', content: userText }],
  // system prompt 加格式约束,效果更好
  systemPrompt: `你是一个有帮助的助手。
【格式要求】回复在手机聊天窗口显示:
- 每段不超过3-4行,段落间空一行
- 禁止Markdown格式
- 编号用①②③
- 总回复300字以内`,
  maxTokens: 500,
});
// bot.reply() 会自动处理残留的 Markdown
await bot.reply(msg, result.text);

🔐 飞书权限配置指南

飞书插件需要用户在飞书开放平台创建应用并配置权限。以下是完整的配置流程和常见问题。

第一步:创建飞书应用

  1. 打开 飞书开放平台
  2. 点击「创建企业自建应用」
  3. 填写应用名称和描述,创建完成后获得 App ID 和 App Secret
  4. 在 BotFox 控制台「🔑 平台凭证」中填入 App ID 和 App Secret

第二步:配置权限

在飞书开放平台 → 你的应用 → 权限管理 → 搜索并开通以下权限:

插件需要的权限权限说明
📄 飞书文档docx:document
docx:document:readonly
drive:drive
drive:drive:readonly
drive:permission
文档读写 + 云盘管理 + 权限控制
📊 飞书表格sheets:spreadsheet
drive:drive
电子表格读写
📚 飞书知识库wiki:wiki
wiki:wiki:readonly
知识空间和节点管理
💬 飞书消息im:message
im:message:send_as_bot
im:chat
im:chat:readonly
消息发送 + 群管理
👥 飞书通讯录contact:user.base:readonly
contact:department.base:readonly
用户和部门查询(只读)
✅ 飞书审批approval:approval
approval:instance
审批流程查询和催办

💡 只需开通你要用的插件对应的权限,不需要全部开通。

第三步:发布应用

  1. 权限配置完成后,点击「创建版本」
  2. 填写版本号和更新说明
  3. 提交审核(企业管理员审核通过后权限生效)
  4. 如果是测试环境,可以在「版本管理」中直接发布到测试企业

常见问题

Q: 报错「飞书权限不足」怎么办?
A: 检查是否开通了对应权限,并且已经创建新版本发布。权限修改后必须重新发布才能生效。

Q: 报错「飞书认证失败」怎么办?
A: 检查 App ID 和 App Secret 是否正确。在控制台「🔑 平台凭证」中重新填写。

Q: 个人版飞书能用吗?
A: 飞书开放平台的自建应用需要企业版。个人版用户可以创建测试企业来使用。

Q: 一个飞书应用可以给多个 Bot 用吗?
A: 可以。平台凭证是按用户维度管理的,同一个用户的所有 Bot 共享凭证。

Q: 权限范围怎么控制?
A: 飞书权限分为「应用权限」和「数据权限」。应用权限在开放平台配置,数据权限(如能访问哪些文档)取决于应用被授权的范围。建议按最小权限原则配置。

🔧 公共工具库 (_utils.js)

所有插件都可以从 ./_utils.js 导入公共工具函数,避免重复实现。

以下内容从源码自动生成,始终与最新版本同步。

加载中...

函数详解

函数类型说明示例
加载中...

完整示例

天气查询插件

export const slug = 'weather-query';
export const name = '天气查询';
export const description = '发送"天气 城市名"查询天气';
export const icon = '🌤️';
export const config_schema = [];

export async function handler({ bot, msg }) {
  if (msg.type !== 'text') return { handled: false };
  const match = msg.text.match(/^天气\s+(.+)/);
  if (!match) return { handled: false };

  const city = match[1].trim();
  try {
    const resp = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=3`);
    const text = await resp.text();
    await bot.reply(msg, `🌤️ ${text.trim()}`);
  } catch {
    await bot.reply(msg, '❌ 查询失败,请稍后再试');
  }
  return { handled: true };
}

图片尺寸检测插件

export const slug = 'image-info';
export const name = '图片信息';
export const description = '收到图片自动回复尺寸和大小';
export const icon = '📐';

export async function handler({ bot, msg, mediaBuffer }) {
  if (msg.type !== 'image' || !mediaBuffer) return { handled: false };

  const w = msg.image?.thumb_width || '?';
  const h = msg.image?.thumb_height || '?';
  const sizeKB = Math.round(mediaBuffer.length / 1024);

  await bot.reply(msg, `📐 图片信息\n尺寸: ${w} × ${h}\n大小: ${sizeKB} KB`);
  return { handled: true };
}

语音复读机插件

export const slug = 'voice-echo';
export const name = '语音复读机';
export const description = '收到语音自动回复转写文字';
export const icon = '🦜';

export async function handler({ bot, msg }) {
  if (msg.type !== 'voice') return { handled: false };

  const text = msg.voice?.text;
  const sec = Math.round((msg.voice?.playtime || 0) / 1000);

  if (text) {
    await bot.reply(msg, `🦜 你说了 (${sec}秒):\n"${text}"`);
  } else {
    await bot.reply(msg, `🦜 收到 ${sec} 秒语音,但没听清~`);
  }
  return { handled: true };
}

带存储的计数器插件

export const slug = 'msg-counter';
export const name = '消息计数器';
export const description = '统计你发了多少条消息';
export const icon = '🔢';

export async function handler({ bot, msg, storage }) {
  if (msg.text === '统计') {
    const count = await storage.get('count') || 0;
    await bot.reply(msg, `📊 你一共发了 ${count} 条消息`);
    return { handled: true };
  }

  // 每条消息 +1(不拦截,让后续插件继续处理)
  const count = (await storage.get('count') || 0) + 1;
  await storage.set('count', count);
  return { handled: false };
}