├── plugins ├── button │ └── .gitignore └── 纯文模板.js ├── resources ├── icon.png ├── help │ ├── icon.png │ ├── imgs │ │ ├── bg.jpg │ │ ├── main.png │ │ └── config.js │ ├── version-info.html │ ├── index.html │ ├── version-info.css │ ├── index.css │ ├── index.less │ └── version-info.less ├── DAU │ ├── img │ │ └── bg.jpg │ ├── font │ │ └── ruizizhenyan.ttf │ └── index.html ├── admin │ ├── imgs │ │ ├── bg.png │ │ ├── bg1.jpg │ │ ├── cfg-right.jpg │ │ └── cfg-right.png │ ├── index.html │ ├── index.css │ └── index.less ├── default_avatar.jpg ├── common │ ├── bg │ │ ├── bg-geo.jpg │ │ ├── bg-anemo.jpg │ │ ├── bg-cryo.jpg │ │ ├── bg-dendro.jpg │ │ ├── bg-hydro.jpg │ │ ├── bg-pyro.jpg │ │ └── bg-electro.jpg │ ├── cont │ │ ├── logo.png │ │ └── card-bg.png │ ├── font │ │ ├── NZBZ.ttf │ │ ├── 华文中宋.TTF │ │ ├── NZBZ.woff │ │ ├── HYWH-65W.ttf │ │ ├── HYWH-65W.woff │ │ ├── tttgbnumber.ttf │ │ └── tttgbnumber.woff │ ├── base.less │ ├── base.css │ ├── layout │ │ ├── default.html │ │ └── elem.html │ └── common.less ├── shamrock │ ├── img │ │ ├── star.png │ │ ├── shamrock.webp │ │ ├── github-logo-white.png │ │ ├── eye.svg │ │ └── code-branch.svg │ ├── index.css │ ├── index.html │ └── index1.html ├── QRCode │ └── QRCode.html └── index.html ├── .npmrc ├── .github └── dependabot.yml ├── .gitignore ├── config └── defSet │ ├── Config-Other.yaml │ ├── Config-Server.yaml │ ├── Config-Adapter.yaml │ ├── Config-Guild.yaml │ └── Token.yaml ├── docs ├── WeXin.md ├── OneBotV11.md ├── stdin.md ├── QQGuild.md ├── Shamrock.md ├── CHANGELOG_qg.md ├── WeChat.md └── Lagrange.Core.md ├── .eslintrc.cjs ├── index.js ├── apps ├── clear_msgs.js ├── task.js ├── login.js ├── index.js ├── restart.js ├── help.js ├── master.js └── shamrock.js ├── CHANGELOG.md ├── package.json ├── model ├── render.js ├── version.js ├── help.js ├── config.js ├── shamrock │ ├── shamrock.js │ ├── client.js │ └── face.js └── YamlHandler.js ├── adapter ├── adapter.js ├── Bot │ └── icqq.js ├── QQBot │ ├── plugins.js │ └── QQSDK.js ├── WebSocket.js ├── WeChat │ ├── api.js │ ├── sendMsg.js │ └── message.js ├── QQGuild │ └── log.js └── shamrock │ ├── sendMsg.js │ └── xiaofei │ └── weather.js ├── lib ├── bot.js ├── init.js └── config │ └── config.js ├── README.md └── lain.support.js /plugins/button/.gitignore: -------------------------------------------------------------------------------- 1 | /lain.support.js 2 | *.js -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/help/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/help/icon.png -------------------------------------------------------------------------------- /resources/DAU/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/DAU/img/bg.jpg -------------------------------------------------------------------------------- /resources/admin/imgs/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/admin/imgs/bg.png -------------------------------------------------------------------------------- /resources/admin/imgs/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/admin/imgs/bg1.jpg -------------------------------------------------------------------------------- /resources/default_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/default_avatar.jpg -------------------------------------------------------------------------------- /resources/help/imgs/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/help/imgs/bg.jpg -------------------------------------------------------------------------------- /resources/help/imgs/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/help/imgs/main.png -------------------------------------------------------------------------------- /resources/common/bg/bg-geo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/bg/bg-geo.jpg -------------------------------------------------------------------------------- /resources/common/cont/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/cont/logo.png -------------------------------------------------------------------------------- /resources/common/font/NZBZ.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/font/NZBZ.ttf -------------------------------------------------------------------------------- /resources/common/font/华文中宋.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/font/华文中宋.TTF -------------------------------------------------------------------------------- /resources/common/bg/bg-anemo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/bg/bg-anemo.jpg -------------------------------------------------------------------------------- /resources/common/bg/bg-cryo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/bg/bg-cryo.jpg -------------------------------------------------------------------------------- /resources/common/bg/bg-dendro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/bg/bg-dendro.jpg -------------------------------------------------------------------------------- /resources/common/bg/bg-hydro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/bg/bg-hydro.jpg -------------------------------------------------------------------------------- /resources/common/bg/bg-pyro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/bg/bg-pyro.jpg -------------------------------------------------------------------------------- /resources/common/cont/card-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/cont/card-bg.png -------------------------------------------------------------------------------- /resources/common/font/NZBZ.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/font/NZBZ.woff -------------------------------------------------------------------------------- /resources/shamrock/img/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/shamrock/img/star.png -------------------------------------------------------------------------------- /resources/DAU/font/ruizizhenyan.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/DAU/font/ruizizhenyan.ttf -------------------------------------------------------------------------------- /resources/admin/imgs/cfg-right.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/admin/imgs/cfg-right.jpg -------------------------------------------------------------------------------- /resources/admin/imgs/cfg-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/admin/imgs/cfg-right.png -------------------------------------------------------------------------------- /resources/common/bg/bg-electro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/bg/bg-electro.jpg -------------------------------------------------------------------------------- /resources/common/font/HYWH-65W.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/font/HYWH-65W.ttf -------------------------------------------------------------------------------- /resources/common/font/HYWH-65W.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/font/HYWH-65W.woff -------------------------------------------------------------------------------- /resources/common/font/tttgbnumber.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/font/tttgbnumber.ttf -------------------------------------------------------------------------------- /resources/common/font/tttgbnumber.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/common/font/tttgbnumber.woff -------------------------------------------------------------------------------- /resources/shamrock/img/shamrock.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/shamrock/img/shamrock.webp -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | sharp_binary_host = https://npmmirror.com/mirrors/sharp 2 | sharp_libvips_binary_host = https://npmmirror.com/mirrors/sharp-libvips -------------------------------------------------------------------------------- /resources/shamrock/img/github-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowtafir/Lain-plugin/HEAD/resources/shamrock/img/github-logo-white.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config/* 3 | !config/defSet/ 4 | resources/image 5 | resources/QQBotApi 6 | resources/avatar.* 7 | plugins/button 8 | /.idea 9 | /src 10 | adapter/Bot/icqqs.js 11 | *.ts 12 | *.bak 13 | -------------------------------------------------------------------------------- /config/defSet/Config-Other.yaml: -------------------------------------------------------------------------------- 1 | # 定时清理缓存临时文件夹 每天凌晨4点 2 | DelFileCron: 0 4 * * * 3 | 4 | # ICQQ魔法文件 5 | ICQQtoFile: false 6 | 7 | # QQBotdau统计 8 | QQBotdau: true 9 | 10 | # URL白名单,在白名单中的链接不会转为二维码 11 | WhiteLink: 12 | - https://www.lain.com 13 | 14 | -------------------------------------------------------------------------------- /resources/common/base.less: -------------------------------------------------------------------------------- 1 | .font-YS { 2 | font-family: Number, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 3 | } 4 | 5 | .font-NZBZ { 6 | font-family: Number, "印品南征北战NZBZ体", NZBZ, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 7 | } -------------------------------------------------------------------------------- /resources/common/base.css: -------------------------------------------------------------------------------- 1 | .font-ys { 2 | font-family: Number, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 3 | } 4 | .font-nzbz { 5 | font-family: Number, "印品南征北战NZBZ体", NZBZ, PingFangSC-Medium, "PingFang SC", sans-serif; 6 | } 7 | /*# sourceMappingURL=base.css.map */ -------------------------------------------------------------------------------- /docs/WeXin.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | - 不限制服务器类型,能跑云崽即可使用。 4 | - 需要实名:`钱包` -> `身份信息` -> `实名验证` 5 | - #微信登陆 6 | - #微信账号 7 | - #微信删除 8 | 9 | 10 | ## 微信登陆 11 | 12 | 安装插件之后直接发送指令 `#微信登陆` 即可 13 | 14 | 15 | ## 其他 16 | 17 | 相较于PC微信的,网页版稳定性更高,基本能做到`99.99%`不封号,但是缺点是无固定id,账号离线5分钟后重新登陆id就会发送变化,无at功能。 -------------------------------------------------------------------------------- /config/defSet/Config-Server.yaml: -------------------------------------------------------------------------------- 1 | port: 2955 # HTTP端口,ComWeChat、Shamrock、QQBot临时文件使用 2 | baseIP: # 临时文件服务器IP,与下方 baseUrl 二选一,推荐公网使用上方2955端口的配置,可填域名:127.0.0.1 或 www.lain.com 3 | baseUrl: # 临时文件服务器访问url,QQBot使用。端口转发、域名等使用:http://www.lain.com 或 http://192.168.0.1:2956 4 | InvalidTime: 30 # 临时文件服务器失效时间,QQBot使用 5 | -------------------------------------------------------------------------------- /docs/OneBotV11.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | - 作为反向ws服务端,能跑云崽即可使用。 4 | - 当前 `YunzaiJS` 基础消息收发送测试初步通过。 5 | - 不保证所有 oneBotv11 标准实现的可用性,有事烧纸,不接受催更,仅欢迎pr完善。 6 | 7 | ## 连接方式 8 | 9 | 安装插件之后,在客户端反向ws连接添加: `ws://localhost:2955/OneBotV11` 即可,如自定义端口自行配置文件更换并更新链接地址端口 10 | 11 | 12 | ## 其他 13 | 14 | - OneBotv11 相关客户端的获取、使用,请遵守对应项目的使用范围和条款。 15 | -------------------------------------------------------------------------------- /resources/shamrock/img/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/shamrock/img/code-branch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/stdin.md: -------------------------------------------------------------------------------- 1 | # 标准输入 2 | - 作用:在控制台和在QQ一样执行指令,用于无法登录QQ情况下想执行指令。 3 | - 主人:`标准输入`默认为主人 4 | - 支持大部分基础指令,类似于锅巴登录等,支持保存图片、视频、语音至`data/stdin/`目录。 5 | 6 | # 如何使用 7 | 8 | 请直接把`控制台`当成您的QQ`输入指令`即可! 9 | 10 | # 自定义椰奶状态头像 11 | 12 | 如何使用:在`./resources`文件夹下方创建一个名称为`avatar.jpg`的图片 13 | 14 | 也可通过`config/config/Config-Adapter.yaml`中修改`Stdin.avatar`配置,`./resources/avatar.jpg`优先级大于`Stdin.avatar`;`Stdin.avatar`可选绝对路径或以崽目录开始相对路径 15 | 16 | # 自动更换椰奶状态头像 17 | 每次启动会从目录中随机选择一张图片作为椰奶状态头像 18 | 19 | 如何使用:先配置`Stdin.avatar`为`auto`,在`resources/Avatar/`目录放置头像图片即可 20 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true 5 | }, 6 | extends: ['standard'], 7 | parserOptions: { 8 | ecmaVersion: 'latest', 9 | sourceType: 'module' 10 | }, 11 | globals: { 12 | Bot: true, 13 | redis: true, 14 | logger: true, 15 | plugin: true, 16 | Renderer: true, 17 | segment: true, 18 | lain: true 19 | }, 20 | rules: { 21 | eqeqeq: ['off'], 22 | 'prefer-const': ['off'], 23 | 'arrow-body-style': 'off', 24 | camelcase: [0, { 25 | properties: 'always' 26 | }] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/help/imgs/config.js: -------------------------------------------------------------------------------- 1 | export const style = { 2 | // 主文字颜色 3 | fontColor: '#ceb78b', 4 | // 主文字阴影: 横向距离 垂直距离 阴影大小 阴影颜色 5 | // fontShadow: '0px 0px 1px rgba(6, 21, 31, .9)', 6 | fontShadow: 'none', 7 | // 描述文字颜色 8 | descColor: '#eee', 9 | 10 | /* 面板整体底色,会叠加在标题栏及帮助行之下,方便整体帮助有一个基础底色 11 | * 若无需此项可将rgba最后一位置为0即为完全透明 12 | * 注意若综合透明度较低,或颜色与主文字颜色过近或太透明可能导致阅读困难 */ 13 | contBgColor: 'rgba(6, 21, 31, .5)', 14 | 15 | // 面板底图毛玻璃效果,数字越大越模糊,0-10 ,可为小数 16 | contBgBlur: 3, 17 | 18 | // 板块标题栏底色 19 | headerBgColor: 'rgba(6, 21, 31, .4)', 20 | // 帮助奇数行底色 21 | rowBgColor1: 'rgba(6, 21, 31, .2)', 22 | // 帮助偶数行底色 23 | rowBgColor2: 'rgba(6, 21, 31, .35)' 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import './lib/init.js' 3 | import './lib/bot.js' 4 | import './model/config.js' 5 | import './adapter/adapter.js' 6 | import './adapter/Bot/bot.js' 7 | import './adapter/Bot/icqq.js' 8 | 9 | let ret = [] 10 | let apps = {} 11 | const files = fs.readdirSync('./plugins/Lain-plugin/apps').filter(file => file.endsWith('.js')) 12 | 13 | files.forEach((file) => { 14 | ret.push(import(`./apps/${file}`)) 15 | }) 16 | ret = await Promise.allSettled(ret) 17 | 18 | for (let i in files) { 19 | let name = files[i].replace('.js', '') 20 | if (ret[i].status != 'fulfilled') { 21 | logger.error(`载入插件错误:${logger.red(name)}`) 22 | logger.error(ret[i].reason) 23 | continue 24 | } 25 | 26 | // apps[name] = ret[i].value[Object.keys(ret[i].value)[0]] 27 | for (let clazz of Object.keys(ret[i].value)) { 28 | apps[clazz] = ret[i].value[clazz] 29 | } 30 | } 31 | 32 | export { apps } 33 | -------------------------------------------------------------------------------- /config/defSet/Config-Adapter.yaml: -------------------------------------------------------------------------------- 1 | Stdin: # 标准输入 2 | state: true # 标准输入开关 3 | name: "标准输入" # 标准输入名称 => 椰奶状态显示 4 | avatar: # 标准输入自定义头像路径,auto: 自动从resources/Avatar目录中选择一张 5 | 6 | ComWeChat: # PC微信 7 | name: # PC微信名称 => 椰奶状态显示 8 | autoFriend: 1 # 自动同意加好友 1-同意 0-不处理 9 | 10 | WeXin: # 网页版微信 11 | name: # 网页版微信名称 => 椰奶状态显示 12 | autoFriend: 1 # 自动同意加好友 1-同意 0-不处理 13 | 14 | Shamrock: # Shamrock 15 | baseUrl: # shamrock主动http链接,例如http://localhost:5700。若填写将通过此端口进行文件上传等被动ws不支持的操作 16 | token: # 鉴权token,如果开放公网强烈建议配置 17 | githubKey: # Github personal access token, 用于查看和下载shamrock版本信息 18 | 19 | # 反向ws连接: 20 | # port: 2955 查看 Config-Server.yaml 21 | # ws://localhost:2955/LagrangeCore 22 | # ws://localhost:2955/ComWeChat 23 | # ws://localhost:2955/shamrock 24 | # ws://localhost:2955/OneBotV11 25 | -------------------------------------------------------------------------------- /resources/common/layout/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | miao-plugin 12 | {{block 'css'}} 13 | {{/block}} 14 | 15 | 16 |
17 | {{block 'main'}}{{/block}} 18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /resources/common/layout/elem.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ws-plugin 12 | {{block 'css'}} 13 | {{/block}} 14 | 15 | 16 |
17 | {{block 'main'}}{{/block}} 18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /resources/help/version-info.html: -------------------------------------------------------------------------------- 1 | {{extend elemLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | {{/block}} 6 | 7 | {{block 'main'}} 8 | {{each changelogs ds idx}} 9 |
10 | {{set v = ds.version }} 11 | {{set isDev = v[v.length-1] === 'v'}} 12 |
13 | {{if idx === 0 }} 14 |
当前版本 {{v}}
15 | {{else}} 16 |
{{name || '铃音'}}版本 {{v}}
17 | {{/if}} 18 |
19 |
    20 | {{each ds.logs log}} 21 |
  • 22 |

    {{@log.title}}

    23 | {{if log.logs.length > 0}} 24 |
      25 | {{each log.logs ls}} 26 |
    • {{@ls}}
    • 27 | {{/each}} 28 |
    29 | {{/if}} 30 |
  • 31 | {{/each}} 32 |
33 |
34 |
35 |
36 | {{/each}} 37 | {{/block}} -------------------------------------------------------------------------------- /config/defSet/Config-Guild.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | ImageSize: 2.5 # 图片压缩阈值 3 | width: 1000 # 压缩后图片宽度像素大小 4 | quality: 100 # 压缩后的图片质量 5 | recallQR: 0 # 撤回url转换成二维码的时间(秒) 0表示不撤回 6 | 7 | blackGuild: # 黑名单频道 8 | - qg_6120782151088355109 9 | 10 | blackChannel: # 黑名单子频道 11 | - 514216167 12 | 13 | whiteGuild: # 白名单频道 14 | 15 | whiteChannel: # 白名单子频道 16 | 17 | '1234567': 18 | ImageSize: 2.5 # 图片压缩阈值 19 | width: 1000 # 压缩后图片宽度像素大小 20 | quality: 100 # 压缩后的图片质量 21 | recallQR: 0 # 撤回url转换成二维码的时间(秒) 0表示不撤回 22 | 23 | blackGuild: # 黑名单频道 24 | - qg_6120782151088355109 25 | 26 | blackChannel: # 黑名单子频道 27 | - 514216167 28 | 29 | whiteGuild: # 白名单频道 30 | 31 | whiteChannel: # 白名单子频道 32 | -------------------------------------------------------------------------------- /apps/clear_msgs.js: -------------------------------------------------------------------------------- 1 | import api from '../adapter/shamrock/api.js' 2 | 3 | export class clear_msgs extends plugin { 4 | constructor () { 5 | super({ 6 | name: '三叶草-清理缓存', 7 | priority: -50, 8 | rule: [ 9 | { 10 | reg: /^#清理缓存$/, 11 | fnc: 'clearFile', 12 | permission: 'master' 13 | } 14 | ] 15 | }) 16 | } 17 | 18 | async clearFile () { 19 | await this.reply('开始清理~,请等待完成', true, { at: true }) 20 | /** 获取群列表 */ 21 | const gl = this.e.bot.gl 22 | gl.forEach(async (value, key) => { 23 | try { 24 | await this.e.bot.api.clear_msgs(this.e.self_id, 'group', Number(key)) 25 | } catch (error) { 26 | this.reply(`清理群${key}发生错误:${error?.message || error}`) 27 | } 28 | }) 29 | 30 | /** 获取群列表 */ 31 | const fl = this.e.bot.fl 32 | fl.forEach(async (value, key) => { 33 | try { 34 | await api.clear_msgs(this.e.self_id, 'private', Number(key)) 35 | } catch (error) { 36 | this.reply(`清理好友${key}发生错误:${error?.message || error}`) 37 | } 38 | }) 39 | 40 | return await this.reply('清理完成~', true, { at: true }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.4.8 2 | * 修复 `oneBotV11` 转发消息支持 3 | * 合并 `sky-summer/Lain-plugin`的`master`分支 4 | 5 | # 1.4.7 6 | * add `oneBotV11` Adapter 7 | 8 | # 1.4.6 9 | * 适配`QQBot`私聊 10 | * 优化`QQBot`日志输出 11 | * 重构配置文件,重新适配锅巴 12 | * 本次改动较大,请谨慎更新 13 | * 优化`QQGuild`适配器 14 | 15 | # 1.4.5 16 | * 合并`QQbot`和`QQGuild`适配器 17 | * 资源更新,本次改动较大,请谨慎更新 18 | 19 | # 1.4.4 20 | 21 | * 添加`#shamrock版本`、`#shamrock(测试)安装包`**@ikechan8370** 22 | 23 | # 1.4.3 24 | 25 | * 添加`#铃音帮助`、`#铃音版本` **@小叶** 26 | * 小飞插件点歌、shamrock更多事件 **@ikechan8370** 27 | 28 | 29 | # 1.4.1 ~ 1.4.2 30 | 31 | * 1.4.2忘了... 32 | * 添加更多图片API 33 | * 点赞、ck、头衔等接口实现 **@ikechan8370** 34 | 35 | # 1.4.0 36 | 37 | * 添加`QQBot`适配器 38 | 39 | # 1.3.4 40 | 41 | * `Shamrock`:实现合并转发 42 | * `Shamrock`:获取历史记录、文件上传、清除本地缓存等接口实现 **@ikechan8370** 43 | 44 | # 1.3.3 45 | 46 | * `Shamrock`适配语音,整个适配器全部使用单独方法进行重启 47 | 48 | # 1.3.2 49 | 50 | * `Shamrock`适配椰奶引用撤回、踢人、禁言、修改群名称、设置管理、取消管理、适配meme表情包 51 | 52 | # 1.3.1 53 | 54 | * `Shamrock`适配拍一拍方法、适配icqq禁言、全体禁言接口,修改转发 55 | 56 | # 1.3.0 57 | 58 | * 添加`Shamrock`,QQ 59 | 60 | # 1.2.0 61 | 62 | * 添加`WeChat`,PC微信 63 | 64 | # 1.1.0 65 | 66 | * 添加`stdin`,控制台标准输入 67 | 68 | # 1.0.0 69 | 70 | * `QQGuild-plugin`合并到`Lain-plugin` 71 | -------------------------------------------------------------------------------- /resources/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | 14 | 15 | Lain-plugin 16 | 17 | 18 | 19 | 20 | 21 |
22 | {{if head }} {{@head}} {{/if}} 23 | 24 | {{each box i}} 25 | {{@i}} 26 | {{/each}} 27 | 28 | {{if copyright }} {{@copyright}} {{/if}} 29 |
30 |
?
31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/QQGuild.md: -------------------------------------------------------------------------------- 1 | `请给予机器人基础的权限...什么权限都没有的发个鬼消息啊= =` 2 | 3 | ## 1.获取频道机器人 4 | 5 | 前往 [QQ开放平台](https://q.qq.com/) -> 登录 -> 应用管理 -> 创建机器人 -> 创建完成 6 | 7 | 前往应用管理 -> 选择你注册的机器人 -> 开发 -> 开发设置 -> 获取`AppID(机器人ID)`、`Token(机器人令牌)`。 8 | 9 | ## 2.机器人指令配置 10 | 11 | 如果你没有在登录QQ,可以在控制台使用 [标准输入](./stdin.md) 来执行指令,直接像QQ一样输入指令! 12 | 13 | 添加机器人(删除机器人同理):**是=1 否=0** 14 | ``` 15 | #QQ频道设置 沙盒:私域:机器人ID:机器人令牌 16 | ``` 17 | 18 | 查看机器人: 19 | ``` 20 | #QQ频道账号 21 | ``` 22 | 23 | ## 使用例子 24 | 25 |
展开/收起 26 | 27 | 是否沙盒:`否` 28 | 29 | 是否私域:`是` 30 | 31 | AppID(机器人ID):`123456789` 32 | 33 | Token(机器人令牌):`abcdefghijklmnopqrstuvwxyz123456` 34 | 35 | 36 | 添加机器人: 37 | ``` 38 | #QQ频道设置 0:1:123456789:abcdefghijklmnopqrstuvwxyz123456 39 | ``` 40 | 41 | 删除机器人(相同指令): 42 | ``` 43 | #QQ频道设置 0:1:123456789:abcdefghijklmnopqrstuvwxyz123456 44 | ``` 45 | 46 | 查看机器人: 47 | ``` 48 | #QQ频道账号 49 | ``` 50 |
51 | 52 | ## 其他 53 | 54 | - 更换所有指令前缀:请编辑 [config/config.yaml](../config/config.yaml) 配置文件,开启将 `/` 转换为 `#` ,推荐使用锅巴设置 55 | - #QQ频道解除私信 `//解除私信3条后等待回复问题...机器人每天仅可发送两次私信主动消息` 56 | - #QQ频道设置分片转发开启 57 | - #QQ频道设置分片转发关闭 58 | - #id | #我的id | #我的信息 | #ID | #信息 `//获取个人id、频道id` 59 | - #id@用户` //可查看他人id` 60 | 61 | ## 已知问题 62 | 63 | 目前如果两个机器人在同一个频道,并且其中一个非管理员,在非管理员不可见的频道触发指令后,会导致管理员的机器人报错`11263`,根据官方文档,这是系统错误,暂无法解决。 64 | 65 | 更新日志:[点击查看](./CHANGELOG_qg.md) -------------------------------------------------------------------------------- /resources/help/index.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | <% style = style.replace(/{{_res_path}}/g, _res_path) %> 6 | {{@style}} 7 | {{/block}} 8 | 9 | {{block 'main'}} 10 | 11 |
12 |
13 |
{{helpCfg.title||"使用帮助"}}
14 |
{{helpCfg.subTitle || "Yunzai-Bot & Miao-Plugin"}}
15 |
16 |
17 | 18 | {{each helpGroup group}} 19 | {{set len = group?.list?.length || 0 }} 20 |
21 |
{{group.group}}
22 | {{if len > 0}} 23 |
24 |
25 | {{each group.list help idx}} 26 |
27 | 28 | {{help.title}} 29 | {{help.desc}} 30 |
31 | {{if idx%colCount === colCount-1 && idx>0 && idx< len-1}} 32 |
33 |
34 | {{/if}} 35 | {{/each}} 36 | <% for(let i=(len-1)%colCount; i< colCount-1 ; i++){ %> 37 |
38 | <% } %> 39 |
40 |
41 | {{/if}} 42 |
43 | {{/each}} 44 | {{/block}} -------------------------------------------------------------------------------- /apps/task.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import Cfg from '../lib/config/config.js' 3 | 4 | export class LainTask extends plugin { 5 | constructor () { 6 | super({ 7 | name: 'Lain-定时任务', 8 | priority: 0, 9 | rule: [ 10 | { 11 | reg: /^#?清理缓存$/, 12 | fnc: 'TaskFile' 13 | } 14 | ] 15 | }) 16 | 17 | /** 定时任务 */ 18 | this.task = [] 19 | for (let i of Array.isArray(Cfg.Other.DelFileCron) ? Cfg.Other.DelFileCron : [Cfg.Other.DelFileCron]) { 20 | this.task.push({ 21 | /** 任务cron表达式 */ 22 | cron: i, 23 | name: ' 清除临时文件', 24 | /** 任务方法名 */ 25 | fnc: () => this.TaskFile() 26 | }) 27 | } 28 | } 29 | 30 | TaskFile (e) { 31 | try { 32 | logger.mark(' <定时任务> 开始清理缓存文件') 33 | const _path = { 34 | './temp/FileToUrl': () => true, 35 | './resources/temp': (i) => i.endsWith('.silk'), 36 | './data/stdin': () => true 37 | } 38 | for (const i of Object.keys(_path)) { 39 | if (!fs.existsSync(i)) continue 40 | const files = fs.readdirSync(i) 41 | files.forEach(file => _path[i](file) && fs.promises.unlink(i + `/${file}`)) 42 | } 43 | logger.mark(' <定时任务> 清理缓存文件完成~') 44 | if (e?.reply) e.reply('清理缓存文件完成~') 45 | } catch (error) { 46 | logger.error(' <定时任务> 清理缓存文件发送错误:', error.message) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/Shamrock.md: -------------------------------------------------------------------------------- 1 | ### 温馨提示: 2 | - 目前`Shamrock`搭建难度较高,不推荐小白现阶段进行迁移。 3 | - 目前本插件正在跟随上游`Shamrock`高速更新,追求稳定更建议您当前迁移至`QQNT`~ 4 | 5 | ### 使用方法: 6 | 7 | - shamrock安装教程:[快速开始](https://whitechi73.github.io/OpenShamrock/guide/getting-started.html) 8 | - 安装好`Shamrock`并登录QQ之后,请打开`shamrock`,按照以下教程进行配置。 9 | - 启动`Shamrock`,打开`被动WebSocket` 10 | - 填写`被动WebSocket地址`:`ws://localhost:2955/Shamrock` 11 | - 彻底关闭`QQ`,注意需要彻底关闭 12 | - 彻底关闭`Shamrock`,注意需要彻底关闭 13 | - 启动`Shamrock` 14 | - 启动`QQ` 15 | - 启动喵崽即可 16 | 17 | 18 | 解释一下`ws://localhost:2955/Shamrock`这个地址 19 | - `ws://`这部分是固定的,无需更改 20 | - `localhost`这个是本地地址,如果你的喵崽在`云服务器`,请更换为云服务器的`公网IP地址` 21 | - `:2956`这部分是端口,需要使用`:`和`IP地址`连接起来,如需更改,请自行修改配置文件`config.yaml`或使用锅巴修改 22 | - `/Shamrock`这部分是固定的,无需更改 23 | 24 | 25 | 如果加载资源失败不想重启喵崽,可尝试使用`#重载资源`指令进行重新加载好友、群聊等。 26 | 27 | # 适配进度 28 | 29 | 没有注明的在下方的有需求并且我时间充裕的情况下会实现... 欢迎pr 30 | 31 | - [√] 接收`文本`、`表情`、`at`、`图片`、`语音`、`视频`、`文件`消息 32 | - [√] 发送`文本`、`表情`、`at`、`图片`、`语音`、`视频`、`戳一戳`消息 33 | - [√] 好友主动消息`Bot[BotQQ号].pickUser(user_id).sendMsg("主动消息")` 34 | - [√] 群聊主动消息`Bot[BotQQ号].pickGroup(group_id).sendMsg("主动消息")` 35 | - [√] 撤回消息 36 | - [ ] 临时会话消息收(√)发(×) 37 | - [√] 聊天记录 38 | - [√] 合并转发 39 | - [√] 禁言 40 | - [√] 戳一戳 41 | - [√] 点赞 42 | - [√] 群踢人、改群名片、设置精华消息、群头衔、设置群管理员 43 | - [√] 进退群、群撤回、禁言等事件 44 | - [√] cookie(`Bot[BotQQ号].cookies`)和bkn(`Bot[BotQQ号].bkn`) 45 | - [ ] OCR 46 | - [√] 查看群荣誉、群系统消息、群精华消息 47 | - [√] 发送天气、音乐、位置和分享卡片 48 | - [ ] 处理群申请和好友申请 49 | - [√] 上传和发送文件 50 | 51 | 如需使用`yenai-plugin`,请使用为`shamrock`专门适配的椰奶:[yenai-plugin](https://github.com/Zyy955/yenai-plugin) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lain-plugin", 3 | "version": "1.4.8", 4 | "author": "sky-summer, snowtafir Lain.", 5 | "gitee": "https://gitee.com/snowtafir/Lain-plugin.git", 6 | "type": "module", 7 | "scripts": { 8 | "commitmsg": "validate-commit-msg", 9 | "log": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1" 10 | }, 11 | "imports": { 12 | "#Lain": "./lib/init.js" 13 | }, 14 | "adapter": { 15 | "QQGuild": "0.5.3", 16 | "ComWeChat": "0.2.3", 17 | "CWeChatRobot": "0.0.8", 18 | "stdin": "0.1.3", 19 | "QQBot": "0.0.3", 20 | "WeXin": "0.0.3", 21 | "OneBotv11": "0.0.1", 22 | "ICQQ": "0.1.1" 23 | }, 24 | "dependencies": { 25 | "axios": "^1.7.7", 26 | "conventional-changelog-cli": "^5.0.0", 27 | "express": "^4.21.0", 28 | "file-type": "^19.5.0", 29 | "@karinjs/geturls": "^1.0.2", 30 | "icqq": "0.6.10", 31 | "get-urls": "^12.1.0", 32 | "qq-official-bot": "1.0.7", 33 | "qrcode": "^1.5.4", 34 | "sharp": "^0.33.5", 35 | "silk-wasm": "^3.6.3", 36 | "wechat4u": "^0.7.14", 37 | "ws": "^8.18.0", 38 | "ulid": "2.3.0", 39 | "node-fetch": "3.3.2" 40 | }, 41 | "devDependencies": { 42 | "cz-conventional-changelog": "^3.3.0", 43 | "eslint": "^9.9.0", 44 | "eslint-config-standard": "^17.1.0", 45 | "eslint-plugin-import": "^2.31.0", 46 | "eslint-plugin-n": "^16.0.0", 47 | "eslint-plugin-promise": "^6.0.0", 48 | "express-art-template": "^1.0.1", 49 | "husky": "^9.1.6", 50 | "validate-commit-msg": "^2.14.0" 51 | }, 52 | "config": { 53 | "commitizen": { 54 | "path": "./node_modules/cz-conventional-changelog" 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /docs/CHANGELOG_qg.md: -------------------------------------------------------------------------------- 1 | # 0.5.2 2 | 3 | * 合并到`Lain-plugin` 4 | 5 | # 0.5.1 6 | 7 | * 兼容`ws-plugin` 8 | * 设置默认镜像源 9 | * 修复已知bug 10 | 11 | # 0.5.0 12 | 13 | * 适配锅巴 14 | 15 | # 0.4.8 16 | 17 | * 增加QQ频道、子频道黑白名单 18 | 19 | # 0.4.7 20 | 21 | * 增加在url转为二维码图片后,默认等待20秒后撤回,可关闭。 22 | 23 | # 0.4.6 24 | 25 | * 增加`sharp`,对过大的图像进行压缩,可自行调整参数。 26 | 27 | # 0.4.5 28 | 29 | * 修复`设置主人`指令在at已经是主人的用户时,依旧将其设置为主人 30 | 31 | # 0.4.4 32 | 33 | * 修复`前缀转换`在公域场景下失效问题 34 | * 修复指令添加机器人失败问题 35 | 36 | # 0.4.3 37 | 38 | * 适配椰奶的`撤回消息`,不支持私信 39 | 40 | # 0.4.2 41 | 42 | * 增加`#QQ频道账号`每个机器人前面显示名称 43 | * 增加在`README.md`中的机器人注册基本流程 44 | * 添加`CHANGELOG.md` 45 | 46 | # 0.4.1 47 | 48 | * 对`url替换`进行二次检测是否为`url链接` 49 | * 增加`二维码渲染模板`、转换的同时保持显示原url 50 | * 修复`设置主人`成功后无法输出提示词 51 | * 修复存在多个bot时,重启提示发送错误 52 | * 修复`appID`指向错误问题 53 | 54 | # 0.4.0 55 | 56 | * 第三次重构插件 57 | * 增加前缀`/`转换`#` 58 | * 修改分片转发为默认开启 59 | * 重写`Yaml`文件的添加、修改方式 60 | 61 | # 0.3.0 ~ 0.3.9 62 | 63 | * 取消接收图片后进行缓存 64 | * 修改`设置主人` 65 | * 将`设置成功的主人id`添加到最后一行 66 | * 增加`取消主人`功能 67 | * 修改`分片发送`延迟 68 | * 适配`chatgpt-plugin`的转发,修改分片转发为默认开启 69 | * 适配`椰奶状态` 70 | * 给`频道id`和`个人id`统一添加前缀`qg_` 71 | * 增加`米游社推送` 72 | * 修复`公域机器人`无法解除私信 73 | * 二次优化插件结构 74 | * 增加在`无权获取频道列表`场景下的支持 75 | * 适配`yenai-plugin`违禁词、踢出、禁言、解禁 76 | * 适配`chatgpt-plugin` 77 | * 适配`喵喵维护版云崽` 78 | * 劫持本体所有转发消息的处理方法 79 | * 适配`l-plugin`插件的塔罗牌显示效果 80 | * 去除`压缩图像依赖`、无科学上网用户安装失败 81 | * 增加`链接转二维码`功能 82 | * 修改`压缩图像依赖`为可选安装 83 | * 增加对`没有进行备案的url`进行`关键词替换`使其可进行发送 84 | 85 | # 0.2.0 86 | 87 | * 优化插件结构 88 | * 适配套娃转发 89 | * 增加`控制台执行#QQ频道设置...` 90 | * 适配`xiaoyao-cvs-plugin`转发消息 91 | * 增加`toString`方法 92 | * 增加一些基础功能 93 | * 增加`图像压缩`功能 94 | * 增加指令`#QQ频道更新` 95 | * 增加指令`#QQ频道解除私信` 96 | 97 | # 0.1.0 98 | 99 | * 初版发布 100 | -------------------------------------------------------------------------------- /model/render.js: -------------------------------------------------------------------------------- 1 | import Version from './version.js' 2 | 3 | function scale (pct = 1) { 4 | let scale = 100 5 | scale = Math.min(2, Math.max(0.5, scale / 100)) 6 | pct = pct * scale 7 | return `style=transform:scale(${pct})` 8 | } 9 | 10 | const Render = { 11 | async render (path, params, cfg) { 12 | let { e } = cfg 13 | if (!e.runtime) { 14 | console.log('未找到e.runtime,请升级至最新版Yunzai') 15 | } 16 | 17 | let BotName = 'Miao-Yunzai' 18 | return e.runtime.render('Lain-plugin', path, params, { 19 | retType: cfg.retMsgId ? 'msgId' : 'default', 20 | beforeRender ({ data }) { 21 | let pluginName = '' 22 | if (data.pluginName !== false) { 23 | pluginName = ` & ${data.pluginName || 'Lain-plugin'}` 24 | if (data.pluginVersion !== false) { 25 | pluginName += `${data.pluginVersion || Version.version}` 26 | } 27 | } 28 | let resPath = data.pluResPath 29 | const layoutPath = process.cwd() + '/plugins/Lain-plugin/resources/common/layout/' 30 | return { 31 | ...data, 32 | _res_path: resPath, 33 | _ws_path: resPath, 34 | _layout_path: layoutPath, 35 | _tpl_path: process.cwd() + '/plugins/Lain-plugin/resources/common/tpl/', 36 | defaultLayout: layoutPath + 'default.html', 37 | elemLayout: layoutPath + 'elem.html', 38 | sys: { 39 | scale: scale(cfg.scale || 1) 40 | }, 41 | copyright: `Created By ${BotName}${Version.yunzai}${pluginName}`, 42 | pageGotoParams: { 43 | waitUntil: 'networkidle2' 44 | } 45 | } 46 | } 47 | }) 48 | } 49 | } 50 | 51 | export default Render 52 | -------------------------------------------------------------------------------- /resources/admin/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | transform: scale(1); 3 | width: 660px; 4 | } 5 | .container { 6 | background: url("./imgs/bg1.jpg") #6364a7 left top no-repeat; 7 | background-size: 700px auto; 8 | width: 660px; 9 | } 10 | .head-box { 11 | margin: 0 0 80px 0; 12 | } 13 | .cfg-box { 14 | border-radius: 15px; 15 | margin-top: 20px; 16 | margin-bottom: 20px; 17 | padding: 5px 15px; 18 | overflow: hidden; 19 | background: #f5f5f5; 20 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15); 21 | position: relative; 22 | background: rgba(35, 38, 57, 0.65); 23 | } 24 | .cfg-group { 25 | color: #ceb78b; 26 | font-size: 18px; 27 | font-weight: bold; 28 | padding: 10px 20px; 29 | } 30 | .cfg-li { 31 | border-radius: 18px; 32 | min-height: 36px; 33 | position: relative; 34 | overflow: hidden; 35 | margin-bottom: 10px; 36 | background: rgba(203, 196, 190, 0); 37 | } 38 | .cfg-line { 39 | color: #4e5769; 40 | line-height: 36px; 41 | padding-left: 20px; 42 | font-weight: bold; 43 | border-radius: 16px; 44 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); 45 | background: url("./imgs/cfg-right.jpg") right top #cbc4be no-repeat; 46 | background-size: auto 36px; 47 | } 48 | .cfg-hint { 49 | font-size: 12px; 50 | font-weight: normal; 51 | margin-top: 3px; 52 | margin-bottom: -3px; 53 | } 54 | .cfg-status { 55 | position: absolute; 56 | top: 0; 57 | right: 0; 58 | height: 36px; 59 | width: 160px; 60 | text-align: center; 61 | line-height: 36px; 62 | font-size: 16px; 63 | color: #495366; 64 | font-weight: bold; 65 | border-radius: 0 16px 16px 0; 66 | } 67 | .cfg-status.status-off { 68 | color: #a95151; 69 | } 70 | .cfg-desc { 71 | font-size: 12px; 72 | color: #cbc4be; 73 | margin: 5px 0 5px 20px; 74 | } 75 | /*# sourceMappingURL=index.css.map */ -------------------------------------------------------------------------------- /resources/help/version-info.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | user-select: none; 6 | } 7 | body { 8 | font-size: 18px; 9 | color: #1e1f20; 10 | transform: scale(1.3); 11 | transform-origin: 0 0; 12 | width: 600px; 13 | } 14 | .container { 15 | width: 600px; 16 | padding: 10px 0 10px 0; 17 | background-size: 100% 100%; 18 | } 19 | .log-cont { 20 | background-size: cover; 21 | margin: 5px 15px 5px 10px; 22 | border-radius: 10px; 23 | } 24 | .log-cont .cont { 25 | margin: 0; 26 | } 27 | .log-cont .cont-title { 28 | font-size: 16px; 29 | padding: 10px 20px 6px; 30 | } 31 | .log-cont .cont-title.current-version { 32 | font-size: 20px; 33 | } 34 | .log-cont ul { 35 | font-size: 14px; 36 | padding-left: 20px; 37 | } 38 | .log-cont ul li { 39 | margin: 3px 0; 40 | } 41 | .log-cont ul.sub-log-ul li { 42 | margin: 1px 0; 43 | } 44 | .log-cont .cmd { 45 | color: #d3bc8e; 46 | display: inline-block; 47 | border-radius: 3px; 48 | background: rgba(0, 0, 0, 0.5); 49 | padding: 0 3px; 50 | margin: 1px 2px; 51 | } 52 | .log-cont .strong { 53 | color: #24d5cd; 54 | } 55 | .log-cont .new { 56 | display: inline-block; 57 | width: 18px; 58 | margin: 0 -3px 0 1px; 59 | } 60 | .log-cont .new:before { 61 | content: "NEW"; 62 | display: inline-block; 63 | transform: scale(0.6); 64 | transform-origin: 0 0; 65 | color: #d3bc8e; 66 | white-space: nowrap; 67 | } 68 | .dev-cont { 69 | background: none; 70 | } 71 | .dev-cont .cont-title { 72 | background: rgba(0, 0, 0, 0.7); 73 | } 74 | .dev-cont .cont-body { 75 | background: rgba(0, 0, 0, 0.5); 76 | } 77 | .dev-cont .cont-body.dev-info { 78 | background: rgba(0, 0, 0, 0.2); 79 | } 80 | .dev-cont .strong { 81 | font-size: 15px; 82 | } 83 | /*# sourceMappingURL=version-info.css.map */ -------------------------------------------------------------------------------- /config/defSet/Token.yaml: -------------------------------------------------------------------------------- 1 | # 示例配置 2 | QQ_Default: 3 | "default": 4 | mode: "websocket" # 运行模式 websocket webhook middleware 5 | port: 0 # webhook监听端口 0-跟随Lain-plugin 6 | path: "/webhook" # webhook监听路径 7 | applacation: "express" # 中间件 express koa 8 | type: 0 # 0-全部启用 1-仅启用QQ频道 2-仅启用QQ群Bot 3-不启用 9 | appid: "default" # 机器人id 10 | sandbox: false # 沙盒 true-开启 false-关闭 11 | timeout: 10000 # 请求接口超时时间,默认10秒 12 | allMsg: true # QQ频道接收全部消息 true-私域 false-公域 13 | removeAt: false # 移除at true-开启 false-关闭 14 | token: # 机器人令牌 15 | secret: # 机器人密钥 16 | maxRetry: 10 # 重连次数 17 | autoRetry: true # 掉线自动重连 true-开启 false-关闭 18 | autoRetryTime: 60 # 掉线自动重连间隔,默认60秒 19 | markdown: # QQBot高阶能力 20 | id: # 模板ID 21 | type: 0 # 0-关闭 1-全局 2-正则模式 3-按钮模式 详情请查看文档 22 | text: text_start # markdown模板文字键 23 | img_dec: img_dec # markdown模板图片宽高键 24 | img_url: img_url # markdown模板图片url键 25 | other: # 其他配置 26 | Prefix: true # QQ频道、QQ群Bot前缀转换 [/] => [#] true-开启 false-关闭 27 | QQCloud: # QQ群Bot => QQ图床,填写QQ号。需使用QQ发送图片 28 | Tips: false # QQ群Bot => 进入新群后,发送防倒卖提示 true-开启 false-关闭 29 | Tips-GroupId: # QQ群Bot => 防倒卖提示中的QQ群号 30 | 31 | QQ_Token: -------------------------------------------------------------------------------- /resources/admin/index.less: -------------------------------------------------------------------------------- 1 | body { 2 | transform: scale(1); 3 | width: 660px; 4 | } 5 | 6 | .container { 7 | background: url("./imgs/bg1.png") #000144 left top no-repeat; 8 | background-size: 700px auto; 9 | width:660px; 10 | } 11 | 12 | .head-box { 13 | margin: 0 0 80px 0; 14 | } 15 | 16 | .cfg-box { 17 | border-radius: 15px; 18 | margin-top: 20px; 19 | margin-bottom: 20px; 20 | padding: 5px 15px; 21 | overflow: hidden; 22 | background: #f5f5f5; 23 | box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); 24 | position: relative; 25 | background: rgba(35, 38, 57, .8); 26 | } 27 | 28 | 29 | .cfg-group { 30 | color: #ceb78b; 31 | font-size: 18px; 32 | font-weight: bold; 33 | padding: 10px 20px; 34 | } 35 | 36 | .cfg-ul { 37 | 38 | } 39 | 40 | .cfg-li { 41 | border-radius: 18px; 42 | min-height: 36px; 43 | position: relative; 44 | overflow: hidden; 45 | margin-bottom: 10px; 46 | background: rgba(203, 196, 190, 0); 47 | } 48 | 49 | .cfg-line { 50 | color: #4e5769; 51 | line-height: 36px; 52 | padding-left: 20px; 53 | font-weight: bold; 54 | border-radius: 16px; 55 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); 56 | background: url("./imgs/cfg-right.jpg") right top #cbc4be no-repeat; 57 | background-size: auto 36px; 58 | } 59 | 60 | .cfg-hint { 61 | font-size: 12px; 62 | font-weight: normal; 63 | margin-top: 3px; 64 | margin-bottom: -3px; 65 | } 66 | 67 | .cfg-status { 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 36px; 72 | width: 160px; 73 | text-align: center; 74 | line-height: 36px; 75 | font-size: 16px; 76 | color: #495366; 77 | font-weight: bold; 78 | border-radius: 0 16px 16px 0; 79 | } 80 | 81 | .cfg-status.status-off { 82 | color: #a95151; 83 | } 84 | 85 | .cfg-desc { 86 | font-size: 12px; 87 | color: #cbc4be; 88 | margin: 5px 0 5px 20px; 89 | } -------------------------------------------------------------------------------- /docs/WeChat.md: -------------------------------------------------------------------------------- 1 | 微信应用端只支持在`Windows`环境运行,仅支持`Miao-Yunzai` 2 | 3 | 4 | ## 温馨提示 5 | 6 | 没有`Windows`环境,请使用[WeXin.md](./WeXin.md) 7 | 8 | # 使用必读 9 | 10 | 应用端和云崽都可以单独启动,并没有必须先启动谁的说法 11 | 12 | 应用端显示`远程计算机拒绝访问`是因为云崽这边没有启动或者没有安装插件。 13 | 14 | ## 1.下载微信 15 | 仅支持`3.7.0.30`版本,[点击下载](https://ghproxy.com/https://github.com/tom-snow/wechat-windows-versions/releases/download/v3.7.0.30/WeChatSetup-3.7.0.30.exe) 16 | 17 | 如果担心和电脑现有的高版本冲突可在下载安装包之后`直接解压exe安装包`,运行`WeChat.exe`即可 18 | 19 | ## 2.下载禁用更新补丁 20 | 21 | [点击跳转下载页面](https://cup.lanzoui.com/pcwxnoupdate),下载后启动禁用即可 22 | 23 | ## 3.微信机器人应用端 24 | 25 | 两个下载源任选其一 26 | 27 | [点击下载(无加速环境推荐使用)](https://ghproxy.com/https://github.com/JustUndertaker/ComWeChatBotClient/releases/download/v0.0.8/ComWeChat-Client-v0.0.8.zip) 28 | 29 | [初始源](https://github.com/JustUndertaker/ComWeChatBotClient/releases/v0.0.8) 30 | 31 | 下载后得到`ComWeChat-Client-v0.0.8.zip` 32 | 33 | 解压`ComWeChat-Client-v0.0.8.zip` 34 | 35 | #### 请严格按照我所给出的配置进行修改! 36 | 37 | 使用记事本打开`.env`文件,需要修改两个配置 38 | ``` 39 | websocekt_type = "Unable" 40 | 修改为 41 | websocekt_type = "Backward" 42 | 43 | 44 | websocket_url = ["ws://127.0.0.1:8080/onebot/v12/ws/"] 45 | 修改为 46 | websocket_url = ["ws://localhost:2955/ComWeChat"] 47 | 48 | 可选: 49 | `如果经常发生连接已关闭,请增加缓冲区大小`` 50 | # 反向 WebSocket 的缓冲区大小,单位(Mb) 51 | websocket_buffer_size = 4 52 | 53 | ``` 54 | 修改完成保存 55 | 56 | 57 | ## 5.管理员运行`install.bat` 58 | 59 | 注:如运行`install.bat`报错或者`闪退`,如下: 60 | ![报错](https://user-images.githubusercontent.com/74231782/230714709-95faea89-ac18-44fb-a704-fb114c675800.png) 61 | 62 | 请安装[vc_redist.x86](https://download.microsoft.com/download/6/D/F/6DF3FF94-F7F9-4F0B-838C-A328D1A7D0EE/vc_redist.x86.exe) 63 | 64 | ## 6.管理员启动应用端 65 | 66 | 管理员运行`ComWeChat-Client-v0.0.8.exe`随后登录你的微信小号即可 67 | 68 | 69 | ## 其他 70 | 71 | - 好友申请: 72 | - 推荐使用锅巴设置,默认自动通过好友申请,如果关闭请前往配置修改`plugins/WeChat-plugin/config.yaml` 73 | 74 | - 修改椰奶状态显示名称 75 | - `#微信修改名称<新名称>` -------------------------------------------------------------------------------- /resources/help/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | transform: scale(1); 3 | width: 830px; 4 | background: url("../common/theme/bg-01.jpg"); 5 | } 6 | .container { 7 | background: url(../common/theme/main-01.png) top left no-repeat; 8 | background-size: 100% auto; 9 | width: 830px; 10 | } 11 | .head-box { 12 | margin: 60px 0 0 0; 13 | padding-bottom: 0; 14 | } 15 | .head-box .title { 16 | font-size: 50px; 17 | } 18 | .cont-box { 19 | border-radius: 15px; 20 | margin-top: 20px; 21 | margin-bottom: 20px; 22 | overflow: hidden; 23 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15); 24 | position: relative; 25 | } 26 | .help-group { 27 | font-size: 18px; 28 | font-weight: bold; 29 | padding: 15px 15px 10px 20px; 30 | } 31 | .help-table { 32 | text-align: center; 33 | border-collapse: collapse; 34 | margin: 0; 35 | border-radius: 0 0 10px 10px; 36 | display: table; 37 | overflow: hidden; 38 | width: 100%; 39 | color: #fff; 40 | } 41 | .help-table .tr { 42 | display: table-row; 43 | } 44 | .help-table .td, 45 | .help-table .th { 46 | font-size: 14px; 47 | display: table-cell; 48 | box-shadow: 0 0 1px 0 #888 inset; 49 | padding: 12px 0 12px 50px; 50 | line-height: 24px; 51 | position: relative; 52 | text-align: left; 53 | } 54 | .help-table .tr:last-child .td { 55 | padding-bottom: 12px; 56 | } 57 | .help-table .th { 58 | background: rgba(34, 41, 51, 0.5); 59 | } 60 | .help-icon { 61 | width: 40px; 62 | height: 40px; 63 | display: block; 64 | position: absolute; 65 | background: url("icon.png") 0 0 no-repeat; 66 | background-size: 500px auto; 67 | border-radius: 5px; 68 | left: 6px; 69 | top: 12px; 70 | transform: scale(0.85); 71 | } 72 | .help-title { 73 | display: block; 74 | color: #d3bc8e; 75 | font-size: 16px; 76 | line-height: 24px; 77 | } 78 | .help-desc { 79 | display: block; 80 | font-size: 13px; 81 | line-height: 18px; 82 | } 83 | /*# sourceMappingURL=index.css.map */ -------------------------------------------------------------------------------- /resources/help/index.less: -------------------------------------------------------------------------------- 1 | body { 2 | transform: scale(1); 3 | width: 830px; 4 | background: url("../common/theme/bg-01.jpg"); 5 | } 6 | 7 | .container { 8 | background: url(../common/theme/main-01.png) top left no-repeat; 9 | background-size: 100% auto; 10 | width: 830px; 11 | } 12 | 13 | .head-box { 14 | margin: 60px 0 0 0; 15 | padding-bottom: 0; 16 | } 17 | 18 | .head-box .title { 19 | font-size: 50px; 20 | } 21 | 22 | .cont-box { 23 | border-radius: 15px; 24 | margin-top: 20px; 25 | margin-bottom: 20px; 26 | overflow: hidden; 27 | box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); 28 | position: relative; 29 | } 30 | 31 | .help-group { 32 | font-size: 18px; 33 | font-weight: bold; 34 | padding: 15px 15px 10px 20px; 35 | } 36 | 37 | .help-table { 38 | text-align: center; 39 | border-collapse: collapse; 40 | margin: 0; 41 | border-radius: 0 0 10px 10px; 42 | display: table; 43 | overflow: hidden; 44 | width: 100%; 45 | color: #fff; 46 | } 47 | 48 | .help-table .tr { 49 | display: table-row; 50 | } 51 | 52 | .help-table .td, 53 | .help-table .th { 54 | font-size: 14px; 55 | display: table-cell; 56 | box-shadow: 0 0 1px 0 #888 inset; 57 | padding: 12px 0 12px 50px; 58 | line-height: 24px; 59 | position: relative; 60 | text-align: left; 61 | } 62 | 63 | .help-table .tr:last-child .td { 64 | padding-bottom: 12px; 65 | } 66 | 67 | .help-table .th { 68 | background: rgba(34, 41, 51, .5) 69 | } 70 | 71 | .help-icon { 72 | width: 40px; 73 | height: 40px; 74 | display: block; 75 | position: absolute; 76 | background: url("icon.png") 0 0 no-repeat; 77 | background-size: 500px auto; 78 | border-radius: 5px; 79 | left: 6px; 80 | top: 12px; 81 | transform: scale(0.85); 82 | } 83 | 84 | .help-title { 85 | display: block; 86 | color: #d3bc8e; 87 | font-size: 16px; 88 | line-height: 24px; 89 | } 90 | 91 | .help-desc { 92 | display: block; 93 | font-size: 13px; 94 | line-height: 18px; 95 | } 96 | 97 | -------------------------------------------------------------------------------- /adapter/adapter.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | import Cfg from '../lib/config/config.js' 4 | import WebSocket from './WebSocket.js' 5 | import stdin from './stdin/index.js' 6 | import QQSDK from './QQBot/QQSDK.js' 7 | import QQBot from './QQBot/index.js' 8 | import QQGuild from './QQBot/QQGuild.js' 9 | import WeChat4u from './WeChat-Web/index.js' 10 | 11 | /** 启动HTTP服务器,加载shamrock、Com微信适配器 */ 12 | WebSocket.start() 13 | 14 | /** 加载标准输入 */ 15 | if (Cfg.Stdin.state) stdin() 16 | 17 | /** QQBot适配器 */ 18 | const QQ_Token = Cfg.getToken('QQ_Token') 19 | if (QQ_Token && Object.keys(QQ_Token).length) { 20 | Object.keys(QQ_Token).forEach(async id => { 21 | if (id !== 'default') { 22 | let bot = Cfg.getToken('QQ_Token', id) 23 | if (bot.type == 0 || bot.type == 2) { 24 | try { 25 | const SDK = new QQSDK(bot) 26 | await SDK.start() 27 | lain.info(SDK.id, await new QQBot(SDK.sdk)) 28 | lain.info(SDK.id, await new QQGuild(SDK.sdk)) 29 | } catch (err) { 30 | lain.error('Lain-plugin', `QQBot <${bot.appid}> 启动失败`, err) 31 | } 32 | } 33 | if (bot.type == 1) { 34 | try { 35 | const SDK = new QQSDK(bot) 36 | await SDK.start() 37 | lain.info(SDK.id, await new QQGuild(SDK.sdk)) 38 | } catch (err) { 39 | lain.error('Lain-plugin', `QQGuild <${bot.appid}> 启动失败`, err) 40 | } 41 | } 42 | } 43 | }) 44 | } 45 | 46 | /** 加载微信 */ 47 | const _path = fs.readdirSync('./plugins/Lain-plugin/config') 48 | const JSONFile = _path.filter(file => file.endsWith('.json')) 49 | if (JSONFile.length > 0) { 50 | JSONFile.forEach(async i => { 51 | const id = i.replace(/\.json$/gi, '') 52 | try { 53 | await new WeChat4u(id, i) 54 | } catch (error) { 55 | lain.error('Lain-plugin', `微信 ${id} 登录失败`, error) 56 | } 57 | }) 58 | } 59 | 60 | lain.info('Lain-plugin', `Lain-plugin插件${Bot.lain.version}全部初始化完成~`) 61 | lain.info('Lain-plugin', 'https://gitee.com/snowtafir/Lain-plugin') 62 | -------------------------------------------------------------------------------- /resources/help/version-info.less: -------------------------------------------------------------------------------- 1 | .linear-bg(@color) { 2 | background-image: linear-gradient(to right, @color, @color 80%, fade(@color, 0) 100%); 3 | } 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | user-select: none; 10 | } 11 | 12 | body { 13 | font-size: 18px; 14 | color: #1e1f20; 15 | transform: scale(1.3); 16 | transform-origin: 0 0; 17 | width: 600px; 18 | } 19 | 20 | .container { 21 | width: 600px; 22 | padding: 10px 0 10px 0; 23 | background-size: 100% 100%; 24 | 25 | } 26 | 27 | .log-cont { 28 | background-size: cover; 29 | margin: 5px 15px 5px 10px; 30 | border-radius: 10px; 31 | 32 | .cont { 33 | margin: 0; 34 | } 35 | 36 | .cont-title { 37 | font-size: 16px; 38 | padding: 10px 20px 6px; 39 | 40 | &.current-version { 41 | font-size: 20px; 42 | } 43 | } 44 | 45 | .cont-body { 46 | } 47 | 48 | ul { 49 | font-size: 14px; 50 | padding-left: 20px; 51 | 52 | li { 53 | margin: 3px 0; 54 | } 55 | 56 | &.sub-log-ul { 57 | li { 58 | margin: 1px 0; 59 | } 60 | } 61 | } 62 | 63 | .cmd { 64 | color: #d3bc8e; 65 | display: inline-block; 66 | border-radius: 3px; 67 | background: rgba(0, 0, 0, 0.5); 68 | padding: 0 3px; 69 | margin: 1px 2px; 70 | } 71 | 72 | .strong { 73 | color: #24d5cd; 74 | } 75 | 76 | .new { 77 | display: inline-block; 78 | width: 18px; 79 | margin: 0 -3px 0 1px; 80 | } 81 | 82 | .new:before { 83 | content: "NEW"; 84 | display: inline-block; 85 | transform: scale(0.6); 86 | transform-origin: 0 0; 87 | color: #d3bc8e; 88 | white-space: nowrap; 89 | } 90 | } 91 | 92 | .dev-cont { 93 | background: none; 94 | 95 | .cont-title { 96 | background: rgba(0, 0, 0, .7); 97 | } 98 | 99 | .cont-body { 100 | background: rgba(0, 0, 0, .5); 101 | 102 | &.dev-info { 103 | background: rgba(0, 0, 0, .2); 104 | } 105 | } 106 | 107 | .strong { 108 | font-size: 15px; 109 | } 110 | } -------------------------------------------------------------------------------- /lib/bot.js: -------------------------------------------------------------------------------- 1 | const CopyBot = Bot 2 | 3 | const botMethods = { 4 | pickGroup, 5 | pickFriend, 6 | pickMember, 7 | pickUser: pickFriend 8 | } 9 | 10 | Bot = new Proxy({}, { 11 | get (target, prop, receiver) { 12 | if (prop in botMethods) return botMethods[prop] 13 | if (prop in target) return target[prop] 14 | return Reflect.get(CopyBot, prop, receiver) 15 | } 16 | }) 17 | 18 | /** 19 | * 得到一个群对象, 通常不会重复创建、调用 20 | * @param gid 群号 21 | * @param strict 严格模式,若群不存在会抛出异常 22 | * @returns 一个`Group`对象 23 | */ 24 | function pickGroup (gid, strict) { 25 | gid = Number(gid) || String(gid) 26 | const group = Bot.gl.get(gid) 27 | if (group) return Bot[group.uin || Bot.uin].pickGroup(gid, strict) 28 | if (Number(gid)) return Bot[Bot.uin].pickGroup(gid, strict) 29 | logger.error(`获取群对象错误:找不到群 ${logger.red(gid)}`) 30 | } 31 | 32 | /** 33 | * 得到一个好友对象, 通常不会重复创建、调用 34 | * @param uid 好友账号 35 | * @param strict 严格模式,若好友不存在会抛出异常 36 | * @returns 一个`Friend`对象 37 | */ 38 | function pickFriend (uid, strict) { 39 | uid = Number(uid) || String(uid) 40 | const user = Bot.fl.get(uid) 41 | if (user) return Bot[user.uin || Bot.uin].pickFriend(uid, strict) 42 | if (Number(uid)) return Bot[Bot.uin].pickFriend(uid, strict) 43 | logger.error(`获取好友对象错误:找不到好友 ${logger.red(uid)}`) 44 | } 45 | 46 | /** 47 | * 得到一个群员对象, 通常不会重复创建、调用 48 | * @param gid 群员所在的群号 49 | * @param uid 群员的账号 50 | * @param strict 严格模式,若群员不存在会抛出异常 51 | * @returns 一个`Member`对象 52 | */ 53 | function pickMember (gid, uid, strict) { 54 | if (uid == 88888) { 55 | let nickname = 'Yunzai-Bot' 56 | return { 57 | group_id: gid, 58 | user_id: uid, 59 | nickname, 60 | card: nickname, 61 | sex: 'female', 62 | age: 6, 63 | join_time: '', 64 | last_sent_time: '', 65 | level: 1, 66 | role: 'member', 67 | title: '', 68 | title_expire_time: '', 69 | shutup_time: 0, 70 | update_time: '', 71 | area: '南极洲', 72 | rank: '潜水' 73 | } 74 | } 75 | const group = Bot.pickGroup(gid, strict) 76 | if (group) return group.pickMember(uid) 77 | logger.error(`获取群员对象错误:从群 ${logger.red(gid)} 中找不到群员 ${logger.red(uid)}`) 78 | } 79 | -------------------------------------------------------------------------------- /apps/login.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import StartWeChat4u from '../adapter/WeChat-Web/index.js' 3 | 4 | export class WebWcChat extends plugin { 5 | constructor () { 6 | super({ 7 | name: '微信', 8 | dsc: '网页版微信机器人', 9 | event: 'message', 10 | priority: 1, 11 | rule: [ 12 | { 13 | reg: '^#微信登(录|陆)$', 14 | fnc: 'login', 15 | permission: 'master' 16 | }, 17 | { 18 | reg: '^#微信账号$', 19 | fnc: 'account', 20 | permission: 'master' 21 | }, 22 | { 23 | reg: '^#微信删除.*$', 24 | fnc: 'delUser', 25 | permission: 'master' 26 | } 27 | ] 28 | }) 29 | } 30 | 31 | async login () { 32 | let login = false 33 | const id = `wx_${parseInt(Date.now() / 1000)}` 34 | await new StartWeChat4u(id) 35 | 36 | for (let i = 0; i < 60; i++) { 37 | if (!login && Bot.lain.loginMap.get(id)) { 38 | login = true 39 | const { url } = Bot.lain.loginMap.get(id) 40 | const msg = [ 41 | '请于60秒内通过手机扫码登录微信~', 42 | segment.image(url) 43 | ] 44 | await this.e.reply(msg, false, { recall: 60 }) 45 | break 46 | } 47 | await lain.sleep(1000) 48 | } 49 | 50 | for (let i = 0; i < 60; i++) { 51 | const bot = Bot.lain.loginMap.get(id) 52 | if (login && bot && bot.login) { 53 | return this.e.reply(`Bot:${id} 登录成功~`, true, { at: true }) 54 | } 55 | await lain.sleep(1000) 56 | } 57 | } 58 | 59 | async account () { 60 | const _path = fs.readdirSync('./plugins/Lain-plugin/config') 61 | const Jsons = _path.filter(file => file.endsWith('.json')).map(file => file.replace('.json', '')) 62 | if (Jsons.length > 0) { 63 | return await this.reply(`微信账号:\n${Jsons.join('\n')}`, true) 64 | } else { 65 | return await this.reply('还没有账号呢~', true) 66 | } 67 | } 68 | 69 | async delUser () { 70 | const msg = this.e.msg.replace(/#微信删除/, '').trim() 71 | try { 72 | const _path = Bot.lain._path + `/${msg}.json` 73 | Bot[msg].stop() 74 | if (fs.existsSync(_path)) fs.unlinkSync(_path) 75 | return await this.reply(`已停止并删除${msg}`, true) 76 | } catch (error) { 77 | return await this.e.reply(`账号 ${msg} 不存在`) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/Lagrange.Core.md: -------------------------------------------------------------------------------- 1 | # 请注意,`Lagrange.Core`作者不接受任何形式的传播 2 | # 请勿将`Lagrange.Core`在中国大陆任何公开的平台进行任何传播,特别是“B站”~ 3 | # 请勿对`Lagrange.Core`进行制作任何教程,包括于本插件的任何教程 4 | # 谨记:上一个走的还是超时空猫猫。 5 | 6 | ### 使用方法: 7 | 8 | 你需要自行前往[Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core)找到文件、sign 9 | 10 | ### Linux 11 | 12 | 提权: 13 | ```bash 14 | chmod +777 Lagrange.OneBot 15 | ./Lagrange.OneBot 16 | ``` 17 | 18 | 随后先进行关闭,进行下一步 19 | 20 | ### windows 21 | 22 | 解压,运行一次,随后关闭,进行下一步 23 | 24 | 25 | ### 编辑`appsettings.json` 26 | 27 | 在根目录打开`appsettings.json` 28 | 29 | 修改为以下这样 30 | 31 | ```json 32 | { 33 | "Logging": { 34 | "LogLevel": { 35 | "Default": "Information", 36 | "Microsoft": "Warning", 37 | "Microsoft.Hosting.Lifetime": "Information" 38 | } 39 | }, 40 | "SignServerUrl": "你需要自己找到sign,填写到这里", 41 | "Account": { 42 | "Uin": 0, 43 | "Password": "", 44 | "Protocol": "Linux", 45 | "AutoReconnect": true, 46 | "GetOptimumServer": true 47 | }, 48 | "Message": { 49 | "IgnoreSelf": true 50 | }, 51 | "Implementations": [ 52 | { 53 | "Type": "ReverseWebSocket", 54 | "Host": "127.0.0.1", 55 | "Port": 2955, 56 | "Suffix": "/LagrangeCore", 57 | "ReconnectInterval": 5000, 58 | "HeartBeatInterval": 5000, 59 | "AccessToken": "" 60 | } 61 | ] 62 | } 63 | ``` 64 | 65 | **如果你的端口地址是云服务器,请自行更改** 66 | ```json 67 | { 68 | "Type": "ReverseWebSocket", 69 | "Host": "192.168.1.1", 70 | "Port": 8888, 71 | "Suffix": "/LagrangeCore", 72 | "ReconnectInterval": 5000, 73 | "HeartBeatInterval": 5000, 74 | "AccessToken": "" 75 | } 76 | ``` 77 | 78 | 79 | 解释一下`ws://localhost:2955/LagrangeCore`这个地址 80 | - `ws://`这部分是固定的,无需更改 81 | - `localhost`这个是本地地址,如果你的喵崽在`云服务器`,请更换为云服务器的`公网IP地址` 82 | - `:2956`这部分是端口,需要使用`:`和`IP地址`连接起来,如需更改,请自行修改配置文件`config.yaml`或使用锅巴修改 83 | - `/Shamrock`这部分是固定的,无需更改 84 | 85 | 86 | # 适配进度 87 | 88 | 没有注明的在下方的有需求并且我时间充裕的情况下会实现... 欢迎pr 89 | 90 | - [√] 接收`文本`、`表情`、`at`、`图片`消息 91 | - [√] 发送`文本`、`表情`、`at`、`图片`消息 92 | - [ ] 语音、视频、文件、合并转发 93 | - [√] 好友主动消息`Bot[BotQQ号].pickUser(user_id).sendMsg("主动消息")` 94 | - [√] 群聊主动消息`Bot[BotQQ号].pickGroup(group_id).sendMsg("主动消息")` 95 | - [√] 撤回消息 96 | 97 | 98 | 如需使用`yenai-plugin`,请使用为`shamrock`专门适配的椰奶:[yenai-plugin](https://github.com/Zyy955/yenai-plugin) -------------------------------------------------------------------------------- /apps/index.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | import { update as Update } from '../../other/update.js' 3 | import { xiaofei_music } from '../adapter/shamrock/xiaofei/music.js' 4 | import { xiaofei_weather } from '../adapter/shamrock/xiaofei/weather.js' 5 | 6 | export class Lain extends plugin { 7 | constructor () { 8 | super({ 9 | name: '铃音基本设置', 10 | priority: -50, 11 | rule: [ 12 | { 13 | reg: /^#(Lain|铃音)(强制)?更新(日志)?$/gi, 14 | fnc: 'update', 15 | permission: 'master' 16 | }, 17 | { 18 | reg: /^#(我的|当前)?(id|信息)$/gi, 19 | fnc: 'user_id' 20 | }, 21 | { 22 | reg: /^#(重载|重新加载)资源/, 23 | fnc: 'loadRes', 24 | permission: 'master' 25 | } 26 | ] 27 | }) 28 | } 29 | 30 | async update (e) { 31 | let new_update = new Update() 32 | new_update.e = e 33 | new_update.reply = this.reply 34 | const name = 'Lain-plugin' 35 | if (e.msg.includes('更新日志')) { 36 | if (new_update.getPlugin(name)) { 37 | this.e.reply(await new_update.getLog(name)) 38 | } 39 | } else { 40 | if (new_update.getPlugin(name)) { 41 | if (this.e.msg.includes('强制')) { execSync('git reset --hard', { cwd: `${process.cwd()}/plugins/${name}/` }) } 42 | await new_update.runUpdate(name) 43 | if (new_update.isUp) { setTimeout(() => new_update.restart(), 2000) } 44 | } 45 | } 46 | return true 47 | } 48 | 49 | async user_id (e) { 50 | const msg = [] 51 | if (e.isMaster) msg.push(`Bot:${e.bot.uin || e.self_id}`) 52 | msg.push(`您的个人ID:${e.user_id}`) 53 | if (e.guild_id) msg.push(`当前频道ID:${e.guild_id}`) 54 | if (e.channel_id) msg.push(`当前子频道ID:${e.channel_id}`) 55 | if (e.group_id) msg.push(`当前群聊ID:${e.group_id}`) 56 | if (e.isMaster && e?.adapter === 'QQGuild') msg.push('\n温馨提示:\n使用本体黑白名单请使用「群聊ID」\n使用插件黑白名单请按照配置文件说明进行添加~') 57 | 58 | /** at用户 */ 59 | if (e.isMaster && e.at) msg.push(`\n目标用户ID:${e.at}`) 60 | return await e.reply(`\n${msg.join('\n')}`, true, { at: true }) 61 | } 62 | 63 | /** shamrock重载资源 */ 64 | // async loadRes (e) { 65 | // await e.reply('开始重载,请稍等...', true) 66 | // let res = (await import('../adapter/shamrock/bot.js')).default 67 | // res = new res(e.self_id) 68 | // const msg = await res.LoadList() 69 | // return await e.reply(msg, true) 70 | // } 71 | } 72 | 73 | export { xiaofei_music, xiaofei_weather } 74 | -------------------------------------------------------------------------------- /model/version.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import lodash from 'lodash' 3 | 4 | let packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')) 5 | 6 | const getLine = function (line) { 7 | line = line.replace(/(^\s*\*|\r)/g, '') 8 | line = line.replace(/\s*`([^`]+`)/g, '$1') 9 | line = line.replace(/`\s*/g, '') 10 | line = line.replace(/\s*\*\*([^*]+\*\*)/g, '$1') 11 | line = line.replace(/\*\*\s*/g, '') 12 | line = line.replace(/ⁿᵉʷ/g, '') 13 | return line 14 | } 15 | 16 | const readLogFile = function (root, versionCount = 5) { 17 | let logPath = `${root}/CHANGELOG.md` 18 | let logs = {} 19 | let changelogs = [] 20 | let currentVersion 21 | 22 | try { 23 | if (fs.existsSync(logPath)) { 24 | logs = fs.readFileSync(logPath, 'utf8') || '' 25 | logs = logs.split('\n') 26 | 27 | let temp = {} 28 | let lastLine = {} 29 | lodash.forEach(logs, (line) => { 30 | if (versionCount <= -1) { 31 | return false 32 | } 33 | let versionRet = /^#\s*([0-9a-zA-Z\\.~\s]+?)\s*$/.exec(line) 34 | if (versionRet && versionRet[1]) { 35 | let v = versionRet[1].trim() 36 | if (!currentVersion) { 37 | currentVersion = v 38 | } else { 39 | changelogs.push(temp) 40 | // if (/0\s*$/.test(v) && versionCount > 0) { 41 | // versionCount = 0 42 | // } else { 43 | versionCount-- 44 | // } 45 | } 46 | 47 | temp = { 48 | version: v, 49 | logs: [] 50 | } 51 | } else { 52 | if (!line.trim()) { 53 | return 54 | } 55 | if (/^\*/.test(line)) { 56 | lastLine = { 57 | title: getLine(line), 58 | logs: [] 59 | } 60 | temp.logs.push(lastLine) 61 | } else if (/^\s{2,}\*/.test(line)) { 62 | lastLine.logs.push(getLine(line)) 63 | } 64 | } 65 | }) 66 | } 67 | } catch (e) { 68 | // do nth 69 | } 70 | return { changelogs, currentVersion } 71 | } 72 | 73 | const { changelogs, currentVersion } = readLogFile(`${process.cwd()}/plugins/Lain-plugin/`) 74 | 75 | const yunzaiVersion = packageJson.version 76 | const isMiao = !!packageJson.dependencies.sequelize 77 | 78 | let Version = { 79 | isMiao, 80 | get version () { 81 | return currentVersion 82 | }, 83 | get yunzai () { 84 | return yunzaiVersion 85 | }, 86 | get changelogs () { 87 | return changelogs 88 | }, 89 | readLogFile 90 | } 91 | 92 | export default Version 93 | -------------------------------------------------------------------------------- /model/help.js: -------------------------------------------------------------------------------- 1 | export const helpCfg = { 2 | themeSet: false, 3 | title: '铃音帮助', 4 | subTitle: 'Miao-Yunzai & Lain-plugin', 5 | colWidth: 265, 6 | theme: 'all', 7 | themeExclude: [ 8 | 'default' 9 | ], 10 | colCount: 2, 11 | bgBlur: true 12 | } 13 | export const helpList = [ 14 | { 15 | group: 'QQBot ---> #QQBot设置 沙盒:私域:appID:token:secret', 16 | auth: 'master', 17 | list: [ 18 | { 19 | icon: 1, 20 | title: '#QQ群设置', 21 | desc: '是=1 否=0 再次添加为删除' 22 | }, 23 | { 24 | icon: 13, 25 | title: '#QQ频道设置', 26 | desc: '是=1 否=0 再次添加为删除' 27 | }, 28 | { 29 | icon: 23, 30 | title: '#QQBot设置', 31 | desc: '同时连接群和频道' 32 | }, 33 | { 34 | icon: 3, 35 | title: '#QQBot账号', 36 | desc: '查看机器人' 37 | }, 38 | { 39 | icon: 6, 40 | title: '#QQBot设置MD', 41 | desc: '机器人ID:模板ID' 42 | }, 43 | { 44 | icon: 8, 45 | title: '#QQBotMD 2', 46 | desc: '0=关闭 1=全局 2=仅正则 3=与内容分离' 47 | }, 48 | { 49 | icon: 10, 50 | title: '#QQBotDau', 51 | desc: '查看消息统计' 52 | } 53 | ] 54 | }, 55 | { 56 | group: 'Shamrock', 57 | auth: 'master', 58 | list: [ 59 | { 60 | icon: 2, 61 | title: '#重载资源', 62 | desc: '用于重新加载好友列表,群列表等。' 63 | }, 64 | { 65 | icon: 5, 66 | title: '#shamrock版本', 67 | desc: '查询OpenShamrock官方库版本信息' 68 | }, 69 | { 70 | icon: 4, 71 | title: '#shamrock(测试)安装包', 72 | desc: '从github下载apk安装包发送到群聊/私聊' 73 | } 74 | ] 75 | }, 76 | { 77 | group: 'WeChat', 78 | auth: 'master', 79 | list: [ 80 | { 81 | icon: 9, 82 | title: '#微信修改名称<新名称>', 83 | desc: '修改椰奶状态显示名称' 84 | } 85 | ] 86 | }, 87 | { 88 | group: '其他', 89 | auth: 'master', 90 | list: [ 91 | { 92 | icon: 16, 93 | title: '#设置主人', 94 | desc: '可以艾特指定用户' 95 | }, 96 | { 97 | icon: 5, 98 | title: '#删除主人', 99 | desc: '艾特指定用户' 100 | }, 101 | { 102 | icon: 18, 103 | title: '#铃音更新', 104 | desc: '更新插件' 105 | }, 106 | { 107 | icon: 24, 108 | title: '#铃音版本', 109 | desc: '查看版本' 110 | }, 111 | { 112 | icon: 7, 113 | title: '#ID', 114 | desc: '获取个人id、群id' 115 | } 116 | ] 117 | } 118 | ] 119 | export const style = { 120 | fontColor: '#ceb78b', 121 | fontShadow: 'none', 122 | descColor: '#eee', 123 | contBgColor: 'rgba(6, 21, 31, .5)', 124 | contBgBlur: 3, 125 | headerBgColor: 'rgba(6, 21, 31, .4)', 126 | rowBgColor1: 'rgba(6, 21, 31, .2)', 127 | rowBgColor2: 'rgba(6, 21, 31, .35)' 128 | } 129 | -------------------------------------------------------------------------------- /resources/QRCode/QRCode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
QRCode
102 |
Miao-Yunzai & Lain-plugin {{@adapter.lain.version.version}}
103 | -------------------------------------------------------------------------------- /model/config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import Yaml from 'yaml' 3 | 4 | const _path = process.cwd() + '/plugins/Lain-plugin/config' 5 | if (!fs.existsSync(process.cwd() + '/temp/WeXin')) fs.mkdirSync(process.cwd() + '/temp/WeXin') 6 | 7 | const packYZ = JSON.parse(fs.readFileSync('./package.json', 'utf-8')) 8 | const BotCfg = Yaml.parse(fs.readFileSync('./config/config/bot.yaml', 'utf8')) 9 | const packLain = JSON.parse(fs.readFileSync('./plugins/Lain-plugin/package.json', 'utf-8')) 10 | 11 | const { name, version, adapter, dependencies } = packLain 12 | Bot.lain = { 13 | /** 云崽信息 */ 14 | ...packLain, 15 | /** 配置文件夹路径 */ 16 | _path, 17 | BotCfg, 18 | /** 全部频道列表 */ 19 | guilds: {}, 20 | /** 适配器版本及依赖 */ 21 | adapter: { 22 | lain: { 23 | /** 插件 */ 24 | version: { 25 | id: '云崽', 26 | name, 27 | version 28 | }, 29 | /** 主体 */ 30 | apk: { 31 | display: 'Yunzai-Bot', 32 | version: packYZ.version 33 | } 34 | }, 35 | QQGuild: { 36 | /** 插件 */ 37 | version: { 38 | id: '公域', 39 | name: 'QQ频道', 40 | version: adapter.QQGuild 41 | }, 42 | /** 依赖包 */ 43 | apk: { 44 | display: 'qq-official-bot', 45 | version: dependencies['qq-official-bot'].replace('^', '') 46 | } 47 | }, 48 | ComWeChat: { 49 | /** 插件 */ 50 | version: { 51 | id: 'PC', 52 | name: '微信', 53 | version: adapter.ComWeChat 54 | }, 55 | /** 依赖包 */ 56 | apk: { 57 | display: 'CWeChatRobot', 58 | version: adapter.CWeChatRobot.replace('^', '') 59 | } 60 | }, 61 | stdin: { 62 | /** 插件 */ 63 | version: { 64 | id: 'stdin', 65 | name: '标准输入', 66 | version: adapter.stdin 67 | }, 68 | /** 依赖包 */ 69 | apk: { 70 | display: 'stdin', 71 | version: adapter.stdin 72 | } 73 | }, 74 | Shamrock: { 75 | /** 插件 */ 76 | version: { 77 | id: 'Shamrock', 78 | name: '三叶草', 79 | version: adapter.stdin 80 | }, 81 | /** 依赖包 */ 82 | apk: { 83 | display: '', 84 | version: '' 85 | } 86 | }, 87 | QQBot: { 88 | /** 插件 */ 89 | version: { 90 | id: 'QQBot', 91 | name: 'QQBot', 92 | version: adapter.QQBot 93 | }, 94 | /** 依赖包 */ 95 | apk: { 96 | display: 'qq-official-bot', 97 | version: dependencies['qq-official-bot'].replace('^', '') 98 | } 99 | }, 100 | WeXin: { 101 | /** 插件 */ 102 | version: { 103 | id: 'WeXin', 104 | name: 'WeXin', 105 | version: adapter.WeXin 106 | }, 107 | /** 依赖包 */ 108 | apk: { 109 | display: 'wechat4u', 110 | version: dependencies.wechat4u.replace('^', '') 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 原仓库已删库跑路。 2 | ## 目前QQbot相关适配器维护代码来源于 [sky-summer](https://gitee.com/sky-summer/Lain-plugin.git) 3 | ## 简介 4 | - 插件更新日志:[点击查看](./CHANGELOG.md) 5 | - 本项目使用 [GPL-3.0](./LICENSE) 开源协议,欢迎任何形式的贡献! 6 | 7 | `Lain-plugin`是一个围绕云崽`Yunzai-Bot-V3`开发的多适配器插件,让喵崽有更多途径接入`QQ频道`、`微信`、`shamrock`等消息平台~,不再局限于ICQQ。 8 | 9 | 我正在为 [kritor](https://github.com/KarinJS/kritor) 开发新的机器人框架,如果您有时间且热爱开源并且想参与其中,您可以联系我~ 10 | 11 | 新框架:[Karin](https://github.com/KarinJS/carrying) 12 | 13 | ### 这里特别声明: 14 | 15 | 不想登录ICQQ并继续使用本插件: 16 | 17 | - 更新云崽到最新 18 | - 打开云崽的`config/config/bot.yaml`文件将 `skip_login: false` 修改为 `skip_login: true` 19 | - 如果不存在这个,自行加一行 `skip_login: true` 即可。 20 | 21 | ## 1.安装插件 22 | 23 | 在`Yunzai`根目录执行 24 | 25 | `github:` 26 | ``` 27 | git clone --depth=1 https://github.com/snowtafir/Lain-plugin ./plugins/Lain-plugin 28 | ``` 29 | 30 | `gitee:` 31 | ``` 32 | git clone --depth=1 https://gitee.com/snowtafir/Lain-plugin ./plugins/Lain-plugin 33 | ``` 34 | 35 | ## 2.安装依赖 36 | 37 | ``` 38 | pnpm install -P 39 | ``` 40 | 41 | `安装失败再用这个:` 42 | ``` 43 | pnpm config set sharp_binary_host "https://npmmirror.com/mirrors/sharp" && pnpm config set sharp_libvips_binary_host "https://npmmirror.com/mirrors/sharp-libvips" && pnpm install -P 44 | ``` 45 | 46 | ## 3.使用适配器 47 | 48 | 请点击查看对应教程/说明~ 49 | 50 | - [标准输入](./docs/stdin.md) 51 | - [QQ频道(旧版)](./docs/QQGuild.md) 52 | - [PC微信](./docs/WeChat.md) 53 | - [Shamrock](./docs/Shamrock.md) 54 | - [QQBot(群和频道)](./docs/QQBot.md) 55 | - [网页版微信](./docs/WeXin.md) 56 | - [Lagrange.Core](./docs/Lagrange.Core.md) 57 | - [OneBotV11](./docs/OneBotV11.md) 58 | 59 | ## 4.设置主人 60 | 61 | - 使用方法 62 | - 方法1:发送`#设置主人`,随后复制发送控制台的验证码即可成为主人 63 | - 方法2:发送`#设置主人@用户`,需要你是主人的情况下,指定此用户成为主人 64 | 65 | 主人可通过`#取消主人@用户`或者`#删除主人@用户` 66 | 67 | ## 插件更新 68 | 69 | - #铃音更新 70 | - #Lain更新 71 | 72 | ## 如何区分适配器 73 | 74 | - `e.adapter` || `Bot[uin].adapter` 75 | - 标准输入:`stdin` 76 | - QQ频道:`QQGuild` 77 | - Shamrock:`shamrock` 78 | - PC微信:`ComWeChat` 79 | - QQBot:`QQBot` 80 | - 网页版微信:`WeXin` 81 | - LagrangeCore: `LagrangeCore` 82 | - OneBotV11: `OneBotV11` 83 | 84 | ## 适配进度 85 | - [√] 标准输入 86 | - [√] 跳过登录QQ 87 | - [√] QQ频道适配器 88 | - [√] PC微信适配器 89 | - [√] 网页版微信适配器 90 | - [√] Shamrock适配器 91 | - [√] QQBot适配器 92 | - [√] LagrangeCore 93 | - [√] OneBotV11适配器 94 | 95 | ## 特别鸣谢 96 | 97 | 以下排名不分先后 98 | 99 | - [Trss-Yunzai](https://github.com/TimeRainStarSky/Yunzai) 100 | - [Miao-Yunzai](https://github.com/yoimiya-kokomi/Miao-Yunzai) 101 | - [索引库](https://github.com/yhArcadia/Yunzai-Bot-plugins-index) 102 | - [OpenShamrock](https://github.com/whitechi73/OpenShamrock) 103 | - [ComWeChat](https://github.com/JustUndertaker/ComWeChatBotClient) 104 | - [wechat4u](https://github.com/nodeWechat/wechat4u/blob/master/run-core.js) 105 | - [qq-official-bot](https://github.com/lc-cn/qq-official-bot) 106 | - [QQBot按钮库](https://gitee.com/lava081/button) 107 | - [xiaoye12123](https://gitee.com/xiaoye12123) 108 | - [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 109 | - [sky-summer | Lain-plugin](https://gitee.com/sky-summer/Lain-plugin.git) :QQbot适配器 110 | -------------------------------------------------------------------------------- /apps/restart.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import pm2 from 'pm2' 3 | import { exec } from 'child_process' 4 | 5 | let state = false 6 | 7 | export class AdapterRestart extends plugin { 8 | constructor (e = '') { 9 | super({ 10 | name: '铃音-重启', 11 | dsc: '适用于适配器重启', 12 | event: 'message', 13 | priority: 0, 14 | rule: [ 15 | { 16 | reg: '^#重启$', 17 | fnc: 'restart', 18 | permission: 'master' 19 | } 20 | ] 21 | }) 22 | 23 | if (e) this.e = e 24 | 25 | this.key = 'Lain:restart' 26 | } 27 | 28 | async restart () { 29 | if (state) return true 30 | state = true 31 | if (!this.e?.adapter) return false 32 | this.key = `Lain:restart:${this.e.adapter}` 33 | // "Lain:restart:QQBot" 34 | await this.e.reply('开始执行重启,请稍等...') 35 | logger.mark(`${this.e.logFnc} 开始执行重启,请稍等...`) 36 | 37 | const data = JSON.stringify({ 38 | adapter: this.e.adapter, 39 | uin: this.e?.bot?.uin || this.e?.self_id || Bot.uin, 40 | isGroup: !!this.e.isGroup, 41 | id: this.e.isGroup ? this.e.group_id : this.e.user_id, 42 | time: Date.now(), 43 | msg_id: this.e.message_id 44 | }) 45 | 46 | const npm = await this.checkPnpm() 47 | 48 | try { 49 | await redis.set(this.key, data, { EX: 24000 }) 50 | pm2.connect((err) => { 51 | if (err) return logger.error(err) 52 | 53 | pm2.list((err, processList) => { 54 | if (err) { 55 | logger.error(err) 56 | } else { 57 | const PM2Data = JSON.parse(fs.readFileSync('./config/pm2/pm2.json')) 58 | const processExists = processList.some(processInfo => processInfo.name === PM2Data.apps[0].name) 59 | const cm = processExists ? `${npm} run restart` : `${npm} start` 60 | pm2.disconnect() 61 | exec(cm, { windowsHide: true }, (error, stdout) => { 62 | if (error) { 63 | redis.del(this.key) 64 | this.e.reply(`操作失败!\n${error.stack}`) 65 | logger.error(`重启失败\n${error.stack}`) 66 | } else if (stdout) { 67 | logger.mark('重启成功,运行已由前台转为后台') 68 | logger.mark(`查看日志请用命令:${npm} run log`) 69 | logger.mark(`停止后台运行命令:${npm} stop`) 70 | state = false 71 | process.exit() 72 | } 73 | }) 74 | } 75 | }) 76 | }) 77 | } catch (error) { 78 | state = false 79 | redis.del(this.key) 80 | const errorMessage = error.stack ?? error 81 | this.e.reply(`操作失败!\n${errorMessage}`) 82 | logger.error(`重启失败\n${errorMessage}`) 83 | } 84 | 85 | return true 86 | } 87 | 88 | async checkPnpm () { 89 | let npm = 'npm' 90 | let ret = await this.execSync('pnpm -v') 91 | if (ret.stdout) npm = 'pnpm' 92 | return npm 93 | } 94 | 95 | async execSync (cmd) { 96 | return new Promise((resolve, reject) => { 97 | exec(cmd, { windowsHide: true }, (error, stdout, stderr) => { 98 | resolve({ error, stdout, stderr }) 99 | }) 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /resources/shamrock/index.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | user-select: none; 8 | } 9 | 10 | body { 11 | font-size: 18px; 12 | color: #1e1f20; 13 | transform: scale(1.3); 14 | transform-origin: 0 0; 15 | width: 600px; 16 | } 17 | .container { 18 | width: 600px; 19 | padding: 10px 0 10px 0; 20 | background-size: 100% 100%; 21 | } 22 | .log-cont { 23 | background-size: cover; 24 | margin: 5px 15px 5px 10px; 25 | border-radius: 10px; 26 | /*padding-bottom: 300px;*/ 27 | } 28 | .log-cont .cont { 29 | margin: 0; 30 | } 31 | .log-cont .cont-title { 32 | font-size: 16px; 33 | padding: 20px 20px 20px; 34 | display: flex; 35 | } 36 | 37 | .log-cont .cont-title .cont-logo { 38 | width: 70px; 39 | border-radius: 20%; 40 | } 41 | 42 | .log-cont .cont-title .cont-text { 43 | font-family: Number, serif; 44 | font-size: 24px; 45 | padding-left: 10px; 46 | color: #e3e3e3; 47 | padding-bottom: 3px; 48 | } 49 | 50 | .github { 51 | margin-top: 2px; 52 | display: flex; 53 | align-items: center; /* 将子元素垂直居中 */ 54 | } 55 | 56 | .cont-text-sub { 57 | font-family: Number, serif; 58 | font-size: 14px !important; 59 | /*padding-left: 10px;*/ 60 | margin-top: 2px; 61 | } 62 | 63 | .github-logo { 64 | width: 23px; 65 | padding-left: 10px; 66 | } 67 | 68 | .github-fork { 69 | margin-left: 20px; 70 | width: 25px; 71 | padding-left: 10px; 72 | } 73 | 74 | .github-subscribe { 75 | margin-left: 20px; 76 | width: 25px; 77 | padding-left: 10px; 78 | } 79 | 80 | .github-status { 81 | margin-top: 5px; 82 | display: flex; 83 | align-items: center; /* 将子元素垂直居中 */ 84 | } 85 | 86 | .status-block { 87 | border-radius: 5px; 88 | margin: 10px 0; 89 | background: rgba(0, 0, 0, 20%); 90 | color: #d0d0d0; 91 | width: 575px; 92 | } 93 | 94 | .status-blck-title { 95 | background: rgba(0, 0, 0, 50%); 96 | padding: 10px; 97 | border-radius: 5px 5px 0 0; 98 | } 99 | 100 | .status-blck-content { 101 | padding: 10px; 102 | border-radius: 5px 5px 0 0; 103 | } 104 | .digest { 105 | font-size: 12px; 106 | } 107 | .release { 108 | 109 | } 110 | 111 | .release-version { 112 | color: #82d0ac; 113 | } 114 | 115 | .release-version-time { 116 | color: #aaaaaa; 117 | font-size: 12px; 118 | } 119 | 120 | .release-log { 121 | margin-top: 2px; 122 | font-size: 12px; 123 | } 124 | 125 | .strong { 126 | color: #24d5cd; 127 | } 128 | .sharp { 129 | color: #a05acb; 130 | } 131 | .commit { 132 | padding-bottom: 10px; 133 | } 134 | 135 | .commit-sha { 136 | display: flex; 137 | align-items: center; /* 将子元素垂直居中 */ 138 | 139 | } 140 | 141 | .commit-content { 142 | display: flex; 143 | align-items: center; /* 将子元素垂直居中 */ 144 | 145 | font-size: 12px; 146 | margin-top: 3px; 147 | } 148 | .commit-sha-value { 149 | /*margin-left: 8px;*/ 150 | } 151 | .commit-time { 152 | margin-left: 8px; 153 | color: #aaaaaa; 154 | font-size: 12px; 155 | } 156 | .committer-avatar { 157 | width: 18px; 158 | border-radius: 5px; 159 | margin-right: 5px; 160 | } 161 | 162 | .help { 163 | padding-bottom: 10px; 164 | } 165 | 166 | .help-item { 167 | display: flex; 168 | align-items: center; /* 将子元素垂直居中 */ 169 | } 170 | 171 | .help-item-main { 172 | font-size: 12px; 173 | } -------------------------------------------------------------------------------- /adapter/Bot/icqq.js: -------------------------------------------------------------------------------- 1 | const _0x5173e7=_0x353d;function _0x353d(_0x162348,_0x2ed826){const _0x3f10f4=_0x3f10();return _0x353d=function(_0x353d02,_0x20ce00){_0x353d02=_0x353d02-0x151;let _0x34ca82=_0x3f10f4[_0x353d02];return _0x34ca82;},_0x353d(_0x162348,_0x2ed826);}(function(_0x3c6c9c,_0xc89b2){const _0x240e43=_0x353d,_0x424acb=_0x3c6c9c();while(!![]){try{const _0x2a5fce=parseInt(_0x240e43(0x155))/0x1*(parseInt(_0x240e43(0x159))/0x2)+-parseInt(_0x240e43(0x16c))/0x3*(parseInt(_0x240e43(0x174))/0x4)+-parseInt(_0x240e43(0x164))/0x5*(parseInt(_0x240e43(0x168))/0x6)+-parseInt(_0x240e43(0x172))/0x7*(parseInt(_0x240e43(0x175))/0x8)+parseInt(_0x240e43(0x15d))/0x9*(-parseInt(_0x240e43(0x171))/0xa)+parseInt(_0x240e43(0x167))/0xb*(-parseInt(_0x240e43(0x15a))/0xc)+parseInt(_0x240e43(0x16a))/0xd*(parseInt(_0x240e43(0x15e))/0xe);if(_0x2a5fce===_0xc89b2)break;else _0x424acb['push'](_0x424acb['shift']());}catch(_0x260cdb){_0x424acb['push'](_0x424acb['shift']());}}}(_0x3f10,0x9e64a));import{core,segment}from'icqq';class ICQQToFile{[_0x5173e7(0x179)](_0x3654bc){const _0x27dd06=_0x5173e7;return core['pb'][_0x27dd06(0x156)](Buffer['from'](_0x3654bc[_0x27dd06(0x163)](_0x27dd06(0x170),''),_0x27dd06(0x161)));}async[_0x5173e7(0x16e)](_0x31a1bd){const _0x4d02f5=_0x5173e7,_0x4828c7=Bot[Bot[_0x4d02f5(0x158)]][_0x4d02f5(0x151)](Math['ceil'](Math[_0x4d02f5(0x15f)]()*0xa**0x9)),_0x5c606c=(await _0x4828c7[_0x4d02f5(0x16b)](segment['image'](_0x31a1bd)))[_0x4d02f5(0x160)][0x0],_0x461e26=_0x4d02f5(0x166)+(_0x5c606c['md5'][_0x4d02f5(0x157)]('hex')||'')[_0x4d02f5(0x15c)]()+'/0';return{..._0x5c606c,'url':_0x461e26};}async[_0x5173e7(0x177)](_0xa3caf9){const _0x307c37=_0x5173e7,_0x16df8e=Bot[Bot[_0x307c37(0x158)]][_0x307c37(0x151)](Math[_0x307c37(0x16f)](Math[_0x307c37(0x15f)]()*0xa**0x9)),_0x460500=await _0x16df8e['uploadVideo']({'file':_0xa3caf9}),_0x5a5fc4=this['proto'](_0x460500[_0x307c37(0x173)]);return await _0x16df8e[_0x307c37(0x154)](_0x5a5fc4[0x1],_0x5a5fc4[0x2]);}async[_0x5173e7(0x178)](_0x3f5420,_0xb87a27=!![],_0x31cd4a=''){const _0x5d74f6=_0x5173e7,_0x486121=Bot[Bot[_0x5d74f6(0x158)]]['pickGroup'](Math[_0x5d74f6(0x16f)](Math[_0x5d74f6(0x15f)]()*0xa**0x9)),_0x362883=await _0x486121[_0x5d74f6(0x178)]({'file':_0x3f5420},_0xb87a27,_0x31cd4a),_0xea0ea9=this[_0x5d74f6(0x179)](_0x362883['file']);return await this['getPttUrl'](_0xea0ea9[0x3]);}async[_0x5173e7(0x15b)](_0x3971e1){const _0x2b800e=_0x5173e7,_0x945116=core['pb']['encode']({0x1:0x4b0,0x2:0x0,0xe:{0xa:Bot[_0x2b800e(0x158)],0x14:_0x3971e1,0x1e:0x2},0x65:0x11,0x66:0x68,0x1869f:{0x160bc:0x1,0x16378:0x2,0x163dc:0x1}}),_0x2cc480=core['pb']['decode'](await Bot[Bot[_0x2b800e(0x158)]][_0x2b800e(0x165)](_0x2b800e(0x176),_0x945116))[0xe];_0x2cc480[0xa]!==0x0&&logger[_0x2b800e(0x16d)](_0x2cc480,_0x2b800e(0x162));const _0x58702f=new URL(_0x2cc480[0x1e][0x32][_0x2b800e(0x157)]());return _0x58702f[_0x2b800e(0x153)]=_0x2b800e(0x152),_0x58702f[_0x2b800e(0x169)]='https',_0x58702f[_0x2b800e(0x157)]();}}function _0x3f10(){const _0x1e0e19=['9KNAHCZ','650594XpnMNI','random','imgs','base64','获取语音文件地址错误','replace','4915AzaaEc','sendUni','https://gchat.qpic.cn/gchatpic_new/0/0-0-','2308174NnNZGa','1014TOzyQo','protocol','1027LDKEBc','_preprocess','63ZIULTy','error','uploadImage','ceil','protobuf://','7100180AxpTit','21GVnnVP','file','237896ZPdKAw','2142176mimkAE','PttCenterSvr.pb_pttCenter_CMD_REQ_APPLY_DOWNLOAD-1200','uploadVideo','uploadPtt','proto','pickGroup','grouptalk.c2c.qq.com','host','getVideoUrl','194eEeynK','decode','toString','uin','1194BTxEew','12tptNrS','getPttUrl','toUpperCase'];_0x3f10=function(){return _0x1e0e19;};return _0x3f10();}Bot[_0x5173e7(0x158)]!=0x15b38&&(lain[_0x5173e7(0x173)]=new ICQQToFile()); -------------------------------------------------------------------------------- /apps/help.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import Render from '../model/render.js' 3 | import Version from '../model/version.js' 4 | import { helpCfg, helpList, style } from '../model/help.js' 5 | 6 | export class help extends plugin { 7 | constructor () { 8 | super({ 9 | name: '铃音帮助', 10 | priority: -50, 11 | rule: [ 12 | { 13 | reg: /^#(Lain|铃音)版本$/, 14 | fnc: 'version' 15 | }, 16 | { 17 | reg: /^#(Lain|铃音)(.*)帮助$/, 18 | fnc: 'help' 19 | } 20 | ] 21 | }) 22 | } 23 | 24 | async version (e) { 25 | return await Render.render('help/version-info', { 26 | currentVersion: Version.version, 27 | changelogs: Version.changelogs, 28 | name: 'Lain', 29 | elem: 'cryo' 30 | }, { e, scale: 1.2 }) 31 | } 32 | 33 | async help (e) { 34 | if (!e.msg.match(/^#(Lain|铃音)帮助$/)) { 35 | return await this.reply(e.msg.replace(/帮助/, '菜单')) 36 | } 37 | let helpGroup = [] 38 | _.forEach(helpList, (group) => { 39 | _.forEach(group.list, (help) => { 40 | let icon = help.icon * 1 41 | if (!icon) { 42 | help.css = 'display:none' 43 | } else { 44 | let x = (icon - 1) % 10 45 | let y = (icon - x - 1) / 10 46 | help.css = `background-position:-${x * 50}px -${y * 50}px` 47 | } 48 | }) 49 | 50 | helpGroup.push(group) 51 | }) 52 | 53 | let themeData = await this.getThemeData(helpCfg, helpCfg) 54 | return await Render.render('help/index', { 55 | helpCfg, 56 | helpGroup, 57 | ...themeData, 58 | element: 'default' 59 | }, { e, scale: 1.6 }) 60 | } 61 | 62 | async getThemeData (diyStyle, sysStyle) { 63 | let helpConfig = _.extend({}, sysStyle, diyStyle) 64 | let colCount = Math.min(5, Math.max(parseInt(helpConfig?.colCount) || 3, 2)) 65 | let colWidth = Math.min(500, Math.max(100, parseInt(helpConfig?.colWidth) || 265)) 66 | let width = Math.min(2500, Math.max(800, colCount * colWidth + 30)) 67 | let resPath = '{{_res_path}}/help/imgs/' 68 | let theme = { 69 | main: `${resPath}/main.png`, 70 | bg: `${resPath}/bg.jpg`, 71 | style 72 | } 73 | let themeStyle = theme.style || {} 74 | let ret = [` 75 | body{background-image:url(${theme.bg});width:${width}px;} 76 | .container{background-image:url(${theme.main});width:${width}px;} 77 | .help-table .td,.help-table .th{width:${100 / colCount}%} 78 | `] 79 | let css = function (sel, css, key, def, fn) { 80 | let val = getDef(themeStyle[key], diyStyle[key], sysStyle[key], def) 81 | if (fn) { 82 | val = fn(val) 83 | } 84 | ret.push(`${sel}{${css}:${val}}`) 85 | } 86 | css('.help-title,.help-group', 'color', 'fontColor', '#ceb78b') 87 | css('.help-title,.help-group', 'text-shadow', 'fontShadow', 'none') 88 | css('.help-desc', 'color', 'descColor', '#eee') 89 | css('.cont-box', 'background', 'contBgColor', 'rgba(43, 52, 61, 0.8)') 90 | css('.cont-box', 'backdrop-filter', 'contBgBlur', 3, (n) => diyStyle.bgBlur === false ? 'none' : `blur(${n}px)`) 91 | css('.help-group', 'background', 'headerBgColor', 'rgba(34, 41, 51, .4)') 92 | css('.help-table .tr:nth-child(odd)', 'background', 'rowBgColor1', 'rgba(34, 41, 51, .2)') 93 | css('.help-table .tr:nth-child(even)', 'background', 'rowBgColor2', 'rgba(34, 41, 51, .4)') 94 | return { 95 | style: ``, 96 | colCount 97 | } 98 | } 99 | } 100 | 101 | function getDef () { 102 | for (let idx in arguments) { 103 | if (!_.isUndefined(arguments[idx])) { 104 | return arguments[idx] 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /apps/master.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import Yaml from '../model/YamlHandler.js' 3 | 4 | /** 设置主人 */ 5 | let sign = {} 6 | 7 | export class LainMaster extends plugin { 8 | constructor () { 9 | super({ 10 | name: '铃音-设置主人', 11 | priority: -50, 12 | rule: [ 13 | { 14 | reg: /^#设置主人.*/, 15 | fnc: 'master' 16 | }, 17 | { 18 | reg: /^#(删除|取消)主人.*/, 19 | fnc: 'del_master', 20 | permission: 'master' 21 | }, 22 | { 23 | reg: /^#(禁用|启用|恢复)主人$/, 24 | fnc: 'off_master' 25 | } 26 | ] 27 | }) 28 | } 29 | 30 | async master (e) { 31 | let user_id = e.at || e.msg.replace(/#设置主人/, '') || e.user_id 32 | user_id = Number(user_id) || String(user_id) 33 | 34 | /** 检测是否为触发用户自身 */ 35 | if (user_id === e.user_id) { 36 | if (e.isMaster) { 37 | return await e.reply([segment.at(user_id), "已经是主人了哦(〃'▽'〃)"]) 38 | } 39 | } else { 40 | /** 如果不是触发用户自身,检测触发用户是否为主人 */ 41 | if (!e.isMaster) return await e.reply('只有主人才能命令我哦~\n(*/ω\*)') 42 | const cfg = new Yaml('./config/config/other.yaml') 43 | /** 检查指定用户是否已经是主人 */ 44 | if (cfg.value('masterQQ', user_id)) return e.reply([segment.at(user_id), "已经是主人了哦(〃'▽'〃)"]) 45 | // return await this.e.reply(this.addmaster(user_id)) 46 | } 47 | 48 | /** 生成验证码 */ 49 | sign[e.user_id] = { user_id, sign: crypto.randomUUID() } 50 | logger.mark(`设置主人验证码:${logger.green(sign[e.user_id].sign)}`) 51 | await e.reply([segment.at(e.user_id), '请输入控制台的验证码']) 52 | /** 开始上下文 */ 53 | return await this.setContext('SetAdmin') 54 | } 55 | 56 | async del_master (e) { 57 | let user_id = e.at || e.msg.replace(/#|删除|取消|主人/g, '') 58 | user_id = Number(user_id) || String(user_id) 59 | 60 | if (!user_id) return await e.reply('你都没有告诉我是谁!^_^') 61 | const cfg = new Yaml('./config/config/other.yaml') 62 | if (!cfg.value('masterQQ', user_id)) return await e.reply("这个人不是主人啦(〃'▽'〃)", false, { at: true }) 63 | cfg.delVal('masterQQ', user_id) 64 | return await e.reply([segment.at(user_id), '拜拜~']) 65 | } 66 | 67 | async off_master (e) { 68 | let user_id = Number(e.user_id) || e.user_id 69 | if (/禁用/.test(e.msg)) { 70 | /** 检测用户是否是主人 */ 71 | if (!e.isMaster) return e.reply([segment.at(e.user_id), '只有主人才能命令我哦~\n(*/ω\*)']) 72 | const cfg = new Yaml('./config/config/other.yaml') 73 | cfg.addVal('masterQQ', '--' + String(user_id), 'Array') 74 | cfg.delVal('masterQQ', user_id) 75 | return await e.reply([segment.at(user_id), '已临时禁用你的主人权限!\n如需恢复发送 #启用主人']) 76 | } else { 77 | /** 检测用户是否是主人 */ 78 | if (e.isMaster) return e.reply([segment.at(e.user_id), "已经是主人了哦(〃'▽'〃)"]) 79 | const cfg = new Yaml('./config/config/other.yaml') 80 | if (!cfg.value('masterQQ', '--' + String(user_id))) return e.reply([segment.at(user_id), '只有主人才能命令我哦~\n(*/ω\*)']) 81 | cfg.addVal('masterQQ', user_id, 'Array') 82 | cfg.delVal('masterQQ', '--' + String(user_id), 'Array') 83 | return await e.reply([segment.at(user_id), '已恢复你的主人权限~(*/ω\*)']) 84 | } 85 | } 86 | 87 | SetAdmin () { 88 | /** 结束上下文 */ 89 | this.finish('SetAdmin') 90 | /** 判断验证码是否正确 */ 91 | if (this.e.msg.trim() === sign[this.e.user_id]?.sign) { 92 | this.e.reply(this.addmaster(sign[this.e.user_id]?.user_id)) 93 | } else { 94 | return this.reply([segment.at(this.e.user_id), '验证码错误']) 95 | } 96 | } 97 | 98 | /** 设置主人 */ 99 | addmaster (user_id) { 100 | const cfg = new Yaml('./config/config/other.yaml') 101 | cfg.addVal('masterQQ', user_id, 'Array') 102 | return [segment.at(user_id), '新主人好~(*/ω\*)'] 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /plugins/纯文模板.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 模板源码 3 | * {{.text_0}}{{.text_1}}{{.text_2}}{{.text_3}}{{.text_4}}{{.text_5}}{{.text_6}}{{.text_7}}{{.text_8}}{{.text_9}} 4 | * 正常设置模板ID 模式设置4:#QQBotMD4 5 | */ 6 | 7 | import plugin from '../Lain-plugin/adapter/QQBot/plugins.js' 8 | 9 | Bot.Markdown = async function (e, data, button = []) { 10 | let text = [] 11 | const image = [] 12 | const message = [] 13 | 14 | for (let i of data) { 15 | switch (i.type) { 16 | case 'text': 17 | text.push(i.text.replace(/\n/g, '\r').trim()) 18 | break 19 | case 'image': 20 | image.push(i) 21 | break 22 | default: 23 | break 24 | } 25 | } 26 | 27 | /** 处理二笔语法,分割为数组 */ 28 | text = parseMD(text.join('')) 29 | 30 | /** 先分个组吧! */ 31 | if (image.length > text.length) { 32 | for (const i in image) message.push({ text: text?.[i], image: image?.[i] }) 33 | } else { 34 | for (const i in text) message.push({ text: text?.[i], image: image?.[i] }) 35 | } 36 | 37 | return await combination(e, message, button) 38 | } 39 | 40 | /** 处理md标记 */ 41 | function parseMD (str) { 42 | /** 处理第一个标题 */ 43 | str = str.replace(/^#/, '\r#') 44 | let msg = str.split(/(\*\*\*|\*\*|\*|__|_|~~|~|``)/).filter(Boolean) 45 | 46 | let mdSymbols = ['***', '**', '*', '__', '_', '~~', '~'] 47 | let result = [] 48 | let temp = '' 49 | 50 | for (let i = 0; i < msg.length; i++) { 51 | if (mdSymbols.includes(msg[i])) { 52 | temp += msg[i] 53 | } else { 54 | if (temp !== '') { 55 | result.push(temp) 56 | temp = '' 57 | } 58 | temp += msg[i] 59 | } 60 | } 61 | 62 | if (temp !== '') result.push(temp) 63 | return result 64 | } 65 | 66 | /** 按9进行分类 */ 67 | function sort (arr) { 68 | const Array = [] 69 | for (let i = 0; i < arr.length; i += 9) Array.push(arr.slice(i, i + 9)) 70 | return Array 71 | } 72 | 73 | /** 组合 */ 74 | async function combination (e, data, but) { 75 | const all = [] 76 | /** 按9分类 */ 77 | data = sort(data) 78 | for (let p of data) { 79 | const params = [] 80 | const length = p.length 81 | /** 头要特殊处理 */ 82 | params.push({ key: 'text_0', values: [(p[0]?.text || '') + (p[0].image ? `![图片 #${p[0].image?.width}px #${p[0].image?.height}px` : '')] }) 83 | for (let i = 1; i < length; i++) { 84 | let val = [] 85 | /** 上一个图片的后续链接 */ 86 | if (p[i - 1]?.image) val.push(`](${p[i - 1].image.file})`) 87 | /** 当前对象的文字和图片的开头 */ 88 | val.push(p[i]?.image ? `${(p[i].text || '')}![图片 #${p[i].image.width}px #${p[i].image.height}px` : (p[i].text || '')) 89 | params.push({ key: 'text_' + (i), values: [val.join('')] }) 90 | } 91 | 92 | /** 尾巴也要! */ 93 | if (p[length - 1]?.image) params.push({ key: `text_${length}`, values: [`](${p[length - 1].image.file})`] }) 94 | 95 | /** 转为md */ 96 | const markdown = { 97 | type: 'markdown', 98 | custom_template_id: e.bot.config.markdown.id, 99 | params 100 | } 101 | 102 | /** 按钮 */ 103 | const button = await Button(e) 104 | button && button?.length ? all.push([markdown, ...button, ...but]) : all.push([markdown, ...but]) 105 | } 106 | return all 107 | } 108 | 109 | /** 按钮添加 */ 110 | async function Button (e) { 111 | try { 112 | for (let p of plugin) { 113 | for (let v of p.plugin.rule) { 114 | const regExp = new RegExp(v.reg) 115 | if (regExp.test(e.msg)) { 116 | p.e = e 117 | const button = await p[v.fnc](e) 118 | /** 无返回不添加 */ 119 | if (button) return [...(Array.isArray(button) ? button : [button])] 120 | return false 121 | } 122 | } 123 | } 124 | } catch (error) { 125 | logger.error('Lain-plugin', error) 126 | return false 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /adapter/QQBot/plugins.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import moment from 'moment' 3 | import chokidar from 'chokidar' 4 | 5 | class Button { 6 | constructor () { 7 | this.plugin = './plugins' 8 | this.botModules = [] 9 | this.initialize() 10 | } 11 | 12 | /** 加载按钮 */ 13 | async loadModule (filePath) { 14 | filePath = filePath.replace(/\\/g, '/') 15 | try { 16 | let Plugin = (await import(`../../../.${filePath}?${moment().format('x')}`)).default 17 | Plugin = new Plugin() 18 | Plugin.plugin._path = filePath 19 | this.botModules.push(Plugin) 20 | /** 排序 */ 21 | this.botModules.sort((a, b) => a.plugin.priority - b.plugin.priority) 22 | logger.debug(`按钮模块 ${filePath} 已加载。`) 23 | } catch (error) { 24 | logger.error(`导入按钮模块 ${filePath} 时出错:${error.message}`) 25 | } 26 | } 27 | 28 | /** 卸载指定文件路径的模块 */ 29 | unloadModule (filePath) { 30 | const index = this.botModules.findIndex(module => module.plugin._path === filePath) 31 | if (index !== -1) this.botModules.splice(index, 1) 32 | /** 排序 */ 33 | this.botModules.sort((a, b) => a.plugin.priority - b.plugin.priority) 34 | } 35 | 36 | /** 37 | * 处理文件变化事件 38 | * @param {string} filePath - 文件路径 39 | * @param {string} eventType - 事件类型 ('add', 'change', 'unlink') 40 | */ 41 | async handleFileChange (filePath, eventType, state) { 42 | filePath = './' + filePath.replace(/\\/g, '/') 43 | if (filePath.endsWith('.js')) { 44 | if (eventType === 'add') { 45 | this.unloadModule(filePath) 46 | await this.loadModule(filePath) 47 | if (!state) logger.mark(`[Lain-plugin][新增按钮插件][${filePath}]`) 48 | } else if (eventType === 'add' || eventType === 'change') { 49 | this.unloadModule(filePath) 50 | await this.loadModule(filePath) 51 | logger.mark(`[Lain-plugin][修改按钮插件][${filePath}]`) 52 | } else if (eventType === 'unlink') { 53 | this.unloadModule(filePath) 54 | logger.mark(`[Lain-plugin][卸载按钮插件][${filePath}]`) 55 | } 56 | } 57 | } 58 | 59 | /** 初始化 */ 60 | async initialize () { 61 | try { 62 | const filesList = [] 63 | /** 遍历插件目录 */ 64 | const List = fs.readdirSync(this.plugin) 65 | for (let folder of List) { 66 | const folderPath = this.plugin + `/${folder}` 67 | /** 检查是否为文件夹 */ 68 | if (!fs.lstatSync(folderPath).isDirectory()) continue 69 | /** 保存插件包目录 */ 70 | filesList.push(this.plugin + `/${folder}/lain.support.js`) 71 | } 72 | 73 | /** 获取插件包内的文件夹,进行热更 */ 74 | const pluginList = fs.readdirSync(this.plugin + '/Lain-plugin/plugins') 75 | /** 支持插件包按钮 */ 76 | for (let folder of pluginList) { 77 | const folderPath = this.plugin + `/Lain-plugin/plugins/${folder}` 78 | /** 检查是否为文件夹 */ 79 | if (!fs.lstatSync(folderPath).isDirectory()) continue 80 | /** 保存 */ 81 | filesList.push(folderPath) 82 | } 83 | 84 | /** 热更新 */ 85 | filesList.map(folder => { 86 | let state = true 87 | const watcher = chokidar.watch(folder, { ignored: /[/\\]\./, persistent: true }) 88 | watcher 89 | .on('add', async filePath => { 90 | await this.handleFileChange(filePath, 'add', state) 91 | if (state) state = false 92 | }) 93 | .on('change', async filePath => await this.handleFileChange(filePath, 'change')) 94 | .on('unlink', async filePath => await this.handleFileChange(filePath, 'unlink')) 95 | 96 | return watcher 97 | }) 98 | 99 | return this.botModules 100 | } catch (error) { 101 | logger.error(`读取插件目录时出错:${error.message}`) 102 | } 103 | } 104 | } 105 | 106 | const plugin = new Button() 107 | export default plugin.botModules 108 | -------------------------------------------------------------------------------- /model/shamrock/shamrock.js: -------------------------------------------------------------------------------- 1 | import { GithubClient } from './client.js' 2 | 3 | export const SHAMROCK_OWNER = 'whitechi73' 4 | export const SHAMROCK_REPO = 'OpenShamrock' 5 | 6 | export class ShamrockRepoClient { 7 | constructor (key) { 8 | this.client = new GithubClient(key) 9 | this.cache = { 10 | commits: {} 11 | } 12 | } 13 | 14 | /** 15 | * 获取最近提交 16 | * @returns {Promise} 17 | */ 18 | async getCommits (num = 30, deal = false) { 19 | let commits = [] 20 | if (this.cache.commits?.[num]) { 21 | commits = this.cache.commits?.[num] 22 | } else { 23 | commits = await this.client.getCommits({ per_page: num }) 24 | this.cache.commits[num] = commits 25 | } 26 | if (deal) { 27 | commits.forEach(c => { 28 | c.commit.message = c.commit.message.replace(/#(\d+)/g, '#$1') 29 | }) 30 | } 31 | return commits 32 | } 33 | 34 | /** 35 | * 根据sha获取commit信息 36 | * @param sha 37 | * @returns {Promise} 38 | */ 39 | async getCommitBySha (sha) { 40 | if (this.cache[`commits-${sha}`]) { 41 | return this.cache[`commits-${sha}`] 42 | } 43 | let commit = await this.client.getCommitBySha(sha) 44 | this.cache[`commits-${sha}`] = commit 45 | return commit 46 | } 47 | 48 | /** 49 | * 获取当前版本已经落后最新多少个测试版本了 50 | * @param version 如 1.0.6-dev.b5a9884 或者直接b5a9884 51 | * @param type beta还是release,分别对应commit喝release 52 | * @returns {Promise} 比自己当前版本新的所有commits或release 53 | */ 54 | async getVersionBehind (version, type = 'beta') { 55 | if (this.cache[`newCommits-${type}`]) { 56 | return this.cache[`newCommits-${type}`] 57 | } 58 | const shaMatch = version.match(/[0-9a-f]{7,40}/) 59 | let sha = shaMatch ? shaMatch[0] : '' 60 | if (!sha) { 61 | console.error('错误的版本号格式,无法获取sha值') 62 | return null 63 | } 64 | // 当前版本对应的 commit 65 | let commit = await this.getCommitBySha(sha) 66 | let time = commit.commit.committer.date 67 | if (type === 'beta') { 68 | let newCommits = await this.client.getCommits({ since: time }) 69 | // last one commit is itself 70 | newCommits.pop() 71 | this.cache['newCommits-beta'] = newCommits 72 | return newCommits 73 | } else if (type === 'release') { 74 | let releases = await this.client.getReleases() 75 | let newReleases = releases.filter(r => r.created_at > time) 76 | this.cache['newCommits-release'] = newReleases 77 | return newReleases 78 | } 79 | throw new Error('unknown type ' + type) 80 | } 81 | 82 | /** 83 | * 获取当前仓库状态 84 | * @returns {Promise} 85 | */ 86 | async getRepoStatus () { 87 | if (this.cache.repo) { 88 | return this.cache.repo 89 | } 90 | let repo = await this.client.getRepository() 91 | this.cache.repo = repo 92 | return repo 93 | } 94 | 95 | /** 96 | * 获取release 97 | * @returns {Promise} 98 | */ 99 | async getRelease (num = 5, deal = false) { 100 | let releases = await this.client.getReleases({ per_page: num }) 101 | if (deal) { 102 | const regex = /@(\w+)/g 103 | releases.forEach(r => { 104 | let body = r.body 105 | if (body) { 106 | body = body 107 | .replaceAll('\r\n', '
') 108 | .replace(regex, '@$1') 109 | .replace(/#(\d+)/g, '#$1') 110 | r.body = body 111 | } 112 | }) 113 | } 114 | return releases 115 | } 116 | 117 | /** 118 | * 获取action产物 119 | * @param num 120 | * @returns {Promise<{total_count: number, artifacts: Object[]}>} 121 | */ 122 | async getActions (num = 3) { 123 | return await this.client.getActionsArtifacts({ per_page: num }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /adapter/WebSocket.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import fs from 'fs' 3 | import { createServer } from 'http' 4 | import Cfg from '../lib/config/config.js' 5 | import LagrangeCore from './LagrangeCore/index.js' 6 | import ComWeChat from './WeChat/index.js' 7 | import shamrock from './shamrock/index.js' 8 | import OneBotV11 from './OneBot-V11/onebotv11.js' 9 | 10 | class WebSocket { 11 | constructor () { 12 | this.port = Cfg.port 13 | this.path = { 14 | '/Shamrock': shamrock, 15 | '/ComWeChat': ComWeChat, 16 | '/LagrangeCore': LagrangeCore, 17 | '/OneBotV11': OneBotV11 18 | } 19 | } 20 | 21 | /** run! */ 22 | start () { 23 | this.server() 24 | } 25 | 26 | async server () { 27 | /** 保存监听器返回 */ 28 | lain.echo = new Map() 29 | /** 微信登录 */ 30 | Bot.lain.loginMap = new Map() 31 | /** 临时文件 */ 32 | lain.Files = new Map() 33 | /** 创建Express应用程序 */ 34 | const app = express() 35 | /** 创建HTTP服务器 */ 36 | this.Server = createServer(app) 37 | 38 | // 日志中间件 39 | app.use((req, res, next) => { 40 | const { method, url } = req 41 | const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress 42 | logger.info(`[请求日志] ${method} ${url} 来自IP: ${ip} 来自请求: ${req.headers.host}`) 43 | next() 44 | }) 45 | 46 | /** 设置静态文件服务 */ 47 | app.use('/api/File', express.static(process.cwd() + '/temp/FileToUrl')) 48 | 49 | /** QQBotApi */ 50 | app.get('/api/File/:token', async (req, res) => { 51 | const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress 52 | const token = req.params.token 53 | const filePath = process.cwd() + '/temp/FileToUrl/' + req.params.token 54 | /** 收到日志 */ 55 | logger.mark('[GET请求] ' + logger.blue(`[${token}] -> [${req.get('host')}] -> [${ip}]`)) 56 | 57 | try { 58 | /** 读 */ 59 | const File = lain.Files.get(filePath) 60 | 61 | /** 缓存有 */ 62 | if (File) { 63 | // res.setHeader('Content-Type', File.mime) 64 | res.setHeader('Content-Type', 'application/octet-stream') 65 | res.setHeader('Content-Disposition', 'inline') 66 | logger.mark('[发送文件] ' + logger.blue(`[${token}] => [${File.md5}] => [${ip}]`)) 67 | fs.createReadStream(filePath).pipe(res) 68 | } else { 69 | res.status(410).json({ status: 'failed', message: '资源过期' }) 70 | logger.mark('[请求返回] ' + logger.blue(`[${token}] => [文件已过期] => [${ip}]`)) 71 | } 72 | } catch (error) { 73 | res.status(500).json({ status: 'failed', message: '哎呀,报错了捏' }) 74 | logger.mark('[请求返回] ' + logger.blue(`[${token}] => [服务器内部错误] => [${ip}]`)) 75 | logger.error(error) 76 | } 77 | }) 78 | 79 | /** 将WebSocket服务器实例与HTTP服务器关联 */ 80 | this.Server.on('upgrade', (request, socket, head) => { 81 | const pathname = request.url 82 | if (!this.path[pathname]) { 83 | logger.error(`未知连接,已拒绝连接:${request.url}`) 84 | return socket.destroy() 85 | } 86 | 87 | this.path[pathname].handleUpgrade(request, socket, head, (socket) => { 88 | this.path[pathname].emit('connection', socket, request) 89 | }) 90 | }) 91 | 92 | this.Server.listen(this.port, async () => this.log()) 93 | 94 | /** 捕获错误 */ 95 | this.Server.on('error', async (error) => { 96 | if (error.code === 'EADDRINUSE') { 97 | logger.error(`[Lain-plugin] 端口${this.port}已被占用请自行解除`) 98 | } else { 99 | logger.error(error) 100 | } 101 | }) 102 | } 103 | 104 | /** 打印启动日志 */ 105 | log () { 106 | logger.info('Lain-plugin', `HTTP服务器:${logger.blue(`http://localhost:${this.port}`)}`) 107 | /** 转为数组对象,循环打印 */ 108 | Object.entries(this.path).forEach(([key, value]) => { 109 | logger.info('Lain-plugin', `本地 ${key.replace('/', '')} 连接地址:${logger.blue(`ws://localhost:${this.port}${key}`)}`) 110 | }) 111 | } 112 | } 113 | 114 | export default new WebSocket() 115 | -------------------------------------------------------------------------------- /adapter/QQBot/QQSDK.js: -------------------------------------------------------------------------------- 1 | import { Bot as QQBot } from 'qq-official-bot' 2 | import Cfg from '../../../../lib/config/config.js' 3 | 4 | export default class QQSDK { 5 | constructor (config) { 6 | this.config = config 7 | } 8 | 9 | async start () { 10 | /** appid */ 11 | this.id = this.config.appid 12 | /** QQBotID */ 13 | this.QQBot = this.config.appid 14 | /** QQGuidID */ 15 | this.QQGuid = `qg_${this.config.appid}` 16 | /** 最大重连次数 */ 17 | this.config.maxRetry = this.config.maxRetry || 10 18 | /** 日志等级 */ 19 | this.config.logLevel = Cfg.bot.log_level 20 | /** 监听事件 */ 21 | this.config.intents = [] 22 | /** 离线自动重连次数 */ 23 | let autoRetryCount = 0 24 | 25 | /** 是否启用群 */ 26 | if (this.config.type == 0 || this.config.type == 2) { 27 | /** 群私聊事件 */ 28 | this.config.intents.push('C2C_MESSAGE_CREATE') 29 | /** 群@消息事件 */ 30 | this.config.intents.push('GROUP_AT_MESSAGE_CREATE') 31 | /** 群按钮点击回调事件 */ 32 | this.config.intents.push('INTERACTION') 33 | } 34 | 35 | /** 是否启用频道 */ 36 | if (this.config.type == 0 || this.config.type == 1) { 37 | /** 频道变更事件 */ 38 | this.config.intents.push('GUILDS') 39 | /** 频道成员变更事件 */ 40 | this.config.intents.push('GUILD_MEMBERS') 41 | /** 频道私信事件 */ 42 | this.config.intents.push('DIRECT_MESSAGE') 43 | /** 频道消息表态事件 */ 44 | this.config.intents.push('GUILD_MESSAGE_REACTIONS') 45 | /** 公域 私域事件 */ 46 | this.config.allMsg ? this.config.intents.push('GUILD_MESSAGES', 'FORUMS_EVENTS') : this.config.intents.push('PUBLIC_GUILD_MESSAGES', 'OPEN_FORUMS_EVENTS') 47 | } 48 | 49 | /** 创建机器人 */ 50 | this.sdk = new QQBot(this.config) 51 | /** 连接机器人 */ 52 | if (!Bot.adapter.includes(String(this.id)) && !Bot.adapter.includes(`qg_${this.id}`)) { 53 | await this.sdk.start() 54 | /** 实现自动重连(10秒后运行每1分钟定时检测) */ 55 | if (this.config.mode === 'websocket' && this.config.autoRetry && this.config.autoRetryTime > 0) { 56 | setTimeout(() => { 57 | this.sdk.timer = setInterval(async () => { 58 | if (![0, 1].includes(this.sdk.receiver?.handler?.ws?.readyState)) { 59 | lain.warn(this.id, "检测到账号离线,已自动重连", this.sdk.receiver?.handler?.ws?.readyState, ++autoRetryCount) 60 | await this.sdk.stop() 61 | await lain.sleep(10) 62 | await this.sdk.start() 63 | } 64 | }, this.config.autoRetryTime * 1000) 65 | }, 10 * 1000) 66 | } 67 | } 68 | /** 修改sdk日志为喵崽日志 */ 69 | this.sdk.logger = { 70 | info: (...log) => this.logger(...log), 71 | trace: (...log) => lain.trace(this.id, ...log), 72 | debug: (...log) => lain.debug(this.id, ...log), 73 | mark: (...log) => lain.mark(this.id, ...log), 74 | warn: (...log) => lain.warn(this.id, ...log), 75 | error: (...log) => lain.error(this.id, ...log), 76 | fatal: (...log) => lain.fatal(this.id, ...log) 77 | } 78 | } 79 | 80 | /** 修改一下日志 */ 81 | logger (...data) { 82 | let msg = data[0] 83 | 84 | if (typeof msg !== 'string' || data.length > 1) return lain.info(this.id, ...data) 85 | 86 | msg = msg.trim().replace(/base64:\/\/.*?(,|]|")/g, 'base64://...$1') 87 | try { 88 | if (/^(recv from Group|recv from Guild|recv from User|recv from Direct)/.test(msg)) { 89 | return '' 90 | } else if (/^send to Group/.test(msg)) { 91 | msg = msg.replace(/^send to Group\([^)]+\): /, `<发送群聊: ${msg.match(/\(([^)]+)\)/)[1]}> => `) 92 | } else if (/^send to User/.test(msg)) { 93 | msg = msg.replace(/^send to User\([^)]+\): /, `<发送私聊: ${msg.match(/\(([^)]+)\)/)[1]}> => `) 94 | } else if (/^send to Channel/.test(msg)) { 95 | msg = msg.replace(/^send to Channel\([^)]+\): /, `<发送频道: ${msg.match(/\(([^)]+)\)/)[1]}> => `) 96 | } else if (/^send to Direct/.test(msg)) { 97 | msg = msg.replace(/^send to Direct\([^)]+\): /, `<发送私信: ${msg.match(/\(([^)]+)\)/)[1]}> => `) 98 | } 99 | } catch { } 100 | return lain.info(this.id, msg) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import chalk from 'chalk' 3 | // import { exec } from 'child_process' 4 | // import { Restart } from '../../other/restart.js' 5 | // import { AdapterRestart } from '../apps/restart.js' 6 | 7 | const _path = process.cwd() + '/plugins/Lain-plugin' 8 | 9 | const isNull = i => i !== null && i !== '' && i !== undefined 10 | 11 | /** 全局变量lain */ 12 | global.lain = { 13 | _path, 14 | _pathCfg: _path + '/config/config', 15 | /** 16 | * 休眠函数 17 | * @param ms 毫秒 18 | */ 19 | sleep: function (ms) { 20 | return new Promise((resolve) => setTimeout(resolve, ms)) 21 | }, 22 | nickname: function (id) { 23 | return chalk.hex('#868ECC')(Bot?.[id]?.nickname ? `<${Bot?.[id]?.nickname}:${id}>` : (id ? `` : '')) 24 | }, 25 | info: function (id, ...log) { 26 | logger.info(...[this.nickname(id) || '', ...log].filter(isNull)) 27 | }, 28 | mark: function (id, ...log) { 29 | logger.mark(...[this.nickname(id) || '', ...log].filter(isNull)) 30 | }, 31 | error: function (id, ...log) { 32 | logger.error(...[this.nickname(id) || '', ...log].filter(isNull)) 33 | }, 34 | warn: function (id, ...log) { 35 | logger.warn(...[this.nickname(id) || '', ...log].filter(isNull)) 36 | }, 37 | debug: function (id, ...log) { 38 | logger.debug(...[this.nickname(id) || '', ...log].filter(isNull)) 39 | }, 40 | trace: function (id, ...log) { 41 | logger.trace(...[this.nickname(id) || '', ...log].filter(isNull)) 42 | }, 43 | fatal: function (id, ...log) { 44 | logger.fatal(...[this.nickname(id) || '', ...log].filter(isNull)) 45 | }, 46 | em: function (name = '', data) { 47 | while (true) { 48 | Bot.emit(name, data) 49 | let i = name.lastIndexOf('.') 50 | if (i === -1) { 51 | break 52 | } 53 | name = name.slice(0, i) 54 | } 55 | } 56 | } 57 | 58 | /** 还是修改一下,不然cvs这边没法用... */ 59 | if (!fs.existsSync('./plugins/ws-plugin/model/dlc/index.js') && 60 | !fs.existsSync('./plugins/ws-plugin/model/red/index.js')) { 61 | const getGroupMemberInfo = Bot.getGroupMemberInfo 62 | Bot.getGroupMemberInfo = async function (group_id, user_id) { 63 | try { 64 | return await getGroupMemberInfo.call(this, group_id, user_id) 65 | } catch (error) { 66 | let nickname 67 | error?.stack?.includes('ws-plugin') ? nickname = 'chronocat' : nickname = 'Yunzai-Bot' 68 | return { 69 | group_id, 70 | user_id, 71 | nickname, 72 | card: nickname, 73 | sex: 'female', 74 | age: 6, 75 | join_time: '', 76 | last_sent_time: '', 77 | level: 1, 78 | role: 'member', 79 | title: '', 80 | title_expire_time: '', 81 | shutup_time: 0, 82 | update_time: '', 83 | area: '南极洲', 84 | rank: '潜水' 85 | } 86 | } 87 | } 88 | } 89 | 90 | /* 91 | Restart.prototype.restart = async function () { 92 | if (this.e?.adapter) { 93 | let adapter = new AdapterRestart() 94 | adapter.restart.call(this) 95 | } else { 96 | await this.e.reply('开始执行重启,请稍等...') 97 | logger.mark(`${this.e.logFnc} 开始执行重启,请稍等...`) 98 | 99 | await redis.set(this.key, JSON.stringify({ 100 | uin: this.e?.bot?.uin || this.e?.self_id || Bot.uin, 101 | isGroup: !!this.e.isGroup, 102 | id: this.e.isGroup ? this.e.group_id : this.e.user_id, 103 | msg_id: this.e.message_id, 104 | time: Date.now(), 105 | })) 106 | 107 | let npm = await this.checkPnpm() 108 | 109 | try { 110 | let cm = `${npm} start` 111 | if (process.argv[1].includes('pm2')) { 112 | cm = `${npm} run restart` 113 | } 114 | 115 | exec(cm, { windowsHide: true }, (error, stdout, stderr) => { 116 | if (error) { 117 | redis.del(this.key) 118 | this.e.reply(`操作失败!\n${error.stack}`) 119 | logger.error(`重启失败\n${error.stack}`) 120 | } else if (stdout) { 121 | logger.mark('重启成功,运行已由前台转为后台') 122 | logger.mark(`查看日志请用命令:${npm} run log`) 123 | logger.mark(`停止后台运行命令:${npm} stop`) 124 | process.exit() 125 | } 126 | }) 127 | } catch (error) { 128 | redis.del(this.key) 129 | let e = error.stack ?? error 130 | this.e.reply(`操作失败!\n${e}`) 131 | } 132 | 133 | return true 134 | } 135 | } 136 | */ 137 | -------------------------------------------------------------------------------- /model/YamlHandler.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import Yaml from 'yaml' 3 | import lodash from 'lodash' 4 | 5 | /** 6 | * YAML 文件处理器类,提供读取、修改和保存 YAML 文件的功能。 7 | */ 8 | export default class YamlHandler { 9 | /** 10 | * 构造函数,接受 YAML 文件路径作为参数。 11 | * @param {string} _path - YAML 文件路径 12 | */ 13 | constructor (_path) { 14 | /** 15 | * YAML 文件路径。 16 | * @type {string} 17 | * @private 18 | */ 19 | this._path = _path 20 | this.parse() 21 | } 22 | 23 | /** 24 | * 解析 YAML 文件内容。 25 | * @private 26 | */ 27 | parse () { 28 | /** 29 | * YAML 文件的解析文档。 30 | * @type {Yaml.Document} 31 | */ 32 | this.document = Yaml.parseDocument(fs.readFileSync(this._path, 'utf8')) 33 | } 34 | 35 | /** 36 | * 获取 YAML 文件的 JSON 数据。 37 | * @returns {object} - YAML 文件的 JSON 数据 38 | */ 39 | data () { 40 | return this.document.toJSON() 41 | } 42 | 43 | /** 44 | * 获取指定键的值。 45 | * @param {string} key - 指定的键 46 | * @returns {*} - 指定键的值 47 | */ 48 | get (key, key2 = '*') { 49 | const ret = lodash.get(this.data(), key) 50 | return key2 === '*' ? ret : ret?.[key2] ?? {} 51 | } 52 | 53 | /** 54 | * 检查指定键是否存在。 55 | * @param {string} key - 指定的键 56 | * @returns {boolean} - 指定键是否存在 57 | */ 58 | hasIn (key) { 59 | key = key.split('.') 60 | return this.document.hasIn(key) 61 | } 62 | 63 | /** 64 | * 检查指定的键是否存在对应的值。 65 | * @param {string} key - 需要检查的键 66 | * @param {string} value - 需要检查的值 67 | * @returns {boolean} - 指定键是否存在指定值 68 | */ 69 | value (key, value) { 70 | const res = this.get(key) 71 | if (!res) return false 72 | if (Array.isArray(res)) { 73 | return !!res.includes(value) 74 | } 75 | return !!res[value] 76 | } 77 | 78 | /** 79 | * 设置指定键的值。 80 | * @param {string} key - 指定的键 81 | * @param {*} value - 要设置的值 82 | */ 83 | set (key, value) { 84 | key = key.split('.') 85 | this.document.setIn(key, value) 86 | this.save() 87 | } 88 | 89 | /** 90 | * 在指定键的位置添加新的值,不能是不存在的键。 91 | * @param {string} key - 指定的键 92 | * @param {*} value - 要添加的值 93 | */ 94 | addIn (key, value) { 95 | key = key.split('.') 96 | this.document.addIn(key, value) 97 | this.save() 98 | } 99 | 100 | /** 101 | * 在指定键的位置添加新的键值对。 102 | * @param {string} key - 指定的键 103 | * @param {*} val - 要添加的键值对 104 | * @param {Array|object|string} type - 用于初始值为空的时候初始化,默认数组 105 | */ 106 | addVal (key, val, type = 'Array') { 107 | let value = this.get(key) 108 | 109 | /** 值为空,进行初始化 */ 110 | if (!value) { 111 | if (type === 'Array') { 112 | value = [] 113 | } else if (type === 'object') { 114 | value = {} 115 | } else if (type === 'string') { 116 | value = '' 117 | } else { 118 | value = [] 119 | } 120 | } 121 | 122 | if (Array.isArray(value)) { 123 | value.push(val) 124 | } else if (typeof value === 'object') { 125 | value = { ...value, ...val } 126 | } else if (typeof value === 'object') { 127 | value = val 128 | } 129 | 130 | this.set(key, value) 131 | } 132 | 133 | /** 134 | * 删除指定键及其对应的值。 135 | * @param {string} key - 指定的键 136 | */ 137 | del (key) { 138 | key = key.split('.') 139 | this.document.deleteIn(key) 140 | this.save() 141 | } 142 | 143 | /** 144 | * 删除指定键的特定值。 145 | * @param {string} key - 指定的键 146 | * @param {*} val - 要删除的值 147 | */ 148 | delVal (key, val) { 149 | const value = this.get(key) 150 | if (Array.isArray(value)) { 151 | const index = value.indexOf(val) 152 | if (index !== -1) { 153 | value.splice(index, 1) 154 | this.set(key, value) 155 | } else { 156 | logger.error(`Value ${val} does not exist in the array.`) 157 | } 158 | } else if (typeof value === 'object' && value !== null) { 159 | delete value[val] 160 | this.set(key, value) 161 | } else { 162 | logger.error('Cannot delete key/value from non-object or non-array.') 163 | } 164 | } 165 | 166 | /** 167 | * 将修改后的 YAML 文件保存到磁盘。 168 | */ 169 | save () { 170 | try { 171 | fs.writeFileSync(this._path, this.document.toString(), 'utf8') 172 | } catch (err) { 173 | logger.error(`Failed to update YAML: ${err?.message}`) 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /apps/shamrock.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import common from '../lib/common/common.js' 4 | import Cfg from '../lib/config/config.js' 5 | import Render from '../model/render.js' 6 | import { ShamrockRepoClient } from '../model/shamrock/shamrock.js' 7 | 8 | export class shamrock extends plugin { 9 | constructor () { 10 | super({ 11 | name: '铃音Shamrock版本信息', 12 | priority: -50, 13 | rule: [ 14 | { 15 | reg: /^#(shamrock|三叶草)(版本|更新日志)$/gi, 16 | fnc: 'version' 17 | }, 18 | { 19 | reg: /^#(shamrock|三叶草)(发布|测试)?(安装包|apk|APK)$/gi, 20 | fnc: 'apk' 21 | } 22 | ] 23 | }) 24 | } 25 | 26 | async version (e) { 27 | let self_id 28 | for (const i of Bot.uin2) if (Bot[i].adapter === 'shamrock') self_id = i 29 | 30 | try { 31 | let version = Bot[self_id]?.version?.version || '1.0.7.r228.d44150e' 32 | let qqVer = Bot[self_id]?.apk?.version || '8.9.63' 33 | let client = new ShamrockRepoClient(Cfg.Shamrock.githubKey) 34 | let versionBehindBeta = await client.getVersionBehind(version, 'beta') 35 | let versionBehindRelease = await client.getVersionBehind(version, 'release') 36 | let releases = await client.getRelease(3, true) 37 | let commits = await client.getCommits(10, false) 38 | let repo = await client.getRepoStatus() 39 | return await Render.render('shamrock/index', { 40 | elem: 'anemo', 41 | releases, 42 | commits, 43 | repo, 44 | versionBehind: { 45 | beta: versionBehindBeta.length, 46 | release: versionBehindRelease.length 47 | }, 48 | version, 49 | qqVer 50 | }, { e, scale: 1.2 }) 51 | } catch (err) { 52 | console.error(err) 53 | await e.reply(err.message, true) 54 | } 55 | } 56 | 57 | async apk (e) { 58 | // 不用shamrock也能用吧? 59 | // if (e.adapter !== 'shamrock') { 60 | // return false 61 | // } 62 | let filePath 63 | if (!e.msg.includes('测试')) { 64 | // release 65 | let client = new ShamrockRepoClient(Cfg.Shamrock.githubKey) 66 | let releases = await client.getRelease(1, true) 67 | let release = releases[0] 68 | let allAssets = release.assets.find(a => a.name.includes('all')) 69 | let url = allAssets.browser_download_url 70 | let name = allAssets.name 71 | const _path = process.cwd() 72 | let dest = path.join(_path, 'data', 'lain', 'shamrock', 'release', name) 73 | if (fs.existsSync(dest)) { 74 | filePath = dest 75 | } else { 76 | await e.reply('开始下载安装包 ' + name, true) 77 | try { 78 | filePath = await common.downloadFile(url, 'shamrock/release/' + name) 79 | } catch (err) { 80 | console.error(err) 81 | await e.reply('安装包下载错误:' + err.message) 82 | return 83 | } 84 | } 85 | } else { 86 | // beta 87 | if (!Cfg.Shamrock.githubKey) { 88 | await e.reply('未配置github access token无法下载最新测试版shamrock') 89 | return 90 | } 91 | let client = new ShamrockRepoClient(Cfg.Shamrock.githubKey) 92 | let actions = await client.getActions(10) 93 | let latestAll = actions.artifacts.find(a => a.name.includes('all')) 94 | let url = latestAll.archive_download_url 95 | let name = latestAll.name 96 | const _path = process.cwd() 97 | let dest = path.join(_path, 'data', 'lain', 'shamrock', 'beta', name + '.zip') 98 | if (fs.existsSync(dest)) { 99 | filePath = dest 100 | } else { 101 | await e.reply('开始下载安装包 ' + name + '.zip', true) 102 | // todo move to client 103 | try { 104 | filePath = await common.downloadFile(url, 'shamrock/beta/' + name + '.zip', { Authorization: `bearer ${Cfg.Shamrock.githubKey}` }) 105 | } catch (err) { 106 | console.error(err) 107 | await e.reply('安装包下载错误:' + err.message) 108 | return 109 | } 110 | } 111 | } 112 | if (!filePath) { 113 | console.error('获取安装包下载地址失败') 114 | await e.reply('获取安装包下载地址失败') 115 | return 116 | } 117 | if (e.isGroup || e.group) { 118 | await e.group.sendFile(filePath) 119 | } else { 120 | await e.friend.sendFile(filePath) 121 | } 122 | // 30分钟后删除 123 | setTimeout(() => { 124 | if (filePath) { 125 | fs.unlinkSync(filePath) 126 | } 127 | }, 30 * 60 * 1000) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /model/shamrock/client.js: -------------------------------------------------------------------------------- 1 | import cfg from '../../../../lib/config/config.js' 2 | import { HttpsProxyAgent } from 'https-proxy-agent' 3 | import { SHAMROCK_OWNER, SHAMROCK_REPO } from './shamrock.js' 4 | import fetch from 'node-fetch' 5 | 6 | export class GithubClient { 7 | constructor (key) { 8 | this.key = key 9 | let proxy = cfg.bot.proxyAddress 10 | this.client = { 11 | request: (url, options = {}) => { 12 | const defaultOptions = proxy 13 | ? { 14 | agent: new HttpsProxyAgent(proxy) 15 | } 16 | : {} 17 | const mergedOptions = { 18 | ...defaultOptions, 19 | ...options 20 | } 21 | 22 | return fetch(url, mergedOptions) 23 | } 24 | } 25 | this.commonHeaders = { 26 | 'X-GitHub-Api-Version': '2022-11-28', 27 | Accept: 'application/vnd.github+json' 28 | } 29 | if (this.key) { 30 | this.commonHeaders.Authorization = `Bearer ${this.key}` 31 | } 32 | } 33 | 34 | /** 35 | * 获取仓库详情 36 | * @param owner 37 | * @param repo 38 | * @returns {Promise} 39 | */ 40 | async getRepository (owner = SHAMROCK_OWNER, repo = SHAMROCK_REPO) { 41 | let res = await this.client.request(`https://api.github.com/repos/${owner}/${repo}`, { 42 | headers: this.commonHeaders 43 | }) 44 | return await this.toJson(res) 45 | } 46 | 47 | /** 48 | * 获取仓库commits信息 49 | * @see https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28 50 | * @param owner 51 | * @param repo 52 | * @param options 可选参数:since, until, per_page, page, sha等 53 | * @returns {Promise} 54 | */ 55 | async getCommits (options = {}, owner = SHAMROCK_OWNER, repo = SHAMROCK_REPO) { 56 | let res = await this.client.request(`https://api.github.com/repos/${owner}/${repo}/commits${this.query(options)}`, { 57 | headers: this.commonHeaders 58 | }) 59 | return await this.toJson(res) 60 | } 61 | 62 | /** 63 | * 获取仓库某个commit信息 64 | * @see https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit 65 | * @param owner 66 | * @param repo 67 | * @param sha commit sha 68 | * @returns {Promise} 69 | */ 70 | async getCommitBySha (sha, owner = SHAMROCK_OWNER, repo = SHAMROCK_REPO) { 71 | if (!sha) { 72 | throw new Error('sha cannot be empty') 73 | } 74 | let res = await this.client.request(`https://api.github.com/repos/${owner}/${repo}/commits/${sha}`, { 75 | headers: this.commonHeaders 76 | }) 77 | return await this.toJson(res) 78 | } 79 | 80 | /** 81 | * 获取仓库releases信息 82 | * @see https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28 83 | * @param owner 84 | * @param repo 85 | * @param options 可选参数:per_page, page 86 | * @returns {Promise} 87 | */ 88 | async getReleases (options = {}, owner = SHAMROCK_OWNER, repo = SHAMROCK_REPO) { 89 | let res = await this.client.request(`https://api.github.com/repos/${owner}/${repo}/releases${this.query(options)}`, { 90 | headers: this.commonHeaders 91 | }) 92 | return await this.toJson(res) 93 | } 94 | 95 | /** 96 | * 获取仓库action artifacts信息 97 | * @see https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28 98 | * @param owner 99 | * @param repo 100 | * @param options 可选参数:per_page, page, name 101 | * @returns {Promise} 102 | */ 103 | async getActionsArtifacts (options = {}, owner = SHAMROCK_OWNER, repo = SHAMROCK_REPO) { 104 | let res = await this.client.request(`https://api.github.com/repos/${owner}/${repo}/actions/artifacts${this.query(options)}`, { 105 | headers: this.commonHeaders 106 | }) 107 | return await this.toJson(res) 108 | } 109 | 110 | /** 111 | * params to query string 112 | * @param params 113 | * @param containsQuestionMark 结果前面是否包含? 114 | * @returns {string} 115 | */ 116 | query (params, containsQuestionMark = true) { 117 | if (!params || typeof params !== 'object') { 118 | return '' 119 | } 120 | let q = '' 121 | Object.keys(params).forEach(k => { 122 | if (q) { 123 | q += '&' 124 | } 125 | q += `${k}=${params[k]}` 126 | }) 127 | if (containsQuestionMark) { 128 | return q ? `?${q}` : '' 129 | } 130 | return q 131 | } 132 | 133 | /** 134 | * 135 | * @param {Response} res 136 | * @returns {Promise} 137 | */ 138 | async toJson (res) { 139 | if (res.status === 200) { 140 | return await res.json() 141 | } else if (res.status === 429 || (await res.text())?.includes('limited')) { 142 | throw new Error('Github API 访问速率超限,您可以配置免费的Github personal access token以将访问速率从60/小时提升至5,000/小时') 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /adapter/WeChat/api.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto' 2 | 3 | const api = { 4 | /** 获取支持的动作列表 */ 5 | async get_supported_actions () { 6 | const params = {} 7 | return await this.SendApi(params, 'get_supported_actions') 8 | }, 9 | /** 获取运行状态 */ 10 | async get_status () { 11 | const params = {} 12 | return await this.SendApi(params, 'get_status') 13 | }, 14 | /** 获取版本信息 */ 15 | async get_version () { 16 | const params = {} 17 | return await this.SendApi(params, 'get_version') 18 | }, 19 | 20 | /** 获取机器人自身信息 */ 21 | async get_self_info () { 22 | const params = {} 23 | return await this.SendApi(params, 'get_self_info') 24 | }, 25 | /** 获取好友信息 */ 26 | async get_user_info (user_id) { 27 | const params = { user_id } 28 | return await this.SendApi(params, 'get_user_info') 29 | }, 30 | /** 获取好友列表 */ 31 | async get_friend_list () { 32 | const params = {} 33 | return await this.SendApi(params, 'get_friend_list') 34 | }, 35 | 36 | /** 获取群信息 */ 37 | async get_group_info (group_id) { 38 | const params = { group_id } 39 | return await this.SendApi(params, 'get_group_info') 40 | }, 41 | /** 获取群列表 */ 42 | async get_group_list () { 43 | const params = {} 44 | return await this.SendApi(params, 'get_group_list') 45 | }, 46 | /** 获取群成员信息 */ 47 | async get_group_member_info (group_id, user_id) { 48 | const params = { group_id, user_id } 49 | return await this.SendApi(params, 'get_group_member_info') 50 | }, 51 | /** 获取群成员列表 */ 52 | async get_group_member_list (group_id) { 53 | const params = { group_id } 54 | return await this.SendApi(params, 'get_group_member_list') 55 | }, 56 | /** 设置群名称 */ 57 | async set_group_name (group_id, group_name) { 58 | const params = { group_id, group_name } 59 | return await this.SendApi(params, 'set_group_name') 60 | }, 61 | /** 上传文件 */ 62 | async upload_file (type, name, file) { 63 | const params = { type, name, [type]: file } 64 | return await this.SendApi(params, 'upload_file') 65 | }, 66 | /** 获取文件 */ 67 | async get_file (type, file_id) { 68 | const params = { type, file_id } 69 | return await this.SendApi(params, 'get_file') 70 | }, 71 | /** 通过好友请求 */ 72 | async accept_friend (v3, v4) { 73 | const params = { v3, v4 } 74 | return await this.SendApi(params, 'wx.accept_friend') 75 | }, 76 | /** 获取微信版本 */ 77 | async get_wechat_version () { 78 | const params = {} 79 | return await this.SendApi(params, 'wx.get_wechat_version') 80 | }, 81 | /** 设置微信版本号 */ 82 | async set_wechat_version (version) { 83 | const params = { version } 84 | return await this.SendApi(params, 'wx.set_wechat_version') 85 | }, 86 | /** 删除好友 */ 87 | async delete_friend (user_id) { 88 | const params = { user_id } 89 | return await this.SendApi(params, 'wx.delete_friend') 90 | }, 91 | /** 设置群昵称 */ 92 | async set_group_nickname (group_id, nickname) { 93 | const params = { group_id, nickname } 94 | return await this.SendApi(params, 'wx.set_group_nickname') 95 | }, 96 | /** 发送消息 */ 97 | async send_message (type, id, message) { 98 | const ty = { 99 | group: 'group', 100 | private: 'private', 101 | 'wx.get_group_poke': 'group', 102 | 'wx.get_private_poke': 'private' 103 | } 104 | /** 群消息、好友消息 */ 105 | let send_type = ty[type] ?? 'group' 106 | /** 群id、好友id */ 107 | let msg_type = send_type === 'private' ? 'user_id' : 'group_id' 108 | const params = { detail_type: send_type, [msg_type]: id, message } 109 | /** 将发送的消息转为适合人类阅读的日志 */ 110 | let concat = '' 111 | for (const i of message) { 112 | if (i.type === 'mention') { 113 | concat += `{at:${i.data.user_id}}` 114 | } else if (i.type === 'image') { 115 | concat += `{image:${i.data.file_id}}` 116 | } else if (i.type === 'text') { 117 | concat += i.data.text 118 | } else { 119 | concat += JSON.stringify(i) 120 | } 121 | } 122 | lain.info(Bot.lain.wc.uin, `发送${send_type === 'private' ? '好友消息' : '群消息'}:<${id}> ${concat}`) 123 | return await this.SendApi(params, 'send_message') 124 | }, 125 | /** 发送请求事件 */ 126 | async SendApi (params, action) { 127 | const echo = randomUUID() 128 | 129 | Bot.lain.wc.send(JSON.stringify({ echo, action, params })) 130 | 131 | for (let i = 0; i < 40; i++) { 132 | const data = await lain.echo.get(echo) 133 | if (data) { 134 | lain.echo.delete(echo) 135 | try { 136 | if (Object.keys(data?.data).length > 0 && data?.data) return data.data 137 | return data 138 | } catch { 139 | return data 140 | } 141 | } else { 142 | await new Promise((resolve) => setTimeout(resolve, 1000)) 143 | } 144 | } 145 | /** 获取失败 */ 146 | return '获取失败' 147 | } 148 | } 149 | 150 | export default api 151 | -------------------------------------------------------------------------------- /resources/shamrock/index.html: -------------------------------------------------------------------------------- 1 | {{extend elemLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | {{/block}} 6 | 7 | {{block 'main'}} 8 |
9 |
10 | 11 |
12 |
{{version}}
13 |
14 | 15 | https://github.com/whitechi73/OpenShamrock 16 |
17 |
18 | 19 | {{repo.stargazers_count}} 20 | 21 | 22 | {{repo.forks_count}} 23 | 24 | 25 | {{repo.subscribers_count}} 26 |
27 | 28 |
29 |
30 |
31 |
32 | 版本摘要 33 |
34 |
35 |
36 |
37 | {{if versionBehind.beta > 0 }} 38 | · 当前使用的版本({{version}})落后最新测试版{{versionBehind.beta}}个提交 39 | {{else}} 40 | · 当前使用的版本({{version}})已经是最新测试版啦 41 | {{/if}} 42 |
43 |
44 | {{if versionBehind.release > 0 }} 45 | · 当前使用的版本({{version}})落后最新发布版{{versionBehind.release}}个版本 46 | {{else}} 47 | · 当前使用的版本({{version}})已经是最新版啦或者比最新版还新 48 | {{/if}} 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 | 发布版本(近3个版本) 57 |
58 | {{each releases release}} 59 |
60 |
61 |
62 | {{release.name}} 63 | 64 | {{release.published_at}} 65 | 66 |
67 |
68 | {{@release.body}} 69 |
70 |
71 |
72 | {{/each}} 73 |
74 | 75 |
76 |
77 | 测试版本(近10次提交) 78 |
79 |
80 | {{each commits commit}} 81 |
82 |
83 | 84 |
85 | {{commit.sha.slice(0, 8)}} 86 |
87 | 88 | {{commit.commit.author.date}} 89 | 90 |
91 |
92 | 93 | {{commit.author.login}} 94 | · {{commit.commit.message}} 95 |
96 |
97 | {{/each}} 98 |
99 |
100 | 101 |
102 |
103 | 更多命令 104 |
105 |
106 |
107 |
108 |
109 | #shamrock安装包 110 |
111 | 112 | 下载最新版release安装包 113 | 114 |
115 |
116 |
117 | #shamrock测试安装包 118 |
119 | 120 | 下载最新测试版安装包,需要配置github APIKey 121 | 122 |
123 |
124 |
125 |
126 | 127 |
128 | {{/block}} -------------------------------------------------------------------------------- /adapter/WeChat/sendMsg.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import fetch from 'node-fetch' 4 | import api from './api.js' 5 | import common from '../../model/common.js' 6 | import { fileTypeFromBuffer } from 'file-type' 7 | 8 | export default class SendMsg { 9 | /** 传入基本配置 */ 10 | constructor (id, data) { 11 | /** 开发者id */ 12 | this.id = id 13 | const { group_id, detail_type, user_id } = data 14 | this.group_id = group_id 15 | this.user_id = user_id 16 | this.detail_type = detail_type 17 | } 18 | 19 | /** 发送消息 */ 20 | async message (msg) { 21 | /** 将云崽过来的消息统一为数组 */ 22 | msg = await common.array(msg) 23 | /** 发送 */ 24 | return await this.wc_msg(msg) 25 | } 26 | 27 | /** 转换格式并发送消息 */ 28 | async wc_msg (msg) { 29 | const content = [] 30 | /** 单独存储多图片,严格按照图片顺序进行发送 */ 31 | const ArrImg = [] 32 | 33 | /** chatgpt-plugin */ 34 | if (msg?.[0].type === 'xml') msg = msg?.[0].msg 35 | 36 | for (const i of msg) { 37 | /** 加个延迟防止过快 */ 38 | await lain.sleep(200) 39 | switch (i.type) { 40 | case 'at': 41 | content.push({ 42 | type: 'mention', 43 | data: { user_id: String(i.qq) == 0 ? i.id : i.qq } 44 | }) 45 | break 46 | case 'face': 47 | content.push(`[emoji:${i.text}]`) 48 | break 49 | case 'text': 50 | content.push({ 51 | type: 'text', 52 | data: { text: i.text.replace(' 0 ? `\n${i.text.replace(' 0) { 92 | res = await api.send_message(this.detail_type, this.group_id || this.user_id, content) 93 | try { await common.MsgTotal(this.id, 'ComWeChat') } catch { } 94 | } 95 | 96 | await lain.sleep(200) 97 | 98 | /** 发送图片 */ 99 | if (ArrImg.length > 0) { 100 | res = await api.send_message(this.detail_type, this.group_id || this.user_id, ArrImg) 101 | try { await common.MsgTotal(this.id, 'ComWeChat', 'image') } catch { } 102 | } 103 | return res 104 | } 105 | 106 | /** 上传图片获取图片id */ 107 | async get_file_id (i) { 108 | let name 109 | let type = 'data' 110 | let file = i.file 111 | 112 | if (i.file?.type === 'Buffer') { 113 | /** 特殊格式?... */ 114 | file = `base64://${Buffer.from(i.file.data).toString('base64')}` 115 | } else if (i.file instanceof Uint8Array) { 116 | /** 将二进制的base64转字符串 防止报错 */ 117 | file = `base64://${Buffer.from(i.file).toString('base64')}` 118 | } else if (i.file instanceof fs.ReadStream) { 119 | /** 天知道从哪里蹦出来的... */ 120 | file = `./${i.file.path}` 121 | } else if (typeof i.file === 'string') { 122 | /** 去掉本地图片的前缀 */ 123 | file = i.file.replace(/^file:\/\//, '') || i.url 124 | if (fs.existsSync(i.file.replace(/^file:\/\//, ''))) { 125 | file = i.file.replace(/^file:\/\//, '') 126 | } else if (fs.existsSync(i.file.replace(/^file:\/\/\//, ''))) { 127 | file = i.file.replace(/^file:\/\/\//, '') 128 | } 129 | } 130 | 131 | if (fs.existsSync(file)) { 132 | /** 本地文件 */ 133 | name = path.basename(file) 134 | file = fs.readFileSync(file).toString('base64') 135 | } else if (/^base64:\/\//.test(file)) { 136 | /** base64 */ 137 | file = file.replace(/^base64:\/\//, '') 138 | name = `${Date.now()}.${(await fileTypeFromBuffer(Buffer.from(file, 'base64'))).ext}` 139 | } else if (/^http(s)?:\/\//.test(file)) { 140 | /** url图片 */ 141 | file = Buffer.from(await (await fetch(file)).arrayBuffer()).toString('base64') 142 | name = `${Date.now()}.${(await fileTypeFromBuffer(Buffer.from(file, 'base64'))).ext}` 143 | } else { 144 | lain.error(this.id, i) 145 | return { type: 'text', data: { text: JSON.stringify(i) } } 146 | } 147 | 148 | /** 上传文件 获取文件id */ 149 | let file_id 150 | /** 如果获取为空 则进行重试 最多3次 */ 151 | for (let retries = 0; retries < 3; retries++) { 152 | file_id = (await api.upload_file(type, name, file))?.file_id 153 | if (file_id) break 154 | else logger.error(`第${retries + 1}次上传文件失败,正在重试...`) 155 | } 156 | 157 | /** 处理文件id为空 */ 158 | if (!file_id) return { type: 'text', data: { text: '图片上传失败...' } } 159 | 160 | /** 特殊处理表情包 */ 161 | if (/.gif$/.test(name)) { 162 | return { type: 'wx.emoji', data: { file_id } } 163 | } else { 164 | return { type: 'image', data: { file_id } } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /resources/DAU/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
19 | 20 | {{nickname}} - QQBOTDAU 21 |
22 |
23 |
24 |
25 |
26 |
最近{{totalDAU.days}}日平均DAU
27 |
28 |
29 |
上行消息人数:{{totalDAU.user_count}}
30 |
31 |
32 |
33 |
上行消息群数:{{totalDAU.group_count}}
34 |
35 |
36 |
37 |
上行消息量:{{totalDAU.msg_count}}
38 |
39 |
40 |
41 |
下行消息量:{{totalDAU.send_count}}
42 |
43 |
44 |
45 |
今日DAU
46 |
47 |
48 |
上行消息人数:{{todayDAU.user_count}}
49 |
50 |
51 |
52 |
上行消息群数:{{todayDAU.group_count}}
53 |
54 |
55 |
56 |
上行消息量:{{todayDAU.msg_count}}
57 |
58 |
59 |
60 |
下行消息量:{{todayDAU.send_count}}
61 |
62 |
63 |
64 | {{ each monthly month}} 65 |
66 |
DAU - {{month}}
67 |
68 |
69 |
70 |
71 | {{ /each }} 72 |
73 | 74 | 75 | 76 | 164 | 165 | -------------------------------------------------------------------------------- /model/shamrock/face.js: -------------------------------------------------------------------------------- 1 | /** 表情字典 */ 2 | const faceMap = { 3 | 0: '/惊讶', 4 | 1: '/撇嘴', 5 | 2: '/色', 6 | 3: '/发呆', 7 | 4: '/得意', 8 | 5: '/流泪', 9 | 6: '/害羞', 10 | 7: '/闭嘴', 11 | 8: '/睡', 12 | 9: '/大哭', 13 | 10: '/尴尬', 14 | 11: '/发怒', 15 | 12: '/调皮', 16 | 13: '/呲牙', 17 | 14: '/微笑', 18 | 15: '/难过', 19 | 16: '/酷', 20 | 18: '/抓狂', 21 | 19: '/吐', 22 | 20: '/偷笑', 23 | 21: '/可爱', 24 | 22: '/白眼', 25 | 23: '/傲慢', 26 | 24: '/饥饿', 27 | 25: '/困', 28 | 26: '/惊恐', 29 | 27: '/流汗', 30 | 28: '/憨笑', 31 | 29: '/悠闲', 32 | 30: '/奋斗', 33 | 31: '/咒骂', 34 | 32: '/疑问', 35 | 33: '/嘘', 36 | 34: '/晕', 37 | 35: '/折磨', 38 | 36: '/衰', 39 | 37: '/骷髅', 40 | 38: '/敲打', 41 | 39: '/再见', 42 | 41: '/发抖', 43 | 42: '/爱情', 44 | 43: '/跳跳', 45 | 46: '/猪头', 46 | 49: '/拥抱', 47 | 53: '/蛋糕', 48 | 54: '/闪电', 49 | 55: '/炸弹', 50 | 56: '/刀', 51 | 57: '/足球', 52 | 59: '/便便', 53 | 60: '/咖啡', 54 | 61: '/饭', 55 | 63: '/玫瑰', 56 | 64: '/凋谢', 57 | 66: '/爱心', 58 | 67: '/心碎', 59 | 69: '/礼物', 60 | 74: '/太阳', 61 | 75: '/月亮', 62 | 76: '/赞', 63 | 77: '/踩', 64 | 78: '/握手', 65 | 79: '/胜利', 66 | 85: '/飞吻', 67 | 86: '/怄火', 68 | 89: '/西瓜', 69 | 96: '/冷汗', 70 | 97: '/擦汗', 71 | 98: '/抠鼻', 72 | 99: '/鼓掌', 73 | 100: '/糗大了', 74 | 101: '/坏笑', 75 | 102: '/左哼哼', 76 | 103: '/右哼哼', 77 | 104: '/哈欠', 78 | 105: '/鄙视', 79 | 106: '/委屈', 80 | 107: '/快哭了', 81 | 108: '/阴险', 82 | 109: '/亲亲', 83 | 110: '/吓', 84 | 111: '/可怜', 85 | 112: '/菜刀', 86 | 113: '/啤酒', 87 | 114: '/篮球', 88 | 115: '/乒乓', 89 | 116: '/示爱', 90 | 117: '/瓢虫', 91 | 118: '/抱拳', 92 | 119: '/勾引', 93 | 120: '/拳头', 94 | 121: '/差劲', 95 | 122: '/爱你', 96 | 123: '/不', 97 | 124: '/好', 98 | 125: '/转圈', 99 | 126: '/磕头', 100 | 127: '/回头', 101 | 128: '/跳绳', 102 | 129: '/挥手', 103 | 130: '/激动', 104 | 131: '/街舞', 105 | 132: '/献吻', 106 | 133: '/左太极', 107 | 134: '/右太极', 108 | 136: '/双喜', 109 | 137: '/鞭炮', 110 | 138: '/灯笼', 111 | 140: '/K歌', 112 | 144: '/喝彩', 113 | 145: '/祈祷', 114 | 146: '/爆筋', 115 | 147: '/棒棒糖', 116 | 148: '/喝奶', 117 | 151: '/飞机', 118 | 158: '/钞票', 119 | 168: '/药', 120 | 169: '/手枪', 121 | 171: '/茶', 122 | 172: '/眨眼睛', 123 | 173: '/泪奔', 124 | 174: '/无奈', 125 | 175: '/卖萌', 126 | 176: '/小纠结', 127 | 177: '/喷血', 128 | 178: '/斜眼笑', 129 | 180: '/惊喜', 130 | 181: '/骚扰', 131 | 182: '/笑哭', 132 | 183: '/我最美', 133 | 184: '/河蟹', 134 | 185: '/羊驼', 135 | 187: '/幽灵', 136 | 188: '/蛋', 137 | 190: '/菊花', 138 | 192: '/红包', 139 | 193: '/大笑', 140 | 194: '/不开心', 141 | 197: '/冷漠', 142 | 198: '/呃', 143 | 199: '/好棒', 144 | 200: '/拜托', 145 | 201: '/点赞', 146 | 202: '/无聊', 147 | 203: '/托脸', 148 | 204: '/吃', 149 | 205: '/送花', 150 | 206: '/害怕', 151 | 207: '/花痴', 152 | 208: '/小样儿', 153 | 210: '/飙泪', 154 | 211: '/我不看', 155 | 212: '/托腮', 156 | 214: '/啵啵', 157 | 215: '/糊脸', 158 | 216: '/拍头', 159 | 217: '/扯一扯', 160 | 218: '/舔一舔', 161 | 219: '/蹭一蹭', 162 | 220: '/拽炸天', 163 | 221: '/顶呱呱', 164 | 222: '/抱抱', 165 | 223: '/暴击', 166 | 224: '/开枪', 167 | 225: '/撩一撩', 168 | 226: '/拍桌', 169 | 227: '/拍手', 170 | 228: '/恭喜', 171 | 229: '/干杯', 172 | 230: '/嘲讽', 173 | 231: '/哼', 174 | 232: '/佛系', 175 | 233: '/掐一掐', 176 | 234: '/惊呆', 177 | 235: '/颤抖', 178 | 236: '/啃头', 179 | 237: '/偷看', 180 | 238: '/扇脸', 181 | 239: '/原谅', 182 | 240: '/喷脸', 183 | 241: '/生日快乐', 184 | 242: '/头撞击', 185 | 243: '/甩头', 186 | 244: '/扔狗', 187 | 245: '/加油必胜', 188 | 246: '/加油抱抱', 189 | 247: '/口罩护体', 190 | 260: '/搬砖中', 191 | 261: '/忙到飞起', 192 | 262: '/脑阔疼', 193 | 263: '/沧桑', 194 | 264: '/捂脸', 195 | 265: '/辣眼睛', 196 | 266: '/哦哟', 197 | 267: '/头秃', 198 | 268: '/问号脸', 199 | 269: '/暗中观察', 200 | 270: '/emm', 201 | 271: '/吃瓜', 202 | 272: '/呵呵哒', 203 | 273: '/我酸了', 204 | 274: '/太南了', 205 | 276: '/辣椒酱', 206 | 277: '/汪汪', 207 | 278: '/汗', 208 | 279: '/打脸', 209 | 280: '/击掌', 210 | 281: '/无眼笑', 211 | 282: '/敬礼', 212 | 283: '/狂笑', 213 | 284: '/面无表情', 214 | 285: '/摸鱼', 215 | 286: '/魔鬼笑', 216 | 287: '/哦', 217 | 288: '/请', 218 | 289: '/睁眼', 219 | 290: '/敲开心', 220 | 291: '/震惊', 221 | 292: '/让我康康', 222 | 293: '/摸锦鲤', 223 | 294: '/期待', 224 | 295: '/拿到红包', 225 | 296: '/真好', 226 | 297: '/拜谢', 227 | 298: '/元宝', 228 | 299: '/牛啊', 229 | 300: '/胖三斤', 230 | 301: '/好闪', 231 | 302: '/左拜年', 232 | 303: '/右拜年', 233 | 304: '/红包包', 234 | 305: '/右亲亲', 235 | 306: '/牛气冲天', 236 | 307: '/喵喵', 237 | 308: '/求红包', 238 | 309: '/谢红包', 239 | 310: '/新年烟花', 240 | 311: '/打call', 241 | 312: '/变形', 242 | 313: '/嗑到了', 243 | 314: '/仔细分析', 244 | 315: '/加油', 245 | 316: '/我没事', 246 | 317: '/菜汪', 247 | 318: '/崇拜', 248 | 319: '/比心', 249 | 320: '/庆祝', 250 | 321: '/老色痞', 251 | 322: '/拒绝', 252 | 323: '/嫌弃', 253 | 324: '/吃糖', 254 | 325: '/惊吓', 255 | 326: '/生气', 256 | 327: '/加一', 257 | 328: '/错号', 258 | 329: '/对号', 259 | 330: '/完成', 260 | 331: '/明白', 261 | 332: '/举牌牌', 262 | 333: '/烟花', 263 | 334: '/虎虎生威', 264 | 335: '/绿马护体', 265 | 336: '/豹富', 266 | 337: '/花朵脸', 267 | 338: '/我想开了', 268 | 339: '/舔屏', 269 | 340: '/热化了', 270 | 341: '/打招呼', 271 | 342: '/酸Q', 272 | 343: '/我方了', 273 | 344: '/大怨种', 274 | 345: '/红包多多', 275 | 346: '/你真棒棒', 276 | 347: '/大展宏兔', 277 | 348: '/福萝卜' 278 | } 279 | /** 戳一戳字典 */ 280 | const pokeMap = { 281 | 0: '回戳', 282 | 1: '戳一戳', 283 | 2: '比心', 284 | 3: '点赞', 285 | 4: '心碎', 286 | 5: '666', 287 | 6: '放大招', 288 | 2000: '敲门', 289 | 2001: '抓一下', 290 | 2002: '碎屏', 291 | 2003: '勾引', 292 | 2004: '手雷', 293 | 2005: '结印', 294 | 2006: '召唤术', 295 | 2007: '玫瑰花', 296 | 2008: '迎春', 297 | 2009: '让你皮', 298 | 2010: '音响', 299 | 2011: '宝贝球' 300 | } 301 | 302 | export { faceMap, pokeMap } 303 | -------------------------------------------------------------------------------- /resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 100 | 101 | 148 | 149 | 150 | 151 | 152 | {{if typeof error === 'string'}} 153 |
154 |

原内容:

155 |
156 |
157 |

{{error}}

158 |
159 | {{else if error && typeof error === 'object'}} 160 |
161 |

状态码:{{error.code}}

162 |

错误原因:{{error.message}}

163 |

错误标识符: {{error.traceid}}

164 |
165 | {{/if}} 166 | 167 |
168 |
Created By {{@lain.apk.display}} {{@lain.apk.version}} & Lain-plugin {{@lain.version.version}}
169 | -------------------------------------------------------------------------------- /lain.support.js: -------------------------------------------------------------------------------- 1 | const list = [ 2 | { label: '铃音帮助', data: '#铃音帮助' }, 3 | { label: '设置主人', data: '#设置主人' } 4 | ] 5 | 6 | export default class Button { 7 | constructor () { 8 | this.plugin = { 9 | name: 'Lain-plugin', 10 | dsc: '铃音插件', 11 | priority: 99, 12 | rule: [ 13 | { 14 | reg: '^#?(id|ID)', 15 | fnc: 'Id' 16 | }, 17 | { 18 | reg: /^#(Lain|铃音)帮助$/i, 19 | fnc: 'menu', 20 | permission: 'master' 21 | }, 22 | { 23 | reg: /^#(Lain|铃音)控制台帮助$/i, 24 | fnc: 'stdin', 25 | permission: 'master' 26 | }, 27 | { 28 | reg: /^#(Lain|铃音)设置标准输入.*/i, 29 | fnc: 'stdin', 30 | permission: 'master' 31 | }, 32 | { 33 | reg: /^#(Lain|铃音)三叶草帮助$/i, 34 | fnc: 'shamrock', 35 | permission: 'master' 36 | }, 37 | { 38 | reg: /^#(Lain|铃音)设置三叶草.*/i, 39 | fnc: 'shamrock', 40 | permission: 'master' 41 | }, 42 | { 43 | reg: /^#(shamrock|三叶草)(发布|测试)?(版本|更新日志|安装包|apk|APK)$/gi, 44 | fnc: 'shamrock', 45 | permission: 'master' 46 | }, 47 | { 48 | reg: /^#(Lain|铃音)微信帮助$/i, 49 | fnc: 'wechat', 50 | permission: 'master' 51 | }, 52 | { 53 | reg: /^#(Lain|铃音)设置(PC)?微信.*/i, 54 | fnc: 'wechat', 55 | permission: 'master' 56 | }, 57 | { 58 | reg: /^#微信(登(录|陆)|账号|删除.*)$/, 59 | fnc: 'wechat', 60 | permission: 'master' 61 | }, 62 | { 63 | reg: /^#(Lain|铃音)QQBot帮助$/i, 64 | fnc: 'qqbot', 65 | permission: 'master' 66 | }, 67 | { 68 | reg: /^#QQ(群|Bot|频道).*/i, 69 | fnc: 'qqbot', 70 | permission: 'master' 71 | }, 72 | { 73 | reg: /^#(Lain|铃音)全局帮助$/i, 74 | fnc: 'bot', 75 | permission: 'master' 76 | }, 77 | { 78 | reg: /^#(Lain|铃音)设置.*/i, 79 | fnc: 'bot', 80 | permission: 'master' 81 | } 82 | ] 83 | } 84 | } 85 | 86 | Id (e) { 87 | const button = [ 88 | { label: '群聊ID', data: `${e.group_id}`, reply: true }, 89 | { label: '用户ID', data: `${e.user_id}`, reply: true } 90 | ] 91 | return Bot.Button(button) 92 | } 93 | 94 | menu (e) { 95 | const button = [ 96 | list, 97 | [ 98 | { label: '全局', data: '#铃音全局帮助' }, 99 | { label: '控制台', data: '#铃音控制台帮助' }, 100 | { label: '三叶草', data: '#铃音三叶草帮助' } 101 | ], 102 | [ 103 | { label: '微信', data: '#铃音微信帮助' }, 104 | { label: 'QQBot', data: '#铃音QQBot帮助' } 105 | ], 106 | [ 107 | { label: '铃音更新', data: '#铃音更新' }, 108 | { label: '更新日志', data: '#铃音更新日志' } 109 | ] 110 | ] 111 | return Bot.Button(button) 112 | } 113 | 114 | bot () { 115 | const button = [ 116 | list, 117 | [ 118 | { label: 'IP', data: '#铃音设置IP192.168.0.1 (临时文件服务器IP,与 url 二选一,可填域名:127.0.0.1 或 www.lain.com)' }, 119 | { label: '端口', data: '#铃音设置端口2955 (2955是你的HTTP端口,ComWeChat、Shamrock、QQBot临时文件使用)' }, 120 | { label: 'url', data: '#铃音设置urlhttp://192.168.0.1:2956 (临时文件服务器访问url,无特殊需要请不要填写此项,QQBot使用。端口转发、域名等使用:http://www.lain.com 或 http://192.168.0.1:2956,使用前请自行配置转发)' }, 121 | { label: '过期时间', data: '#铃音设置文件过期时间30 (临时文件服务器过期时间,单位:秒)' } 122 | ] 123 | ] 124 | return Bot.Button(button) 125 | } 126 | 127 | stdin () { 128 | const button = [ 129 | list, 130 | [ 131 | { label: '开关', data: '#铃音设置标准输入开启 (是否在椰奶状态显示标准输入)' }, 132 | { label: '昵称', data: '#铃音设置标准输入名称铃音 (铃音是你的标准输入在椰奶状态的昵称)' }, 133 | { label: '铃音更新', data: '#铃音更新' }, 134 | { label: '更新日志', data: '#铃音更新日志' } 135 | ] 136 | ] 137 | return Bot.Button(button) 138 | } 139 | 140 | shamrock () { 141 | const button = [ 142 | list, 143 | [ 144 | { label: 'GithubKey', data: '#铃音设置三叶草gitghp_xxxxx (ghp_xxxxx是你的Github personal access token, 用于查看和下载shamrock版本信息)' }, 145 | { label: '安装包', data: '#三叶草测试安装包' }, 146 | { label: '查看版本', data: '#三叶草版本' } 147 | ] 148 | ] 149 | return Bot.Button(button) 150 | } 151 | 152 | wechat () { 153 | const button = [ 154 | list, 155 | [ 156 | { label: 'PC微信:昵称', data: '#铃音设置PC微信名称铃音 (铃音是你的ComWechat在椰奶状态的昵称)' }, 157 | { label: 'PC微信:好友', data: '#铃音设置PC微信好友开启' } 158 | ], 159 | [ 160 | { label: '网页微信:昵称', data: '#铃音设置微信名称铃音 (铃音是你的网页微信在椰奶状态的昵称)' }, 161 | { label: '网页微信:好友', data: '#铃音设置PC微信好友开启' } 162 | ], 163 | [ 164 | { label: '网页微信:登录', data: '#微信登录' }, 165 | { label: '网页微信:退出', data: '#微信删除' } 166 | ], 167 | [ 168 | { label: '网页微信:查看账号', data: '#微信账号' } 169 | ] 170 | ] 171 | return Bot.Button(button) 172 | } 173 | 174 | qqbot () { 175 | const button = [ 176 | list, 177 | [ 178 | { label: '仅群:连接', data: '#QQ群设置0(0或1表示沙盒模式开关,即使上线前也建议关闭此项):0(0-公域 1-私域,机器人类型在QQ开放平台的沙箱配置中有一次设置机会):机器人ID(在QQ开放平台(q.qq.com)的开发设置中查看):机器人令牌:机器人密钥(需要机器人创建者通过重置获取)' }, 179 | { label: '频道:连接', data: '#QQ频道设置0(0或1表示沙盒模式开关,即使上线前也建议关闭此项):0(0-公域 1-私域,机器人类型在QQ开放平台的沙箱配置中有一次设置机会):机器人ID(在QQ开放平台(q.qq.com)的开发设置中查看):机器人令牌:机器人密钥(需要机器人创建者通过重置获取)' }, 180 | { label: '全域:连接', data: '#QQBot设置0(0或1表示沙盒模式开关,即使上线前也建议关闭此项):0(0-公域 1-私域,机器人类型在QQ开放平台的沙箱配置中有一次设置机会):机器人ID(在QQ开放平台(q.qq.com)的开发设置中查看):机器人令牌:机器人密钥(需要机器人创建者通过重置获取)' } 181 | ], 182 | [ 183 | { label: '前缀转换', data: '#QQBot设置前缀开启' }, 184 | { label: 'QQ图床', data: '#QQBot设置QQ图床12345 (12345是连接在同一机器人的野生机器人的qq号)' }, 185 | { label: '查看连接', data: '#QQBot账号' } 186 | ], 187 | [ 188 | { label: 'MD模式', data: '#QQBotMD1 (0-关闭 1-全局 2-正则模式(仅对有相应按钮的指令启用MD消息) 3-按钮模式(响应内容时发送普通消息,对有相应按钮的指令额外发送一条内容为空的MD消息以显示按钮))' }, 189 | { label: '模板ID', data: '#QQBot设置MD 机器人ID:模板ID(模板ID在QQ开放平台(q.qq.com)的高阶能力中查看)' } 190 | ] 191 | ] 192 | return Bot.Button(button) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /resources/shamrock/index1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | shamrock版本 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 |
OpenShamrock 1.0.6-dev.abbcccsd
16 |
17 | 18 | https://github.com/whitechi73/OpenShamrock 19 |
20 |
21 | 22 | 532 23 | 24 | 25 | 89 26 | 27 | 28 | 10 29 |
30 | 31 |
32 |
33 |
34 |
35 | 版本摘要 36 |
37 |
38 |
39 |
40 | · 当前使用的版本落后最新测试版17个提交 41 |
42 |
43 | · 当前使用的版本落后最新发布版1个版本 44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 | 发布版本(近3个版本) 52 |
53 |
54 |
55 |
56 | 1.0.7-dev 57 | 58 | 2023-12-02 19:45:23 59 | 60 |
61 |
62 | 更新内容:

1. 允许系统自动复活 by @whitechi73
2. 支持群公告系列api by @ikechan8370
3. 支持自定义WebSocket心跳 by @whitechi73
4. 防止日志覆盖旧的日志 by @whitechi73 #78
5. 允许获取文件信息返回`md5`等信息 by @ikechan8370
6. 防止检测模拟器信息 by @whitechi73
7. 添加反调用栈检测开关 by @Simplxss
10. 戳一戳事件扩展 by @ikechan8370
11. 新QQ超表情支持 (骰子, 篮球, 猜拳)
12. 支持忽略Doze模式 by @whitechi73

感谢文档贡献者:@callng, @kagg886, @ikechan8370, @tobycroft, @fred913 63 |
64 |
65 |
66 |
67 | 68 |
69 |
70 | 测试版本(近10次提交) 71 |
72 |
73 |
74 |
75 | 76 |
77 | a0ff4782 78 |
79 | 80 | 2023-12-02 19:45:23 81 | 82 |
83 |
84 | 85 | whitechi73 86 | · Merge remote-tracking branch 'origin/master' 87 | 88 |
89 |
90 |
91 |
92 | 93 |
94 | a0ff4782 95 |
96 | 97 | 2023-12-02 19:45:23 98 | 99 |
100 |
101 | 102 | Simplxss 103 | · Merge remote-tracking branch 'origin/master' 104 | 105 |
106 |
107 |
108 |
109 | 110 |
111 | a0ff4782 112 |
113 | 114 | 2023-12-02 19:45:23 115 | 116 |
117 |
118 | 119 | whitechi73 120 | · Merge remote-tracking branch 'origin/master' 121 | 122 |
123 |
124 |
125 | 126 |
127 | 128 |
129 |
130 | 更多命令 131 |
132 |
133 |
134 |
135 |
136 | #shamrock发布安装包 137 |
138 | 139 | 发送最新版release安装包 140 | 141 |
142 |
143 |
144 | #shamrock测试安装包 145 |
146 | 147 | 发送最新测试版安装包,需要配置github APIKey 148 | 149 |
150 |
151 |
152 |
153 | 154 |
155 | 156 | 157 | -------------------------------------------------------------------------------- /adapter/QQGuild/log.js: -------------------------------------------------------------------------------- 1 | export default class QQGuildLog { 2 | /** 传入基本配置 */ 3 | constructor(id) { 4 | /** 开发者id */ 5 | this.id = id 6 | } 7 | 8 | /** 处理频道事件 */ 9 | async event(data) { 10 | const eventHandler = { 11 | GUILD_CREATE: async (msg) => { 12 | /** 新加入频道稍等服务器一会 */ 13 | await lain.sleep(2000) 14 | let admin = false 15 | try { 16 | const Member = (await Bot[this.id].client.guildApi.guildMember(msg.id, this.tiny_id)).data 17 | admin = !!Member.roles.includes("2") 18 | } catch (err) { 19 | lain.warn(this.id, `Bot无法在频道 ${msg.id} 中读取基础信息,请给予权限...错误信息:${err.message}`) 20 | } 21 | 22 | let qg 23 | try { 24 | qg = (await Bot[this.id].client.guildApi.guild(msg.id)).data 25 | } catch (err) { 26 | lain.warn(this.id, `Bot无法在频道 ${msg.id} 中读取基础信息,请给予权限...错误信息:${err.message}`) 27 | } 28 | 29 | Bot.lain.guilds[qg.id] = { 30 | ...qg, 31 | admin, 32 | id: this.id, 33 | channels: {} 34 | } 35 | 36 | await lain.sleep(200) 37 | 38 | try { 39 | const channelList = (await Bot[this.id].client.channelApi.channels(msg.id)).data 40 | for (const i of channelList) { 41 | /** 给锅巴用的 */ 42 | Bot.gl.set(`qg_${i.guild_id}-${i.id}`, { 43 | id: this.id, 44 | group_id: `qg_${i.guild_id}-${i.id}`, 45 | group_name: `${qg.name || i.guild_id}-${i.name || i.id}`, 46 | guild_id: i.guild_id, 47 | guild_name: qg.name || i.guild_id, 48 | channel_id: i.id, 49 | channel_name: i.name || i.id 50 | }) 51 | /** 添加频道列表到Bot.gl中,用于主动发送消息 */ 52 | Bot[this.id].gl.set(`qg_${i.guild_id}-${i.id}`, { 53 | id: this.id, 54 | group_id: `qg_${i.guild_id}-${i.id}`, 55 | group_name: `${qg.name || i.guild_id}-${i.name || i.id}`, 56 | guild_id: i.guild_id, 57 | guild_name: qg.name || i.guild_id, 58 | channel_id: i.id, 59 | channel_name: i.name || i.id 60 | }) 61 | /** 子频道名称 */ 62 | Bot.lain.guilds[i.guild_id].channels[i.id] = i.name || i.id 63 | } 64 | } catch (err) { 65 | lain.warn(this.id, `Bot无法在频道 ${qg.id} 中读取子频道列表,请给予权限...错误信息:${err.message}`) 66 | } 67 | return `[${msg.name}(qg_${msg.id})] 机器人加入频道,操作人:${msg.op_user_id}` 68 | }, 69 | GUILD_UPDATE: (msg) => { 70 | return `[${msg.name}(${msg.id})] 频道信息变更,操作人:${msg.op_user_id}` 71 | }, 72 | GUILD_DELETE: (msg) => { 73 | return `[${msg.name}(${msg.id})] 机器人被移除频道,操作人:${msg.op_user_id}` 74 | }, 75 | CHANNEL_CREATE: (msg) => { 76 | return `[${msg.name}(${msg.id})] 子频道被创建,操作人:${msg.op_user_id}` 77 | }, 78 | CHANNEL_UPDATE: (msg) => { 79 | return `[${msg.name}(${msg.id})] 子频道信息变更,操作人:${msg.op_user_id}` 80 | }, 81 | CHANNEL_DELETE: (msg) => { 82 | return `[${msg.name}(${msg.id})] 子频道被删除,操作人:${msg.op_user_id}` 83 | }, 84 | GUILD_MEMBER_ADD: async (msg) => { 85 | await lain.sleep(2000) 86 | if (msg.user.bot) { 87 | return `[${Bot.lain.guilds[msg.guild_id]?.name}(${msg.guild_id})] 频道新增机器人:${msg.user.username}(${msg.user.id}),操作人:${msg.op_user_id}` 88 | } else { 89 | return `[${Bot.lain.guilds[msg.guild_id]?.name}(${msg.guild_id})] 新用户加入频道:${msg.user.username}(${msg.user.id})` 90 | } 91 | }, 92 | GUILD_MEMBER_UPDATE: (msg) => { 93 | return `[${Bot.lain.guilds[msg.guild_id]?.name}(${msg.guild_id})] 用户的频道属性发生变化:${msg.user.username}(${msg.user.id})` 94 | }, 95 | GUILD_MEMBER_REMOVE: (msg) => { 96 | if (msg.op_user_id === msg.user.id) { return `[${Bot.lain.guilds[msg.guild_id]?.name}(${msg.guild_id})] 用户退出频道:${msg.user.username}(${msg.user.id})` } else { return `[${Bot.lain.guilds[msg.guild_id]?.name}(${msg.guild_id})] 用户被移除频道:${msg.user.username}(${msg.user.id})` } 97 | }, 98 | MESSAGE_REACTION_ADD: (msg) => { 99 | const guild_id = msg.guild_id 100 | const channel_id = msg.channel_id 101 | const group_name = Bot.lain.guilds[guild_id]?.name + "-" + Bot.lain.guilds[guild_id].channels[channel_id] 102 | let logs = `[${group_name}(qg_${guild_id}-${channel_id})] 表情表态:` 103 | logs += `\n消息ID:${msg.target.id}\n操作人:${msg.user_id}\n操作类型:添加表情动态\n表情ID:emoji:${msg.emoji.id}` 104 | return logs 105 | }, 106 | MESSAGE_REACTION_REMOVE: (msg) => { 107 | const guild_id = msg.guild_id 108 | const channel_id = msg.channel_id 109 | const group_name = Bot.lain.guilds[guild_id]?.name + "-" + Bot.lain.guilds[guild_id].channels[channel_id] 110 | let logs = `[${group_name}(qg_${guild_id}-${channel_id})] 表情表态:` 111 | logs += `\n消息ID:${msg.target.id}\n操作人:${msg.user_id}\n操作类型:取消表情动态\n表情ID:emoji:${msg.emoji.id}` 112 | return logs 113 | }, 114 | MESSAGE_DELETE: async (msg) => { 115 | return `[${Bot.lain.guilds[msg.message.guild_id]?.name}(${msg.message.guild_id}),${await this.recallMsg(msg)}` 116 | }, 117 | DIRECT_MESSAGE_DELETE: async (msg) => { 118 | return `[${Bot.lain.guilds[msg.message.src_guild_id]?.name}(${msg.message.src_guild_id}),${await this.recallMsg(msg)}` 119 | }, 120 | PUBLIC_MESSAGE_DELETE: async (msg) => { 121 | return `[${Bot.lain.guilds[msg.message.guild_id]?.name}(${msg.message.guild_id}),${await this.recallMsg(msg)}` 122 | } 123 | } 124 | 125 | lain.warn(this.id, await eventHandler[data.eventType](data.msg) || `未知事件:${JSON.stringify(data)}`) 126 | } 127 | 128 | async recallMsg(msg) { 129 | let recallMsg = "" 130 | const { author, channel_id, src_guild_id, guild_id, direct_message, id } = msg.message 131 | try { 132 | const content = await redis.get(id) 133 | recallMsg += `用户撤回消息:` 134 | recallMsg += `\n操作人:${msg.op_user.id}` 135 | recallMsg += `\n频道ID:${src_guild_id || guild_id}` 136 | recallMsg += `\n子频道ID:${direct_message ? "私信" : channel_id}` 137 | recallMsg += `\n用户ID:${author.id}` 138 | recallMsg += `\n用户昵称:${author.username}` 139 | recallMsg += `\n用户是否为机器人:${author.bot}` 140 | recallMsg += `\n消息内容:${content || "未知内容"}` 141 | recallMsg += `\n消息ID:${id}` 142 | } catch (error) { 143 | recallMsg = `撤回消息:${id}` 144 | } 145 | return recallMsg 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /adapter/WeChat/message.js: -------------------------------------------------------------------------------- 1 | import api from './api.js' 2 | import SendMsg from './sendMsg.js' 3 | import common from '../../model/common.js' 4 | 5 | export default class message { 6 | /** 传入基本配置 */ 7 | constructor (id) { 8 | /** 开发者id */ 9 | this.id = id 10 | /** bot名称 */ 11 | this.name = Bot[id].nickname 12 | } 13 | 14 | /** 消息转换为Yunzai格式 */ 15 | async msg (data) { 16 | /** 调试日志 */ 17 | lain.debug(this.id, JSON.stringify(data)) 18 | const { group_id, detail_type, time, message_id } = data 19 | /** 存一份原始消息到redis中,用于引用消息 */ 20 | if (message_id) { 21 | const msg = JSON.stringify({ 22 | id: data.alt_message, 23 | user_id: data.user_id 24 | }) 25 | try { 26 | await redis.set(message_id, msg, { EX: 1800 }) 27 | } catch (error) { } 28 | } 29 | 30 | let user_id = data.user_id 31 | /** 构建Yunzai的message */ 32 | let { message, atme, source } = await this.message(data.message) 33 | /** 获取用户名称 */ 34 | let user_name 35 | if (detail_type === 'private' || detail_type === 'wx.get_private_poke') { 36 | user_name = (await api.get_user_info(user_id))?.user_name || '' 37 | } else { 38 | user_name = (await api.get_group_member_info(group_id, user_id))?.user_name || '' 39 | } 40 | 41 | const sub_type = (detail_type === 'private' || detail_type === 'wx.get_private_poke') ? 'friend' : 'normal' 42 | 43 | const member = { 44 | info: { 45 | group_id, 46 | user_id, 47 | nickname: user_name, 48 | last_sent_time: time 49 | }, 50 | group_id 51 | } 52 | 53 | let e = { 54 | atBot: atme, 55 | atme, 56 | adapter: 'ComWeChat', 57 | uin: this.id, 58 | group_id, 59 | group_name: Bot.gl.get(group_id)?.group_name || '未知', 60 | post_type: 'message', 61 | message_id, 62 | user_id, 63 | time, 64 | raw_message: data.alt_message, 65 | message_type: detail_type, 66 | sub_type, 67 | source, 68 | self_id: this.id, 69 | seq: message_id, 70 | member, 71 | sender: { 72 | user_id, 73 | nickname: user_name, 74 | card: user_name, 75 | role: 'member' 76 | } 77 | } 78 | if (detail_type === 'private' || detail_type === 'wx.get_private_poke') { 79 | e.friend = { 80 | recallMsg: () => { 81 | 82 | }, 83 | makeForwardMsg: async (forwardMsg) => { 84 | return await common.makeForwardMsg(forwardMsg) 85 | }, 86 | getChatHistory: (seq, num) => { 87 | return ['message', 'test'] 88 | }, 89 | sendMsg: async (msg) => { 90 | return await (new SendMsg(this.id, data)).message(msg) 91 | } 92 | } 93 | } else { 94 | e.group = { 95 | getChatHistory: (seq, num) => { 96 | return ['message', 'test'] 97 | }, 98 | recallMsg: () => { 99 | 100 | }, 101 | sendMsg: async (msg) => { 102 | return await (new SendMsg(this.id, data)).message(msg) 103 | }, 104 | makeForwardMsg: async (forwardMsg) => { 105 | return await common.makeForwardMsg(forwardMsg) 106 | }, 107 | pickMember: (id) => { 108 | let info = { 109 | group_id, 110 | user_id: id 111 | } 112 | if (id === user_id) { 113 | info = member.info 114 | } 115 | return { 116 | ...info, 117 | info, 118 | getAvatarUrl: async () => (await api.get_group_member_info(group_id, id))?.['wx.avatar'] 119 | } 120 | } 121 | } 122 | } 123 | e.recall = () => { 124 | 125 | } 126 | e.reply = async (msg) => { 127 | return await (new SendMsg(this.id, data)).message(msg) 128 | } 129 | e.toString = () => { 130 | return data.alt_message 131 | } 132 | 133 | if (message) e.message = [...message] 134 | 135 | /** 私聊拍一拍 */ 136 | if (data.detail_type === 'wx.get_private_poke') { 137 | e.action = '戳了戳' 138 | e.sub_type = 'poke' 139 | e.post_type = 'notice' 140 | e.notice_type = 'private' 141 | e.user_id = data.from_user_id 142 | e.target_id = data.user_id 143 | e.operator_id = data.from_user_id 144 | user_id = data.from_user_id 145 | } 146 | /** 群聊拍一拍 */ 147 | if (data.detail_type === 'wx.get_group_poke') { 148 | e.action = '戳了戳' 149 | e.sub_type = 'poke' 150 | e.post_type = 'notice' 151 | e.notice_type = 'group' 152 | e.target_id = data.user_id 153 | e.operator_id = data.from_user_id 154 | } 155 | 156 | /** 保存消息次数 */ 157 | try { common.recvMsg(e.self_id, e.adapter) } catch { } 158 | return e 159 | } 160 | 161 | /** 构建message */ 162 | async message (msg) { 163 | let atme = false 164 | let message = [] 165 | let source = {} 166 | if (!msg) return false 167 | 168 | for (let i of msg) { 169 | const { type, data } = i 170 | switch (type) { 171 | case 'text': 172 | message.push({ type: 'text', text: data.text }) 173 | break 174 | case 'mention': 175 | if (data.user_id === this.id) atme = true 176 | else message.push({ type: 'at', text: '', qq: data.user_id }) 177 | break 178 | case 'mention_all': 179 | break 180 | case 'image': 181 | // eslint-disable-next-line no-case-declarations 182 | const image = await api.get_file('url', data.file_id) 183 | message.push({ type: 'image', name: image.name, url: image.url }) 184 | break 185 | case 'voice': 186 | break 187 | case 'audio': 188 | break 189 | case 'video': 190 | break 191 | case 'file': 192 | break 193 | case 'location': 194 | break 195 | case 'reply': 196 | try { 197 | const res = JSON.parse(await redis.get(i.data.message_id) || { id: '', user_id: '' }) 198 | source = { message: res.id, rand: 0, seq: 0, time: 0, user_id: res.user_id } 199 | } catch (err) { } 200 | break 201 | case 'wx.emoji': 202 | message.push({ type: 'emoji', text: data.file_id }) 203 | break 204 | case 'wx.link': 205 | message.push({ type: 'wx.link', data }) 206 | break 207 | case 'wx.app': 208 | break 209 | } 210 | } 211 | return { message, atme, source } 212 | } 213 | 214 | /** 处理消息、转换格式 */ 215 | async reply (msg, quote, group_name) { 216 | const { guild_id, channel_id } = this.data.msg 217 | /** 处理云崽过来的消息 */ 218 | return await (new SendMsg(this.id, { guild_id, channel_id }, this.data.eventType, this.msg_id, group_name)).message(msg, quote) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /adapter/shamrock/sendMsg.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | import fs from 'fs' 3 | import { randomUUID } from 'crypto' 4 | import common from '../../model/common.js' 5 | import api from './api.js' 6 | 7 | export default class SendMsg { 8 | /** 传入基本配置 */ 9 | constructor (id, isGroup = true) { 10 | /** 机器人uin */ 11 | this.id = id 12 | /** 是否群聊 */ 13 | this.isGroup = isGroup 14 | /** 机器人名称 */ 15 | this.name = Bot?.[id]?.nickname || '未知' 16 | } 17 | 18 | /** 发送消息 */ 19 | async message (data, id, quote = false) { 20 | /** 将云崽过来的消息统一为数组 */ 21 | data = common.array(data) 22 | /** 转为shamrock可以使用的格式 */ 23 | let { msg, CQ, node } = await this.msg(data) 24 | 25 | /** 引用消息 */ 26 | if (quote && !node) msg.unshift({ type: 'reply', data: { id: quote } }) 27 | if (node) CQ = ['[转发消息]'] 28 | 29 | /** 发送消息 */ 30 | return await this.sendMsg(id, msg, CQ, node) 31 | } 32 | 33 | /** 转为shamrock可以使用的格式 */ 34 | async msg (data) { 35 | if (typeof data == 'string') data = [{ type: 'text', text: data }] 36 | if (!Array.isArray(data)) data = [data] 37 | const CQ = [] 38 | let msg = [] 39 | let node = false 40 | 41 | /** chatgpt-plugin */ 42 | if (data?.[0]?.type === 'xml') data = data?.[0].msg 43 | 44 | for (let i of data) { 45 | if (i?.node) node = true 46 | switch (i.type) { 47 | case 'at': 48 | CQ.push(`{at:${Number(i.qq) == 0 ? i.id : i.qq}}`) 49 | msg.push({ type: 'at', data: { qq: Number(i.qq) == 0 ? i.id : i.qq } }) 50 | break 51 | case 'face': 52 | CQ.push(`{face:${i.text}}`) 53 | msg.push({ type: 'face', data: { id: i.text } }) 54 | break 55 | case 'text': 56 | CQ.push(i.text) 57 | msg.push({ type: 'text', data: { text: i.text } }) 58 | break 59 | case 'file': 60 | break 61 | case 'record': 62 | CQ.push(`{record:${i.file}}`) 63 | try { 64 | const base64 = await this.getFile(i, 'record') 65 | /** 上传文件 */ 66 | const { file } = await api.download_file(this.id, base64.data.file) 67 | msg.push({ type: 'record', data: { file: `file://${file}` } }) 68 | } catch (err) { 69 | lain.error(this.id, err) 70 | msg.push(await this.getFile(i, 'record')) 71 | } 72 | break 73 | case 'video': 74 | CQ.push(`{video:${i.file}}`) 75 | try { 76 | const base64 = await this.getFile(i, 'video') 77 | /** 上传文件 */ 78 | const { file } = await api.download_file(this.id, base64.data.file) 79 | msg.push({ type: 'video', data: { file: `file://${file}` } }) 80 | } catch (err) { 81 | lain.error(this.id, err) 82 | msg.push(await this.getFile(i, 'video')) 83 | } 84 | break 85 | case 'image': 86 | CQ.push('{image:base64://...}') 87 | msg.push(await this.getFile(i, 'image')) 88 | break 89 | case 'poke': 90 | CQ.push(`[CQ:poke,id=${i.id}]`) 91 | msg.push({ type: 'poke', data: { type: i.id, id: 0, strength: i?.strength || 0 } }) 92 | break 93 | case 'touch': 94 | CQ.push(`{poke:${i.id}}`) 95 | msg.push({ type: 'touch', data: { id: i.id } }) 96 | break 97 | case 'weather': 98 | CQ.push(`[CQ=weather,${i.city ? ('city=' + i.city) : ('code=' + i.code)}]`) 99 | msg.push({ type: 'weather', data: { code: i.code, city: i.city } }) 100 | break 101 | case 'json': 102 | let json = i.data 103 | if (typeof i.data !== 'string') json = JSON.stringify(i.data) 104 | CQ.push(`[CQ=json,data=${json}]`) 105 | msg.push({ type: 'json', data: { data: json } }) 106 | break 107 | case 'music': 108 | CQ.push(`[CQ=music,type=${i.data.type},id=${i.data.id}]`) 109 | msg.push({ type: 'music', data: i.data }) 110 | break 111 | case 'location': 112 | const { lat, lng: lon } = data 113 | CQ.push(`[CQ=json,lat=${lat},lon=${lon}]`) 114 | msg.push({ type: 'location', data: { lat, lon } }) 115 | break 116 | case 'share': 117 | const { url, title, image, content } = data 118 | CQ.push(`[CQ=json,url=${url},title=${title},image=${image},content=${content}]`) 119 | msg.push({ type: 'share', data: { url, title, content, image } }) 120 | break 121 | case 'forward': 122 | CQ.push(i.text) 123 | msg.push({ type: 'text', data: { text: i.text } }) 124 | break 125 | case 'node': 126 | msg.push({ type: 'node', data: { ...i } }) 127 | break 128 | default: 129 | CQ.push(JSON.stringify(i)) 130 | msg.push({ type: 'text', data: { text: JSON.stringify(i) } }) 131 | break 132 | } 133 | } 134 | 135 | /** 合并转发 */ 136 | if (node) { 137 | const NodeMsg = [] 138 | NodeMsg.push(...msg 139 | .filter(i => !(i.type == 'at' || i.type == 'record')) 140 | .map(i => ({ 141 | type: 'node', 142 | data: { 143 | name: this.name, 144 | content: [i] 145 | } 146 | })) 147 | ) 148 | msg = NodeMsg 149 | } 150 | 151 | return { msg, CQ, node } 152 | } 153 | 154 | /** 统一文件格式 */ 155 | async getFile (i, type) { 156 | const res = common.getFile(i) 157 | const { file } = res 158 | switch (res.type) { 159 | case 'file': 160 | return { type, data: { file: 'base64://' + fs.readFileSync(file.replace(/^file:\/\//, '')).toString('base64') } } 161 | case 'buffer': 162 | return { type, data: { file: `base64://${Buffer.from(file).toString('base64')}` } } 163 | case 'base64': 164 | return { type, data: { file } } 165 | case 'http': 166 | return { type, data: { file } } 167 | default: 168 | return { type: 'text', data: { text: `无法处理此格式:${JSON.stringify(i)}` } } 169 | } 170 | } 171 | 172 | /** 发送消息 */ 173 | async sendMsg (id, msg, CQ, node) { 174 | /** 打印日志 */ 175 | lain.info(this.id, `发送${this.isGroup ? '群' : '好友'}消息:<${id}>${CQ.join('')}`) 176 | 177 | if (CQ.includes('{image:base64://...}')) { 178 | try { await common.MsgTotal(this.id, 'shamrock', 'image') } catch { } 179 | } else { 180 | try { await common.MsgTotal(this.id, 'shamrock') } catch { } 181 | } 182 | /** 处理合并转发 */ 183 | if (node) { 184 | if (this.isGroup) { 185 | return await api.send_group_forward_msg(this.id, id, msg) 186 | } else { 187 | return await api.send_private_forward_msg(this.id, id, msg) 188 | } 189 | } 190 | 191 | /** 非合并转发 */ 192 | const bot = Bot.shamrock.get(this.id) 193 | if (!bot) return lain.warn(this.id, '不存在此Bot') 194 | 195 | const echo = randomUUID() 196 | /** 判断群聊、私聊 */ 197 | const action = this.isGroup ? 'send_group_msg' : 'send_private_msg' 198 | const params = { [this.isGroup ? 'group_id' : 'user_id']: id, message: msg } 199 | /** 发送消息 */ 200 | bot.socket.send(JSON.stringify({ echo, action, params })) 201 | 202 | /** 等待返回结果 */ 203 | for (let i = 0; i < 1200; i++) { 204 | let data = await Bot.lain.on.get(echo) 205 | if (data) { 206 | Bot.lain.on.delete(echo) 207 | if (data.status === 'ok') { 208 | const { message_id, time } = data.data 209 | 210 | /** 储存自身发送的消息 */ 211 | try { await redis.set(`Shamrock:${this.id}:${message_id}`, JSON.stringify(data), { EX: 120 }) } catch { } 212 | 213 | return { 214 | time, 215 | message_id, 216 | seq: message_id, 217 | rand: 1, 218 | ...data.data 219 | } 220 | } else { 221 | lain.error('Lain-plugin', data) 222 | return data 223 | } 224 | } else { 225 | await lain.sleep(50) 226 | } 227 | } 228 | return '获取失败' 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/config/config.js: -------------------------------------------------------------------------------- 1 | import Yaml from 'yaml' 2 | import fs from 'node:fs' 3 | import chokidar from 'chokidar' 4 | import YamlHandler from '../../model/YamlHandler.js' 5 | 6 | /** 配置文件 */ 7 | class Cfg { 8 | constructor () { 9 | this._path = './plugins/Lain-plugin/config/' 10 | this.config = {} 11 | 12 | /** 监听文件 */ 13 | this.watcher = { config: {}, defSet: {} } 14 | 15 | this.initCfg() 16 | this.delFile() 17 | } 18 | 19 | /** 初始化配置 */ 20 | initCfg () { 21 | this.path = this._path + 'config/' 22 | if (!fs.existsSync(this.path)) { 23 | fs.mkdirSync(this.path) 24 | } 25 | this.pathDef = this._path + 'defSet/' 26 | const files = fs.readdirSync(this.pathDef).filter(file => file.endsWith('.yaml')) 27 | for (let file of files) { 28 | if (!fs.existsSync(`${this.path}${file}`)) { 29 | fs.copyFileSync(`${this.pathDef}${file}`, `${this.path}${file}`) 30 | } 31 | } 32 | this.lodCfg() 33 | if (!fs.existsSync('./temp/FileToUrl')) fs.mkdirSync('./temp/FileToUrl') 34 | } 35 | 36 | /** 旧版本配置迁移 */ 37 | async lodCfg () { 38 | const QQBot = this._path + 'QQBot.yaml' 39 | const bot = this._path + 'bot.yaml' 40 | let state = false 41 | if (fs.existsSync(QQBot)) { 42 | state = true 43 | const config = new YamlHandler(this.path + 'Token.yaml') 44 | const QQBotCfg = Object.values(Yaml.parse(fs.readFileSync(QQBot, 'utf8'))) 45 | for (const i of QQBotCfg) { 46 | if (!i?.appid) continue 47 | let val = { 48 | model: 2, 49 | appid: i.appid, 50 | token: i.token, 51 | sandbox: i.sandbox, 52 | allMsg: i.allMsg, 53 | removeAt: i.removeAt, 54 | secret: i.secret, 55 | markdown: { 56 | id: i.markdown || '', 57 | type: i.markdown ? 1 : 0, 58 | text: 'text_start', 59 | img_dec: 'img_dec', 60 | img_url: 'img_url' 61 | }, 62 | other: { 63 | Prefix: true, 64 | QQCloud: '', 65 | Tips: false, 66 | 'Tips-GroupId': '' 67 | } 68 | } 69 | config.addVal('QQ_Token', { [i.appid]: val }, 'object') 70 | await lain.sleep(2000) 71 | } 72 | fs.renameSync(QQBot, this._path + 'QQBot.yaml-old') 73 | } 74 | 75 | if (fs.existsSync(bot)) { 76 | state = true 77 | const config = new YamlHandler(this.path + 'Token.yaml') 78 | const botCfg = Object.values(Yaml.parse(fs.readFileSync(bot, 'utf8'))) 79 | for (const i of botCfg) { 80 | if (!i?.appID) continue 81 | if (config.value('QQ_Token', i.appID)) { 82 | config.set(`QQ_Token.${i.appID}.model`, 0) 83 | await lain.sleep(2000) 84 | } else { 85 | let val = { 86 | model: 2, 87 | appid: i.appID, 88 | token: i.token, 89 | sandbox: i.sandbox, 90 | allMsg: i.allMsg, 91 | removeAt: '', 92 | secret: '', 93 | markdown: { 94 | id: '', 95 | type: 0, 96 | text: 'text_start', 97 | img_dec: 'img_dec', 98 | img_url: 'img_url' 99 | }, 100 | other: { 101 | Prefix: true, 102 | QQCloud: '', 103 | Tips: false, 104 | 'Tips-GroupId': '' 105 | } 106 | } 107 | config.addVal('QQ_Token', { [i.appID]: val }, 'object') 108 | await lain.sleep(2000) 109 | } 110 | } 111 | fs.renameSync(bot, this._path + 'bot.yaml-old') 112 | } 113 | if (state) logger.warn('[Lain-plugin] 旧版本配置迁移完毕,请重启生效') 114 | } 115 | 116 | /** QQ频道配置 */ 117 | getQQGuild (guild_id = '') { 118 | let defSet = this.getdefSet('Config-Guild') 119 | let config = this.getConfig('Config-Guild') 120 | return { ...defSet.default, ...config.default, ...config[guild_id] } 121 | } 122 | 123 | /** QQ群、频道机器人token配置 */ 124 | getToken (type, appid = 'all') { 125 | let defSet = this.getdefSet('Token') 126 | let config = this.getConfig('Token') 127 | if (config?.[type]?.[appid]) { 128 | return { ...(defSet?.[type.replace("_Token", "_Default")]?.default || {}), ...(config?.[type.replace("_Token", "_Default")]?.default || {}), ...config[type][appid] } 129 | } 130 | return { ...(defSet?.[type.replace("_Token", "_Default")] || {}), ...(config?.[type.replace("_Token", "_Default")] || {}), ...(config?.[type] || {}) } 131 | } 132 | 133 | /** HTTP服务器配置 */ 134 | get Server () { 135 | return this.getDefOrConfig('Config-Server') 136 | } 137 | 138 | /** HTTP服务器端口 */ 139 | get port () { 140 | return Number(this.Server.port) 141 | } 142 | 143 | /** link替换白名单配置 */ 144 | get WhiteLink () { 145 | return this.Other.WhiteLink 146 | } 147 | 148 | /** 适配器配置 */ 149 | get Adapter () { 150 | return this.getDefOrConfig('Config-Adapter') 151 | } 152 | 153 | /** 标准输入 */ 154 | get Stdin () { 155 | return this.Adapter.Stdin 156 | } 157 | 158 | /** ComWeChat */ 159 | get ComWeChat () { 160 | return this.Adapter.ComWeChat 161 | } 162 | 163 | /** WeXin */ 164 | get WeXin () { 165 | return this.Adapter.WeXin 166 | } 167 | 168 | /** QQ频道基本配置 */ 169 | get GuildCfg () { 170 | return this.getDefOrConfig('Config-Guild') 171 | } 172 | 173 | /** 三叶草配置 */ 174 | get Shamrock () { 175 | return this.Adapter.Shamrock 176 | } 177 | 178 | /** 其他配置 */ 179 | get Other () { 180 | return this.getDefOrConfig('Config-Other') 181 | } 182 | 183 | /** toICQQ */ 184 | get toICQQ () { 185 | return this.Other.ICQQtoFile 186 | } 187 | 188 | /** 本体package.json */ 189 | get YZPackage () { 190 | if (this._YZPackage) return this._YZPackage 191 | 192 | this._YZPackage = JSON.parse(fs.readFileSync('./package.json', 'utf8')) 193 | return this._YZPackage 194 | } 195 | 196 | /** package.json */ 197 | get package () { 198 | if (this._package) return this._package 199 | 200 | this._package = JSON.parse(fs.readFileSync(this._path + '../package.json', 'utf8')) 201 | return this._package 202 | } 203 | 204 | /** 默认配置和用户配置 */ 205 | getDefOrConfig (name) { 206 | let defSet = this.getdefSet(name) 207 | let config = this.getConfig(name) 208 | return { ...defSet, ...config } 209 | } 210 | 211 | /** 212 | *.获取默认配置 213 | * @param name 配置文件名称 214 | */ 215 | getdefSet (name) { 216 | return this.getYaml('defSet', name) 217 | } 218 | 219 | /** 用户配置 */ 220 | getConfig (name) { 221 | return this.getYaml('config', name) 222 | } 223 | 224 | /** 225 | * 获取配置yaml 226 | * @param type 默认跑配置-defSet,用户配置-config 227 | * @param name 名称 228 | */ 229 | getYaml (type, name) { 230 | let file = `${this._path}/${type}/${name}.yaml` 231 | let key = `${type}.${name}` 232 | if (this.config[key]) return this.config[key] 233 | 234 | this.config[key] = Yaml.parse( 235 | fs.readFileSync(file, 'utf8') 236 | ) 237 | 238 | this.watch(file, name, type) 239 | 240 | return this.config[key] 241 | } 242 | 243 | /** 监听配置文件 */ 244 | watch (file, name, type = 'defSet') { 245 | let key = `${type}.${name}` 246 | 247 | if (this.watcher[key]) return 248 | 249 | const watcher = chokidar.watch(file) 250 | watcher.on('change', path => { 251 | delete this.config[key] 252 | if (typeof Bot == 'undefined') return 253 | logger.mark(`[修改配置文件][${type}][${name}]`) 254 | if (this[`change_${name}`]) { 255 | this[`change_${name}`]() 256 | } 257 | }) 258 | 259 | this.watcher[key] = watcher 260 | } 261 | 262 | /** 更新全局Bot中的配置 */ 263 | change_Token () { 264 | const CfgList = Object.values(this.getToken('QQ_Token') ?? {}) 265 | if (CfgList.length) { 266 | for (const i of CfgList) { 267 | if (typeof Bot[i.appid] !== 'undefined') { 268 | Bot[i.appid].config = i 269 | } 270 | if (typeof Bot[`qg_${i.appid}`] !== 'undefined') { 271 | Bot[`qg_${i.appid}`].config = i 272 | } 273 | } 274 | } 275 | } 276 | 277 | /** 删除临时文件 */ 278 | delFile () { 279 | try { 280 | const files = fs.readdirSync('./temp/FileToUrl') 281 | files.map((file) => fs.promises.unlink(`./temp/FileToUrl/${file}`, () => { })) 282 | } catch { } 283 | } 284 | } 285 | 286 | export default new Cfg() 287 | -------------------------------------------------------------------------------- /adapter/shamrock/xiaofei/weather.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import plugin from '../../../../../lib/plugins/plugin.js' 3 | import fetch from 'node-fetch' 4 | import api from "../api.js"; 5 | 6 | let Config, Version 7 | try { 8 | let index = await import('../../../../xiaofei-plugin/components/index.js') 9 | Config = index.Config 10 | Version = index.Version 11 | } catch (err) { 12 | // 没装小飞 忽略 13 | } 14 | const city_list = {} 15 | 16 | export class xiaofei_weather extends plugin { 17 | constructor() { 18 | super({ 19 | /** 功能名称 */ 20 | name: '小飞插件_天气_Shamrock', 21 | /** 功能描述 */ 22 | dsc: '请求腾讯天气网站进行页面截图,目前支持以下命令:【#天气】', 23 | /** https://oicqjs.github.io/oicq/#events */ 24 | event: 'message', 25 | /** 优先级,数字越小等级越高 */ 26 | priority: 1999, 27 | rule: [ 28 | { 29 | /** 命令正则匹配 */ 30 | reg: '^#?(小飞)?(.*)天气$', 31 | /** 执行方法 */ 32 | fnc: 'query_weather' 33 | } 34 | ] 35 | }) 36 | 37 | try { 38 | let setting = Config.getdefSet('setting', 'system') || {} 39 | this.priority = setting.weather == true ? 10 : 2000 40 | } catch (err) { 41 | } 42 | } 43 | 44 | async query_weather() { 45 | if (!Config) { 46 | // 没装小飞 47 | return false 48 | } 49 | if (/^#?小飞设置.*$/.test(this.e.msg)) return false 50 | if (this.e.adapter !== 'shamrock') return false 51 | 52 | let msg = this.e.msg 53 | .replace('#', '') 54 | .replace('小飞', '') 55 | .replace('天气', '') 56 | return await weather(this.e, msg) 57 | } 58 | } 59 | 60 | async function weather(e, search) { 61 | if (search.replace(/ /g, '') == '' || search == '地区') { 62 | if (e.msg.includes('#')) e.reply('格式:#地区天气\r\n例如:#北京天气', true) 63 | return true 64 | } 65 | let area_id = -1; 66 | let reg = null; 67 | let province = ''; 68 | let city = ''; 69 | let district = '' 70 | search = search.replace(/\s\s/g, ' ').replace(/\s\s/g, ' ') 71 | reg = /((.*)省)?((.*)市)?((.*)区)?/.exec(search) 72 | if (reg[2]) { 73 | province = reg[2]; 74 | search = search.replace(province + '省', ' ') 75 | } 76 | if (reg[4]) { 77 | city = reg[4]; 78 | search = search.replace('市', ' ') 79 | } 80 | if (reg[6]) { 81 | district = reg[6]; 82 | search = search.replace('区', ' ') 83 | } 84 | 85 | let res = null 86 | let arr = search.trim().split(' ').reverse() 87 | arr.push(search.trim()) 88 | 89 | for (let index in arr) { 90 | let value = arr[index] 91 | let url = `https://wis.qq.com/city/matching?source=xw&city=${encodeURI(value)}`// 地区名取area_id接口 92 | let response = await fetch(url) // 获取area_id列表 93 | try { 94 | res = await response.json() 95 | } catch (err) { 96 | } 97 | if (res == null || res.status != 200 || !res.data?.internal || res.data?.internal.length < 1) { 98 | continue 99 | } 100 | let internal = res.data.internal 101 | let keys = Object.keys(internal).reverse() 102 | for (let key of keys) { 103 | for (let i = parseInt(index) + 1; i < arr.length; i++) { 104 | if (internal[key].includes(arr[i]) || arr[i].includes(internal[key])) { 105 | area_id = key 106 | break 107 | } 108 | } 109 | if (area_id != -1) break 110 | } 111 | if (area_id != -1) break 112 | } 113 | 114 | if (res == null || res.status != 200 || !res.data?.internal || res.data?.internal.length < 1) { 115 | if (e.msg.includes('#')) e.reply('没有查询到该地区的天气!', true) 116 | return true 117 | } 118 | 119 | let internal = res.data.internal 120 | let keys = Object.keys(internal).reverse() 121 | for (let key of keys) { 122 | let arr = internal[key].split(', ') 123 | 124 | if (province && !province.includes(arr[0])) { 125 | continue 126 | } 127 | 128 | if (city && !city.includes(arr[1])) { 129 | continue 130 | } 131 | 132 | if (district && !district.includes(arr[2])) { 133 | continue 134 | } 135 | 136 | if (arr[0]) province = arr[0] 137 | if (arr[1]) city = arr[1] 138 | if (arr[2]) district = arr[2] 139 | area_id = key 140 | break 141 | } 142 | 143 | if (area_id == -1) { 144 | if (e.msg.includes('#')) e.reply('没有查询到该地区的天气!', true) 145 | return true 146 | } 147 | 148 | let setting = Config.getdefSet('setting', 'system') || {} 149 | if (setting.card_weather) { 150 | try { 151 | let code = 0 152 | for (let index in arr) { 153 | let value = arr[index] 154 | let codes = await api.get_weather_city_code(e.self_id, value); 155 | if (codes?.length > 0) { 156 | city = codes[0].adcode 157 | break 158 | } 159 | } 160 | code === 0 ? code = city_list[area_id] : false 161 | if (!code) { 162 | let cookie = (e.bot || Bot[e.self_id]).cookies['ti.qq.com'] 163 | let options = { 164 | method: 'GET', 165 | headers: { 166 | 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 12; MI 9 Build/SKQ1.211230.001) V1_AND_SQ_8.9.8_3238_YYB_D A_8090800 QQ/8.9.8.9000 NetType/WIFI WebP/0.4.1 Pixel/1080 StatusBarHeight/75 SimpleUISwitch/0 QQTheme/1000 InMagicWin/0 StudyMode/0 CurrentMode/0 CurrentFontScale/1.0 GlobalDensityScale/0.9818182 AppId/537132847', 167 | 'Content-Type': 'application/json', 168 | 'Cookie': cookie 169 | } 170 | }; 171 | 172 | if (!code) { 173 | let url = `https://ti.qq.com/v2/city-selector/index?star=11&redirect=true`; 174 | let response = await fetch(url, options); 175 | let res = await response.text(); 176 | let match = /\
  • /g; 177 | let arr; 178 | while (arr = match.exec(res)) { 179 | city_list[arr[2]] = arr[1]; 180 | } 181 | code = city_list[area_id]; 182 | } 183 | } 184 | if (code > 0) { 185 | await e.reply({ 186 | type: 'weather', 187 | code 188 | }) 189 | } else { 190 | logger.warn("天气卡片发送失败:城市未找到") 191 | } 192 | } catch (err) { 193 | logger.error(err) 194 | if (e.msg.includes('#')) await e.reply('[小飞插件]卡片天气发送失败!') 195 | } 196 | } 197 | 198 | let attentionCity = JSON.stringify([{ 199 | province, 200 | city, 201 | district, 202 | isDefault: true 203 | }]) 204 | 205 | let buff = null 206 | try { 207 | const browser = await xiaofei_plugin.puppeteer.browserInit() 208 | const page = await browser.newPage() 209 | await page.setViewport({ 210 | width: 1280, 211 | height: 1320 212 | }) 213 | 214 | await page.goto('https://tianqi.qq.com/favicon.ico') 215 | await page.evaluate(`localStorage.setItem('attentionCity', '${attentionCity}')`)// 设置默认地区信息 216 | 217 | await page.setRequestInterception(true) 218 | page.on('request', req => { 219 | let urls = [ 220 | 'trace.qq.com' 221 | ] 222 | 223 | let url = req.url() 224 | if (urls.find(val => { 225 | return url.includes(val) 226 | })) { 227 | req.abort() 228 | } else { 229 | req.continue() 230 | } 231 | }) 232 | await page.goto('https://tianqi.qq.com/')// 请求天气页面 233 | 234 | await page.evaluate(() => { 235 | $('a').remove() 236 | $('#ct-footer').remove()// 删除底部导航栏 237 | }) 238 | 239 | await page.evaluate(`$('body').append('

    Created By Yunzai-Bot ${Version.yunzai} & xiaofei-Plugin ${Version.ver}


    ');`)// 增加版本号显示 240 | 241 | let body = await page.$('body') 242 | buff = await body.screenshot({ 243 | // fullPage: true, 244 | type: 'jpeg', 245 | omitBackground: false, 246 | quality: 100 247 | }) 248 | 249 | page.close().catch((err) => logger.error(err)) 250 | 251 | xiaofei_plugin.puppeteer.renderNum++ 252 | xiaofei_plugin.puppeteer.restart() 253 | } catch (err) { 254 | logger.error(err) 255 | } 256 | 257 | if (!buff) { 258 | if (e.msg.includes('#')) await e.reply('[小飞插件]天气截图失败!') 259 | return false 260 | } 261 | 262 | await e.reply(segment.image(buff)) 263 | 264 | return true 265 | } 266 | 267 | function get_bkn(skey) { 268 | let bkn = 5381 269 | skey = new Buffer(skey) 270 | for (let v of skey) { 271 | bkn = bkn + (bkn << 5) + v 272 | } 273 | bkn &= 2147483647 274 | return bkn 275 | } 276 | -------------------------------------------------------------------------------- /resources/common/common.less: -------------------------------------------------------------------------------- 1 | .font(@name, @file) { 2 | @font-face { 3 | font-family: @name; 4 | src: url("./font/@{file}.woff") format('woff'), url("./font/@{file}.ttf") format('truetype'); 5 | } 6 | } 7 | 8 | .font('Number', 'tttgbnumber'); 9 | .font('NZBZ', 'NZBZ'); 10 | .font('YS', 'HYWH-65W'); 11 | 12 | @import "base.less"; 13 | 14 | * { 15 | margin: 0; 16 | padding: 0; 17 | box-sizing: border-box; 18 | -webkit-user-select: none; 19 | user-select: none; 20 | } 21 | 22 | body { 23 | font-size: 18px; 24 | color: #1e1f20; 25 | font-family: Number, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 26 | transform: scale(1.4); 27 | transform-origin: 0 0; 28 | width: 600px; 29 | } 30 | 31 | .container { 32 | width: 600px; 33 | padding: 20px 15px 10px 15px; 34 | background-size: contain; 35 | } 36 | 37 | 38 | .head-box { 39 | border-radius: 15px; 40 | padding: 10px 20px; 41 | position: relative; 42 | color: #fff; 43 | margin-top: 30px; 44 | 45 | .title { 46 | .font-NZBZ; 47 | font-size: 36px; 48 | text-shadow: 0 0 1px #000, 1px 1px 3px rgba(0, 0, 0, .9); 49 | 50 | .label { 51 | display: inline-block; 52 | margin-left: 10px; 53 | } 54 | } 55 | 56 | .genshin_logo { 57 | position: absolute; 58 | top: 1px; 59 | right: 15px; 60 | width: 97px; 61 | } 62 | 63 | .label { 64 | font-size: 16px; 65 | text-shadow: 0 0 1px #000, 1px 1px 3px rgba(0, 0, 0, .9); 66 | 67 | span { 68 | color: #d3bc8e; 69 | padding: 0 2px; 70 | } 71 | } 72 | } 73 | 74 | 75 | .notice { 76 | color: #888; 77 | font-size: 12px; 78 | text-align: right; 79 | padding: 12px 5px 5px; 80 | } 81 | 82 | .notice-center { 83 | color: #fff; 84 | text-align: center; 85 | margin-bottom: 10px; 86 | text-shadow: 1px 1px 1px #333; 87 | } 88 | 89 | .copyright { 90 | font-size: 14px; 91 | text-align: center; 92 | color: #fff; 93 | position: relative; 94 | padding-left: 10px; 95 | text-shadow: 1px 1px 1px #000; 96 | margin: 10px 0; 97 | 98 | .version { 99 | color: #d3bc8e; 100 | display: inline-block; 101 | padding: 0 3px; 102 | } 103 | } 104 | 105 | 106 | /* */ 107 | 108 | .cons { 109 | display: inline-block; 110 | vertical-align: middle; 111 | padding: 0 5px; 112 | border-radius: 4px; 113 | } 114 | 115 | 116 | .cons(@idx, @bg, @color:#fff) { 117 | .cons-@{idx} { 118 | background: @bg; 119 | color: @color; 120 | } 121 | } 122 | 123 | .cons(0, #666); 124 | .cons(n0, #404949); 125 | .cons(1, #5cbac2); 126 | .cons(2, #339d61); 127 | .cons(3, #3e95b9); 128 | .cons(4, #3955b7); 129 | .cons(5, #531ba9cf); 130 | .cons(6, #ff5722); 131 | 132 | .cons2(@idx, @bg, @color:#fff) { 133 | .cons2-@{idx} { 134 | border-radius: 4px; 135 | background: @bg; 136 | color: @color; 137 | } 138 | } 139 | 140 | .cons2(0, #666); 141 | .cons2(1, #71b1b7); 142 | .cons2(2, #369961); 143 | .cons2(3, #4596b9); 144 | .cons2(4, #4560b9); 145 | .cons2(5, #531ba9cf); 146 | .cons2(6, #ff5722); 147 | 148 | /******** Fetter ********/ 149 | 150 | .fetter { 151 | width: 50px; 152 | height: 50px; 153 | display: inline-block; 154 | background: url('./item/fetter.png'); 155 | background-size: auto 100%; 156 | @fetters: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; 157 | each(@fetters, { 158 | &.fetter@{value} { 159 | background-position: (-100%/9)+(100%/9)*@value 0; 160 | } 161 | }) 162 | } 163 | 164 | /******** ELEM ********/ 165 | 166 | @elems: hydro, anemo, cryo, electro, geo, pyro, dendro; 167 | 168 | each(@elems, { 169 | .elem-@{value} .talent-icon { 170 | background-image: url("./bg/talent-@{value}.png"); 171 | } 172 | 173 | .elem-@{value} .elem-bg, 174 | .@{value}-bg { 175 | background-image: url("./bg/bg-@{value}.jpg"); 176 | } 177 | }) 178 | 179 | 180 | /* cont */ 181 | 182 | .cont { 183 | border-radius: 10px; 184 | background: url("../common/cont/card-bg.png") top left repeat-x; 185 | background-size: auto 100%; 186 | // backdrop-filter: blur(3px); 187 | margin: 5px 15px 5px 10px; 188 | position: relative; 189 | box-shadow: 0 0 1px 0 #ccc, 2px 2px 4px 0 rgba(50, 50, 50, .8); 190 | overflow: hidden; 191 | color: #fff; 192 | font-size: 16px; 193 | } 194 | 195 | 196 | .cont-title { 197 | background: rgba(0, 0, 0, .4); 198 | box-shadow: 0 0 1px 0 #fff; 199 | color: #d3bc8e; 200 | padding: 10px 20px; 201 | text-align: left; 202 | border-radius: 10px 10px 0 0; 203 | 204 | span { 205 | font-size: 12px; 206 | color: #aaa; 207 | margin-left: 10px; 208 | font-weight: normal; 209 | } 210 | 211 | &.border-less { 212 | background: linear-gradient(rgba(0, 0, 0, .5), rgba(0, 0, 0, 0)); 213 | box-shadow: none; 214 | padding-bottom: 5px; 215 | } 216 | } 217 | 218 | .cont-body { 219 | padding: 10px 15px; 220 | font-size: 12px; 221 | background: rgba(0, 0, 0, 0.5); 222 | box-shadow: 0 0 1px 0 #fff; 223 | font-weight: normal; 224 | } 225 | 226 | 227 | .cont-footer { 228 | padding: 10px 15px; 229 | font-size: 12px; 230 | background: rgba(0, 0, 0, 0.5); 231 | font-weight: normal; 232 | } 233 | 234 | .cont > ul.cont-msg { 235 | display: block; 236 | padding: 5px 10px; 237 | background: rgba(0, 0, 0, 0.5); 238 | } 239 | 240 | ul.cont-msg, .cont-footer ul { 241 | padding-left: 15px; 242 | 243 | li { 244 | margin: 5px 0; 245 | margin-left: 15px; 246 | 247 | strong { 248 | font-weight: normal; 249 | margin: 0 2px; 250 | color: #d3bc8e; 251 | } 252 | } 253 | } 254 | 255 | .cont-table { 256 | display: table; 257 | width: 100%; 258 | } 259 | 260 | .cont-table .tr { 261 | display: table-row; 262 | } 263 | 264 | .cont-table .tr:nth-child(even) { 265 | background: rgba(0, 0, 0, .4); 266 | } 267 | 268 | .cont-table .tr:nth-child(odd) { 269 | background: rgba(50, 50, 50, .4); 270 | } 271 | 272 | .cont-table .tr > div, 273 | .cont-table .tr > td { 274 | display: table-cell; 275 | box-shadow: 0 0 1px 0 #fff; 276 | } 277 | 278 | .cont-table .tr > div.value-full { 279 | display: table; 280 | width: 200%; 281 | } 282 | 283 | .cont-table .tr > div.value-none { 284 | box-shadow: none; 285 | } 286 | 287 | .cont-table .thead { 288 | text-align: center; 289 | } 290 | 291 | .cont-table .thead > div, 292 | .cont-table .thead > td { 293 | color: #d3bc8e; 294 | background: rgba(0, 0, 0, .4); 295 | line-height: 40px; 296 | height: 40px; 297 | } 298 | 299 | 300 | .cont-table .title, 301 | .cont-table .th { 302 | color: #d3bc8e; 303 | padding-right: 15px; 304 | text-align: right; 305 | background: rgba(0, 0, 0, .4); 306 | min-width: 100px; 307 | vertical-align: middle; 308 | } 309 | 310 | .logo { 311 | font-size: 18px; 312 | text-align: center; 313 | color: #fff; 314 | margin: 20px 0 10px 0; 315 | } 316 | 317 | /* item-icon */ 318 | .item-icon { 319 | width: 100%; 320 | height: 100%; 321 | border-radius: 4px; 322 | position: relative; 323 | overflow: hidden; 324 | 325 | .img { 326 | width: 100%; 327 | height: 100%; 328 | display: block; 329 | background-size: contain; 330 | background-position: center; 331 | background-repeat: no-repeat; 332 | } 333 | 334 | &.artis { 335 | .img { 336 | width: 84%; 337 | height: 84%; 338 | margin: 8%; 339 | } 340 | } 341 | 342 | @stars: 1, 2, 3, 4, 5; 343 | each(@stars, { 344 | &.star@{value} { 345 | background-image: url("../common/item/bg@{value}.png"); 346 | } 347 | &.opacity-bg.star@{value} { 348 | background-image: url("../common/item/bg@{value}-o.png"); 349 | } 350 | }) 351 | 352 | &.star-w { 353 | background: #fff; 354 | } 355 | } 356 | 357 | .item-list { 358 | display: flex; 359 | 360 | .item-card { 361 | width: 70px; 362 | background: #e7e5d9; 363 | } 364 | 365 | .item-icon { 366 | border-bottom-left-radius: 0; 367 | border-bottom-right-radius: 12px; 368 | } 369 | 370 | .item-title { 371 | color: #222; 372 | font-size: 13px; 373 | text-align: center; 374 | padding: 2px; 375 | white-space: nowrap; 376 | overflow: hidden; 377 | } 378 | 379 | .item-icon { 380 | height: initial; 381 | } 382 | 383 | .item-badge { 384 | position: absolute; 385 | display: block; 386 | left: 0; 387 | top: 0; 388 | background: rgba(0, 0, 0, 0.6); 389 | font-size: 12px; 390 | color: #fff; 391 | padding: 4px 5px 3px; 392 | border-radius: 0 0 6px 0; 393 | } 394 | } --------------------------------------------------------------------------------