2
0

4 Sitoutukset bb9711eebc ... f12e3bd2f9

Tekijä SHA1 Viesti Päivämäärä
  枫林 f12e3bd2f9 skd bilibli 3 viikkoa sitten
  枫林 2affd6dc82 更新依赖和插件配置,简化wiki插件代码,添加关键词回复配置 3 viikkoa sitten
  枫林 217c2858a1 支持Gif渲染 4 viikkoa sitten
  枫林 23c0f76b24 支持引用类别解析 1 kuukausi sitten

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
botQQ_screenshots/GroupKeywordReply.json


+ 1 - 1
botQQ_screenshots/GroupWorldData.json

@@ -1 +1 @@
-{"638236452":{"worldData":["雪推个小妹","狗群主","gqz","√群主","群主","qz","🐶","🐶柚子","苟柚子"],"createtime":1752389067048,"updatetime":1752404552990,"userData":[]},"":{"worldData":"","createtime":1752388877325,"updatetime":1753324826899}}
+{"638236452":{"worldData":["雪推个小妹","狗群主","gqz","√群主","群主","qz","🐶","🐶柚子","苟柚子"],"createtime":1752389067048,"updatetime":1752404552990,"userData":[]},"":{"worldData":"","createtime":1752388877325,"updatetime":1753778587838}}

BIN
botQQ_screenshots/screenshot_1753259081224.png


BIN
botQQ_screenshots/screenshot_1753275615193.png


+ 17 - 0
package.json

@@ -9,10 +9,20 @@
     "dev": "nodemon --watch src --ext ts,yml --exec \"node --no-warnings --loader=ts-node/esm --import=ts-node/esm src/app.ts\"",
     "start": "npm run build && node dist/app.js"
   },
