diff --git a/README.md b/README.md index ae9c1f0..cfca54a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# helloworld +# JX3API AstrBot 插件模板 diff --git a/_conf_schema.json b/_conf_schema.json new file mode 100644 index 0000000..654c645 --- /dev/null +++ b/_conf_schema.json @@ -0,0 +1,36 @@ +{ + "jx3_kfjk_en": { + "description": "开服监控功能", + "type": "bool", + "default": true, + "hint": "是否启用剑网三开服监控功能。" + }, + "jx3_kfjk_time": { + "description": "开服监控循环时间", + "type": "int", + "default": 5, + "hint": "请求服务器状态的循环时间,单位秒。" + }, + "jx3_kfjk_server": { + "description": "开服监控服务器", + "type": "string", + "default": "梦江南", + "hint": "请求服务器状态的名称,要官方名称。" + }, + "jx3_kfjk_list": { + "description": "开服监控推送列表", + "type": "list", + "hint": "可以填写多个会话唯一ID。", + "items": { + "type": "string", + "description": "会话唯一ID" + }, + "default": ["default:GroupMessage:768280021","default:GroupMessage:690665795"] + }, + "jx3_kfjk_test": { + "description": "开服监控功能测试", + "type": "bool", + "default": false, + "hint": "可以手动修改服务器的状态,进行推送测试。" + } +} \ No newline at end of file diff --git a/api_config.json b/api_config.json new file mode 100644 index 0000000..a73e0cd --- /dev/null +++ b/api_config.json @@ -0,0 +1,149 @@ +{ + "jx3_richang":{ + "url":"https://www.jx3api.com/data/active/calendar", + "method":"GET", + "description":"获取剑网三日常任务", + "params":{ + "server": "眉间雪", + "num": 0 + } + }, + "jx3_shaohua":{ + "url":"https://www.jx3api.com/data/saohua/random", + "method":"GET", + "description":"获取剑网三骚话", + "params":{ + "name": "万花" + } + }, + "jx3_jigai":{ + "url":"https://www.jx3api.com/data/skills/records", + "method":"GET", + "description":"获取剑网三技能记录", + "params":{ + } + }, + "jx3_kaifu":{ + "url":"https://www.jx3api.com/data/server/check", + "method":"GET", + "description":"剑网三开服查询", + "params":{ + "server": "梦江南" + } + }, + "aijx3_shapan":{ + "url":"https://www.jianxiachaguan.cn/api2/aijx3-jxcg/game/get-sand-table-img", + "method":"POST", + "description":"获取剑网三沙盘状态", + "params":{ + "serverName": "眉间雪" + } + }, + "aijx3_jinjia":{ + "url":"https://www.jianxiachaguan.cn/api2/aijx3-jxcg/game/get-gold", + "method":"POST", + "description":"获取剑网三某区服金价", + "params":{ + "serverName": "眉间雪" + } + }, + "aijx3_qiyu":{ + "url":"https://www.jianxiachaguan.cn/api2/aijx3-jxcg/game/get-adventure-record", + "method":"POST", + "description":"获取剑网三某区服奇遇触发记录", + "params":{ + "adventureName": "阴阳两界", + "serverName": "眉间雪" + } + }, + "aijx3_SearchData":{ + "url":"https://www.aijx3.cn/api2/aijx3-wblwg/basedata/getSearchData", + "method":"POST", + "description":"获取爱剑网三所有外观数据", + "params":{ + } + }, + "jx3box_exterior":{ + "url":"https://node.jx3box.com/api/node/v2/exterior/list", + "method":"GET", + "description":"魔盒中模糊查找剑网三的外观数据", + "params":{ + "keyword": "秃盒", + "tradable": "0", + "list_type": "0" + } + }, + "aijx3_GoodsDetail":{ + "url":"https://www.aijx3.cn/api2/aijx3-wj/goods/getGoodsDetail", + "method":"POST", + "description":"在爱剑网三中获取外观详细数据", + "params":{ + "goodsName": "秃盒" + } + }, + "aijx3_wblwg":{ + "url":"https://www.aijx3.cn/api2/aijx3-wblwg/record/queryByCondition", + "method":"POST", + "description":"在爱剑网三中获取外观详细数据", + "params":{ + "accoSeq": "", + "orderMode": 1, + "orderBy": "price_num", + "searchId": [48356], + "current": 1, + "size": 10, + "tradeStatus": "" + } + }, + "jx3box_item":{ + "url":"https://node.jx3box.com/api/node/item/search", + "method":"GET", + "description":"魔盒中模糊查找剑网三的物品数据", + "params":{ + "keyword": "守缺式", + "page": "1", + "per": "50" + } + }, + "jx3box_itemprice":{ + "url":"https://next2.jx3box.com/api/item-price/list", + "method":"GET", + "description":"交易行中在售物品数据", + "params":{ + "server": "眉间雪", + "itemIds": ["5_76650"] + } + }, + "wbl_waiguan":{ + "url":"https://trade-api.seasunwbl.com/api/buyer/goods/list", + "method":"GET", + "description":"万宝楼官方交易数据", + "params":{ + "filter[state]": "0", + "filter[role_appearance]": "浮屠明音礼盒", + "page": "1", + "size": "10", + "goods_type": "3", + "sort[price]": "1" + } + }, + "wzry_zhanji":{ + "url":"https://api.t1qq.com/api/tool/wzrr/morebattle", + "method":"GET", + "description":"王者荣耀战绩查询", + "params":{ + "key": "vBpEzoiC9z5A9c9Nn83IhLn6M9", + "id": "489048724", + "option": "1" + } + }, + "wzry_ziliao":{ + "url":"https://api.t1qq.com/api/tool/wzrr/ydtp", + "method":"GET", + "description":"王者荣耀资料查询", + "params":{ + "key": "vBpEzoiC9z5A9c9Nn83IhLn6M9", + "id": "489048724" + } + } +} \ No newline at end of file diff --git a/core/APIClient.py b/core/APIClient.py new file mode 100644 index 0000000..f7a3edb --- /dev/null +++ b/core/APIClient.py @@ -0,0 +1,150 @@ +# core/request.py +import aiohttp +import json +from aiohttp import ClientTimeout +from astrbot.api import logger + +class APIClient: + """ + API客户端类,支持GET/POST请求,同时兼容JSON和二进制数据 + """ + + def __init__(self, base_timeout=10, ssl_verify=False): + self.base_timeout = base_timeout + self.ssl_verify = ssl_verify + + async def _make_request(self, method, url, params_data=None): + timeout = ClientTimeout(total=self.base_timeout) + try: + logger.debug(f"发起 {method} 请求: {url}") + logger.debug(f"参数数据: {params_data}") + async with aiohttp.ClientSession(timeout=timeout) as session: + if method.upper() == 'GET': + async with session.get(url, params=params_data, ssl=self.ssl_verify) as response: + return await self._handle_response(response) + elif method.upper() == 'POST': + headers = {'Content-Type': 'application/json'} + async with session.post(url, json=params_data, headers=headers, ssl=self.ssl_verify) as response: + return await self._handle_response(response) + else: + logger.error(f"不支持的HTTP方法: {method}") + return None + except aiohttp.ClientError as e: + logger.error(f"请求出错: {e}") + return None + except Exception as e: + logger.error(f"未知错误: {e}") + return None + + async def _handle_response(self, response): + """ + 处理响应:支持 JSON 和二进制数据 + """ + try: + logger.debug(f"响应状态: {response.status}") + response.raise_for_status() + content_type = response.headers.get('Content-Type', '') + + if 'image' in content_type or 'octet-stream' in content_type: + # 返回二进制数据 + data = await response.read() + return data + + # 尝试解析为 JSON + try: + data = await response.json(content_type=None) + except Exception: + text = await response.text() + logger.debug(f"原始文本响应: {text}") + try: + data = json.loads(text) + except json.JSONDecodeError: + logger.error("无法解析为 JSON 数据") + return None + + logger.debug(f"响应数据: {data}") + return self._check_response_data(data) + except aiohttp.ClientError as e: + logger.error(f"HTTP错误: {e}") + return None + except Exception as e: + logger.error(f"未知错误: {e}") + return None + + def _check_response_data(self, data): + """ + 检查 API 返回 JSON 数据 + """ + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError: + logger.error("响应数据是无效的JSON字符串") + return None + + if data and isinstance(data, dict) and 'code' in data: + if data.get('code') not in [200, "0", 0, 1]: + logger.error(f"API返回错误:{data.get('code', '未知状态')} {data.get('msg', '未知错误')}") + return None + elif not data: + logger.error("API返回空数据") + return None + + return data + + async def post(self, api_url, params_data=None, outdata=None): + data = await self._make_request('POST', api_url, params_data) + if data is None: + return None + if isinstance(data, bytes): + return data + if not outdata: + return data + return data.get(outdata, {}) + + async def get(self, api_url, params_data=None, outdata=None): + data = await self._make_request('GET', api_url, params_data) + if data is None: + return None + if isinstance(data, bytes): + return data + if not outdata: + return data + return data.get(outdata, {}) + + async def all_pages(self, http, api_url, params_data=None, outdata: str = "", listdata: str = "list", max_pages: int = 10): + all_data = [] + current_page = 1 + while True: + params = params_data.copy() if params_data else {} + params["page"] = str(current_page) + + if http.upper() == "POST": + data = await self.post(api_url, params, outdata) + else: + data = await self.get(api_url, params, outdata) + + if not data or isinstance(data, bytes): + # 二进制数据或者空数据,不分页 + break + + if not data.get(listdata): + break + + all_data.extend(data[listdata]) + + if max_pages and current_page >= max_pages: + break + + current_page += 1 + logger.info(f"已获取第 {current_page} 页数据") + return all_data + +# 保持原接口兼容 +async def api_data_post(api_url, params_data=None, outdata=None): + client = APIClient() + return await client.post(api_url, params_data, outdata) + +async def api_data_get(api_url, params_data=None, outdata=None): + client = APIClient() + return await client.get(api_url, params_data, outdata) diff --git a/core/AsyncMySQL.py b/core/AsyncMySQL.py new file mode 100644 index 0000000..255fae6 --- /dev/null +++ b/core/AsyncMySQL.py @@ -0,0 +1,90 @@ +import aiomysql + + +class AsyncMySQL: + def __init__(self, db_config: dict): + self.db_config = db_config + self.pool = None + + async def init_pool(self): + """初始化连接池""" + if self.pool is None: + self.pool = await aiomysql.create_pool(**self.db_config) + + async def close_pool(self): + """关闭连接池""" + if self.pool: + self.pool.close() + await self.pool.wait_closed() + self.pool = None + + async def fetch_one(self, sql: str, params=None): + """查询单条数据""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + await cursor.execute(sql, params or ()) + return await cursor.fetchone() + + async def fetch_all(self, sql: str, params=None): + """查询多条数据""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + await cursor.execute(sql, params or ()) + return await cursor.fetchall() + + async def execute(self, sql: str, params=None): + """执行 SQL(insert/update/delete)""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cursor: + await cursor.execute(sql, params or ()) + await conn.commit() + return cursor.rowcount + + async def executemany(self, sql: str, params_list): + """批量执行 SQL""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cursor: + await cursor.executemany(sql, params_list) + await conn.commit() + return cursor.rowcount + + async def truncate_table(self, table_name: str): + """清空指定表""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cursor: + sql = f"TRUNCATE TABLE `{table_name}`" + await cursor.execute(sql) + await conn.commit() + return True + + # ---------------------------------------------------------------------- + # 新增:自动生成 SQL 的增删改功能 + # ---------------------------------------------------------------------- + + async def insert_record(self, table: str, data: dict): + """插入记录:data 是 dict""" + keys = ", ".join(f"`{k}`" for k in data.keys()) + placeholders = ", ".join(["%s"] * len(data)) + sql = f"INSERT INTO `{table}` ({keys}) VALUES ({placeholders})" + return await self.execute(sql, tuple(data.values())) + + async def update_record(self, table: str, data: dict, where: dict): + """更新记录:data、where 都是 dict""" + set_clause = ", ".join(f"`{k}`=%s" for k in data.keys()) + where_clause = " AND ".join(f"`{k}`=%s" for k in where.keys()) + + sql = f"UPDATE `{table}` SET {set_clause} WHERE {where_clause}" + + params = tuple(data.values()) + tuple(where.values()) + return await self.execute(sql, params) + + async def delete_record(self, table: str, where: dict): + """删除记录:where 是 dict""" + where_clause = " AND ".join(f"`{k}`=%s" for k in where.keys()) + sql = f"DELETE FROM `{table}` WHERE {where_clause}" + return await self.execute(sql, tuple(where.values())) diff --git a/core/JX3Function.py b/core/JX3Function.py new file mode 100644 index 0000000..9b77648 --- /dev/null +++ b/core/JX3Function.py @@ -0,0 +1,536 @@ +from datetime import datetime + +from astrbot.api import logger + +from .APIClient import APIClient +from .AsyncMySQL import AsyncMySQL +from .function_basic import load_template,extract_field,flatten_field,extract_fields,gold_to_string,plot_line_chart_base64 + +class JX3Function: + def __init__(self, api_config,db: AsyncMySQL ): + self.__api = APIClient() + self.__db = db + self.__api_config = api_config + + async def richang(self,server: str = "眉间雪",num: int = 0): + """ + 日常活动 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["jx3_richang"] + #更新参数 + api_config["params"]["server"] = server + api_config["params"]["num"] = num + # 获取数据 + data = await self.__api.get(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 处理返回数据 + try: + result_msg = f"{server}-{data.get('date')}-星期{data.get('week')}\n" + result_msg += f"大战:{data.get('war')}\n" + result_msg += f"战场:{data.get('battle')}\n" + result_msg += f"阵营:{data.get('orecar')}\n" + result_msg += f"宗门:{data.get('school')}\n" + result_msg += f"驰援:{data.get('rescue')}\n" + result_msg += f"画像:{data.get('draw')}\n" + result_msg += f"宠物福缘:{data.get('luck')[0]},{data.get('luck')[1]},{data.get('luck')[2]}\n" + return_data["data"] = result_msg + return_data["code"] = 200 + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + + async def shapan(self,server: str = "梦江南"): + """ + 区服沙盘 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["aijx3_shapan"] + #更新参数 + api_config["params"]["serverName"] = server + # 获取数据 + data = await self.__api.post(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 处理返回数据 + try: + return_data["data"] = data.get("picUrl") + return_data["code"] = 200 + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + async def kaifu(self,server: str = "梦江南"): + """ + 开服状态查询 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["jx3_kaifu"] + #更新参数 + api_config["params"]["server"] = server + # 获取数据 + data = await self.__api.get(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 处理返回数据 + try: + status = data.get("status") + status_time = datetime.fromtimestamp(data.get("time")).strftime("%Y-%m-%d %H:%M:%S") + if status == 1: + status_str = f"{server}服务器已开服,快冲,快冲!\n开服时间:{status_time}" + status_bool = True + else: + status_str = f"{server}服务器当前维护中,等会再来吧!\n维护时间:{status_time}" + status_bool = False + return_data["status"] = status_bool + return_data["data"] = status_str + return_data["code"] = 200 + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + async def shaohua(self): + """ + 骚话 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["jx3_shaohua"] + # 获取数据 + data = await self.__api.get(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 处理返回数据 + try: + return_data["data"] = data.get("text") + return_data["code"] = 200 + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + + async def jigai(self): + """ + 技改记录 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["jx3_jigai"] + # 获取数据 + data = await self.__api.get(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 处理返回数据 + try: + result_msg = f"剑网三最近技改\n" + for i, item in enumerate(data[:1], 1): + result_msg += f"{i}. {item.get('title', '无标题')}\n" + result_msg += f"时间:{item.get('time', '未知时间')}\n" + result_msg += f"链接:{item.get('url', '无链接')}\n\n" + return_data["data"] = result_msg + return_data["code"] = 200 + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + + async def jinjia(self, server: str = "眉间雪"): + """ + 区服金价 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["aijx3_jinjia"] + #更新参数 + api_config["params"]["serverName"] = server + # 获取数据 + data = await self.__api.post(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + chart_data = data + data = data[:15] + # 加载模板 + try: + return_data["temp"] = load_template("jinjia.html") + except FileNotFoundError as e: + logger.error(f"加载模板失败: {e}") + return_data["msg"] = "系统错误:模板文件不存在" + return return_data + # 生成折线图 + try: + chart_base64 = plot_line_chart_base64(chart_data, "date", "priceWanbaolou", "万宝楼金价走势",True) + except Exception as e: + logger.error(f"生成折线图失败: {e}") + return_data["msg"] = "系统错误:生成折线图失败" + return return_data + # 准备模板渲染数据 + try: + + return_data["data"] = { + "items": data, + "server": api_config["params"]["serverName"], + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "img": chart_base64 + } + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "系统错误:模板渲染数据准备失败" + return return_data + return_data["code"] = 200 + return return_data + + + async def qiyu(self, adventureName: str = "阴阳两界", serverName: str = "眉间雪"): + """ + 区服奇遇 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["aijx3_qiyu"] + #更新参数 + api_config["params"]["adventureName"] = adventureName + api_config["params"]["serverName"] = serverName + # 获取数据 + data = await self.__api.post(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 格式化时间 + for item in data: + if "time" in item: + item["time"] = datetime.fromtimestamp(item["time"]/1000).strftime("%Y-%m-%d %H:%M:%S") + # 加载模板 + try: + return_data["temp"] = load_template("qiyuliebiao.html") + except FileNotFoundError as e: + logger.error(f"加载模板失败: {e}") + return_data["msg"] = "系统错误:模板文件不存在" + return return_data + # 准备模板渲染数据 + try: + return_data["data"] = { + "items": data, + "server": serverName, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ,"qiyuname": adventureName + } + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "系统错误:模板渲染数据准备失败" + return return_data + return_data["code"] = 200 + return return_data + + + async def SearchData(self): + """ + 外观数据插入数据库 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["aijx3_SearchData"] + # 获取数据 + data = await self.__api.post(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 提取数据 + try: + extracted_data = flatten_field(data, "dataModels") + except FileNotFoundError as e: + logger.error(f"提取数据失败: {e}") + return_data["msg"] = "提取指定数据字段失败" + return return_data + # 准备SQL和批量数据 + sql = """ + INSERT INTO searchdata + (typeName, name, showName, picUrl, searchId, searchDescType) + VALUES (%s, %s, %s, %s, %s, %s) + """ + values_list = [ + ( + item['typeName'], + item['name'], + item['showName'], + item['picUrl'], + item['searchId'], + item['searchDescType'] + ) + for item in extracted_data + ] + # 插入数据 + try: + await self.__db.truncate_table("searchdata") + await self.__db.executemany(sql, values_list) + except FileNotFoundError as e: + logger.error(f"数据插入失败: {e}") + return_data["msg"] = "数据插入数据库失败" + return return_data + return_data["msg"] = f"成功批量插入 {len(extracted_data)} 条数据!" + return_data["code"] = 200 + return return_data + + + async def wujia(self,Name: str = "秃盒"): + """ + 物价查询 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["jx3box_exterior"] + #更新参数 + api_config["params"]["keyword"] = Name + # 获取查找信息 + data = await self.__api.get(api_config["url"],api_config["params"],"data") + if data["total"] == 0: + try: + sql = "SELECT showName, searchId FROM searchdata WHERE name = %s OR showName = %s" + sqldata = await self.__db.fetch_one(sql, (Name,Name)) + searchId=sqldata["searchId"] + showName=sqldata["showName"] + return_data["data"]["searchId"]=searchId + return_data["data"]["showName"]=showName + except Exception as e: + logger.error(f"获取外观信息错误: {e}") + return_data["msg"] = "未找到该外观信息" + return return_data + else: + # 查询魔盒获取外观名称 + try: + showName=data["list"][0]["name"] + return_data["data"]["showName"]=showName + except Exception as e: + logger.error(f"获取外观名称错误: {e}") + return_data["msg"] = "未找到该外观信息" + return return_data + # 查询爱剑三获取外观ID + try: + sql = "SELECT searchId FROM searchdata WHERE showName=%s" + sqldata = await self.__db.fetch_one(sql, (return_data["data"]["showName"],)) + searchId=sqldata["searchId"] + return_data["data"]["searchId"]=searchId + except Exception as e: + logger.error(f"获取外观ID错误: {e}\n{return_data['data']['showName']}") + return_data["msg"] = "未找到该外观信息" + return return_data + + #在配置文件中获取接口配置 + api_config = self.__api_config["aijx3_GoodsDetail"] + #更新参数 + api_config["params"]["goodsName"] = showName + # 获取数据外观详细数据 + data = await self.__api.post(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取外观详细数据失败" + return return_data + # 提取外观详细数据 + try: + imgs = data.get("imgs", []) + return_data["data"]["goodsDesc"]=data.get("goodsDesc", "无描述") + return_data["data"]["publishTime"]=data.get("publishTime", "无价格") + return_data["data"]["priceNum"]=data.get("priceNum", 0) + return_data["data"]["goodsId"]=data.get("goodsId", "无数据") + return_data["data"]["imgs"]=imgs[0] if imgs else "" + return_data["data"]["goodsAlias"]=data.get("goodsAlias", "无别名") + except Exception as e: + logger.error(f"提取外观详细数据失败: {e}") + return_data["msg"] = "提取外观详细数据失败" + return return_data + + # 查询万宝楼数据(公示和在售) + wbl_data = await self.__get_wbl_data(showName) + if not wbl_data: + return_data["msg"] = "获取万宝楼数据失败" + return return_data + if wbl_data: + return_data["data"]["wblgs"]=wbl_data["wblgs"] + return_data["data"]["wblzs"]=wbl_data["wblzs"] + return_data["msg"] = "获取万宝楼数据完成" + # 加载模板 + try: + return_data["temp"] = load_template("wujia.html") + except FileNotFoundError as e: + logger.error(f"加载模板失败: {e}") + return_data["msg"] = "系统错误:模板文件不存在" + return return_data + return_data["code"] = 200 + return return_data + + + async def __get_wbl_data(self,showName): + """获取万宝楼数据(公示和在售)""" + try: + #在配置文件中获取接口配置 + #api_config = self.__api_config["aijx3_wblwg"] + api_config = self.__api_config["wbl_waiguan"] + #更新参数 + api_config["params"]["filter[role_appearance]"] = showName + # 获取公示数据 + api_config["params"]["filter[state]"] = "1" + datawblgs = await self.__api.get(api_config["url"], api_config["params"], "data") + # 获取在售数据 + api_config["params"]["filter[state]"] = "2" + datawblzs = await self.__api.get(api_config["url"], api_config["params"], "data") + + return { + "wblgs": await self.__process_wbl_records(datawblgs.get("list", [])), + "wblzs": await self.__process_wbl_records(datawblzs.get("list", [])) + } + except Exception as e: + logger.error(f"获取万宝楼数据出错: {e}") + return None + + + async def __process_wbl_records(self,records): + """处理万宝楼记录数据""" + processed = [] + for record in records: + try: + # 转换时间戳为可读格式 + timestamp = record.get("remaining_time", 0) + days = timestamp // 86400 # 每天有 86400 秒 + hours = (timestamp % 86400) // 3600 # 每小时有 3600 秒 + minutes = (timestamp % 3600) // 60 # 每分钟有 60 秒 + result = "" + if days > 0: + result = (f"{days}天") + if hours > 0: + result += (f"{hours}时") + if minutes > 0: + result += (f"{minutes}分钟") + single_unit_price = record.get("single_unit_price", 0) + processed.append({ + "priceNum": "{:.2f}".format( single_unit_price / 100), + "belongQf2": record.get("server_name", "无数据"), + "replyTime": result, + "discountRate": record.get("discountRate", 0.0), + }) + except Exception as e: + logger.error(f"处理万宝楼记录出错: {e}") + continue + + return processed + + + async def jiaoyihang(self,Name: str = "守缺式",server: str = "梦江南"): + """ + 区服交易行 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["jx3box_item"] + #更新参数 + api_config["params"]["keyword"] = Name + # 获取多页查找信息 + data = await self.__api.all_pages("GET",api_config["url"],api_config["params"],"data","data") + if not data: + logger.error(f"获取多页数据失败") + return_data["msg"] = "未找到该物品" + return return_data + # 提取指定字段 + fields = ["IconID", "Name","id"] + result = extract_fields(data, fields) + if not data: + logger.error(f"提取字段失败") + return_data["msg"] = "未找到该物品" + return return_data + # 提取id列表 + lists_id = extract_field(result, "id") + strlists_id = ",".join(str(x) for x in lists_id) + logger.info(f"{strlists_id}") + #在配置文件中获取接口配置 + api_config = self.__api_config["jx3box_itemprice"] + #更新参数 + api_config["params"]["itemIds"] = strlists_id + api_config["params"]["server"] = server + # 获取数据 + data = await self.__api.get(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "未找到在售物品" + return return_data + #提取需要的字段 + fieldsjyh = ["ItemId", "SampleSize", "LowestPrice", "AvgPrice", "Date"] + resultjyh = [{f: v[f] for f in fieldsjyh} for v in data.values()] + #合并表格数据 + # 先建立一个字典映射,加快查找 + map_result = {item["id"]: {"IconID": item["IconID"], "Name": item["Name"]} for item in result} + # 遍历 resultjyh,合并字段 + for item in resultjyh: + if item["ItemId"] in map_result: + item.update(map_result[item["ItemId"]]) + #处理数据 + for item in resultjyh: + item["LowestPrice"] = gold_to_string(item["LowestPrice"]) + item["AvgPrice"] = gold_to_string(item["AvgPrice"]) + item["IconID"] = f"https://icon.jx3box.com/icon/{item['IconID']}.png" + # 准备模板渲染数据 + return_data["data"] = { + "items": resultjyh, + "server": server, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + # 加载模板 + try: + return_data["temp"] = load_template("jiaoyihang.html") + except FileNotFoundError as e: + logger.error(f"加载模板失败: {e}") + return_data["msg"] = "系统错误:模板文件不存在" + return return_data + return_data["code"] = 200 + return return_data diff --git a/core/WZRYFunction.py b/core/WZRYFunction.py new file mode 100644 index 0000000..10b697d --- /dev/null +++ b/core/WZRYFunction.py @@ -0,0 +1,190 @@ +from datetime import datetime +import base64 +from astrbot.api import logger + +from .APIClient import APIClient +from .AsyncMySQL import AsyncMySQL +from .function_basic import load_template,extract_field,flatten_field,extract_fields,gold_to_string,plot_line_chart_base64 + +class WZRYFunction: + def __init__(self, api_config,db: AsyncMySQL ): + self.__api = APIClient() + self.__db = db + self.__api_config = api_config + + async def _SELECT_ID(self,name: str): + """ + 查询ID + """ + sql = "SELECT id FROM wzydid WHERE name = %s or id = %s" + sqlid = await self.__db.fetch_one(sql, (name,name)) + return sqlid["id"] if sqlid else None + + async def all_user(self): + """ + 查询所有用户 + """ + sql = "SELECT id,name FROM wzydid" + sqlid = await self.__db.fetch_all(sql) + return_data = "营地ID\t\t昵称\n" + for m in sqlid: + return_data += f"{m['id']}\t{m['name']}\n" + return return_data + + async def add_user(self,id: str ,name: str): + """ + 添加用户 + """ + self_id = await self._SELECT_ID(id) + if self_id is not None: + return "该用户已存在,无需重复添加" + sql = "INSERT INTO wzydid (id,name) VALUES (%s, %s)" + rowcount = await self.__db.execute(sql, (id,name)) + if rowcount > 0: + return f"id:{id}昵称:{name}\n添加成功" + else: + return f"id:{id}昵称:{name}\n添加失败,请稍后再试" + + async def update_user(self,id: str ,name: str): + """ + 更新用户 + """ + self_id = await self._SELECT_ID(id) + if self_id is None: + return "该用户不存在,请先添加用户" + sql = "UPDATE wzydid SET name = %s WHERE id = %s" + rowcount = await self.__db.execute(sql, (name,id)) + if rowcount > 0: + return f"id:{id}昵称:{name}\n更新成功" + else: + return f"id:{id}昵称:{name}\n更新失败,请稍后再试" + + async def delete_user(self,id: str ): + """ + 删除用户 + """ + self_id = await self._SELECT_ID(id) + if self_id is None: + return "该用户不存在,无需删除" + sql = "DELETE FROM wzydid WHERE id = %s" + rowcount = await self.__db.execute(sql, (id,)) + if rowcount > 0: + return f"id:{id}\n删除成功" + else: + return f"id:{id}\n删除失败,请稍后再试" + + async def zhanji(self,name: str ,option: str): + """ + 战绩查询 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["wzry_zhanji"] + # ID查询 + sql_id = await self._SELECT_ID(name) + if sql_id is None: + return_data["msg"] = "未查询到该用户,请确认输入正确的昵称或营地ID" + return return_data + #更新参数 + api_config["params"]["id"] = sql_id + api_config["params"]["option"] = option + # 需要提取的字段 + fields = ["gametime","killcnt","deadcnt","assistcnt","gameresult","mvpcnt","losemvp","mapName", + "oldMasterMatchScore","newMasterMatchScore","usedTime","winNum","failNum","roleJobName","stars","desc", + "gradeGame","heroIcon","godLikeCnt", "firstBlood","hero1TripleKillCnt","hero1UltraKillCnt","hero1RampageCnt","evaluateUrlV3","mvpUrlV3"] + # 处理返回数据 + try: + # 获取数据 + data = await self.__api.get(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 提取字段 + result = extract_fields(data["list"], fields) + result = result[:25] + # 数据处理 + for m in result: + minutes = m["usedTime"] // 60 + seconds = m["usedTime"] % 60 + m["time_str"] = f"{minutes}:{seconds:02d}" + + return_data["data"] = result + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + # 加载模板 + try: + return_data["temp"] = load_template("wangzhezhanji.html") + except FileNotFoundError as e: + logger.error(f"加载模板失败: {e}") + return_data["msg"] = "系统错误:模板文件不存在" + return return_data + return_data["code"] = 200 + return return_data + + async def ziliao(self, name: str): + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + # 查询 ID + sql_id = await self._SELECT_ID(name) + + if sql_id is None: + return_data["msg"] = "未查询到该用户,请确认输入正确的昵称或营地ID" + return return_data + api_config = self.__api_config["wzry_ziliao"] + api_config["params"]["id"] = sql_id + + try: + data = await self.__api.get(api_config["url"], api_config["params"]) + # 转 base64 + return_data["data"]["img_base64"] = base64.b64encode(data).decode("utf-8") + return_data["code"] = 200 + + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + + # 加载模板 + try: + return_data["temp"] = load_template("wzry_zl.html") + except FileNotFoundError: + logger.error(f"加载模板失败") + return_data["msg"] = "系统错误:模板文件不存在" + + return return_data + + async def bilei_all(self): + """ + 查询仇人列表 + """ + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + # 查询所有 + try: + sql = "SELECT * FROM wzrybl" + sqlid = await self.__db.fetch_all(sql) + except Exception as e: + logger.error(f"处理数据时出错: {e}") + return_data["msg"] = "处理数据库信息时出错" + return return_data + + # 加载模板 + try: + return_data["temp"] = load_template("temp_test.html") + except FileNotFoundError as e: + logger.error(f"加载模板失败: {e}") + return_data["msg"] = "系统错误:模板文件不存在" + return return_data + return_data["data"]["lists"] = sqlid + return_data["code"] = 200 + return return_data \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/function_basic.py b/core/function_basic.py new file mode 100644 index 0000000..5886593 --- /dev/null +++ b/core/function_basic.py @@ -0,0 +1,167 @@ +import os +from pathlib import Path +import matplotlib.pyplot as plt +from matplotlib.font_manager import FontProperties +from io import BytesIO +import base64 + +def load_template(template_name): + """ + 从模板文件加载模板内容 + + Args: + template_name: 模板文件名(不带路径) + + Returns: + str: 模板内容 + """ + # 获取模板文件路径 + plugin_dir = Path(__file__).parent.parent + template_path = plugin_dir / "templates" / template_name + + # 检查文件是否存在 + if not template_path.exists(): + raise FileNotFoundError(f"模板文件不存在: {template_path}") + + # 读取模板内容 + with open(template_path, 'r', encoding='utf-8') as f: + return f.read() + + +def flatten_field(data_list, field_name): + """ + 提取并扁平化指定字段的值 + + 从一个字典列表中,收集指定字段的所有值。 + 如果字段的值是列表,则会被展开(扁平化)加入结果; + 如果字段的值是单个元素,则直接加入结果。 + + Args: + data_list (list[dict]): 数据列表,每个元素是字典 + field_name (str): 要提取的字段名 + + Returns: + list: 扁平化后的字段值列表 + """ + extracted_data = [] + for item in data_list: + if field_name in item and item[field_name]: + # 如果是 list,展开 + if isinstance(item[field_name], list): + extracted_data.extend(item[field_name]) + else: + extracted_data.append(item[field_name]) + return extracted_data + + +def extract_fields(data_list, fields): + """ + 从字典列表中提取多个字段 + + Args: + data_list (list[dict]): 包含字典的列表 + fields (list[str]): 要提取的字段名列表 + + Returns: + list[dict]: 只包含指定字段的新字典列表 + """ + result = [] + try: + for item in data_list: + extracted = {field: item.get(field) for field in fields} + result.append(extracted) + except Exception as e: + print(f"提取字段时出错: {e}") + return [] + return result + + +def extract_field(data_list, field_name): + """ + 从列表中的字典提取指定字段的所有值 + + Args: + data_list (list): 数据列表,每个元素是 dict + field_name (str): 要提取的字段名 + + Returns: + list: 提取出来的字段值列表 + """ + return [item[field_name] for item in data_list if field_name in item] + + +def gold_to_string(gold_amount): + """ + 将金钱数值转换为字符串表示形式 + + Args: + gold_amount (int): 金钱数值,单位为铜币 + + Returns: + str: 格式化后的金钱字符串,例如 "1金2银3铜" + """ + if not gold_amount: + return "无价格" + + parts = [] + started = False # 标记是否已经遇到第一个非零位 + + bricks = gold_amount // 100000000 + gold = (gold_amount % 100000000) // 10000 + silver = (gold_amount % 10000) // 100 + copper = gold_amount % 100 + + for value, unit in [(bricks, "砖"), (gold, "金"), (silver, "银"), (copper, "铜")]: + if value != 0: + started = True + if started: + parts.append(f"{value}{unit}") + + return "".join(parts) + + +def plot_line_chart_base64(data, x_field, y_field, title=None, reverse_x=False): + """ + 根据列表数据绘制折线图,返回 base64 图片字符串。 + + :param data: list[dict] 数据列表 + :param x_field: str X轴字段 + :param y_field: str Y轴字段 + :param title: str 图表标题(可选) + :param reverse_x: bool 是否反转 X 轴方向(默认 False:从左往右;True:从右往左) + :return: str base64 图片字符串,可直接放到 中 + """ + if not data: + raise ValueError("数据列表不能为空") + + # 字体设置(支持中文) + plt.rcParams['font.sans-serif'] = ['SimHei'] + plt.rcParams['axes.unicode_minus'] = False + + # 提取 X / Y 数据 + x_values = [str(item.get(x_field, "")) for item in data] + y_values = [float(item.get(y_field, 0)) for item in data] + + # 创建画布 + plt.figure(figsize=(8, 5)) + plt.plot(x_values, y_values, marker='o', color='#4a90e2', linewidth=2) + + # 反转 X 轴 + if reverse_x: + plt.gca().invert_xaxis() + + # 标题与标签 + if title is None: + title = f"{y_field} 折线图" + + plt.grid(True, linestyle='--', alpha=0.5) + plt.xticks(rotation=30) + plt.tight_layout() + + # 转 Base64 + buffer = BytesIO() + plt.savefig(buffer, format='png', dpi=150) + plt.close() + + img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + return f"data:image/png;base64,{img_base64}" \ No newline at end of file diff --git a/core/test.py b/core/test.py new file mode 100644 index 0000000..ab2efbd --- /dev/null +++ b/core/test.py @@ -0,0 +1,14 @@ +import http.client + +conn = http.client.HTTPSConnection("api.t1qq.com") +payload = '' +headers = { + 'User-Agent': 'Apifox/1.0.0 (https://apifox.com)', + 'Accept': '*/*', + 'Host': 'api.t1qq.com', + 'Connection': 'keep-alive' +} +conn.request("GET", "/api/tool/wzrr/ydtp?key=vBpEzoiC9z5A9c9Nn83IhLn6M9&id=489048724", payload, headers) +res = conn.getresponse() +data = res.read() +print(data) \ No newline at end of file diff --git a/main.py b/main.py index e6af67e..067a97a 100644 --- a/main.py +++ b/main.py @@ -1,24 +1,400 @@ -from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult -from astrbot.api.star import Context, Star, register -from astrbot.api import logger +import json +import asyncio +from pathlib import Path +from datetime import datetime -@register("helloworld", "YourName", "一个简单的 Hello World 插件", "1.0.0") -class MyPlugin(Star): - def __init__(self, context: Context): +from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult, MessageChain +from astrbot.api.star import Context, Star, register, StarTools +from astrbot.api import logger +from astrbot.api import AstrBotConfig + +from .core.AsyncMySQL import AsyncMySQL +from .core.JX3Function import JX3Function +from .core.WZRYFunction import WZRYFunction + + + +@register("astrbot_plugin_jx3api", + "fxdyz", + "通过接口调用剑网三API接口获取游戏数据", + "1.0.0", + "https://github.com/qsc20001102/astrbot_plugin_jx3api.git" +) +class Jx3ApiPlugin(Star): + def __init__(self, context: Context, config: AstrBotConfig): super().__init__(context) + #获取配置 + self.conf = config + # 本地数据存储路径 + self.local_data_dir = StarTools.get_data_dir("astrbot_plugin_jx3api") + # api数据文件 + self.api_file_path = Path(__file__).parent / "api_config.json" + # 读取文件内容 + with open(self.api_file_path, 'r', encoding='utf-8') as f: + self.api_config = json.load(f) + # 初始化数据 + self.inidata() + self.kf_task = asyncio.create_task(self.cycle_kaifjiankong()) + logger.info("jx3api插件初始化完成") + async def initialize(self): """可选择实现异步的插件初始化方法,当实例化该插件类之后会自动调用该方法。""" + # 数据库配置 + db_config = { + 'host': '38.12.28.24', + 'port': 3306, + 'user': 'asrtbot', + 'password': 'qsc123456', + 'db': 'asrtbot', + 'charset': 'utf8mb4', + 'autocommit': True + } + #创建类实例 + self.db = AsyncMySQL(db_config) + self.jx3fun = JX3Function(self.api_config,self.db) + self.wzry = WZRYFunction(self.api_config,self.db) + # 周期函数调用 + + + logger.info("jx3api插件创建实例完成") + + def inidata(self): + """数据初始化""" + self.test_server = False + logger.info("数据初始化完成") + + async def cycle_kaifjiankong(self): + """开服监控后台程序""" + # 获取配置信息 + time_int = self.conf.get("jx3_kfjk_time", 10) + self.kfjk_en = self.conf.get("jx3_kfjk_en", True) + self.kfjk_umos = self.conf.get("jx3_kfjk_list", []) + self.kfjk_servername = self.conf.get("jx3_kfjk_server", "梦江南") + kfjk_test = self.conf.get("jx3_kfjk_test", False) + + self.kfjk_server_state = True # 上一次查询的状态 + self.kfjk_server_state_new = False # 最新查询的状态 + + if self.kfjk_en: + logger.info(f"开服监控功能开启") + while self.kfjk_en: + data = await self.jx3fun.kaifu(self.kfjk_servername) + if kfjk_test: + self.kfjk_server_state_new = self.test_server + else: + self.kfjk_server_state_new = data["status"] + # logger.info(f"开服监控功能循环中,上次询问服务器状态{self.kfjk_server_state},本次询问的服务器状态{self.kfjk_server_state_new}") + if self.kfjk_server_state != self.kfjk_server_state_new: + logger.info(f"开服监控功能循环中,上次询问服务器状态{self.kfjk_server_state},本次询问的服务器状态{self.kfjk_server_state_new}") + if self.kfjk_server_state and not self.kfjk_server_state_new: + message_chain = MessageChain().message(f"{self.kfjk_servername}服务器已关闭\n休息一会把,开服了喊你!") + if self.kfjk_server_state_new and not self.kfjk_server_state: + message_chain = MessageChain().message(f"{self.kfjk_servername}服务器已开启\n快冲!快冲!") + if self.kfjk_umos: + for umo in self.kfjk_umos: + await self.context.send_message(umo, message_chain) + self.kfjk_server_state = self.kfjk_server_state_new + await asyncio.sleep(time_int) + if not self.kfjk_en: + # 销毁进程 + self.kf_task.cancel() + logger.info(f"开服监控功能关闭") + + @filter.command_group("剑三") + def jx3(self): + pass + + @jx3.command("日常") + async def jx3_richang(self, event: AstrMessageEvent,server: str = "梦江南",num: int = 0): + """剑三 日常 服务器 天数""" + try: + data= await self.jx3fun.richang(server,num) + if data["code"] == 200: + yield event.plain_result(data["data"]) + else: + yield event.plain_result("msg") + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @jx3.command("开服") + async def jx3_kaifu(self, event: AstrMessageEvent,server: str = "梦江南"): + """剑三 日常 服务器 天数""" + try: + data= await self.jx3fun.kaifu(server) + if data["code"] == 200: + yield event.plain_result(data["data"]) + else: + yield event.plain_result("msg") + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @jx3.command("沙盘") + async def jx3_shapan(self, event: AstrMessageEvent,server: str = "梦江南"): + """剑三 沙盘 服务器""" + try: + data= await self.jx3fun.shapan(server) + if data["code"] == 200: + yield event.image_result(data["data"]) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + + @jx3.command("骚话") + async def jx3_shaohua(self, event: AstrMessageEvent,): + """剑三 骚话""" + try: + data= await self.jx3fun.shaohua() + if data["code"] == 200: + yield event.plain_result(data["data"]) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + + @jx3.command("技改") + async def jx3_jigai(self, event: AstrMessageEvent,): + """剑三 技改""" + try: + data= await self.jx3fun.jigai() + if data["code"] == 200: + yield event.plain_result(data["data"]) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + + @jx3.command("金价") + async def jx3_jinjia(self, event: AstrMessageEvent,server: str = "梦江南"): + """剑三 金价 服务器""" + try: + data= await self.jx3fun.jinjia(server) + if data["code"] == 200: + url = await self.html_render(data["temp"], data["data"], options={}) + yield event.image_result(url) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + + @jx3.command("奇遇") + async def jx3_qiyu(self, event: AstrMessageEvent,adventureName: str = "阴阳两界", serverName: str = "梦江南"): + """剑三 奇遇 奇遇名称 服务器""" + try: + data= await self.jx3fun.qiyu(adventureName,serverName) + if data["code"] == 200: + url = await self.html_render(data["temp"], data["data"], options={}) + yield event.image_result(url) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + + @jx3.command("物价") + async def jx3_wujia(self, event: AstrMessageEvent,Name: str = "秃盒"): + """剑三 外观名称""" + try: + data=await self.jx3fun.wujia(Name) + if data["code"] == 200: + url = await self.html_render(data["temp"], data["data"], options={}) + yield event.image_result(url) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + + @jx3.command("交易行") + async def jx3_jiaoyihang(self, event: AstrMessageEvent,Name: str = "守缺式",server: str = "梦江南"): + """剑三 外观名称""" + try: + data=await self.jx3fun.jiaoyihang(Name,server) + if data["code"] == 200: + url = await self.html_render(data["temp"], data["data"], options={}) + yield event.image_result(url) + + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @jx3.command("开服监控") + async def jx3_kaifhujiank(self, event: AstrMessageEvent,en: str = "状态"): + """剑三 开服监控""" + if en == "开启": + umo_set = set(self.kfjk_umos) + umo_set.add(event.unified_msg_origin) + self.kfjk_umos = list(umo_set) + yield event.plain_result(f"开服监控已开启") + elif en =="关闭": + try: + self.kfjk_umos.remove(event.unified_msg_origin) + yield event.plain_result(f"开服监控已关闭") + except ValueError as e: + yield event.plain_result(f"开服监控已关闭") + elif en == "状态": + yield event.plain_result(f"开服监控后台状态:{self.kfjk_en}\n监控服务器:{self.kfjk_servername}\n上次询问服务器状态{self.kfjk_server_state}\n本次询问的服务器状态{self.kfjk_server_state_new}\n推送会话列表:\n{self.kfjk_umos}") + else: + yield event.plain_result("开服监控指令错误,请输入 开启/关闭") + + + @filter.permission_type(filter.PermissionType.ADMIN) + @jx3.command("外观数据同步") + async def jx3_SearchData(self, event: AstrMessageEvent): + """剑三 外观数据同步""" + try: + data=await self.jx3fun.SearchData() + if data["code"] == 200: + yield event.plain_result(data["msg"]) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @filter.command("测试值") + async def jx3_testvar(self, event: AstrMessageEvent,var: str = "0"): + """测试程序""" + if var == "1": + self.test_server = True + if var == "0": + self.test_server = False + + yield event.plain_result(f"测试值:{self.test_server}") + + + @filter.command_group("王者") + def wz(self): + pass + + + @wz.command("列表") + async def wz_yingdilist(self, event: AstrMessageEvent): + """王者 用户列表""" + try: + data = await self.wzry.all_user() + logger.info(f"王者荣耀营地列表查询结果{data}") + yield event.plain_result(data) + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @wz.command("添加") + async def wz_adduesr(self, event: AstrMessageEvent,id: str,name: str): + """王者 添加用户""" + try: + data = await self.wzry.add_user(id,name) + yield event.plain_result(data) + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @wz.command("修改") + async def wz_updateuser(self, event: AstrMessageEvent,id: str,name: str): + """王者 修改用户""" + try: + data = await self.wzry.update_user(id,name) + yield event.plain_result(data) + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @wz.command("删除") + async def wz_deleteuser(self, event: AstrMessageEvent,id: str): + """王者 删除用户""" + try: + data = await self.wzry.delete_user(id) + yield event.plain_result(data) + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @wz.command("战绩") + async def wz_zhanji(self, event: AstrMessageEvent,name: str = "飞翔大野猪",option: str = "0"): + """王者 战绩 营地ID 对局类型""" + + try: + data = await self.wzry.zhanji(name,option) + # logger.info(f"王者荣耀战绩查询结果{data}") + #prompt = f"{data['data']}\n" + #prompt += f"以上数据是获取到的王者荣耀战绩信息,请根据这些数据生成一两句话的战绩总结,突出玩家的优势和特点,语言风格轻松幽默,适合在游戏群内分享。" + if data["code"] == 200: + url = await self.html_render(data["temp"],{"data":data["data"]}, options={}) + yield event.image_result(url) + else: + yield event.plain_result(data["msg"]) + return + #provider_id = await self.context.get_current_chat_provider_id(umo=event.unified_msg_origin) + #llm_resp = await self.context.llm_generate(chat_provider_id=provider_id, prompt=prompt,) + #out = llm_resp.completion_text + #yield event.plain_result(f"{out}") + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + + @wz.command("资料") + async def wz_ziliao(self, event: AstrMessageEvent,name: str = "489048724"): + """王者 战绩 营地ID 对局类型""" + try: + data = await self.wzry.ziliao(name) + logger.info(f"输出结果{data}") + if data["code"] == 200: + url = await self.html_render(data["temp"], data["data"], options={}) + yield event.image_result(url) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @filter.command("仇人列表") + async def wz_bilei(self, event: AstrMessageEvent,): + """仇人列表""" + try: + data = await self.wzry.bilei_all() + if data["code"] == 200: + url = await self.html_render(data["temp"], data["data"], options={}) + yield event.image_result(url) + else: + yield event.plain_result(data["msg"]) + return + #logger.info(f"输出结果{event.get_sender_name()}\n{out}") + #yield event.plain_result(f"{out}") + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") - # 注册指令的装饰器。指令名为 helloworld。注册成功后,发送 `/helloworld` 就会触发这个指令,并回复 `你好, {user_name}!` - @filter.command("helloworld") - async def helloworld(self, event: AstrMessageEvent): - """这是一个 hello world 指令""" # 这是 handler 的描述,将会被解析方便用户了解插件内容。建议填写。 - user_name = event.get_sender_name() - message_str = event.message_str # 用户发的纯文本消息字符串 - message_chain = event.get_messages() # 用户所发的消息的消息链 # from astrbot.api.message_components import * - logger.info(message_chain) - yield event.plain_result(f"Hello, {user_name}, 你发了 {message_str}!") # 发送一条纯文本消息 async def terminate(self): """可选择实现异步的插件销毁方法,当插件被卸载/停用时会调用。""" + await self.db.close_pool() + # 后台进程销毁 + self.kf_task.cancel() + logger.info("jx3api插件已卸载/停用") \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml index dd8acf6..28603d4 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,6 +1,5 @@ -name: helloworld # 插件唯一识别名,以 astrbot_plugin_ 前缀开头 -display_name: helloworld # 用于展示的名字,可以是方便人类阅读的名字(需要版本 >= v4.5.0,低版本不会报错,请放心填写) -desc: AstrBot 插件示例。 # 插件简短描述 -version: v1.3.0 # 插件版本号。格式:v1.1.1 或者 v1.1 -author: Soulter # 作者 -repo: https://github.com/Soulter/helloworld # 插件的仓库地址 +name: jx3api # 这是你的插件的唯一识别名。 +desc: 通过接口调用剑网三API接口获取游戏数据 # 插件简短描述 +version: v1.0.0 # 插件版本号。格式:v1.1.1 或者 v1.1 +author: fxdyz # 作者 +repo: https://github.com/qsc20001102/astrbot_plugin_jx3api # 插件的仓库地址 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3132c6a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +aiomysql +matplotlib \ No newline at end of file diff --git a/templates/jiaoyihang.html b/templates/jiaoyihang.html new file mode 100644 index 0000000..4c4a416 --- /dev/null +++ b/templates/jiaoyihang.html @@ -0,0 +1,30 @@ +
+

