This commit is contained in:
2026-06-24 14:34:17 +08:00
parent d8ceb93ea7
commit 97ad05baa5
8 changed files with 358 additions and 96 deletions
+10
View File
@@ -1,5 +1,15 @@
## 更新记录
### version: 2.7
重构 **交易行 物品名称 [服务器]** 指令,改为免令牌图片查询。
新增 JX3BOX 交易行物品库缓存,用户输入物品名后先在本地模糊匹配物品 ID,再批量请求交易行价格接口。
交易行结果图改为列表展示,包含物品图标与名称、砖金银铜图标化价格、数量和数据时间。
价格接口未返回的物品不再展示,全部无价格数据时返回明确提示。
### version: 2.6
新增 **资历 角色名称 [服务器]** 指令,支持两轮对话选择资历总览或分类总览,并以图片形式展示角色资历点数完成进度。
+19 -2
View File
@@ -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 实现轮询推送。
+32 -1
View File
@@ -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
return icons
+132 -27
View File
@@ -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"] = "处理交易行数据失败"
+13 -5
View File
@@ -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":{
+1 -1
View File
@@ -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"
+4 -3
View File
@@ -837,7 +837,8 @@ body {
</div>
<div class="section-meta">
<span class="mini">4 条</span>
<span class="mini">令牌 4</span>
<span class="mini">令牌 1</span>
<span class="mini">需令牌 3</span>
</div>
</div>
<div class="command-grid">
@@ -973,10 +974,10 @@ body {
<p class="desc">查询指定外观在服务器内的物价行情。</p>
</article>
<article class="cmd-card advanced-source">
<article class="cmd-card basic-source">
<div class="cmd-top">
<div class="cmd-name">交易行</div>
<div class="source-tag">令牌</div>
<div class="source-tag">令牌</div>
</div>
<code class="usage">交易行 物品名称 [服务器]</code>
<p class="desc">查询指定物品交易行价格与挂单信息。</p>
+147 -57
View File
@@ -5,119 +5,209 @@
<title>剑网3交易行价格查询</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background: #f1f2f6;
background: #f3f4f7;
margin: 0;
padding: 20px;
padding: 24px;
color: #1f2933;
}
.container {
max-width: 1220px;
width: 1120px;
margin: 0 auto;
text-align: center;
}
.header {
background: #ffffff;
border: 1px solid #e6e8ee;
border-radius: 8px;
padding: 22px 26px;
margin-bottom: 16px;
box-shadow: 0 6px 18px rgba(35, 38, 47, 0.06);
}
h1 {
font-size: 32px;
margin-bottom: 10px;
color: #222;
margin: 0;
font-size: 30px;
line-height: 1.3;
font-weight: 800;
color: #20232a;
}
.server-info {
font-size: 16px;
color: #555;
margin-bottom: 25px;
.meta {
display: flex;
gap: 18px;
margin-top: 10px;
font-size: 15px;
color: #667085;
}
/* ===== 表格样式 ===== */
.table-wrapper {
overflow-x: auto;
background: #ffffff;
border: 1px solid #e6e8ee;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 6px 18px rgba(35, 38, 47, 0.06);
}
table {
width: 100%;
border-collapse: collapse;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
text-align: left;
table-layout: fixed;
}
thead {
background: #d93939;
background: #b92b35;
color: #ffffff;
font-size: 18px;
}
th, td {
padding: 14px 12px;
border-bottom: 1px solid #eee;
th {
padding: 14px 16px;
font-size: 16px;
text-align: center;
font-weight: 700;
text-align: left;
white-space: nowrap;
}
tbody tr:hover {
background: #fff2f2;
th:nth-child(2),
th:nth-child(3),
th:nth-child(4),
td:nth-child(2),
td:nth-child(3),
td:nth-child(4) {
text-align: center;
}
/* ===== 物品列 ===== */
.item-col {
text-align: left;
font-weight: 600;
color: #333;
}
.item-col img {
width: 22px;
height: 22px;
td {
padding: 14px 16px;
border-bottom: 1px solid #edf0f5;
font-size: 15px;
vertical-align: middle;
border-radius: 4px;
margin-right: 8px;
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:nth-child(even) {
background: #fafbfc;
}
.item {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.item-icon {
width: 34px;
height: 34px;
border-radius: 6px;
flex: 0 0 auto;
background: #edf0f5;
}
.item-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700;
color: #20232a;
}
.price {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
font-weight: 700;
color: #2f343d;
}
.money-part {
display: inline-flex;
align-items: center;
gap: 2px;
margin-right: 3px;
}
.money-icon {
width: 17px;
height: 17px;
object-fit: contain;
}
.sample {
font-weight: 700;
color: #2f343d;
}
.time {
color: #667085;
font-size: 14px;
white-space: nowrap;
}
</style>
</head>
<body>
<div class="container">
<h1>剑网3交易行价格查询</h1>
<div class="header">
<h1>交易行:{{ search_name }} · {{ server }}</h1>
<div class="meta">
<span>展示 {{ result_count }} 条价格数据</span>
<span>匹配 {{ matched_count }} 个物品</span>
<span>生成时间:{{ update_time }}</span>
</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>服务器</th>
<th>图标</th>
<th>物品名称</th>
<th>价格</th>
<th>数量</th>
<th>时间</th>
<th style="width: 44%;">物品</th>
<th style="width: 24%;">物价</th>
<th style="width: 12%;">数量</th>
<th style="width: 20%;">数据时间</th>
</tr>
</thead>
<tbody>
{% for item in list %}
<tr>
<td>{{ item.sever }}</td>
<td class="item-col">
<img src="{{ item.icon }}" alt="icon">
<td>
<div class="item">
<img class="item-icon" src="{{ item.icon }}" alt="">
<div class="item-name">{{ item.name }}</div>
</div>
</td>
<td>{{ item.name }}</td>
<td>{{ item.unit_price }}</td>
<td>{{ item.count }}</td>
<td>{{ item.created }}</td>
<td>
<div class="price">
{% if item.price_parts %}
{% for part in item.price_parts %}
<span class="money-part">
<span>{{ part.value }}</span>
<img class="money-icon" src="{{ icons.img[part.key] }}" alt="{{ part.name }}">
</span>
{% endfor %}
{% else %}
<span>无价格</span>
{% endif %}
</div>
</td>
<td class="sample">{{ item.sample }}</td>
<td class="time">{{ item.created }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</body>
</html>