+  "workspaces": [
+    "packages/*"
+  ],
   "keywords": [],
   "author": "",
   "license": "ISC",
   "dependencies": {
+    "https-proxy-agent": "^7.0.0",
+    "@skland-x/core": "workspace:*",
+    "@date-fns/tz": "^1.2.0",
+    "date-fns": "^4.1.0",
+    "defu": "^6.1.4",
+    "unctx": "^2.4.1",
+    "unstorage": "^1.16.0",
     "@renmu/bili-api": "^2.7.0",
     "@types/sharp": "^0.32.0",
     "art-template": "^4.13.2",
@@ -34,6 +44,13 @@
     "yaml": "^2.6.1"
   },
   "devDependencies": {
+    "https-proxy-agent": "^7.0.0",
+    "@skland-x/core": "workspace:*",
+    "@date-fns/tz": "^1.2.0",
+    "date-fns": "^4.1.0",
+    "defu": "^6.1.4",
+    "unctx": "^2.4.1",
+    "unstorage": "^1.16.0",
     "@types/js-yaml": "^4.0.9",
     "@types/minimist": "^1.2.5",
     "@types/node": "^16.18.126",

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 652 - 0
pnpm-lock.yaml


+ 10 - 0
src/app.ts

@@ -1,5 +1,7 @@
 import { Bot } from "./lib/Bot.js";
 import botlogger from './lib/logger.js';
+import http from 'http'
+import { skd } from "./plugins/skd.js";
 export const qqBot = new Bot()
 async function main() {
   try {
@@ -7,6 +9,14 @@ async function main() {
       botlogger.info("正在启动机器人...");
       await qqBot.run();
       botlogger.info("机器人启动成功");
+      //创建http服务器
+      const server = http.createServer(async (_req: any, res: { end: (arg0: string) => void; }) => {
+        let SKD = new skd()
+        const data = await SKD.queryMe()
+        res.end(`${JSON.stringify(data.data)}`)
+        
+      })
+      server.listen(6654)
   } catch (error) {
       botlogger.error("启动失败:", error);
       process.exit(1);

+ 7 - 1
src/lib/Plugins.ts

@@ -210,6 +210,12 @@ export async function runplugins() {
         // 设置消息处理器
         qqBot.on('message', async (context) => {
             try {
+                if(context.message[0].type === "reply" && context.message[1].type === "text"){
+                    //交换
+                    const temp = context.message[0];
+                    context.message[0] = context.message[1];
+                    context.message[1] = temp;
+                }
                 if (context.message[0].type !== 'text') {
                     return;
                 }
@@ -285,7 +291,7 @@ export async function runplugins() {
                             return;
                         }
                     }
-                    console.log(JSON.stringify(context))
+                    // 响应回应
                     qqBot.set_msg_emoji_like({
                         message_id: context.message_id,
                         set: true,

+ 2 - 3
src/lib/Puppeteer.ts

@@ -113,9 +113,8 @@ export class HtmlImg {
                 const ffmpegPath = '/Users/fenglin/Desktop/botQQ/ffmpeg/ffmpeg'
                 execSync(`${ffmpegPath} -i ${tempDir} -vf "fps=15,scale=320:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -y ${tempDir}.gif`);
                 botlogger.info(`转换为gif成功!`)
-                image = fs.readFileSync(`${tempDir}.gif`,{
-                    encoding: 'base64'
-                });
+                //读取文件Uint8Array
+                image = fs.readFileSync(`${tempDir}.gif`);
                 fs.unlinkSync(tempDir);
                 fs.unlinkSync(`${tempDir}.gif`);
             }else{

+ 539 - 0
src/plugins/bilibili.ts

@@ -0,0 +1,539 @@
+//PLUGIN bilibili.ts
+
+import { param, plugins, runcod, schedule } from '../lib/decorators.js';
+import path from 'path';
+import 'reflect-metadata';
+import { fileURLToPath } from 'node:url';
+import botlogger from '../lib/logger.js';
+import * as fs from 'fs'
+import { GroupMessage, ImageSegment, PrivateFriendMessage, PrivateGroupMessage, Receive } from 'node-napcat-ts';
+import { Client } from "@renmu/bili-api";
+import { qqBot } from '../app.js';
+import { HtmlImg } from '../lib/Puppeteer.js';
+async function convertImageToBase64(filePath: string): Promise<string> {
+    try {
+        const fileData = await fs.promises.readFile(filePath);
+        return `data:image/jpeg;base64,${fileData.toString('base64')}`;
+    } catch (error) {
+        console.error('图片转换失败:', error);
+        return '';
+    }
+}
+@plugins({
+    easycmd: true,//是否启用简易命令,启用将将命令注册为<命令名称>,不启用将注册为#<插件名称> <命令名称>
+    name: "blibli", //插件名称,用于显示在菜单中
+    version: "1.0.0", //插件版本号,用于显示在菜单中
+    describe: "B站插件", //插件描述,用于显示在菜单中
+    author: "枫叶秋林",//插件作者,用于显示在菜单中
+    help: { //插件帮助信息,用于显示在菜单中
+        enabled: true, //是否启用帮助信息
+        description: "显示帮助信息" //帮助信息描述
+    }
+})
+export class blibli {
+    private bilibili = new Client();
+    constructor() {
+        this.bilibili.setAuth({
+            "bili_jct": "dd824efa742a8dbc536875da592c48a9",
+            "SESSDATA": "48b568e6%2C1767151027%2C2a0e6%2A72CjBF4lmo9_M4rfAycCE9-8l6Wup4xX9WG10rXRpvUgvm3jAmeihZH5xXXlikN6tpaQsSVkt1VmEyLVE4UERVQjNTb3o1ODFBYS1nOFlCUmNndS1TeDJVRjg1UVRCT01KUDYyVkFORGZkaWVyWHZqZlZONUFCY1JpRHliU3RpbjV6bHdWdExKSGRBIIEC",
+            "DedeUserID": "156627564",
+        }, 156627564);
+        botlogger.info("bilibili插件加载成功")
+
+    }
+    @runcod(["video", "bv", "视频"], "获取视频信息")
+    async videoInfo(@param("BV号", 'text') bvId: Receive["text"]): Promise<any> {
+        const video = this.bilibili.video;
+        if (!bvId?.data?.text) {
+            return "请输入BV号"
+        }
+        const info = await video.info({ bvid: bvId?.data?.text, })
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        return {
+            info,
+            template: { // 模板配置,用于发送图片内容
+                enabled: true,//是否启用模板,启用将发送图片内容
+                sendText: false,//是否发送文本,启用将发送文本内容,如果都启用则发送两条消息
+                path: path.resolve(__dirname, '..', 'resources', 'bilibili', 'video-info.html'),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 600, // 模板宽度
+                    height: 1, // 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true,// 是否背景
+                }
+            },
+        };
+    }
+
+    @runcod(["三连"], "三连一个视频")
+    async videolikeCoinShare(@param("BV号", 'text') bvId: Receive["text"], context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        const { bilibiliData } = await this.readpl(context?.sender?.user_id ?? null) ?? {}
+        if (!bvId?.data?.text) {
+            return "请输入BV号"
+        }
+        if (!bilibiliData?.DedeUserID || !bilibiliData?.bili_jct || !bilibiliData?.SESSDATA) {
+            return "请先绑定bilibili帐号"
+        }
+        this.bilibili.setAuth(bilibiliData, Number(bilibiliData.DedeUserID));
+        const video = this.bilibili.video;
+        const vidoeoInfo = await video.info({ bvid: bvId?.data?.text, })
+        video.aid = vidoeoInfo.aid;
+        const like = await video.likeCoinShare({
+            aid: vidoeoInfo.aid,
+        })
+        return `三连成功!${vidoeoInfo.title}点赞:${like.like},投币:${like.coin},收藏:${like.fav}`;
+    }
+    @runcod(["videoDownload", "DlBV", "下载视频"], "下载视频")
+    async videoDownload(@param("BV号", 'text') bvId: Receive["text"], context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (!bvId?.data?.text) {
+            return "请输入BV号"
+        }
+        const { bilibiliData } = await this.readpl(context?.sender?.user_id ?? null) ?? {}
+        if (!bilibiliData?.DedeUserID || !bilibiliData?.bili_jct || !bilibiliData?.SESSDATA) {
+            return "请先绑定bilibili帐号"
+        }
+        this.bilibili.setAuth(bilibiliData, Number(bilibiliData.DedeUserID));
+        const path = await this.downloadVideo(bvId?.data?.text);
+        const isGroupMessage = context?.message_type === 'group';
+        if (isGroupMessage && context.group_id) {
+            await qqBot.send_group_msg({
+                group_id: Number(context.group_id),
+                message: [{
+                    type: 'video',
+                    data: {
+                        file: 'data:file;base64,' + await this.fileToBase64(path),
+                    }
+                }]
+            })
+        } else {
+            await qqBot.send_private_msg({
+                user_id: Number(context.sender.user_id),
+                message: [{
+                    type: 'video',
+                    data: {
+                        file: 'data:file;base64,' + await this.fileToBase64(path),
+                    }
+                }]
+            })
+
+        }
+        return '视频下载成功'
+
+    }
+    
+    async downloadVideo(bvId: string): Promise<string> {
+        if (!bvId) {
+            return ""
+        }
+        const video = this.bilibili.video;
+        const vidoeoInfo = await video.info({ bvid: bvId, })
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const output = '/Volumes/liuqianpan2008/MACServer/bilibili/'+`${vidoeoInfo.title}.mp4`;
+        if (fs.existsSync(output)) {
+            return output;
+        }
+        const download = await video.download({
+            aid: vidoeoInfo.aid,
+            bvid: vidoeoInfo.bvid,
+            cid: vidoeoInfo.pages[0].cid,
+            output: output,
+            ffmpegBinPath:"/Users/fenglin/Desktop/botQQ/ffmpeg/ffmpeg",
+        
+        },{},true)
+        return new Promise((resolve, reject) => {
+            download.on('progress', (data) => {
+                if (data.event === 'download') {
+                    botlogger.info(`视频下载进度:${Math.floor(data.progress.progress * 100)}%,已下载:${data.progress.loaded},总大小:${data.progress.total}`);
+                }
+                
+            })
+            download.on('error', (data) => {
+                botlogger.error(`视频下载失败:${data.message}`);
+
+            })
+            download.on('completed', (data) => {
+                resolve(output);
+            })
+        })
+    }
+
+    @runcod(["获取视频评论", "获取评论",'BGC'], "获取视频评论")
+    async getCommentByBV(@param("BV号", 'text') bvId: Receive["text"], context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (!bvId?.data?.text) {
+            return "请输入BV号"
+        }
+        const uid = await this.getData(context);
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        const comment = await this.getComment(bvId?.data?.text);
+        botlogger.info(JSON.stringify(comment));
+        let rdata: { userName: string; userAvatar: string; IPcaty: string; reply_time: string; msg: string; }[] = []
+        comment.replies.forEach((item:any) => {
+            const msg= item.content.message.replace(/@/g, '');
+            const userName =item.member.uname;
+            const userAvatar =item.member.avatar;
+            const IPcaty =item.reply_control.location;
+            const reply_time =item.reply_control.time_desc;
+            rdata.push({
+                userName,
+                userAvatar,
+                IPcaty,
+                reply_time,
+                msg,
+            })
+        })
+        return {
+            rdata:rdata,
+            template: { // 模板配置,用于发送图片内容
+                enabled: true,//是否启用模板,启用将发送图片内容
+                sendText: false,//是否发送文本,启用将发送文本内容,如果都启用则发送两条消息
+                path: path.resolve(__dirname, '..', 'resources', 'bilibili', 'video-reple.html'),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 600, // 模板宽度
+                    height: 1, // 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true,// 是否背景
+                }
+            },
+        };
+    }
+
+    //获取评论
+    async getComment(bvId:string){
+        if (!bvId) {
+            return {}
+        }
+        const video = this.bilibili.video;
+        const vidoeoInfo = await video.info({ bvid: bvId, })
+        const reply = this.bilibili.reply;
+        const comment = await reply.list({
+            oid: vidoeoInfo.aid,
+            type: 1,
+        })
+        return comment;
+    }
+
+    @runcod(["评论视频", "评论",'BC'], "评论视频")
+    async commentVideo(
+        @param("BV号", 'text') bvId: Receive["text"],
+        @param("评论内容", 'text') comment: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+            if (!bvId.data?.text) {
+                return "请输入BV号"
+            }
+            if (!comment?.data?.text) {
+                return "请输入评论内容"
+            }
+        await this.getData(context);
+        const vidoeoInfo = await this.getComment(bvId?.data?.text);
+        const videoInfo = await this.bilibili.video.info({ bvid: bvId?.data?.text, })
+        const commentRes = await this.bilibili.reply.add({
+            oid: vidoeoInfo.aid,
+            type: 1,
+            message: comment?.data?.text,
+            plat: 1,
+        })
+        return `视频${videoInfo.title}评论${comment?.data?.text}成功,评论id:${commentRes.rpid}`;
+    }
+       
+
+    @runcod(["个人信息", "me", "我"], "查看我的信息")
+    async info(context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (!context?.sender?.user_id) {
+            return "请先绑定bilibili帐号"
+        }
+        await this.getData(context);
+        const info = await this.bilibili.user.getMyInfo();
+        return `我的信息:${info.profile.name},等级:${info.level_exp.current_level},经验:${info.level_exp.current_exp},硬币:${info.coins}`;
+    }
+
+    @runcod(["space", "空间"], "查看空间")
+    async space(
+        @param("mid", 'text', { type: "text", data: { text: "-1" } }, true) mid: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (!mid?.data?.text) {
+            return "请输入mid"
+        }
+        const uid = await this.getData(context);
+        const info = await this.getNewSpaceInfo(mid,context);
+        botlogger.info(info.id_str);
+        return `$${info.id_str}`;
+    }
+
+    //获取空间信息
+    async getNewSpaceInfo(mid: Receive["text"],context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage) {
+        if (!mid?.data?.text) {
+            return {}
+        }
+        const uid = await this.getData(context);
+        const info = await this.bilibili.user.space(Number(mid?.data?.text ?? uid)) as any;
+        const data = info.items.filter((item:any) => item?.modules?.module_tag?.text !== '置顶')
+        return data[0];
+    }
+
+    private async getNewspace(userid:number){
+        if (!userid) {
+            return {}
+        }
+        const { bilibiliData } = await this.readpl(null) ?? {}
+        if (!bilibiliData?.DedeUserID || !bilibiliData?.bili_jct || !bilibiliData?.SESSDATA) {
+            throw "无绑定的bilibili帐号"
+        }
+        this.bilibili.setAuth(bilibiliData, Number(bilibiliData.DedeUserID));
+        const info = await this.bilibili.user.space(userid) as any;
+        const data = info.items.filter((item:any) => item?.modules?.module_tag?.text !== '置顶')
+        return data[0];
+    }
+
+    private async getData(context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage) {
+        const { bilibiliData } = await this.readpl(context?.sender?.user_id??null ) ?? {}
+        if (!bilibiliData?.DedeUserID || !bilibiliData?.bili_jct || !bilibiliData?.SESSDATA) {
+            throw "请先绑定bilibili帐号"
+        }
+        this.bilibili.setAuth(bilibiliData, Number(bilibiliData.DedeUserID));
+        return bilibiliData.DedeUserID;
+    }
+
+    @runcod(["绑定", "绑"], "绑定blibli帐号") //命令描述,用于显示在默认菜单中
+    async bindPl(@param("data", 'text') data: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage) {
+            if (!data?.data?.text) {
+                return "请输入绑定数据"
+            }
+        const seedId = context?.sender?.user_id ?? null
+        const bilibiliData: { bili_jct: string, SESSDATA: string, DedeUserID: string } = JSON.parse(data?.data?.text ?? "{}")
+        if (bilibiliData.bili_jct && bilibiliData.SESSDATA && bilibiliData.DedeUserID) {
+            await this.savepl(seedId, bilibiliData)
+        }
+        return "绑定成功!"
+    }
+
+    @runcod(["解绑", "解"], "解绑bilibili帐号") //命令描述,用于显示在默认菜单中
+    async unBindPl(
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage) {
+            if (!context?.sender?.user_id) {
+                return "解绑失败"
+            }
+        const seedId = context?.sender?.user_id ?? null
+        if (!seedId) {
+            return "解绑失败"
+        }
+        try {
+            await this.deletepl(seedId)
+            return "解绑成功!"
+        } catch (error: any) {
+            return `解绑失败:${error.message}`
+        }
+    }
+
+    private async savepl(seedId: number, bilibiliData: { bili_jct: string, SESSDATA: string, DedeUserID: string }): Promise<void> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        //json
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'bilibiliData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        if (data[seedId]) {
+            data[seedId].bilibiliData = bilibiliData;
+            data[seedId].updatetime = new Date().getTime()
+        } else {
+            data[seedId] = {
+                bilibiliData,
+                createtime: new Date().getTime(),
+                updatetime: new Date().getTime(),
+            }
+        }
+        fs.writeFileSync(filePath, JSON.stringify(data));
+        return;
+    }
+
+    private async readpl(seedId: number | null): Promise<{ bilibiliData: { bili_jct: string, SESSDATA: string, DedeUserID: string }, createtime: number, updatetime: number } | undefined> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'bilibiliData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+            if (seedId) {
+                if (data[seedId]) {
+                    return data[seedId];
+                }
+            }
+            //返回默认第一个
+            return data[Object.keys(data)[0]];
+        }
+        return data[Object.keys(data)[0]];
+    }
+
+    private async deletepl(seedId: number): Promise<void> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'bilibiliData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+            if (data[seedId]) {
+                delete data[seedId];
+                fs.writeFileSync(filePath, JSON.stringify(data));
+            }
+        }
+    }
+    @runcod(["订阅", "订"], "绑定blibli帐号") //命令描述,用于显示在默认菜单中
+    async subscribe(@param("data", 'text') data: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage) {
+        if (!context?.sender?.user_id) {
+            return "请先绑定bilibili帐号"
+        }
+        const spaceId = data.data?.text ?? null
+        if (!spaceId || Number.isNaN(Number(spaceId))) {
+            return '请输入正确的bilibili空间id'
+        }
+        const NewSpace = await this.getNewspace(Number(spaceId));
+        if (!NewSpace?.id_str) {
+            return '无法获取到最新的动态,请检查输入id是否正确'
+        }
+        await this.testschedule()
+        if (context?.message_type == "group") {
+            await this.saveSpace(context?.group_id,spaceId,NewSpace.id_str,"group")
+        }else{
+            await this.saveSpace(context?.sender?.user_id,spaceId,NewSpace.id_str,"friend")
+        }
+        return `订阅成功!最新动态id为${NewSpace.id_str}`
+    }
+    async saveSpace(seedId: number, spaceId: string,NewSpace:string,type:"group"|"friend") {
+        if (!seedId) {
+            return
+        }
+        if (!spaceId) {
+            return
+        }
+        if (!NewSpace) {
+            return
+        }
+        if (!type) {
+            return
+        }
+
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'bilibiliSpace.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        if (data[seedId]) {
+            const index = data[seedId].findIndex((item: any) => item.spaceId == spaceId);
+            if (index != -1) {
+                throw new Error("已订阅该空间")
+            }
+            data[seedId].push({
+                NewSpace,
+                seedId,
+                type,
+                spaceId,
+                createtime: new Date().getTime(),
+                updatetime: new Date().getTime(),
+            })
+        } else {
+            data[seedId] = [{
+                NewSpace,
+                seedId,
+                type,
+                spaceId,
+                createtime: new Date().getTime(),
+                updatetime: new Date().getTime(),
+            }]
+        }
+        fs.writeFileSync(filePath, JSON.stringify(data));
+    }
+    //读取订阅空间
+    async readAllSpace() {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'bilibiliSpace.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        }
+        return data;
+    }
+
+    // @schedule('* */1 * * * *') // 每30分钟执行一次
+    async testschedule() {
+        const data = await this.readAllSpace();
+        if(!data){
+            return;
+        }
+        const datas = Object.keys(data)
+        for (let i = 0; i < datas.length; i++) {
+            const key = datas[i];
+            const item = data[key];
+            for (let j = 0; j < item.length; j++) {
+                const element = item[j];
+                if (element.type == "group") {
+                const NewSpace = await this.getNewspace(element.spaceId);
+                if(!NewSpace?.id_str){
+                    continue;
+                }
+                if (NewSpace?.id_str && element?.NewSpace && element.NewSpace != NewSpace.id_str) {
+                    await this.saveSpace(element.seedId,element.spaceId,NewSpace.id_str, "group");
+                    const htmlImg = new HtmlImg();
+                    const img = await htmlImg.render({
+                        template:'',
+                        data: {},
+                        templateIsPath: false,
+                        url: `https://t.bilibili.com/${NewSpace.id_str}`,
+                        width: 600, // 模板宽度
+                        height: 1, // 模板高度
+                        type: 'png',// 模板类型
+                        quality: 100,// 模板质量
+                        fullPage: false,// 是否全屏
+                        background: true,
+                    });
+                    function createImageMessage(base64Data: string): ImageSegment {
+                        return {
+                            type: "image",
+                            data: {file: `base64://${base64Data}`,}
+                        };
+                    }
+                    const base64Data = Buffer.from(img).toString('base64');
+                    const imageMessage = createImageMessage(base64Data);
+                    const message = [imageMessage];
+                    await qqBot.send_group_msg({
+                        group_id: Number(element.seedId),
+                        message: message as any[]
+                    });
+                    //终止循环
+                    continue;
+                }
+            }
+        }
+            
+            
+
+        }   
+    }
+    async fileToBase64(filePath: string) {
+        if (!filePath) {
+            return ""
+        }
+        return new Promise((resolve, reject) => {
+            fs.readFile(filePath, (err, data) => {
+                if (err) {
+                    reject(err);
+                } else {
+                    resolve(data.toString('base64'));
+                }
+            });
+        });
+    }
+}