剑网3交易行价格查询

+

服务器: {{ server }}

+ +
+ + + + + + + + + + + {% for item in items %} + + + + + + + {% endfor %} + +
物品名称最低价总数量数据更新时间
+ icon + {{ item.Name }} + {{ item.AvgPrice }}{{ item.SampleSize }}{{ item.Date }}
+
+
diff --git a/templates/jinjia.html b/templates/jinjia.html new file mode 100644 index 0000000..2b4c44c --- /dev/null +++ b/templates/jinjia.html @@ -0,0 +1,33 @@ +
+

剑网三金价

+ +
+ + + + + {% for item in items %} + + {% endfor %} + + + + + + + {% for item in items %} + + {% endfor %} + + + + + {% for item in items %} + + {% endfor %} + + +
时间{{ item.date }}
万宝楼{{ item.priceWanbaolou }}
贴吧{{ item.priceTieba }}
+
+ +
diff --git a/templates/qiyuliebiao.html b/templates/qiyuliebiao.html new file mode 100644 index 0000000..7356f8f --- /dev/null +++ b/templates/qiyuliebiao.html @@ -0,0 +1,34 @@ +
+

剑网3奇遇查询

+

奇遇名称:{{ qiyuname }} 服务器:{{ server }}

+ +
+ + + + + + + + + + + {% for item in items %} + + + + + + + {% endfor %} + +
奇遇名称服务器角色名称触发时间
{{ item.adventureName }}{{ server }}{{ item.gameName }}{{ item.time }}
+
+ + +
+ 奇遇趋势图 +
+
diff --git a/templates/temp_test.html b/templates/temp_test.html new file mode 100644 index 0000000..19c55c1 --- /dev/null +++ b/templates/temp_test.html @@ -0,0 +1,101 @@ + + + + +王者荣耀 — 仇人列表 + + + + + +
+

