Yunzai 插件开发完整技能指南
目录
OneBot11 协议接入详解
1.1 协议适配器架构
Yunzai 采用适配器模式支持多协议接入,OneBotv11 适配器位于 plugins/adapter/OneBotv11.js。
核心类结构
class OneBotv11Adapter {
id = "QQ" // 适配器标识
name = "OneBotv11" // 适配器名称
path = this.name // WebSocket 路径
echo = new Map() // 请求响应映射
timeout = 60000 // API 超时时间
}
1.2 WebSocket 连接流程
协议端 (go-cqhttp/LLOneBot/Lagrange)
↓ 反向 WebSocket 连接
Yunzai Server (ws://localhost:2536/OneBotv11)
↓ 触发 message 事件
OneBotv11Adapter.message()
↓ 分发到对应处理器
connect / message / notice / request
1.3 配置协议端
略
1.4 核心 API 调用
发送 API 请求
// 适配器内部封装
sendApi(data, ws, action, params = {}) {
const echo = ulid() // 生成唯一标识
const request = { action, params, echo }
ws.sendMsg(request)
// 使用 Promise.withResolvers 等待响应
const cache = Promise.withResolvers()
this.echo.set(echo, cache)
// 设置超时
setTimeout(() => {
cache.reject(Bot.makeError("请求超时", request))
}, this.timeout)
return cache.promise
}
常用 API 列表
| API | 功能 | 参数 |
|---|---|---|
send_msg |
发送消息 | user_id/group_id, message |
delete_msg |
撤回消息 | message_id |
get_msg |
获取消息 | message_id |
get_friend_list |
获取好友列表 | - |
get_group_list |
获取群列表 | - |
get_group_member_list |
获取群成员 | group_id |
set_group_ban |
禁言成员 | group_id, user_id, duration |
set_group_kick |
踢出成员 | group_id, user_id |
upload_group_file |
上传群文件 | group_id, file, name |
get_forward_msg |
获取转发消息 | message_id |
1.5 消息格式转换
Yunzai 消息段 → OneBot 消息段
// 文本
{ type: "text", data: { text: "消息内容" } }
// @某人
{ type: "at", data: { qq: "123456" } }
// 图片
{ type: "image", data: { file: "base64://..." } }
// 回复
{ type: "reply", data: { id: "12345" } }
// 转发消息节点
{ type: "node", data: { name: "昵称", uin: "123", content: [...] } }
发送消息示例
// 发送文本
await e.reply("Hello World")
// 发送图片
await e.reply(segment.image("http://example.com/img.jpg"))
// 发送混合消息
await e.reply([
segment.at(user_id),
"\n",
segment.image("base64://..."),
"\n文本内容"
])
// 发送转发消息(合并转发)
const forwardMsg = Bot.makeForwardMsg([
{ user_id: 123, nickname: "用户1", message: "消息1" },
{ user_id: 456, nickname: "用户2", message: "消息2" }
])
await e.reply(forwardMsg)
1.6 事件处理
消息事件
// 私聊消息
Bot.on("message.private", (e) => {
console.log(e.user_id, e.message)
})
// 群消息
Bot.on("message.group", (e) => {
console.log(e.group_id, e.user_id, e.message)
})
通知事件
// 群成员增加
Bot.on("notice.group.increase", (e) => {
console.log(`新成员:${e.user_id}`)
})
// 群成员减少
Bot.on("notice.group.decrease", (e) => {
console.log(`成员离开:${e.user_id}`)
})
// 群禁言
Bot.on("notice.group.ban", (e) => {
console.log(`禁言:${e.user_id} ${e.duration}秒`)
})
请求事件
// 好友申请
Bot.on("request.friend.add", (e) => {
// 同意申请
e.approve(true)
// 拒绝申请
e.approve(false, "拒绝理由")
})
// 群申请
Bot.on("request.group.add", (e) => {
e.approve(true)
})
图片渲染系统
2.1 渲染器架构
Yunzai 使用 Puppeteer + art-template 实现图片渲染。
插件调用 puppeteer.render()
↓
Renderer.dealTpl() 处理模板
↓
art-template 渲染 HTML
↓
Puppeteer.screenshot() 截图
↓
返回图片 Buffer
2.2 渲染器配置
config/default_config/renderer.yaml
# 渲染器名称
name: puppeteer
# Puppeteer 配置
chromiumPath: '' # Chromium 路径(可选)
puppeteerWS: '' # Puppeteer WebSocket 地址(可选)
puppeteerTimeout: 0 # 截图超时时间(毫秒)
# 截图参数
pageGotoParams:
timeout: 120000
waitUntil: "networkidle2"
2.3 渲染方法详解
基础渲染
import { puppeteer } from "../model/index.js"
// 方式1:使用插件内 puppeteer 实例
await puppeteer.render("template/name", {
// 模板数据
title: "标题",
content: "内容"
}, {
e, // 事件对象(必需)
scale: 1.2 // 缩放比例
})
// 方式2:使用全局 Renderer
const renderer = Renderer.getRenderer()
const img = await renderer.render("template/name", data)
完整渲染参数
await puppeteer.render(name, data, options)
// name: 模板路径(相对于 resources 目录)
// data: 模板数据对象
// options: {
// e: 事件对象(必需)
// scale: 缩放比例(默认 1.0)
// saveId: 保存ID(默认使用 name)
// }
data 数据对象详解
{
// 模板文件路径(必需)
tplFile: "./plugins/your-plugin/resources/template/index.html",
// 其他自定义数据
title: "页面标题",
list: [/* 数据列表 */],
// 系统注入数据
_res_path: "./resources/", // 资源路径
sys: {
scale: "data-scale=1.2",
copyright: "Yunzai-Bot"
}
}
2.4 高级截图功能
分页截图
await puppeteer.render("template/name", {
tplFile: "...",
multiPage: true, // 启用分页
multiPageHeight: 4000, // 每页高度(默认 4000px)
// ... 其他数据
})
截图参数
{
imgType: "jpeg", // 图片格式:jpeg/png
quality: 90, // 图片质量(jpeg 有效)
omitBackground: false, // 是否透明背景
path: "/path/to/save.jpg" // 保存路径(可选)
}
2.5 yenai-plugin 渲染封装
封装示例
// model/index.js
export { default as puppeteer } from "#yenai.puppeteer"
// components/index.js 中定义别名
"#yenai.puppeteer": "./plugins/yenai-plugin/components/puppeteer.js"
使用示例
import { puppeteer } from "../model/index.js"
// 渲染帮助界面
await puppeteer.render("help/index", {
helpCfg: config,
helpGroup: groups,
bg: "background.jpg",
colCount: 3
}, {
e,
scale: 1.2
})
标准渲染模板
3.1 模板目录结构
resources/
├── common/
│ ├── layout/
│ │ └── default.html # 默认布局模板
│ ├── common.css # 公共样式
│ └── font/ # 字体文件
├── help/
│ ├── index.html # 帮助模板
│ ├── index.css # 帮助样式
│ └── imgs/ # 背景图片
└── admin/
├── index.html # 配置界面模板
└── index.css
3.2 布局模板详解
default.html(标准布局)
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<link rel="preload" href="{{_res_path}}common/font/FZB.woff" as="font">
<link rel="stylesheet" type="text/css" href="{{_res_path}}common/common.css" />
<title>{{title}}</title>
{{block 'css'}}{{/block}}
</head>
<body class="elem-{{element||'hydro'}} {{displayMode||'default'}}-mode" {{@sys.scale}}>
<div class="container" id="container">
{{block 'main'}}{{/block}}
<div class="copyright">{{@sys.copyright}}</div>
</div>
</body>
</html>
继承布局模板
{{extend defaultLayout}}
{{block 'css'}}
<link rel="stylesheet" href="{{_res_path}}/help/index.css" />
<style>
.container {
background: url("{{_res_path}}/help/imgs/{{bg}}") center;
}
</style>
{{/block}}
{{block 'main'}}
<!-- 主内容区域 -->
<div class="content">
{{each list item}}
<div class="item">{{item.name}}</div>
{{/each}}
</div>
{{/block}}
3.3 帮助界面模板
help/index.html
{{extend defaultLayout}}
{{block 'css'}}
<link rel="stylesheet" type="text/css" href="{{_res_path}}/help/index.css" />
<style>
.container {
background: url("{{_res_path}}/help/imgs/{{bg}}") center !important;
background-size: cover !important;
}
</style>
{{/block}}
{{block 'main'}}
<div class="info-box">
<div class="head-box type{{bgType}}">
<div class="title">{{helpCfg.title||"使用帮助"}}</div>
<div class="label">{{helpCfg.subTitle || "Yunzai-Bot"}}</div>
</div>
</div>
{{each helpGroup group}}
{{set len = group?.list?.length || 0 }}
<div class="cont-box">
<div class="help-group">{{group.group}}</div>
{{if len > 0}}
<div class="help-table">
<div class="tr">
{{each group.list help idx}}
<div class="td">
<span class="help-icon" style="{{help.css}}"></span>
<strong class="help-title">{{help.title}}</strong>
<span class="help-desc">{{help.desc}}</span>
</div>
{{if idx%colCount === colCount-1 && idx>0 && idx< len-1}}
</div>
<div class="tr">
{{/if}}
{{/each}}
</div>
</div>
{{/if}}
</div>
{{/each}}
{{/block}}
帮助配置数据结构
// config/system/help_system.js
export const helpCfg = {
title: "xx帮助",
subTitle: "Yunzai-Bot & xx-Plugin",
columnCount: 3, // 列数
colWidth: 265, // 列宽
theme: "all",
themeExclude: ["default"],
style: {
fontColor: "#ceb78b",
descColor: "#eee",
contBgColor: "rgba(6, 21, 31, .5)",
contBgBlur: 3,
headerBgColor: "rgba(6, 21, 31, .4)",
rowBgColor1: "rgba(6, 21, 31, .2)",
rowBgColor2: "rgba(6, 21, 31, .35)"
}
}
export const helpList = [
{
group: "功能分组名称",
auth: "master", // 权限:master/owner/admin(可选)
list: [
{
icon: 1, // 图标编号
title: "#命令", // 命令
desc: "功能描述" // 描述
}
]
}
]
3.4 配置界面模板
admin/index.html
{{extend defaultLayout}}
{{block 'css'}}
<link rel="stylesheet" href="{{_res_path}}/admin/index.css" />
{{/block}}
{{block 'main'}}
<div class="config-box">
<div class="config-header">
<h2>{{title}}</h2>
</div>
{{each groups group}}
<div class="config-group">
<div class="group-title">{{group.name}}</div>
{{each group.items item}}
<div class="config-item">
<div class="item-label">{{item.label}}</div>
<div class="item-value">{{item.value}}</div>
<div class="item-desc">{{item.desc}}</div>
</div>
{{/each}}
</div>
{{/each}}
</div>
{{/block}}
3.5 art-template 语法
基础语法
<!-- 变量输出 -->
{{name}}
{{user.name}}
{{user['name']}}
<!-- 不转义输出 HTML -->
{{@htmlContent}}
<!-- 条件判断 -->
{{if user}}
<div>{{user.name}}</div>
{{else if guest}}
<div>访客</div>
{{else}}
<div>匿名</div>
{{/if}}
<!-- 循环 -->
{{each list item index}}
<div>{{index}}: {{item.name}}</div>
{{/each}}
<!-- 设置变量 -->
{{set temp = value}}
<!-- 引入子模板 -->
{{include './header.html'}}
过滤器
<!-- 默认值 -->
{{name | default:'匿名'}}
<!-- 日期格式化 -->
{{time | date:'yyyy-MM-dd'}}
<!-- 截取字符串 -->
{{content | truncate:20}}
Guoba 配置界面
4.1 Guoba 简介
Guoba 是 Yunzai 的图形化配置管理插件,支持通过 Web 界面管理插件配置。
4.2 接入 Guoba
创建 guoba.support.js
// guoba.support.js
export { supportGuoba } from "./guoba/index.js"
创建 guoba/index.js
import pluginInfo from "./pluginInfo.js"
import configInfo from "./configInfo.js"
export function supportGuoba() {
return {
pluginInfo,
configInfo
}
}
4.3 插件信息配置
guoba/pluginInfo.js
export default {
// 插件名称
name: "yenai-plugin",
// 插件标题
title: "椰奶插件",
// 插件描述
description: "Yunzai 扩展插件",
// 作者
author: "@yenai",
// 版本
version: "1.0.0",
// 仓库地址
repo: "https://github.com/your-repo/yenai-plugin",
// 图标
icon: "mdi:puzzle",
// 依赖
depends: []
}
4.4 配置信息配置
guoba/configInfo.js
import { schemas, getConfigData, setConfigData } from "./schemas/index.js"
export default {
schemas, // 表单 schema
getConfigData, // 获取配置方法
setConfigData // 保存配置方法
}
4.5 Schema 定义
guoba/schemas/index.js
import notice from "./notice.js"
import other from "./other.js"
// 合并所有 schemas
export const schemas = [
...notice,
...other
]
// 获取配置数据
export function getConfigData() {
return {
notice: Config.getNotice(),
other: Config.other
}
}
// 保存配置数据
export function setConfigData(data, { Result }) {
// 保存到配置文件
Config.setNotice(data.notice)
Config.setOther(data.other)
return Result.ok({}, "保存成功")
}
Schema 字段类型
// guoba/schemas/notice.js
export default [
{
field: "notice.friendRecall",
label: "好友撤回通知",
component: "Switch", // 组件类型
required: false,
defaultValue: true
},
{
field: "notice.groupRecall",
label: "群撤回通知",
component: "Switch"
},
{
field: "notice.groupIncrease",
label: "群成员增加通知",
component: "Switch"
},
{
field: "notice.groupDecrease",
label: "群成员减少通知",
component: "Switch"
},
// 分组
{
component: "Divider",
label: "通知设置"
},
// 输入框
{
field: "notice.customMsg",
label: "自定义消息",
component: "Input",
placeholder: "请输入自定义消息"
},
// 数字输入
{
field: "notice.timeout",
label: "超时时间",
component: "InputNumber",
min: 0,
max: 3600,
defaultValue: 30
},
// 下拉选择
{
field: "notice.mode",
label: "通知模式",
component: "Select",
options: [
{ label: "简洁", value: "simple" },
{ label: "详细", value: "detail" }
]
},
// 多选
{
field: "notice.types",
label: "通知类型",
component: "CheckboxGroup",
options: [
{ label: "文本", value: "text" },
{ label: "图片", value: "image" }
]
}
]
4.6 支持的组件类型
| 组件 | 用途 | 配置项 |
|---|---|---|
Switch |
开关 | defaultValue |
Input |
文本输入 | placeholder |
InputNumber |
数字输入 | min, max, step |
Select |
下拉选择 | options |
RadioGroup |
单选组 | options |
CheckboxGroup |
多选组 | options |
Textarea |
多行文本 | rows |
Slider |
滑块 | min, max, step |
Divider |
分隔线 | label |
Alert |
提示信息 | message, type |
插件开发最佳实践
5.1 目录结构规范
your-plugin/
├── index.js # 入口文件
├── guoba.support.js # Guoba 支持(可选)
├── package.json # 依赖配置(可选)
├── apps/ # 功能模块
│ ├── index.js # 主插件
│ ├── admin/ # 管理功能
│ └── events/ # 事件监听
├── components/ # 公共组件
│ ├── index.js # 组件导出
│ ├── Data.js # 数据工具
│ └── Config.js # 配置管理
├── config/ # 配置文件
│ ├── default_config/ # 默认配置
│ ├── config/ # 用户配置(运行时)
│ └── system/ # 系统配置
├── guoba/ # Guoba 配置
│ ├── index.js
│ ├── pluginInfo.js
│ ├── configInfo.js
│ └── schemas/
├── model/ # 数据模型
│ ├── index.js
│ └── api/
├── resources/ # 资源文件
│ ├── common/
│ ├── help/
│ └── admin/
└── lib/ # 工具库
5.2 插件基类使用
import plugin from "../../lib/plugins/plugin.js"
export class MyPlugin extends plugin {
constructor() {
super({
name: "插件名称",
dsc: "插件描述",
event: "message",
priority: 5000,
rule: [
{
reg: "^#测试$",
fnc: "test",
permission: "all"
}
],
task: [
{
name: "定时任务",
cron: "0 0 * * *",
fnc: "dailyTask"
}
]
})
}
async test(e) {
await e.reply("测试成功")
return true
}
async dailyTask() {
// 定时任务逻辑
}
}
5.3 配置管理
Config.js 封装
import YAML from "yaml"
import fs from "fs"
const _path = process.cwd()
const plugin = "your-plugin"
export default class Config {
// 获取配置
static getConfig(name) {
const file = `${_path}/plugins/${plugin}/config/config/${name}.yaml`
if (!fs.existsSync(file)) {
// 复制默认配置
this.copyDefault(name)
}
return YAML.parse(fs.readFileSync(file, "utf8"))
}
// 保存配置
static setConfig(name, data) {
const file = `${_path}/plugins/${plugin}/config/config/${name}.yaml`
fs.writeFileSync(file, YAML.stringify(data))
}
// 复制默认配置
static copyDefault(name) {
const defaultFile = `${_path}/plugins/${plugin}/config/default_config/${name}.yaml`
const configFile = `${_path}/plugins/${plugin}/config/config/${name}.yaml`
if (fs.existsSync(defaultFile)) {
fs.mkdirSync(`${_path}/plugins/${plugin}/config/config`, { recursive: true })
fs.copyFileSync(defaultFile, configFile)
}
}
}
5.4 图片渲染封装
// components/puppeteer.js
import { segment } from "oicq"
export default {
async render(template, data, options = {}) {
const { e, scale = 1.0 } = options
if (!e) {
logger.error("[render] 缺少事件对象 e")
return false
}
// 获取渲染器
const renderer = Renderer.getRenderer()
// 准备模板数据
const templateData = {
...data,
tplFile: `./plugins/your-plugin/resources/${template}.html`,
_res_path: `./plugins/your-plugin/resources/`
}
// 渲染
const img = await renderer.render(template, templateData)
if (!img) {
logger.error("[render] 图片渲染失败")
return false
}
return segment.image(img)
}
}
5.5 错误处理
async myFunction(e) {
try {
// 业务逻辑
const result = await api.call()
if (!result) {
await e.reply("操作失败,请稍后重试")
return false
}
await e.reply("操作成功")
return true
} catch (err) {
logger.error("[插件名] 错误:", err)
await e.reply(`操作失败: ${err.message}`)
return false
}
}
5.6 日志规范
// 不同级别的日志
logger.trace("跟踪信息") // 最详细
logger.debug("调试信息") // 调试
logger.info("一般信息") // 普通
logger.mark("重要标记") // 重要
logger.warn("警告信息") // 警告
logger.error("错误信息") // 错误
// 带标签的日志
logger.info(`[插件名] 消息内容`)
logger.mark(`[图片生成][模板名] 完成`)
5.7 权限检查
// 检查主人权限
if (!e.isMaster) {
await e.reply("暂无权限,只有主人才能操作")
return false
}
// 检查群主权限
if (e.group && !e.member.is_owner) {
await e.reply("暂无权限,只有群主才能操作")
return false
}
// 检查管理员权限
if (e.group && !e.member.is_admin && !e.member.is_owner) {
await e.reply("暂无权限,只有管理员才能操作")
return false
}
5.8 常用工具函数
import _ from "lodash"
import moment from "moment"
// 睡眠等待
await util.sleep(1000) // 1秒
// 随机数
_.random(1, 100)
// 日期格式化
moment().format("YYYY-MM-DD HH:mm:ss")
// 数组分块
_.chunk(array, 10)
// 去重
_.uniq(array)
// 深拷贝
_.cloneDeep(obj)
// 获取对象值
_.get(obj, "path.to.value", defaultValue)
附录
A. 完整插件示例
// plugins/my-plugin/apps/index.js
import plugin from "../../../lib/plugins/plugin.js"
import { puppeteer } from "../model/index.js"
import Config from "../components/Config.js"
export class MyPlugin extends plugin {
constructor() {
super({
name: "我的插件",
dsc: "插件描述",
event: "message",
priority: 5000,
rule: [
{
reg: "^#我的帮助$",
fnc: "help"
},
{
reg: "^#我的设置$",
fnc: "settings",
permission: "master"
}
]
})
}
async help(e) {
const helpCfg = Config.getConfig("help")
const img = await puppeteer.render("help/index", {
helpCfg,
helpGroup: [
{
group: "基础功能",
list: [
{ icon: 1, title: "#命令1", desc: "功能1" },
{ icon: 2, title: "#命令2", desc: "功能2" }
]
}
],
bg: "default.jpg",
colCount: 3
}, { e, scale: 1.2 })
if (img) {
await e.reply(img)
}
return true
}
async settings(e) {
// 设置逻辑
return true
}
}
B. 参考资源
6. 机器人账号操作详解
6.1 Bot 对象结构
// Bot 对象结构(以 OneBotv11 为例)
Bot[uin] = {
// 基础信息
uin: 123456789,
nickname: "机器人昵称",
avatar: "https://q.qlogo.cn/g?b=qq&s=0&nk=123456789",
// 适配器信息
adapter: OneBotv11Adapter,
ws: WebSocket,
// 统计信息
stat: {
start_time: 1234567890,
packet_lost: 0,
message_received: 100,
message_sent: 50
},
// 好友列表
fl: Map<user_id, friend_info>,
// 群组列表
gl: Map<group_id, group_info>,
// 群成员列表
gml: Map<group_id, Map<user_id, member_info>>,
// Cookie 信息
cookies: {
"qun.qq.com": "cookie_string",
"qzone.qq.com": "cookie_string"
},
// CSRF Token
bkn: 123456789
}
6.2 发送消息
发送好友消息
// 方式1:使用 pickFriend
await Bot.pickFriend(user_id).sendMsg(message)
// 方式2:通过 Bot 对象
await Bot[bot_id].pickFriend(user_id).sendMsg(message)
// 方式3:使用 Bot.sendFriendMsg
await Bot.sendFriendMsg(bot_id, user_id, message)
// 实际示例
async sendFriendMsg(e) {
let bot = Bot[bot_id] //指定特定Bot
let qq = 123456789
// 检查是否为好友
if (!bot.fl.get(Number(qq))) {
return e.reply("❎ 好友列表查无此人")
}
// 发送消息
await bot.pickFriend(qq).sendMsg(e.message)
.then(() => e.reply(`✅ ${qq} 私聊消息已送达`))
.catch(err => logger.error("发送失败", err))
}
发送群消息
// 方式1:使用 pickGroup
await Bot.pickGroup(group_id).sendMsg(message)
// 方式2:通过群对象
await e.group.sendMsg(message)
// 实际示例
async sendGroupMsg(e) {
let bot = Bot
let group_id = 123456789
// 检查是否在群中
if (!bot.gl.get(Number(group_id))) {
return e.reply("❎ 群聊列表查无此群")
}
// 发送消息
await bot.pickGroup(group_id).sendMsg(e.message)
.then(() => e.reply(`✅ ${group_id} 群聊消息已送达`))
.catch(err => logger.error("发送失败", err))
}
// 批量发送群消息
async sendGroupListMsg(e) {
let group_ids = [123456, 789012, 345678]
for (let group_id of group_ids) {
await Bot.pickGroup(group_id).sendMsg(e.message)
await sleep(5000) // 间隔5秒,防止风控
}
}
6.3 获取列表信息
获取好友列表
// 获取好友数组
const friendArray = await Bot.getFriendArray()
// 返回: [{ user_id, nickname, ... }, ...]
// 获取好友ID列表
const friendList = Bot.getFriendList()
// 返回: [123456, 789012, ...]
// 获取好友Map
const friendMap = Bot.getFriendMap()
// 返回: Map<user_id, friend_info>
// 遍历好友列表
for (const [user_id, info] of Bot.fl) {
console.log(user_id, info.nickname)
}
获取群组列表
// 获取群组数组
const groupArray = await Bot.getGroupArray()
// 返回: [{ group_id, group_name, ... }, ...]
// 获取群组ID列表
const groupList = Bot.getGroupList()
// 获取群组Map
const groupMap = Bot.getGroupMap()
// 遍历群组列表
for (const [group_id, info] of Bot.gl) {
console.log(group_id, info.group_name)
}
6.4 机器人资料管理
修改昵称
// 设置昵称
await Bot.setNickname("新昵称")
// 或通过 pickFriend
await Bot.pickFriend(Bot.uin).setNickname("新昵称")
修改头像
// 设置头像(支持URL、Buffer、base64)
await Bot.setAvatar("https://example.com/avatar.jpg")
await Bot.setAvatar(Buffer.from(...))
await Bot.setAvatar("base64://...")
设置资料
// 设置完整资料
await Bot.setProfile({
nickname: "昵称",
sex: 1, // 0: 未知, 1: 男, 2: 女
age: 18,
sign: "个性签名"
})
6.5 QQ空间操作
获取说说列表
import request from "../lib/request/request.js"
async getQzone(num = 20, pos = 0) {
const url = "https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6"
return await request.get(url, {
headers: {
Cookie: Bot.cookies["qzone.qq.com"]
},
params: {
uin: Bot.uin,
pos,
num,
g_tk: Bot.bkn,
format: "json"
},
responseType: "json"
})
}
// 使用
let list = await getQzone(5, 0)
console.log(list.msglist)
发表说说
async setQzone(content, images = []) {
const url = "https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_publish_v6"
return request.post(url, {
headers: {
Cookie: Bot.cookies["qzone.qq.com"]
},
params: { g_tk: Bot.bkn },
data: {
con: content,
hostuin: Bot.uin,
format: "json"
},
responseType: "json"
})
}
删除说说
async delQzone(tid, t1_source) {
const url = "https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_delete_v6"
return request.post(url, {
headers: { Cookie: Bot.cookies["qzone.qq.com"] },
params: { g_tk: Bot.bkn },
data: { tid, t1_source, hostuin: Bot.uin },
responseType: "json"
})
}
6.6 好友管理
删除好友
// 删除好友
await Bot.pickFriend(user_id).delete()
// 删除后刷新好友列表
await Bot.getFriendMap()
处理好友申请
// 监听好友申请
Bot.on("request.friend.add", async (e) => {
// 同意申请
await e.approve(true)
// 拒绝申请
await e.approve(false, "拒绝理由")
})
// 主动处理
await Bot.setFriendAddRequest(flag, true, "备注")
点赞
// 给好友点赞
await Bot.pickFriend(user_id).thumbUp(10) // 点赞10次
6.7 群公告管理
获取群公告列表
async getAnnouncelist(group_id) {
const url = "https://web.qun.qq.com/cgi-bin/announce/get_t_list"
return await request.get(url, {
headers: { Cookie: Bot.cookies["qun.qq.com"] },
params: {
bkn: Bot.bkn,
qid: group_id,
ft: 23,
s: 0,
n: 20
},
responseType: "json"
})
}
发送群公告
async setAnnounce(group_id, msg, img) {
const data = {
qid: group_id,
bkn: Bot.bkn,
text: msg,
pinned: 0,
type: 1
}
if (img) {
// 上传图片逻辑
const res = await uploadImg(img)
data.pic = res.id
}
let url = `https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${Bot.bkn}`
return await request.post(url, {
data,
headers: { Cookie: Bot.cookies["qun.qq.com"] }
})
}
删除群公告
async delAnnounce(group_id, num) {
// 先获取公告fid
let fid = await getAnnouncelist(group_id, num)
let url = "https://web.qun.qq.com/cgi-bin/announce/del_feed"
return await request.post(url, {
params: { bkn: Bot.bkn },
data: { fid: fid.fid, qid: group_id },
headers: { Cookie: Bot.cookies["qun.qq.com"] }
})
}
7. 群聊管理详解
7.1 群基础操作
获取群信息
// 获取群信息
const groupInfo = await Bot.pickGroup(group_id).getInfo()
// 获取群头像URL
const avatarUrl = Bot.pickGroup(group_id).getAvatarUrl()
// 返回: https://p.qlogo.cn/gh/{group_id}/{group_id}/0
设置群名称
await Bot.pickGroup(group_id).setName("新群名")
// 或
await e.group.setName("新群名")
设置群头像
await Bot.pickGroup(group_id).setAvatar("https://example.com/group-avatar.jpg")
退群/解散群
// 退群
await Bot.pickGroup(group_id).quit()
// 解散群(需要群主权限)
await Bot.pickGroup(group_id).quit(true)
7.2 成员管理
获取成员信息
// 获取成员数组
const memberArray = await Bot.pickGroup(group_id).getMemberArray()
// 获取成员Map
const memberMap = await Bot.pickGroup(group_id).getMemberMap()
// 获取特定成员信息
const memberInfo = await Bot.pickGroup(group_id).pickMember(user_id).getInfo()
// 获取成员头像
const avatarUrl = Bot.pickGroup(group_id).pickMember(user_id).getAvatarUrl()
禁言成员
// 禁言单个成员(秒)
await Bot.pickGroup(group_id).muteMember(user_id, 3600) // 禁言1小时
// 通过成员对象
await e.group.pickMember(user_id).mute(3600)
// 批量禁言
async muteMembers(group_id, user_ids, duration) {
for (let user_id of user_ids) {
await Bot.pickGroup(group_id).muteMember(user_id, duration)
}
}
解除禁言
// 解除禁言(duration设为0)
await Bot.pickGroup(group_id).muteMember(user_id, 0)
踢出成员
// 踢出成员
await Bot.pickGroup(group_id).kickMember(user_id)
// 踢出并拉黑
await Bot.pickGroup(group_id).kickMember(user_id, true)
// 批量踢人
async BatchKickMember(group_id, user_ids) {
// 每20个一组
for (let chunk of _.chunk(user_ids, 20)) {
await request.post("https://qun.qq.com/cgi-bin/qun_mgr/delete_group_member", {
data: {
gc: group_id,
ul: chunk.join("|"),
flag: 0,
bkn: Bot.bkn
}
})
await sleep(2000) // 间隔2秒
}
}
设置管理员
// 设置管理员
await Bot.pickGroup(group_id).setAdmin(user_id, true)
// 取消管理员
await Bot.pickGroup(group_id).setAdmin(user_id, false)
设置群名片
await Bot.pickGroup(group_id).setCard(user_id, "新名片")
设置群头衔
// 设置专属头衔(群主权限)
await Bot.pickGroup(group_id).setTitle(user_id, "头衔名称", -1) // -1表示永久
// 成员自己申请头衔
await e.group.setTitle(e.user_id, "申请的头衔")
7.3 全体禁言
// 开启全体禁言
await Bot.pickGroup(group_id).muteAll(true)
// 关闭全体禁言
await Bot.pickGroup(group_id).muteAll(false)
// 或通过 e.group
await e.group.muteAll(true)
7.4 群文件管理
获取群文件系统
const fs = Bot.pickGroup(group_id).fs
// 获取文件系统信息
await fs.df() // disk free
// 列出根目录文件
await fs.ls()
// 列出文件夹内文件
await fs.ls(folder_id)
上传群文件
// 上传文件
await Bot.pickGroup(group_id).sendFile("/path/to/file.txt", "file.txt")
// 上传到指定文件夹
await Bot.pickGroup(group_id).fs.upload("/path/to/file.txt", folder_id, "file.txt")
删除群文件
await Bot.pickGroup(group_id).fs.rm(file_id, busid)
创建文件夹
await Bot.pickGroup(group_id).fs.mkdir("新文件夹")
7.5 群精华消息
// 设置精华消息
await Bot.setEssenceMessage(message_id)
// 移除精华消息
await Bot.removeEssenceMessage(message_id)
// 获取精华消息列表
await Bot.pickGroup(group_id).getEssence()
7.6 定时群管任务
import schedule from "node-schedule"
// 设置定时禁言
async setMuteTask(group_id, cron, type, bot_id) {
// cron 格式: "0 0 22 * * ?" 每天22:00
const task = {
group_id,
cron,
type, // true: 禁言, false: 解禁
bot_id
}
// 保存到Redis
await redis.set(`yenai:muteTask:${group_id}:${type}`, JSON.stringify(task))
// 创建定时任务
schedule.scheduleJob(cron, async () => {
await Bot.pickGroup(group_id).muteAll(type)
})
}
// 取消定时任务
async delMuteTask(group_id, type) {
await redis.del(`yenai:muteTask:${group_id}:${type}`)
}
7.7 群成员活跃度统计
// 获取不活跃成员
async getNoactiveList(group_id, time, unit) {
// time: 时间数值, unit: 单位(天/周/月)
const members = await Bot.pickGroup(group_id).getMemberMap()
let inactiveList = []
for (const [user_id, info] of members) {
// 根据 last_sent_time 判断
if (info.last_sent_time < Date.now() / 1000 - time * unit) {
inactiveList.push(info)
}
}
return inactiveList
}
// 获取从未发言的成员
async getNeverSpeak(group_id) {
const members = await Bot.pickGroup(group_id).getMemberMap()
return Array.from(members.values()).filter(member =>
member.last_sent_time === 0 || !member.last_sent_time
)
}
8. 实用工具函数
8.1 权限检查工具
/**
* 检查权限
* @param {object} e - 事件对象
* @param {string} permission - 用户所需权限 (master/admin/owner/all)
* @param {string} role - Bot所需权限 (admin/owner/all)
* @returns {boolean}
*/
function checkPermission(e, permission = "all", role = "all") {
// 检查Bot权限
if (role === "owner" && !e.group.is_owner) {
e.reply("❎ Bot权限不足,需要群主权限")
return false
}
if (role === "admin" && !e.group.is_admin && !e.group.is_owner) {
e.reply("❎ Bot权限不足,需要管理员权限")
return false
}
// 检查用户权限
if (!e.isMaster) {
const member = e.group.pickMember(e.user_id)
if (permission === "master") {
e.reply("❎ 该命令仅限主人可用")
return false
} else if (permission === "owner" && !member.is_owner) {
e.reply("❎ 该命令仅限群主可用")
return false
} else if (permission === "admin" && !member.is_admin && !member.is_owner) {
e.reply("❎ 该命令仅限管理可用")
return false
}
}
return true
}
8.2 消息处理工具
获取引用消息
/**
* 获取引用消息
* @param {object} e - 消息事件
* @param {object} options - 选项
* @param {boolean} options.img - 是否获取图片
* @param {boolean} options.file - 是否获取文件
*/
async function takeSourceMsg(e, { img, file } = {}) {
let source = ""
// 方式1:使用 getReply
if (e.getReply) {
source = await e.getReply()
}
// 方式2:通过 source 获取
else if (e.source) {
if (e.group?.getChatHistory) {
source = (await e.group.getChatHistory(e.source.seq, 1)).pop()
} else if (e.friend?.getChatHistory) {
source = (await e.friend.getChatHistory(e.source.time, 1)).pop()
}
}
if (!source) return false
// 提取图片
if (img) {
let imgArr = []
for (let i of source.message) {
if (i.type === "image") imgArr.push(i.url)
}
return imgArr.length ? imgArr : false
}
// 提取文件
if (file) {
if (source.message[0].type === "file") {
let { fid } = source.message[0]
return e.group?.getFileUrl(fid)
}
return false
}
return source
}
创建转发消息
/**
* 创建转发消息
* @param {object} e - 事件对象
* @param {Array} msgList - 消息列表
* @param {object} options - 选项
*/
async function getforwardMsg(e, msgList, { xmlTitle = "聊天记录" } = {}) {
const forwardMsg = []
for (let msg of msgList) {
forwardMsg.push({
user_id: Bot.uin,
nickname: Bot.nickname,
message: Array.isArray(msg) ? msg : [msg]
})
}
const msg = Bot.makeForwardMsg(forwardMsg)
return e.reply(msg)
}
8.3 Cookie 处理工具
/**
* 获取并解析 Cookie
* @param {string} domain - 域名,如 "qun.qq.com"
* @param {object} bot - Bot对象
* @param {boolean} transformation - 是否转换为Puppeteer格式
*/
function getck(domain, bot = Bot, transformation = false) {
let cookie = bot.cookies[domain]
function parseCkString(str) {
const pairs = str.split(";")
const obj = {}
pairs.forEach(pair => {
const [key, value] = pair.trim().split("=")
if (key) obj[key] = decodeURIComponent(value)
})
return obj
}
const ck = parseCkString(cookie)
if (transformation) {
// 转换为Puppeteer浏览器使用的格式
let arr = []
for (let i in ck) {
arr.push({
name: i,
value: ck[i],
domain: domain,
path: "/",
expires: Date.now() + 3600 * 1000
})
}
return arr
}
return ck
}
/**
* 计算 GTK (用于QQ空间等接口)
* @param {string} skey - skey值
*/
function getGtk(skey) {
let hash = 5381
for (let i = 0; i < skey.length; i++) {
hash += (hash << 5) + skey.charCodeAt(i)
}
return hash & 0x7fffffff
}
8.4 数字转换工具
/**
* 中文数字转阿拉伯数字
* @param {string} chinaNum - 中文数字,如 "一百二十三"
*/
function translateChinaNum(chinaNum) {
if (!chinaNum) return 0
const chinaNumMap = {
'零': 0, '一': 1, '二': 2, '三': 3, '四': 4,
'五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
'十': 10, '百': 100, '千': 1000, '万': 10000,
'壹': 1, '贰': 2, '叁': 3, '肆': 4, '伍': 5,
'陆': 6, '柒': 7, '捌': 8, '玖': 9
}
let result = 0
let temp = 0
for (let char of chinaNum) {
const num = chinaNumMap[char]
if (num >= 10) {
if (temp === 0) temp = 1
result += temp * num
temp = 0
} else {
temp = temp * 10 + num
}
}
return result + temp
}
8.5 限流工具
/**
* 每日次数限制
* @param {number} userId - 用户ID
* @param {string} key - 限制键
* @param {number} maxLimit - 最大次数
*/
async function limit(userId, key, maxLimit) {
if (maxLimit <= 0) return true
let redisKey = `plugin:${key}:limit:${userId}`
let nowNum = await redis.get(redisKey)
if (nowNum > maxLimit) return false
if (!nowNum) {
// 设置过期时间为当天结束
const expire = moment().add(1, "days").startOf("day").diff(undefined, "second")
await redis.set(redisKey, 1, { EX: expire })
} else {
await redis.incr(redisKey)
}
return true
}
// 使用
if (!await limit(e.user_id, "command_name", 10)) {
return e.reply("今日次数已用完")
}
8.6 异常处理工具
/**
* 统一异常处理
* @param {object} e - 事件对象
* @param {Error} error - 错误对象
* @param {object} options - 选项
*/
function handleException(e, error, { MsgTemplate } = {}) {
if (!(error instanceof Error)) return false
let errMsg = error.message
logger.error(error)
if (MsgTemplate) {
errMsg = MsgTemplate.replace(/{error}/g, errMsg)
}
return e.reply(errMsg)
}
// 自定义错误类
class ReplyError extends Error {
constructor(message) {
super(message)
this.name = "ReplyError"
}
}
8.7 异步池工具
/**
* 异步池,控制并发数
* @param {number} poolLimit - 并发限制
* @param {Array} array - 任务数组
* @param {Function} iteratorFn - 迭代函数
*/
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = []
const executing = []
for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array))
ret.push(p)
if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1))
executing.push(e)
if (executing.length >= poolLimit) {
await Promise.race(executing)
}
}
}
return Promise.all(ret)
}
// 使用示例
await asyncPool(5, userList, async (user) => {
await sendMsg(user.id, "消息")
})
文档版本: 2.0
最后更新: 2026-02-23
提取转发消息内图片(高级技巧)
感谢 m0_69204072 提供的参考实现。
核心能力
在 Yunzai 中,转发消息(合并转发)内的图片无法直接通过 e.img 获取,需要递归解析转发消息结构。此技巧支持:
- 单层转发消息内图片提取
- 双层/多层嵌套转发递归解析
- 兼容 TRSS-Yunzai / ICQQ / NapCat 不同的消息结构
关键 API
// 获取转发消息内容(通过 resid/forwardId)
const forwardMsgs = await e.bot.getForwardMsg(forwardId)
// 获取引用消息(通过 reply_id)
const sourceMsg = await e.getReply(e.reply_id, { message_type: e.message_type })
// 获取聊天历史(通过 source.seq)
const chatHistory = e.isGroup
? await e.group.getChatHistory(e.source.seq, 1)
: await e.friend.getChatHistory(e.source.seq, 1)
转发消息结构适配
不同框架的转发消息结构不同,需要兼容多种路径:
// 转发消息类型判断
msg.type === 'multimsg' // multimsg 类型,resid 在 msg.resid 或 msg.id
msg.type === 'forward' // TRSS/ICQQ 封装的 forward 类型
msg.type === 'text'/'json' // 文本中包含 [CQ:forward,id=xxx]
// forward 类型内容提取(兼容多种存储路径)
const forwardContent = Array.isArray(msg.data?.content) ? msg.data.content // 优先:msg.data.content
: Array.isArray(msg.content) ? msg.content
: msg.content?.message ? [msg.content]
: Array.isArray(msg.messages) ? msg.messages
: [];
// 子消息段提取
const subMessages = Array.isArray(itm.message) ? itm.message
: Array.isArray(itm.msg) ? itm.msg
: (itm.content ? [itm.content] : []);
完整示例:提取转发图片插件
export class GetForwardImgPlugin extends plugin {
constructor() {
super({
name: '提取转发图片',
dsc: '提取转发消息图片(支持多层转发)',
event: 'message',
priority: -5000,
rule: [{ reg: /^#取图片$/, fnc: 'getForwardImages' }]
});
}
async getForwardImages(e) {
const imgList = await this.extractImages(e);
if (!imgList.length) return e.reply('❌ 未检测到任何图片');
await e.reply(`✅ 提取到 ${imgList.length} 张图片:`);
// 分批发送,每批最多9张
for (let i = 0; i < imgList.length; i += 9) {
const batch = imgList.slice(i, i + 9).map(url => segment.image(url));
await e.reply(batch);
}
}
async extractImages(e) {
let imgList = e.img || [];
// 递归解析消息段
const parseMessages = async (messages, depth = 0) => {
if (depth > 5) return; // 防止无限递归
for (const msg of messages) {
if (!msg || typeof msg !== 'object') continue;
// 图片类型
if (msg.type === 'image' || msg.type === 'img') {
const url = msg.url || msg.data?.url || msg.data?.file || msg.file;
if (url && !imgList.includes(url)) imgList.push(url);
}
// multimsg 转发
else if (msg.type === 'multimsg') {
const fid = msg.resid || msg.id;
if (fid && e.bot?.getForwardMsg) {
const fwdMsgs = await e.bot.getForwardMsg(fid).catch(() => []);
for (const fm of fwdMsgs) {
if (Array.isArray(fm.message)) await parseMessages(fm.message, depth + 1);
}
}
}
// forward 类型(TRSS/ICQQ)
else if (msg.type === 'forward') {
const content = Array.isArray(msg.data?.content) ? msg.data.content
: Array.isArray(msg.content) ? msg.content
: Array.isArray(msg.messages) ? msg.messages : [];
for (const itm of content) {
const sub = Array.isArray(itm?.message) ? itm.message
: Array.isArray(itm?.msg) ? itm.msg : [];
if (sub.length) await parseMessages(sub, depth + 1);
}
}
// 文本中的 CQ 转发码
else if (['text', 'json', 'plain'].includes(msg.type)) {
const text = typeof msg.text === 'string' ? msg.text : JSON.stringify(msg.data || '');
const match = text.match(/\[CQ:forward,id=(\d+)\]/) || text.match(/"resid":"(.*?)"/);
if (match?.[1] && e.bot?.getForwardMsg) {
const fwdMsgs = await e.bot.getForwardMsg(match[1]).catch(() => []);
for (const fm of fwdMsgs) {
if (Array.isArray(fm.message)) await parseMessages(fm.message, depth + 1);
}
…(truncated)