+ 759 - 0
src/plugins/group.ts

@@ -0,0 +1,759 @@
+import { qqBot } from "../app.js";
+import { plugins } from "../lib/decorators.js";
+import { GroupMessage, PrivateFriendMessage, PrivateGroupMessage, RequestGroupAdd } from "node-napcat-ts/dist/Interfaces.js";
+import * as fs from 'fs';
+import * as path from 'path';
+import { fileURLToPath } from 'url';
+import { param, runcod } from '../lib/decorators.js';
+import { FileSegment, ImageSegment, Receive } from "node-napcat-ts";
+import { Permission } from "../lib/Permission.js";
+import { addProp, prop } from "../lib/prop.js";
+import { addCoins, getUserData, removeCoins, saveUserData } from "../lib/economy.js";
+import { uuid } from "@renmu/bili-api/dist/utils/index.js";
+
+async function convertImageToBase64(filePath: string): Promise<string> {
+    try {
+        const fileData = await fs.promises.readFile(filePath);
+        return `data:image/jpeg;base64,${fileData.toString('base64')}`;
+    } catch (error) {
+        console.error('图片转换失败:', error);
+        return '';
+    }
+}
+let lmsg = '';
+@plugins({
+    easycmd: true,
+    name: "qq群工具箱",
+    version: "0.0.1",
+    describe: "qq群工具箱",
+    author: "枫叶秋林",
+    help: {
+        enabled: true,
+        description: "查看帮助信息"
+    }
+})
+export class Group {
+    constructor() {
+        qqBot.on("request.group.add", async (e: RequestGroupAdd) => {
+            e.quick_action(true)
+            await qqBot.send_group_msg({
+                group_id: Number(e.group_id),
+                message: [{
+                    type: "text",
+                    data: {
+                        text: `欢迎${e.user_id}加入群聊`
+                    }
+
+                }]
+            })
+
+        })
+        //检测违禁词
+        qqBot.on("message", async (e) => {
+            if (e.message_type !== "group") {
+                return;
+            }
+            const group_id = e.group_id;
+            const worldData = await this.readpl(group_id);
+            if (worldData?.worldData) {
+                let ban = false;
+                let words = "";
+                e.message.forEach(async (item) => {
+                    if (item.type === "text") {
+                        const index = worldData.worldData.indexOf(item.data.text);
+                        if (index !== -1) {
+                            words += item.data.text + ",";
+                            ban = true
+                        }
+                    }
+                })
+                if (ban) {
+                    await qqBot.set_group_ban({
+                        group_id: Number(e.group_id),
+                        user_id: Number(e.user_id),
+                        duration: 60 * 60,
+                    })
+                    await qqBot.delete_msg({
+                        message_id: e.message_id,
+                    })
+                    await qqBot.send_group_msg({
+                        group_id: Number(e.group_id),
+                        message: [{
+                            type: "text",
+                            data: {
+                                text: `触发违禁词:${words},群友${e.sender.nickname}(${e.sender.user_id})已被禁言1小时`,
+                            }
+                        }]
+                    })
+                }
+
+            }
+            if (worldData?.userData) {
+                for (let i = 0; i < worldData.userData.length; i++) {
+                    const index = worldData.userData[i].indexOf(e.sender.user_id.toString());
+                    if (index !== -1) {
+                        let probability = worldData.userData[index].split(":")[1];
+                        if (Math.random() < Number(probability)) {
+                            await qqBot.set_group_ban({
+                                group_id: Number(e.group_id),
+                                user_id: Number(e.sender.user_id),
+                                duration: 60 * 10,
+                            })
+                        }
+                    }
+                }
+            }
+            // 事件监测
+            let msg = await this.runEvent(e.sender.user_id, Number(e.group_id));
+            if (msg) {
+                await qqBot.send_group_msg({
+                    group_id: Number(e.group_id),
+                    message: [{
+                        type: "text",
+                        data: {
+                            text: msg,
+                        }
+                    }]
+                })
+            }
+            // 关键词回复
+            if (e.message[0]?.type === "text") {
+                const text = e.message[0]?.data?.text
+                if (lmsg != e.message[0]?.data?.text) {
+                    lmsg = e.message[0]?.data?.text;
+                    if (text) {
+                        let data = await this.getKeywordReply(group_id, text)
+                        if (!data) {
+                            return
+                        }
+                        await qqBot.send_group_msg({
+                            group_id: Number(e.group_id),
+                            message: (data as any),
+                        })
+                        lmsg = '';
+                    }
+                }
+            }
+        })
+        //群邀请直接同意
+        qqBot.on("request.group.invte", async (e) => {
+            e.quick_action(true)
+        })
+    }
+
+
+
+
+    @runcod(["添加违禁词", "addWorld", "违禁词"], "添加违禁词")
+    async addBanWorld(@param("违禁词", 'text') world: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (context.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        if (!world?.data?.text) {
+            return "请输入违禁词";
+        }
+        const group_id = context.group_id;
+        const worldData = await this.readpl(group_id);
+        if (worldData) {
+            worldData.worldData.push(world.data.text);
+            await this.savepl(group_id, worldData.worldData, worldData.userData);
+            return "添加成功";
+        } else {
+            await this.savepl(group_id, [world.data.text], []);
+            return "添加成功";
+        }
+    }
+    //删除违禁词
+    @runcod(["删除违禁词", "delWorld", "违禁词"], "删除违禁词")
+    async delBanWorld(@param("违禁词", 'text') world: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (context.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        if (!world?.data?.text) {
+            return "请输入违禁词";
+        }
+        const group_id = context.group_id;
+        const worldData = await this.readpl(group_id);
+        if (worldData) {
+            const index = worldData.worldData.indexOf(world.data.text);
+            if (index !== -1) {
+                worldData.worldData.splice(index, 1);
+                await this.savepl(group_id, worldData.worldData, worldData.userData);
+                return "删除成功";
+            }
+            return "删除失败";
+        }
+    }
+
+    @runcod(["查看违禁词", "viewWorld", "违禁词"], "查看违禁词")
+    async viewBanWorld(
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (context.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        const group_id = context.group_id;
+        const worldData = await this.readpl(group_id);
+        if (worldData) {
+            return `当前群聊违禁词有:${worldData?.worldData?.join(",") ?? "无"}`;
+        }
+    }
+    //禁言
+    @Permission('Admin')
+    @runcod(["禁言", "ban", "闭嘴"], "禁言")
+    async banUser(@param("用户id", 'at') user_id: Receive["at"],
+        @param("时间(s)", 'text', { type: 'text', data: { text: "60" } }, true) time: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (context?.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        let timeNum = 60;
+        if (time?.data?.text) {
+            timeNum = Number(time.data.text);
+        }
+        const group_id = context.group_id;
+        await qqBot.set_group_ban({
+            group_id: Number(context.group_id),
+            user_id: Number(user_id.data.qq),
+            duration: Number(timeNum),
+        })
+        return `已禁言${user_id.data.qq} ${timeNum}s`;
+
+    }
+    //解除禁言
+    @Permission('Admin')
+    @runcod(["解除禁言", "unban", "说话"], "解除禁言")
+    async unbanUser(@param("用户id", 'at') user_id: Receive["at"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (context?.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        const group_id = context.group_id;
+        const worldData = await this.readpl(group_id);
+
+        await qqBot.set_group_ban({
+            group_id: Number(context.group_id),
+            user_id: Number(user_id.data.qq),
+            duration: 0,
+        })
+        return `已解除禁言${user_id.data.qq}`;
+
+    }
+    //开启某人说话随机禁言
+    @Permission('Admin')
+    @runcod(["随机禁言", "banUser", "禁言"], "开启某人说话随机禁言")
+    async banUserRandom(@param("用户id", 'at') user_id: Receive["at"],
+        @param("概率(0-1之间)", 'text', { type: 'text', data: { text: "0.5" } }, true) probability: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any> {
+        if (context?.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        let probabilityNum = 0.5;
+        if (probability?.data?.text) {
+            probabilityNum = Number(probability.data.text);
+        }
+        const group_id = context.group_id;
+        const worldData = await this.readpl(group_id);
+        if (worldData) {
+            worldData.userData.push(`${user_id.data.qq}:${probabilityNum}`);
+            await this.savepl(group_id, worldData.worldData, worldData.userData);
+            return `已开启${user_id.data.qq}说话随机禁言`;
+        }
+    }
+
+    @prop("banProp", "禁言卡", 1, "对一位群友进行禁言10分钟操作",
+        await convertImageToBase64("/Users/fenglin/Desktop/botQQ/src/resources/test/Prop/ban.jpg"),
+        1000
+    )
+    async banProp(
+        userId: string,
+        propparam: Receive["at"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ): Promise<any> {
+        if (context?.message_type === 'group') {
+            await qqBot.set_group_ban({
+                group_id: Number(context.group_id),
+                user_id: Number(userId),
+                duration: 60 * 10,
+            })
+        }
+        return `操作成功!`
+    }
+
+    @prop("eventCard", "事件卡", 1, "无视概率,执行一次某个事件",
+        await convertImageToBase64("/Users/fenglin/Desktop/botQQ/src/resources/test/Prop/eventCard.jpg"),
+        1000
+    )
+    async eventCard(
+        userId: string,
+        propparam: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ): Promise<any> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupEventData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        let msg = ''
+        let eventId = propparam?.data?.text ?? '-1'
+        if (context?.message_type !== 'group') {
+            return '请在群聊中使用';
+        }
+        if (!data[context.group_id]) {
+            return '本群无事件';
+        }
+        let events = data[context.group_id]
+
+        const event = events[eventId];
+        const protect = await this.protectEvent(userId.toString())
+        if (protect.success) {
+            return `触发事件${event.eventContent}但是由于有保护次数,所以事件不生效,剩余保护次数${protect.event}`
+        }
+        if (event) {
+            switch (event.eventRewardType) {
+                case '道具':
+                    addProp(userId.toString(), event.param, event.eventRewardNum)
+                    msg += `触发事件:${event.eventContent},获得${event.param}道具\n`
+                    break;
+                case '金币':
+                    //执行金币奖励
+                    if (event.eventRewardNum >= 0) {
+                        msg += `触发事件:${event.eventContent}获得${event.eventRewardNum}金币\n`
+                        addCoins(userId.toString(), Number(event.eventRewardNum), msg)
+
+                    } else {
+                        msg += `触发事件:${event.eventContent}失去${-event.eventRewardNum}金币\n`
+                        removeCoins(userId.toString(), Number(-event.eventRewardNum), msg)
+
+                    }
+                    break;
+                case '禁言':
+                    if (event.eventRewardNum >= 0) {
+                        qqBot.set_group_ban({
+                            group_id: Number(context.group_id),
+                            user_id: Number(userId),
+                            duration: 60 * event.eventRewardNum,
+                        })
+                        msg += `触发事件:${event.eventContent}获得${event.eventRewardNum}分钟禁言`
+                    } else {
+                        qqBot.set_group_ban({
+                            group_id: Number(context.group_id),
+                            user_id: Number(userId),
+                            duration: 0,
+                        })
+                        msg += `触发事件:${event.eventContent}解除禁言`
+                    }
+                    break;
+            }
+        } else {
+            msg += `无效事件,无奖励`
+        }
+        return msg;
+
+
+    }
+    //保护卡
+    @prop("protectCard", "保护卡", 1, "触发事件时保护次数减1",
+        await convertImageToBase64("/Users/fenglin/Desktop/botQQ/src/resources/test/Prop/protectCard.jpg"),
+        1000
+    )
+    async protectCard(
+        userId: string,
+        propparam: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ): Promise<any> {
+        const userData = getUserData(userId);
+        if (userData) {
+            userData.events++;
+            saveUserData(userId, userData);
+            return `操作成功!当前保护次数${userData.events}`
+        }
+    }
+
+    async protectEvent(userId: string): Promise<{
+        success: boolean,
+        event: number
+    }> {
+        const userData = getUserData(userId);
+        if (userData) {
+            if (Number.isNaN(userData.events)) {
+                userData.events = 0;
+            }
+            if (userData.events <= 0) {
+                return {
+                    success: false,
+                    event: userData.events
+                }
+            }
+            userData.events--;
+            saveUserData(userId, userData);
+            return {
+                success: true,
+                event: userData.events
+            }
+        }
+
+        return {
+            success: false,
+            event: 0
+        }
+
+    }
+
+    //设置事件
+    @runcod(["设置事件", "setEvent"], "设置事件")
+    async setEvent(
+        @param("事件内容", "text") eventContent: Receive["text"],
+        @param("事件奖励类别", "text") eventRewardType: Receive["text"],
+        @param("事件奖励数量", "text") eventRewardNum: Receive["text"],
+        @param("事件奖励概率", "text") probability: Receive["text"],
+        @param("事件奖励参数", "text", { type: 'text', data: { text: "" } }, true) param: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        if (context?.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        if (eventRewardType?.data?.text !== '道具' && eventRewardType?.data?.text !== '金币' && eventRewardType?.data?.text !== '禁言') {
+            return "事件奖励类别错误,请选择道具、金币、禁言";
+        }
+
+        let Num = 1
+        if (eventRewardNum?.data?.text) {
+            Num = Number(eventRewardNum?.data?.text);
+        }
+
+        let probabilityNum = 0.3
+        if (probability?.data?.text) {
+            probabilityNum = Number(probability.data.text);
+        }
+
+        //道具时候填写参数
+        if (eventRewardType?.data?.text === '道具') {
+            if (!param?.data?.text) {
+                return "道具时候填写参数";
+            }
+        }
+
+        const eventId = uuid();
+        this.saveEvent(context.group_id, eventId, eventContent.data.text, eventRewardType.data.text, Num, probabilityNum, param?.data?.text ?? '');
+        return `事件设置成功,事件id:${eventId}`;
+    }
+
+    //保存事件
+    private async saveEvent(group_id: number, eventId: string, eventContent: string, eventRewardType: '道具' | '金币' | '禁言', eventRewardNum: number = 1, probability: number = 0.3, param: string = '') {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupEventData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        if (data[group_id]) {
+            data[group_id][eventId] = {
+                eventContent: eventContent,
+                eventRewardType: eventRewardType,
+                param: param,
+                eventRewardNum: Number(eventRewardNum),
+                probability: Number(probability),
+            }
+        }
+        else {
+            data[group_id] = {};
+            data[group_id][eventId] = {
+                eventContent: eventContent,
+                eventRewardType: eventRewardType,
+                param: param,
+                eventRewardNum: Number(eventRewardNum),
+                probability: Number(probability),
+            }
+        }
+        fs.writeFileSync(filePath, JSON.stringify(data));
+    }
+
+    async getAllEvent(group_id: number) {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupEventData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        let eventIdAndProbability = '事件列表:\n'
+        if (!data[group_id]) {
+            return '本群没有事件';
+        }
+        Object.keys(data[group_id]).forEach((key) => {
+            if (key !== '') {
+                eventIdAndProbability += `事件id:${key},事件内容:${data[group_id][key].eventContent},事件奖励类别:${data[group_id][key].eventRewardType},事件奖励数量:${data[group_id][key].eventRewardNum},事件奖励概率:${data[group_id][key].probability},事件奖励参数:${data[group_id][key].param}\n`;
+            }
+        })
+        return eventIdAndProbability;
+    }
+
+    @runcod(["事件列表", "getEvent"], "获取事件")
+    async getEvent(context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage) {
+        if (context?.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        const eventIdAndProbability = await this.getAllEvent(context.group_id);
+        return eventIdAndProbability;
+    }
+
+    async runEvent(user_id: number, group_id: number) {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupEventData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        let msg = ''
+        let eventId = ''
+
+        if (!data[group_id]) {
+            return '';
+        }
+        let events = data[group_id]
+        for (let i = 0; i < Object.keys(events).length; i++) {
+            if (Object.keys(events)[i] === '') {
+                continue;
+            }
+            let randomNum = Math.random();
+            if (randomNum >= events[Object.keys(events)[i]].probability) {
+                continue;
+            } else {
+                const event = events[Object.keys(events)[i]];
+                if (event) {
+                    const protect = await this.protectEvent(user_id.toString())
+                    if (protect.success) {
+                        return `触发事件${event.eventContent}但是由于有保护次数,所以本次事件不生效,剩余保护次数${protect.event}`
+                    }
+                    switch (event.eventRewardType) {
+                        case '道具':
+                            addProp(user_id.toString(), event.param, event.eventRewardNum)
+                            msg += `触发事件:${event.eventContent},获得${event.param}道具\n`
+                            break;
+                        case '金币':
+                            //执行金币奖励
+                            if (event.eventRewardNum >= 0) {
+                                msg += `触发事件:${event.eventContent}获得${event.eventRewardNum}金币\n`
+                                addCoins(user_id.toString(), Number(event.eventRewardNum), msg)
+
+                            } else {
+                                msg += `触发事件:${event.eventContent}失去${-event.eventRewardNum}金币\n`
+                                removeCoins(user_id.toString(), Number(-event.eventRewardNum), msg)
+
+                            }
+                            break;
+                        case '禁言':
+                            if (event.eventRewardNum >= 0) {
+                                qqBot.set_group_ban({
+                                    group_id: Number(group_id),
+                                    user_id: Number(user_id),
+                                    duration: 60 * event.eventRewardNum,
+                                })
+                                msg += `触发事件:${event.eventContent}获得${event.eventRewardNum}分钟禁言`
+                            } else {
+                                qqBot.set_group_ban({
+                                    group_id: Number(group_id),
+                                    user_id: Number(user_id),
+                                    duration: 0,
+                                })
+                                msg += `触发事件:${event.eventContent}解除禁言`
+                            }
+                            break;
+                    }
+                }
+            }
+        }
+        return msg;
+    }
+
+    private async savepl(group_id: number, worldData: string[], userData: string[]): Promise<void> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupWorldData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        if (data[group_id]) {
+            data[group_id].worldData = worldData;
+            data[group_id].userData = userData;
+            data[group_id].updatetime = new Date().getTime()
+        } else {
+            data[group_id] = {
+                worldData,
+                userData,
+                createtime: new Date().getTime(),
+                updatetime: new Date().getTime(),
+            }
+        }
+        fs.writeFileSync(filePath, JSON.stringify(data));
+        return;
+    }
+
+    private async readpl(group_id: number): Promise<{ worldData: string[], userData: string[] } | undefined> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupWorldData.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+            if (data[group_id]) {
+                return { worldData: data[group_id].worldData, userData: data[group_id].userData ?? [] };
+            }
+        } else {
+            return { worldData: [], userData: [] };
+        }
+    }
+
+    @runcod(["记录关键词", "Key","key"], "记录关键词")
+    async SaveKey(
+        @param("key", 'text')
+        keyword: Receive["text"],
+        @param("引用", 'reply') reply: Receive["reply"],//引用参数必须是最后一个
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        if (context?.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        const msg = await qqBot.get_msg({
+            message_id: Number(reply.data.id),
+        })
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        for (let i = 0; i < msg.message.length; i++) {
+            if (msg.message[i].type == 'image') {
+                const image = msg.message[i]  as ImageSegment
+                let imagePath = path.join('/Volumes/liuqianpan2008/MACServer/Keyimage', image.data.file)
+                await this.downloadFile((image?.data as any).url, imagePath);
+                (msg.message[i] as ImageSegment).data.file = imagePath
+            }
+            if(msg.message[i].type == 'file'){
+                const file = msg.message[i]  as FileSegment
+                let filePath = path.join('/Volumes/liuqianpan2008/MACServer/Keyimage', file.data.file)
+                await this.downloadFile((file?.data as any).url, filePath);
+                (msg.message[i] as FileSegment).data.file = filePath
+            }
+        }
+        await this.saveKeywordReply(context.group_id, keyword.data.text, msg.message);
+        return "记录成功";
+    }
+
+    //删除关键词
+    @runcod(["删除关键词", "delkey", "delKey"], "删除关键词")
+    async delKey(
+        @param("key", 'text')
+        keyword: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        if (context?.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        await this.delKeywordReply(context.group_id, keyword.data.text);
+        return "删除成功";
+    }
+
+    //查看关键词
+    @runcod(["查看关键词", "viewkey", "viewKey"], "查看关键词")
+    async viewKey(
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        if (context?.message_type !== "group") {
+            return "请在群聊中使用";
+        }
+        const reply = await this.getAllKeyBygroup(context.group_id);
+        if (reply) {
+            return reply;
+        } else {
+            return "关键词不存在";
+        }
+    }
+
+
+    //保存关键词回复
+    private async saveKeywordReply(group_id: number, keyword: string, reply: Receive[keyof Receive][]): Promise<void> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupKeywordReply.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        if (data[group_id]) {
+            data[group_id][keyword] = reply;
+        } else {
+            data[group_id] = {
+                [keyword]: reply,
+            }
+        }
+        fs.writeFileSync(filePath, JSON.stringify(data));
+        return;
+    }
+
+    private async getAllKeyBygroup(group_id: number){
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupKeywordReply.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+            if (data[group_id]) {
+                return Object.keys(data[group_id]);
+            }
+        }
+        return "";
+    }
+    //获取关键词回复
+    private async getKeywordReply(group_id: number, keyword: string): Promise<string> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupKeywordReply.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+            if (data[group_id]) {
+                return data[group_id][keyword];
+            }
+        }
+        return "";
+    }
+    //删除关键词回复
+    private async delKeywordReply(group_id: number, keyword: string): Promise<void> {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'GroupKeywordReply.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+            if (data[group_id]) {
+                delete data[group_id][keyword];
+            }
+        }
+        fs.writeFileSync(filePath, JSON.stringify(data));
+        return;
+    }
+    //下载文件
+    private async downloadFile(url: string, path: string) {
+        const response = await fetch(url);
+        const arrayBuffer = await response.arrayBuffer();
+        const buffer = Buffer.from(arrayBuffer);
+        fs.writeFileSync(path, buffer);
+    }
+
+}

+ 215 - 0
src/plugins/sakulin.ts

@@ -0,0 +1,215 @@
+//PLUGIN sakulin.ts
+
+import axios from 'axios';
+import { param, plugins, runcod } from '../lib/decorators.js';
+import 'reflect-metadata';
+import { GroupMessage, PrivateFriendMessage, PrivateGroupMessage, Receive } from 'node-napcat-ts';
+import * as fs from 'fs';
+import { RootObject } from '../interface/sakulin.js';
+import { qqBot } from '../app.js';
+import { uuid } from '@renmu/bili-api/dist/utils/index.js';
+
+
+
+const imgSourceMap: { [key: string]: string } = {
+    "二次元": "https://app.zichen.zone/api/acg/api.php",
+    "原神": "https://t.alcy.cc/ysz",
+    "三次元": "https://api.lolimi.cn/API/tup/xjj.php",
+    "碧蓝档案": "https://image.anosu.top/pixiv/direct?r18=0&keyword=bluearchive",
+    "碧蓝航线": "https://image.anosu.top/pixiv/direct?r18=0&keyword=azurlane",
+    "明日方舟": "https://image.anosu.top/pixiv/direct?r18=0&keyword=arknights",
+    "公主连接": "https://image.anosu.top/pixiv/direct?r18=0&keyword=princess",
+    "东方": "https://image.anosu.top/pixiv/direct?r18=0&keyword=touhou"
+};
+
+const defaultSource = "二次元";
+
+const imageSourceDesc = Object.keys(imgSourceMap).map(e => ((e == defaultSource) ? (e + "(默认)") : e)).join("、");
+
+
+@plugins({
+    easycmd: true,
+    name: "【推荐】红磷的黑科技工具箱,输入 #sakulass 查看具体使用方法",
+    version: "1.0.0",
+    describe: "日常制作许多有趣好玩的工具箱,如果有什么更好的想法可联系作者活性红磷 😄",
+    author: "活性红磷",
+    help: {
+        enabled: false,
+        description: "查看帮助信息"
+    }
+})
+export class sakulass {
+
+    @runcod(["help", "帮助"], "查看帮助信息")
+    async help() {
+        return {
+            template: {
+                enabled: true,
+                sendText: false,
+                html: `
+<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5; border-radius: 10px;">
+    <h1 style="color: #333; text-align: center;">红磷的黑科技工具箱 - 插件文档</h1>
+    <h2 style="color: #555; margin-top: 30px;">插件信息</h2>
+    <ul style="list-style-type: none; padding: 0;">
+        <li><strong>ID:</strong> saku</li>
+        <li><strong>名称:</strong> 【推荐】红磷的黑科技工具箱</li>
+        <li><strong>版本:</strong> 1.0.0</li>
+        <li><strong>描述:</strong> 日常制作许多有趣好玩的工具箱,如果有什么更好的想法可联系作者活性红磷 😄</li>
+        <li><strong>作者:</strong> 活性红磷</li>
+    </ul>
+    <h2 style="color: #555; margin-top: 30px;">命令列表</h2>
+    <div style="background-color: white; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
+        <h3 style="color: #666;">help / 帮助</h3>
+        <p>查看帮助信息</p>
+    </div>
+    <div style="background-color: white; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 15px;">
+        <h3 style="color: #666;">ping / test</h3>
+        <p>这是一个测试用的命令,用来测试这个插件是否正常工作</p>
+    </div>
+    <div style="background-color: white; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 15px;">
+        <h3 style="color: #666;">图 / tu</h3>
+        <p>从网上获取随机美图,可添加不同图源作为参数,例如发送 #saku 图 原神。目前可选的图源有:${imageSourceDesc},如果想要更多的图源,可联系作者活性红磷添加</p>
+    </div>
+</div>
+                `,
+                render: {
+                    width: 800,
+                    fullpage: true
+                }
+            }
+        }
+    }
+
+    @runcod(["ping", "test"], "这是一个测试用的命令,用来测试这个插件是否正常工作")
+    async test() {
+        return {
+            template: {
+                enabled: true,
+                sendText: false,
+                html: `
+                <div>这是一个测试用的命令,用来测试这个插件是否正常工作</div>
+                `
+            }
+        };
+    }
+
+    @runcod(["图", "tu"], `从网上获取随机美图,可添加不同图源作为参数,例如发送 #saku 图 原神来获取目前可选的图源有:${imageSourceDesc},如果想要更多的图源,可联系作者活性红磷添加`)
+    async image(
+        @param("图源", "text", { type: 'text', data: { text: defaultSource } }, true) type: Receive["text"],
+    ) {
+
+        const source = imgSourceMap[type?.data?.text] ?? imgSourceMap[defaultSource];
+
+        try {
+            const response = await fetch(source);
+            const blob = await response.blob();
+
+            return {
+                picture: {
+                    enabled: true,
+                    base64: Buffer.from(await blob.arrayBuffer()).toString("base64")
+                }
+            }
+        } catch (e) {
+            return `获取时发生错误:${JSON.stringify(e)}`;
+        }
+    }
+    @runcod(["一言", "yiyan"], `获取随机的一励志鸡汤`)
+    async yiyan() {
+        try {
+            const response = await axios.get('https://v1.hitokoto.cn/');
+            const data = response.data.hitokoto;
+            return {
+                template: {
+                    enabled: true,
+                    sendText: false,
+                    render: {
+                        fullpage: true
+                    },
+                    html: `
+                        <div style="font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5; border-radius: 10px;">
+                            <h1 style="color: #333; text-align: center;">随机的一励志鸡汤</h1>
+                            <p style="color: #666; font-size: 18px; text-align: center;">${data}</p>
+                        </div>
+                    `,
+                }
+            }
+        } catch (e) {
+            return `获取时发生错误:${JSON.stringify(e)}`;
+        }
+    }
+
+    @runcod(["jm", "jmc", "禁漫", "禁漫天堂"], `老司机必备`)
+    async jm(@param("id", "text") jid: Receive["text"],
+        @param("episode", "text", { type: 'text', data: { text: "1" } }, true) episode: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        const host = '127.0.0.1'
+        const port = 24357
+        let id = 0;
+        if (!Number.isInteger(Number(jid?.data?.text))) {
+            //随机5-8位数
+            id = Math.floor(Math.random() * (899999 - 100000 + 1)) + 100000;
+        }else{
+            id = Number(jid?.data?.text);
+        }
+        if (!Number.isInteger(episode?.data?.text ?? 1)) {
+            return `请输入正确的章节`
+        }
+
+        const target: RootObject | undefined = await new Promise<RootObject | undefined>((resolve) => {
+            const ws = new WebSocket(`ws://${host}:${port}`);
+            ws.onopen = () => {
+                ws.send(JSON.stringify({ id }));
+            }
+            ws.onmessage = (res) => {
+                const responseData = JSON.parse(res.data)
+                if (responseData["SIGNAL"] === "RESPONSE") {
+                    ws.close(1000);
+                    resolve(responseData);
+                }
+            }
+        }).catch(e => {
+            console.log(e);
+            return void 0;
+        });
+        if (!target) {
+            return "好像发生了点异常?能联系开发者看看发生什么了吗";
+        }
+        if (target.success) {
+            if (target.pdf.length) {
+                let numberEpisode = Number(episode?.data?.text ?? 1);
+                --numberEpisode;
+                const filename = `${uuid()}.pdf`;
+                const isGroupMessage = context.message_type === 'group';
+                if (isGroupMessage && context.group_id) {
+                    await qqBot.upload_group_file({
+                        group_id: Number(context.group_id),
+                        file: 'data:file;base64,' + await this.fileToBase64(target.pdf[numberEpisode]),
+                        name: filename
+                    })
+                } else {
+                    await qqBot.upload_private_file({
+                        user_id: Number(context.sender.user_id),
+                        file: 'data:file;base64,' + await this.fileToBase64(target.pdf[numberEpisode]),
+                        name: filename
+                    })
+
+                }
+                return `已发送`
+            }
+        }
+
+    }
+    async fileToBase64(filePath: string) {
+        return new Promise((resolve, reject) => {
+            fs.readFile(filePath, (err, data) => {
+                if (err) {
+                    reject(err);
+                } else {
+                    resolve(data.toString('base64'));
+                }
+            });
+        });
+    }
+}

+ 618 - 0
src/plugins/skd.ts

@@ -0,0 +1,618 @@
+import { GroupMessage, PrivateFriendMessage, PrivateGroupMessage, Receive } from "node-napcat-ts";
+import { param, plugins, runcod } from "../lib/decorators.js";
+import { attendance, auth, getBinding, signIn } from "@skland-x/core";
+import axios from "axios";
+import fs from 'fs'
+import path from 'path';
+import { fileURLToPath } from "url";
+import crypto from 'crypto'
+import botlogger from "../lib/logger.js";
+
+type Character = {
+    uid: string;
+    isOfficial: boolean;
+    isDefault: boolean;
+    channelMasterId: string;
+    channelName: string;
+    nickName: string;
+    isDelete: boolean;
+}
+type UserInfoData = {
+    ap:{
+        now:number,
+        max:number,
+    },
+    level:number,
+    registerTs: string,
+    name: string,
+    skins: number,
+    mainStageProgress: string,
+    furniture: number,
+    chars: number,
+    avatar: string,
+}
+
+@plugins({
+    easycmd: true,//是否启用简易命令,启用将将命令注册为<命令名称>,不启用将注册为#<插件名称> <命令名称>
+    name: "森空岛", //插件名称,用于显示在菜单中
+    version: "1.0.0", //插件版本号,用于显示在菜单中
+    describe: "森空岛插件", //插件描述,用于显示在菜单中
+    author: "枫叶秋林",//插件作者,用于显示在菜单中
+    help: { //插件帮助信息,用于显示在菜单中
+        enabled: true, //是否启用帮助信息
+        description: "显示帮助信息" //帮助信息描述
+    }
+})
+export class skd {
+    private REQUEST_HEADERS_BASE = {
+        "User-Agent": "Skland/1.0.1 (com.hypergryph.skland; build:100001014; Android 31; ) Okhttp/4.11.0",
+        "Accept-Encoding": "gzip",
+        "Connection": "close",
+    }
+    constructor() {
+        
+    }
+
+    @runcod(["绑定skd", "skd绑定"], `绑定skd`)
+    async bind(
+        @param("token", 'text') token: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+       if (!token?.data?.text) {
+            return '请输入token'
+        }
+        const { code } = await auth(token?.data?.text)
+        const { cred, token: signToken } = await signIn(code)
+        const { list } = await getBinding(cred, signToken)
+        const characterList = (list.filter((i: { appCode: string; }) => i.appCode === 'arknights').map((i: { bindingList: any; }) => i.bindingList).flat()) as Character[]
+        if (characterList.length === 0) {
+            return '该通行证未查询到绑定的账号'
+        }
+        this.saveBinding(context?.sender?.user_id, token?.data?.text)
+        return `绑定账号:${characterList.map((i: { nickName: string; }) => i.nickName).join(',')},绑定成功`
+    }
+
+    @runcod(["干员", "查询干员"], `干员bilibiliWiki截图`)
+    async browser(
+        @param("干员名称", 'text') name: Receive["text"],
+        @param("内容信息", 'text', { type: 'text', data: { text: '' } }, true) element: Receive["text"]
+    ) {
+        let sandbox = ''
+        switch (element?.data?.text) {
+            case '评论':
+                sandbox = '#flowthread'
+                break;
+            case '语音':
+                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[27])'
+                break;
+            case '档案':
+                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[24])'
+                break;
+            case '模组':
+                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[21])'
+                break;
+            case '基建':
+                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[19])'
+                break;
+            case '技能材料':
+                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[17])'
+            default:
+                sandbox = ''
+        }
+        return {
+            selector: sandbox,
+            template: { // 模板配置,用于发送图片内容
+                enabled: true,//是否启用模板,启用将发送图片内容
+                sendText: false,//是否发送文本,启用将发送文本内容,如果都启用则发送两条消息
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 600, // 模板宽度
+                    height: 1, // 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true,
+                    url: `https://wiki.biligame.com/arknights/${name?.data?.text}`// 模板路径,推荐按规范放置在resources目录下
+                }
+            },
+            toString() { //重写toString方法,用于返回文本内容,启用sendText时将发送文本内容,不启用时将发送图片内容,图片发送失败时发送文字内容
+                return `访问${name?.data?.text}`;
+            }
+        }
+    }
+    @runcod(['个人卡片', 'skd卡片','卡片','skdCard'], `skd查询干员信息`)
+    async query(
+        @param("顺序", 'text', { type: 'text', data: { text: '' } }, true) index: Receive["text"],
+         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        let indexNum = 0
+        if (!index?.data?.text || isNaN(Number(index.data.text))) {
+            indexNum = 0
+        } else {
+            indexNum = Number(index.data.text)
+        }
+        const data = await this.getInfo(context?.sender?.user_id, indexNum)
+        if (data === '-1') {
+            return this.getErronStr()
+        }
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        const info = await this.userInfoData(data as any)
+        console.log(JSON.stringify(info));
+        return {
+            data: info,
+            pluginResources: path.resolve(__dirname, '..', 'resources', 'skd'),
+            template: {
+                enabled: true,//是否启用模板,启用将发送图片内容
+                path: path.resolve(__dirname, '..', 'resources', 'skd', 'userinfo.html'),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 600, // 模板宽度
+                    height: 1, // 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true,// 是否背景
+                }
+            }
+        }
+    }
+    async queryMe() {
+        const data = await this.getInfo(2180323481, 0)
+        const info = await this.userInfoData(data as any)
+        console.log(JSON.stringify(info));
+        return {data: info}
+    }
+    async userInfoData({status,building,chars,skins}:{status:any,building:any,chars:any,skins:any}):Promise<UserInfoData> {
+        if (!status || !building || !chars) {
+            return {
+                ap:{
+                    now:0,
+                    max:0,
+                },
+                level:0,
+                registerTs: '',
+                name: '',
+                mainStageProgress: '',
+                furniture: 0,
+                chars: 0,
+                skins: 0,
+                avatar: '',
+            }
+        }
+        let data1 = {
+            ap:{
+                now:0,
+                max:0,
+            },
+            skins: 0,
+            level:0,
+            registerTs: '',
+            name: '',
+            mainStageProgress: '',
+            furniture: 0,
+            chars: 0,
+            avatar: '',
+        }
+        // 注册时间
+        //格式化时间8位
+        function formatTime(time: Date) {
+            return time.getFullYear() + '-' + (time.getMonth() + 1) + '-' + time.getDate() + ' ' + time.getHours() + ':' + time.getMinutes() + ':' + time.getSeconds();
+        }
+        data1.registerTs = formatTime(new Date(status.registerTs * 1000))
+
+        // 游戏昵称
+        data1.name = 'Dr.' + status.name
+        //取整
+        let apAddTime = Number((new Date().getTime()-new Date(status.ap.lastApAddTime*1000).getTime())/1000/60/6)
+        apAddTime = Math.floor(apAddTime)
+        console.log(status.ap);
+        data1.ap = {
+            now: status.ap.current + apAddTime,
+            max: status.ap.max,
+        }
+        // 等级
+        data1.level = status.level
+        // 头像
+        data1.avatar = status.avatar.url
+        // 作战进度
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const lvPath = path.resolve(__dirname, '..', 'resources', 'wiki', 'levels.json')
+        let levels = JSON.parse(fs.readFileSync(lvPath, 'utf8'))
+        data1.mainStageProgress = levels[status.mainStageProgress] || ''
+        // 家具保有
+        data1.furniture = building.furniture.total
+        // 干员数量
+        data1.chars = chars.length-2
+        // 皮肤数量
+        data1.skins = skins.length
+        return data1
+    }
+    @runcod(['基建卡片', 'skd基建卡片','基建','skdBuildingCard'], `skd查询干员信息`)
+    async buildingCard(
+        @param("顺序", 'text', { type: 'text', data: { text: '' } }, true) index: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        let indexNum = 0
+        if (!index?.data?.text || isNaN(Number(index.data.text))) {
+            indexNum = 0
+        } else {
+            indexNum = Number(index.data.text)
+        }
+        const data = await this.getInfo(context?.sender?.user_id, indexNum)
+        if (data === '-1') {
+            return this.getErronStr()
+        }
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        const buildingData = await this.buildingData(data as any)
+        return {
+            data: buildingData,
+            pluginResources: path.resolve(__dirname, '..', 'resources', 'skd'),
+            template: {
+                enabled: true,//是否启用模板,启用将发送图片内容
+                path: path.resolve(__dirname, '..', 'resources', 'skd', 'building.html'),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 600, // 模板宽度
+                    height: 1, // 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true,// 是否背景
+                }
+            }
+        }
+    }
+    async buildingData({ building }:{building:any}) {
+        if (!building) {
+            return {}
+        }
+        let data = {
+            tradings: {
+                now: 0,
+                max: 0,
+            },
+            manufactures: {
+                now: 0,
+                max: 0,
+            },
+            dormitories: {
+                now: 0,
+                max: 0,
+            },
+            board: {
+                now: 0,
+                max: 0,
+            },
+            tiredChars: {
+                now: 0,
+                max: 0,
+            },
+            labor: {
+                now: 0,
+                max: 0,
+            },
+        }
+        // 订单进度
+        data.tradings.now = building.tradings.reduce((acc: any, cur: { chars: string | any[]; }) => acc + cur.chars.length, 0)
+        data.tradings.max = building.tradings.reduce((acc: any, cur: { stockLimit: any; }) => acc + cur.stockLimit, 0)
+        // 制造进度
+        data.manufactures.now = building.manufactures.reduce((acc: any, cur: { weight: any; }) => acc + cur.weight, 0)
+        data.manufactures.max = building.manufactures.reduce((acc: any, cur: { capacity: any; }) => acc + cur.capacity, 0)
+
+        // 休息进度
+        data.dormitories.now = building.dormitories.reduce((acc: any, cur: { chars: any[]; }) => acc + cur.chars.reduce((acc, cur) => acc + (cur.ap === 8640000 ? 1 : 0), 0), 0)
+        data.dormitories.max = building.dormitories.reduce((acc: any, cur: { chars: string | any[]; }) => acc + cur.chars.length, 0)
+        // 线索进度
+        data.board.now = building.meeting.clue.board.length
+        data.board.max = 7
+        // 干员疲劳
+        data.tiredChars.now = building?.tiredChars?.length??0
+        // 无人机
+        data.labor.now = Math.min(Math.round((Date.now() / 1000 - building.labor.lastUpdateTime) / 360 + building.labor.value), building.labor.maxValue)
+        data.labor.max = building.labor.maxValue
+        return data
+
+    }
+    @runcod(['qd'], `森空岛签到`)
+    async sign(
+        @param("顺序", 'text', { type: 'text', data: { text: '' } }, true) index: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+
+    ) {
+        const tokenList = await this.getBinding(context?.sender?.user_id)
+        if (tokenList.length === 0) {
+            return this.getErronStr()
+        }
+        let indexNum = 0
+        if (!index?.data?.text || isNaN(Number(index.data.text))) {
+            indexNum = 0
+        } else {
+            indexNum = Number(index.data.text)
+        }
+        const token = tokenList[indexNum]
+        if (!token) {
+            return this.getErronStr()
+        }
+        const { code } = await auth(token);
+        const { cred, token: signToken } = await signIn(code);
+        const { list } = await getBinding(cred, signToken);
+        const characterList = (list.filter((i: { appCode: string; }) => i.appCode === 'arknights').map((i: { bindingList: any; }) => i.bindingList).flat()) as Character[]
+        let data = await attendance(cred, signToken, {
+            uid: characterList[0].uid,
+            gameId: characterList[0].channelMasterId,
+        })
+        if (data.code === 0 && data.message === 'OK') {
+            return `签到成功${data.data.awards.length > 0 ? `,获得了${data.data.awards.map((a: { resource: { name: any; }; count: any; }) => `「${a.resource.name}」${a.count}个`).join(',')}` : ''}`
+        }else{
+            return '已经签过到了,无需在操作'
+        }
+    }
+    @runcod(['kj'], `我的氪金`)
+    async kj(
+        @param("顺序", 'text', { type: 'text', data: { text: '' } }, true) index: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+
+    ) {
+        const tokenList = await this.getBinding(context?.sender?.user_id)
+        if (tokenList.length === 0) {
+            return this.getErronStr()
+        }
+        let indexNum = 0
+        if (!index?.data?.text || isNaN(Number(index.data.text))) {
+            indexNum = 0
+        } else {
+            indexNum = Number(index.data.text)
+        }
+        const token = tokenList[indexNum]
+        if (!token) {
+            return this.getErronStr()
+        }
+        const { code } = await auth(token);
+        const { cred, token: signToken } = await signIn(code);
+        const timestamp = await this.getTimestamp()
+        let signedHeaders = await this.getSignHeader('https://u8.hypergryph.com/u8/pay/v1/recent', 'post', {appId:1,channelMasterId:1}, this.REQUEST_HEADERS_BASE, signToken, timestamp);
+        const {data:playerInfo} = (await axios({
+            url:'https://u8.hypergryph.com/u8/pay/v1/recent',
+            method:'POST',
+            headers: {
+                ...signedHeaders,
+                token: signToken,
+                cred,
+            },
+            data: {
+                appId:1,
+                channelMasterId:1,
+                channelToken:{token,},
+            },
+        }))
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        return {
+            data:playerInfo.data,
+            pluginResources: path.resolve(__dirname, '..', 'resources', 'skd'),
+            template: {
+                enabled: true,//是否启用模板,启用将发送图片内容
+                path: path.resolve(__dirname, '..', 'resources', 'skd', 'kj.html'),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 600, // 模板宽度
+                    height: 1, // 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true,// 是否背景
+                }
+            }
+            
+        }
+    }
+    @runcod(['jczl','集成战略','集成查询','jc'], `集成战略查询`)
+    async jczl(
+        @param("顺序", 'text', { type: 'text', data: { text: '' } }, true) index: Receive["text"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+
+    ) {
+        const tokenList = await this.getBinding(context?.sender?.user_id)
+        if (tokenList.length === 0) {
+            return this.getErronStr()
+        }
+        let indexNum = 0
+        if (!index?.data?.text || isNaN(Number(index.data.text))) {
+            indexNum = 0
+        } else {
+            indexNum = Number(index.data.text)
+        }
+        const token = tokenList[indexNum]
+        if (!token) {
+            return this.getErronStr()
+        }
+        const { code } = await auth(token);
+        const { cred, token: signToken } = await signIn(code);
+        const { list } = await getBinding(cred, signToken);
+        const characterList = (list.filter((i: { appCode: string; }) => i.appCode === 'arknights').map((i: { bindingList: any; }) => i.bindingList).flat()) as Character[]
+        if (characterList.length === 0) {
+            return '-1'
+        }
+        const timestamp = await this.getTimestamp()
+        let signedHeaders = await this.getSignHeader('https://zonai.skland.com/api/v1/game/arknights/rogue', 'get', {uid: characterList[0].uid}, this.REQUEST_HEADERS_BASE, signToken, timestamp);
+        const {data} = (await axios({
+            url:'https://zonai.skland.com/api/v1/game/arknights/rogue',
+            method:'get',
+            headers: {
+                ...signedHeaders,
+                token: signToken,
+                cred,
+            },
+            params: {
+                uid: characterList[0].uid,
+            },
+        }))
+        //写入文件
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        fs.writeFileSync(path.resolve(__dirname, '..', 'resources', 'skd', 'jczl.json'), JSON.stringify(data));
+        return {
+            data:data.data,
+            pluginResources: path.resolve(__dirname, '..', 'resources', 'skd'),
+            template: {
+                enabled: true,//是否启用模板,启用将发送图片内容
+                path: path.resolve(__dirname, '..', 'resources', 'skd', 'jczl.html'),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 600, // 模板宽度
+                    height: 1, // 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true,// 是否背景
+                }
+            }
+            
+        }
+    }
+    private async getTimestamp() {
+        return String(Math.floor(Date.now() / 1000) - 2);
+    }
+    private async getSignHeader(apiUrl: string, method: string, body: any, oldHeader: any, signToken: string, timestamp: string) {
+        if (!apiUrl) {
+            return '请输入apiUrl'
+        }
+        if (!method) {
+            return '请输入method'
+        }
+        if (!signToken) {
+            return '请输入signToken'
+        }
+        if (!timestamp) {
+            return '请输入timestamp'
+        }
+
+        let header = { ...oldHeader };
+        const urlParsed = new URL(apiUrl);
+        let bodyOrQuery = method.toLowerCase() === 'get'
+            ? new URLSearchParams(body || urlParsed.searchParams).toString()
+            : (body ? JSON.stringify(body) : '');
+        const {
+            md5: sign, headerCa
+        } = await this.generateSignature(signToken, urlParsed.pathname, bodyOrQuery, timestamp);
+        header['sign'] = sign;
+        header = { ...header, ...headerCa };
+        return header;
+    }
+    private async generateSignature(token: string, path: string, bodyOrQuery: string, timestamp: string) {
+        if (!token) {
+            return {
+                md5: '',
+                headerCa: {},
+            }
+        }
+        if (!path) {
+            return {
+                md5: '',
+                headerCa: {},
+            }
+        }
+        if (!bodyOrQuery) {
+            return {
+                md5: '',
+                headerCa: {},
+            }
+        }
+        if (!timestamp) {
+            return {
+                md5: '',
+                headerCa: {},
+            }
+        }
+        let headerCa = Object.assign({}, {
+            "platform": "", "timestamp": "", "dId": "", "vName": ""
+        },);
+        headerCa.timestamp = timestamp;
+        let headerCaStr = JSON.stringify(headerCa);
+
+        let s = path + bodyOrQuery + timestamp + headerCaStr;
+        let hmac = crypto.createHmac('sha256', Buffer.from(token, 'utf-8')).update(s).digest('hex');
+        let md5 = crypto.createHash('md5').update(hmac).digest('hex');
+        return { md5: md5, headerCa: headerCa };
+    }
+    //储存绑定
+    private async saveBinding(seedId: number, token: string): Promise<void> {
+        if (!seedId) {
+            return;
+        }
+        if (!token) {
+            return;
+        }
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'SKDBinding.json');
+        let data: any = {};
+        if (!seedId) {
+            return;
+        }
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+        } else {
+            fs.writeFileSync(filePath, JSON.stringify(data));
+        }
+        if (!data[seedId]) {
+            data[seedId] = [token];
+        }else{
+            if (data[seedId].indexOf(token) === -1) {
+                data[seedId].push(token);
+            }
+        }
+        fs.writeFileSync(filePath, JSON.stringify(data));
+        return;
+    }
+    //获取绑定
+    private async getBinding(seedId: number): Promise<string[]> {
+        if (!seedId) {
+            return [];
+        }
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'SKDBinding.json');
+        let data: any = {};
+        if (fs.existsSync(filePath)) {
+            const fileContent = fs.readFileSync(filePath, 'utf-8');
+            data = JSON.parse(fileContent);
+            if (data[seedId]) {
+                return data[seedId];
+            }
+        }
+        return [];
+    }
+    //获取信息
+    private async getInfo(seedId: number,index:number=0): Promise<string> {
+        if (!seedId) {
+            return '请输入seedId'
+        }
+        const tokenList = await this.getBinding(seedId)
+        if (tokenList.length === 0) {
+            return '-1'
+        }
+        const token = tokenList[index]
+        if (!token) {
+            return '-1'
+        }
+        const { code } = await auth(token);
+        const { cred, token: signToken } = await signIn(code);
+        const { list } = await getBinding(cred, signToken);
+        const characterList = (list.filter((i: { appCode: string; }) => i.appCode === 'arknights').map((i: { bindingList: any; }) => i.bindingList).flat()) as Character[]
+        if (characterList.length === 0) {
+            return '-1'
+        }
+        const timestamp = await this.getTimestamp()
+        let signedHeaders = await this.getSignHeader('https://zonai.skland.com/api/v1/game/player/info', 'get', {uid: characterList[0].uid}, this.REQUEST_HEADERS_BASE, signToken, timestamp);
+        const {data:playerInfo} = (await axios({
+            url:'https://zonai.skland.com/api/v1/game/player/info',
+            method:'get',
+            headers: {
+                ...signedHeaders,
+                token: signToken,
+                cred,
+            },
+            params: {
+                uid: characterList[0].uid,
+            },
+        }))
+        
+        return playerInfo.data
+    }
+    private getErronStr():string{
+        return '登录 森空岛(https://www.skland.com/)网页版 后,打开 https://web-api.skland.com/account/info/hg 记下 content 字段的值,发送 #绑定skd content即可完成绑定'
+    }
+}

