diff --git a/CHANGELOG.md b/CHANGELOG.md index 7851945..a409e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## 更新记录 +### version: 2.7: + +重构 **交易行 物品名称 [服务器]** 指令,改为免令牌图片查询。 + +新增 JX3BOX 交易行物品库缓存,用户输入物品名后先在本地模糊匹配物品 ID,再批量请求交易行价格接口。 + +交易行结果图改为列表展示,包含物品图标与名称、砖金银铜图标化价格、数量和数据时间。 + +价格接口未返回的物品不再展示,全部无价格数据时返回明确提示。 + ### version: 2.6: 新增 **资历 角色名称 [服务器]** 指令,支持两轮对话选择资历总览或分类总览,并以图片形式展示角色资历点数完成进度。 diff --git a/README.md b/README.md index c035a19..e51648a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ pip install -r requirements.txt - `apscheduler`:后台推送任务调度。 - `matplotlib`:部分图像/数据展示依赖。 -插件元信息在 `metadata.yaml` 中维护,当前版本为 `v2.6`,要求 AstrBot 版本 `>=4.11.0`。 +插件元信息在 `metadata.yaml` 中维护,当前版本为 `v2.7`,要求 AstrBot 版本 `>=4.11.0`。 ## 插件配置 @@ -171,6 +171,7 @@ pip install -r requirements.txt 日常 诛恶 资历 飞翔大野猪 +交易行 五行石 梦江南 奇遇 飞翔大野猪 未做奇遇 飞翔大野猪 梦江南 战绩 飞翔大野猪 梦江南 33 @@ -340,6 +341,7 @@ html_renderer.render_custom_template() - 避雷记录。 - 推送任务状态缓存。 - 资历菜单与资历点数基础数据缓存,默认缓存 30 天,接口失败时可使用旧缓存兜底。 +- 交易行物品库基础数据缓存,默认缓存 30 天,接口失败时可使用旧缓存兜底。 `core/sqlite.py` 封装了异步增删改查;`core/bilei_data.py` 基于该封装实现避雷数据管理。 @@ -360,7 +362,22 @@ html_renderer.render_custom_template() 4. 展开菜单中单个 ID 和数组 ID,按资历点数计算 `已完成点数 / 总点数` 与百分比。 5. 使用 `templates/zili.html` 渲染进度条图片。 -### 9. 后台推送 +### 9. 交易行查询 + +`交易行 物品名称 [服务器]` 在 2.7 中完成重构,属于交易功能,免令牌,输出为图片。 + +实现流程: + +1. 从 JX3BOX 交易行物品库接口读取所有可查询物品,并缓存到本地 SQLite。 +2. 用户输入物品名后,在本地物品库中按名称进行模糊匹配。 +3. 匹配结果按“完全匹配、前缀匹配、包含匹配”排序,默认最多取前 50 个物品 ID。 +4. 调用 `https://next2.jx3box.com/api/auction/` 批量查询指定服务器的交易行价格。 +5. 价格接口未返回的物品不展示;全部无价格数据时直接返回文本提示。 +6. 使用 `templates/jiaoyihang.html` 渲染列表图片,展示物品图标、物品名称、价格、数量和数据时间。 + +价格单位按铜钱换算为砖、金、银、铜,并使用 `templates/img` 下的 `zhuang.png`、`jin.png`、`yin.png`、`tong.png` 图标展示;0 砖不会显示砖图标。 + +### 10. 后台推送 `core/async_task.py` 使用 APScheduler 实现轮询推送。 diff --git a/core/fun_basic.py b/core/fun_basic.py index 1727c1f..1dee598 100644 --- a/core/fun_basic.py +++ b/core/fun_basic.py @@ -50,6 +50,37 @@ def gold_to_string(gold_amount): return "".join(parts) +def gold_to_parts(gold_amount): + """将铜钱数拆成模板可渲染的货币片段""" + try: + amount = int(gold_amount or 0) + except (TypeError, ValueError): + amount = 0 + + if amount <= 0: + return [] + + bricks = amount // 100000000 + gold = (amount % 100000000) // 10000 + silver = (amount % 10000) // 100 + copper = amount % 100 + + parts = [] + started = False + for key, name, value in [ + ("zhuang", "砖", bricks), + ("jin", "金", gold), + ("yin", "银", silver), + ("tong", "铜", copper), + ]: + if value != 0: + started = True + if started: + parts.append({"key": key, "name": name, "value": value}) + + return parts + + def week_to_num(week :str): week_map = { "一": 0, @@ -110,4 +141,4 @@ def load_as_base64(icons_dir: str) -> dict[str, str]: icons[display_name] = f"data:{mime};base64,{data}" - return icons \ No newline at end of file + return icons diff --git a/core/jx3_data.py b/core/jx3_data.py index 117af7d..644eb0c 100644 --- a/core/jx3_data.py +++ b/core/jx3_data.py @@ -9,7 +9,7 @@ import astrbot.api.message_components as Comp from .request import APIClient from .sqlite import AsyncSQLiteDB -from .fun_basic import load_template,gold_to_string,week_to_num,compare_date_str +from .fun_basic import load_template,gold_to_string,gold_to_parts,week_to_num,compare_date_str ROLE_RANK_NAMES = { "名士五十强", @@ -2411,7 +2411,7 @@ class JX3Service: return return_data - async def _load_achievement_cache(self, key: str) -> tuple[Optional[Dict[str, Any]], bool]: + async def _load_achievement_cache(self, key: str) -> tuple[Optional[Any], bool]: """读取资历基础数据缓存,返回数据和是否已过期""" try: row = await self._cache_db.select_one("achievement_cache", "key=?", (key,)) @@ -2432,7 +2432,7 @@ class JX3Service: return None, True - async def _save_achievement_cache(self, key: str, payload: Dict[str, Any]): + async def _save_achievement_cache(self, key: str, payload: Any): """写入资历基础数据缓存""" try: await self._cache_db.execute( @@ -2471,6 +2471,74 @@ class JX3Service: return None + async def _get_trade_item_groups(self) -> Optional[List[Dict[str, Any]]]: + """获取交易行物品库,优先使用未过期缓存""" + cache_key = "trade_item_groups" + cached, expired = await self._load_achievement_cache(cache_key) + if isinstance(cached, list) and not expired: + return cached + + data = await self._base_request("jx3box_trade_items", "GET") + if isinstance(data, list) and data: + await self._save_achievement_cache(cache_key, data) + return data + + if isinstance(cached, list) and cached: + logger.warning("交易行物品库接口失败,使用旧缓存") + return cached + + return None + + + def _flatten_trade_items(self, groups: List[Dict[str, Any]]) -> list[Dict[str, Any]]: + """从交易行物品分组中提取可查询物品""" + items = [] + for group in groups: + if not isinstance(group, dict): + continue + for item in group.get("items", []) or []: + if not isinstance(item, dict): + continue + item_id = item.get("item_id") + label = item.get("label") + if not item_id or not label: + continue + items.append( + { + "item_id": str(item_id), + "label": str(label), + "icon": str(item.get("icon") or ""), + } + ) + return items + + + def _match_trade_items(self, items: list[Dict[str, Any]], keyword: str, limit: int = 50) -> list[Dict[str, Any]]: + """按物品名模糊匹配交易行物品""" + keyword = (keyword or "").strip() + if not keyword: + return [] + + matched = [] + seen = set() + for item in items: + label = item.get("label", "") + item_id = item.get("item_id", "") + if keyword not in label or item_id in seen: + continue + seen.add(item_id) + if label == keyword: + rank = 0 + elif label.startswith(keyword): + rank = 1 + else: + rank = 2 + matched.append((rank, len(label), label, item)) + + matched.sort(key=lambda row: (row[0], row[1], row[2])) + return [row[3] for row in matched[:limit]] + + def _flatten_achievement_ids(self, values: Any) -> list[int]: """展开菜单中的单个资历 ID 和数组资历 ID""" result = [] @@ -3016,36 +3084,73 @@ class JX3Service: """区服交易行""" return_data = self._init_return_data() - # 1. 构造请求参数 - params = {"server": server, "name": name,"token": self.token} + item_groups = await self._get_trade_item_groups() + if not item_groups: + return_data["msg"] = "交易行基础物品数据获取失败" + return return_data - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_jiaoyihang", "GET", params=params + trade_items = self._flatten_trade_items(item_groups) + matched_items = self._match_trade_items(trade_items, name, 50) + if not matched_items: + return_data["msg"] = "未找到匹配的交易行物品" + return return_data + + item_map = {item["item_id"]: item for item in matched_items} + params = { + "item_ids": list(item_map.keys()), + "server": server, + "aggregate_type": "hourly", + } + price_data: Optional[List[Dict[str, Any]]] = await self._base_request( + "jx3_jiaoyihang", "POST", params=params, out_key="" ) - if not data: - return_data["msg"] = "未找到该物品" + if not price_data or not isinstance(price_data, list): + return_data["msg"] = "未查询到交易行价格数据" return return_data - - # 2. 数据处理 - result = [] - + try: - for item in data: - if isinstance(item, dict): - inner_list = item.get("data", []) - first = inner_list[0] if inner_list else {} - new_item = { - "name": item.get("name"), - "icon": f"https://icon.jx3box.com/icon/{item.get('icon')}.png", - "sever": first.get("server"), - "count": len(inner_list), - "unit_price": gold_to_string(first.get("unit_price")), - "created": datetime.fromtimestamp(first.get("created", "")).strftime("%Y-%m-%d %H:%M:%S"), + result = [] + for price_item in price_data: + if not isinstance(price_item, dict): + continue + + item_id = str(price_item.get("item_id") or "") + base_item = item_map.get(item_id) + if not base_item: + continue + + timestamp = price_item.get("timestamp") + try: + created = datetime.fromtimestamp(int(timestamp)).strftime("%Y-%m-%d %H:%M:%S") + except (TypeError, ValueError, OSError): + created = "" + + result.append( + { + "item_id": item_id, + "name": base_item.get("label", ""), + "icon": f"https://icon.jx3box.com/icon/{base_item.get('icon', '')}.png", + "server": price_item.get("server", server), + "price": price_item.get("price", 0), + "price_parts": gold_to_parts(price_item.get("price", 0)), + "sample": price_item.get("sample", 0), + "created": created, } - result.append(new_item) - return_data["data"]["list"] = result + ) + + if not result: + return_data["msg"] = "未查询到交易行价格数据" + return return_data + + return_data["data"] = { + "search_name": name, + "server": server, + "matched_count": len(matched_items), + "result_count": len(result), + "list": result, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } except Exception as e: logger.error(f"处理交易行数据失败: {e}") return_data["msg"] = "处理交易行数据失败" diff --git a/data/api_config.json b/data/api_config.json index e0f74cb..5e3a95a 100644 --- a/data/api_config.json +++ b/data/api_config.json @@ -491,13 +491,21 @@ } }, "jx3_jiaoyihang":{ - "url":"https://www.jx3api.com/data/trade/market", - "method":"GET", - "description":"获取剑网三区服交易行数据", + "url":"https://next2.jx3box.com/api/auction/", + "method":"POST", + "description":"获取剑网三区服交易行物品价格数据", "params":{ + "item_ids": [], "server": "梦江南", - "name": "玄晶", - "token": "" + "aggregate_type": "hourly" + } + }, + "jx3box_trade_items":{ + "url":"https://cms.jx3box.com/api/cms/pvx/item/group", + "method":"GET", + "description":"魔盒交易行物品分组列表", + "params":{ + "client": "std" } }, "jx3_tiebawujia":{ diff --git a/metadata.yaml b/metadata.yaml index 34c33ae..4e8c944 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,7 +1,7 @@ name: astrbot_plugin_jx3 display_name: 剑网三游戏数据查询工具 desc: 通过接口调用剑网三API接口获取游戏数据,处理发送。 -version: v2.6 +version: v2.7 author: 飞翔大野猪 repo: https://github.com/qsc20001102/astrbot_plugin_jx3 astrbot_version: ">=4.11.0" diff --git a/templates/helps.html b/templates/helps.html index 6554bd5..18288a4 100644 --- a/templates/helps.html +++ b/templates/helps.html @@ -837,7 +837,8 @@ body {
4 条 - 需令牌 4 + 免令牌 1 + 需令牌 3
@@ -973,10 +974,10 @@ body {

查询指定外观在服务器内的物价行情。

-
+
交易行
-
需令牌
+
免令牌
交易行 物品名称 [服务器]

查询指定物品交易行价格与挂单信息。

diff --git a/templates/jiaoyihang.html b/templates/jiaoyihang.html index 428ae67..ece25e2 100644 --- a/templates/jiaoyihang.html +++ b/templates/jiaoyihang.html @@ -5,119 +5,209 @@ 剑网3交易行价格查询 -
- -

剑网3交易行价格查询

+
+

交易行:{{ search_name }} · {{ server }}

+
+ 展示 {{ result_count }} 条价格数据 + 匹配 {{ matched_count }} 个物品 + 生成时间:{{ update_time }} +
+
- - - - - - + + + + - {% for item in list %} - - - - - - + + + {% endfor %}
服务器图标物品名称价格数量时间物品物价数量数据时间
{{ item.sever }} - icon + +
+ +
{{ item.name }}
+
{{ item.name }}{{ item.unit_price }}{{ item.count }}{{ item.created }} +
+ {% if item.price_parts %} + {% for part in item.price_parts %} + + {{ part.value }} + {{ part.name }} + + {% endfor %} + {% else %} + 无价格 + {% endif %} +
+
{{ item.sample }}{{ item.created }}
-
-