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
text/typescript
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")
.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\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