+ 43 - 0
src/plugins/test.ts

@@ -89,7 +89,50 @@ export class test {
             }
         };
     }
+    @runcod(["reply"], "引用实例测试" )//命令装饰器,用于注册命令
+    async testparam(
+        @param("参数3", 'reply') param3: Receive["reply"],//引用参数必须是最后一个
+        @param("参数1", 'text') param1: Receive["text"],
+        @param("参数2", 'at') param2: Receive["at"],
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ): Promise<any> {
+        if (!param1) {
+            return "请输入正确的参数格式: #test reply <引用消息>";
+        }
+
+        const msg = await qqBot.get_msg({
+            message_id: Number(param3.data.id),
+        })
+        
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        // 返回带模板的响应
+        return {
+            param1,//参数1,用于显示在菜单中
+            //渲染优先级 url渲染 > 简易渲染 > 模版渲染
+            template: { // 模板配置,用于发送图片内容
+                enabled: true,//是否启用模板,启用将发送图片内容
+                sendText: false,//是否发送文本,启用将发送文本内容,如果都启用则发送两条消息
+                // path: path.resolve(__dirname, '..', 'resources', 'test', 'param.html'),//模版路径,推荐按规范放置在resources目录下
+                html: `<div>${JSON.stringify(msg.message)}参数一${param1.data.text}参数二${param2.data.qq} 参数三${param3.data.id}</div>`,//简易渲染,填写html内容
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    isgif: false,
+                    width: 600, // 模板宽度
+                    height: 1, // 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true,// 是否背景
+                    // url: 'http://www.baidu.com'// 直接使用网站截图渲染支持90%的网站,需要自行测试
+                }
+            },
+            toString() { //重写toString方法,用于返回文本内容,启用sendText时将发送文本内容,不启用时将发送图片内容,图片发送失败时发送文字内容
+                return `参数1(字符串): ${param1}`;
+            }
+        };
+    }
+
 
