Pārlūkot izejas kodu

feat(Puppeteer): 新增Puppeteer启动参数以允许文件访问

feat(config): 添加权限配置文件的加载和保存功能

refactor(Permission): 重构权限保存逻辑以使用新的配置文件方法

feat(prop): 新增道具列表和用户道具仓库的HTML模板

refactor(test): 重构测试插件中的道具功能,支持设置群成员称号

refactor(prop): 重构道具插件,支持HTML模板渲染和道具ID操作
枫林 3 mēneši atpakaļ
vecāks
revīzija
5130c14d40

+ 0 - 91
src/config/permission.yml

@@ -1,91 +0,0 @@
-enable: true
-admins:
-  - '2180323481'
-  - '1814872986'
-users:
-  '211249983':
-    plugins:
-      test:
-        enable: true
-        commands:
-          help: true
-          param: true
-      testupload:
-        commands:
-          help: true
-          param: true
-      downloadPlugins:
-        commands:
-          download: true
-          help: true
-          plugins: true
-          logs: true
-          downloadlog: true
-      saku:
-        commands:
-          ping: true
-          help: true
-          图: true
-      sakulass:
-        commands:
-          图: true
-          help: true
-      PluginsFile:
-        commands:
-          help: true
-          logs: true
-          downloadlog: true
-  '914085636':
-    plugins:
-      saku:
-        commands:
-          help: true
-      reload:
-        commands:
-          help: true
-      downloadPlugins:
-        commands:
-          help: true
-          plugins: true
-          download: true
-          logs: true
-          downloadlog: true
-      sakulass:
-        commands:
-          help: true
-          图: true
-  '1051027747':
-    plugins:
-      sakulass:
-        commands:
-          图: true
-          一言: true
-          help: true
-      ecomony:
-        commands:
-          info: true
-          add: true
-          help: true
-      test:
-        commands:
-          remove: true
-          help: true
-          param: true
-      Botlog:
-        commands:
-          logs: true
-          downloadlog: true
-      Prop:
-        commands:
-          list: true
-          help: true
-          buy: true
-          use: true
-          my: true
-  default:
-    plugins:
-      test:
-        enable: true
-        commands:
-          help: true
-          param: false

+ 2 - 2
src/lib/Permission.ts

