UNPKG

koishi-plugin-jrys-prpr

Version:

[<ruby>**jrys-prpr**<rp>(</rp><rt>点我查看预览图</rt><rp>)</rp></ruby>](https://i0.hdslb.com/bfs/article/ae33f1b2e9dbc3fe89363a40fbf040703493298333289018.png)😽QQ官方json按钮支持,20个群即可发按钮!支持 monetary!很好看的字体! 支持自动清理记录内容。

931 lines (864 loc) 76.5 kB
import { Schema, h, Random, Context, sleep } from "koishi" import { } from "koishi-plugin-puppeteer" import { } from "koishi-plugin-monetary" import { } from "koishi-plugin-canvas" import jrys_json from "./../data/jrys.json" import fs from 'node:fs' import path from "node:path" import crypto from "node:crypto" import { pathToFileURL, fileURLToPath } from 'node:url' // 运势数据 interface JrysData { fortuneSummary: string luckyStar: string signText: string unsignText: string luckValue: number } export const name = 'jrys-prpr' export const inject = { required: ['i18n', 'logger', 'http', 'puppeteer'], optional: ['canvas', "monetary", "database"] } export const usage = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>运势卡片说明</title> </head> <body> <div> <h1>获取运势卡片 🧧</h1> <p>发送指令 <code>jrysprpr</code> 即可获取一张个性化的运势卡片。</p> <p>您还可以使用 <code>--split</code> 选项来获取图文模式的运势,只需发送 <code>jrysprpr -s</code> 即可。</p> <h3>如果您想获取运势卡的背景图,需要启用<code>原图</code>指令</h3> <h3>可以直接回复一张已发送的运势卡图片并输入指令 <code>获取原图</code>。</h3> <p>或者使用 <code>获取原图 ********</code> 来获取对应标识码的背景图。</p> <p>如果您使用的是QQ官方bot,也可以通过点击markdown运势卡上的“查看原图”按钮来获取。</p> <hr> </div> </body> </html> ` const defaultFortuneProbability = [ { "Fortune": "☆☆☆☆☆☆☆", "luckValue": 0, "Probability": 5 }, { "Fortune": "★☆☆☆☆☆☆", "luckValue": 14, "Probability": 10 }, { "Fortune": "★★☆☆☆☆☆", "luckValue": 28, "Probability": 12 }, { "Fortune": "★★★☆☆☆☆", "luckValue": 42, "Probability": 15 }, { "Fortune": "★★★★☆☆☆", "luckValue": 56, "Probability": 30 }, { "Fortune": "★★★★★☆☆", "luckValue": 70, "Probability": 35 }, { "Fortune": "★★★★★★☆", "luckValue": 84, "Probability": 45 }, { "Fortune": "★★★★★★★", "luckValue": 98, "Probability": 25 } ] export const Config = Schema.intersect([ Schema.object({ command: Schema.string().default('jrysprpr').description("`签到`指令自定义"), command2: Schema.string().default('查看运势背景图').description("`原图`指令自定义"), //authority: Schema.number().default(1).description("指令权限设置"), GetOriginalImageCommand: Schema.boolean().description("开启后启用`原图`指令,可以获取运势背景原图").default(true), autocleanjson: Schema.boolean().description("自动获取原图后,删除对应的json记录信息").default(true), Checkin_HintText: Schema.union([ Schema.const('unset').description('unset').description("不返回提示语"), Schema.string().description('string').description("请在右侧修改提示语").default("正在分析你的运势哦~请稍等~~"), ]).description("`签到渲染中`提示语"), recallCheckin_HintText: Schema.boolean().description("jrys结果发送后,自动撤回`Checkin_HintText`提示语").default(true), GetOriginalImage_Command_HintText: Schema.union([ Schema.const('1').description('不返回文字提示'), Schema.const('2').description('返回文字提示,且为图文消息'), Schema.const('3').description('返回文字提示,且为单独发送的文字消息'), ]).role('radio').default('2').description("是否返回获取原图的文字提示。开启后,会发送`获取原图,请发送「原图 ******」`这样的文字提示"), FortuneProbabilityAdjustmentTable: Schema.array(Schema.object({ Fortune: Schema.string().description('运势种类'),//.disabled() // disabled时,Probability拉条拉到0 ,会偶现点不下去的情况,反正就是难交互 luckValue: Schema.number().description('种类数值').hidden(), Probability: Schema.number().role('slider').min(0).max(100).step(1).description('抽取权重'), })).role('table').description('运势抽取概率调节表`权重均为0时使用默认配置项`').default(defaultFortuneProbability), BackgroundURL: Schema.array(String).description("背景图片,可以写`txt路径(网络图片URL写进txt里)` 或者 `文件夹路径` 或者 `网络图片URL` <br> 建议参考 [emojihub-bili](/market?keyword=emojihub-bili)的图片方法 <br>推荐使用本地图片 以加快渲染速度").role('table') .default([ path.join(__dirname, './../data/backgroundFolder/miao.jpg'), path.join(__dirname, './../data/backgroundFolder'), path.join(__dirname, './../data/backgroundFolder/魔卡.txt'), path.join(__dirname, './../data/backgroundFolder/ba.txt'), path.join(__dirname, './../data/backgroundFolder/猫羽雫.txt'), path.join(__dirname, './../data/backgroundFolder/miku.txt'), path.join(__dirname, './../data/backgroundFolder/白圣女.txt'), //path.join(__dirname, './../data/backgroundFolder/.txt'), ]), }).description('基础设置'), Schema.object({ screenshotquality: Schema.number().role('slider').min(0).max(100).step(1).default(50).description('设置图片压缩质量(%)'), HTML_setting: Schema.object({ UserNameColor: Schema.string().default("rgba(255,255,255,1)").role('color').description('用户名称的颜色').hidden(), //.hidden(), 暂时用不到了 MaskColor: Schema.string().default("rgba(0,0,0,0.5)").role('color').description('`蒙版`的颜色'), Maskblurs: Schema.number().role('slider').min(0).max(100).step(1).default(10).description('模版模糊半径'), HoroscopeTextColor: Schema.string().default("rgba(255,255,255,1)").role('color').description('`运势文字`颜色'), luckyStarGradientColor: Schema.boolean().description("开启后`运势星星`使用彩色渐变").default(true), HoroscopeDescriptionTextColor: Schema.string().default("rgba(255,255,255,1)").role('color').description('`运势说明文字`颜色'), DashedboxThickn: Schema.number().role('slider').min(0).max(20).step(1).default(5).description('`虚线框`的粗细'), Dashedboxcolor: Schema.string().default("rgba(255, 255, 255, 0.5)").role('color').description('`虚线框`的颜色'), fontPath: Schema.string().description("`请填写.ttf 字体文件的绝对路径`").default(path.join(__dirname, './../data/千图马克手写体lite.ttf')), }).collapse().description('可自定义各种颜色搭配和字体'), }).description('面板调节'), Schema.object({ markdown_button_mode: Schema.union([ Schema.const('unset').description('取消应用此配置项'), Schema.const('json').description('json按钮-----------20 群(频道不可用)'), Schema.const('markdown').description('被动md模板--------2000 DAU / 私域'), Schema.const('markdown_raw_json').description('被动md模板--------2000 DAU - 原生按钮'), Schema.const('raw').description('原生md------------10000 DAU'), Schema.const('raw_jrys').description('原生md-不渲染jrys-----------10000 DAU'), ]).role('radio').description('markdown/按钮模式选择').default("unset"), }).description('QQ官方按钮设置'), Schema.union([ Schema.object({ markdown_button_mode: Schema.const("json").required(), markdown_button_mode_initiative: Schema.boolean().description("开启后,使用 主动消息 发送markdown。<br>即开启后不带`messageId`发送<br>适用于私域机器人频道使用。私域机器人需要使用`被动md模板、json模板`并且开启此配置项<br>`单独发送按钮功能` 已经不能被新建的官方机器人使用").default(false), markdown_button_mode_keyboard: Schema.boolean().description("开启后,markdown加上按钮。关闭后,不加按钮内容哦<br>不影响markdown发送,多用于调试功能使用").default(true).experimental().hidden(), nested: Schema.object({ json_button_template_id: Schema.string().description("模板ID<br>形如 `123456789_1234567890` 的ID编号<br>更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)").pattern(/^\d+_\d+$/), }).collapse().description('➢表情包--按钮设置<br>更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)<hr style="border: 2px solid red"><hr style="border: 2px solid red">'), }), Schema.object({ markdown_button_mode: Schema.const("markdown").required(), markdown_button_mode_initiative: Schema.boolean().description("开启后,使用 主动消息 发送markdown。<br>即开启后不带`messageId`发送<br>适用于私域机器人频道使用。私域机器人需要使用`被动md模板、json模板`并且开启此配置项").default(false), markdown_button_mode_keyboard: Schema.boolean().description("开启后,markdown加上按钮。关闭后,不加按钮内容哦<br>不影响markdown发送,多用于调试功能使用").default(true).experimental(), QQchannelId: Schema.string().description('`填入QQ频道的频道ID`,将该ID的频道作为中转频道 <br> 频道ID可以用[inspect插件来查看](/market?keyword=inspect) `频道ID应为纯数字`').experimental().pattern(/^\S+$/), nested: Schema.object({ markdown_button_template_id: Schema.string().description("md模板ID<br>形如 `123456789_1234567890` 的ID编号,发送markdown").pattern(/^\d+_\d+$/), markdown_button_keyboard_id: Schema.string().description("按钮模板ID<br>形如 `123456789_1234567890` 的ID编号,发送按钮").pattern(/^\d+_\d+$/), markdown_button_content_table: Schema.array(Schema.object({ raw_parameters: Schema.string().description("原始参数名称"), replace_parameters: Schema.string().description("替换参数名称"), })).role('table').default([ { "raw_parameters": "your_markdown_text_1", "replace_parameters": "表情包来啦!" }, { "raw_parameters": "your_markdown_text_2", "replace_parameters": "这是你的表情包哦😽" }, { "raw_parameters": "your_markdown_img", "replace_parameters": "${img_pxpx}" }, { "raw_parameters": "your_markdown_url", "replace_parameters": "${img_url}" } ]).description("替换参数映射表<br>本插件会替换模板变量,请在左侧填入模板变量,右侧填入真实变量值。<br>本插件提供的参数有`encodedMessageTime`、`img_pxpx`、`img_url`、`ctx`、`session`、`config`<br>`img_pxpx`会被替换为`img#...px #...px`<br>`img_url`会被替换为`一个链接`,其中img_pxpx参数需要使用`canvas`服务<br>▶比如你可以使用`{{.session.userId}}`,这会被本插件替换为`真实的userId值`,若无匹配变量,则视为文本<br>更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)"), }).collapse().description('➢表情包--按钮设置<br>更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)<hr style="border: 2px solid red"><hr style="border: 2px solid red">'), }), Schema.object({ markdown_button_mode: Schema.const("markdown_raw_json").required(), markdown_button_mode_initiative: Schema.boolean().description("开启后,使用 主动消息 发送markdown。<br>即开启后不带`messageId`发送<br>适用于私域机器人频道使用。私域机器人需要使用`被动md模板、json模板`并且开启此配置项").hidden().default(false), markdown_button_mode_keyboard: Schema.boolean().description("开启后,markdown加上按钮。关闭后,不加按钮内容哦<br>不影响markdown发送,多用于调试功能使用").default(true).experimental(), QQchannelId: Schema.string().description('`填入QQ频道的频道ID`,将该ID的频道作为中转频道 <br> 频道ID可以用[inspect插件来查看](/market?keyword=inspect) `频道ID应为纯数字`').experimental().pattern(/^\S+$/), nested: Schema.object({ markdown_raw_json_button_template_id: Schema.string().description("md模板ID<br>形如 `123456789_1234567890` 的ID编号,发送markdown").pattern(/^\d+_\d+$/), markdown_raw_json_button_content_table: Schema.array(Schema.object({ raw_parameters: Schema.string().description("原始参数名称"), replace_parameters: Schema.string().description("替换参数名称"), })).role('table').default([ { "raw_parameters": "your_markdown_text_1", "replace_parameters": "表情包来啦!" }, { "raw_parameters": "your_markdown_text_2", "replace_parameters": "这是你的表情包哦😽" }, { "raw_parameters": "your_markdown_img", "replace_parameters": "${img_pxpx}" }, { "raw_parameters": "your_markdown_url", "replace_parameters": "${img_url}" } ]).description("替换参数映射表<br>本插件会替换模板变量,请在左侧填入模板变量,右侧填入真实变量值。<br>本插件提供的参数有`encodedMessageTime`、`img_pxpx`、`img_url`、`ctx`、`session`、`config`<br>`img_pxpx`会被替换为`img#...px #...px`<br>`img_url`会被替换为`一个链接`,其中img_pxpx参数需要使用`canvas`服务<br>▶比如你可以使用`{{.session.userId}}`,这会被本插件替换为`真实的userId值`,若无匹配变量,则视为文本<br>更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)"), markdown_raw_json_button_keyboard: Schema.string().role('textarea', { rows: [12, 12] }).collapse() .default("{\n \"rows\": [\n {\n \"buttons\": [\n {\n \"render_data\": {\n \"label\": \"再来一张😺\",\n \"style\": 2\n },\n \"action\": {\n \"type\": 2,\n \"permission\": {\n \"type\": 2\n },\n \"data\": \"/${config.command}\",\n \"enter\": true\n }\n },\n {\n \"render_data\": {\n \"label\": \"查看原图😽\",\n \"style\": 2\n },\n \"action\": {\n \"type\": 2,\n \"permission\": {\n \"type\": 2\n },\n \"data\": \"/获取原图 ${encodedMessageTime}\",\n \"enter\": true\n }\n }\n ]\n }\n ]\n}") .description('实现QQ官方bot的按钮效果<br>在这里填入你的按钮内容,注意保持json格式,推荐在编辑器中编辑好后粘贴进来'), }).collapse().description('➢表情包--按钮设置<br>更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)<hr style="border: 2px solid red"><hr style="border: 2px solid red">'), }), Schema.object({ markdown_button_mode: Schema.const("raw").required(), markdown_button_mode_initiative: Schema.boolean().description("开启后,使用 主动消息 发送markdown。<br>即开启后不带`messageId`发送<br>适用于私域机器人频道使用。私域机器人需要使用`被动md模板、json模板`并且开启此配置项").hidden().default(false), markdown_button_mode_keyboard: Schema.boolean().description("开启后,markdown加上按钮。关闭后,不加按钮内容哦<br>不影响markdown发送,多用于调试功能使用").default(true).experimental(), QQchannelId: Schema.string().description('`填入QQ频道的频道ID`,将该ID的频道作为中转频道 <br> 频道ID可以用[inspect插件来查看](/market?keyword=inspect) `频道ID应为纯数字`').experimental().pattern(/^\S+$/), nested: Schema.object({ raw_markdown_button_content: Schema.string().role('textarea', { rows: [6, 6] }).collapse().default("## **今日运势😺**\n### 😽您今天的运势是:\n![${img_pxpx}](${img_url})") .description('实现QQ官方bot的按钮效果,需要`canvas`服务。<br>在这里填入你的markdown内容。本插件会替换形如`{{.xxx}}`或`${xxx}`的参数为`xxx`。<br>本插件提供的参数有`encodedMessageTime`、`img_pxpx`、`img_url`、`ctx`、`session`、`config`<br>`img_pxpx`会被替换为`img#...px #...px`<br>`img_url`会被替换为`一个链接`更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)'), raw_markdown_button_keyboard: Schema.string().role('textarea', { rows: [12, 12] }).collapse() .default("{\n \"rows\": [\n {\n \"buttons\": [\n {\n \"render_data\": {\n \"label\": \"再来一张😺\",\n \"style\": 2\n },\n \"action\": {\n \"type\": 2,\n \"permission\": {\n \"type\": 2\n },\n \"data\": \"/${config.command}\",\n \"enter\": true\n }\n },\n {\n \"render_data\": {\n \"label\": \"查看原图😽\",\n \"style\": 2\n },\n \"action\": {\n \"type\": 2,\n \"permission\": {\n \"type\": 2\n },\n \"data\": \"/获取原图 ${encodedMessageTime}\",\n \"enter\": true\n }\n }\n ]\n }\n ]\n}") .description('实现QQ官方bot的按钮效果<br>在这里填入你的按钮内容,注意保持json格式,推荐在编辑器中编辑好后粘贴进来'), }).collapse().description('➢表情包--按钮设置<br>更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)<hr style="border: 2px solid red"><hr style="border: 2px solid red">'), }), Schema.object({ markdown_button_mode: Schema.const("raw_jrys").required(), markdown_button_mode_initiative: Schema.boolean().description("开启后,使用 主动消息 发送markdown。<br>即开启后不带`messageId`发送<br>适用于私域机器人频道使用。私域机器人需要使用`被动md模板、json模板`并且开启此配置项").hidden().default(false), markdown_button_mode_keyboard: Schema.boolean().description("开启后,markdown加上按钮。关闭后,不加按钮内容哦<br>不影响markdown发送,多用于调试功能使用").default(true).experimental(), QQchannelId: Schema.string().description('`填入QQ频道的频道ID`,将该ID的频道作为中转频道 <br> 频道ID可以用[inspect插件来查看](/market?keyword=inspect) `频道ID应为纯数字`').experimental().pattern(/^\S+$/), nested: Schema.object({ raw_jrys_markdown_button_content: Schema.string().role('textarea', { rows: [6, 6] }).collapse().default("${qqbotatuser}\n您的今日运势为:\n**${dJson.fortuneSummary}**\n${dJson.luckyStar}\n\n> ${dJson.unsignText}\n![${img_pxpx}](${img_url})\n\n> 仅供娱乐|相信科学|请勿迷信") .description('实现QQ官方bot的按钮效果,需要`canvas`服务。<br>在这里填入你的markdown内容。本插件会替换形如`{{.xxx}}`或`${xxx}`的参数为`xxx`。<br>本插件提供的参数有`dJson`、`img_pxpx`、`img_url`、`ctx`、`session`、`config`<br>`img_pxpx`会被替换为`img#...px #...px`<br>`img_url`会被替换为`一个链接`更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)'), raw_jrys_markdown_button_keyboard: Schema.string().role('textarea', { rows: [12, 12] }).collapse() .default("{\n \"rows\": [\n {\n \"buttons\": [\n {\n \"render_data\": {\n \"label\": \"再来一张😺\",\n \"style\": 2\n },\n \"action\": {\n \"type\": 2,\n \"permission\": {\n \"type\": 2\n },\n \"data\": \"/${config.command}\",\n \"enter\": true\n }\n }\n ]\n }\n ]\n}") .description('实现QQ官方bot的按钮效果<br>在这里填入你的按钮内容,注意保持json格式,推荐在编辑器中编辑好后粘贴进来'), }).collapse().description('➢表情包--按钮设置<br>更多说明,详见[➩项目README](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/emojihub-bili)<hr style="border: 2px solid red"><hr style="border: 2px solid red">'), }), Schema.object({}), ]), Schema.object({ enablecurrency: Schema.boolean().description("开启后,签到获取货币").default(false), currency: Schema.string().default('jrysprpr').description('monetary 数据库的 currency 字段名称'), maintenanceCostPerUnit: Schema.number().role('slider').min(0).max(1000).step(1).default(100).description("签到获得的货币数量"), }).description('monetary·通用货币设置'), Schema.object({ retryexecute: Schema.boolean().default(false).description(" `重试机制`。触发`渲染失败`时,是否自动重新执行"), }).description('进阶功能'), Schema.union([ Schema.object({ retryexecute: Schema.const(true).required(), maxretrytimes: Schema.number().role('slider').min(0).max(10).step(1).default(1).description("最大的重试次数<br>`0`代表`不重试`"), }), Schema.object({}), ]), Schema.object({ Repeated_signin_for_different_groups: Schema.boolean().default(false).description("允许同一个用户从不同群组签到"), consoleinfo: Schema.boolean().default(false).description("日志调试模式`日常使用无需开启`"), }).description('调试功能'), ]) export function apply(ctx: Context, config) { ctx.on('ready', async () => { const root = path.join(ctx.baseDir, 'data', 'jrys-prpr') const jsonFilePath = path.join(root, 'OriginalImageURL_data.json') // 在全局作用域中定义字体 Base64 缓存 let cachedFontBase64 = null const retryCounts = {} // 使用一个对象来存储每个用户的重试次数 if (!fs.existsSync(root)) { fs.mkdirSync(root, { recursive: true }) } // 检查并创建 JSON 文件 if (!fs.existsSync(jsonFilePath)) { fs.writeFileSync(jsonFilePath, JSON.stringify([])) } ctx.model.extend("jrysprprdata" as any, { userid: "string", // 用户ID唯一标识 channelId: "string", // 频道ID lastSignIn: "string" // 最后签到日期 }, { primary: ["userid", "channelId"] }) ctx.i18n.define("zh-CN", { commands: { [config.command]: { description: "查看今日运势", messages: { Getbackgroundimage: "获取原图,请发送:{0}", CurrencyGetbackgroundimage: "签到成功!获得点数: {0}\n获取原图,请发送:{1}", CurrencyGetbackgroundimagesplit: "签到成功!获得点数: {0}", hasSignedInTodaysplit: "今天已经签到过了,不再获得货币。", hasSignedInToday: "今天已经签到过了,不再获得货币。\n获取原图,请发送:{0}", } }, [config.command2]: { description: "获取运势原图", messages: { Inputerror: "请回复一张运势图,或者输入运势图的消息ID 以获取原图哦\~", QQInputerror: "请输入运势图的消息ID以获取原图哦\~", FetchIDfailed: "未能提取到消息ID,请确认回复的消息是否正确。", aleadyFetchID: "该消息背景已被获取过啦~ 我已经忘掉了~找不到咯", Failedtogetpictures: "获取运势图原图失败,请稍后再试" } } } }) if (config.GetOriginalImageCommand) { ctx.command(`${config.command2} <InputmessageId:text>`, { authority: 1 }) .alias('获取原图') .action(async ({ session }, InputmessageId) => { try { const isQQPlatform = session.platform === 'qq' const hasReplyContent = !!session.quote?.content if (!hasReplyContent && !isQQPlatform && !InputmessageId) { return session.text(".Inputerror") } if (isQQPlatform && !InputmessageId) { return session.text(".QQInputerror") } const messageId = hasReplyContent ? session.quote.messageId : InputmessageId logInfo(`尝试获取背景图:\n${messageId}`) if (!messageId) { return session.text(".FetchIDfailed") } const originalImageURL = await getOriginalImageURL(messageId) logInfo(`运势背景原图链接:\n ${originalImageURL}`) if (originalImageURL) { const sendsuccess = await session.send(h.image(originalImageURL)) if (config.autocleanjson && sendsuccess) { // 删除对应的JSON记录 await deleteImageRecord(messageId, originalImageURL) } return } else if (config.autocleanjson) { return session.text(".aleadyFetchID") } else { return session.text(".FetchIDfailed") } } catch (error) { ctx.logger.error("获取运势图原图时出错: ", error) return session.text(".Failedtogetpictures") } }) } ctx.command(`${config.command}`, { authority: 1 }) .userFields(["id"]) .option('split', '-s 以图文输出今日运势') .action(async ({ session, options }) => { let hasSignedInToday = await alreadySignedInToday(ctx, session.userId, session.channelId) retryCounts[session.userId] = retryCounts[session.userId] || 0 // 初始化重试次数 let Checkin_HintText_messageid let backgroundImage = getRandomBackground(config) let BackgroundURL = backgroundImage.replace(/\\/g, '/') let imageBuffer const dJson = await getJrys(session) if (options.split) { // 如果开启了分离模式,那就只返回图文消息内容。即文字运势内容与背景图片 if (config.Checkin_HintText) { Checkin_HintText_messageid = await session.send(config.Checkin_HintText) } let textjrys = ` ${dJson.fortuneSummary} ${dJson.luckyStar}\n ${dJson.signText}\n ${dJson.unsignText}\n ` let enablecurrencymessage if (config.enablecurrency) { if (hasSignedInToday) { enablecurrencymessage = h.text(session.text(".hasSignedInTodaysplit")) } else { enablecurrencymessage = h.text(session.text(".CurrencyGetbackgroundimagesplit", [config.maintenanceCostPerUnit])) } } let backgroundImage = getRandomBackground(config) let BackgroundURL = backgroundImage.replace(/\\/g, '/') let BackgroundURL_base64 = await convertToBase64image(BackgroundURL) let message = [ h.image(BackgroundURL_base64), h.text(textjrys), enablecurrencymessage ] if (config.enablecurrency && !hasSignedInToday) { await updateUserCurrency(session.user.id, config.maintenanceCostPerUnit) } await recordSignIn(ctx, session.userId, session.channelId) await session.send(message) if (Checkin_HintText_messageid && config.recallCheckin_HintText) { await session.bot.deleteMessage(session.channelId, Checkin_HintText_messageid) } return } if (config.Checkin_HintText) { Checkin_HintText_messageid = await session.send(config.Checkin_HintText) } let page try { if (config.markdown_button_mode !== "raw_jrys") { page = await ctx.puppeteer.page() await page.setViewport({ width: 1080, height: 1920 }) let BackgroundURL_base64 = await convertToBase64image(BackgroundURL) // 读取 Base64 字体字符串 logInfo(config.HTML_setting.fontPath) // 如果字体 Base64 未缓存,则读取并缓存 if (!cachedFontBase64) { cachedFontBase64 = await getFontBase64(config.HTML_setting.fontPath) } // 使用缓存的字体 Base64 const fontBase64 = cachedFontBase64 let insertHTMLuseravatar = session.event.user.avatar let luckyStarHTML = ` .lucky-star { font-size: 60px; margin-bottom: 10px; } ` if (config.HTML_setting.luckyStarGradientColor) { luckyStarHTML = ` .lucky-star { font-size: 60px; margin-bottom: 10px; background: linear-gradient(to right, #fcb5b5, #fcd6ae, #fde8a6, #c3f7b1, #aed6fa, #c4aff5, #f1afcc); -webkit-background-clip: text; background-clip: text; color: transparent; } ` } const formattedDate = await getFormattedDate() let HTMLsource = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>运势卡片</title> <style> @font-face { font-family: "千图马克手写体lite"; src: url('data:font/ttf;base64,${fontBase64}') format('truetype'); } body, html { height: 100%; margin: 0; overflow: hidden; font-family: "千图马克手写体lite"; } .background { background-image: url('${BackgroundURL_base64}'); background-size: cover; background-position: center; position: relative; width: 1080px; height: 1920px; } .overlay { position: absolute; bottom: 0; left: 0; width: 100%; min-height: 1%; background-color: ${config.HTML_setting.MaskColor}; backdrop-filter: blur(${config.HTML_setting.Maskblurs}px); border-radius: 20px 20px 0 0; overflow: visible; } .user-info { display: flex; align-items: center; padding: 10px 20px; position: relative; } .user-avatar { width: 120px; height: 120px; border-radius: 60px; background-image: url('${insertHTMLuseravatar}'); background-size: cover; background-position: center; margin-left: 20px; position: absolute; top: 40px; } .username { margin-left: 10px; color: ${config.HTML_setting.UserNameColor}; font-size: 50px; padding-top: 28px; } .fortune-info1 { display: flex; color: ${config.HTML_setting.HoroscopeTextColor}; flex-direction: column; align-items: center; position: relative; width: 100%; justify-content: center; /* 居中 */ margin-top: 0px; /* 上边距 */ } .fortune-info1 > * { margin: 10px; /* 元素之间的间距 */ } .fortune-info2 { color: ${config.HTML_setting.HoroscopeDescriptionTextColor}; padding: 0 20px; margin-top: 40px; } .lucky-star, .sign-text, .unsign-text { margin-bottom: 12px; font-size: 42px; } .fortune-summary { font-size: 60px; } ${luckyStarHTML} .sign-text, .unsign-text { font-size: 32px; line-height: 1.6; padding: 10px; border: ${config.HTML_setting.DashedboxThickn}px dashed ${config.HTML_setting.Dashedboxcolor}; border-radius: 15px; margin-top: 10px; } .today-text { font-size: 45px; margin-bottom: 10px; background: linear-gradient(to right, #fcb5b5, #fcd6ae, #fde8a6, #c3f7b1, #aed6fa, #c4aff5, #f1afcc); -webkit-background-clip: text; background-clip: text; color: transparent; } </style> </head> <body> <div class="background"> <div class="overlay"> <div class="user-info"> <div class="user-avatar"></div> <!--span class="username">上学大人</span--> </div> <div class="fortune-info1"> <div class="today-text">${formattedDate}</div> <div class="fortune-summary">${dJson.fortuneSummary}</div> <div class="lucky-star">${dJson.luckyStar}</div> </div> <div class="fortune-info2"> <div class="sign-text">${dJson.signText}</div> <div class="unsign-text"> ${dJson.unsignText} </div> <!-- 不要迷信哦 --> <div style="text-align: center; font-size: 24px; margin-bottom: 15px;"> 仅供娱乐 | 相信科学 | 请勿迷信 </div> </div> </div> </div> </body> </html> ` logInfo(`触发用户: ${session.event.user?.id}`) logInfo(`使用的格式化时间: ${formattedDate}`) if (session.platform === 'qq') { logInfo(`QQ官方:bot: ${session.bot.config.id}`) logInfo(`QQ官方:用户头像: http://q.qlogo.cn/qqapp/${session.bot.config.id}/${session.event.user?.id}/640`) } logInfo(`使用背景URL: ${BackgroundURL}`) logInfo(`蒙版颜色: ${config.HTML_setting.MaskColor}`) logInfo(`虚线框粗细: ${config.HTML_setting.DashedboxThickn}`) logInfo(`虚线框颜色: ${config.HTML_setting.Dashedboxcolor}`) await page.setContent(HTMLsource) // 等待网络空闲 await page.waitForNetworkIdle() const element = await page.$('body') imageBuffer = await element.screenshot({ type: "jpeg", // 使用 JPEG 格式 encoding: "binary", quality: config.screenshotquality // 设置图片质量 }) } else { if (BackgroundURL.startsWith('data:image/')) { // Base64 图片数据 const base64Data = BackgroundURL.split(',')[1] imageBuffer = Buffer.from(base64Data, 'base64') } else if (BackgroundURL.startsWith('http://') || BackgroundURL.startsWith('https://')) { // 网络 URL imageBuffer = await ctx.http.get(BackgroundURL, { responseType: 'arraybuffer' }) imageBuffer = Buffer.from(imageBuffer) } else if (BackgroundURL.startsWith('file:///')) { // 本地文件路径(file:/// 格式) const localPath = fileURLToPath(BackgroundURL) imageBuffer = fs.readFileSync(localPath) } else if (fs.existsSync(BackgroundURL)) { // 本地文件路径 imageBuffer = fs.readFileSync(BackgroundURL) } else { throw new Error('不支持的背景图格式') } } const encodeTimestamp = (timestamp) => { // 将日期和时间部分分开 let [date, time] = timestamp.split('T') // 替换一些字符 date = date.replace(/-/g, '') time = time.replace(/:/g, '').replace(/\..*/, '') // 去掉毫秒部分 // 加入随机数 const randomNum = Math.floor(Math.random() * 10000) // 生成一个0到9999的随机数 // 重排字符顺序 return `${time}${date}${randomNum}` } if (config.enablecurrency && !hasSignedInToday) { await updateUserCurrency(session.user.id, config.maintenanceCostPerUnit) } // 发送图片消息并处理响应 const sendImageMessage = async (imageBuffer) => { let sentMessage //let markdownmessageId const messageTime = new Date().toISOString() // 获取当前时间的ISO格式 // 这里就不考虑时区了 只是标记ID而已 确保唯一即可 const encodedMessageTime = encodeTimestamp(messageTime) // 对时间戳进行简单编码 if ((config.markdown_button_mode === "markdown" || config.markdown_button_mode === "raw" || config.markdown_button_mode === "markdown_raw_json" || config.markdown_button_mode === "raw_jrys") && session.platform === 'qq') { const uploadedImageURL = await uploadImageToChannel(imageBuffer, session.bot.config.id, session.bot.config.secret, session.bot.config.token, config.QQchannelId) const qqmarkdownmessage = await markdown(session, encodedMessageTime, uploadedImageURL.url, `data:image/pngbase64,${imageBuffer.toString('base64')}`) await sendmarkdownMessage(session, qqmarkdownmessage) } else { // 根据不同的配置发送不同类型的消息 const imageMessage = h.image(imageBuffer, "image/png") switch (config.GetOriginalImage_Command_HintText) { case '2': // 返回文字提示,且为图文消息 const hintText2_encodedMessageTime = `${config.command2} ${encodedMessageTime}` let hintText2 if (config.enablecurrency) { if (!hasSignedInToday) { hintText2 = session.text(".CurrencyGetbackgroundimage", [config.maintenanceCostPerUnit, hintText2_encodedMessageTime]) } else { hintText2 = session.text(".hasSignedInToday", [hintText2_encodedMessageTime]) } } else { hintText2 = session.text(".Getbackgroundimage", [hintText2_encodedMessageTime]) } const combinedMessage2 = `${imageMessage}\n${hintText2}` logInfo(`获取原图:\n${encodedMessageTime}`) sentMessage = await session.send(combinedMessage2) break case '3': // 返回文字提示,且为单独发送的文字消息 const hintText3_encodedMessageTime = `${config.command2} ${encodedMessageTime}` let hintText3: string if (config.enablecurrency) { if (!hasSignedInToday) { hintText3 = session.text(".CurrencyGetbackgroundimage", [config.maintenanceCostPerUnit, hintText3_encodedMessageTime]) } else { hintText3 = session.text(".hasSignedInToday", [hintText3_encodedMessageTime]) } } else { hintText3 = session.text(".Getbackgroundimage", [hintText3_encodedMessageTime]) } logInfo(`获取原图:\n${encodedMessageTime}`) sentMessage = await session.send(imageMessage) // 先发送图片消息 await session.send(hintText3) // 再单独发送提示 break default: '1'//不返回文字提示,只发送图片 sentMessage = await session.send(imageMessage) break } } if (config.markdown_button_mode === "json" && session.platform === 'qq') { let markdownMessage = { msg_id: session.event.message.id, msg_type: 2, keyboard: { id: config.nested.json_button_template_id }, } await sendmarkdownMessage(session, markdownMessage) } if (config.markdown_button_mode !== "raw_jrys") { // 记录日志 if (config.consoleinfo && session.platform !== 'qq') { if (Array.isArray(sentMessage)) { sentMessage.forEach((messageId, index) => { ctx.logger.info(`发送图片消息ID [${index}]: ${messageId}`) }) } else { ctx.logger.info(`发送的消息对象: ${JSON.stringify(sentMessage, null, 2)}`) } } // 记录消息ID和背景图URL到JSON文件 if (config.GetOriginalImageCommand) { const imageData = { // 使用 encodedMessageTime 作为唯一标识符的一部分 messageId: session.platform === 'qq' ? [encodedMessageTime] : (Array.isArray(sentMessage) ? sentMessage : [sentMessage]), messageTime: encodedMessageTime, // 使用预先获取的时间戳 backgroundURL: BackgroundURL } try { let data = [] if (fs.existsSync(jsonFilePath)) { // 读取JSON文件内容 const fileContent = fs.readFileSync(jsonFilePath, 'utf8') if (fileContent.trim()) { data = JSON.parse(fileContent) } } // 检查数据是否已存在 const exists = data.some(item => item.messageId.includes(imageData.messageId)) if (!exists) { // 添加新数据 data.push(imageData) fs.writeFileSync(jsonFilePath, JSON.stringify(data, null, 2)) } } catch (error) { ctx.logger.error(`处理JSON文件时出错 [${encodedMessageTime}]: `, error) // 记录错误信息并包含时间戳 } } return sentMessage } await recordSignIn(ctx, session.userId, session.channelId) } // 调用函数发送消息 await sendImageMessage(imageBuffer) if (Checkin_HintText_messageid && config.recallCheckin_HintText) { await session.bot.deleteMessage(session.channelId, Checkin_HintText_messageid) } } catch (e) { const errorTime = new Date().toISOString() // 获取错误发生时间的ISO格式 ctx.logger.error(`状态渲染失败 [${errorTime}]: `, e) // 记录错误信息并包含时间戳 if (config.retryexecute && retryCounts[session.userId] < config.maxretrytimes) { retryCounts[session.userId]++ ctx.logger.warn(`用户 ${session.userId} 尝试第 ${retryCounts[session.userId]} 次重试...`) try { await session.execute(config.command) // 使用 session.execute 重试 delete retryCounts[session.userId] // 执行成功,删除重试次数 return // 阻止发送错误消息,因为我们正在重试 } catch (retryError) { ctx.logger.error(`重试失败 [${errorTime}]: `, retryError) // 重试失败,继续执行错误处理 } } // 如果达到最大重试次数或未启用重试,则发送错误消息 delete retryCounts[session.userId] // 清理重试次数 return "渲染失败 " + e.message + '\n' + e.stack } finally { if (page && !page.isClosed()) { page.close() } // 仅在成功或达到最大重试后清理 if (!config.retryexecute || retryCounts[session.userId] >= config.maxretrytimes) { delete retryCounts[session.userId] } } }) function logInfo(...args: any[]) { if (config.consoleinfo) { (ctx.logger.info as (...args: any[]) => void)(...args) } } // 读取 TTF 字体文件并转换为 Base64 编码 function getFontBase64(fontPath: fs.PathOrFileDescriptor) { const fontBuffer = fs.readFileSync(fontPath) return fontBuffer.toString('base64') } // 删除记录的函数 async function deleteImageRecord(messageId, imageURL) { try { const data = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8')) const index = data.findIndex(record => record.messageId.includes(messageId) && record.backgroundURL === imageURL) if (index !== -1) { data.splice(index, 1) fs.writeFileSync(jsonFilePath, JSON.stringify(data, null, 2), 'utf-8') logInfo(`已删除消息ID ${messageId} 的记录`) } } catch (error) { ctx.logger.error("删除记录时出错: ", error) } } // 提取消息发送逻辑为函数 async function sendmarkdownMessage(session, message) { logInfo(message) try { const { guild, user } = session.event const { qq, qqguild, channelId } = session if (guild?.id) { if (qq) { await qq.sendMessage(channelId, message) } else if (qqguild) { await qqguild.sendMessage(channelId, message) } } else if (user?.id && qq) { await qq.sendPrivateMessage(user.id, message) } } catch (error) { ctx.logger.error(`发送markdown消息时出错:`, error) } } async function uploadImageToChannel(imageBuffer, appId, secret, token, channelId) { async function refreshToken(bot) { const { access_token: accessToken, expires_in: expiresIn } = await ctx.http.post('https://bots.qq.com/app/getAppAccessToken', { appId: bot.appId, clientSecret: bot.secret }) bot.token = accessToken ctx.setTimeout(() => refreshToken(bot), (expiresIn - 30) * 1000) } // 临时的bot对象 const bot = { appId, secret, token, channelId } // 刷新令牌 await refreshToken(bot) const payload = new FormData() payload.append('msg_id', '0') payload.append('file_image', new Blob([imageBuffer], { type: 'image/png' }), 'image.jpg') await ctx.http.post(`https://api.sgroup.qq.com/channels/${bot.channelId}/messages`, payload, { headers: { Authorization: `QQBot ${bot.token}`, 'X-Union-Appid': bot.appId } }) // 计算MD5并返回图片URL const md5 = crypto.createHash('md5').update(imageBuffer).digest('hex').toUpperCase() if (channelId !== undefined && config.consoleinfo) { ctx.logger.info(`使用本地图片*QQ频道 发送URL为: https://gchat.qpic.cn/qmeetpic/0/0-0-${md5}/0`) } return { url: `https://gchat.qpic.cn/qmeetpic/0/0-0-${md5}/0` } } async function markdown(session, encodedMessageTime, imageUrl, imageToload) { const markdownMessage: any = { msg_type: 2, markdown: {}, keyboard: {}, } // 只有在非主动模式下才添加 msg_id if (!config.markdown_button_mode_initiative) { markdownMessage.msg_id = session.messageId } let originalWidth let originalHeight // 尝试从 URL 中解析尺寸 const sizeMatch = imageUrl.match(/\?px=(\d+)x(\d+)$/) if (sizeMatch) { originalWidth = parseInt(sizeMatch[1], 10) originalHeight = parseInt(sizeMatch[2], 10) } else { const canvasimage = await ctx.canvas.loadImage(imageToload || imageUrl) // @ts-ignore originalWidth = canvasimage.naturalWidth || canvasimage.width // @ts-ignore originalHeight = canvasimage.naturalHeight || canvasimage.height } // 获取 dJson const dJson = await getJrys(session) if (config.markdown_button_mode === "markdown") { const templateId = config.nested.markdown_button_template_id const keyboardId = config.nested.markdown_button_keyboard_id const contentTable = config.nested.markdown_button_content_table const params = contentTable.map(item => ({ key: item.raw_parameters, values: replacePlaceholders(item.replace_parameters, { session, config, img_pxpx: `img#${originalWidth}px #${originalHeight}px`, img_url: imageUrl, encodedMessageTime, dJson }), })) markdownMessage.markdown = { custom_template_id: templateId, params: params, } if (config.markdown_button_mode_keyboard) { markdownMessage.keyboard = { id: keyboardId, } } } else if (config.markdown_button_mode === "markdown_raw_json") { const templateId = config.nested.markdown_raw_json_button_template_id const contentTable = config.nested.markdown_raw_json_button_content_table let keyboard = JSON.parse(config.nested.markdown_raw_json_button_keyboard) keyboard = replacePlaceholders(keyboard, { session, config, img_pxpx: `img#${originalWidth}px #${originalHeight}px`, img_url: imageUrl, encodedMessageTime, dJson }, true) const params = contentTable.map(item => ({ key: item.raw_parameters, values: replacePlacehold