+    
     @Permission('Group')
     @runcod(['tp',"权限测试"],"权限测试,执行此指令返回对应权限")//命令装饰器,用于注册命令
     async Permission(): Promise<any> {

+ 8 - 55
src/plugins/wiki.ts

@@ -22,57 +22,10 @@ import sharp from 'sharp';
         description: "查看帮助信息"
     }
 })
-export class wiki {
+export class abd {
 
-    @runcod(["干员", "查询干员"], `干员bilibiliWiki截图`)
-    async browser(
-        @param("干员名称", 'text') name: Receive["text"],
-        @param("内容信息", 'text', { type: 'text', data: { text: '' } }, true) element: Receive["text"]
-    ) {
-        let sandbox = ''
-        switch (element?.data?.text) {
-            case '评论':
-                sandbox = '#flowthread'
-                break;
-            case '语音':
-                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[27])'
-                break;
-            case '档案':
-                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[24])'
-                break;
-            case '模组':
-                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[21])'
-                break;
-            case '基建':
-                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[19])'
-                break;
-            case '技能材料':
-                sandbox = '::-p-xpath(/html/body/div[2]/div[2]/div[4]/div[5]/div/div[17])'
-            default:
-                sandbox = ''
-        }
-        return {
-            selector: sandbox,
-            template: { // 模板配置,用于发送图片内容
-                enabled: true,//是否启用模板,启用将发送图片内容
-                sendText: false,//是否发送文本,启用将发送文本内容,如果都启用则发送两条消息
-                render: {//浏览器默认参数设置,用于打开浏览器的设置
-                    width: 600, // 模板宽度
-                    height: 1, // 模板高度
-                    type: 'png',// 模板类型
-                    quality: 100,// 模板质量
-                    fullPage: false,// 是否全屏
-                    background: true,
-                    url: `https://wiki.biligame.com/arknights/${name?.data?.text}`// 模板路径,推荐按规范放置在resources目录下
-                }
-            },
-            toString() { //重写toString方法,用于返回文本内容,启用sendText时将发送文本内容,不启用时将发送图片内容,图片发送失败时发送文字内容
-                return `访问${name?.data?.text}`;
-            }
-        }
-    }
 