@@ -1,4 +1,4 @@
-import { PermissionConfig, saveConfig } from "./config.js";
+import { PermissionConfig, saveConfig, savePermission } from "./config.js";
 import botlogger from "./logger.js";
 export const IsAdmin = async function (id:number){return await PermissionConfig.admins.some((admin: string) => admin === String(id)) }
 export async function IsPermission(id: number, plugin: string, command: string): Promise<boolean> {
@@ -65,7 +65,7 @@ async function saveNewCommandConfig(id: number, plugin: string, command: string)
         // 设置新命令默认权限
         PermissionConfig.users[id].plugins[plugin].commands[command] = true;
         
-        saveConfig('permission', PermissionConfig);
+        savePermission('permission', PermissionConfig);
         botlogger.info(`自动创建 [${id}] 的 ${plugin}.${command} 命令权限`);
     } catch (error) {
         botlogger.error(`配置保存失败:${error instanceof Error ? error.stack : error}`);

+ 5 - 1
src/lib/Puppeteer.ts

@@ -10,7 +10,11 @@ export class HtmlImg {
         if (!this.browser) {
             const options: PuppeteerLaunchOptions = {
                 headless: true,
-                args: ['--no-sandbox', '--disable-setuid-sandbox']
+                args: [
+                    '--no-sandbox',
+                    '--disable-setuid-sandbox',
+                    '--allow-file-access-from-files' // 新增参数
+                ]
             };
             this.browser = await puppeteer.launch(options);
         }

+ 9 - 1
src/lib/config.ts

@@ -13,7 +13,15 @@ export function saveConfig(file: string, data: any): void {
     const configPath = path.join(__dirname, `../config/${file}.yml`);  // 保持源码与编译后一致
     fs.writeFileSync(configPath, yaml.dump(data));
 }
+export async function loadPermission(){
+    const configPath = path.join(__dirname, `../../data/permission.yml`);  // 保持源码与编译后一致
+    return await yaml.load(fs.readFileSync(configPath, 'utf8')) as any;
+}
+export function savePermission(file: string, data: any): void {
+    const configPath = path.join(__dirname, `../../data/permission.yml`);  // 保持源码与编译后一致
+    fs.writeFileSync(configPath, yaml.dump(data));
+}
 export const Botconfig = await loadConfig('bot');
-export const PermissionConfig = await loadConfig('permission');
+export const PermissionConfig = await loadPermission();
 export const load = await loadConfig('load')
 export const economy = await loadConfig('economy')

+ 92 - 17
src/plugins/prop.ts

@@ -4,6 +4,9 @@ import { param, plugins, runcod } from "../lib/decorators.js";
 import { addProp, getuserProp, Props, reduceProp } from "../lib/prop.js";
 import { GroupMessage, PrivateFriendMessage, PrivateGroupMessage } from "node-napcat-ts";
 import { removeCoins } from "../lib/economy.js";
+import path from "path";
+import { fileURLToPath } from "url";
+import { Prop } from "../interface/prop.js";
 
 @plugins({
     easycmd: true,//是否启用简易命令,启用将将命令注册为<命令名称>,不启用将注册为#<插件名称> <命令名称>
@@ -16,27 +19,27 @@ import { removeCoins } from "../lib/economy.js";
         description: "显示道具插件" //帮助信息描述
     }
 })
-export class Prop {
+export class Propplu {
     @runcod(["use", "使用道具"],"使用道具" )
     async useprop(
-        @param("道具名称", ParamType.String) propName: string,
+        @param("道具Id", ParamType.String) propId: string,
         @param("QQ", ParamType.Number) userId: string,
         @param("数量", ParamType.Number, 1, true) Num: number,
         @param("道具参数",ParamType.String,"",true) propparam:string,
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
     ): Promise<any> {
         const userProp = await getuserProp(context?.sender?.user_id.toString()??"0") || [];
-        let findprop = userProp.find((prop) => prop.propName == propName);
+        let findprop = userProp.find((prop) => prop.propId == propId);
         if (!findprop) {
-            return `你没有${propName}道具`;
+            return this.tl(`你没有${propId}道具`,'error',`你没有${propId}道具`)
         }
         if (findprop.Num < Num) {
-            return "道具数量不足";
+            return this.tl(`道具数量不足`,'error',`道具数量不足`)
         }
         Props.forEach((prop) => {
-            if (prop.propName === propName) {
+            if (prop.propId === propId) {
                 if(prop.maxuse<Num){
-                   throw new Error(`该道具允许最大使用数量为${prop.maxuse}超过最大使用数量`) 
+                    return this.tl(`该道具允许最大使用数量为${prop.maxuse}超过最大使用数量`,'error',`该道具允许最大使用数量为${prop.maxuse}超过最大使用数量`)
                 }
             }    
         });
@@ -46,14 +49,14 @@ export class Prop {
                 let classConstructor;
                 let propid=''
                 Props.forEach((prop) => {
-                    if (prop.propName==propName) {
+                    if (prop.propId==propId) {
                         fn = prop.fn;
                         classConstructor = prop.classConstructor
                         propid= prop.propId
                     }
                 })
                 if (await reduceProp(context?.sender?.user_id.toString()??"0", propid, 1)){
-                    const result = (fn as any).call(classConstructor, userId, propparam);
+                    const result = (fn as any).call(classConstructor, userId, propparam,context);
                     if (result) {
                         return result;
                     }
@@ -61,7 +64,7 @@ export class Prop {
             }
         } catch (error: any) {
             botlogger.error(error);
-            return `道具使用失败:${error.message}`;
+            return this.tl(`道具使用失败:${error.message}`,'error',`道具使用失败:${error.message}`)
         }
         return "道具使用失败";
     }
@@ -69,38 +72,110 @@ export class Prop {
     @runcod(["list", "道具列表"],"道具列表")
     async getprop(){
         let s ='道具列表:\n'
+        let p: Prop[] =[]
         Props.forEach((prop) => {
-            s += `名称:${prop.propName}---描述:${prop.describe}---价格:${prop.price}\n`;
+            p.push(prop)
+            s += `名称:${prop.propName}[${prop.propId}]---描述:${prop.describe}---价格:${prop.price}\n`;
         })
-        return s;
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        return {
+            Prs:p,
+            template:{
+            enabled: true,
+            sendText: false,
+            path: path.resolve(__dirname, '..','resources', 'prop',`getprop.html`),//模版路径,推荐按规范放置在resources目录下
+            render: {//浏览器默认参数设置,用于打开浏览器的设置
+                width: 800, // 模板宽度
+                type: 'png',// 模板类型
+                quality: 100,// 模板质量
+                fullPage: false,// 是否全屏
+                background: true// 是否背景
+            },
+            
+        },
+        toString(){
+            return s;
+        }
     }
+    
+}
 
     @runcod(["my", "道具" ,"我的道具"], "我的道具")
     async userprop(context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage){
         const props = (await getuserProp(context?.sender?.user_id?.toString()??"0"));
+        let p: Prop[] =[]
         let s ='道具列表:\n'
         props.forEach((prop) => {
+            Props.forEach((Allprop) => {
+                if(Allprop.propId === prop.propId){
+                    Allprop.Num=prop.Num
+                    p.push(Allprop) 
+                }
+            })
             s += `名称:${prop.propName}---描述:${prop.describe}---数量:${prop.Num}\n`;
         })
-        return s;
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        return {
+                Prs:p,
+                nickname:context?.sender?.nickname??"未知",
+                template:{
+                enabled: true,
+                sendText: false,
+                path: path.resolve(__dirname, '..','resources', 'prop',`userprop.html`),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 800, // 模板宽度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true// 是否背景
+                },
+                
+            },
+            toString(){
+                return s;
+            }
+        }
     }
     @runcod(["buy","买"],"购买道具")
     async buyprop(
-        @param("道具名称", ParamType.String) propName: string,
+        @param("道具Id", ParamType.String) propId: string,
         @param("数量", ParamType.Number, 1, true) Num: number,
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
     ){
         if(Num<=0){
-            return('道具数量不能小于0')
+            return this.tl('道具数量不能小于0','error','道具数量不能小于0')
         }
         let res = ''
         Props.forEach(async (prop) => {
-            if(prop.propName===propName){
+            if(prop.propId===propId){
                 removeCoins(context?.sender?.user_id?.toString(),prop?.price??0 * Num,`购买道具${prop?.propName??'未知'}`)
                 addProp(context?.sender?.user_id?.toString(),prop?.propId,Num)
                 res = `购买${prop.propName}成功!消费${prop.price * Num}!`
             }
         })
-        return res ;
+        return this.tl(res,'success',res)
+          
+    }
+    tl ( msg:string, type:'success'|'error',text:string) {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        return {
+            msgtype: 'success',
+            ecomsg: msg,
+            template: {
+                enabled: true,
+                sendText: false,
+                path: path.resolve(__dirname, '..', 'resources', 'ecomony', 'msg.html'),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 800, // 模板宽度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true// 是否背景
+                }
+            },
+            toString() { //重写toString方法,用于返回文本内容,启用sendText时将发送文本内容,不启用时将发送图片内容,图片发送失败时发送文字内容
+                return text;
+            }
+        }   
     }
 }

+ 26 - 6
src/plugins/test.ts

@@ -1,5 +1,6 @@
 //PLUGIN test.ts
 
+  
 import { coins, param, plugins, runcod, schedule } from '../lib/decorators.js';
 import path from 'path';
 import 'reflect-metadata';
@@ -8,7 +9,17 @@ import { qqBot } from '../app.js';
 import botlogger from '../lib/logger.js';
 import { ParamType } from '../interface/plugin.js';
 import { prop } from '../lib/prop.js';
-
+import * as fs from 'fs'
+import { GroupMessage, PrivateFriendMessage, PrivateGroupMessage } from 'node-napcat-ts/dist/Interfaces.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: "测试插件", //插件名称,用于显示在菜单中
@@ -86,17 +97,26 @@ export class test {
     }
     @prop(
         "testProp",//道具id
-        "测试道具",//道具名称
+        "称号卡",//道具名称
         1,//道具最大使用数量
-        "测试使用道具",//道具描述
-        "",//道具图片
+        "实例道具,使用后可以给指定群友设置称号,需要管理权限",//道具描述
+        await convertImageToBase64("/Users/fenglin/Desktop/botQQ/src/resources/test/Prop/test.jpg"),//道具图片
         1//道具价格
     )
     async test(
         userId:string,
-        propparam:string
+        propparam:string,
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
     ): Promise<any>{
-        return `成功对用户${userId}使用测试道具--接受道具参数${propparam}`
+        debugger
+        if (context?.message_type === 'group') {
+            await qqBot.set_group_special_title({
+                group_id:Number(context.group_id),
+                user_id:Number(userId),
+                special_title:propparam
+            })
+        }
+        return `操作成功!`
     }
 
 }

+ 78 - 0
src/resources/prop/getprop.html

@@ -0,0 +1,78 @@
+<div class="prop-grid">
+    {{each Prs}}
+    <div class="prop-card">
+      <img src="{{$value.img}}" class="prop-image" alt="道具图片">
+      <div class="prop-body">
+        <h3 class="prop-name">🤩{{$value.propName}}[{{$value.propId}}]🤩</h3>
+        <p class="prop-describe">📖{{$value.describe}}</p>
+        <div class="prop-price">
+          💰{{$value.price}}
+        </div>
+      </div>
+    </div>
+    {{/each}}
+</div>
+
+<style>
+.prop-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  padding: 15px;
+}
+
+.prop-card {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+  width: 220px;
+  transition: transform 0.2s;
+}
+
+.prop-image {
+  width: 100%;
+  height: 180px;
+  object-fit: cover;
+  border-radius: 12px 12px 0 0;
+}
+
+.prop-body {
+  padding: 15px;
+  text-align: center;
+}
+
+.prop-name {
+  margin: 12px 0;
+  color: #2d2d2d;
+  font-size: 18px;
+}
+
+.prop-describe {
+  color: #666;
+  font-size: 14px;
+  line-height: 1.5;
+  min-height: 60px;
+}
+
+.prop-price span {
+  justify-content: flex-end;
+  background: transparent;
+  color: #ff5722;
+  font-size: 18px;
+  padding: 6px 12px;
+  border-radius: 4px;
+  margin: 10px 0;
+  gap: 8px;
+}
+
+coin-icon {
+  width: 20px;
+  height: 20px;
+  order: 1; /* 图标放在价格右侧 */
+}
+
+.prop-card:hover .prop-price {
+  transform: scale(1.03);
+  background: rgba(255, 87, 34, 0.05);
+}
+</style>

+ 79 - 0
src/resources/prop/userprop.html

@@ -0,0 +1,79 @@
+<h3>{{nickname}}道具仓库</h3>
+<div class="prop-grid">
+    {{each Prs}}
+    <div class="prop-card">
+      <img src="{{$value.img}}" class="prop-image" alt="道具图片">
+      <div class="prop-body">
+        <h3 class="prop-name">🤩{{$value.propName}}[{{$value.propId}}]🤩</h3>
+        <p class="prop-describe">📖{{$value.describe}}</p>
+        <div class="prop-price">
+          库存:{{$value.Num}} 个
+        </div>
+      </div>
+    </div>
+    {{/each}}
+</div>
+
+<style>
+.prop-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  padding: 15px;
+}
+
+.prop-card {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+  width: 220px;
+  transition: transform 0.2s;
+}
+
+.prop-image {
+  width: 100%;
+  height: 180px;
+  object-fit: cover;
+  border-radius: 12px 12px 0 0;
+}
+
+.prop-body {
+  padding: 15px;
+  text-align: center;
+}
+
+.prop-name {
+  margin: 12px 0;
+  color: #2d2d2d;
+  font-size: 18px;
+}
+
+.prop-describe {
+  color: #666;
+  font-size: 14px;
+  line-height: 1.5;
+  min-height: 60px;
+}
+
+.prop-price span {
+  justify-content: flex-end;
+  background: transparent;
+  color: #ff5722;
+  font-size: 18px;
+  padding: 6px 12px;
+  border-radius: 4px;
+  margin: 10px 0;
+  gap: 8px;
+}
+
+coin-icon {
+  width: 20px;
+  height: 20px;
+  order: 1; /* 图标放在价格右侧 */
+}
+
+.prop-card:hover .prop-price {
+  transform: scale(1.03);
+  background: rgba(255, 87, 34, 0.05);
+}
+</style>