├── .gitignore ├── Abandon ├── README.md ├── 短网址.js └── mcsm管理器.js ├── Demo ├── README.md └── 表情按钮Demo.js ├── Archiving ├── README.md ├── 随机小姐姐视频.js ├── cosplay.js ├── cosplay频道版.js ├── 播放.js ├── 点歌.js ├── 清凉图.js └── 签名状态检测.js ├── 综合帮助.js ├── 三角洲每日密码.js ├── 唱鸭.js ├── 哈希转磁力.js ├── mcwiki.js ├── GH仓库.js ├── 随机超能力.js ├── 快举报.js ├── 取群员列表.js ├── mc服务器状态.js ├── 点赞续火.js ├── 抽头衔.js ├── 智谱绘图.js ├── icp查询.js ├── B站卡片转链接.js ├── 快讯.js ├── 手办化.js ├── 文案.js ├── 米游社cos.js ├── 自动优选签名IP.js ├── README.md ├── 吊图.js ├── new-api.js └── 智谱GLM.js /.gitignore: -------------------------------------------------------------------------------- 1 | /hide -------------------------------------------------------------------------------- /Abandon/README.md: -------------------------------------------------------------------------------- 1 | # 已放弃开发的插件 2 | 3 | 此处的插件仍未完成开发 4 | 仅供参考使用,无法保证可用性 5 | 6 | ## 插件介绍 7 | 8 | ### MCSM管理器 9 | 10 | 对接 mcsm 控制面板,通过 api 进行控制 11 | 需自行填写 key 和 url 12 | 13 | ### 短网址 14 | 15 | 通过api将长网址转换为短网址 16 | -------------------------------------------------------------------------------- /Demo/README.md: -------------------------------------------------------------------------------- 1 | # Demo类插件 2 | 3 | 此处的插件已经归档,不再维护 4 | 仅供参考使用,无法保证可用性 5 | 6 | ## 插件介绍 7 | 8 | ### 表情按钮Demo 9 | 10 | 类似官方机器人的按钮,但是用表情实现 11 | 需要 ICQQ 1.5.8 及以上版本 12 | 13 | ### 播放 14 | 15 | 发送播放后跟上音频的链接即可 16 | 主要提供发送语音的示例代码 17 | 18 | > 需安装 ffmpeg 否则无法生效,默认开启高清语音,需安装枫叶,土块等包含高清语音的插件或独立的[高清语音模块](https://github.com/xiaotian2333/YunzaiBOT-HD-Voice-module) -------------------------------------------------------------------------------- /Archiving/README.md: -------------------------------------------------------------------------------- 1 | # 已归档插件 2 | 3 | 此处的插件已经归档,不再维护 4 | 仅供参考使用,无法保证可用性 5 | 6 | ## 插件介绍 7 | 8 | ### 签名状态检测 9 | 10 | 检测各签名服务的可用性,可修改插件内的配置自定义检测的服务 11 | 12 | ### 小姐姐视频 13 | 14 | 随机发送一个小姐姐视频 15 | 感谢桑帛云API 16 | 17 | ### 清凉图 18 | 19 | 返回一些清凉的图片 20 | 可自己更换接口 21 | 目前有4个触发词 22 | 23 | ``` 触发词 24 | 清凉图 - 分级大概为12+ 25 | 二元图 - 二次元相关的图片 26 | 三元图 - 三次元相关的图片 27 | r16 - 分级大概为16+ 28 | ``` 29 | 30 | ### 点歌 31 | 32 | 点歌后跟上歌曲名称即可点歌 33 | 支持vip歌曲 34 | -------------------------------------------------------------------------------- /Archiving/随机小姐姐视频.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | 5 | import plugin from '../../lib/plugins/plugin.js' 6 | 7 | export class example extends plugin { 8 | constructor () { 9 | super({ 10 | name: '小姐姐视频', 11 | dsc: '发送随机小姐姐视频', 12 | // 匹配的消息类型,参考https://oicqjs.github.io/oicq/#events 13 | event: 'message', 14 | priority: 5000, 15 | rule: [ 16 | { 17 | reg: '^#?小姐姐视频', 18 | fnc: 'start' 19 | } 20 | ] 21 | }) 22 | } 23 | 24 | async start(e) { 25 | e.reply(segment.video('https://api.lolimi.cn/API/xjj/xjj.php')) 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /综合帮助.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | // help图片生成方式:使用锅巴备份喵喵插件,然后改里面的文案,发送帮助保存图片就行了。最后把备份的还原回去 5 | import plugin from '../../lib/plugins/plugin.js' 6 | 7 | export class help extends plugin { 8 | constructor() { 9 | super({ 10 | name: '综合帮助', 11 | dsc: '发送综合帮助图片', 12 | event: 'message', 13 | priority: -99999, 14 | rule: [ 15 | { 16 | reg: /^#?(云崽)?(命令|帮助|菜单|help|说明|功能|指令|使用说明)$/, 17 | fnc: 'help' 18 | } 19 | ] 20 | }) 21 | } 22 | 23 | async help(e) { 24 | e.reply(segment.image('C:/BOT/Yunzai-Bot-res/resources/help.jpg')) 25 | return true 26 | } 27 | } -------------------------------------------------------------------------------- /Archiving/cosplay.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | 5 | // 从 '../../lib/plugins/plugin.js' 文件中导入 plugin 6 | import plugin from '../../lib/plugins/plugin.js' 7 | 8 | // 定义一个名为 example 的类,继承自 plugin 类 9 | export class example extends plugin { 10 | constructor() { 11 | super({ 12 | name: 'cosplay', 13 | dsc: '返回一组cosplay图片,数据来自葫芦侠,发送图片链接而非图片以节省服务器流量', 14 | event: 'message', 15 | priority: 5000, 16 | rule: [{ 17 | reg: '^#?cosplay.*$', 18 | fnc: 'cos' 19 | } 20 | ] 21 | }) 22 | } 23 | 24 | async cos(e) { 25 | fetch('https://api.yujn.cn/api/cosplay.php?type=json') 26 | .then(response => { 27 | if (!response.ok) { 28 | logger.error('网络请求失败'); 29 | } 30 | return response.json(); 31 | }) 32 | .then(data => { 33 | let m = data.data.title + "\n" 34 | let n = data.data.images.join("\n---\n"); 35 | m = m + n 36 | e.reply(m) 37 | }) 38 | .catch(error => { 39 | //输出错误提示 40 | logger.error('获取错误:', error); 41 | }); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Archiving/cosplay频道版.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | 5 | // 从 '../../lib/plugins/plugin.js' 文件中导入 plugin 6 | import plugin from '../../lib/plugins/plugin.js' 7 | 8 | // 定义一个名为 example 的类,继承自 plugin 类 9 | export class example extends plugin { 10 | constructor() { 11 | super({ 12 | name: 'cosplay', 13 | dsc: '返回一组cosplay图片,数据来自葫芦侠,发送图片链接而非图片以节省服务器流量', 14 | event: 'message', 15 | priority: 5000, 16 | rule: [{ 17 | reg: '^#?cosplay.*$', 18 | fnc: 'cos' 19 | } 20 | ] 21 | }) 22 | } 23 | 24 | async cos(e) { 25 | fetch('https://api.yujn.cn/api/cosplay.php?type=json') 26 | .then(response => { 27 | if (!response.ok) { 28 | logger.error('网络请求失败'); 29 | } 30 | return response.json(); 31 | }) 32 | .then(data => { 33 | e.reply(data.data.title) 34 | data.data.images.forEach(image_url => { 35 | e.reply(segment.image(image_url)) 36 | }) 37 | .catch(error => { 38 | //输出错误提示 39 | logger.error('获取错误:', error); 40 | }); 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /三角洲每日密码.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | const plugin_name = '三角洲每日密码' 5 | 6 | /**统一网络请求函数,不管返回格式 7 | * @param url 要请求的url 8 | */ 9 | async function get_data(url) { 10 | let result = await fetch(url, { 11 | headers: { 12 | 'User-Agent': 'sjz (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 13 | }, 14 | }) 15 | 16 | return result 17 | } 18 | 19 | export class sjz extends plugin { 20 | constructor() { 21 | super({ 22 | name: plugin_name, 23 | event: 'message', 24 | priority: 4999, 25 | rule: [ 26 | { 27 | reg: /^#?(sjz|三角洲)?(每日|行动门)密码$/, 28 | fnc: 'sjz' 29 | }, 30 | ] 31 | }) 32 | } 33 | 34 | 35 | async sjz(e) { 36 | let data = await get_data('http://sjz-mrmm.api.xt-url.com/') 37 | data = await data.json() 38 | let msg = [ 39 | `${data.UpdatedData}` 40 | ] 41 | // 遍历data.passwords并输出key和value 42 | for (let key in data.passwords) { 43 | let value = data.passwords[key] 44 | msg.push(`【${key}】:${value}`) 45 | } 46 | msg = msg.join('\n') 47 | e.reply(msg, true) 48 | return true 49 | } 50 | } -------------------------------------------------------------------------------- /Archiving/播放.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | 5 | // 从 '../../lib/plugins/plugin.js' 文件中导入 plugin 6 | import plugin from '../../lib/plugins/plugin.js' 7 | 8 | // 定义一个名为 example 的类,继承自 plugin 类 9 | export class example extends plugin { 10 | // 构造函数 11 | constructor () { 12 | // 调用父类的构造函数 13 | super({ 14 | // 功能名称 15 | name: '播放', 16 | // 功能描述 17 | dsc: '播放指定的音频链接', 18 | // 匹配的消息类型,参考https://oicqjs.github.io/oicq/#events 19 | event: 'message', 20 | // 优先级,数字越小等级越高 21 | priority: 5000, 22 | // 定义匹配词 23 | rule: [ 24 | { 25 | // 命令正则匹配 26 | reg: '^播放', 27 | // 执行方法 28 | fnc: 'start' 29 | } 30 | ] 31 | }) 32 | } 33 | 34 | async start(e) { 35 | // 简化消息变量,同时方便调用 36 | let msg = e.msg 37 | // 删除不需要的部分 38 | msg = msg.replace('播放', ''); 39 | msg = msg.replace(/ /g, ""); 40 | // 如果 msg 为空,则返回 41 | if (!msg) return 42 | // 输出日志 43 | logger.mark('[播放]处理完毕,链接:', msg) 44 | // 使用 reply 方法回复消息 45 | // e.reply(segment.record(msg)) //普通语音 46 | e.reply(await uploadRecord(msg,0,false)) //高清语音,参数说明 :1.音频链接 2.音频时长 欺骗,0=关闭 3.压缩音质 47 | // 返回 true 拦截消息继续往下 48 | return true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Abandon/短网址.js: -------------------------------------------------------------------------------- 1 | import {URL} from 'url' 2 | 3 | /**统一网络请求函数,不管返回格式 4 | * @param url 要请求的url 5 | */ 6 | async function get_data(url) { 7 | let result = await fetch(url, { 8 | headers: { 9 | 'User-Agent': 'url (url by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 10 | } 11 | }) 12 | return result 13 | } 14 | 15 | function isValidURL(string) { 16 | try { 17 | new URL(string) 18 | return true 19 | } catch (_) { 20 | return false 21 | } 22 | } 23 | 24 | 25 | export class MemoryCheck extends plugin { 26 | constructor() { 27 | super({ 28 | name: '生成短链接', 29 | event: 'message', 30 | rule: [ 31 | { 32 | reg: "^#?短(链接|网址|连接)", 33 | fnc: 'url', 34 | permission: 'master' 35 | } 36 | ] 37 | }) 38 | } 39 | 40 | async url(e) { 41 | let msg = e.msg 42 | msg = msg.replace(/^#?短(链接|网址|连接)/, '') 43 | // 如果为空则返回 44 | if (!msg) { 45 | e.reply('请发送正确的链接') 46 | return false 47 | } 48 | // 不符合url格式返回 49 | if (!isValidURL(msg)) { 50 | e.reply('请发送正确的链接') 51 | return false 52 | } 53 | 54 | let result = await get_data(`https://oiapi.net/API/ShortLink/add?url=${msg}`) 55 | result = await result.json() 56 | e.reply(`=========${result.message}=========\n短链接:${result.data.url}\n过期时间:${result.data.time}`) 57 | return true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /唱鸭.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | 5 | // 从 '../../lib/plugins/plugin.js' 文件中导入 plugin 6 | import plugin from '../../lib/plugins/plugin.js' 7 | 8 | // 定义一个名为 example 的类,继承自 plugin 类 9 | export class example extends plugin { 10 | constructor() { 11 | super({ 12 | name: '唱鸭', 13 | dsc: '随机返回一段唱鸭的音频片段', 14 | event: 'message', 15 | priority: 5000, 16 | rule: [{ 17 | reg: '^#?唱[呀鸭吖丫]$', 18 | fnc: 'changya' 19 | } 20 | ] 21 | }) 22 | } 23 | 24 | async changya(e) { 25 | fetch('https://api.cenguigui.cn/api/singduck/') 26 | .then(response => { 27 | if (!response.ok) { 28 | logger.erro('网络请求失败'); 29 | } 30 | return response.json(); 31 | }) 32 | .then(data => { 33 | // 提取出data中的audioSrc并发送语音 34 | logger.debug('[唱鸭]获取到歌曲链接:',data.data.audioSrc) 35 | // this.e.reply(segment.record(data.data.audioSrc)) //普通语音 36 | e.reply(uploadRecord(data.data.audioSrc,0,false)) //高清语音,参数说明 :1.音频链接 2.音频时长 欺骗,0=关闭 3.压缩音质 37 | // 提取出data中的lyrics并发送歌词 38 | logger.debug('[唱鸭]获取到歌词:',data.data.lyrics) 39 | // 处理歌词换行 40 | let lyrics = data.data.lyrics.replace(/ /g, "\n"); 41 | lyrics = "歌手:" + data.data.nickname + "\n" + lyrics/* + "\n---\n歌曲链接\n" + data.data.audioSrc*/; 42 | e.reply(lyrics) 43 | }) 44 | .catch(error => { 45 | //输出错误提示 46 | logger.error('获取错误:', error); 47 | }); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /哈希转磁力.js: -------------------------------------------------------------------------------- 1 | const trackers = [ 2 | 'udp://tracker.opentrackr.org:1337/announce', 3 | 'udp://tracker.openbittorrent.com:6969/announce', 4 | 'udp://tracker.bittorrent.am:80/announce', 5 | 'udp://tracker.coppersurfer.tk:6969/announce', 6 | 'udp://tracker.leechers-paradise.org:6969/announce', 7 | 'udp://p4p.arenabg.com:1337/announce', 8 | 'udp://tracker.internetwarriors.net:1337/announce', 9 | 'udp://9.rarbg.to:2710/announce', 10 | 'udp://9.rarbg.me:2710/announce', 11 | 'udp://9.rarbg.to:2710/announce', 12 | 'udp://9.rarbg.me:2710/announce', 13 | 'udp://tracker.opentrackr.org:1337/announce', 14 | 'udp://tracker.coppersurfer.tk:6969/announce', 15 | 'udp://tracker.leechers-paradise.org:6969/announce', 16 | 'udp://tracker.openbittorrent.com:80/announce', 17 | 'udp://explodie.org:6969/announce', 18 | 'udp://p4p.arenabg.com:1337/announce', 19 | 'http://vps02.net.orel.ru:80/announce', 20 | 'http://tracker.files.fm:6969/announce', 21 | 'http://173.254.204.71:10068/announce' 22 | ] 23 | 24 | export class magnet extends plugin { 25 | constructor() { 26 | super({ 27 | name: 'magnet', 28 | dsc: '哈希转磁力', 29 | event: 'message', 30 | priority: 5000, 31 | rule: [ 32 | { 33 | reg: /^#?(哈希|hash)?转(磁力|bt|BT)(详细|扩展)?[0-9a-fA-F]{40}$/, 34 | fnc: 'hash' 35 | } 36 | ] 37 | }) 38 | } 39 | 40 | async hash(e) { 41 | let hash = e.msg 42 | hash = await hash.replace(/^#?(哈希|hash)?转(磁力|bt|BT)(详细|扩展)?/, '') 43 | let magnet = `magnet:?xt=urn:btih:${hash}` 44 | if (e.msg.includes('详细') || e.msg.includes('扩展')) { 45 | magnet += `&tr=${trackers.join('&tr=')}` 46 | } 47 | e.reply(magnet, true) 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /mcwiki.js: -------------------------------------------------------------------------------- 1 | // 插件作者github:https://github.com/Midnight-2004/mc-wiki 2 | // 本仓库(https://github.com/xiaotian2333/yunzai-plugins-Single-file)仅收集 3 | 4 | import plugin from '../../lib/plugins/plugin.js'; 5 | import puppeteer from '../../lib/puppeteer/puppeteer.js'; 6 | import { segment } from 'icqq'; 7 | import common from "../../lib/common/common.js"; 8 | 9 | export class QueryHandler extends plugin { 10 | constructor(query) { 11 | let rule = { 12 | reg: /^#mcwiki(.*)/, 13 | fnc: 'handleWikiQuery', 14 | } 15 | super({ 16 | name: 'mcwiki', 17 | dsc: 'mcwiki', 18 | event: 'message', 19 | priority: 5000, 20 | rule: [rule], 21 | }) 22 | } 23 | 24 | async handleWikiQuery(query) { 25 | let querywithoutwiki = (query + "").replace('#mcwiki', '') 26 | let encodeQuery = encodeURI(querywithoutwiki) 27 | let url = `https://zh.minecraft.wiki/w/${encodeQuery}` 28 | //await this.reply(url) 29 | const browser = await puppeteer.browserInit() 30 | const page = await browser.newPage() 31 | await page.goto(url) 32 | const height = await page.evaluate(() => document.documentElement.scrollHeight) 33 | if (height === 894) { 34 | return this.reply('搜索不到此结果,请检查关键词是否准确无误') 35 | } else 36 | await page.setViewport({ width: 1280, height }) 37 | const buff = await page.screenshot() 38 | await page.close() 39 | //await this.reply(segment.image(buff)) 40 | let message = [url] 41 | message.push(segment.image(buff)) 42 | message.push('受服务器网络波动影响,如果无法加载出正确的图片,还请您重新发送或点开上面的链接进入网页查看') 43 | return this.reply(await common.makeForwardMsg(this.e, message)) 44 | } 45 | } -------------------------------------------------------------------------------- /Archiving/点歌.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | // API接口需要KEY,请先注册 5 | let key = 'UmxWMFdtcEZlWFl5VjFsalMwc3JLMWRVWVZWblFUMDk=' //53bd859c7703a7812c828fe768cfb27e 6 | 7 | // 从 '../../lib/plugins/plugin.js' 文件中导入 plugin 8 | import plugin from '../../lib/plugins/plugin.js' 9 | 10 | // 定义一个名为 example 的类,继承自 plugin 类 11 | export class example extends plugin { 12 | // 构造函数 13 | constructor () { 14 | // 调用父类的构造函数 15 | super({ 16 | // 功能名称 17 | name: '点歌', 18 | // 功能描述 19 | dsc: '检测到github链接时发送仓库速览图', 20 | // 匹配的消息类型,参考https://oicqjs.github.io/oicq/#events 21 | event: 'message', 22 | // 优先级,数字越小等级越高 23 | priority: 5000, 24 | // 定义匹配词 25 | rule: [ 26 | { 27 | // 命令正则匹配 28 | reg: '^#?点歌.*$', 29 | // 执行方法 30 | fnc: 'start' 31 | } 32 | ] 33 | }) 34 | } 35 | 36 | async start(e) { 37 | // 简化消息变量,同时方便调用 38 | let msg = e.msg 39 | // 删除不需要的部分 40 | msg = msg.replace('#', ''); 41 | msg = msg.replace('点歌', ''); 42 | msg = msg.replace(' ', ''); 43 | // 如果 msg 为空,则返回 44 | if (!msg) { 45 | e.reply('点歌后跟上歌名,可点VIP歌曲') 46 | return true 47 | } 48 | fetch(`https://api.linhun.vip/api/qqyy?name=${msg}=1&n=1&apiKey=${key}`) 49 | .then(response => { 50 | if (!response.ok) { 51 | logger.erro('网络请求失败'); 52 | } 53 | return response.json(); 54 | }) 55 | .then(data => { 56 | // 提取出data中的audioSrc并发送语音 57 | logger.debug('[点歌]获取到歌曲链接:',data.mp3) 58 | // this.e.reply(segment.record(data.mp3)) //普通语音 59 | e.reply(uploadRecord(data.mp3,0,false)) //高清语音,参数说明 :1.音频链接 2.音频时长 欺骗,0=关闭 3.压缩音质 60 | }) 61 | .catch(error => { 62 | //输出错误提示 63 | logger.error('获取错误:', error); 64 | }); 65 | // 返回 true 拦截消息继续往下 66 | return true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /GH仓库.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | 5 | // 定义调用接口 6 | const baseurl = 'https://opengraph.githubassets.com/xiaotian' 7 | 8 | export class example extends plugin { 9 | constructor() { 10 | super({ 11 | name: 'GH仓库', 12 | dsc: '检测到github链接时发送仓库速览图', 13 | // 匹配的消息类型,参考https://oicqjs.github.io/oicq/#events 14 | event: 'message', 15 | priority: 3000, 16 | rule: [ 17 | { 18 | reg: '^(https://|http://)?github.com/[a-zA-Z0-9-]{1,39}/[a-zA-Z0-9_-]{1,100}(.git)?', 19 | fnc: 'start' 20 | } 21 | ] 22 | }) 23 | } 24 | 25 | async start(e) { 26 | // 简化消息变量,同时方便调用 27 | let msg = e.msg 28 | // 删除不需要的部分 29 | msg = msg.replace('https://', '') 30 | msg = msg.replace('http://', '') 31 | msg = msg.replace('.git', '') 32 | 33 | // 如果 msg 为空,则返回 34 | if (!msg) return 35 | 36 | // 提取用户名字段 37 | const name = msg.split('/')[1] 38 | // 提取仓库名字段 39 | const repo = msg.split('/')[2] 40 | // 构建完整的URL 41 | let url = baseurl + '/' + name + '/' + repo 42 | 43 | // 提取子页面字段 44 | const Subpage = msg.split('/')[3] 45 | if (Subpage == 'issues' || Subpage == 'pull' || Subpage == 'commit') { 46 | const Quantity = msg.split('/')[4]; 47 | // 如果是数字(issues、pr)或commit哈希值,则添加到url 48 | if (Quantity && (/^\d+$/.test(Quantity) || /^[a-f0-9]{40}$/.test(Quantity))) { 49 | url += '/' + Subpage + '/' + Quantity 50 | } 51 | } 52 | // 提取releases字段 53 | if (Subpage == 'releases') { 54 | let tag = msg.split('/')[4] 55 | // 如果tag字段是download表明这是下载链接,需替换为tag 56 | if (tag == 'download') { 57 | tag = 'tag' 58 | } 59 | const version = msg.split('/')[5] 60 | if (tag) { 61 | url += '/' + Subpage + '/' + tag + '/' + version 62 | } 63 | } 64 | 65 | // 发送消息 66 | e.reply(segment.image(url)) 67 | return true 68 | } 69 | } -------------------------------------------------------------------------------- /Archiving/清凉图.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | import plugin from '../../lib/plugins/plugin.js' 5 | 6 | //是否允许私聊使用,设为false则禁止私聊使用(主人除外) 7 | let group = true 8 | 9 | export class ql extends plugin { 10 | constructor() { 11 | super({ 12 | name: '清凉图', 13 | dsc: '图片', 14 | event: 'message', 15 | priority: 5000, 16 | rule: [ 17 | { 18 | reg: '清凉图', 19 | fnc: 'ql' 20 | }, 21 | { 22 | reg: '二元图', 23 | fnc: 'yuan2' 24 | }, 25 | { 26 | reg: '三元图', 27 | fnc: 'yuan3' 28 | }, 29 | { 30 | reg: 'r16', 31 | fnc: 'r16' 32 | } 33 | ] 34 | }) 35 | } 36 | 37 | async ql(e) { 38 | if (!group) 39 | if (e.isPrivate && !e.isMaster) { 40 | return true 41 | } 42 | e.reply(segment.image('https://imgapi.cn/cos.php')) 43 | return true 44 | } 45 | 46 | async yuan2(e) { 47 | if (!group) 48 | if (e.isPrivate && !e.isMaster) { 49 | return true 50 | } 51 | e.reply(segment.image('http://api.liangx.link/API/AGG.php')) 52 | return true 53 | } 54 | 55 | async yuan3(e) { 56 | if (!group) 57 | if (e.isPrivate && !e.isMaster) { 58 | return true 59 | } 60 | e.reply(segment.image('https://api.r10086.com/樱道随机图片api接口.php?图片系列=少女写真1')) 61 | return true 62 | } 63 | 64 | async r16(e) { 65 | if (!group) 66 | if (e.isPrivate && !e.isMaster) { 67 | return true 68 | } 69 | e.reply(segment.image('https://api.r10086.com/樱道随机图片api接口.php?图片系列=萝莉')) 70 | return true 71 | } 72 | } -------------------------------------------------------------------------------- /Demo/表情按钮Demo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 作者:xiaotian2333 3 | * 开源地址:https://github.com/xiaotian2333/yunzai-plugins-Single-file 4 | * 需要 ICQQ 1.5.8 及以上版本 5 | * 表情内容参考官方 https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html 6 | */ 7 | 8 | 9 | // 接受到表情的回应处理逻辑 10 | export class Reaction extends plugin { 11 | constructor() { 12 | super({ 13 | name: '监听表情', 14 | event: 'notice.group.reaction', 15 | priority: -9999, 16 | rule: [ 17 | { 18 | fnc: 'reaction', 19 | log: false 20 | } 21 | ] 22 | }) 23 | } 24 | 25 | async reaction(e) { 26 | // 过滤自己 27 | if (e.user_id == e.self_id) return false 28 | // 过滤非管理员 29 | if (!e.isMaster) return false 30 | 31 | //console.dir(e, { depth: null }) // 输出日志 32 | //e.reply(`收到表情回应\n来源群:${e.group_id}\n来源QQ:${e.user_id}\n表情ID:${e.id}\n是否管理员:${e.isMaster}\nseq:${e.seq}`) 33 | 34 | // 判断表情ID 35 | if (e.id == "128076") { 36 | e.reply(`收到👌表情`) 37 | 38 | // 接下来可以处理同意逻辑 39 | let data = await redis.type(`AN_Demo/${e.seq}`) 40 | if (data == 'none') { 41 | e.reply("按钮已过期") 42 | return true 43 | } 44 | 45 | // 获取业务数据 46 | data = await redis.get(`AN_Demo/${e.seq}`) 47 | // 及时删除业务数据避免冲突 48 | redis.del(`AN_Demo/${e.seq}`) 49 | // 处理业务逻辑 50 | e.reply(`对应消息的业务数据:${data}`) 51 | return true 52 | } 53 | 54 | if (e.id == "10060") { 55 | e.reply(`收到❌表情`) 56 | // 接下来可以处理拒绝逻辑 57 | // 略 58 | return true 59 | } 60 | 61 | } 62 | } 63 | 64 | // 发送按钮消息 65 | export class example extends plugin { 66 | constructor() { 67 | super({ 68 | name: '按钮Demo', 69 | dsc: '按钮Demo', 70 | event: 'message', 71 | priority: 5000, 72 | rule: [{ 73 | reg: '^#?按钮$', 74 | fnc: 'an' 75 | } 76 | ] 77 | }) 78 | } 79 | 80 | async an(e) { 81 | const data = await e.reply("按钮测试\n按👌同意,❌拒绝") 82 | //console.dir(await data, { depth: null }) // 输出日志 83 | Bot.pickGroup(e.group_id).setReaction(data.seq, 128076, 1) 84 | Bot.pickGroup(e.group_id).setReaction(data.seq, 10060, 1) 85 | // 通过redis存储seq,建议设置过期时间 86 | // EX: 600 为600秒过期 87 | await redis.set(`AN_Demo/${data.seq}`, "要存储的业务数据", { EX: 600 }) 88 | } 89 | } -------------------------------------------------------------------------------- /随机超能力.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | // 单文件版本已停止更新,请安装新版本使用 https://github.com/xiaotian2333/special-ability 4 | // 欢迎投稿超能力列表,前往新版本页面即可查看要求 5 | 6 | // 能力列表链接 7 | const cnl_url = 'https://oss.xt-url.com/超能力/超能力列表.txt' 8 | const cnl_url_pro = 'https://oss.xt-url.com/超能力/超能力列表pro.txt' 9 | // 能力列表pro 10 | const fzy_url = 'https://oss.xt-url.com/超能力/副作用列表.txt' 11 | const fzy_url_pro = 'https://oss.xt-url.com/超能力/副作用列表pro.txt' 12 | // 本地列表路径,具体到txt文件,要求utf-8编码 13 | const cnl_file = 'D:/资料/github-blog仓库/oss/超能力/测试列表1.txt' 14 | const fzy_file = 'D:/资料/github-blog仓库/oss/超能力/测试列表2.txt' 15 | // 功能开关选项 16 | const ispro = false 17 | const islocal = false 18 | 19 | import fs from 'fs' 20 | 21 | /** 22 | * 获取能力列表,返回一个数组 23 | * @param url 资源列表链接 24 | */ 25 | async function get_data(url) { 26 | let result = await fetch(url, { 27 | headers: { 28 | 'User-Agent': 'Special-ability(author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 29 | } 30 | }) 31 | result = await result.text() 32 | return result.split(/\r?\n/) 33 | } 34 | 35 | /** 36 | * 获取本地列表,返回一个数组 37 | * @param filePath 本地文件路径 38 | */ 39 | async function read_File(filePath) { 40 | try { 41 | let data = fs.readFileSync(filePath, 'utf8') 42 | data = data.split(/\r?\n/) 43 | data = data.filter(line => line.trim() !== '') 44 | return data 45 | } catch (err) { 46 | logger.error('[随机超能力]读取文件时发生错误:', err) 47 | } 48 | } 49 | 50 | 51 | // 读取超能力列表 52 | let cnl_list = await get_data(cnl_url) 53 | // 读取副作用列表 54 | let fzy_list = await get_data(fzy_url) 55 | // 当pro标识开启时并入pro列表 56 | if (ispro) { 57 | cnl_list.push(...await get_data(cnl_url_pro)) 58 | fzy_list.push(...await get_data(fzy_url_pro)) 59 | } 60 | // 当本地标识开启时并入本地列表 61 | if (islocal) { 62 | cnl_list.push(...await read_File(cnl_file)) 63 | fzy_list.push(...await read_File(fzy_file)) 64 | } 65 | 66 | logger.info("随机超能力初始化完毕") 67 | 68 | export class nb extends plugin { 69 | constructor() { 70 | super({ 71 | name: '随机超能力', 72 | dsc: '获取一个超能力及对应的副作用', 73 | event: 'message', 74 | priority: 5000, 75 | rule: [ 76 | { 77 | reg: "^#?超能力$", 78 | fnc: 'nb' 79 | } 80 | ] 81 | }) 82 | } 83 | 84 | async nb(e) { 85 | // 随机选择一行 86 | const cnl = cnl_list[Math.floor(Math.random() * cnl_list.length)] 87 | const fzy = fzy_list[Math.floor(Math.random() * fzy_list.length)] 88 | e.reply(`你的超能力是:\n${cnl}\n但是:\n${fzy}`, true) 89 | return true 90 | } 91 | } -------------------------------------------------------------------------------- /快举报.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | // 灵感来源为简幻欢群机器人,但代码均为原创 4 | 5 | // 由于新版快举报实现逻辑过于复杂,并入群管插件中 6 | // 单文件版本已停止更新,请安装新版本使用 https://github.com/xiaotian2333/xiaotian-qunguan 7 | 8 | const QQ = '1719549416' // 举报信息发到这个QQ号,填你自己的 9 | 10 | // 用户举报信息存放 11 | let Select_list = {} // 举报类型 12 | let violator_list = {} // 被举报者 13 | // 配置举报类型 14 | const ly = { 15 | 1: '发布色情/违法信息', 16 | 2: '存在诈骗骗钱行为', 17 | 3: '提问群公告已存在问题', 18 | 4: '水军/营销广告', 19 | 5: '网络暴力/侵权', 20 | 6: '无底线追星', 21 | 7: '以上内容均不贴切' 22 | } 23 | 24 | export class report extends plugin { 25 | constructor() { 26 | super({ 27 | name: '快举报', 28 | event: 'message', 29 | rule: [ 30 | { 31 | reg: "^#?快举报$", 32 | fnc: 'trigger', 33 | //permission: 'master' 34 | } 35 | ] 36 | }) 37 | } 38 | 39 | // 用户触发快举报 40 | async trigger(e) { 41 | let msg = '===快举报===\n' 42 | for (let tmp of Object.keys(ly)) { 43 | msg = `${msg}[${tmp}] ${ly[tmp]}\n` 44 | } 45 | msg = `${msg}------------------\n*发送对应序号来选择` 46 | e.reply(msg, true) 47 | 48 | this.setContext('Select') // 监听用户信息,触发选择理由流程 49 | return true //返回这个可能会存在bug,但是先留着 50 | } 51 | 52 | // 用户选择举报类型 53 | async Select(e) { 54 | e = this.e 55 | 56 | let num = parseInt(e.msg, 10) 57 | if (isNaN(num) || num > 8 || num < 0) { 58 | e.reply('请正确输入举报理由编号', true) 59 | } 60 | 61 | // 用户退出快举报 62 | if (e.msg == '0') { 63 | this.finish('Select') // 停止选择理由流程监听 64 | e.reply('快举报已退出', true) 65 | return true 66 | } 67 | 68 | // 记录用户的举报原因 69 | Select_list[e.user_id] = e.msg 70 | 71 | this.finish('Select') // 停止选择理由流程监听 72 | e.reply('请发送被举报人QQ,直接发送QQ号,可发送多个但请用空格断开', true) 73 | this.setContext('violator') // 开始举报者监听 74 | return true 75 | } 76 | 77 | // 用户发送被举报者QQ号 78 | async violator(e) { 79 | e = this.e 80 | 81 | violator_list[e.user_id] = e.msg // 记录被举报者 82 | this.finish('violator') // 停止举报者监听 83 | e.reply('请发送相应的聊天记录截图', true) 84 | this.setContext('Evidence') // 开始证据收集监听 85 | return true 86 | } 87 | 88 | // 用户发送被举报者违规行为 89 | async Evidence(e) { 90 | e = this.e 91 | 92 | this.finish('Evidence') // 停止证据收集监听 93 | 94 | e.reply('举报成功,请等待管理组审核', true) 95 | 96 | // 私聊发送举报信息 97 | Bot.pickFriend(QQ).sendMsg([ 98 | '===快举报信息===\n', 99 | `来源群号:${e.group_id}\n`, 100 | `举报理由:${ly[Select_list[e.user_id]]}\n`, 101 | `举报人:${e.user_id}\n`, 102 | `被举报者:${violator_list[e.user_id]}\n`, 103 | `聊天记录:\n`, 104 | segment.image(e.img[0]) 105 | ]) 106 | 107 | return true 108 | } 109 | } -------------------------------------------------------------------------------- /取群员列表.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | 5 | import fs from "fs" 6 | import path from "path" 7 | const plugin_name = '取群员列表' 8 | 9 | export class getlist extends plugin { 10 | constructor() { 11 | super({ 12 | name: "取群员列表", 13 | dsc: "一个插件示例", 14 | event: "message", 15 | priority: 5000, 16 | rule: [{ 17 | reg: /^#?(取|导出)群(员|友)列表(\d{5,12})?$/, 18 | fnc: "getlist" 19 | }] 20 | }) 21 | } 22 | 23 | async getlist(e) { 24 | // 检查消息内容是否包含5到12位的数字 25 | const match = e.msg.match(/^#?(取|导出)群(员|友)列表(\d{5,12})?$/) 26 | let group_id 27 | 28 | // 检查是否匹配到群号 29 | if (match) { 30 | group_id = Number(match[1]) 31 | } else { 32 | group_id = Number(e.group_id) 33 | } 34 | 35 | logger.debug(`[${plugin_name}]开始获取群号${group_id}的成员列表`) 36 | 37 | // 尝试获取对应群的成员列表 38 | let userList = await Bot.gml.get(group_id) 39 | if (!userList) { 40 | logger.error(`[${plugin_name}]群号${group_id}不存在或无法获取成员列表`) 41 | e.reply(`群号${group_id}不存在或无法获取成员列表`) 42 | return true 43 | } 44 | // 将 Map 或对象转换为可迭代的数组格式 45 | userList = Array.from(userList.entries()) 46 | 47 | // 创建一个数组来缓存所有用户信息 48 | const userInfoList = [] 49 | 50 | // 处理数据并输出到日志 51 | userList.forEach(([userId, userInfo]) => { 52 | // 获取card,如果不存在则使用nickname 53 | let displayName = userInfo?.card || userInfo?.nickname || '未知用户' 54 | 55 | // 清理不可见字符,保留可读文本(字母、数字、标点、符号、空格等) 56 | displayName = displayName.replace(/[^\p{L}\p{N}\p{P}\p{S}\p{Z}]/gu, '').trim() 57 | 58 | // 如果清理后为空字符串,可以设置一个默认值 59 | if (!displayName) { 60 | displayName = '用户名为不可见字符,已过滤' 61 | } 62 | 63 | logger.debug(`${userId}---${displayName}`) 64 | 65 | // 将用户信息添加到缓存数组 66 | userInfoList.push(`${userId},${displayName}`) 67 | }) 68 | 69 | const dirPath = 'data/plugins/取群员列表' 70 | const filePath = path.join(dirPath, `${group_id}_userlist.csv`) 71 | 72 | try { 73 | // 确保目录存在 74 | if (!fs.existsSync(dirPath)) { 75 | fs.mkdirSync(dirPath, { recursive: true }) 76 | } 77 | 78 | // 写入数据(覆盖模式) 79 | fs.writeFileSync(filePath, userInfoList.join('\n') + '\n') 80 | } catch (err) { 81 | logger.error(`[${plugin_name}]写入文件失败,报错信息:${err}`) 82 | return true 83 | } 84 | 85 | logger.info(`[${plugin_name}]已将${group_id}群成员列表保存为数据文件\n位于${filePath}`) 86 | e.reply(`任务处理完成,已保存为数据文件\n位于${filePath}`) 87 | return true 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /mc服务器状态.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | import plugin from '../../lib/plugins/plugin.js' 5 | 6 | /**默认服务器,没有发送域名/ip时使用 */ 7 | const Default_server = 'mc.xt-url.com' 8 | /**使用默认查询时的提示 */ 9 | const Default_Tips = '没有发送服务器信息,查询默认服务器' 10 | 11 | /**进行正则表达式匹配,过滤非域名或ip的触发,不懂别乱动 */ 12 | const Domain = /^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})(?::\d+)?$/ // 匹配域名,支持带端口号 13 | const ip = /\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b(?:\:\d{1,5})?/ //匹配ip,支持端口号 14 | 15 | export class example extends plugin { 16 | constructor() { 17 | super({ 18 | name: 'mc服务器状态', 19 | dsc: '通过api获取mc-java服务器的状态', 20 | event: 'message', 21 | priority: 5000, 22 | rule: [{ 23 | reg: '^#?(mc|MC|Mc|我的世界|java|jv|minecraft|Minecraft)(服务器|版)?(状态情况|状态|情况)', 24 | fnc: 'java' 25 | }] 26 | }) 27 | } 28 | 29 | async java(e) { 30 | // 简化消息变量,同时方便调用 31 | let msg = e.msg 32 | // 删除不需要的部分 33 | msg = msg.replace(/#?(mc|MC|Mc|我的世界|java|jv|minecraft|Minecraft)(服务器|版)?(状态情况|状态|情况)/g, '') 34 | // 没有发送服务器信息,使用默认参数 35 | if (msg == '') { 36 | e.reply(Default_Tips) 37 | msg = Default_server 38 | } 39 | 40 | // 使用test方法检查字符串是否符合正则表达式 41 | if (Domain.test(msg) || ip.test(msg)) { 42 | fetch(`https://api.mcstatus.io/v2/status/java/${msg}`) 43 | .then(response => { 44 | if (!response.ok) { 45 | logger.erro('网络请求失败') 46 | e.reply('网络请求失败') 47 | } 48 | return response.json() 49 | }) 50 | .then(data => { 51 | // 开始解析服务器数据 52 | let msglist = `服务器地址:${msg}\n` 53 | // 判断在线状态 54 | if (data.online) { 55 | msglist += '服务器状态:在线🟢\n' 56 | } else { 57 | e.reply(`服务器地址:${msg}\n服务器状态:离线🔴`) 58 | return true 59 | } 60 | 61 | // 正版验证状态 62 | if (data.eula_blocked) { 63 | msglist += `正版验证:开启\n` 64 | } else if (!data.eula_blocked) { 65 | msglist += `正版验证:关闭\n` 66 | } 67 | else { 68 | msglist += `正版验证:无法判断,请查看日志输出\n` 69 | logger.error(`正版验证值无法判断,接口返回:${data.eula_blocked}`) 70 | } 71 | 72 | msglist += `版本:${data.version.name_clean}\n` 73 | msglist += `介绍:\n${data.motd.clean.replace(' ', '')}\n` 74 | msglist += `在线玩家数:${data.players.max}/${data.players.online}` 75 | 76 | // 服务器图片 77 | const regex = /^data:image\/png;base64,/ 78 | if (regex.test(data.icon)) { 79 | const img = data.icon.replace("data:image/png;base64,", "base64://") 80 | e.reply([segment.image(img), msglist]) 81 | } else if (data.icon === null) { 82 | e.reply(['[该服务器没有设置LOGO]\n', msglist]) 83 | } else { 84 | e.reply(['[该服务器的LOGO无法识别]\n', msglist]) 85 | } 86 | 87 | return true 88 | }) 89 | .catch(error => { 90 | //输出错误提示 91 | e.reply('发生错误,请查看控制台日志') 92 | logger.error('获取错误:', error) 93 | return false 94 | }) 95 | } else { 96 | e.reply('请输入正确的域名或IP,支持带有端口号') 97 | return false 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /点赞续火.js: -------------------------------------------------------------------------------- 1 | import schedule from 'node-schedule' 2 | 3 | /** 自动点赞续火列表 4 | * @push 是否开启点赞消息推送 5 | * @hitokoto 是否开启推送一言 6 | */ 7 | const thumbsUpMelist = { 8 | /** 作者 */ 9 | 1719549416: { 10 | push: false, 11 | hitokoto: true 12 | }, 13 | /** 作者的机器人 */ 14 | 2859278670: { 15 | push: false, 16 | hitokoto: false 17 | } 18 | } 19 | /** 点赞次数,非会员10次,会员20次 */ 20 | const thumbsUpMe_sum = 10 21 | 22 | /** 点赞消息推送文本 */ 23 | const thumbsUpMe_msg = '派蒙给你点赞啦,记得给我回赞哦' 24 | 25 | /** 一言接口,请使用纯文本的接口 */ 26 | const hitokoto_api = 'https://v1.hitokoto.cn/?encode=text&charset=utf-8&c=d&c=i&c=h&c=e' 27 | 28 | /** 一言默认文案,网络请求失败时发送这个 */ 29 | const hitokoto_Default_text = '种自己的花,爱自己的宇宙🌍' 30 | 31 | /** 冷却相关配置 */ 32 | const cd = 2 // 一天只能触发1次 33 | const cd_tips = "点过就别继续发了,还搁这讨赞呢?你是乞丐吗?😏" // 冷却提示 34 | let user_cd = {} // 初始化冷却数据 35 | 36 | 37 | async function getHitokoto() { 38 | try { 39 | let res = await fetch(hitokoto_api) 40 | return res.text() 41 | } catch (e) { 42 | logger.warn(`[点赞续火][续火] 接口请求失败,使用默认文案。报错详情:${e}`) 43 | return hitokoto_Default_text 44 | } 45 | } 46 | 47 | 48 | /** 被消息触发 */ 49 | export class dzxh extends plugin { 50 | constructor() { 51 | super({ 52 | name: "点赞续火", 53 | dsc: "给群友点赞及续火", 54 | event: "message", 55 | priority: 5000, 56 | rule: [ 57 | { 58 | reg: "^#*赞我$", 59 | fnc: "thumbsUpMe", 60 | }, 61 | { 62 | reg: "^#*(续火|一言|壹言)$", 63 | fnc: "hitokoto", 64 | } 65 | ], 66 | }) 67 | } 68 | 69 | /** 赞我 */ 70 | async thumbsUpMe(e) { 71 | // 字段不存在则默认0,存在则保留原值 72 | user_cd[e.user_id] = user_cd[e.user_id] ?? 0 73 | 74 | // 到达边界提示 75 | if (user_cd[e.user_id] == cd) { 76 | e.reply(cd_tips) 77 | user_cd[e.user_id] += 1 78 | return true 79 | } 80 | 81 | // 已越界,不再回复消息 82 | if (user_cd[e.user_id] > cd) { 83 | user_cd[e.user_id] += 1 84 | return true 85 | } 86 | 87 | // 加入冷却 88 | user_cd[e.user_id] += 1 89 | 90 | Bot.pickFriend(this.e.user_id).thumbUp(thumbsUpMe_sum) 91 | this.e.reply(thumbsUpMe_msg) 92 | return true 93 | } 94 | /** 续火 */ 95 | async hitokoto(e) { 96 | let msg = await getHitokoto() 97 | e.reply(msg) 98 | return true 99 | } 100 | } 101 | 102 | /** 休眠函数 103 | * @time 毫秒 104 | */ 105 | function sleep(time) { 106 | return new Promise((resolve) => setTimeout(resolve, time)); 107 | } 108 | 109 | /** 主动触发-点赞 110 | * 点赞开始时间 111 | * cron表达式定义推送时间 (秒 分 时 日 月 星期) 112 | * 可使用此网站辅助生成:https://www.matools.com/cron/ 113 | * 注意,每天都需要触发,因此日及以上选通配符或不指定 114 | * 只选小时就可以了 115 | */ 116 | schedule.scheduleJob('30 5 12 * * *', async () => { 117 | //schedule.scheduleJob('1 * * * * *', async () => { 118 | for (let qq of Object.keys(thumbsUpMelist)) { 119 | Bot.pickFriend(qq).thumbUp(thumbsUpMe_sum) 120 | logger.mark(`[点赞续火][自动点赞] 已给QQ${qq}点赞${thumbsUpMe_sum}次`) 121 | if (thumbsUpMelist[qq].push) { 122 | Bot.pickFriend(qq).sendMsg(thumbsUpMe_msg) 123 | } 124 | await sleep(10000) // 等10秒在下一个 125 | } 126 | }) 127 | 128 | // 主动触发-续火 129 | schedule.scheduleJob('30 15 12 * * *', async () => { 130 | //schedule.scheduleJob('1 * * * * *', async () => { 131 | logger.mark(`[点赞续火][自动续火] 触发一言定时`) 132 | let msg = await getHitokoto() 133 | 134 | for (let qq of Object.keys(thumbsUpMelist)) { 135 | if (thumbsUpMelist[qq].hitokoto) { 136 | Bot.pickFriend(qq).sendMsg(msg) 137 | } 138 | await sleep(2000) // 等2秒在下一个 139 | } 140 | }) 141 | 142 | // 每日重置使用限制 143 | schedule.scheduleJob('0 0 0 * * *', async () => { 144 | user_cd = {} 145 | logger.mark(`[点赞续火][赞我] 冷却已重置`) 146 | }); -------------------------------------------------------------------------------- /抽头衔.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 作者:xiaotian2333 3 | * 开源地址:https://github.com/xiaotian2333/yunzai-plugins-Single-file 4 | */ 5 | 6 | import schedule from 'node-schedule'; 7 | 8 | /** 冷却相关配置 */ 9 | const cd = 3 // 一天只能触发1次 10 | const cd_tips = "今天的次数就这么多,明天继续吧" // 冷却提示 11 | let user_cd = {} // 初始化冷却数据 12 | 13 | /** 反悔相关配置 */ 14 | const regret = true // 是否允许反悔 15 | let regret_data = {} // 初始化反悔数据 16 | 17 | 18 | /** 19 | * 将“一行一段话”的字符串转换为数组(支持过滤空行) 20 | * @param {string} str - 输入的字符串(一行一段话) 21 | * @returns {string[]} 转换后的数组 22 | */ 23 | function strToLineList(str) { 24 | // 分割换行符:兼容 \n(Linux/macOS)和 \r\n(Windows) 25 | const lines = str.split(/\r?\n/); 26 | // 过滤空行,去除纯空格/制表符的行 27 | return lines.filter(line => line.trim() !== ''); 28 | } 29 | 30 | /**统一网络请求函数 31 | * @param {string} url 要请求的url 32 | * @returns {string[]} 数组 33 | */ 34 | async function get_data(url) { 35 | let result = await fetch(url, { 36 | headers: { 37 | 'User-Agent': 'Random-Title (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 38 | }, 39 | }) 40 | result = await result.text() 41 | return strToLineList(result) 42 | } 43 | 44 | let 名字 = await get_data("https://oss.xt-url.com/%E7%BD%91%E6%98%93%E9%9A%8F%E6%9C%BA%E5%90%8D/%E5%90%8D%E5%AD%97.txt") 45 | let 形容词 = await get_data("https://oss.xt-url.com/%E7%BD%91%E6%98%93%E9%9A%8F%E6%9C%BA%E5%90%8D/%E5%BD%A2%E5%AE%B9%E8%AF%8D.txt") 46 | 47 | export class example extends plugin { 48 | constructor() { 49 | super({ 50 | name: "抽头衔", 51 | event: "message.group", 52 | priority: 2000, 53 | rule: [ 54 | { 55 | reg: /^#?抽头衔$/, 56 | fnc: 'ctx' 57 | }, 58 | { 59 | reg: /^#?反悔$/, 60 | fnc: 'fh' 61 | } 62 | ] 63 | }) 64 | } 65 | 66 | async ctx(e) { 67 | // 检查机器人是否是群主 68 | if (!e.group.is_owner) return e.reply("我又不是群主,做不到啊") 69 | 70 | // 检查每日使用次数 71 | // 字段不存在则默认0,存在则保留原值 72 | user_cd[e.user_id] = user_cd[e.user_id] ?? 0 73 | 74 | // 到达边界提示 75 | if (user_cd[e.user_id] == cd) { 76 | e.reply(cd_tips) 77 | user_cd[e.user_id] += 1 78 | return true 79 | } 80 | 81 | // 已越界,不再回复消息 82 | if (user_cd[e.user_id] > cd) { 83 | user_cd[e.user_id] += 1 84 | return true 85 | } 86 | 87 | // 加入冷却 88 | user_cd[e.user_id] += 1 89 | 90 | const txt = `${形容词[Math.floor(Math.random() * 形容词.length)]}${名字[Math.floor(Math.random() * 名字.length)]}` 91 | e.group.setTitle(e.user_id, txt) 92 | e.reply(`你的新头衔是${txt}`) 93 | regret_data[e.user_id] = e.sender.title 94 | return true 95 | } 96 | 97 | async fh(e) { 98 | // 没有开启反悔 99 | if (!regret) { 100 | e.reply("反悔无效,受着") 101 | return false 102 | } 103 | // 没有历史数据 104 | if (!regret_data[e.user_id]) { 105 | e.reply("忘了你上一个是啥了,现在这个先用着吧") 106 | return false 107 | } 108 | // 历史与当前一致 109 | if (regret_data[e.user_id] === e.sender.title) { 110 | e.reply("你又没抽新的,反悔个什么玩意?") 111 | return false 112 | } 113 | // 反悔流程 114 | e.group.setTitle(e.user_id, regret_data[e.user_id]) 115 | e.reply("哼哼,给你新的你也不敢用啊") 116 | return true 117 | } 118 | } 119 | 120 | // 每日重置使用限制 121 | schedule.scheduleJob('0 0 0 * * *', async () => { 122 | user_cd = {} 123 | logger.mark(`[抽头衔] 冷却已重置`) 124 | }); -------------------------------------------------------------------------------- /智谱绘图.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 作者:xiaotian2333 3 | * 开源地址:https://github.com/xiaotian2333/yunzai-plugins-Single-file 4 | */ 5 | 6 | import fetch from "node-fetch" 7 | 8 | // 智谱API Key,需要自行申请(需实名) 9 | // 申请链接:https://www.bigmodel.cn/invite?icode=iGW2wQ0KiXGc0PVU%2BeTSFEjPr3uHog9F4g5tjuOUqno%3D 10 | // 绘图必须填写key,否则无法使用 11 | const Authorization = "" //智谱API Key 12 | const url = "https://open.bigmodel.cn/api/paas/v4/images/generations" //智谱API接口,不要修改 13 | const model = "CogView-4" //模型名称 14 | //图像分辨率 15 | const size = [ 16 | "1024x1024", 17 | "768x1344", 18 | "864x1152", 19 | "1344x768", 20 | "1152x864", 21 | "1440x720", 22 | "720x1440" 23 | ] 24 | // 过滤词列表 25 | const list = [ 26 | '过滤词列表-156411gfchc', 27 | '模糊匹配-15615156htdy1', 28 | ] 29 | 30 | 31 | export class bigmodel extends plugin { 32 | constructor() { 33 | super({ 34 | name: '智谱绘图', 35 | event: 'message', 36 | priority: 2000, 37 | rule: [ 38 | { 39 | reg: '^#?绘(个)?图(.*)', 40 | fnc: 'chat' 41 | } 42 | ] 43 | }) 44 | } 45 | 46 | async chat(e) { 47 | //if (!e.isMaster) { return false } // 只允许主人使用 48 | 49 | if (!Authorization) { 50 | e.reply("智谱API Key未设置,请参照注释设置后重试") 51 | return true 52 | } 53 | 54 | // 删除不需要的部分 55 | let msg = e.msg 56 | msg = msg.replace(' ', ''); 57 | 58 | // 输入过滤 59 | if (list.some(item => msg.includes(item))) { 60 | // 检测到需要过滤的词后的处理逻辑,默认不理人 61 | logger.info(`[智谱绘图]检测到敏感词,已过滤`) 62 | e.reply("输入包含敏感词,已拦截") 63 | return true 64 | } 65 | 66 | // 如果 msg 为空,则返回 67 | if (!msg) { 68 | e.reply('请输入绘图提示词') 69 | return false 70 | } 71 | // 消息长度限制 72 | if (msg.length > 1000) { 73 | e.reply('输入文本长度过长') 74 | return true 75 | } 76 | 77 | 78 | const data = { 79 | "model": `${model}`, 80 | "prompt": msg, 81 | "size": size[Math.floor(Math.random() * size.length)], 82 | "user_id": `${e.group_id}_${e.user_id}`, 83 | 84 | } 85 | 86 | let Reply = await fetch(url, { 87 | method: 'POST', 88 | headers: { 89 | "Content-Type": "application/json", 90 | "Authorization": Authorization, 91 | "User-Agent": "GLM (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)" 92 | }, 93 | body: JSON.stringify(data) 94 | }) 95 | Reply = await Reply.json() 96 | 97 | // 内容安全相关 98 | if (Reply.content_filter) { 99 | const role_list = { 100 | "user": "用户", 101 | "assistant": "模型", 102 | "history": "历史" 103 | } 104 | if (Reply.content_filter.level >= 2) { 105 | // 轻度敏感 106 | logger.mark("[智谱绘图]${role_list[Reply.content_filter.role]}内容包含轻微敏感信息,不直接发送图片") 107 | e.reply(`${role_list[Reply.content_filter.role]}内容包含轻微敏感信息,不直接发送图片\n${Reply.data[0].url}`) 108 | return true 109 | } 110 | // 高度敏感 111 | e.reply(`${role_list[Reply.content_filter.role]}内容包含高度敏感信息,拦截发送`) 112 | logger.warn(`[智谱绘图]${role_list[Reply.content_filter.role]}内容包含高度敏感信息,拦截发送:${Reply.data[0].url}`) 113 | return true 114 | } 115 | // 发送图片 116 | e.reply(segment.image(Reply.data[0].url)) 117 | return true 118 | } 119 | } -------------------------------------------------------------------------------- /icp查询.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'url'; 2 | import { encode } from 'punycode'; 3 | 4 | function extractMainDomain(input) { 5 | // 处理空输入 6 | if (!input || typeof input !== 'string') { 7 | return ''; 8 | } 9 | 10 | // 确保输入有协议,方便url模块解析 11 | let normalizedInput = input; 12 | if (!/^https?:\/\//i.test(normalizedInput)) { 13 | normalizedInput = 'http://' + normalizedInput; 14 | } 15 | 16 | // 使用Node.js内置url模块解析 17 | const parsedUrl = parse(normalizedInput); 18 | let domain = parsedUrl.hostname; 19 | 20 | // 如果解析失败,尝试直接处理输入 21 | if (!domain) { 22 | // 移除端口号、路径、查询参数和哈希 23 | const separatorIndex = input.search(/[/?#:]/); 24 | if (separatorIndex !== -1) { 25 | domain = input.substring(0, separatorIndex); 26 | } else { 27 | domain = input; 28 | } 29 | } 30 | 31 | // 处理主域名提取(考虑常见的多部分顶级域名) 32 | const multiPartTLDs = ['co.uk', 'com.cn', 'org.cn', 'net.cn', 'gov.cn', 'ac.cn', 'eu.org', 'com.hk', 'org.hk']; 33 | const parts = domain.split('.'); 34 | 35 | // 简单情况:本身就是主域名(如 mihoyo.com) 36 | if (parts.length <= 2) { 37 | return encodePunycode(domain); 38 | } 39 | 40 | // 检查是否包含多部分顶级域名 41 | for (const tld of multiPartTLDs) { 42 | const tldParts = tld.split('.'); 43 | const tldLength = tldParts.length; 44 | 45 | // 检查域名最后几个部分是否匹配多部分顶级域名 46 | if (parts.length >= tldLength + 1) { 47 | const domainTld = parts.slice(-tldLength).join('.'); 48 | if (domainTld === tld) { 49 | const mainDomain = parts.slice(-tldLength - 1).join('.'); 50 | return encodePunycode(mainDomain); 51 | } 52 | } 53 | } 54 | 55 | // 普通情况:取最后两个部分作为主域名 56 | const mainDomain = parts.slice(-2).join('.'); 57 | return encodePunycode(mainDomain); 58 | } 59 | 60 | // 将包含非ASCII字符的域名转换为Punycode编码(xn--格式) 61 | function encodePunycode(domain) { 62 | // 检查是否包含非ASCII字符 63 | if (/[^\x00-\x7F]/.test(domain)) { 64 | try { 65 | // 分割域名部分进行编码 66 | return domain.split('.') 67 | .map(part => { 68 | // 只对包含非ASCII的部分进行编码 69 | if (/[^\x00-\x7F]/.test(part)) { 70 | return 'xn--' + encode(part); 71 | } 72 | return part; 73 | }) 74 | .join('.'); 75 | } catch (e) { 76 | console.error('Punycode编码失败:', e); 77 | return domain; 78 | } 79 | } 80 | return domain; 81 | } 82 | 83 | export class icp extends plugin { 84 | constructor() { 85 | super({ 86 | name: 'magnet', 87 | dsc: 'ICP查询', 88 | event: 'message', 89 | priority: 5000, 90 | rule: [ 91 | { 92 | reg: /^#?(ICP|icp|备案)?(强制)?查询/, 93 | fnc: 'icp' 94 | } 95 | ] 96 | }) 97 | } 98 | 99 | async icp(e) { 100 | let domain = e.msg 101 | domain = await domain.replace(/^#?(ICP|icp|备案)?(强制)?查询/, '') 102 | // 常规查询 103 | if (e.msg.includes('强制')) { 104 | logger.debug(`域名[${domain}]触发强制查询`) 105 | } else { 106 | domain = extractMainDomain(domain) 107 | } 108 | 109 | const url = `https://icp.2x.nz/?domain=${domain}` // 感谢二叉树树提供的接口 110 | let res = await fetch(url, { 111 | method: 'GET', 112 | headers: { 113 | 'User-Agent': 'icp-query (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 114 | } 115 | }) 116 | if (res.status !== 200) { 117 | logger.error(`域名[${domain}]查询失败`) 118 | e.reply('查询失败,请稍后再试', true) 119 | return false 120 | } 121 | res = await res.json() 122 | const icp = res.params.list[0] 123 | // 为空则没有备案信息 124 | if (!icp) { 125 | e.reply('该域名未备案', true) 126 | return true 127 | } 128 | // 构建消息 129 | let msg = [ 130 | `域名:${domain}`, 131 | `${icp.natureName}备案:${icp.unitName}`, 132 | `备案号:${icp.serviceLicence}`, 133 | ] 134 | msg = msg.join("\n") 135 | e.reply(msg, true) 136 | return true 137 | } 138 | } -------------------------------------------------------------------------------- /Archiving/签名状态检测.js: -------------------------------------------------------------------------------- 1 | import plugin from '../../lib/plugins/plugin.js' 2 | import fs from 'node:fs' 3 | export class qsign extends plugin { 4 | constructor() { 5 | super({ 6 | name: '签名状态检测', 7 | dsc: '签名状态检测', 8 | event: 'message', 9 | priority: 9, 10 | rule: [{ 11 | reg: `^#?(签名|qsign|sign)状态$`, 12 | fnc: `qsign` 13 | }] 14 | }) 15 | } 16 | 17 | async qsign(e) { 18 | await e.reply('正在检测各签名服务可用性...', true) // 开始检测提示 19 | /** 定义一个列表,存储每个链接的名字及提供者 20 | * 链接必须带上正确的 key 21 | * @name 签名的显示名字 22 | * @provider 签名的提供者 23 | * @qq 可选项,部分节点是白名单的需要填写白名单里的qq才可以检测 24 | */ 25 | let publicUrls = { 26 | 'https://qsign.trpgbot.com?key=null': { 27 | name: 'Cloudflare - High Capacity - M1', 28 | provider: '然' 29 | }, 30 | 'https://zyr15r-astralqsign.hf.space?key=null': { 31 | name: 'HuggingFace - High Capacity - M2', 32 | provider: '然' 33 | }, 34 | 'http://3.qsign.icu?key=XxxX': { 35 | name: 'Qsign-3', 36 | provider: 'hanxuan' 37 | }, 38 | 'http://4.qsign.icu?key=XxxX': { 39 | name: 'Qsign-4', 40 | provider: 'hanxuan' 41 | }, 42 | 'http://5.qsign.icu?key=XxxX': { 43 | name: 'Qsign-5', 44 | provider: 'hanxuan' 45 | }, 46 | 'https://t1.qsign.xt-url.com?key=xiaotian': { 47 | name: '小天t1节点反代', 48 | provider: 'xiaotian(崩了进群628306033反馈)' 49 | }, 50 | // 时雨签名不需要密钥,为了兼容代码加上key字段 51 | 'http://gz.console.microgg.cn:2536?key=null': { 52 | name: '时雨-1', 53 | provider: '时雨', 54 | qq: '123456' 55 | }, 56 | 'http://110.40.249.125:2536?key=null': { 57 | name: '时雨-2', 58 | provider: '时雨', 59 | qq: '123456' 60 | } 61 | } 62 | 63 | /** 合并转发消息列表 */ 64 | let msgList = [] 65 | // 第一条消息(置顶消息),如不需要可删除或注释掉 66 | msgList.push({ 67 | user_id: '系统消息', 68 | message: '状态说明\n正常✅:签名可以正常使用\n异常❗:签名无法正常使用' 69 | }) 70 | /** 兼容TRSS崽用的长消息文本 */ 71 | let mdmsg = '状态说明\n正常✅:签名可以正常使用\n异常❗:签名无法正常使用' 72 | 73 | // 不知道为啥直接用云崽的api会乱返回崽消息,只能自己实现了 74 | /** 云崽的类型 */ 75 | let YZname = 'trss-yunzai' 76 | try { 77 | YZname = await JSON.parse(fs.readFileSync('package.json', 'utf8')).name 78 | } 79 | catch(err) { 80 | logger.warn('[签名状态检测][qsign] 获取云崽版本失败,使用兼容模式发送') 81 | } 82 | 83 | // 遍历列表的键,即链接 84 | for (let publicUrl of Object.keys(publicUrls)) { 85 | /** 单个签名的消息体 */ 86 | let msgList_ = `名称:${publicUrls[publicUrl].name}\n提供者:${publicUrls[publicUrl].provider}\n` //创建基础提示 87 | // 开始获取qq版本 88 | try { 89 | let res = await fetch(publicUrl) // 获取返回的数据 90 | res = await res.json() 91 | if (res.code != 0) {msgList_ += `状态:异常❗:接口返回错误,code非0`} 92 | // 基础接口请求成功,进入二级请求 93 | else { 94 | // 开始处理链接 95 | let parts = publicUrl.split("?") 96 | // 处理白名单qq 97 | let qq = Bot.uin 98 | if (publicUrl.hasOwnProperty("qq")) { 99 | qq = publicUrls[publicUrl].qq 100 | } 101 | // 开始模拟icqq请求 102 | const startTime = new Date().getTime() // 延迟检测开始 103 | let sign = await fetch( 104 | `${parts[0]}/sign?${parts[1]}&uin=${qq}&qua=${res.data.protocol.qua}&cmd=sign&seq=1848698645&buffer=0C099F0C099F0C099F&guid=123456&android_id=2854196310`,{ 105 | headers: { 106 | 'User-Agent': 'icqq@0.6.10 (Released on 2024/2/3)' 107 | } 108 | } 109 | ) 110 | const elapsedTime = new Date().getTime() - startTime // 延迟检测结束,计算时间差 111 | sign = await sign.json() 112 | if (sign.code == 0 && sign.msg == "success") {msgList_ += `版本:${res.data.protocol.version}\n状态:正常✅\n延迟:${elapsedTime}ms`} 113 | else {msgList_ += `状态:异常❗:签名失败`} 114 | } 115 | } 116 | 117 | // 基础请求未成功 118 | catch(err) { 119 | if (err instanceof SyntaxError) { 120 | msgList_ += `状态:异常❗:非QSign接口返回的文本` 121 | } else if(err.message = 'fetch failed') { 122 | msgList_ += '状态:异常❗:请求超时' 123 | } else { 124 | msgList_ += `状态:异常❗:接口请求错误\n${err.message}` 125 | } 126 | } 127 | // 提交信息到合并信息列表 128 | // 获取云崽的分支版本 129 | if (YZname == 'miao-yunzai') { 130 | msgList.push({ 131 | user_id: Bot.uin, 132 | message: msgList_ 133 | }) 134 | } else { 135 | // 非喵崽用长文本消息 136 | mdmsg += '\n---\n'+msgList_ 137 | } 138 | } 139 | 140 | // 处理完毕,发送消息 141 | if (YZname == 'miao-yunzai') { 142 | // 喵崽发送合并转发消息 143 | e.reply(await Bot[Bot.uin].pickUser(e.self_id).makeForwardMsg(msgList)) 144 | } else { 145 | // 非喵崽发送长消息文本 146 | e.reply(mdmsg) 147 | } 148 | return true 149 | } 150 | } -------------------------------------------------------------------------------- /B站卡片转链接.js: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | 3 | async function getRedirectUrl(originalUrl) { 4 | try { 5 | // 发送请求但不自动跟随重定向 6 | const response = await fetch(originalUrl, { 7 | redirect: 'manual' // 关键:不自动跟随重定向 8 | }) 9 | 10 | // 检查是否发生了重定向(状态码3xx) 11 | if (response.status >= 300 && response.status < 400) { 12 | let redirectUrl = response.headers.get('location') 13 | 14 | // 如果重定向地址是相对路径,需要拼接成完整URL 15 | redirectUrl = new URL(redirectUrl, originalUrl).href 16 | // 去除?后的参数 17 | return redirectUrl.split('?')[0] 18 | } else { 19 | return false 20 | } 21 | } catch (error) { 22 | logger.error('[B站卡片转链接]获取重定向链接时出错:', error.message) 23 | return false 24 | } 25 | } 26 | 27 | 28 | export class bili extends plugin { 29 | constructor() { 30 | super({ 31 | name: "B站卡片转链接", 32 | event: "message", 33 | priority: 5000, 34 | rule: [ 35 | { 36 | reg: /view_8C8E89B49BE609866298ADDFF2DBABA4/, 37 | fnc: "video" 38 | }, 39 | { 40 | reg: /b23.tv/, 41 | fnc: "live" 42 | }, 43 | { 44 | reg: /^(https:\/\/)?b23.tv\/(.*)/, 45 | fnc: "conversion" 46 | }, 47 | { 48 | reg: /^(https:\/\/)?(live|www|m).bilibili.com\/(.*)/, 49 | fnc: "simplify" 50 | } 51 | ] 52 | }) 53 | } 54 | 55 | async video(e) { 56 | // 不是json格式,中断 57 | if (e.message[0].type != 'json') { return false } 58 | 59 | const data = JSON.parse(e.message[0].data) 60 | 61 | // 不是b站视频分享卡片,中断 62 | if (data?.view != 'view_8C8E89B49BE609866298ADDFF2DBABA4' && data.meta?.detail_1?.appid != '1109937557') { return false } 63 | 64 | // 处理图片链接 65 | let img_url = data.meta.detail_1.preview 66 | // 如无协议头,添加https 67 | if (!/^https?:\/\//i.test(img_url)) { 68 | img_url = `https://${img_url}` 69 | } 70 | 71 | // 处理视频链接 72 | let BV = await getRedirectUrl(data.meta.detail_1.qqdocurl.split('?')[0]) 73 | BV = await BV.split('https://www.bilibili.com/video/')[1] 74 | const BV_URL = `https://b23.tv/${BV}` 75 | 76 | // 制作数据 77 | const bilidata = { 78 | url: BV_URL || data.meta.detail_1.qqdocurl.split('?')[0], // 视频链接 79 | title: data.meta.detail_1.desc, // 视频标题 80 | img: img_url, // 带有信息的视频封面 81 | host: data.meta.detail_1?.host, // 原始分享者消息:uin,nick 82 | bv: BV, // 视频BV号 83 | } 84 | // 制作消息 85 | const msg = [ 86 | segment.at(e.user_id), 87 | `分享了一个B站链接\n`, 88 | `标题:${bilidata.title}\n`, 89 | `链接:${bilidata.url}\n`, 90 | segment.image(bilidata.img), 91 | `\n原始分享者:${bilidata?.host?.nick}(${bilidata?.host?.uin})`, 92 | ] 93 | e.reply(msg) 94 | e.recall() // 撤回卡片消息 95 | return true 96 | } 97 | 98 | async live(e) { 99 | // 不是json格式,中断 100 | if (e.message[0].type != 'json') { return false } 101 | 102 | const data = JSON.parse(e.message[0].data) 103 | 104 | // 不是b站直播分享卡片,中断 105 | if (data?.extra?.appid != '100951776' && data.meta?.news?.tag != '哔哩哔哩') { return false } 106 | 107 | // 取直播间纯净链接 108 | const live_url = await getRedirectUrl(data.meta.news.jumpUrl) 109 | 110 | // 取直播间图片 111 | let img_url = await fetch(live_url) 112 | img_url = await img_url.text() 113 | img_url = await img_url.match(/"cover":"(.*?)"/) 114 | // 还原链接中的转义符 115 | if (img_url && img_url[1]) { 116 | img_url = await img_url[1].replace(/\\u002F/g, '/'); // 将 \u002F 替换回 / 117 | } else { 118 | logger.warn('[B站卡片转链接]未能获取到直播间封面,将使用卡片中的图片代替') 119 | img_url = data.meta.news.preview 120 | } 121 | 122 | // 构建数据 123 | const bilidata = { 124 | url: live_url, 125 | title: data.meta.news.title, 126 | img: img_url, 127 | } 128 | 129 | const msg = [ 130 | segment.at(e.user_id), 131 | `分享了一个B站直播链接\n`, 132 | `标题:${bilidata.title}\n`, 133 | `链接:${bilidata.url}\n`, 134 | segment.image(bilidata.img), 135 | `\n原始分享者:${data?.extra?.uin || '获取失败'}` 136 | ] 137 | e.reply(msg) 138 | e.recall() 139 | return true 140 | } 141 | 142 | async conversion(e) { 143 | // 不是text格式,中断 144 | if (e.message[0].type != 'text') { return false } 145 | 146 | const msg = await e.msg.split("?")[0] 147 | try{ 148 | const airurl = await getRedirectUrl(msg) 149 | 150 | // 接口返回false、链接无效、会员购的情况,中断 151 | if (airurl == false || airurl == "https://b23.tv/" || airurl == "https://mall.bilibili.com/detail.html") { 152 | return false 153 | } 154 | 155 | e.reply(`还原后链接:${airurl}`, true) 156 | return true 157 | } 158 | catch(err) { 159 | logger.error('[B站卡片转链接]获取重定向链接时出错:', err.message) 160 | return false 161 | } 162 | } 163 | 164 | async simplify(e) { 165 | // 不是text格式,中断 166 | if (e.message[0].type != 'text') { return false } 167 | 168 | const msg = await e.msg.split("?")[0] 169 | 170 | // 跟原链接一致,中断 171 | if (msg == e.msg) {return false} 172 | 173 | e.reply(`提纯后链接:${msg}`, true) 174 | return true 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /快讯.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 作者:xiaotian2333 3 | * 开源地址:https://github.com/xiaotian2333/yunzai-plugins-Single-file 4 | */ 5 | 6 | 7 | // 自动触发相关 8 | /** 自动发送的群/人列表 9 | * @param type (group,private)群还是人 10 | */ 11 | const postlist = { 12 | /** 人示例 */ 13 | 1719549416: { 14 | type: "private" 15 | }, 16 | /** 群示例 */ 17 | 628306033: { 18 | type: "group" 19 | } 20 | } 21 | /** 自动触发的时间 22 | * cron表达式定义推送时间 (秒 分 时 日 月 星期) 23 | * 可使用此网站辅助生成:https://www.matools.com/cron/ 24 | * 注意,每天都需要触发,因此日及以上选通配符或不指定 25 | * 只选小时就可以了 26 | */ 27 | const auto_cron = "30 8 8 * * *" 28 | //const auto_cron = "1 * * * * *" // 每分钟触发一次,测试用 29 | 30 | 31 | import fetch from 'node-fetch' 32 | import schedule from 'node-schedule' 33 | 34 | 35 | 36 | /** 统一主动信息发送 37 | * 成功true,失败false 38 | * @param msg 要发送的信息 39 | * @param source (group,private) 发到什么渠道 40 | * @param channel_id 渠道的标识ID 41 | */ 42 | function post_msg(msg, source, channel_id) { 43 | if (source == "group") { 44 | // 群 45 | Bot.pickGroup(channel_id).sendMsg(msg) 46 | return true 47 | } else if (source == "private") { 48 | // 私聊 49 | Bot.pickUser(channel_id).sendMsg(msg) 50 | return true 51 | } else { 52 | logger.error(`[快讯][消息发送] 没有匹配的发送渠道,关键信息:source=${source},channel_id=${channel_id},msg=${msg}`) 53 | return false 54 | } 55 | } 56 | 57 | /** 休眠函数 58 | * @time 毫秒 59 | */ 60 | function sleep(time) { 61 | return new Promise((resolve) => setTimeout(resolve, time)) 62 | } 63 | 64 | /** 获取资源列表 */ 65 | async function get_data() { 66 | // 拼接请求地址 67 | const url = `https://bc.weixin.qq.com/mp/recommendtag?f=json&action=show_news_feed&msg_type=1&tag_type=24&tag=快讯&hotnewsfgeed=1&sn=xiaotian2333` 68 | 69 | // 发起请求 70 | let result = await fetch(url, { 71 | headers: { 72 | 'User-Agent': 'News-flash (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 73 | } 74 | }) 75 | result = await result.json() 76 | // 提取新闻部分并返回 77 | return result.mp_msgs 78 | } 79 | 80 | /** 时间转换 */ 81 | function formatTimeAgo(send_time) { 82 | const currentTime = Math.floor(Date.now() / 1000) // 当前时间的秒级时间戳,Date.now()返回的是毫秒,除以1000转换为秒 83 | const diffSeconds = currentTime - send_time 84 | 85 | const minutes = Math.floor(diffSeconds / 60) 86 | const hours = Math.floor(minutes / 60) 87 | 88 | if (hours >= 1) { 89 | return `${hours}小时前` 90 | } else if (minutes >= 1) { 91 | return `${minutes}分钟前` 92 | } else { 93 | return '刚刚' 94 | } 95 | } 96 | 97 | /** 去除追踪参数 */ 98 | function removeParamsFromUrl(url) { 99 | const paramsToRemove = ['listen_content_id_', 'exptype', 'subscene', 'scene', 'chksm'] 100 | const urlObject = new URL(url) 101 | const searchParams = new URLSearchParams(urlObject.search) 102 | 103 | paramsToRemove.forEach(param => { 104 | searchParams.delete(param) 105 | }) 106 | 107 | urlObject.search = searchParams.toString() 108 | return urlObject.href 109 | } 110 | 111 | /** 主函数 */ 112 | async function mian(uin) { 113 | // 请求快讯列表 114 | const news_list = await get_data() 115 | /** 合并消息列表 */ 116 | let msgList = [] 117 | // 这是在合并消息前面的提示 118 | /** 119 | msgList.push({ 120 | user_id: uin, 121 | nickname: '实时快讯', 122 | message: `本实时快讯来源微信` 123 | }) 124 | */ 125 | // 制作合并消息 126 | for (const news of news_list) { 127 | //logger.info(news) // 输出每个新闻的详细 128 | let msg = [ 129 | `${news.title}\n\n`, 130 | `${news.digest}\n\n`, 131 | //await segment.image(news?.cover_url), 132 | `来源:${news.biz_info.name}\n`, 133 | `发布时间:${formatTimeAgo(news.send_time)}\n`, 134 | `阅读原文:${removeParamsFromUrl(news.jump_url)}` 135 | ] 136 | logger.debug(news.title, news?.cover_url) 137 | msgList.push({ 138 | user_id: uin, 139 | nickname: '实时快讯', 140 | message: msg 141 | }) 142 | //await sleep(5000) // 服务端有限速,等5秒在下一个 143 | 144 | } 145 | return msgList 146 | } 147 | 148 | 149 | export class example extends plugin { 150 | constructor() { 151 | super({ 152 | name: '快讯', 153 | dsc: '来源微信订阅号-快讯', 154 | event: 'message', 155 | priority: 5000, 156 | rule: [{ 157 | reg: '^#?快讯', 158 | fnc: 'flash', 159 | //permission: 'master', // 仅限主人可触发 160 | }] 161 | }) 162 | } 163 | 164 | async flash(e) { 165 | //e.reply('正在获取快讯,请稍后', true, 2) 166 | let channel_id = "undefined" 167 | if (e.message_type == "group") { 168 | // 群 169 | channel_id = e.group_id 170 | } else if (e.message_type == "private") { 171 | // 私聊 172 | channel_id = e.from_id 173 | } 174 | await e.reply(await Bot.makeForwardMsg(await mian(e.user_id))) 175 | return true 176 | } 177 | } 178 | 179 | 180 | /** 主动触发-发到指定群 */ 181 | schedule.scheduleJob(auto_cron, async () => { 182 | logger.mark('[快讯][定时触发] 开始定时发送') 183 | // 取到消息列表 184 | const msg = await mian(Bot.uin) 185 | 186 | // 发送 187 | for (let channel_id of Object.keys(postlist)) { 188 | post_msg(Bot.makeForwardMsg(msg), postlist[channel_id].type, channel_id) 189 | await sleep(10000) // 等10秒在下一个 190 | } 191 | logger.mark('[快讯][定时触发] 结束定时发送') 192 | }) -------------------------------------------------------------------------------- /手办化.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | import axios from 'axios' 5 | 6 | // 填入你的智谱key或token,如没有可加群获取token 7 | // 收费标准为按调用次数后付费,0.06 元/次 8 | const API_KEY = "" 9 | 10 | const prompt = "Using the nano-banana model, a commercial 1/7 scale figurine of the character in the picture was created, depicting a realistic style and a realistic environment. The figurine is placed on a computer desk with a round transparent acrylic base. There is no text on the base. The computer screen shows the Zbrush modeling process of the figurine. Next to the computer screen is a BANDAI-style toy box with the original painting printed on it. Please turn this photo into a figure. Behind it, there should be a partially transparent plastic paper box with the character from this photo printed on it. In front of the box, on a round plastic base, place the figure version of the photo I gave you. I'd like the PVC material to be clearly represented. It would be even better if the background is indoors" 11 | 12 | 13 | async function GLM(imageUrl) { 14 | 15 | let data = JSON.stringify({ 16 | "stream": false, 17 | "agent_id": "cartoon_generator_agent", 18 | "messages": [ 19 | { 20 | "role": "user", 21 | "content": [ 22 | { 23 | "type": "image_url", 24 | "image_url": imageUrl 25 | }, 26 | { 27 | "type": "text", 28 | "text": prompt 29 | } 30 | ] 31 | } 32 | ] 33 | }) 34 | 35 | try { 36 | 37 | let config = { 38 | method: 'POST', 39 | url: 'https://open.bigmodel.cn/api/v1/agents', 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | 'Authorization': `Bearer ${API_KEY}`, 43 | }, 44 | data: data 45 | } 46 | 47 | 48 | const res = await axios.request(config) 49 | 50 | const image_url = res.data?.choices?.[0]?.messages[0]?.content[0]?.image_url 51 | if (image_url) { 52 | return { success: true, imageUrl: image_url } 53 | } else { 54 | logger.info(image_url) 55 | return { success: false, error: '图片生成失败' } 56 | } 57 | 58 | } catch (error) { 59 | return { success: false, error: error.message } 60 | } 61 | } 62 | 63 | 64 | export class GLM_手办化 extends plugin { 65 | constructor() { 66 | super({ 67 | name: '手办化', 68 | event: 'message', 69 | priority: -Infinity, 70 | rule: [{ reg: '^#?(#手办|手办化|变手办|转手办)$', fnc: 'X' }] 71 | }) 72 | } 73 | 74 | 75 | async X(e) { 76 | try { 77 | const { url: imageUrl, txt: sourceType } = await getPriorityImage(e) 78 | 79 | if (!imageUrl) return e.reply('请发送或引用1张图片', true) 80 | 81 | e.reply('来了来了,等派蒙一小会', true) 82 | 83 | const result = await GLM(imageUrl) 84 | 85 | if (result.success) { 86 | await e.reply(segment.image(result.imageUrl), true) 87 | } else { 88 | await e.reply(`生成失败:${result.error}`, true) 89 | } 90 | 91 | } catch (err) { 92 | let msg = '生成失败' 93 | if (axios.isAxiosError(err)) { 94 | if (err.code === 'ECONNABORTED') msg += ':请求超时' 95 | else if (err.response) msg += `:HTTP ${err.response.status}` 96 | else msg += `:${err.message}` 97 | } else if (err instanceof Error) msg += `:${err.message}` 98 | await e.reply(msg, true) 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * 获取优先级图片 105 | * 优先级:引用消息图片 > 当前消息图片 > @用户头像 > 发送者头像 106 | * @param {object} e - 消息事件对象 107 | * @returns {Promise<{url: string|null, txt: string|null}>} 108 | */ 109 | async function getPriorityImage(e) { 110 | const result = { url: null, txt: null } 111 | 112 | // 1️⃣ 引用消息里的图片 113 | if (e.getReply || e.source) { 114 | try { 115 | let source 116 | if (e.getReply) { 117 | source = await e.getReply() 118 | } else if (e.source) { 119 | if (e.group?.getChatHistory) { 120 | source = (await e.group.getChatHistory(e.source.seq, 1)).pop() 121 | } else if (e.friend?.getChatHistory) { 122 | source = (await e.friend.getChatHistory(e.source.time, 1)).pop() 123 | } 124 | } 125 | 126 | if (source?.message?.length) { 127 | const imgs = source.message 128 | .filter(m => m.type === "image") 129 | .map(m => m.url) 130 | if (imgs.length > 0) { 131 | result.url = imgs[0] 132 | result.txt = "引用" 133 | return result 134 | } 135 | } 136 | } catch { } 137 | } 138 | 139 | // 2️⃣ 当前消息中的图片 140 | const imgSeg = e.message.find(m => m.type === "image") 141 | if (imgSeg?.url) { 142 | result.url = imgSeg.url 143 | result.txt = "消息" 144 | return result 145 | } 146 | 147 | // 3️⃣ 被 @ 的用户头像 148 | const atSeg = e.message.find(m => m.type === "at") 149 | if (atSeg?.qq) { 150 | result.url = `https://q1.qlogo.cn/g?b=qq&nk=${atSeg.qq}&s=640` 151 | result.txt = "@头像" 152 | return result 153 | } 154 | 155 | // 4️⃣ 当前触发用户头像 156 | if (e.user_id) { 157 | result.url = `https://q1.qlogo.cn/g?b=qq&nk=${e.user_id}&s=640` 158 | result.txt = "用户头像" 159 | return result 160 | } 161 | 162 | return result 163 | } 164 | 165 | -------------------------------------------------------------------------------- /文案.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | /**统一网络请求函数,不管返回格式 5 | * @param url 要请求的url 6 | */ 7 | async function get_data(url) { 8 | let result = await fetch(url, { 9 | headers: { 10 | 'User-Agent': 'copywriting (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 11 | }, 12 | }) 13 | 14 | return result 15 | } 16 | 17 | /** 统一图片请求 18 | * 返回一个格式化数据 19 | * @param url 图片的链接 20 | * @returns return.success 是否成功 21 | * @returns return.base64Img 成功时返回的base64值,不包含头 22 | * @returns return.errmsg 失败时返回失败原因 23 | */ 24 | //import https from 'https' 25 | /** 暂时用不着 26 | async function imgUrlToBase64(url) { 27 | let base64Img 28 | return new Promise(function (resolve) { 29 | const options = { 30 | headers: { 31 | 'User-Agent': 'copywriting (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 32 | } 33 | } 34 | let req = https.get(url,options, function (res) { 35 | var chunks = [] 36 | var size = 0 37 | res.on('data', function (chunk) { 38 | chunks.push(chunk) 39 | size += chunk.length //累加缓冲数据的长度 40 | }) 41 | res.on('end', function (err) { 42 | var data = Buffer.concat(chunks, size) 43 | base64Img = data.toString('base64') 44 | resolve({ success: true, base64Img }) 45 | }) 46 | }) 47 | req.on('error', (e) => { 48 | resolve({ success: false, errmsg: e.message }) 49 | }) 50 | req.end() 51 | }) 52 | } 53 | */ 54 | 55 | 56 | export class copywriting extends plugin { 57 | constructor() { 58 | super({ 59 | name: '文案', 60 | dsc: '搜集各类文案API', 61 | event: 'message', 62 | priority: 5000, 63 | rule: [ 64 | { 65 | reg: /^#?(每|今)(日|天)(一)?(句|言|英语).*$/, 66 | fnc: 'dsapi' 67 | }, 68 | { 69 | reg: /^#*(一言|壹言)$/, 70 | fnc: "hitokoto", 71 | }, 72 | { 73 | reg: /^#?(每日|今日|本日)?(新闻|60s|60S).*$/, 74 | fnc: 'news' 75 | }, 76 | { 77 | reg: /^#?(青年)?大学习(完成|截图)?/, 78 | fnc: 'qndxx' 79 | }, 80 | { 81 | reg: /^#?古(诗|词|名句|诗词)/, 82 | fnc: 'poetry' 83 | }, 84 | { 85 | reg: /^#?历史上的今天/, 86 | fnc: 'histoday' 87 | } 88 | ] 89 | }) 90 | } 91 | 92 | // 金山词霸每日一句 93 | // API 文档:https://open.iciba.com/index.php?c=wiki 94 | async dsapi(e) { 95 | let result = await get_data('https://open.iciba.com/dsapi/') 96 | result = await result.json() 97 | //await e.reply(uploadRecord(result.tts, 0, false)) // 高清语音,需安装支持模块 98 | await e.reply(segment.record(result.tts)) //普通语音 99 | await e.reply(segment.image(result.fenxiang_img)) 100 | return true 101 | } 102 | 103 | // 一言 104 | async hitokoto(e) { 105 | let result = await get_data('https://v1.hitokoto.cn/?encode=text&charset=utf-8&c=d&c=i&c=h&c=e') 106 | result = await result.text() 107 | e.reply(result) 108 | } 109 | 110 | // 每日60秒新闻,数据来自《每天60秒读懂世界》公众号 111 | async news(e) { 112 | let result = await fetch('https://uapi.woobx.cn/app/alapi', { 113 | method: 'POST', 114 | headers: { 115 | 'User-Agent': 'copywriting (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)', 116 | 'Accept-Encoding': 'gzip' 117 | }, 118 | body: new URLSearchParams({ 119 | 'path': '/api/zaobao' 120 | }) 121 | }) 122 | result = await result.json() 123 | 124 | await e.reply(segment.record(result.data.audio)) 125 | await e.reply(segment.image(result.data.image)) 126 | return true 127 | } 128 | 129 | // 青年大学习完成图 130 | async qndxx(e) { 131 | let result = await get_data('https://quickso.cn/api/qndxx/api.php?sort=random') 132 | result = await result.text() 133 | result = result.replace(/<\/br>/g, '').trim() 134 | e.reply(segment.image(`https://${result}`)) 135 | } 136 | 137 | // 古诗词名句 138 | async poetry(e) { 139 | let result = await get_data('https://oiapi.net/API/Sentences') 140 | result = await result.json() 141 | e.reply(`${result.data.content}——${result.data.author}《${result.data.works}》`) 142 | } 143 | 144 | // 历史上的今天 145 | async histoday(e) { 146 | if (true) { e.reply('此功能极其危险,请谨慎开启\n如需开启请编辑源码注释或删除此行代码'); return true } 147 | const today = new Date() 148 | const month = Number(today.getMonth() + 1) // 月份从0开始,所以要加1 149 | const day = Number(today.getDate()) 150 | 151 | let result = await get_data(`https://uapi.woobx.cn/app/histoday?month=${month}&day=${day}`) 152 | result = await result.json() 153 | let msg_list = [] 154 | msg_list.push({ 155 | user_id: 2854200865, 156 | nickname: '今日日期', 157 | message: `${month}月${day}日` 158 | }) 159 | // 历史事件 160 | result.data.forEach(event => { 161 | let msg = [`${event.year}年 ${event.title}\n${event.content}`] 162 | 163 | // 如果有图片,则添加图片 164 | if (event.cover) { 165 | msg.push(segment.image(event.cover)) 166 | } 167 | 168 | // 如果有链接,则添加链接 169 | if (event.detail) { 170 | msg.push(`相关链接:${event.detail}`) 171 | } 172 | 173 | // 合并消息 174 | msg_list.push({ 175 | user_id: 2854200865, 176 | nickname: '历史事件', 177 | message: msg 178 | }) 179 | }) 180 | 181 | await e.reply(await Bot.makeForwardMsg(msg_list)) 182 | } 183 | } -------------------------------------------------------------------------------- /Abandon/mcsm管理器.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | 5 | import plugin from '../../lib/plugins/plugin.js' 6 | 7 | /** 用户的 apikey */ 8 | const api_key = '' 9 | /** mcsm 的域名/IP 注意不能以 `/` 结尾 */ 10 | const api_url = '' 11 | 12 | /** 统一网络请求模块 13 | * @url api入口,只需要 api/ 以后的部分 14 | */ 15 | function mcsmapi(url) { 16 | // 拼接链接,务必保证传进来的 url 格式正确 17 | url = `${api_url}/api/${url}` 18 | if (url[-1] == '?') { 19 | url = `${url}apikey=${api_key}` 20 | } else { 21 | url = `${url}&apikey=${api_key}` 22 | } 23 | logger.mark(`[mcsm管理器][网络请求] 开始请求${url}`) 24 | try { 25 | return fetch(url) 26 | .then(response => { 27 | if (!response.ok) { 28 | logger.error('[mcsm管理器][网络请求] 网络请求失败') 29 | return false 30 | } 31 | return response.json() 32 | }) 33 | } 34 | catch { 35 | logger.error('[mcsm管理器][网络请求] 网络请求失败') 36 | return false 37 | } 38 | 39 | } 40 | /** 通用消息精简模块 41 | * @msg 待精简的消息 42 | */ 43 | function msg_simplify(msg) { 44 | msg = msg.replace('mcsm', '') 45 | msg = msg.replace('#', '') 46 | msg = msg.replace('实例', '') 47 | msg = msg.replace('容器', '') 48 | msg = msg.replace('列表', '') 49 | msg = msg.replace('状态', '') 50 | msg = msg.replace('重启', '') 51 | return msg 52 | } 53 | /** 获取节点的GID 54 | * @n 可选项,整数型,选择第几个节点 55 | */ 56 | async function git_default_gid(n) { 57 | const data = await mcsmapi('service/remote_services_list?') 58 | if (n === "") {n = 0} 59 | return data.data[n].uuid 60 | 61 | } 62 | 63 | 64 | // 开始主函数部分 65 | export class example extends plugin { 66 | constructor() { 67 | super({ 68 | name: 'mcsm管理器', 69 | dsc: '对接mcsm面板进行控制', 70 | event: 'message', 71 | priority: 5000, 72 | rule: [{ 73 | reg: '^#?(mcsm)?(进程守护|服务器|节点|主机)(列表|状态)$', 74 | fnc: 'remote_services_list' 75 | }, 76 | { 77 | reg: '^#?(mcsm)?(实例|容器)(列表|状态)', 78 | fnc: 'remote_service_instances' 79 | }, 80 | { 81 | reg: '^#?(mcsm)?重启(实例|容器)', 82 | fnc: 'protected_instance_restart' 83 | } 84 | ] 85 | }) 86 | } 87 | 88 | 89 | // 进程守护列表 90 | async remote_services_list(e) { 91 | /** 最终发送的消息 */ 92 | let mdmsg = '节点列表' 93 | // 调用函数并处理返回的数据或错误 94 | mcsmapi(`service/remote_services_list?`) 95 | .then(async data => { 96 | // 开始处理返回的数据 97 | data.data.forEach(item => { 98 | let msgList_ = `名称:${item.remarks}\n` 99 | msgList_ += `GID:${item.uuid}\n` 100 | if (item.available) {msgList_ += `状态:在线`} else {msgList_ += `状态:离线`} 101 | // 提交信息到信息列表 102 | mdmsg += '\n---\n'+msgList_ 103 | }) 104 | e.reply(mdmsg) 105 | return true 106 | }) 107 | .catch(error => { 108 | // 在这里处理错误 109 | logger.error('[mcsm管理器][进程守护列表]',error) 110 | e.reply('网络请求错误',error) 111 | return false 112 | }) 113 | } 114 | 115 | // 查询指定节点内的实例状态 116 | async remote_service_instances(e) { 117 | /** 最终发送的消息 */ 118 | let mdmsg = '实例状态\n' 119 | /** 节点的唯一识别号 */ 120 | let gid = msg_simplify(e.msg) 121 | gid = gid.replace(/ /g, "") 122 | if (!gid) { 123 | mdmsg += '未发送GID,默认查询主节点状态' 124 | gid = await git_default_gid(0) 125 | } else { 126 | mdmsg += `GID:${gid}` 127 | } 128 | // 调用函数并处理返回的数据或错误 129 | mcsmapi(`service/remote_service_instances?remote_uuid=${gid}&page=1&page_size=50&instance_name=`) 130 | .then(async data => { 131 | // 开始处理返回的数据 132 | data.data.data.forEach(data => { 133 | let msgList_ = '' 134 | msgList_ += `名称:${data.config.nickname}\nUID:${data.instanceUuid}\n` 135 | switch (true) { 136 | // 会返回的值及其解释:-1(状态未知);0(已停止);1(正在停止);2(正在启动);3(正在运行) 137 | case data.status == -1: 138 | msgList_ += '状态:未知' 139 | break 140 | case data.status == 0: 141 | msgList_ += '状态:已停止' 142 | break 143 | case data.status == 1: 144 | msgList_ += '状态:正在停止' 145 | break 146 | case data.status == 2: 147 | msgList_ += '状态:正在启动' 148 | break 149 | case data.status == 3: 150 | msgList_ += '状态:正常运行' 151 | break 152 | default: 153 | msgList_ += '状态:异常返回值' 154 | logger.error('异常的返回值:' + data.status) 155 | } 156 | mdmsg += '\n---\n'+msgList_ 157 | }) 158 | e.reply(mdmsg) 159 | return true 160 | }) 161 | .catch(error => { 162 | // 在这里处理错误 163 | logger.error('[mcsm管理器][实例列表]',error) 164 | e.reply('网络请求错误',error) 165 | return false 166 | }) 167 | } 168 | 169 | async protected_instance_restart(e) { 170 | /** 最终发送的消息 */ 171 | let mdmsg = '重启实例\n' 172 | // 简化消息 173 | let msg = msg_simplify(e.msg) 174 | msg = msg.split(" ") 175 | e.reply(msg[0] + '===' + msg[1]) 176 | if (msg[1] == '') {} //这里想实现的是gid可选,msg0是gid,1是uid。如果msg1是空则代表没写giu,此时msg0是uid,giu则自动获取 177 | if (!gid) { 178 | mdmsg += '未发送GID,默认查询主节点状态' 179 | gid = await git_default_gid(0) 180 | } else { 181 | mdmsg += `GID:${gid}\n--\n` 182 | } 183 | // 调用函数并处理返回的数据或错误 184 | mcsmapi(`protected_instance/restart?remote_uuid=${msg[0]}&uuid=${msg[1]}`) 185 | .then(async data => { 186 | return // 未实现 187 | }) 188 | } 189 | } -------------------------------------------------------------------------------- /米游社cos.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 此插件为二改插件 3 | * 二改作者:xiaotian2333 4 | * 开源地址:https://github.com/xiaotian2333/yunzai-plugins-Single-file 5 | * 源作者:bling_yshs 6 | * 源开源地址:https://gitee.com/bling_yshs/yunzaiv3-ys-plugin 7 | */ 8 | 9 | // 主动触发相关 10 | /** 文件大小提示开关,开启后总大小超过提示值时提示 */ 11 | const size_tips = true 12 | /** 文件大小提示值,仅在提示开关开启后生效,单位M */ 13 | const size_than = 15 14 | 15 | // 自动触发相关 16 | /** 自动发送cos的群/人列表 17 | * @param type (group,private)群还是人 18 | */ 19 | const postlist = { 20 | /** 人示例 */ 21 | 1719549416: { 22 | type: "private" 23 | }, 24 | /** 群示例 */ 25 | 628306033: { 26 | type: "group" 27 | } 28 | } 29 | /** 自动触发的时间 30 | * cron表达式定义推送时间 (秒 分 时 日 月 星期) 31 | * 可使用此网站辅助生成:https://www.matools.com/cron/ 32 | * 注意,每天都需要触发,因此日及以上选通配符或不指定 33 | * 只选小时就可以了 34 | */ 35 | const auto_cron = "30 8 12 * * *" 36 | //const auto_cron = "1 * * * * *" // 每分钟触发一次,测试用 37 | 38 | 39 | import fetch from 'node-fetch' 40 | import schedule from 'node-schedule' 41 | 42 | 43 | /** 统一主动信息发送 44 | * 成功true,失败false 45 | * @param msg 要发送的信息 46 | * @param source (group,private) 发到什么渠道 47 | * @param channel_id 渠道的标识ID 48 | */ 49 | function post_msg(msg, source, channel_id) { 50 | if (source == "group") { 51 | // 群 52 | Bot.pickGroup(channel_id).sendMsg(msg) 53 | return true 54 | } else if (source == "private") { 55 | // 私聊 56 | Bot.pickUser(channel_id).sendMsg(msg) 57 | return true 58 | } else { 59 | logger.error(`[米游社cos][消息发送] 没有匹配的发送渠道,关键信息:source=${source},channel_id=${channel_id},msg=${msg}`) 60 | return false 61 | } 62 | } 63 | 64 | 65 | /** 获取资源列表 */ 66 | async function get_data() { 67 | // forumId对应不同社区的不同板块,gameType对返回值没有影响,但可作为游戏类型的判断 68 | const config = [ 69 | // 原神 70 | { forumId: '49', gameType: '2' }, 71 | // 崩铁 72 | { forumId: '62', gameType: '6' }, 73 | // 大别野 74 | { forumId: '47', gameType: '5' }, 75 | // 绝区零 76 | { forumId: '65', gameType: '8' }, 77 | // 崩环3 78 | //由于崩环3没有专门的cos分区,因此可能导致很多杂乱的东西,自行考虑是否开启,默认关闭 79 | // { forumId: '4', gameType: '1' } 80 | ] 81 | /** 随机生成0到3的整数,用于随机选择 config 里的其中一个并保存到 selected 变量 */ 82 | const selected = config[Math.floor(Math.random() * 4)] 83 | /** 随机生成1到3的整数,貌似只有1跟2影响数据,还是先保留着吧 */ 84 | const pageNum = Math.floor(Math.random() * 3) + 1 85 | /** 是否获取热门数据 */ 86 | let is_hot = Math.floor(Math.random() * 2) 87 | if (is_hot == 1) { is_hot = "false" } else if (is_hot == 0) { is_hot = "true" } 88 | 89 | // 拼接请求地址 90 | const url = `https://bbs-api.miyoushe.com/post/wapi/getForumPostList?forum_id=${selected.forumId}&gids=${selected.gameType}&is_good=false&is_hot=${is_hot}&page_size=20&sort_type=${pageNum}` 91 | // 这是测试用地址,由于米游社的数据每天都会变,因此采用自存挡数据 92 | //const url = 'https://oss.xt-url.com/%E7%B1%B3%E6%B8%B8%E7%A4%BEcos%E6%8F%92%E4%BB%B6%E6%A0%B7%E6%9C%AC%E6%95%B0%E6%8D%AE.json' 93 | 94 | // 发起请求 95 | let result = await fetch(url, { 96 | headers: { 97 | 'User-Agent': 'msy-cos (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 98 | } 99 | }) 100 | result = await result.json() 101 | // 随机生成0到19的整数,用于随机选择列表里的一个帖子并展开 102 | return result?.data?.list[Math.floor(Math.random() * 20)] 103 | // 搭配测试用地址 , 0 视频帖子,1 图片帖子 104 | //result = result?.data?.list[0] 105 | } 106 | 107 | /** 108 | * 信息发送函数 109 | * 110 | * @param sender 合并信息里的图片/视频发送者QQ号 111 | * @param source (Group,User) 信息来自源标识 112 | * @param channel_id 信息来源的标识ID 113 | * @param tips 是否开启提醒,自动触发时保持关闭 114 | */ 115 | async function mian(sender, source, channel_id, tips) { 116 | const result = await get_data() 117 | /** 合并消息列表,多图时使用 */ 118 | let msgList = [] 119 | 120 | // 这是在合并消息前面的提示 121 | msgList.push({ 122 | user_id: 2854200865, 123 | nickname: '帖子信息', 124 | // 原帖地址貌似只看id,具体分类是不管的,那也懒得去动态变化了 125 | // 如果未来失效了就改ys成真的分区的就好 126 | message: `标题:${result.post.subject}\n原帖地址:\nhttps://www.miyoushe.com/ys/article/${result.post.post_id}\n作者:${result.user.nickname}` 127 | }) 128 | 129 | // 访问images数组并检查其长度 130 | if (result.post.images.length === 0) { 131 | /** 132 | * 空数组,这种情况有两种可能 133 | * 1.米游社改接口了 134 | * 2.这是个视频帖子,不包含图片 135 | * 现在要做的就是排除第二种可能 136 | */ 137 | 138 | // 鬼知道他为什么没法用if判断,只能用try去试了 139 | try { 140 | /** 视频最高画质挡的数据 */ 141 | const video_data = result.vod_list[result.vod_list.length - 1].resolutions[result.vod_list[result.vod_list.length - 1].resolutions.length - 1] 142 | 143 | // 这里不需要循环累计,性能损失可以忽略,就不作区分了 144 | /** 视频大小,单位M */ 145 | const video_size = ((video_data.size) / 1024 / 1024).toFixed(2) 146 | 147 | if (video_size > size_than && tips) { 148 | // 如果视频大于提示值则提示用户 149 | //e.reply() 150 | post_msg(`正在发送较大视频(${video_size}M),请耐心等待`, source, channel_id) 151 | } 152 | 153 | msgList.push({ 154 | user_id: sender, 155 | nickname: '视频', 156 | message: segment.video(video_data.url) 157 | }) 158 | msgList.push({ 159 | user_id: 2854200865, 160 | nickname: '视频信息', 161 | message: `画质:${video_data.definition}(${video_data.height}x${video_data.width})\n编码格式:${video_data.format}(${video_data.codec})\n文件大小:${video_size}M` 162 | }) 163 | return msgList 164 | } 165 | catch (err) { 166 | // 凉凉,需要人工更新插件 167 | post_msg("获取图片失败,可能是米游社接口已发生变动", source, channel_id) 168 | return false 169 | } 170 | 171 | } 172 | 173 | // 仅当开启提示时执行 174 | if (tips) { 175 | /** 图片的总大小,单位字节*/ 176 | let totalSize = 0 177 | 178 | // 计算图片总大小 179 | result.image_list.forEach(image => { 180 | // 将每个图片的size(字符串)转换为数字,然后累加到totalSize上 181 | totalSize += parseInt(image.size, 10) // 10是基数,表示十进制 182 | }) 183 | 184 | // 图片的总大小,单位变为M 185 | totalSize = ((totalSize) / 1024 / 1024).toFixed(2) 186 | if (totalSize > size_than) { 187 | // 如果图片总大小大于提示值则提示用户 188 | post_msg(`正在发送较大图片(${totalSize}M),请耐心等待`, source, channel_id) 189 | } 190 | } 191 | 192 | // 这是正常取到多张图片的处理 193 | // 获取图片列表 194 | const imgUrls = result.post?.images 195 | for (const image of imgUrls) { 196 | // 米游社从2024年10月12日起开始在域名upload-bbs.miyoushe.com拦截ua axios/1.7.7 197 | // 临时解决方法为升级或降低axios版本即可,反正别是1.7.7 198 | // 或者换个请求域名,这个域名暂时没有拦截 199 | // image = image.replace('upload-bbs.miyoushe.com','upload-bbs.mihoyo.com') 200 | 201 | // 自定义icqq的图片请求ua,感谢寒暄 202 | segment.image = (file, name) => ({ 203 | type: 'image', 204 | file, 205 | name, 206 | headers: { 207 | 'User-Agent': 'msy-cos (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 208 | } 209 | }) 210 | msgList.push({ 211 | user_id: sender, 212 | nickname: '图片', 213 | message: segment.image(image) 214 | }) 215 | } 216 | return msgList 217 | } 218 | 219 | export class example extends plugin { 220 | constructor() { 221 | super({ 222 | name: '米游社cos', 223 | dsc: '发送米游社cos图片或视频', 224 | event: 'message', 225 | priority: 5000, 226 | rule: [{ 227 | reg: /^#?(米游社|mys)?cos$/, 228 | fnc: 'cos', 229 | //permission: 'master', // 仅限主人可触发 230 | }] 231 | }) 232 | } 233 | 234 | async cos(e) { 235 | let channel_id = "undefined" 236 | if (e.message_type == "group") { 237 | // 群 238 | channel_id = e.group_id 239 | } else if (e.message_type == "private") { 240 | // 私聊 241 | channel_id = e.from_id 242 | } 243 | await e.reply(await Bot.makeForwardMsg(await mian(e.user_id, e.message_type, channel_id, size_tips))) 244 | return true 245 | } 246 | } 247 | 248 | /** 休眠函数 249 | * @time 毫秒 250 | */ 251 | function sleep(time) { 252 | return new Promise((resolve) => setTimeout(resolve, time)) 253 | } 254 | 255 | /** 主动触发-发到指定群 */ 256 | schedule.scheduleJob(auto_cron, async () => { 257 | logger.mark('[米游社cos][定时触发] 开始定时发送') 258 | // 取到消息列表 259 | const msg = await mian(2854196310, "private", Bot.uin, false) 260 | 261 | // 发送 262 | for (let channel_id of Object.keys(postlist)) { 263 | post_msg(Bot.makeForwardMsg(msg), postlist[channel_id].type, channel_id) 264 | await sleep(10000) // 等10秒在下一个 265 | } 266 | logger.mark('[米游社cos][定时触发] 结束定时发送') 267 | }) -------------------------------------------------------------------------------- /自动优选签名IP.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | // 由于hosts文件权限问题,需要以root权限(linux)或Administrator权限(windows)运行云崽 4 | // 否则无法写入hosts文件 5 | 6 | 7 | import { get } from 'https'; 8 | import { createConnection } from 'net'; 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | import cfg from '../../lib/config/config.js'; 12 | import makeConfig from "../../lib/plugins/config.js"; 13 | 14 | // 配置区 15 | const PORT = 443; // 测试用的 TCP 端口,可按需改为 80 或其它 16 | const TIMEOUT = 3000; // 单次连接超时 (ms) 17 | const TRY_COUNT = 3; // 每个 IP 测试次数 18 | const pluginName = "自动优选签名IP"; // 插件名字 19 | 20 | 21 | /** 22 | * 从 ICQQ 插件的配置文件中读取 sign_api_addr 23 | * @returns {Promise} 24 | * 从ICQQ-plugin里面扣的代码 25 | */ 26 | const { config, configSave } = await makeConfig("ICQQ", { 27 | tips: "", 28 | permission: "master", 29 | markdown: { 30 | mode: false, 31 | button: false, 32 | callback: true, 33 | }, 34 | bot: {}, 35 | token: [], 36 | }, { 37 | tips: [ 38 | "欢迎使用 TRSS-Yunzai ICQQ Plugin ! 作者:时雨🌌星空", 39 | "参考:https://github.com/TimeRainStarSky/Yunzai-ICQQ-Plugin", 40 | ], 41 | }) 42 | 43 | /** 44 | * 获取 A 记录里的所有 IP 45 | * @param {string} _DNS_API - DNS API 地址,接受DNS JSON API查询链接输入 46 | * @returns {Promise} IP 地址数组 47 | */ 48 | function fetchIpList(_DNS_API) { 49 | return new Promise((resolve, reject) => { 50 | get(_DNS_API, res => { 51 | let raw = ''; 52 | res.on('data', chunk => raw += chunk); 53 | res.on('end', () => { 54 | try { 55 | const json = JSON.parse(raw); 56 | const ips = (json.Answer || []) 57 | .filter(x => x.type === 1) // 只要 type=1 (A 记录) 58 | .map(x => x.data); 59 | resolve([...new Set(ips)]); // 去重 60 | } catch (e) { reject(e); } 61 | }); 62 | }).on('error', reject); 63 | }); 64 | } 65 | 66 | /** 67 | * 单次 TCP 连接测速 68 | * @param {string} ip - 要测试的 IP 地址 69 | * @param {number} port - 要测试的端口号 70 | * @param {number} timeout - 连接超时时间 71 | * @returns {Promise} 连接延迟(毫秒) 72 | */ 73 | 74 | function tcpPing(ip, port = PORT, timeout = TIMEOUT) { 75 | return new Promise(resolve => { 76 | const start = Date.now(); 77 | const socket = createConnection({ host: ip, port, timeout }, () => { 78 | const cost = Date.now() - start; 79 | socket.destroy(); 80 | resolve(cost); 81 | }); 82 | const onErr = () => { // error / timeout 都算失败 83 | socket.destroy(); 84 | resolve(timeout); // 以超时值计入 85 | }; 86 | socket.on('error', onErr); 87 | socket.on('timeout', onErr); 88 | }); 89 | } 90 | 91 | /** 92 | * 对某 IP 连续测 TRY_COUNT 次并计算平均值 93 | * @param {string} ip - 要测试的 IP 地址 94 | * @returns {Promise<{ip: string, avg: number}>} 包含 IP 和平均延迟的对象 95 | */ 96 | async function testIpLatency(ip) { 97 | let sum = 0; 98 | for (let i = 0; i < TRY_COUNT; i++) { 99 | const cost = await tcpPing(ip); 100 | sum += cost; 101 | } 102 | return { ip, avg: sum / TRY_COUNT }; 103 | } 104 | 105 | /** 106 | * 主逻辑函数 107 | * @param {string} _DNS_API - DNS API 地址,接受DNS JSON API查询链接输入 108 | * @returns {Promise} 最快的 IP 或 false 109 | */ 110 | async function getFastestIp(_DNS_API) { 111 | try { 112 | const ips = await fetchIpList(_DNS_API) 113 | if (!ips.length) return false; 114 | 115 | const results = await Promise.all(ips.map(testIpLatency)); 116 | // 输出详细表格 117 | console.log(`[${pluginName}] 签名延迟测试`); 118 | console.log(`共测试 ${ips.length} 个 IP,每个 IP 测试 ${TRY_COUNT} 次`); 119 | console.table(results.map(r => ({ 120 | IP: r.ip, 121 | 'Avg (ms)': r.avg.toFixed(2) 122 | }))); 123 | results.sort((a, b) => a.avg - b.avg); 124 | console.log(`最快 IP: ${results[0].ip} (${results[0].avg.toFixed(2)} ms)`); 125 | return results[0].ip; 126 | } catch (err) { 127 | return false; 128 | } 129 | } 130 | 131 | /** 132 | * hosts 文件更新函数 133 | * @param {string} FalshIP - 要写入的新 IP 地址 134 | * @param {string} hostName - 要更新的域名 135 | */ 136 | async function updateHosts(FalshIP, hostName) { 137 | const newHostEntry = `${FalshIP} ${hostName}`; 138 | 139 | // 1. 根据不同操作系统,确定 hosts 文件路径 140 | // Windows 路径为 %SystemRoot%\System32\drivers\etc\hosts 141 | // Linux 和 macOS 路径为 /etc/hosts 142 | const hostsPath = process.platform === 'win32' 143 | ? path.join(process.env.SystemRoot, 'System32', 'drivers', 'etc', 'hosts') 144 | : '/etc/hosts'; 145 | 146 | try { 147 | // 2. 读取 hosts 文件内容 148 | const originalHostsContent = fs.readFileSync(hostsPath, 'utf8'); 149 | 150 | // 将文件内容按行分割 151 | const lines = originalHostsContent.split(/\r?\n/); 152 | 153 | let entryExists = false; 154 | let contentChanged = false; 155 | 156 | // 3. 逐行检查并更新 157 | const newLines = lines.map(line => { 158 | // 使用正则表达式匹配包含目标域名的行(忽略行首的#注释) 159 | const trimmedLine = line.trim(); 160 | if (trimmedLine.startsWith('#')) { 161 | return line; // 如果是注释行,则保持不变 162 | } 163 | 164 | // 正则表达式匹配 IP + 空白 + 域名 165 | const regex = new RegExp(`^(\\S+)\\s+(${hostName})$`); 166 | const match = trimmedLine.match(regex); 167 | 168 | if (match) { 169 | entryExists = true; 170 | // 如果 IP 地址与新 IP 不同,则替换为新行 171 | if (match[1] !== FalshIP) { 172 | contentChanged = true; 173 | return newHostEntry; 174 | } 175 | } 176 | return line; // 其他行保持不变 177 | }); 178 | 179 | // 4. 如果域名不存在,则在文件末尾添加新条目 180 | if (!entryExists) { 181 | // 如果文件末尾不是空行,则先添加一个换行符 182 | if (originalHostsContent.length > 0 && !originalHostsContent.endsWith('\n')) { 183 | newLines.push(''); 184 | } 185 | newLines.push(newHostEntry); 186 | contentChanged = true; 187 | } 188 | 189 | // 5. 如果内容有变,则写入文件 190 | if (contentChanged) { 191 | const newHostsContent = newLines.join('\n'); 192 | fs.writeFileSync(hostsPath, newHostsContent); 193 | return { type: true, msg: `Hosts 文件已成功更新: ${hostName} -> ${FalshIP}` } 194 | } else { 195 | return { type: true, msg: `无需更新,Hosts 配置已是最新。` } 196 | } 197 | 198 | } catch (error) { 199 | // 6. 捕获并处理异常 200 | if (error.code === 'EPERM' || error.code === 'EACCES') { 201 | return { type: false, msg: '权限不足,无法修改 hosts 文件。请尝试使用管理员或 root 权限运行。' } 202 | } else { 203 | return { type: false, msg: `更新 hosts 文件时发生未知错误: ${error.message}` } 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * 云崽类型判断函数 210 | * @returns {string} 云崽类型,返回云崽的类型,如:miao、trss、pe、yunzai 211 | */ 212 | function getYunzaiType() { 213 | const type = cfg._package.name 214 | if (type === 'trss-yunzai') { 215 | // https://github.com/TimeRainStarSky/Yunzai 216 | return 'trss' 217 | } 218 | if (type === 'miao-yunzai') { 219 | // https://github.com/yoimiya-kokomi/miao-yunzai 220 | return 'miao' 221 | } 222 | if (type === 'yunzai-pe') { 223 | // https://github.com/yunzaijs/bot 224 | return 'pe' 225 | } 226 | if (type === 'yunzai') { 227 | // 默认的云崽名,通常是云崽v2或v3的原版或轻量版 228 | return 'yunzai' 229 | } 230 | return 'unknown' 231 | } 232 | 233 | /** 234 | * 获取当前配置的签名 API 地址,并解析成HOST 235 | * @returns {Promise} 返回当前配置的签名 API 的 HOST 236 | */ 237 | function getHostName() { 238 | const type = getYunzaiType() 239 | let sign_api_addr 240 | if (type === 'trss') { 241 | // 读取config/ICQQ.yaml 242 | sign_api_addr = config.bot.sign_api_addr 243 | } else if (type === 'miao') { 244 | // 读取cfg.config.bot.sign_api_addr 245 | sign_api_addr = cfg.config.bot.sign_api_addr 246 | } else { 247 | logger.error(`[${pluginName}]暂不支持此云崽分支自动读取签名配置`) 248 | return false 249 | } 250 | if (sign_api_addr) { 251 | return new URL(sign_api_addr).hostname 252 | } 253 | logger.error(`[${pluginName}]云崽签名配置为空,如您已完成配置请重启云崽后重试`) 254 | return false 255 | } 256 | 257 | export class testip extends plugin { 258 | constructor() { 259 | super({ 260 | name: pluginName, 261 | event: 'message', 262 | priority: 5000, 263 | rule: [ 264 | { 265 | reg: /^#?(优选|测试|优化)(签名|sign|qsign)?(ip|IP)$/, 266 | fnc: 'testip' 267 | }, 268 | ] 269 | }) 270 | } 271 | 272 | async testip(e) { 273 | const HOST = getHostName() 274 | if (!HOST) { 275 | await e.reply(`错误:获取云崽签名配置失败\n暂不支持此云崽分支自动读取签名配置或签名配置为空\n详情请查看日志`) 276 | return true 277 | } 278 | 279 | const DNS_API = `https://dns.alidns.com/resolve?name=${HOST}&type=A`; 280 | const FalshIP = await getFastestIp(DNS_API) 281 | if (!FalshIP) { 282 | await e.reply(`获取 IP 失败`) 283 | return true 284 | } 285 | 286 | await e.reply(`获取到最快的 IP 为:${FalshIP}`); 287 | const data = await updateHosts(FalshIP, HOST); 288 | if (data.type) { 289 | await e.reply(data.msg) 290 | } else { 291 | await e.reply(data.msg) 292 | } 293 | return true 294 | } 295 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