-    @runcod(['方舟智能点击', '智能点击'], `会自动识别文字,安卓模拟器的操作方舟点击操作,优先使用创建的点击步骤`)
+    @runcod(['智能点击', '智能点击'], `会自动识别文字,安卓模拟器的操作方舟点击操作,优先使用创建的点击步骤`)
     async rig(
         @param("点击文本", 'text') targetText: Receive["text"],
         @param("x偏移", 'text', { type: 'text', data: { text: '' } }, true) x1: Receive["text"],
@@ -165,7 +118,7 @@ export class wiki {
             }
         }
     }
-    @runcod(['方舟滑动', '滑动'], `滑动屏幕`)
+    @runcod(['滑动', '滑动'], `滑动屏幕`)
     async huadong(
         @param("name", 'text') name: Receive["text"],
         @param("type", 'text', { type: 'text', data: { text: '' } }, true) type: Receive["text"],
@@ -287,7 +240,7 @@ export class wiki {
         }
         return '操作成功';
     }
-    @runcod(['方舟输入', '输入'], `输入文本`)
+    @runcod(['输入', '输入'], `输入文本`)
     async input(
         @param("输入文本", 'text') text: Receive["text"],
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
@@ -348,7 +301,7 @@ export class wiki {
         }
     }
 
-    @runcod(['方舟点击', '点击'], `创建一个点击步骤`)
+    @runcod(['点击', '点击'], `创建一个点击步骤`)
     async rig2(
         @param("点击文本", 'text') targetText: Receive["text"],
         @param("x偏移", 'text', { type: 'text', data: { text: '' } }, true) x1: Receive["text"],
@@ -531,7 +484,7 @@ export class wiki {
         }
         return str ?? '步骤集不存在';
     }