王者荣耀 — 仇人列表

+ + + + + + + + + + + + + + + + {% for m in lists %} + + + + + + + + + + {% endfor %} + +
ID名称英雄分路记录者时间描述
{{ m.id }}{{ m.name }}{{ m.hero }}{{ m.fenglu }}{{ m.user }}{{ m.time }}{{ m.text }}
+ +
+ + + diff --git a/templates/wangzhezhanji.html b/templates/wangzhezhanji.html new file mode 100644 index 0000000..44f469f --- /dev/null +++ b/templates/wangzhezhanji.html @@ -0,0 +1,240 @@ + + + + +王者荣耀 — 对局列表 + + + + + +
+

王者荣耀 — 对局列表

+ + {% for m in data %} + +
+ +
+ +
{{ m.desc }}
+
+ +
+ + +
+ + {% if m.evaluateUrlV3 %} + + {% endif %} + + {% if m.mvpUrlV3 %} + + {% endif %} + +
+ {% if m.gameresult == 1 %}胜利 + {% elif m.gameresult == 2 %}失败 + {% else %}平局 + {% endif %} +
+ +
+ + +
+ {{ m.gametime }} · {{ m.mapName }} +
+ + +
+ {{ m.roleJobName }} · ⭐{{ m.stars }} +
+ + +
+ 巅峰赛积分 {{ m.oldMasterMatchScore }} → {{ m.newMasterMatchScore }} +
+ + +
+ 击杀 {{ m.killcnt }} 死亡 {{ m.deadcnt }} 助攻 {{ m.assistcnt }} 时长 {{ m.time_str }} +
+ + +
+ {% if m.godLikeCnt > 0 %}超神 × {{ m.godLikeCnt }}{% endif %} + {% if m.firstBlood > 0 %}一血{% endif %} + {% if m.hero1TripleKillCnt > 0 %}三杀 × {{ m.hero1TripleKillCnt }}{% endif %} + {% if m.hero1UltraKillCnt > 0 %}四杀 × {{ m.hero1UltraKillCnt }}{% endif %} + {% if m.hero1RampageCnt > 0 %}五杀 × {{ m.hero1RampageCnt }}{% endif %} +
+ + +
+ 评分 {{ m.gradeGame }} +
+ +
+ +
+ + {% endfor %} +
+ + + diff --git a/templates/wujia.html b/templates/wujia.html new file mode 100644 index 0000000..07f7689 --- /dev/null +++ b/templates/wujia.html @@ -0,0 +1,65 @@ +
+ + +

{{ showName }} ({{ goodsAlias }})

+ {{ showName }} +

{{ goodsDesc }}

+

发布时间: {{ publishTime.split('T')[0] if publishTime else '未知' }}

+

参考价格: {{ priceNum }} 元

+ + +
+ + +
+
万宝楼公示
+
+ + + + + + + + + + {% for item in wblgs %} + + + + + + {% endfor %} + +
剩余时间服务器价格
{{ item.replyTime }}{{ item.belongQf2 }}{{ item.priceNum }}元
+
+
+ + +
+
万宝楼在售
+
+ + + + + + + + + + {% for item in wblzs %} + + + + + + {% endfor %} + +
剩余时间服务器价格
{{ item.replyTime }}{{ item.belongQf2 }}{{ item.priceNum }}元
+
+
+ +
+ +
diff --git a/templates/wzry_zl.html b/templates/wzry_zl.html new file mode 100644 index 0000000..0204c02 --- /dev/null +++ b/templates/wzry_zl.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file