云崽轻量级插件

2 | 3 | ## 关于喵崽兼容性的相关说明 4 | 5 | 已放弃对喵崽的主动支持,插件开发转用TRSS-Yunzai 6 | 如插件更新导致喵崽无法使用,但此前能使用的请[回到过去](https://github.com/xiaotian2333/yunzai-plugins-Single-file/tree/ebea035e86f421c92242ff9769488ea236c9e3ef) 7 | 8 | > 虽然放弃主动支持,但不同崽之间是有基础兼容性的,并不代表插件在喵崽上完全无法使用,只是不会进行针对性优化 9 | 10 | 如有问题可加Q群提问 [628306033](https://jq.qq.com/?k=fjSGhscz) 11 | 12 | ## 安装方式 13 | 14 | 下载对应文件放入 `plugins\example` 文件夹即可 15 | 16 | ## 插件介绍 17 | 18 | 排名按编写时间排序 19 | 20 | ### GH仓库 21 | 22 | 在检测到github链接时发送仓库速览图 23 | 支持仓库、issue、pr、commit、release的速览图 24 | 速览图如下 25 | 26 | ![示例图](https://opengraph.githubassets.com/xiaotian/xiaotian2333/yunzai-plugins-Single-file) 27 | 28 | ### 唱鸭 29 | 30 | 随机返回一段唱鸭的音频片段 31 | 32 | > 需安装 ffmpeg 否则无法生效,默认开启高清语音,需安装枫叶,土块等包含高清语音的插件或独立的[高清语音模块](https://github.com/xiaotian2333/YunzaiBOT-HD-Voice-module) 33 | 34 | ### 综合帮助 35 | 36 | 发送一张帮助图片,聚合各菜单的入口(图片需自己生成) 37 | 图片生成方式:使用锅巴备份喵喵插件,然后改里面的文案,发送帮助保存图片就行了。最后把备份的还原回去 38 | 39 | ### 点赞续火 40 | 41 | 自动化点赞和续火,免去每天发“赞我”的麻烦 42 | 需要打开插件自行修改相关配置 43 | 44 | ### MC服务器状态 45 | 46 | 查询mc服务器的状态,可配置默认查询的服务器 47 | 目前仅支持java版 48 | 49 | ### 米游社cos 50 | 51 | 获取米游社里的cos帖子并合并发送里面的图片或视频 52 | 二改自[此插件](https://gitee.com/bling_yshs/yunzaiv3-ys-plugin/blob/master/ys-%E7%B1%B3%E6%B8%B8%E7%A4%BEcos.js) 53 | 54 | 要发送的图片/视频在超过一定大小后会提示,避免误以为机器人无反应 55 | 大宽带的服务器可以关闭或修改提示的阈值 56 | 57 | 新增定时发送功能,需自行配置要发送到的群或人 58 | 发送时间也可自定义,看着注释改就行 59 | 60 | ### 随机超能力 61 | 62 | 此插件的单文件版本已停止更新,请[前往此处](https://github.com/xiaotian2333/special-ability)安装新版本 63 | 单文件版本仍然可用,但不会获得任何更新支持(云列表更新可正常获取) 64 | 65 | 获取一个超能力及对应的副作用 66 | 内容仅供娱乐!!! 67 | 默认开启大众模式,放心使用 68 | 可选开启pro模式,会有更多能力及作用被加入 69 | 支持本地语句,需自行配置 70 | 71 | ### 文案 72 | 73 | 聚合各类文案api 74 | 75 | - 每日一句 - 金山词霸每日一句,发送图片及对应的英语语音,发送音频需要[高清语音模块](https://github.com/xiaotian2333/YunzaiBOT-HD-Voice-module) 76 | 77 | - 一言 - 一言文案 78 | 79 | - 青年大学习 - 发送青年大学习即可获取完成图 80 | 81 | - 新闻 - 数据来自 [一个木函](https://woobx.cn/) 82 | 83 | - 历史上的今天 - 数据来自 [一个木函](https://woobx.cn/) **!!此功能极其危险,请务必谨慎开启!!** 84 | 85 | ### 快举报 86 | 87 | 此插件的单文件版本已停止更新,请[前往此处](https://github.com/xiaotian2333/xiaotian-qunguan)安装新版本 88 | 单文件版本仍然可用,但不会获得任何更新支持 89 | 90 | 在群内快速便捷地向管理员举报违规行为 91 | 灵感来源为简幻欢群机器人,但代码均为原创 92 | 93 | 发送快举报,按流程使用即可 94 | 95 | ### 快讯 96 | 97 | 获取实时快讯,数据来源微信订阅号 98 | 99 | 有定时发送功能,需自行配置要发送到的群或人 100 | 发送时间也可自定义,看着注释改就行 101 | 102 | ### 智谱GLM 103 | 104 | > 此插件半停更状态,仅修复bug,建议使用新插件[new-api](https://github.com/xiaotian2333/yunzai-plugins-Single-file#new-api) 105 | 106 | 基于智谱大模型的聊天插件 107 | 默认使用`glm-4-flash-250414`模型 108 | 109 | 支持开箱即用,但仍建议配置自己的key 110 | 注意:沉浸式翻译官方已开始封堵第三方调用,如后续再加强检测将放弃免配置key的支持 111 | 112 | 直接艾特机器人即可对话 113 | 可发送 `#重置对话` 清除聊天记录 114 | 管理员可使用 `#智谱切换预设` 切换预设 115 | 使用 `#智谱预设列表` 查看预设列表 116 | 117 | 管理员可使用 `#智谱切换模型` 可在机器人运行时临时切换模型 118 | 使用 `#智谱模型列表` 查看模型列表 119 | 120 | 管理员可使用 `#智谱开启/关闭联网` 设置联网功能开关 121 | 122 | 插件初次加载时自动下载云端配置文件 123 | 默认数据目录为`./data/plugins/智谱GLM/` 124 | 125 | 支持统计每日token用量,并于每日0点自动导出昨日统计到数据目录 126 | 127 |
128 | 默认格式 129 | 130 | 此处展示的文本限于篇幅,大幅精简且不会实时更新 131 | 如需查看原始文件请点击对应的链接查看 132 | 133 | [system_prompt.json](https://oss.xt-url.com/GPT-Config/system_prompt.json) 134 | 135 | ``` json 136 | { 137 | "预设名1": "预设内容1", 138 | "预设名2": "预设内容2" 139 | } 140 | ``` 141 | 142 | [model_list.json](https://oss.xt-url.com/GPT-Config/model_list.json) 143 | 144 | ``` json 145 | { 146 | "data": "2025年5月4日", 147 | "version": "1.2.2", 148 | "author": "xiaotian2333", 149 | "tips": [ 150 | "每日token统计信息可在插件数据目录token_log.csv查看" 151 | ], 152 | "bigmodel": { 153 | "name": "智谱", 154 | "url": "https://open.bigmodel.cn/api/paas/v4/chat/completions", 155 | "instructions": "模型价格为官网公示价格", 156 | "model_list": { 157 | "glm-4-flash-250414": { 158 | "instructions": "免费的语言模型", 159 | "Price": "免费", 160 | "free": true, 161 | "type": "语言模型" 162 | }, 163 | "glm-z1-air": { 164 | "instructions": "具备强大推理能力,适用于需要深度推理的任务", 165 | "Price": "0.5元|百万Tokens", 166 | "free": false, 167 | "type": "推理模型" 168 | }, 169 | "codegeex-4": { 170 | "instructions": "代码优化模型,专为程序员准备", 171 | "Price": "0.1元|百万Tokens", 172 | "free": false, 173 | "type": "代码模型" 174 | } 175 | } 176 | } 177 | } 178 | ``` 179 | 180 |
181 | 182 | #### 可配置的参数如下 183 | 184 | | 设置项 | 默认设置 | 说明 | 185 | | --- | --- | --- | 186 | | Authorization | 空 | `API Key` 如不填则默认使用沉浸式翻译的Token | 187 | | model | glm-4-flash-250414 | 默认模型版本,可配置项参考[这里](https://www.bigmodel.cn/dev/howuse/model) | 188 | | web_search | false | 是否开启联网功能,联网搜索至少需要消耗`1000`token | 189 | | search_engine | search_std | 使用哪个搜索引擎,可配置项参考[这里](https://www.bigmodel.cn/pricing) | 190 | | max_log | 10 | 聊天记忆深度,建议范围5~20 | 191 | | think_print | false | 支持思考的模型是否输出思考过程 | 192 | | on_thinking | true | 仅 GLM-4.5 及以上模型支持此参数配置. 控制大模型是否开启思维链 | 193 | | vision_enable | false | 是否开启多模态能力 | 194 | | vision_model | glm-4.5v | 多模态模型版本,可配置项参考[这里](https://www.bigmodel.cn/dev/howuse/model) | 195 | | system_prompt | 详情见配置文件 | 系统提示词可参考[这里](https://www.bigmodel.cn/dev/howuse/prompt) | 196 | | list | 详情见源码 | 屏蔽词列表,用于过滤敏感词 | 197 | 198 | > 从2025年6月1日0点起,联网功能收费单价为0.01元/次起,因此改为默认关闭 199 | > 此处会隐藏一些一般不用调整的高级配置项,如需要调整请自行修改源码内相关配置 200 | > 注意:沉浸式翻译官方已开始封堵第三方调用,如后续再加强检测将放弃免配置key的支持[2025/11/07] 201 | 202 | ### 智谱绘图 203 | 204 | 基于智谱大模型的绘图插件 205 | 默认使用`CogView-4`模型 206 | 207 | 需要配置自己的key,否则无法使用 208 | 前往[智谱官网](https://www.bigmodel.cn/invite?icode=iGW2wQ0KiXGc0PVU%2BeTSFEjPr3uHog9F4g5tjuOUqno%3D)申请即可 209 | 210 | > 新用户赠送400次免费额度,使用完毕后可换成免费模型或继续付费使用 211 | 212 | #### 可配置的参数如下 213 | 214 | | 设置项 | 默认设置 | 说明 | 215 | | --- | --- | --- | 216 | | Authorization | 空 | `API Key` 如不填则无法使用 | 217 | | model | CogView-4 | 模型版本,可配置项参考[这里](https://www.bigmodel.cn/dev/howuse/model) | 218 | | list | 详情见源码 | 屏蔽词列表,用于过滤敏感词 | 219 | 220 | ### 取群员列表 221 | 222 | 获取当前或指定群的群员列表并导出为csv文件 223 | 用于统计群员的小插件 224 | 225 | 发送 `#取群员列表` 即可获取当前群的群员列表并导出为csv文件 226 | 发送 `#取群员列表 群号` 即可获取指定群的群员列表并导出为csv文件 227 | 228 | 默认文件导出目录为`云崽目录/data/plugins/取群员列表/` 229 | 导出文件名为`群号_userlist.csv` 230 | 231 | ### B站卡片转链接 232 | 233 | 将B站卡片解析为链接并展示基础信息 234 | 235 | > 此插件目的为屏蔽B站的QQ小程序,不会请求B站相关api获取数据 236 | > 如有更多信息展示需求请使用其他插件 237 | 238 | 无触发指令,检测到B站小程序卡片自动工作 239 | 240 | ### 哈希转磁力 241 | 242 | 将磁力链接的哈希值转为完整的磁力链接 243 | 244 | 发送 `#哈希转磁力<哈希值>` 即可将哈希值转为完整的磁力链接 245 | 或者使用简洁指令 `转磁力<哈希值>` 246 | 247 | ### 三角洲每日密码 248 | 249 | 获取三角洲每日行动门密码 250 | 251 | ### 自动优选签名IP 252 | 253 | 用于签名域名为CDN的情况 254 | 自动优选签名IP,根据延迟测试结果自动切换签名IP并写入hosts文件 255 | 256 | #### 注意 257 | 258 | 由于hosts文件权限问题,需要以root权限(linux)或Administrator权限(windows)运行云崽 259 | 否则无法写入hosts文件 260 | 261 | ### 手办化 262 | 263 | 通过智谱大模型将图片转换为手办化的图片 264 | 265 | > 需自行配置智谱key或token,没有可进群获取 266 | 267 | 使用 `#手办化` `#手办` 等指令即可将图片转换为手办化的图片 268 | 269 | 使用[AI漫画智能体](https://docs.bigmodel.cn/cn/guide/agents/aicaricature),收费标准为`按调用次数后付费,0.06 元/次` 270 | 271 | #### 安装依赖 272 | 273 | 使用前需要安装依赖 274 | 275 | ``` bash 276 | cd plugins/example && pnpm add axios 277 | ``` 278 | 279 | ### ICP查询 280 | 281 | 查询域名有无备案 282 | 283 | 发送 `#备案查询<域名>` 即可查询域名是否备案 284 | 285 | > 感谢[二叉树树](https://2x.nz/posts/auto-icp-query/)提供的接口 286 | 287 | ### 吊图 288 | 289 | 发一张吊图 290 | 291 | 默认数据源为[此仓库](https://gitee.com/bling_yshs/ys-dio-pic-repo) 292 | 293 | 可自定义本地图片列表,快捷上传群聊/私聊图片 294 | 可自定义每天使用次数,超出次数后的提示 295 | 使用 `avif`格式压缩并存储图片,占用空间更小 296 | 297 | 发送 `#吊图上传图片` 即可上传当前消息或引用消息的图片 298 | 管理员发送 `#吊图列表` 可查看本地的图片列表 299 | 管理员发送 `#吊图查看图片` 可查看具体的图片内容 300 | 301 | ### 抽头衔 302 | 303 | 随机从词库中组合一个头衔 304 | 305 | 默认词库为网易MC随机名称库,可换成自己的库 306 | 支持自定义群友每日可用次数 307 | 308 | 发送 `#抽头衔` 即可随机抽取 309 | 310 | ### new-api 311 | 312 | 主要对接new-api的聊天插件,由智谱GLM插件优化而来 313 | 314 | 直接艾特机器人或消息包含机器人昵称即可对话 315 | 可发送 `#重置对话` 清除聊天记录,管理员可使用 `#重置所有对话` 清除所有聊天记录 316 | 管理员可使用 `#切换预设` 切换预设 317 | 使用 `#预设列表` 查看预设列表 318 | 319 | 管理员可使用 `#切换模型` 可在机器人运行时临时切换模型 320 | 321 | 插件初次加载时自动下载云端配置文件 322 | 默认数据目录为`./data/plugins/智谱GLM/` 323 | 324 | > 处于兼容性考虑,此路径暂不更改 325 | 326 | 支持统计每日token用量,并于每日0点自动导出昨日统计到数据目录 327 | 328 |
329 | 默认格式 330 | 331 | 此处展示的文本限于篇幅,大幅精简且不会实时更新 332 | 如需查看原始文件请点击对应的链接查看 333 | 334 | [system_prompt.json](https://oss.xt-url.com/GPT-Config/system_prompt.json) 335 | 336 | ``` json 337 | { 338 | "预设名1": "预设内容1", 339 | "预设名2": "预设内容2" 340 | } 341 | ``` 342 | 343 | [model_list.json](https://oss.xt-url.com/GPT-Config/model_list.json) 344 | 345 | ``` json 346 | { 347 | "data": "2025年5月4日", 348 | "version": "1.2.2", 349 | "author": "xiaotian2333", 350 | "tips": [ 351 | "每日token统计信息可在插件数据目录token_log.csv查看" 352 | ], 353 | "bigmodel": { 354 | "name": "智谱", 355 | "url": "https://open.bigmodel.cn/api/paas/v4/chat/completions", 356 | "instructions": "模型价格为官网公示价格", 357 | "model_list": { 358 | "glm-4-flash-250414": { 359 | "instructions": "免费的语言模型", 360 | "Price": "免费", 361 | "free": true, 362 | "type": "语言模型" 363 | }, 364 | "glm-z1-air": { 365 | "instructions": "具备强大推理能力,适用于需要深度推理的任务", 366 | "Price": "0.5元|百万Tokens", 367 | "free": false, 368 | "type": "推理模型" 369 | }, 370 | "codegeex-4": { 371 | "instructions": "代码优化模型,专为程序员准备", 372 | "Price": "0.1元|百万Tokens", 373 | "free": false, 374 | "type": "代码模型" 375 | } 376 | } 377 | } 378 | } 379 | ``` 380 | 381 |
382 | 383 | #### 可配置的参数如下 384 | 385 | | 设置项 | 默认设置 | 说明 | 386 | | --- | --- | --- | 387 | | Authorization | 空 | `API Key` | 388 | | base_url | 默认为本地 | openai标准的api地址 | 389 | | model | 空 | 默认模型 | 390 | | max_log | 10 | 聊天记忆深度,建议范围5~20 | 391 | | think_print | false | 支持思考的模型是否输出思考过程 | 392 | | vision_enable | false | 是否开启多模态能力 | 393 | | vision_model | 空 | 多模态模型 | 394 | | system_prompt | 空 | 此配置为空时会自动获取云端默认系统提示词 | 395 | | list | 详情见源码 | 屏蔽词列表,用于过滤敏感词 | 396 | 397 | > 此处会隐藏一些一般不用调整的高级配置项,如需要调整请自行修改源码内相关配置 398 | 399 | --- 400 | 401 |

非常规插件

402 | 403 | 此类插件仅供参考使用 404 | 405 |
406 | 点击展开 407 | 408 | ### Demo类 409 | 410 | 这里都是一些插件开发示例,仅用于参考 411 | 412 | 存放于 Demo 文件夹下 413 | 进入文件夹后有详细说明 414 | 415 | ### 已归档的插件 416 | 417 | 这里存放的是一些已经不再维护的插件 418 | 仅供参考使用,无法保证可用性 419 | 420 | 存放于 Archive 文件夹下 421 | 进入文件夹后有详细说明 422 | 423 | ### 已放弃开发的插件 424 | 425 | 这里存放的是一些已经放弃开发的插件 426 | 仅供参考使用,无法保证可用性 427 | 428 | 存放于 Abandon 文件夹下 429 | 进入文件夹后有详细说明 430 | 431 |
432 | 433 | ## 开源协议 434 | 435 | 还没想好用什么开源协议,暂时不添加 436 | 437 | ## stars 历史 438 | 439 | [![stars 历史图表](https://api.star-history.com/svg?repos=xiaotian2333/yunzai-plugins-Single-file&type=Date)](https://star-history.com/#xiaotian2333/yunzai-plugins-Single-file&Date) -------------------------------------------------------------------------------- /吊图.js: -------------------------------------------------------------------------------- 1 | // 插件作者 xiaotian2333 2 | // 开源地址 https://github.com/xiaotian2333/yunzai-plugins-Single-file 3 | 4 | import sharp from 'sharp' // 图片转换依赖 5 | import axios from 'axios' // 网络请求依赖 6 | import fs from 'fs/promises' 7 | import crypto from 'crypto' 8 | import path from "path" 9 | import schedule from 'node-schedule'; 10 | 11 | // 配置项 12 | // 本地图库配置 13 | const local_img = true // 是否使用本地图库 14 | const local_img_path = 'data/plugins/吊图' // 本地图库路径 15 | 16 | // 网络图库配置 17 | const network_img = true // 是否使用网络图库 18 | 19 | // 冷却相关配置 20 | const cd = 3 // 一天只能触发5次 21 | const cd_tips = "你还没冲够?没也不许冲,憋着😏" // 冷却提示 22 | 23 | // 白名单用户配置,主人默认存在白名单中无需添加 24 | const white_list = [ 25 | 1719549416, 26 | 2859278670 27 | ] 28 | 29 | // 插件常量 30 | const plugin_name = "吊图" 31 | const user_agent = 'ys-dio-pic-repo (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 32 | const timeout = 10000 // 超时设置(10秒) 33 | 34 | 35 | // 开始初始化 36 | logger.debug(`[${plugin_name}]开始初始化`) 37 | 38 | const _path = process.cwd().replace(/\\/g, '/') 39 | 40 | // 构建索引数据 41 | let img_data = { 42 | version: "0.0.0", 43 | data: "未启用网络图库", 44 | source: "", 45 | img_url: "", 46 | img_list: [], 47 | local_img_list: [] 48 | } 49 | // 下载网络索引数据 50 | if (network_img) { 51 | try { 52 | const response = await axios.get("https://oss.xt-url.com/ys-dio-pic-repo/imglist.json", { 53 | timeout: timeout, 54 | headers: { 55 | 'User-Agent': user_agent 56 | } 57 | }) 58 | 59 | // axios会自动解析JSON,响应数据在response.data中 60 | img_data.version = response.data.version 61 | img_data.data = response.data.data 62 | img_data.source = response.data.source 63 | img_data.img_url = response.data.img_url 64 | img_data.img_list = response.data.img_list 65 | 66 | } catch (err) { 67 | logger.error(`[${plugin_name}]网络请求错误:${err.message}`) 68 | img_data = { 69 | version: "0.0.0", 70 | data: "网络请求失败,服务已降级", 71 | source: "", 72 | img_url: "", 73 | img_list: [], 74 | local_img_list: [] 75 | } 76 | } 77 | } 78 | 79 | // 初始化冷却数据 80 | let user_cd = {} 81 | 82 | // 导入本地图库 83 | if (local_img) { 84 | img_data.local_img_list = await ReadLocalImg(path.join(_path, local_img_path)) 85 | } 86 | 87 | 88 | // 加载白名单列表 89 | const whiteListSet = new Set(white_list) 90 | 91 | logger.debug(`[${plugin_name}]初始化完毕`) 92 | 93 | // 函数定义 94 | 95 | /** 96 | * 读取本地图库文件列表 97 | * @param {string} data_path - 本地图库路径 98 | * @returns {Promise} 本地图库文件列表 99 | */ 100 | async function ReadLocalImg(data_path) { 101 | try { 102 | // 使用 fs.promises.access() 检查目录是否存在(异步) 103 | await fs.access(data_path) 104 | 105 | // 目录存在,继续读取 106 | } catch (err) { 107 | // 如果 access 失败(ENOENT = 文件/目录不存在),则创建 108 | if (err.code === 'ENOENT') { 109 | await fs.mkdir(data_path, { recursive: true }) 110 | logger.mark(`[${plugin_name}] 本地图库目录创建成功: ${data_path}`) 111 | } else { 112 | // 其他错误(权限不足等) 113 | logger.error(`[${plugin_name}]访问目录失败:`, err.message) 114 | return [] 115 | } 116 | } 117 | // 目录存在,继续读取 118 | try { 119 | const entries = await fs.readdir(data_path, { withFileTypes: true }) 120 | let local_img_list = [] 121 | for (const entry of entries) { 122 | if (entry.isFile()) { 123 | local_img_list.push(entry.name) 124 | } 125 | } 126 | 127 | logger.debug(`[${plugin_name}]获取到的本地图片列表:`, local_img_list) 128 | return local_img_list 129 | } catch (err) { 130 | logger.error(`[${plugin_name}]读取目录失败:`, err.message) 131 | return [] 132 | } 133 | } 134 | 135 | /** 136 | * 计算Buffer的MD5值 137 | * @param {Buffer} buffer - 要计算MD5的Buffer 138 | * @returns {string} MD5哈希值(32位小写) 139 | */ 140 | function calculateMd5(buffer) { 141 | return crypto.createHash('md5') 142 | .update(buffer) 143 | .digest('hex') 144 | } 145 | 146 | /** 147 | * 获取优先级图片 148 | * 优先级:引用消息图片 > 当前消息图片 149 | * @param {object} e - 消息事件对象 150 | * @returns {Promise<{url: string|null, txt: string|null}>} 151 | */ 152 | async function getPriorityImage(e) { 153 | const result = { url: null, txt: null } 154 | 155 | // 1. 引用消息里的图片 156 | if (e.getReply || e.source) { 157 | try { 158 | let source 159 | if (e.getReply) { 160 | source = await e.getReply() 161 | } else if (e.source) { 162 | if (e.group?.getChatHistory) { 163 | source = (await e.group.getChatHistory(e.source.seq, 1)).pop() 164 | } else if (e.friend?.getChatHistory) { 165 | source = (await e.friend.getChatHistory(e.source.time, 1)).pop() 166 | } 167 | } 168 | 169 | if (source?.message?.length) { 170 | const imgs = source.message 171 | .filter(m => m.type === "image") 172 | .map(m => m.url) 173 | if (imgs.length > 0) { 174 | result.url = imgs[0] 175 | result.txt = "引用" 176 | return result 177 | } 178 | } 179 | } catch { } 180 | } 181 | 182 | // 2. 当前消息中的图片 183 | const imgSeg = e.message.find(m => m.type === "image") 184 | if (imgSeg?.url) { 185 | result.url = imgSeg.url 186 | result.txt = "消息" 187 | return result 188 | } 189 | 190 | return result 191 | } 192 | 193 | /** 194 | * 通过网络URL获取图片并转换为AVIF格式 195 | * @param {string} imageUrl - 图片的网络URL 196 | * @param {number} quality - AVIF压缩质量(0-100,默认60) 197 | * @returns {Promise} 转换后的AVIF格式Buffer 198 | */ 199 | async function convertUrlToAvif(imageUrl, quality = 70) { 200 | try { 201 | // 1. 发送GET请求获取图片Buffer 202 | const response = await axios.get(imageUrl, { 203 | responseType: 'arraybuffer', // 关键:指定响应类型为二进制数据 204 | timeout: timeout, 205 | headers: { 206 | 'User-Agent': user_agent 207 | } 208 | }) 209 | 210 | // 2. 检查响应是否为图片(简单验证Content-Type) 211 | const contentType = response.headers['content-type'] 212 | if (!contentType || !contentType.startsWith('image/')) { 213 | throw new Error(`URL返回的不是图片,Content-Type: ${contentType}`) 214 | } 215 | 216 | // 3. 将获取到的二进制数据转为Buffer并进行AVIF转换 217 | const imageBuffer = Buffer.from(response.data, 'binary') 218 | const avifBuffer = await sharp(imageBuffer) 219 | .avif({ 220 | quality, 221 | speed: 1 // 编码速度(1=最慢最优,10=最快) 222 | }) 223 | .toBuffer() 224 | 225 | logger.debug(`[${plugin_name}]URL图片转换AVIF成功: ${imageUrl}`) 226 | return avifBuffer 227 | 228 | } catch (err) { 229 | logger.error(`[${plugin_name}]URL图片转换失败: ${err.message} (URL: ${imageUrl})`) 230 | return false 231 | } 232 | } 233 | 234 | /** 235 | * 任意图片格式转jpg格式 236 | * @param {Buffer} imageBuffer - 原始图片的Buffer数据 237 | * @returns {Promise} 转换后的JPG格式Buffer 238 | * @throws {Error} 如果转换失败一律返回false 239 | */ 240 | async function convertToJpg(imageBuffer) { 241 | try { 242 | // 转换为JPG格式,设置最高质量(不压缩) 243 | const jpgBuffer = await sharp(imageBuffer) 244 | .jpeg({ 245 | quality: 100, // 最高质量(100=不压缩) 246 | progressive: false, // 禁用渐进式JPG(避免额外处理) 247 | chromaSubsampling: '4:4:4' // 保留完整色彩信息(默认是4:2:0,会轻微压缩色彩) 248 | }) 249 | .toBuffer() 250 | return jpgBuffer 251 | } catch (err) { 252 | logger.error(`[${plugin_name}]图片转换失败: ${err.message}`) 253 | return false 254 | } 255 | } 256 | 257 | /** 258 | * 判断是否为白名单用户 259 | * @param {number} userId 260 | * @returns {bool} 261 | */ 262 | function isInWhiteList(userId) { 263 | return whiteListSet.has(userId) 264 | } 265 | 266 | // 主逻辑 267 | export class example extends plugin { 268 | constructor() { 269 | super({ 270 | name: '吊图', 271 | event: 'message', 272 | priority: 5000, 273 | rule: [ 274 | { 275 | reg: /^#?(吊|叼|屌|铞)图$/, 276 | fnc: 'start' 277 | }, 278 | { 279 | reg: /^#?(吊|叼|屌|铞)图(上传|添加)图片$/, 280 | fnc: 'upload' 281 | }, 282 | { 283 | reg: /^#?(吊|叼|屌|铞)图(图片)?列表$/, 284 | fnc: 'list' 285 | }, 286 | { 287 | reg: /^#?(吊|叼|屌|铞)图查看图片/, 288 | fnc: 'view' 289 | }, 290 | { 291 | reg: /^#?(吊|叼|屌|铞)图状态$/, 292 | fnc: 'status' 293 | }, 294 | { 295 | reg: /^#?(吊|叼|屌|铞)图(图片|上传|图片上传)?统计$/, 296 | fnc: 'statistics' 297 | } 298 | ] 299 | }) 300 | } 301 | 302 | async start(e) { 303 | // 字段不存在则默认0,存在则保留原值 304 | user_cd[e.user_id] = user_cd[e.user_id] ?? 0 305 | 306 | // 冲过头了 307 | if (user_cd[e.user_id] >= cd) { 308 | e.reply(cd_tips) 309 | return true 310 | } 311 | 312 | // 随机一个图片名称 313 | let imgname = [] 314 | // 网络图片 315 | if (network_img && img_data.img_list.length > 0) { 316 | // 不要拼接/,img_data.img_url已经包含了/ 317 | imgname.push(`${img_data.img_url}${img_data.img_list[Math.floor(Math.random() * img_data.img_list.length)]}`) 318 | } 319 | // 本地图片 320 | if (local_img && img_data.local_img_list.length > 0) { 321 | const filePath = path.join(_path, local_img_path, img_data.local_img_list[Math.floor(Math.random() * img_data.local_img_list.length)]) 322 | // 拉格朗日不支持直接发送avif图,需要先转一下,如使用icqq可直接发送avif图 323 | let imgBuffer = await fs.readFile(filePath) 324 | imgBuffer = await convertToJpg(imgBuffer) 325 | 326 | imgname.push(imgBuffer) 327 | } 328 | // 随机一个图片类型 329 | const img = imgname[Math.floor(Math.random() * imgname.length)] 330 | if (!img) { 331 | e.reply('没有可用图片') 332 | return true 333 | } 334 | 335 | e.reply(segment.image(img)) 336 | user_cd[e.user_id] = user_cd[e.user_id] + 1 // 计数加1 337 | return true 338 | } 339 | 340 | async upload(e) { 341 | if (!e.isMaster && !isInWhiteList(e.user_id)) { 342 | e.reply('只有管理员才能上传图片') 343 | return true 344 | } 345 | // 获取图片链接 346 | const img = await getPriorityImage(e) 347 | if (!img.url) { 348 | e.reply('请引用或附带图片') 349 | return true 350 | } 351 | // 压缩图片 352 | const imgBuffer = await convertUrlToAvif(img.url) 353 | if (!imgBuffer) { 354 | e.reply('图片转换失败') 355 | return true 356 | } 357 | // 存储图片 358 | const imgMd5 = calculateMd5(imgBuffer) 359 | const imgPath = path.join(_path, local_img_path, `${e.user_id}-${imgMd5}.avif`) 360 | await fs.writeFile(imgPath, imgBuffer) 361 | 362 | // 刷新本地图片列表 363 | img_data.local_img_list = await ReadLocalImg(path.join(_path, local_img_path)) 364 | 365 | e.reply(`图片上传成功`) 366 | return true 367 | } 368 | 369 | async list(e) { 370 | if (!e.isMaster && !isInWhiteList(e.user_id)) { 371 | e.reply('只有管理员才能查看列表') 372 | return true 373 | } 374 | let msg = [ 375 | "========吊图本地列表========\n", 376 | ...img_data.local_img_list 377 | ] 378 | msg = msg.join('\n') 379 | e.reply(msg) 380 | return true 381 | } 382 | 383 | async view(e) { 384 | if (!e.isMaster && !isInWhiteList(e.user_id)) { 385 | e.reply('只有管理员才能查看图片') 386 | return true 387 | } 388 | let msg = e.msg 389 | msg = msg.replace(/^#?(吊|叼|屌|铞)图查看图片/, '') 390 | if (!img_data.local_img_list.includes(msg)) { 391 | e.reply("不存在此图片,请检查是否为图片的全称") 392 | return true 393 | } 394 | 395 | const filePath = path.join(_path, local_img_path, msg) 396 | // 拉格朗日不支持直接发送avif图,需要先转一下,如使用icqq可直接发送avif图 397 | let imgBuffer = await fs.readFile(filePath) 398 | imgBuffer = await convertToJpg(imgBuffer) 399 | e.reply(segment.image(imgBuffer)) 400 | return true 401 | } 402 | 403 | async status(e) { 404 | let version = img_data.version 405 | if (version === "0.0.0") { 406 | version = "未启用云图库" 407 | } 408 | 409 | const msg = [ 410 | `------吊图状态------`, 411 | `本地图片数量: ${img_data.local_img_list.length}`, 412 | `网络图片数量: ${img_data.img_list.length}`, 413 | `云图库版本: ${version}` 414 | ] 415 | e.reply(msg.join('\n')) 416 | return true 417 | } 418 | 419 | async statistics(e) { 420 | // 统计前缀出现次数 421 | const prefixCounts = {}; 422 | for (const str of img_data.local_img_list) { 423 | const prefix = str.split('-')[0]; 424 | prefixCounts[prefix] = (prefixCounts[prefix] || 0) + 1; 425 | } 426 | 427 | // 转换为数组并按数量排序(从高到低) 428 | const sortedEntries = Object.entries(prefixCounts).sort((a, b) => b[1] - a[1]); 429 | 430 | // 将排序后的结果存入列表 431 | const resultList = sortedEntries.map(([prefix, count]) => `用户[${prefix}]上传${count}张`); 432 | 433 | e.reply([ 434 | "图片上传数量统计\n", 435 | resultList.join("\n") 436 | ]) 437 | return true 438 | } 439 | } 440 | 441 | // 每日重置使用限制 442 | schedule.scheduleJob('0 0 0 * * *', async () => { 443 | user_cd = {} 444 | logger.mark(`[吊图] 冷却已重置`) 445 | }); -------------------------------------------------------------------------------- /new-api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 作者:xiaotian2333 3 | * 开源地址:https://github.com/xiaotian2333/yunzai-plugins-Single-file 4 | * 此版本为针对new-api专门优化的版本,不支持开箱即用,如只使用智谱推荐使用智谱GLM.js 5 | * 两个版本的对话记忆、token统计互通。但智谱GLM.js将暂停更新,仅修复bug 6 | */ 7 | 8 | import fetch from "node-fetch" 9 | import fs from "fs" 10 | import schedule from 'node-schedule' 11 | 12 | const Authorization = "" // API Key 13 | const base_url = "http://localhost:8000/v1/chat/completions" // API接口,openai兼容 14 | let model = "" // 模型名称 15 | const max_log = 10 // 最大历史记录数 16 | const plugin_name = "new-api" // 插件名称 17 | const think_print = false // 支持思考的模型是否输出思考过程 18 | const user_agent_disguise = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" // 伪装ua 19 | 20 | // 多模态相关配置 21 | let vision_enable = false // 是否开启多模态 22 | const version_Authorization = "" // 多模态API Key 23 | const version_url = "http://localhost:8000/v1/chat/completions" // 多模态API接口,openai兼容 24 | const vision_model = "" // 多模态模型名称 25 | const version_do_sample = true // 是否启用采样策略来生成文本。默认值为 true。对于需要一致性和可重复性的任务(如代码生成、翻译),建议设置为 false。 26 | const version_top_p = 0.6 // 不懂勿动 27 | const version_temperature = 0.8 // 不懂勿动 28 | 29 | 30 | // 系统提示词,引导模型进行对话 31 | // 请通过配置文件进行修改,不要直接修改代码 32 | // 配置文件路径 33 | const plugin_data_path = `./data/plugins/智谱GLM/` // 插件数据路径,由于历史兼容考虑,不建议修改 34 | const system_prompt_file = `${plugin_data_path}system_prompt.json` 35 | const model_list_file = `${plugin_data_path}model_list.json` 36 | let system_prompt 37 | 38 | const list = [ 39 | '过滤词列表-156411gfchc', 40 | '模糊匹配-15615156htdy1', 41 | ] 42 | 43 | // 函数:读取并解析JSON文件 44 | // 参数:文件路径 45 | // 返回:解析后的JSON对象 46 | // 抛出错误:文件不存在、文件为空、JSON解析错误 47 | function readJsonFile(path) { 48 | if (!fs.existsSync(path)) { 49 | throw new Error(`配置文件不存在`) 50 | } 51 | 52 | const stats = fs.statSync(path) 53 | if (stats.size === 0) { 54 | throw new Error(`配置文件为空`) 55 | } 56 | 57 | const data = fs.readFileSync(path, 'utf8') 58 | return JSON.parse(data) 59 | } 60 | 61 | 62 | // 下载系统提示词 63 | async function Download_file(url, path) { 64 | try { 65 | // 确保目录存在 66 | fs.mkdirSync(plugin_data_path, { recursive: true }) 67 | // 发送HTTP请求下载文件 68 | logger.debug(`[${plugin_name}]正在下载来自 ${url} 的文件`) 69 | const response = await fetch(url, { 70 | headers: { 71 | 'User-Agent': 'new-api (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 72 | } 73 | }) 74 | // 检查响应状态 75 | if (!response.ok) { 76 | throw new Error(`[${plugin_name}]网络请求错误:\n${response.status}`) 77 | } 78 | // 解析响应数据为JSON 79 | const data = await response.json() 80 | logger.debug(`[${plugin_name}]文件下载完备:\n${data}`) 81 | // 将数据保存到文件 82 | await fs.promises.writeFile(path, JSON.stringify(data)) 83 | return true 84 | } catch (error) { 85 | // 捕获并打印错误信息 86 | //logger.error(`[${plugin_name}]配置文件下载失败:\n`, error) 87 | return false 88 | } 89 | } 90 | 91 | // 重新读取配置文件 92 | async function read_config() { 93 | try { 94 | system_prompt_list = await readJsonFile(system_prompt_file) 95 | model_list = await readJsonFile(model_list_file) 96 | return false 97 | } catch (err) { 98 | logger.error(`[${plugin_name}]读取或解析JSON文件时出错:`, err.message) 99 | return `读取或解析JSON文件时出错: \n${err.message}` 100 | } 101 | } 102 | 103 | // 动态变量实时替换 104 | function replace_var(str, nickname) { 105 | const date = new Date(Date.now()) 106 | 107 | // 年月日 108 | const year = date.getFullYear() 109 | const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始,需要加1,并确保是两位数 110 | const day = String(date.getDate()).padStart(2, '0') 111 | // 时分秒 112 | const hour = String(date.getHours()).padStart(2, '0') 113 | const minute = String(date.getMinutes()).padStart(2, '0') 114 | const second = String(date.getSeconds()).padStart(2, '0') 115 | 116 | str = str.replace(/\$\{Bot\.nickname\}/, `${nickname}`) 117 | str = str.replace(/\$\{now_date\}/, `${year}年${month}月${day}日`) 118 | str = str.replace(/\$\{now_time\}/, `${hour}时${minute}分${second}秒`) 119 | return str 120 | } 121 | 122 | /** 时间戳转可视化日期函数 123 | * @param timestamp 毫秒级时间戳 124 | * 返回格式参考:2023,10,05 125 | */ 126 | function formatTimestamp(timestamp = Date.now()) { 127 | const date = new Date(timestamp) 128 | 129 | // 获取年、月、日 130 | const year = date.getFullYear() 131 | const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始,需要加1,并确保是两位数 132 | const day = String(date.getDate()).padStart(2, '0') 133 | 134 | // 格式化日期字符串 135 | const formattedDate = `${year},${month},${day}` 136 | 137 | return formattedDate 138 | } 139 | 140 | /** 休眠函数 141 | * @time 毫秒 142 | */ 143 | function sleep(time) { 144 | return new Promise((resolve) => setTimeout(resolve, time)) 145 | } 146 | 147 | /** 148 | * 获取用户引用的消息 149 | * @param {Object} getReply - 引用消息对象 150 | * @return {list} - 返回引用的消息内容 151 | */ 152 | async function get_quote_message(getReply) { 153 | if (typeof getReply === 'function') { 154 | try { 155 | const reply = await getReply() 156 | if (reply.message) { 157 | return reply.message 158 | } 159 | } 160 | catch (err) { 161 | logger.warn(`[${plugin_name}]引用消息获取失败,错误信息:${err}`) 162 | return false 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * 将消息列表拆分并分类 169 | * @param {list} message 170 | */ 171 | function split_message(message) { 172 | // 多模态相关信息初始化 173 | let text_list = [] 174 | let image_list = [] 175 | let isVision = false // 是否激活多模态处理 176 | 177 | // 拆分消息 178 | for (let i = 0; i < message.length; i++) { 179 | let element = message[i] 180 | 181 | // 分类 182 | if (element.type === "text") { 183 | // 文字 184 | text_list.push(element.text) 185 | 186 | } else if (element.type === "image") { 187 | // 图片 188 | image_list.push(element.url) 189 | isVision = true 190 | 191 | } 192 | } 193 | return { 194 | text_list, 195 | image_list, 196 | isVision 197 | } 198 | } 199 | 200 | /** 201 | * 批量删除Redis中匹配指定模式的键(完全参考你的实测代码实现) 202 | * @param {string} pattern - 键匹配模式,如GLM:chat_log:123456:* 203 | * @param {number} [count=5000] - 每次SCAN遍历的键数量(和你的代码保持一致,设为5000) 204 | * @returns {number} 总共删除的键数量 205 | */ 206 | async function batchDeleteRedisKeys(pattern, count = 5000) { 207 | let total = 0; // 记录总共删除的键数量 208 | let cursor = 0; // SCAN的游标,0表示开始迭代 209 | 210 | do { 211 | // 完全参考你的代码:scan调用方式为 cursor + 配置对象 { MATCH, COUNT } 212 | const res = await redis.scan(cursor, { 213 | MATCH: pattern, 214 | COUNT: count 215 | }); 216 | // 更新游标(res.cursor是返回的新游标,和你的代码一致) 217 | cursor = res.cursor; 218 | // 有匹配的键时,批量删除(直接传res.keys数组,和你的代码一致) 219 | if (res.keys.length > 0) { 220 | const delCount = await redis.del(res.keys); 221 | total += delCount; 222 | // logger.info(`[${plugin_name}][批量删除] 本次删除${delCount}个键,匹配模式:${pattern},当前游标:${cursor}`); 223 | } 224 | } while (cursor !== 0); // 游标为0时遍历结束 225 | 226 | return total; 227 | } 228 | 229 | export class bigmodel extends plugin { 230 | constructor() { 231 | super({ 232 | name: plugin_name, 233 | event: 'message', 234 | priority: 9000, 235 | rule: [ 236 | { 237 | reg: /^#(智谱|new-api|gpt)?(新开|重启|重置|清空|删除|清楚|清除)(全部|所有|全局|一切|本群|此群|当前群)?(聊天|对话|记录|记忆|历史)$/, 238 | fnc: 'clear', 239 | }, 240 | { 241 | reg: /^#(智谱|new-api|gpt)?(角色|身份|人物|设定|提示词|预设|人格)列表$/, 242 | fnc: 'role_list', 243 | }, 244 | { 245 | reg: /^#(智谱|new-api|gpt)?(切换|更改|换)(角色|身份|人物|设定|提示词|预设|人格)/, 246 | fnc: 'role', 247 | }, 248 | { 249 | reg: /^#(智谱|new-api|gpt)?(更新|下载|克隆)(角色|身份|人物|设定|提示词|预设|人格)?(文件|配置|配置文件|数据)?/, 250 | fnc: 'pull_1', 251 | }, 252 | { 253 | reg: /^#(?:(智谱|new-api|gpt)(状态|info)(信息|数据)?|(状态|info)(信息|数据)?)$/, 254 | fnc: 'GLM_info', 255 | }, 256 | { 257 | reg: /^#(智谱|new-api|gpt)?(更换|切换|换|改|设置)(模型|model)/, 258 | fnc: 'model_set', 259 | }, 260 | { 261 | reg: '', 262 | fnc: 'chat', 263 | log: false 264 | } 265 | ] 266 | }) 267 | } 268 | 269 | async chat(e) { 270 | // if (!e.isMaster) { return false } // 只允许主人使用 271 | 272 | // 先过滤非文本信息 273 | if (!e.msg) { return false } 274 | 275 | // 删除不需要的部分 276 | let msg = e.msg 277 | msg = msg.replace(' ', '') 278 | 279 | // 只有被艾特、私聊、命中机器人名字的消息才会被处理 280 | if (!(e.isPrivate || e.atme || e.atBot || msg.includes(this.e.bot.nickname))) { 281 | return false 282 | } 283 | 284 | // 输入过滤 285 | if (list.some(item => msg.includes(item))) { 286 | // 检测到需要过滤的词后的处理逻辑 287 | logger.mark(`[${plugin_name}]检测到敏感词,已过滤`) 288 | e.reply("输入包含敏感词,已拦截") 289 | return true 290 | } 291 | 292 | // 再过滤空信息 293 | if (!msg) { 294 | return false 295 | } 296 | // 消息长度限制,正常聊天200字足以,字数开放越多越容易被洗脑 297 | if (msg.length > 200 && !e.isMaster) { 298 | e.reply('输入文本长度过长') 299 | return true 300 | } 301 | 302 | logger.mark(`[${plugin_name}]${e.group_id}_${e.user_id} 发送了消息:${msg}`) 303 | 304 | // 多模态相关信息初始化 305 | let text_list = [] // 历史文本列表 306 | let image_list = [] // 图片列表,存储url 307 | let isVision = false // 是否激活多模态处理 308 | let backup_msg = '' // 备份消息,用于多模态处理时,防止单模特模型无法处理多模态内容 309 | 310 | 311 | // 引用历史消息 312 | let quote_message = await get_quote_message(e?.getReply) 313 | 314 | // 存在历史消息 315 | if (quote_message) { 316 | quote_message = split_message(quote_message) 317 | 318 | 319 | text_list.push(...quote_message.text_list) 320 | image_list.push(...quote_message.image_list) 321 | isVision = quote_message.isVision // 如引用的消息包含图片等多模态内容则开启多模态处理 322 | } 323 | 324 | // 判断用户消息是否有图片 325 | let user_message = split_message(e.message) 326 | if (user_message.isVision) { 327 | image_list.push(...user_message.image_list) 328 | isVision = isVision || user_message.isVision // 如用户消息包含图片等多模态内容则开启多模态处理,如本身已激活则保持激活 329 | } 330 | 331 | // 判断引用的消息是否有图片 332 | if (isVision && vision_enable) { 333 | // 多模态处理 334 | let version_msg = [] 335 | for (let i = 0; i < image_list.length; i++) { 336 | version_msg.push({ 337 | "type": "image_url", 338 | "image_url": { 339 | "url": image_list[i] 340 | } 341 | }) 342 | } 343 | 344 | // 添加引导词 345 | if (text_list.length > 0) { 346 | version_msg.push({ 347 | "type": "text", 348 | "text": `用户引用了一些图片和历史消息,这些图片和历史消息大概率会帮助回答用户问题,但也有小概率不关联。历史消息内容如下:\n${text_list.join('\n')}\n以下是用户的输入:\n${msg}` 349 | }) 350 | } else { 351 | version_msg.push({ 352 | "type": "text", 353 | "text": `用户引用了一些图片,图片大概率会帮助回答用户问题,但也有小概率不关联,以下是用户的输入:\n${msg}` 354 | }) 355 | } 356 | // 创建文本模型兼容消息,作为放入redis的历史消息存储 357 | backup_msg = `用户引用了一些图片,已由其他模型处理并回答,如用户询问请根据上下文回答用户问题,以下是用户引用图片时的输入:\n${msg}` 358 | 359 | msg = version_msg 360 | } else if (text_list > 0) { 361 | // 没有激活多模态处理,但引用了消息 362 | msg = `用户引用了这些历史消息:${text_list.join('\n')}\n以上消息可能会帮助回答用户问题,但也有可能不关联,以下是用户的输入:\n${msg}` 363 | } 364 | 365 | let msg_log = await redis.type(`GLM:chat_log:${e.group_id}:${e.user_id}`) 366 | if (msg_log == 'none') { 367 | // 如果msg_log不存在,初始化msg_log 368 | msg_log = [{ 369 | "role": "system", 370 | "content": system_prompt 371 | }] 372 | 373 | } else { 374 | // 如果msg_log存在,获取msg_log 375 | msg_log = await redis.get(`GLM:chat_log:${e.group_id}:${e.user_id}`) 376 | msg_log = JSON.parse(msg_log) 377 | } 378 | // 添加聊天信息 379 | const backup_msg_log = [...msg_log] 380 | 381 | msg_log.push({ 382 | "role": "user", 383 | "content": msg 384 | }) 385 | 386 | // 限制聊天记录长度 387 | if (msg_log.length > max_log) { 388 | // 删除除system_prompt之外的最旧记录 389 | msg_log.splice(1, 1) 390 | } 391 | 392 | // 实时修改system_prompt 393 | msg_log[0].content = replace_var(system_prompt, this.e.bot.nickname) 394 | 395 | let data = {} 396 | let llm_url = base_url 397 | let api_key = Authorization 398 | // 构建请求体 399 | if (isVision && vision_enable) { 400 | // 多模态处理 401 | logger.debug(`[${plugin_name}]进入多模态处理,构建多模态请求`) 402 | llm_url = version_url 403 | api_key = version_Authorization 404 | data = { 405 | model: vision_model, 406 | messages: msg_log, 407 | do_sample: version_do_sample, 408 | temperature: version_temperature, 409 | top_p: version_top_p, 410 | stream: false, 411 | user_id: `${e.group_id}_${e.user_id}`, 412 | } 413 | } else { 414 | // 单模态处理 415 | logger.debug(`[${plugin_name}]未激活多模态处理,构建文本请求`) 416 | data = { 417 | model: model, 418 | messages: msg_log, 419 | do_sample: true, 420 | temperature: 0.8, // 温度,0.8是默认值,可以调整 421 | stream: false, 422 | user_id: `${e.group_id}_${e.user_id}`, 423 | } 424 | } 425 | 426 | // 网络请求 427 | let Reply = await fetch(llm_url, { 428 | method: 'POST', 429 | headers: { 430 | "Content-Type": "application/json", 431 | "Authorization": `Bearer ${api_key}`, 432 | "User-Agent": user_agent_disguise 433 | }, 434 | body: JSON.stringify(data) 435 | }) 436 | Reply = await Reply.json() 437 | 438 | // 错误处理 439 | if (Reply.error) { 440 | e.reply(`[${plugin_name}]发生错误,响应码[${Reply.code || Reply.error.code || "无"}]\n来自API的错误信息:\n${Reply?.error?.message || Reply?.msg || "没有来自API的错误信息"}`) 441 | return false 442 | } 443 | // 检查choices是否存在 444 | if (!Reply?.choices || !Reply?.choices[0]) { 445 | e.reply(`[${plugin_name}]API返回格式错误:缺少回复数据`) 446 | logger.error(`[${plugin_name}]发生错误,缺少回复数据\n来自API的返回数据:\n${JSON.stringify(Reply)}`) 447 | return false 448 | } 449 | // 获取回复内容 450 | let content = Reply.choices[0].message.content 451 | 452 | // 输出过滤 453 | if (list.some(item => content.includes(item))) { 454 | // 检测到需要过滤的词后的处理逻辑 455 | logger.mark(`[${plugin_name}]检测到敏感词,已过滤`) 456 | e.reply("输出包含敏感词,已拦截") 457 | return true 458 | } 459 | 460 | // 过滤思考过程 461 | let think_text = '' 462 | // 标准思考处理 463 | if (Reply.choices[0].message?.reasoning_content) { 464 | logger.debug(`[${plugin_name}]检测到有思考过程`) 465 | think_text = Reply.choices[0].message?.reasoning_content 466 | } 467 | // 兼容早期思考输出 468 | else if (content.startsWith('\n') || content.startsWith('')) { 469 | 470 | logger.debug(`[${plugin_name}]检测到有思考过程`) 471 | // 处理think标签 472 | think_text = content.split('') 473 | think_text[0] = think_text[0].replace('', '').trim() 474 | // 过滤思考过程 475 | content = think_text[1].trim() 476 | think_text = think_text[0] 477 | } 478 | 479 | // 如果用户开启思考发送且think_text不为空,则发送思考过程 480 | if (think_print && think_text) { 481 | logger.debug(`[${plugin_name}]用户开启了发送思考过程`) 482 | // 发送思考过程 483 | let msgList = [{ 484 | user_id: 2854200865, 485 | nickname: '思考过程', 486 | message: think_text 487 | }] 488 | await e.reply(await Bot.makeForwardMsg(msgList)) 489 | } 490 | 491 | // 过滤智普多模态回复中的标记 492 | content = content.replace(/<\|begin_of_box\|>/, '') 493 | content = content.replace(/<\|end_of_box\|>/, '') 494 | 495 | // 过滤首尾空格 496 | content = content.trim() 497 | 498 | // 为兼容文本模型,多模态处理的消息存储为普通格式 499 | if (isVision && vision_enable) { 500 | // 多模态处理 501 | msg_log = backup_msg_log 502 | 503 | // 添加用户消息 504 | msg_log.push({ 505 | "role": "user", 506 | "content": backup_msg 507 | }) 508 | 509 | // 限制聊天记录长度 510 | if (msg_log.length > max_log) { 511 | // 删除除system_prompt之外的最旧记录 512 | msg_log.splice(1, 1) 513 | } 514 | 515 | // 添加模型的回答 516 | msg_log.push({ 517 | "role": "assistant", 518 | "content": content 519 | }) 520 | } else { 521 | // 纯文本处理 522 | msg_log.push({ 523 | "role": "assistant", 524 | "content": content 525 | }) 526 | } 527 | // 保存对话记录 528 | await redis.set(`GLM:chat_log:${e.group_id}:${e.user_id}`, JSON.stringify(msg_log), { EX: 60 * 60 * 24 * 7 }) // 保存到redis,过期时间为7天 529 | 530 | // 长文本分多句发送 531 | content = content.split('\n') 532 | 533 | for (const line of content) { 534 | e.reply(line) 535 | await sleep(Math.floor(Math.random() * (5000 - 1000 + 1)) + 1000) 536 | } 537 | 538 | // 统计token用量 539 | // 今日用量 540 | let token_today = parseInt(await redis.get(`GLM:token:Today`), 10) 541 | if (token_today == 'none') { 542 | token_today = 0 543 | } 544 | await redis.set(`GLM:token:Today`, token_today + Reply.usage.total_tokens) 545 | // 总用量 546 | let token_history = parseInt(await redis.get(`GLM:token:Statistics`), 10) 547 | if (token_history == 'none') { 548 | token_history = 0 549 | } 550 | await redis.set(`GLM:token:Statistics`, token_history + Reply.usage.total_tokens) 551 | 552 | return true 553 | } 554 | 555 | async clear(e) { 556 | const ALL_Group = /全部|所有|全局|一切/; 557 | const Current_Group = /本群|此群|当前群/; 558 | // 解构关键参数 559 | const { group_id, user_id, isMaster, msg, reply } = e; 560 | 561 | // 主人发送关键词,批量删除所有对话记录 562 | if (isMaster && ALL_Group.test(msg)) { 563 | const pattern = `GLM:chat_log:*`; 564 | // 调用批量删除函数 565 | const deleteCount = await batchDeleteRedisKeys(pattern); 566 | logger.info(`[${plugin_name}][清理记录] 主人已清除所有对话记录,匹配模式:${pattern},共删除${deleteCount}条`); 567 | await reply('已清除所有对话记录'); 568 | return true; 569 | } 570 | 571 | // 主人发送关键词,批量删除群内对话记录 572 | if (isMaster && Current_Group.test(msg)) { 573 | const pattern = `GLM:chat_log:${group_id}:*`; 574 | // 调用批量删除函数 575 | const deleteCount = await batchDeleteRedisKeys(pattern); 576 | logger.info(`[${plugin_name}][清理记录] 主人已清除群${group_id}的所有对话记录,匹配模式:${pattern},共删除${deleteCount}条`); 577 | await reply('已清除本群对话记录'); 578 | return true; 579 | } 580 | 581 | // 非主人/未发送关键词:删除单个用户的对话记录 582 | const singleKey = `GLM:chat_log:${group_id}:${user_id}`; 583 | const deleteResult = await redis.del(singleKey); 584 | logger.info(`[${plugin_name}][清理记录] 用户${user_id}已清除群${group_id}的个人对话记录,键:${singleKey},删除结果:${deleteResult}`); 585 | await reply('对话记录已清除'); 586 | return true; 587 | } 588 | 589 | async role_list(e) { 590 | // 刷新配置文件 591 | const err = await read_config() 592 | if (err) { 593 | e.reply(err) 594 | return false 595 | } 596 | 597 | let name_list = ["可切换的角色身份\n"] 598 | 599 | Object.keys(system_prompt_list).forEach(key => { 600 | //console.log(`name: ${key}, key: ${system_prompt_list[key]}`) 601 | // 如果key为system_prompt则跳过本次循环 602 | if (key == 'system_prompt') { 603 | return 604 | } 605 | name_list.push(`${key}\n`) 606 | }) 607 | 608 | e.reply(name_list) 609 | return true 610 | } 611 | 612 | async role(e) { 613 | // 只允许主人使用 614 | if (!e.isMaster) { 615 | e.reply('只有主人才能设置角色') 616 | return false 617 | } 618 | 619 | const name = e.msg.replace(/^#(智谱|new-api|gpt)?(切换|更改|换)(角色|身份|人物|设定|提示词|预设|人格)/, '') 620 | 621 | if (!name) { 622 | e.reply('请输入要切换的预设\n\n发送 #智谱预设列表 查看可切换的预设') 623 | return false 624 | } 625 | 626 | // 刷新配置文件 627 | const err = await read_config() 628 | if (err) { 629 | e.reply(err) 630 | return false 631 | } 632 | 633 | // 标记是否找到匹配的角色 634 | let type = false 635 | 636 | // 遍历人物设定 637 | Object.keys(system_prompt_list).forEach(key => { 638 | //console.log(`name: ${key}, key: ${system_prompt_list[key]}`) 639 | if (key == name) { 640 | system_prompt = system_prompt_list.system_prompt + system_prompt_list[key] 641 | type = true 642 | } 643 | }) 644 | 645 | // 判断是否找到匹配的角色设定 646 | if (type) { 647 | e.reply(`人物设定已切换为${name}`) 648 | } else { 649 | e.reply(`人物设定${name}不存在,想要创建一个临时预设吗\n\n发送 #创建临时预设 进行创建`) 650 | this.setContext('set_system_prompt_1') 651 | return true 652 | } 653 | 654 | return true 655 | } 656 | 657 | async pull_1(e) { 658 | // 只允许主人使用 659 | if (!e.isMaster) { 660 | e.reply('只有主人才覆盖预设文件') 661 | return false 662 | } 663 | e.reply(`警告:此操作不可撤销!!!\n\n确定要进行下载吗,这将会覆盖当前的配置文件\n\n确定覆盖发送 #确认覆盖预设文件 进行下一步操作`) 664 | this.setContext('pull_2') 665 | return true 666 | } 667 | 668 | async pull_2(e) { 669 | e = this.e 670 | this.finish('pull_2') 671 | // 只允许主人使用 672 | if (!e.isMaster) { 673 | e.reply('只有主人才覆盖预设文件') 674 | return false 675 | } 676 | if (e.msg == '#确认覆盖预设文件') { 677 | const type = await Download_file(model_list.system_prompt_url, system_prompt_file) 678 | if (type) { 679 | e.reply(`配置文件覆盖成功`) 680 | return true 681 | } else { 682 | e.reply(`配置文件覆盖失败`) 683 | return true 684 | } 685 | } else { 686 | e.reply(`未发送确认指令,取消覆盖预设文件`) 687 | return true 688 | } 689 | } 690 | 691 | async set_system_prompt_1(e) { 692 | e = this.e 693 | this.finish('set_system_prompt_1') 694 | // 只允许主人使用 695 | if (!e.isMaster) { 696 | e.reply('只有主人才创建临时预设') 697 | return false 698 | } 699 | // 删除不需要的部分 700 | let msg = e.msg 701 | msg = msg.replace(' ', '') 702 | 703 | if (msg == '#创建临时预设') { 704 | e.reply(`请发送要设置的提示词`) 705 | this.setContext('set_system_prompt_2') 706 | return true 707 | } else { 708 | return true 709 | 710 | } 711 | } 712 | 713 | async set_system_prompt_2(e) { 714 | e = this.e 715 | // 只允许主人使用 716 | if (!e.isMaster) { 717 | e.reply('只有主人才创建临时预设') 718 | return false 719 | } 720 | // 删除不需要的部分 721 | let msg = e.msg 722 | msg = msg.replace(' ', '') 723 | 724 | if (!msg) { 725 | e.reply(`请发送要设置的提示词`) 726 | return true 727 | } else { 728 | this.finish('set_system_prompt_2') 729 | system_prompt = msg 730 | e.reply(`已创建并应用临时预设:\n${system_prompt}`) 731 | return true 732 | } 733 | } 734 | 735 | async GLM_info(e) { 736 | // 取token用量 737 | const token_today = parseInt(await redis.get(`GLM:token:Today`), 10) 738 | const token_history = parseInt(await redis.get(`GLM:token:Statistics`), 10) 739 | 740 | // 取随机提示 741 | const tips = model_list.tips[Math.floor(Math.random() * model_list.tips.length)] 742 | 743 | // 构建信息 744 | const msg = [ 745 | `=====${plugin_name}当前状态=====\n`, 746 | `今日token消耗:${token_today}\n`, 747 | `累计token消耗:${token_history}\n`, 748 | `当前模型:${model}\n`, 749 | `数据版本:${model_list.version}\n`, 750 | `思考输出:${think_print ? '开启' : '关闭'}\n`, 751 | `多模态能力:${vision_enable ? '开启' : '关闭'}\n`, 752 | `=====================\n`, 753 | `Tip:${tips}` 754 | 755 | ] 756 | 757 | // 发送信息 758 | e.reply(msg) 759 | return true 760 | } 761 | 762 | async model_set(e) { 763 | // 只允许主人使用 764 | if (!e.isMaster) { 765 | e.reply('只有主人才能更改模型') 766 | return false 767 | } 768 | 769 | // 获取命令参数 770 | const model_name = e.msg.replace(/^#(智谱|new-api|gpt)?(更换|切换|换|改|设置)(模型|model)\s*/, '') 771 | if (!model_name) { 772 | e.reply('请指定要切换的模型名称') 773 | return false 774 | } 775 | 776 | // 切换模型,不再检查模型是否存在 777 | model = model_name 778 | e.reply(`已切换模型为:${model_name}`) 779 | return true 780 | } 781 | } 782 | 783 | // 以下代码在插件载入时执行一次 784 | 785 | // 检测模型列表文件是否存在 786 | if (!fs.existsSync(model_list_file)) { 787 | logger.mark(`[${plugin_name}]模型列表不存在,开始下载`) 788 | const type = await Download_file('https://oss.xt-url.com/GPT-Config/model_list.json', model_list_file) 789 | if (type) { 790 | logger.mark(`[${plugin_name}]模型列表下载成功`) 791 | } else { 792 | logger.error(`[${plugin_name}]模型列表下载失败`) 793 | } 794 | } 795 | 796 | // 全局动态变量model_list 797 | let model_list = await readJsonFile(model_list_file) 798 | 799 | // 检查配置文件是否存在 800 | if (!fs.existsSync(system_prompt_file)) { 801 | logger.mark(`[${plugin_name}]配置文件不存在,开始下载`) 802 | const type = await Download_file(model_list.system_prompt_url, system_prompt_file) 803 | if (type) { 804 | logger.mark(`[${plugin_name}]配置文件下载成功`) 805 | } else { 806 | logger.error(`[${plugin_name}]配置文件下载失败`) 807 | } 808 | } 809 | 810 | // 动态全局变量system_prompt_list 811 | let system_prompt_list = await readJsonFile(system_prompt_file) 812 | 813 | // 设置初始system_prompt 814 | if (!system_prompt) { 815 | system_prompt = system_prompt_list.system_prompt + model_list.default_prompt 816 | } 817 | 818 | // 每日统计token 819 | schedule.scheduleJob('0 0 0 * * *', async () => { 820 | //schedule.scheduleJob('1 * * * * *', async () => { // 测试用 821 | logger.mark(`[${plugin_name}]开始统计昨日token`) 822 | try { 823 | // 获取Redis中的数据 824 | const token_history = parseInt(await redis.get(`GLM:token:Today`), 10) 825 | 826 | // 重置Redis中的数据 827 | await redis.set(`GLM:token:Today`, 0) 828 | 829 | // 写入CSV文件 830 | fs.appendFileSync(`${plugin_data_path}/token_log.csv`, `${formatTimestamp(new Date() - 24 * 60 * 60 * 1000)},${token_history}\n`, 'utf8') 831 | } catch (error) { 832 | logger.error(`[${plugin_name}]导出数据失败: ${error}`) 833 | } 834 | }); 835 | 836 | // 每日检测模型更新 837 | schedule.scheduleJob('0 0 2 * * *', async () => { 838 | //schedule.scheduleJob('1 * * * * *', async () => { // 测试用 839 | logger.mark(`[${plugin_name}]开始检查模型列表更新`) 840 | 841 | // 获取把本地模型列表版本 842 | model_list = await readJsonFile(model_list_file) 843 | 844 | // 获取云端模型列表版本 845 | let Reply = await fetch('https://oss.xt-url.com/GPT-Config/model_list.json', { 846 | headers: { 847 | "Content-Type": "application/json", 848 | "User-Agent": "new-api (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)" 849 | } 850 | }) 851 | Reply = await Reply.json() 852 | 853 | // 比较版本号 854 | if (Reply.version == model_list.version) { 855 | logger.mark(`[${plugin_name}]模型列表已是最新版本`) 856 | } else { 857 | logger.mark(`[${plugin_name}]发现新版本模型列表,正在更新中...`) 858 | 859 | // 更新本地模型列表 860 | fs.writeFileSync(model_list_file, JSON.stringify(Reply)); 861 | 862 | logger.mark(`[${plugin_name}]模型列表更新完成`) 863 | } 864 | }); 865 | 866 | // 旧版本redis数据升级 867 | (async () => { 868 | let migrateDone = {} 869 | 870 | try { 871 | migrateDone.token_today = await redis.get(`GLM_chat_token/today`) 872 | if (migrateDone.token_today) { 873 | await redis.set(`GLM:token:Today`, migrateDone.token_today) 874 | await redis.del(`GLM_chat_token/today`) 875 | } 876 | 877 | migrateDone.token_history = await redis.get(`GLM_chat_token/Statistics`) 878 | if (migrateDone.token_history) { 879 | await redis.set(`GLM:token:Statistics`, migrateDone.token_history) 880 | await redis.del(`GLM_chat_token/Statistics`) 881 | } 882 | 883 | } catch (error) { 884 | logger.error(`[${plugin_name}] Redis数据迁移失败: ${error.stack}`) 885 | } finally { 886 | migrateDone = null 887 | } 888 | })() -------------------------------------------------------------------------------- /智谱GLM.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 作者:xiaotian2333 3 | * 开源地址:https://github.com/xiaotian2333/yunzai-plugins-Single-file 4 | * 此版本为早期针对智谱开发的专属版,兼容标准openai,如果使用new-api来中转请求更推荐使用new-api.js 5 | * 两个版本的对话记忆、token统计互通。但智谱GLM.js将暂停更新,仅修复bug 6 | */ 7 | 8 | import fetch from "node-fetch" 9 | import fs from "fs" 10 | import schedule from 'node-schedule' 11 | 12 | // 智谱API Key,需要自行申请(如需要去水印需实名,仅使用文本模型无需实名) 13 | // 申请链接:https://www.bigmodel.cn/invite?icode=iGW2wQ0KiXGc0PVU%2BeTSFEjPr3uHog9F4g5tjuOUqno%3D 14 | 15 | // 可不填,不填则使用沉浸式翻译的Token(仅可使用 glm-4-flash(旧版),glm-4-flash-250414(新版)模型,其他模型需自行申请) 16 | const Authorization = "" //API Key 17 | const base_url = "https://open.bigmodel.cn/api/paas/v4/chat/completions" //API接口,openai兼容 18 | let model = "glm-4-flash-250414" //模型名称 19 | let web_search = false //是否使用web搜索,从2025年6月1日0点起,收费单价为0.01元/次,因此改为默认关闭 20 | const search_engine = "search_std" //搜索引擎名称,参考:https://www.bigmodel.cn/pricing 21 | const max_log = 10 //最大历史记录数 22 | const plugin_name = "智谱GLM" //插件名称 23 | const think_print = false //支持思考的模型是否输出思考过程 24 | const on_thinking = true //仅 GLM-4.5 及以上模型支持此参数配置. 控制大模型是否开启思维链。 25 | const user_agent_disguise = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" // 伪装ua 26 | 27 | // 多模态相关配置,需配置key才可使用 28 | let vision_enable = false //是否开启多模态 29 | const version_Authorization = "" //多模态API Key 30 | const version_url = "https://open.bigmodel.cn/api/paas/v4/chat/completions" // 模型版本接口,不要修改 31 | const vision_model = "glm-4.6v" //多模态模型名称,参考:https://www.bigmodel.cn/pricing 32 | const version_do_sample = true //是否启用采样策略来生成文本。默认值为 true。对于需要一致性和可重复性的任务(如代码生成、翻译),建议设置为 false。 33 | const version_top_p = 0.6 // 不懂勿动 34 | const version_temperature = 0.8 // 不懂勿动 35 | 36 | 37 | // 系统提示词,引导模型进行对话 38 | // 请通过配置文件进行修改,不要直接修改代码 39 | // 配置文件路径 40 | const plugin_data_path = `./data/plugins/智谱GLM/` 41 | const system_prompt_file = `${plugin_data_path}system_prompt.json` 42 | const model_list_file = `${plugin_data_path}model_list.json` 43 | let system_prompt 44 | 45 | const list = [ 46 | '过滤词列表-156411gfchc', 47 | '模糊匹配-15615156htdy1', 48 | ] 49 | 50 | // 函数:读取并解析JSON文件 51 | // 参数:文件路径 52 | // 返回:解析后的JSON对象 53 | // 抛出错误:文件不存在、文件为空、JSON解析错误 54 | function readJsonFile(path) { 55 | if (!fs.existsSync(path)) { 56 | throw new Error(`配置文件不存在`) 57 | } 58 | 59 | const stats = fs.statSync(path) 60 | if (stats.size === 0) { 61 | throw new Error(`配置文件为空`) 62 | } 63 | 64 | const data = fs.readFileSync(path, 'utf8') 65 | return JSON.parse(data) 66 | } 67 | 68 | 69 | // 生成32位随机字符串 70 | let Random_device_ID 71 | function randomString() { 72 | if (Random_device_ID) { return Random_device_ID } 73 | let str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 74 | for (let i = 0; i < 32; i++) { 75 | let id = Math.ceil(Math.random() * str.length) 76 | Random_device_ID += str.charAt(id) 77 | } 78 | logger.debug(`[${plugin_name}]生成新的设备ID:${Random_device_ID}`) 79 | return Random_device_ID 80 | } 81 | 82 | // 获取Token 83 | async function get_token() { 84 | // 如果Authorization存在,直接返回 85 | if (Authorization) { 86 | return Authorization 87 | } 88 | 89 | // 检查Token是否存在 90 | let token = await redis.type("GLM:token:token") 91 | // 如果Token不存在,获取新的Token 92 | if (token == 'none') { 93 | token = await fetch(`https://api2.immersivetranslate.com/big-model/get-token?deviceId=${randomString()}`, { 94 | headers: { 95 | 'User-Agent': user_agent_disguise 96 | } 97 | }) 98 | token = await token.json() 99 | logger.debug(`[${plugin_name}]获取到新的Token:${token.apiToken}`) 100 | await redis.set('GLM:token:token', token.apiToken, { EX: token.expireTime }) // 保存到redis 101 | } 102 | // 返回Token 103 | return await redis.get('GLM:token:token') 104 | } 105 | 106 | // 下载系统提示词 107 | async function Download_file(url, path) { 108 | try { 109 | // 确保目录存在 110 | fs.mkdirSync(plugin_data_path, { recursive: true }) 111 | // 发送HTTP请求下载文件 112 | logger.debug(`[${plugin_name}]正在下载来自 ${url} 的文件`) 113 | const response = await fetch(url, { 114 | headers: { 115 | 'User-Agent': 'GLM (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)' 116 | } 117 | }) 118 | // 检查响应状态 119 | if (!response.ok) { 120 | throw new Error(`[${plugin_name}]网络请求错误:\n${response.status}`) 121 | } 122 | // 解析响应数据为JSON 123 | const data = await response.json() 124 | logger.debug(`[${plugin_name}]文件下载完备:\n${data}`) 125 | // 将数据保存到文件 126 | await fs.promises.writeFile(path, JSON.stringify(data)) 127 | return true 128 | } catch (error) { 129 | // 捕获并打印错误信息 130 | //logger.error(`[${plugin_name}]配置文件下载失败:\n`, error) 131 | return false 132 | } 133 | } 134 | 135 | // 重新读取配置文件 136 | async function read_config() { 137 | try { 138 | system_prompt_list = await readJsonFile(system_prompt_file) 139 | model_list = await readJsonFile(model_list_file) 140 | return false 141 | } catch (err) { 142 | logger.error(`[${plugin_name}]读取或解析JSON文件时出错:`, err.message) 143 | return `读取或解析JSON文件时出错: \n${err.message}` 144 | } 145 | } 146 | 147 | // 动态变量实时替换 148 | function replace_var(str, nickname) { 149 | const date = new Date(Date.now()) 150 | 151 | // 年月日 152 | const year = date.getFullYear() 153 | const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始,需要加1,并确保是两位数 154 | const day = String(date.getDate()).padStart(2, '0') 155 | // 时分秒 156 | const hour = String(date.getHours()).padStart(2, '0') 157 | const minute = String(date.getMinutes()).padStart(2, '0') 158 | const second = String(date.getSeconds()).padStart(2, '0') 159 | 160 | str = str.replace(/\$\{Bot\.nickname\}/, `${nickname}`) 161 | str = str.replace(/\$\{now_date\}/, `${year}年${month}月${day}日`) 162 | str = str.replace(/\$\{now_time\}/, `${hour}时${minute}分${second}秒`) 163 | return str 164 | } 165 | 166 | /** 时间戳转可视化日期函数 167 | * @param timestamp 毫秒级时间戳 168 | * 返回格式参考:2023,10,05 169 | */ 170 | function formatTimestamp(timestamp = Date.now()) { 171 | const date = new Date(timestamp) 172 | 173 | // 获取年、月、日 174 | const year = date.getFullYear() 175 | const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始,需要加1,并确保是两位数 176 | const day = String(date.getDate()).padStart(2, '0') 177 | 178 | // 格式化日期字符串 179 | const formattedDate = `${year},${month},${day}` 180 | 181 | return formattedDate 182 | } 183 | 184 | /** 休眠函数 185 | * @time 毫秒 186 | */ 187 | function sleep(time) { 188 | return new Promise((resolve) => setTimeout(resolve, time)) 189 | } 190 | 191 | /** 192 | * 获取用户引用的消息 193 | * @param {Object} getReply - 引用消息对象 194 | * @return {list} - 返回引用的消息内容 195 | */ 196 | async function get_quote_message(getReply) { 197 | if (typeof getReply === 'function') { 198 | try { 199 | const reply = await getReply() 200 | if (reply.message) { 201 | return reply.message 202 | } 203 | } 204 | catch (err) { 205 | logger.warn(`[${plugin_name}]引用消息获取失败,错误信息:${err}`) 206 | return false 207 | } 208 | } 209 | } 210 | /** 211 | * 将消息列表拆分并分类 212 | * @param {list} message 213 | */ 214 | function split_message(message) { 215 | // 多模态相关信息初始化 216 | let text_list = [] 217 | let image_list = [] 218 | let isVision = false // 是否激活多模态处理 219 | 220 | // 拆分消息 221 | for (let i = 0; i < message.length; i++) { 222 | let element = message[i] 223 | 224 | // 分类 225 | if (element.type === "text") { 226 | // 文字 227 | text_list.push(element.text) 228 | 229 | } else if (element.type === "image") { 230 | // 图片 231 | image_list.push(element.url) 232 | isVision = true 233 | 234 | } 235 | } 236 | return { 237 | text_list, 238 | image_list, 239 | isVision 240 | } 241 | } 242 | 243 | export class bigmodel extends plugin { 244 | constructor() { 245 | super({ 246 | name: '智谱GLM', 247 | event: 'message', 248 | priority: 9000, 249 | rule: [ 250 | { 251 | reg: /^#(智谱|[Gg][Ll][Mm])?(新开|重启|重置|清空|删除|清楚|清除)(聊天|对话|记录|记忆|历史)$/, 252 | fnc: 'clear', 253 | }, 254 | { 255 | reg: /^#(智谱|[Gg][Ll][Mm])?(角色|身份|人物|设定|提示词|预设|人格)列表$/, 256 | fnc: 'role_list', 257 | }, 258 | { 259 | reg: /^#(智谱|[Gg][Ll][Mm])?(切换|更改|换)(角色|身份|人物|设定|提示词|预设|人格)/, 260 | fnc: 'role', 261 | }, 262 | { 263 | reg: /^#(智谱|[Gg][Ll][Mm])?(更新|下载|克隆)(角色|身份|人物|设定|提示词|预设|人格)?(文件|配置|配置文件|数据)?/, 264 | fnc: 'pull_1', 265 | }, 266 | { 267 | reg: /^#(?:(智谱|[Gg][Ll][Mm])(状态|info)(信息|数据)?|(状态|info)(信息|数据)?)$/, 268 | fnc: 'GLM_info', 269 | }, 270 | { 271 | reg: /^#(智谱|[Gg][Ll][Mm])?(模型|model)(列表|信息|数据)?/, 272 | fnc: 'model_list_help', 273 | }, 274 | { 275 | reg: /^#(智谱|[Gg][Ll][Mm])?(强制|强行)?(更换|切换|换|改|设置)(模型|model)/, 276 | fnc: 'model_set', 277 | }, 278 | { 279 | reg: /^#(智谱|[Gg][Ll][Mm])?(开启|打开|关闭|取消)(联网|搜索|网络)$/, 280 | fnc: 'web_search_set', 281 | }, 282 | { 283 | reg: /^#(智谱|[Gg][Ll][Mm])(查询)?(余额|钱包|金额|消费|财务)$/, 284 | fnc: 'balance', 285 | }, 286 | { 287 | reg: '', 288 | fnc: 'chat', 289 | log: false 290 | } 291 | ] 292 | }) 293 | } 294 | 295 | async chat(e) { 296 | // if (!e.isMaster) { return false } // 只允许主人使用 297 | 298 | // 先过滤非文本信息 299 | if (!e.msg) { return false } 300 | 301 | // 删除不需要的部分 302 | let msg = e.msg 303 | msg = msg.replace(' ', '') 304 | 305 | // 只有被艾特、私聊、命中机器人名字的消息才会被处理 306 | if (!(e.isPrivate || e.atme || e.atBot || msg.includes(this.e.bot.nickname))) { 307 | return false 308 | } 309 | 310 | // 输入过滤 311 | if (list.some(item => msg.includes(item))) { 312 | // 检测到需要过滤的词后的处理逻辑 313 | logger.mark(`[${plugin_name}]检测到敏感词,已过滤`) 314 | e.reply("输入包含敏感词,已拦截") 315 | return true 316 | } 317 | 318 | // 再过滤空信息 319 | if (!msg) { 320 | return false 321 | } 322 | // 消息长度限制,正常聊天200字足以,字数开放越多越容易被洗脑 323 | if (msg.length > 200 && !e.isMaster) { 324 | e.reply('输入文本长度过长') 325 | return true 326 | } 327 | // 多模态能力判断 328 | if (!Authorization && vision_enable) { 329 | logger.warn(`[${plugin_name}]未配置key,无法使用多模态能力`) 330 | vision_enable = false 331 | } 332 | 333 | logger.mark(`[${plugin_name}]${e.group_id}_${e.user_id} 发送了消息:${msg}`) 334 | 335 | // 多模态相关信息初始化 336 | let text_list = [] // 历史文本列表 337 | let image_list = [] // 图片列表,存储url 338 | let isVision = false // 是否激活多模态处理 339 | let backup_msg = '' // 备份消息,用于多模态处理时,防止单模特模型无法处理多模态内容 340 | 341 | 342 | // 引用历史消息 343 | let quote_message = await get_quote_message(e?.getReply) 344 | 345 | // 存在历史消息 346 | if (quote_message) { 347 | quote_message = split_message(quote_message) 348 | 349 | 350 | text_list.push(...quote_message.text_list) 351 | image_list.push(...quote_message.image_list) 352 | isVision = quote_message.isVision // 如引用的消息包含图片等多模态内容则开启多模态处理 353 | } 354 | 355 | // 判断用户消息是否有图片 356 | let user_message = split_message(e.message) 357 | if (user_message.isVision) { 358 | image_list.push(...user_message.image_list) 359 | isVision = isVision || user_message.isVision // 如用户消息包含图片等多模态内容则开启多模态处理,如本身已激活则保持激活 360 | } 361 | 362 | // 判断引用的消息是否有图片 363 | if (isVision && vision_enable) { 364 | // 多模态处理 365 | let version_msg = [] 366 | for (let i = 0; i < image_list.length; i++) { 367 | version_msg.push({ 368 | "type": "image_url", 369 | "image_url": { 370 | "url": image_list[i] 371 | } 372 | }) 373 | } 374 | 375 | // 添加引导词 376 | if (text_list.length > 0) { 377 | version_msg.push({ 378 | "type": "text", 379 | "text": `用户引用了一些图片和历史消息,这些图片和历史消息大概率会帮助回答用户问题,但也有小概率不关联。历史消息内容如下:\n${text_list.join('\n')}\n以下是用户的输入:\n${msg}` 380 | }) 381 | } else { 382 | version_msg.push({ 383 | "type": "text", 384 | "text": `用户引用了一些图片,图片大概率会帮助回答用户问题,但也有小概率不关联,以下是用户的输入:\n${msg}` 385 | }) 386 | } 387 | // 创建文本模型兼容消息,作为放入redis的历史消息存储 388 | backup_msg = `用户引用了一些图片,已由其他模型处理并回答,如用户询问请根据上下文回答用户问题,以下是用户引用图片时的输入:\n${msg}` 389 | 390 | msg = version_msg 391 | } else if (text_list > 0) { 392 | // 没有激活多模态处理,但引用了消息 393 | msg = `用户引用了这些历史消息:${text_list.join('\n')}\n以上消息可能会帮助回答用户问题,但也有可能不关联,以下是用户的输入:\n${msg}` 394 | } 395 | 396 | let msg_log = await redis.type(`GLM:chat_log:${e.group_id}:${e.user_id}`) 397 | if (msg_log == 'none') { 398 | // 如果msg_log不存在,初始化msg_log 399 | msg_log = [{ 400 | "role": "system", 401 | "content": system_prompt 402 | }] 403 | 404 | } else { 405 | // 如果msg_log存在,获取msg_log 406 | msg_log = await redis.get(`GLM:chat_log:${e.group_id}:${e.user_id}`) 407 | msg_log = JSON.parse(msg_log) 408 | } 409 | // 添加聊天信息 410 | const backup_msg_log = [...msg_log] 411 | 412 | msg_log.push({ 413 | "role": "user", 414 | "content": msg 415 | }) 416 | 417 | // 限制聊天记录长度 418 | if (msg_log.length > max_log) { 419 | // 删除除system_prompt之外的最旧记录 420 | msg_log.splice(1, 1) 421 | } 422 | 423 | // 实时修改system_prompt 424 | msg_log[0].content = replace_var(system_prompt, this.e.bot.nickname) 425 | 426 | let data = {} 427 | let llm_url = base_url 428 | let api_key = Authorization 429 | // 构建请求体 430 | if (isVision && vision_enable) { 431 | // 多模态处理 432 | logger.debug(`[${plugin_name}]进入多模态处理,构建多模态请求`) 433 | llm_url = version_url 434 | api_key = version_Authorization 435 | data = { 436 | model: vision_model, 437 | messages: msg_log, 438 | do_sample: version_do_sample, 439 | temperature: version_temperature, 440 | top_p: version_top_p, 441 | stream: false, 442 | user_id: `${e.group_id}_${e.user_id}`, 443 | } 444 | } else { 445 | // 单模态处理 446 | logger.debug(`[${plugin_name}]未激活多模态处理,构建文本请求`) 447 | data = { 448 | model: model, 449 | messages: msg_log, 450 | do_sample: true, 451 | temperature: 0.8, // 温度,0.8是默认值,可以调整 452 | stream: false, 453 | user_id: `${e.group_id}_${e.user_id}`, 454 | } 455 | } 456 | 457 | // 开启搜索功能 458 | if (web_search) { 459 | data.tools = [{ 460 | type: "web_search", 461 | web_search: { 462 | enable: web_search, 463 | search_engine: search_engine, // 选择搜索引擎类型 464 | } 465 | }] 466 | } 467 | // 注入思考参数 468 | if (on_thinking) { 469 | data.thinking = { 470 | type: on_thinking ? 'enabled' : 'disabled', // 仅 GLM-4.5 及以上模型支持此参数配置. 控制大模型是否开启思维链 471 | } 472 | } 473 | 474 | // 网络请求 475 | let Reply = await fetch(llm_url, { 476 | method: 'POST', 477 | headers: { 478 | "Content-Type": "application/json", 479 | "Authorization": `Bearer ${api_key || await get_token()}`, 480 | "User-Agent": user_agent_disguise 481 | }, 482 | body: JSON.stringify(data) 483 | }) 484 | Reply = await Reply.json() 485 | 486 | // 错误处理 487 | if (Reply.error) { 488 | if (Reply.error.code == "1210") { 489 | e.reply(`暂不支持gif或尺寸过小的图片识别`) 490 | logger.error(`[${plugin_name}]发生错误,响应码[${Reply.error.code}]\n来自API的错误信息:\n${Reply?.error?.message || Reply?.msg || "没有来自API的错误信息"}`) 491 | logger.error(`[${plugin_name}]此错误通常由图片尺寸过小或gif图片导致,如图片链接无法被智谱访问也会产生此报错`) 492 | return false 493 | } 494 | e.reply(`[${plugin_name}]发生错误,响应码[${Reply.code || Reply.error.code || "无"}]\n来自API的错误信息:\n${Reply?.error?.message || Reply?.msg || "没有来自API的错误信息"}`) 495 | return false 496 | } 497 | // 检查choices是否存在 498 | if (!Reply?.choices || !Reply?.choices[0]) { 499 | e.reply(`[${plugin_name}]API返回格式错误:缺少回复数据`) 500 | return false 501 | } 502 | // 获取回复内容 503 | let content = Reply.choices[0].message.content 504 | 505 | // 过滤思考过程 506 | let think_text = '' 507 | // 标准思考处理 508 | if (Reply.choices[0].message?.reasoning_content) { 509 | logger.debug(`[${plugin_name}]检测到有思考过程`) 510 | think_text = Reply.choices[0].message?.reasoning_content 511 | } 512 | // 兼容早期思考输出 513 | else if (content.startsWith('\n') || content.startsWith('')) { 514 | 515 | logger.debug(`[${plugin_name}]检测到有思考过程`) 516 | // 处理think标签 517 | think_text = content.split('') 518 | think_text[0] = think_text[0].replace('', '').trim() 519 | // 过滤思考过程 520 | content = think_text[1].trim() 521 | think_text = think_text[0] 522 | } 523 | 524 | // 如果用户开启思考发送且think_text不为空,则发送思考过程 525 | if (think_print && think_text) { 526 | logger.debug(`[${plugin_name}]用户开启了发送思考过程`) 527 | // 发送思考过程 528 | let msgList = [{ 529 | user_id: 2854200865, 530 | nickname: '思考过程', 531 | message: think_text 532 | }] 533 | await e.reply(await Bot.makeForwardMsg(msgList)) 534 | } 535 | 536 | // 过滤智普多模态回复中的标记 537 | content = content.replace(/<\|begin_of_box\|>/, '') 538 | content = content.replace(/<\|end_of_box\|>/, '') 539 | 540 | // 过滤首尾空格 541 | content = content.trim() 542 | 543 | // 为兼容文本模型,多模态处理的消息存储为普通格式 544 | if (isVision && vision_enable) { 545 | // 多模态处理 546 | msg_log = backup_msg_log 547 | 548 | // 添加用户消息 549 | msg_log.push({ 550 | "role": "user", 551 | "content": backup_msg 552 | }) 553 | 554 | // 限制聊天记录长度 555 | if (msg_log.length > max_log) { 556 | // 删除除system_prompt之外的最旧记录 557 | msg_log.splice(1, 1) 558 | } 559 | 560 | // 添加模型的回答 561 | msg_log.push({ 562 | "role": "assistant", 563 | "content": content 564 | }) 565 | } else { 566 | // 纯文本处理 567 | msg_log.push({ 568 | "role": "assistant", 569 | "content": content 570 | }) 571 | } 572 | // 保存对话记录 573 | await redis.set(`GLM:chat_log:${e.group_id}:${e.user_id}`, JSON.stringify(msg_log), { EX: 60 * 60 * 24 * 7 }) // 保存到redis,过期时间为7天 574 | 575 | // 长文本分多句发送 576 | content = content.split('\n') 577 | 578 | for (const line of content) { 579 | e.reply(line) 580 | await sleep(Math.floor(Math.random() * (5000 - 1000 + 1)) + 1000) 581 | } 582 | 583 | // 统计token用量 584 | // 今日用量 585 | let token_today = parseInt(await redis.get(`GLM:token:Today`), 10) 586 | if (token_today == 'none') { 587 | token_today = 0 588 | } 589 | await redis.set(`GLM:token:Today`, token_today + Reply.usage.total_tokens) 590 | // 总用量 591 | let token_history = parseInt(await redis.get(`GLM:token:Statistics`), 10) 592 | if (token_history == 'none') { 593 | token_history = 0 594 | } 595 | await redis.set(`GLM:token:Statistics`, token_history + Reply.usage.total_tokens) 596 | 597 | 598 | return true 599 | } 600 | 601 | async clear(e) { 602 | await redis.del(`GLM:chat_log:${e.group_id}:${e.user_id}`) 603 | e.reply('对话记录已清除') 604 | return true 605 | } 606 | 607 | async role_list(e) { 608 | // 刷新配置文件 609 | const err = await read_config() 610 | if (err) { 611 | e.reply(err) 612 | return false 613 | } 614 | 615 | let name_list = ["可切换的角色身份\n"] 616 | 617 | Object.keys(system_prompt_list).forEach(key => { 618 | //console.log(`name: ${key}, key: ${system_prompt_list[key]}`) 619 | // 如果key为system_prompt则跳过本次循环 620 | if (key == 'system_prompt') { 621 | return 622 | } 623 | name_list.push(`${key}\n`) 624 | }) 625 | 626 | e.reply(name_list) 627 | return true 628 | } 629 | 630 | async role(e) { 631 | // 只允许主人使用 632 | if (!e.isMaster) { 633 | e.reply('只有主人才能设置角色') 634 | return false 635 | } 636 | 637 | const name = e.msg.replace(/^#(智谱|[Gg][Ll][Mm])?(切换|更改|换)(角色|身份|人物|设定|提示词|预设|人格)/, '') 638 | 639 | if (!name) { 640 | e.reply('请输入要切换的预设\n\n发送 #智谱预设列表 查看可切换的预设') 641 | return false 642 | } 643 | 644 | // 刷新配置文件 645 | const err = await read_config() 646 | if (err) { 647 | e.reply(err) 648 | return false 649 | } 650 | 651 | // 标记是否找到匹配的角色 652 | let type = false 653 | 654 | // 遍历人物设定 655 | Object.keys(system_prompt_list).forEach(key => { 656 | //console.log(`name: ${key}, key: ${system_prompt_list[key]}`) 657 | if (key == name) { 658 | system_prompt = system_prompt_list.system_prompt + system_prompt_list[key] 659 | type = true 660 | } 661 | }) 662 | 663 | // 判断是否找到匹配的角色设定 664 | if (type) { 665 | e.reply(`人物设定已切换为${name}`) 666 | } else { 667 | e.reply(`人物设定${name}不存在,想要创建一个临时预设吗\n\n发送 #创建临时预设 进行创建`) 668 | this.setContext('set_system_prompt_1') 669 | return true 670 | } 671 | 672 | return true 673 | } 674 | 675 | async pull_1(e) { 676 | // 只允许主人使用 677 | if (!e.isMaster) { 678 | e.reply('只有主人才覆盖预设文件') 679 | return false 680 | } 681 | e.reply(`警告:此操作不可撤销!!!\n\n确定要进行下载吗,这将会覆盖当前的配置文件\n\n确定覆盖发送 #确认覆盖预设文件 进行下一步操作`) 682 | this.setContext('pull_2') 683 | return true 684 | } 685 | 686 | async pull_2(e) { 687 | e = this.e 688 | this.finish('pull_2') 689 | // 只允许主人使用 690 | if (!e.isMaster) { 691 | e.reply('只有主人才覆盖预设文件') 692 | return false 693 | } 694 | if (e.msg == '#确认覆盖预设文件') { 695 | const type = await Download_file(model_list.system_prompt_url, system_prompt_file) 696 | if (type) { 697 | e.reply(`配置文件覆盖成功`) 698 | return true 699 | } else { 700 | e.reply(`配置文件覆盖失败`) 701 | return true 702 | } 703 | } else { 704 | e.reply(`未发送确认指令,取消覆盖预设文件`) 705 | return true 706 | } 707 | } 708 | 709 | async set_system_prompt_1(e) { 710 | e = this.e 711 | this.finish('set_system_prompt_1') 712 | // 只允许主人使用 713 | if (!e.isMaster) { 714 | e.reply('只有主人才创建临时预设') 715 | return false 716 | } 717 | // 删除不需要的部分 718 | let msg = e.msg 719 | msg = msg.replace(' ', '') 720 | 721 | if (msg == '#创建临时预设') { 722 | e.reply(`请发送要设置的提示词`) 723 | this.setContext('set_system_prompt_2') 724 | return true 725 | } else { 726 | return true 727 | 728 | } 729 | } 730 | 731 | async set_system_prompt_2(e) { 732 | e = this.e 733 | // 只允许主人使用 734 | if (!e.isMaster) { 735 | e.reply('只有主人才创建临时预设') 736 | return false 737 | } 738 | // 删除不需要的部分 739 | let msg = e.msg 740 | msg = msg.replace(' ', '') 741 | 742 | if (!msg) { 743 | e.reply(`请发送要设置的提示词`) 744 | return true 745 | } else { 746 | this.finish('set_system_prompt_2') 747 | system_prompt = msg 748 | e.reply(`已创建并应用临时预设:\n${system_prompt}`) 749 | return true 750 | } 751 | } 752 | 753 | async GLM_info(e) { 754 | // 取token用量 755 | const token_today = parseInt(await redis.get(`GLM:token:Today`), 10) 756 | const token_history = parseInt(await redis.get(`GLM:token:Statistics`), 10) 757 | 758 | // 取随机提示 759 | const tips = model_list.tips[Math.floor(Math.random() * model_list.tips.length)] 760 | 761 | // 构建信息 762 | const msg = [ 763 | `=====${plugin_name}当前状态=====\n`, 764 | `密钥状态:${Authorization ? '已配置' : '未配置'}\n`, 765 | `今日token消耗:${token_today}\n`, 766 | `累计token消耗:${token_history}\n`, 767 | `当前模型:${model}\n`, 768 | `数据版本:${model_list.version}\n`, 769 | `联网能力:${web_search ? '开启' : '关闭'}\n`, 770 | `思考能力:${on_thinking ? '开启' : '关闭'}\n`, 771 | `思考输出:${think_print ? '开启' : '关闭'}\n`, 772 | `多模态能力:${vision_enable ? '开启' : '关闭'}\n`, 773 | `=====================\n`, 774 | `Tip:${tips}` 775 | 776 | ] 777 | 778 | // 发送信息 779 | e.reply(msg) 780 | return true 781 | } 782 | 783 | async model_list_help(e) { 784 | let msgList = [] 785 | msgList.push( 786 | { 787 | user_id: 2854200865, 788 | nickname: '更新时间', 789 | message: model_list.data 790 | }, 791 | { 792 | user_id: 2854200865, 793 | nickname: '当前版本', 794 | message: model_list.version 795 | } 796 | ) 797 | 798 | for (const [modelName, modelInfo] of Object.entries(model_list.bigmodel.model_list)) { 799 | msgList.push({ 800 | user_id: 2854200865, 801 | nickname: '模型介绍', 802 | message: `模型:${modelName}\n介绍:${modelInfo.instructions}\n价格:${modelInfo.Price}\n类型:${modelInfo.type}` 803 | }) 804 | } 805 | 806 | // 发送消息 807 | await e.reply(await Bot.makeForwardMsg(msgList)) 808 | return true 809 | } 810 | 811 | async model_set(e) { 812 | // 只允许主人使用 813 | if (!e.isMaster) { 814 | e.reply('只有主人才能更改模型') 815 | return false 816 | } 817 | 818 | // 获取命令参数 819 | const model_name = e.msg.replace(/^#(智谱|[Gg][Ll][Mm])?(强制|强行)?(更换|切换|换|改|设置)(模型|model)\s*/, '') 820 | if (!model_name) { 821 | e.reply('请指定要切换的模型名称\n\n可发送 #模型列表 查看所有可用模型') 822 | return false 823 | } 824 | // 强制切换则不检查模型是否存在 825 | if (e.msg.includes('强制') || e.msg.includes('强行')) { 826 | model = model_name 827 | e.reply(`已强制切换模型为:${model_name}`) 828 | return true 829 | } 830 | 831 | // 检查模型是否存在 832 | let finish = false 833 | for (const [modelName] of Object.entries(model_list.bigmodel.model_list)) { 834 | if (modelName === model_name) { 835 | finish = true 836 | } 837 | } 838 | if (!finish) { 839 | e.reply(`模型 ${model_name} 不存在,请检查模型名称是否正确\n\n可发送 #模型列表 查看所有可用模型\n如确实需要切换请发送 #强制切换模型 进行切换`) 840 | return false 841 | } 842 | 843 | // 切换模型 844 | model = model_name 845 | e.reply(`已切换模型为:${model_name}`) 846 | return true 847 | } 848 | 849 | async web_search_set(e) { 850 | // 只允许主人使用 851 | if (!e.isMaster) { 852 | e.reply('只有主人才能更改联网设置') 853 | return false 854 | } 855 | 856 | // 判断是开启还是关闭联网 857 | if (e.msg.includes('开启') || e.msg.includes('打开')) { 858 | web_search = true 859 | e.reply(`已开启联网功能`) 860 | } 861 | if (e.msg.includes('关闭') || e.msg.includes('取消')) { 862 | web_search = false 863 | e.reply(`已关闭联网功能`) 864 | } 865 | 866 | return true 867 | } 868 | 869 | async balance(e) { 870 | // 只允许主人使用 871 | if (!e.isMaster) { 872 | e.reply('只有主人才能查询余额') 873 | return false 874 | } 875 | 876 | // 仅在配置了密钥时才查询余额 877 | if (!Authorization) { 878 | e.reply('没有配置密钥,无法查询余额') 879 | return false 880 | } 881 | 882 | // 查询余额 883 | let balance = await fetch("https://www.bigmodel.cn/api/biz/account/query-customer-account-report", { 884 | method: 'GET', 885 | headers: { 886 | 'Authorization': Authorization, 887 | 'Content-Type': 'application/json' 888 | } 889 | }) 890 | balance = await balance.json() 891 | if (!balance.success) { 892 | e.reply('查询余额失败,请检查密钥是否正确') 893 | return false 894 | } 895 | balance = balance.data 896 | 897 | // 构建信息 898 | const msg = [ 899 | `=====${plugin_name}财务总览=====\n`, 900 | `当前余额:${balance.balance}\n`, 901 | `累计充值:${balance.rechargeAmount}\n`, 902 | `赠送金额:${balance.giveAmount}\n`, 903 | `=====================\n`, 904 | `累计消费:${balance.totalSpendAmount}\n`, 905 | `冻结余额:${balance.frozenBalance}\n`, 906 | `可用余额:${balance.availableBalance}\n`, 907 | `=====================\n` 908 | ] 909 | 910 | // 发送信息 911 | e.reply(msg) 912 | return true 913 | } 914 | } 915 | 916 | // 以下代码在插件载入时执行一次 917 | 918 | // 检测模型列表文件是否存在 919 | if (!fs.existsSync(model_list_file)) { 920 | logger.mark(`[${plugin_name}]模型列表不存在,开始下载`) 921 | const type = await Download_file('https://oss.xt-url.com/GPT-Config/model_list.json', model_list_file) 922 | if (type) { 923 | logger.mark(`[${plugin_name}]模型列表下载成功`) 924 | } else { 925 | logger.error(`[${plugin_name}]模型列表下载失败`) 926 | } 927 | } 928 | 929 | // 全局动态变量model_list 930 | let model_list = await readJsonFile(model_list_file) 931 | 932 | // 检查配置文件是否存在 933 | if (!fs.existsSync(system_prompt_file)) { 934 | logger.mark(`[${plugin_name}]配置文件不存在,开始下载`) 935 | const type = await Download_file(model_list.system_prompt_url, system_prompt_file) 936 | if (type) { 937 | logger.mark(`[${plugin_name}]配置文件下载成功`) 938 | } else { 939 | logger.error(`[${plugin_name}]配置文件下载失败`) 940 | } 941 | } 942 | 943 | // 动态全局变量system_prompt_list 944 | let system_prompt_list = await readJsonFile(system_prompt_file) 945 | 946 | // 设置初始system_prompt 947 | if (!system_prompt) { 948 | system_prompt = system_prompt_list.system_prompt + model_list.default_prompt 949 | } 950 | 951 | // 未填写key判断 952 | if (!Authorization) { 953 | logger.warn(`[${plugin_name}]未配置key,无法使用多模态能力`) 954 | vision_enable = false 955 | url = "https://aigw1.immersivetranslate.com/api/paas/v4/chat/completions" 956 | } 957 | 958 | 959 | 960 | // 每日统计token 961 | schedule.scheduleJob('0 0 0 * * *', async () => { 962 | //schedule.scheduleJob('1 * * * * *', async () => { // 测试用 963 | logger.mark(`[${plugin_name}]开始统计昨日token`) 964 | try { 965 | // 获取Redis中的数据 966 | const token_history = parseInt(await redis.get(`GLM:token:Today`), 10) 967 | 968 | // 重置Redis中的数据 969 | await redis.set(`GLM:token:Today`, 0) 970 | 971 | // 写入CSV文件 972 | fs.appendFileSync(`${plugin_data_path}/token_log.csv`, `${formatTimestamp(new Date() - 24 * 60 * 60 * 1000)},${token_history}\n`, 'utf8') 973 | } catch (error) { 974 | logger.error(`[${plugin_name}]导出数据失败: ${error}`) 975 | } 976 | }); 977 | 978 | // 每日检测模型更新 979 | schedule.scheduleJob('0 0 2 * * *', async () => { 980 | //schedule.scheduleJob('1 * * * * *', async () => { // 测试用 981 | logger.mark(`[${plugin_name}]开始检查模型列表更新`) 982 | 983 | // 获取把本地模型列表版本 984 | model_list = await readJsonFile(model_list_file) 985 | 986 | // 获取云端模型列表版本 987 | let Reply = await fetch('https://oss.xt-url.com/GPT-Config/model_list.json', { 988 | headers: { 989 | "Content-Type": "application/json", 990 | "User-Agent": "GLM (author by xiaotian2333) github(https://github.com/xiaotian2333/yunzai-plugins-Single-file)" 991 | } 992 | }) 993 | Reply = await Reply.json() 994 | 995 | // 比较版本号 996 | if (Reply.version == model_list.version) { 997 | logger.mark(`[${plugin_name}]模型列表已是最新版本`) 998 | } else { 999 | logger.mark(`[${plugin_name}]发现新版本模型列表,正在更新中...`) 1000 | 1001 | // 更新本地模型列表 1002 | fs.writeFileSync(model_list_file, JSON.stringify(Reply)); 1003 | 1004 | logger.mark(`[${plugin_name}]模型列表更新完成`) 1005 | } 1006 | }); 1007 | 1008 | // 旧版本redis数据升级 1009 | (async () => { 1010 | let migrateDone = {} 1011 | 1012 | try { 1013 | migrateDone.token_today = await redis.get(`GLM_chat_token/today`) 1014 | if (migrateDone.token_today) { 1015 | await redis.set(`GLM:token:Today`, migrateDone.token_today) 1016 | await redis.del(`GLM_chat_token/today`) 1017 | } 1018 | 1019 | migrateDone.token_history = await redis.get(`GLM_chat_token/Statistics`) 1020 | if (migrateDone.token_history) { 1021 | await redis.set(`GLM:token:Statistics`, migrateDone.token_history) 1022 | await redis.del(`GLM_chat_token/Statistics`) 1023 | } 1024 | 1025 | } catch (error) { 1026 | logger.error(`[${plugin_name}] Redis数据迁移失败: ${error.stack}`) 1027 | } finally { 1028 | migrateDone = null 1029 | } 1030 | })() --------------------------------------------------------------------------------