-    @runcod(['方舟状态', '截图', '状态'], `当前方舟运行状态`)
+    @runcod(['状态', '截图', '状态'], `当前方舟运行状态`)
     async tu(
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
     ) {
@@ -585,8 +538,8 @@ export class wiki {
     }
     @runcod(['红点识别', '点击'], `根据点击的红点识别参数`)
     async recognizeText(@param("识别图片", 'image') image: Receive["image"],
-                        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
-    ) { 
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
         if (!image?.data?.url) {
             return '图片不能为空';
         }

+ 114 - 0
src/resources/skd/building.html

@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>基建信息</title>
+    <style>
+        body {
+            background-color: #1a1a1a;
+            color: #e0e0e0;
+            margin: 0;
+            padding: 20px;
+            font-family: Arial, sans-serif;
+        }
+        .header {
+            text-align: center;
+            margin-bottom: 30px;
+            padding: 15px 0;
+            border-bottom: 1px solid #333;
+        }
+        .header h1 {
+            color: #ffffff;
+            margin: 0;
+            font-size: 24px;
+            font-weight: 600;
+        }
+        .building-container {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 20px;
+            justify-content: center;
+        }
+        .building-card {
+            background-color: #2d2d2d;
+            border-radius: 12px;
+            padding: 20px;
+            width: calc(50% - 20px);
+            box-sizing: border-box;
+            box-shadow: 0 4px 10px rgba(0,0,0,0.2);
+            border: 1px solid #3a3a3a;
+        }
+        .card-title {
+            font-size: 16px;
+            color: #bbbbbb;
+            margin-bottom: 10px;
+            display: flex;
+            align-items: center;
+        }
+        .card-value {
+            font-size: 22px;
+            font-weight: bold;
+            color: #ffffff;
+            display: flex;
+            align-items: center;
+        }
+        .progress-bar {
+            height: 8px;
+            background-color: #3a3a3a;
+            border-radius: 4px;
+            margin-top: 10px;
+            overflow: hidden;
+        }
+        .progress-fill {
+            height: 100%;
+            background-color: #4CAF50;
+            border-radius: 4px;
+            width: 100%;
+        }
+    </style>
+</head>
+<body>
+    <div class="header">
+        <h1>基建信息</h1>
+    </div>
+    
+    <div class="building-container">
+        <!-- 贸易站 -->
+        <div class="building-card">
+            <div class="card-title">订单进程</div>
+            <div class="card-value">{{data.tradings.now}}/{{data.tradings.max}}</div>
+        </div>
+        
+        <!-- 制造站 -->
+        <div class="building-card">
+            <div class="card-title">制造进度</div>
+            <div class="card-value">{{data.manufactures.now}}/{{data.manufactures.max}}</div>
+        </div>
+        
+        <!-- 宿舍 -->
+        <div class="building-card">
+            <div class="card-title">休息进度</div>
+            <div class="card-value">{{data.dormitories.now}}/{{data.dormitories.max}}</div>
+        </div>
+        
+        <!-- 会客室 -->
+        <div class="building-card">
+            <div class="card-title">线索进度</div>
+            <div class="card-value">{{data.board.now}}/{{data.board.max}}</div>
+        </div>
+        
+        <!-- 疲劳干员 -->
+        <div class="building-card">
+            <div class="card-title">疲劳干员</div>
+            <div class="card-value">{{data.tiredChars.now}}</div>
+        </div>
+        
+        <!-- 人力 -->
+        <div class="building-card">
+            <div class="card-title">无人机</div>
+            <div class="card-value">{{data.labor.now}}/{{data.labor.max}}</div>
+        </div>
+    </div>
+</body>
+</html>

+ 219 - 0
src/resources/skd/jczl.html

@@ -0,0 +1,219 @@
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>集成战略</title>
+    <style>
+        :root {
+            --bg-primary: #121212;
+            --bg-secondary: #1e1e1e;
+            --bg-tertiary: #2d2d2d;
+            --text-primary: #e0e0e0;
+            --text-secondary: #9e9e9e;
+            --accent-success: #4caf50;
+            --accent-failure: #f44336;
+            --accent-primary: #bb86fc;
+            --border-radius: 8px;
+            --shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+            --transition: all 0.3s ease;
+        }
+
+        body {
+            background-color: var(--bg-primary);
+            color: var(--text-primary);
+            font-family: 'Segoe UI', Arial, sans-serif;
+            margin: 0;
+            padding: 20px;
+            line-height: 1.6;
+        }
+
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+            padding: 0 15px;
+        }
+
+        h1 {
+            color: var(--accent-primary);
+            text-align: center;
+            margin: 20px 0 30px;
+            padding-bottom: 15px;
+            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+            font-weight: 300;
+            font-size: 2rem;
+        }
+
+        /* 统计卡片样式 */
+        .stats-card {
+            background: linear-gradient(135deg, var(--bg-secondary), var(--bg-tertiary));
+            border-radius: var(--border-radius);
+            padding: 25px;
+            margin-bottom: 30px;
+            box-shadow: var(--shadow);
+            transition: var(--transition);
+        }
+
+        .stats-card:hover {
+            transform: translateY(-5px);
+            box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
+        }
+
+        .stats-card p {
+            margin: 0;
+            font-size: 1.1rem;
+            line-height: 1.8;
+        }
+
+        .stats-highlight {
+            color: var(--accent-primary);
+            font-weight: 500;
+            padding: 0 4px;
+        }
+
+        /* 表格样式 */
+        .transaction-table {
+            width: 100%;
+            border-collapse: separate;
+            border-spacing: 0;
+            border-radius: var(--border-radius);
+            overflow: hidden;
+            box-shadow: var(--shadow);
+        }
+
+        .transaction-table th {
+            background-color: var(--bg-tertiary);
+            color: var(--text-primary);
+            padding: 14px 15px;
+            text-align: left;
+            font-weight: 500;
+            text-transform: uppercase;
+            font-size: 0.85rem;
+            letter-spacing: 0.5px;
+        }
+
+        .transaction-table td {
+            background-color: var(--bg-secondary);
+            padding: 14px 15px;
+            border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+            transition: var(--transition);
+        }
+
+        .transaction-table tr:last-child td {
+            border-bottom: none;
+        }
+
+        .transaction-table tr:hover td {
+            background-color: rgba(255, 255, 255, 0.05);
+            transform: translateX(5px);
+        }
+
+        /* 状态标签样式 */
+        .status-badge {
+            display: inline-block;
+            padding: 4px 10px;
+            border-radius: 20px;
+            font-size: 0.8rem;
+            font-weight: 500;
+            text-transform: uppercase;
+            letter-spacing: 0.5px;
+        }
+
+        .status-success {
+            background-color: rgba(76, 175, 80, 0.2);
+            color: var(--accent-success);
+            border: 1px solid rgba(76, 175, 80, 0.3);
+        }
+
+        .status-failure {
+            background-color: rgba(244, 67, 54, 0.2);
+            color: var(--accent-failure);
+            border: 1px solid rgba(244, 67, 54, 0.3);
+        }
+
+        /* 干员列表样式 */
+        .operators-list {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 6px;
+        }
+
+        .operator-tag {
+            background-color: rgba(255, 255, 255, 0.08);
+            padding: 2px 8px;
+            border-radius: 4px;
+            font-size: 0.85rem;
+            white-space: nowrap;
+        }
+
+        /* 响应式设计 */
+        @media (max-width: 768px) {
+            .container {
+                padding: 0 10px;
+            }
+
+            h1 {
+                font-size: 1.5rem;
+            }
+
+            .stats-card {
+                padding: 15px;
+            }
+
+            .transaction-table th,
+            .transaction-table td {
+                padding: 10px 8px;
+                font-size: 0.9rem;
+            }
+
+            /* 在小屏幕上隐藏部分列 */
+            .transaction-table th:nth-child(4),
+            .transaction-table td:nth-child(4) {
+                display: none;
+            }
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h1>集成战略</h1>
+        <!-- 统计卡片 -->
+        <div class="stats-card">
+            <p>你在<span class="stats-highlight">{{data.history.mode}}</span>最高层数为<span class="stats-highlight">{{data.history.modeGrade}}</span>,
+            最高得分<span class="stats-highlight">{{data.history.score}}</span>, 当前等级<span class="stats-highlight">{{data.history.bpLevel}}</span></p>
+        </div>
+        <!-- 记录表格 -->
+        <table class="transaction-table">
+            <thead>
+                <tr>
+                    <th>集成模式</th>
+                    <th>分队名称</th>
+                    <th>挑战层数</th>
+                    <th>干员</th>
+                    <th>积分</th>
+                    <th>结果</th>
+                </tr>
+            </thead>
+            <tbody>
+                {{each data.history.records}}
+                <tr>
+                    <td>{{$value.mode}}</td>
+                    <td>{{$value.band.name}}</td>
+                    <td>{{$value.modeGrade}}</td>
+                    <td>
+                        <div class="operators-list">
+                            {{$value.lastChars.map((item) => item.name).join(',')}}
+                        </div>
+                    </td>
+                    <td>{{$value.score}}</td>
+                    <td>
+                        <span class="status-badge {{$value.success ? 'status-success' : 'status-failure'}}">
+                            {{$value.success ? '成功' : '失败'}}
+                        </span>
+                    </td>
+                </tr>
+                {{/each}}
+            </tbody>
+        </table>
+    </div>
+</body>
+</html>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/resources/skd/jczl.json


+ 49 - 0
src/resources/skd/kj.html

@@ -0,0 +1,49 @@
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>消费记录</title>
+    <style>
+        body { background-color: #1a1a1a; color: #e0e0e0; font-family: Arial, sans-serif; margin: 0; padding: 20px; }
+        .container { max-width: 1200px; margin: 0 auto; }
+        h1 { color: #ffffff; text-align: center; margin-bottom: 30px; padding-bottom: 15px; border-bottom: 1px solid #333; }
+        .transaction-table { width: 100%; border-collapse: collapse; }
+        .transaction-table th { background-color: #2d2d2d; color: #ffffff; padding: 12px 15px; text-align: left; }
+        .transaction-table td { background-color: #242424; padding: 12px 15px; border-bottom: 1px solid #333; }
+        .transaction-table tr:hover td { background-color: #333; transition: background-color 0.3s; }
+        .platform-badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 12px; color: white; }
+        .platform-ios { background-color: #007aff; }
+        .platform-android { background-color: #34c759; }
+        .platform-other { background-color: #8e8e93; }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h1>消费记录</h1>
+        <table class="transaction-table">
+            <thead>
+                <tr>
+                    <th>平台</th>
+                    <th>金额</th>
+                    <th>商品名称</th>
+                </tr>
+            </thead>
+            <tbody>
+                {{each data}}<tr>
+                    <td>
+                        {{if $value.platform === 0}}
+                            <span class="platform-badge platform-ios">iOS</span>
+                        {{else if $value.platform === 1}}
+                            <span class="platform-badge platform-android">Android</span>
+                        {{else}}
+                            <span class="platform-badge platform-other">其他</span>
+                        {{/if}}
+                    </td>
+                    <td>{{$value.amount/100}} 元</td>
+                    <td>{{$value.productName}}</td>
+                </tr>{{/each}}
+            </tbody>
+        </table>
+    </div>
+</body>
+</html>

+ 141 - 0
src/resources/skd/userinfo.html

@@ -0,0 +1,141 @@
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Document</title>
+    <style>
+        body {
+            background-color: #1a1a1a;
+            color: #e0e0e0;
+            margin: 0;
+            padding: 20px;
+            font-family: Arial, sans-serif;
+        }
+        .container {
+            display: flex;
+            gap: 20px;
+            padding: 20px;
+            background-color: #222222;
+            border-radius: 16px;
+            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
+        }
+        .left-section {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+        }
+        .avatar {
+            width: 120px;
+            height: 120px;
+            border-radius: 50%;
+            object-fit: cover;
+            border: 3px solid #444;
+            box-shadow: 0 2px 15px rgba(0,0,0,0.4);
+        }
+        .name {
+            margin-top: 10px;
+            font-size: 18px;
+            font-weight: bold;
+            color: #ffffff;
+        }
+        .right-section {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            gap: 15px;
+        }
+        .card {
+            background: #2d2d2d;
+            border-radius: 12px;
+            padding: 15px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
+            border: 1px solid #3a3a3a;
+        }
+        .progress-card {
+            height: 80px;
+            display: flex;
+            gap: 10px;
+            align-items: center;
+            justify-content: center;
+            font-size: 16px;
+            color: #bbbbbb;
+        }
+        .bottom-cards {
+            display: flex;
+            gap: 15px;
+        }
+        .stat-card {
+            flex: 1;
+            height: 80px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+        }
+        .stat-label {
+            font-size: 14px;
+            color: #999999;
+            margin-bottom: 5px;
+        }
+        .stat-value {
+            font-size: 24px;
+            font-weight: bold;
+            color: #ffffff;
+        }
+        .tag {
+            font-size: 12px;
+            color: #777777;
+            margin-top: 5px;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <!-- 左侧头像和名字 -->
+        <div class="left-section">
+            <div id="avatar"><img src="{{data.avatar}}" alt="头像" class="avatar"></div>
+            <div id="name" class="name">{{data.name}}</div>
+            <div class="tag">注册时间: {{data.registerTs}}</div>
+        </div>
+
+        <!-- 右侧卡片区域 -->
+        <div class="right-section">
+            <!-- 作战进度卡片 -->
+            <div class="progress-card ">
+                <div class="card stat-card">
+                    <div class="stat-label">等级</div>
+                    <div class="stat-value">{{data.level}}</div>
+                </div>
+                <br>
+                <div class="card stat-card">
+                    <div class="stat-label">理智</div>
+                    <div class="stat-value">{{data.ap.now}}/{{data.ap.max}}</div>
+                </div>
+                <br>
+                <div class="card stat-card">
+                    <div class="stat-label">作战进度</div>
+                    <div class="stat-value">
+                        {{if data.mainStageProgress == ''}}已完成{{else}}{{data.mainStageProgress}}{{/if}}
+                    </div>
+                </div>
+            </div>
+            <br>
+            <!-- 干员数量和家具数量卡片 -->
+            <div class="bottom-cards">
+                <div class="card stat-card">
+                    <div class="stat-label">干员数量</div>
+                    <div class="stat-value">{{data.chars}}</div>
+                </div>
+                <div class="card stat-card">
+                    <div class="stat-label">皮肤数量</div>
+                    <div class="stat-value">{{data.skins}}</div>
+                </div>
+                <div class="card stat-card">
+                    <div class="stat-label">家具数量</div>
+                    <div class="stat-value">{{data.furniture}}</div>
+                </div>
+            </div>
+        </div>
+    </div>
+</body>
+</html>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä