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交易行价格查询
+
- | 服务器 |
- 图标 |
- 物品名称 |
- 价格 |
- 数量 |
- 时间 |
+ 物品 |
+ 物价 |
+ 数量 |
+ 数据时间 |
-
{% for item in list %}
- | {{ item.sever }} |
-
-
+ |
+
+ 
+ {{ item.name }}
+
|
- {{ item.name }} |
- {{ item.unit_price }} |
- {{ item.count }} |
- {{ item.created }} |
+
+
+ {% if item.price_parts %}
+ {% for part in item.price_parts %}
+
+ {{ part.value }}
+
+
+ {% endfor %}
+ {% else %}
+ 无价格
+ {% endif %}
+
+ |
+ {{ item.sample }} |
+ {{ item.created }} |
{% endfor %}
-
-