fix(huya): 查询积分改走积分中心新接口(含成长等级)

旧活动接口 webActUI.getUserScore 对任何 sid 都返回 603「活动已结束」
(接口已下线,与工作台 SID/活动配置无关),刷新积分只会报错。

- 协议层新增两个 RPC(WSS + HTTP 双侧同签名):
  shopMiddleUI.getRedeemNeedGoods(tag2=当前积分、tag3=兑换门槛,
  兑换商品号 POINTS_REDEEM_GOODS_NUM=8315982,传错直接 500)
  与 GameBizUI.getGrowLevelUserInfo(成长值/成长等级/昵称/头像)
- 新增结构体 GetRedeemNeedGoodsReq / GetGrowLevelUserInfoReq,
  与 HuyaEliteWssSession 透传
- 服务层 _execute_query_points 主源改为积分中心接口,结果带
  points/redeem_threshold/grow_level/grow_value,并同步 account.points;
  接口无响应时才回退旧活动接口并如实报告其 603 返回
- 开通支付成功后的积分刷新同样改走积分中心接口
- 前端任务页积分结果优先读 points(兼容旧记录的 available_score),
  并在有成长等级时一并展示
- 新增 tests/test_huya_points_query.py:真机响应字节回归 + 请求参数
  字面量断言 + 服务层积分来源/回退语义(已做反向验证)

真机验证:HTTP 通道取到积分 280 / 门槛 20 / 成长等级 3
This commit is contained in:
yml2213
2026-10-15 15:32:46 +08:00
parent b5d8d5b5d8
commit 905a7ef22c
8 changed files with 508 additions and 21 deletions
+6
View File
@@ -273,5 +273,11 @@ class HuyaEliteWssSession:
def get_follow_wechat_status(self, **kwargs):
return self._run(self.client.get_follow_wechat_status(**kwargs))
def get_redeem_need_goods(self, **kwargs):
return self._run(self.client.get_redeem_need_goods(**kwargs))
def get_grow_level_user_info(self, **kwargs):
return self._run(self.client.get_grow_level_user_info(**kwargs))
__all__ = ["HuyaEliteWssSession"]
+48
View File
@@ -29,6 +29,7 @@ from .wss_client import (
ORDER_BIZ_TYPE,
ORDER_SCENE,
POINTS_PAY_CALLBACK,
POINTS_REDEEM_GOODS_NUM,
SHOP_APP_SCENE,
)
from .wup_protocol import WupRequest, WupResponse
@@ -1448,6 +1449,53 @@ class HuyaHttpClient:
)
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
def get_redeem_need_goods(
self,
uid: int,
guid: str,
cookie: str,
goods_num: int = POINTS_REDEEM_GOODS_NUM,
timeout: float = 15.0,
):
"""shopMiddleUI.getRedeemNeedGoods(当前积分 tag2 / 兑换门槛 tag3)。"""
from .shop_structs import GetRedeemNeedGoodsReq
req = GetRedeemNeedGoodsReq()
req.userId = self._build_shop_user(uid, cookie)
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
req.goodsNum = int(goods_num or 0)
resp = self.call_rpc(
"shopMiddleUI",
"getRedeemNeedGoods",
req,
None,
uid,
guid,
cookie,
timeout=timeout,
)
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
def get_grow_level_user_info(
self, uid: int, guid: str, cookie: str, timeout: float = 15.0
):
"""GameBizUI.getGrowLevelUserInfo(成长值 tag1.tag0 / 等级 tag1.tag1)。"""
from .shop_structs import GetGrowLevelUserInfoReq
req = GetGrowLevelUserInfoReq()
req.userId = self._build_shop_user(uid, cookie)
resp = self.call_rpc(
"GameBizUI",
"getGrowLevelUserInfo",
req,
None,
uid,
guid,
cookie,
timeout=timeout,
)
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
def _self_check():
uid = 1199647239697
+33
View File
@@ -1398,6 +1398,39 @@ class FollowWeChatStatusReq(TafStruct):
pass
class GetRedeemNeedGoodsReq(ShopAppRequest):
"""shopMiddleUI.getRedeemNeedGoods 请求(兑换所需商品 + 当前积分)。
tag2 是兑换商品的数字商品号(抓包为精英宝典兑换商品 ``hy-8315982`` →
``8315982``);传错会返回 500「系统异常」,所以调用方必须给真实商品号。
"""
def __init__(self):
super().__init__()
self.goodsNum: int = 0 # tag 2
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int64(2, self.goodsNum)
def read_from(self, ins: TafInputStream):
pass
class GetGrowLevelUserInfoReq(TafStruct):
"""GameBizUI.getGrowLevelUserInfo 请求(抓包 tReq 只有 tag0 UserId)。"""
def __init__(self):
self.userId = UserId() # tag 0
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
def read_from(self, ins: TafInputStream):
self.userId = ins.read_struct(0, UserId) or self.userId
def shop_spu_number(spu_id: str) -> int:
"""把商品 ID(``hy-8328015``)转成接口需要的数字形式(``8328015``)。"""
text = str(spu_id or "").strip()
+34
View File
@@ -73,6 +73,9 @@ ORDER_BIZ_TYPE = 5
SHOP_APP_SCENE = 7
ORDER_SCENE = 4
PEACE_ELITE_GAME_ID = 9815
# getRedeemNeedGoods 的兑换商品号:抓包里的精英宝典兑换商品 hy-8315982 → 8315982。
# 服务端只认这个商品号(传 0/其它值直接 500「系统异常」)。
POINTS_REDEEM_GOODS_NUM = 8315982
# createOrderV5 的 extraParam.bizParams:标记「积分中心」来源,缺了会走成普通商城单。
JIFENCENTER_BIZ_PARAMS = '{ "jifencenter": 1 }'
@@ -1448,3 +1451,34 @@ class HuyaWssClient:
return await self.call_rpc_raw(
"wupui", "getUserFollowWeChatOfficialStatus", req
)
async def get_redeem_need_goods(
self,
uid: int,
guid: str,
cookie: str,
goods_num: int = POINTS_REDEEM_GOODS_NUM,
):
"""shopMiddleUI.getRedeemNeedGoods — 兑换所需商品与当前积分。
响应:``tag2`` 当前积分、``tag3`` 兑换门槛积分(抓包 + 真机实证)。
"""
from .shop_structs import GetRedeemNeedGoodsReq
req = GetRedeemNeedGoodsReq()
req.userId = self._build_biz_user(uid, cookie)
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
req.goodsNum = int(goods_num or 0)
return await self.call_rpc_raw("shopMiddleUI", "getRedeemNeedGoods", req)
async def get_grow_level_user_info(self, uid: int, guid: str, cookie: str):
"""GameBizUI.getGrowLevelUserInfo — 成长值/成长等级/昵称。
响应:``tag1.tag0`` 成长值、``tag1.tag1`` 成长等级、``tag2`` 昵称、
``tag3`` 头像(抓包 + 真机实证)。
"""
from .shop_structs import GetGrowLevelUserInfoReq
req = GetGrowLevelUserInfoReq()
req.userId = self._build_biz_user(uid, cookie)
return await self.call_rpc_raw("GameBizUI", "getGrowLevelUserInfo", req)
+295
View File
@@ -0,0 +1,295 @@
"""查询积分改走积分中心新接口(2026-10-15 真机实测)。
背景:旧活动接口 ``webActUI.getUserScore`` 对任何 sid 都返回
603「活动已结束」(该接口已下线,与工作台里的 SID/活动配置无关);
积分中心用 ``shopMiddleUI.getRedeemNeedGoods`` 查积分:
``tag2``=当前积分、``tag3``=兑换门槛。下面固化的响应字节来自真机 HTTP 通道。
抓包/真机字面值直接写在测试里(不引用实现常量),避免断言跟着实现漂移。
"""
import asyncio
from core.huya.activity_structs import GetUserScoreResp
from core.huya.frame_decoder import _decode_wup_body, decode_struct_bytes
from core.huya.http_client import HuyaHttpClient
from core.huya.shop_structs import ShopRawRsp
from core.huya.wss_client import HuyaWssClient, WssMessage
from web.backend.models import HuyaAccount
from web.backend.services.huya_runner import HuyaBatchRunner
UID = 1199665170519
GUID = "0a8995af9218ce6a340261946294a9e7"
COOKIE = f"udb_uid={UID}; yyuid={UID}; guid={GUID}"
# 真机(2026-10-15,账号 hy_297832664)getRedeemNeedGoods 响应:200 / 积分 280 / 门槛 20
REDEEM_RSP_HEX = "0a0100c8160ce69fa5e8afa2e68890e58a9f21011830144c50016001790c0b"
# 真机 getGrowLevelUserInfo 响应:成长值 980 / 成长等级 3 / 昵称 / 头像
GROW_RSP_HEX = (
"0a0a0c1606e68890e58a9f0b1a0103d41003230000019f124082043c4c50"
"0360ff7101f48103e80b260fe8bdafe8b88fe697a5e6bdbae6b9b236386874"
"7470733a2f2f6469792d6173736574732e6d737374617469632e636f6d2f6371"
"68666f6c6465722f4e657744656661756c742e706e670b"
)
class _FakeWs:
"""记录发出的帧,并立刻兑现 RPC future(离线跑完整链路)。"""
def __init__(self, client: HuyaWssClient):
self.client = client
self.sent: list[bytes] = []
async def send(self, data: bytes):
self.sent.append(data)
while self.client.rpc_queue:
future = self.client.rpc_queue.popleft()
if not future.done():
future.set_result(b"")
def last_request(self) -> dict:
return _decode_wup_body(WssMessage.decode(self.sent[-1]).body)
def _points_client() -> HuyaWssClient:
client = HuyaWssClient(logger=lambda _message: None)
client._points_mode = True
client.ws = _FakeWs(client)
return client
def _raw_rsp(payload_hex: str) -> ShopRawRsp:
return ShopRawRsp.from_payload(
decode_struct_bytes(bytes.fromhex("".join(payload_hex.split())))
)
class _StubClient:
"""按需返回真机响应字节的积分中心客户端。"""
def __init__(self, redeem=True, grow=True):
self.redeem = _raw_rsp(REDEEM_RSP_HEX) if redeem else None
self.grow = _raw_rsp(GROW_RSP_HEX) if grow else None
self.calls: list[tuple[str, dict]] = []
def get_redeem_need_goods(self, **kwargs):
self.calls.append(("get_redeem_need_goods", kwargs))
return self.redeem
def get_grow_level_user_info(self, **kwargs):
self.calls.append(("get_grow_level_user_info", kwargs))
return self.grow
class _LegacyClient:
"""旧活动接口:真机实测任何 sid 都返回 603「活动已结束」。"""
def query_user_score(self, uid: int, cookie: str, sid: int):
response = GetUserScoreResp()
response.status = 603
response.msg = "活动已结束"
response.score = 0
return response
def _runner(monkeypatch, client, legacy=None) -> tuple[HuyaBatchRunner, list]:
runner = HuyaBatchRunner(db=None, batch_id="test-batch", task_type="query_points")
marks: list[tuple[str, str, dict | None]] = []
monkeypatch.setattr(
runner, "_shop_client", lambda uid, cookie: client, raising=False
)
if legacy is not None:
monkeypatch.setattr(
runner, "_activity_client", lambda uid, cookie: legacy, raising=False
)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
raising=False,
)
return runner, marks
def test_wss_redeem_need_goods_request_matches_real_call():
async def run():
client = _points_client()
await client.get_redeem_need_goods(uid=UID, guid=GUID, cookie=COOKIE)
message = WssMessage.decode(client.ws.sent[-1])
# 积分中心业务帧统一 0x1d000107(只有下单/支付用 0x1d000108)
assert message.sequence == 0x1D000107
decoded = client.ws.last_request()
assert (decoded["servant"], decoded["func"]) == (
"shopMiddleUI",
"getRedeemNeedGoods",
)
t_req = decoded["tReq"]
assert t_req["tag0"]["tag0"] == UID
assert t_req["tag1"]["tag0"] == "huya"
assert t_req["tag1"]["tag4"] == 7
assert t_req["tag1"]["tag5"] == "yellowcarlist"
# 默认兑换商品号必须是真机可用的 hy-8315982 的数字形式
assert t_req["tag2"] == 8315982
asyncio.run(run())
def test_wss_grow_level_request_carries_only_user_id():
async def run():
client = _points_client()
await client.get_grow_level_user_info(uid=UID, guid=GUID, cookie=COOKIE)
decoded = client.ws.last_request()
assert (decoded["servant"], decoded["func"]) == (
"GameBizUI",
"getGrowLevelUserInfo",
)
t_req = decoded["tReq"]
assert t_req["tag0"]["tag0"] == UID
# 抓包 tReq 只有 tag0,没有 shopAppInfo
assert set(t_req) == {"tag0"}
asyncio.run(run())
def test_http_redeem_need_goods_request_matches_real_call():
client = HuyaHttpClient(logger=lambda _message: None)
recorded: list[tuple] = []
def fake_call_rpc(
service, method, req_struct, rsp_class=None, uid=0, guid="", cookie="", **kwargs
):
recorded.append((service, method, req_struct))
client.call_rpc = fake_call_rpc # type: ignore[method-assign]
client.get_redeem_need_goods(uid=UID, guid="", cookie=COOKIE)
service, method, req = recorded[-1]
assert (service, method) == ("shopMiddleUI", "getRedeemNeedGoods")
assert req.userId.lUid == UID
assert req.shopAppInfo.scene == 7
assert req.shopAppInfo.sourceId == "yellowcarlist"
assert req.goodsNum == 8315982
def test_real_redeem_response_bytes_are_points_and_threshold():
rsp = _raw_rsp(REDEEM_RSP_HEX)
assert rsp.code == 200
assert rsp.message == "查询成功"
# tag2 = 当前积分,tag3 = 兑换门槛积分(真机 280 / 20)
assert rsp.value("tag2") == 280
assert rsp.value("tag3") == 20
def test_real_grow_level_response_bytes_are_level_and_nickname():
rsp = _raw_rsp(GROW_RSP_HEX)
assert rsp.code == 0
assert rsp.message == "成功"
assert rsp.value("tag1", "tag0") == 980 # 成长值
assert rsp.value("tag1", "tag1") == 3 # 成长等级
assert rsp.value("tag2") == "软踏日潺湲"
assert str(rsp.value("tag3")).startswith("https://diy-assets.msstatic.com/")
def test_query_points_reads_points_from_points_center(monkeypatch):
client = _StubClient()
runner, marks = _runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None, None, account, {"uid": UID, "cookie": COOKIE}, {}
)
assert [name for name, _kwargs in client.calls] == [
"get_redeem_need_goods",
"get_grow_level_user_info",
]
assert client.calls[0][1]["goods_num"] == 8315982
status, message, result = marks[-1]
assert status == "success"
assert message == "积分: 280"
assert account.points == 280
assert result["points"] == 280
assert result["redeem_threshold"] == 20
assert result["grow_level"] == 3
assert result["grow_value"] == 980
assert result["nickname"] == "软踏日潺湲"
def test_query_points_keeps_working_when_grow_level_fails(monkeypatch):
client = _StubClient(grow=False)
runner, marks = _runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None, None, account, {"uid": UID, "cookie": COOKIE}, {}
)
status, message, result = marks[-1]
assert status == "success"
assert message == "积分: 280"
assert "grow_level" not in result
def test_query_points_reports_server_error_of_points_center(monkeypatch):
class _FailingClient(_StubClient):
def get_redeem_need_goods(self, **kwargs):
self.calls.append(("get_redeem_need_goods", kwargs))
return ShopRawRsp.from_payload(
decode_struct_bytes(
bytes.fromhex(
"0a0101f4161ee7b3bbe7bb9fe5bc82e5b8b8efbc8ce8afb7e7a88de5"
"908ee9878de8af952c3c4c5c6c0b"
)
)
)
client = _FailingClient()
runner, marks = _runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None, None, account, {"uid": UID, "cookie": COOKIE}, {}
)
status, message, result = marks[-1]
assert status == "failed"
assert message == "系统异常,请稍后重试"
assert result["code"] == 500
assert result["goods_num"] == 8315982
def test_query_points_fallback_reports_legacy_interface_offline(monkeypatch):
client = _StubClient(redeem=False)
runner, marks = _runner(monkeypatch, client, legacy=_LegacyClient())
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None, None, account, {"uid": UID, "cookie": COOKIE}, {"sid": "2203"}
)
status, message, _result = marks[-1]
assert status == "failed"
# 旧接口的真实返回必须如实暴露,而不是只报「活动已结束」
assert "旧活动接口(sid=2203)返回 603 活动已结束" in message
assert "积分中心接口无响应" in message
def test_query_points_without_sid_reports_points_center_offline(monkeypatch):
client = _StubClient(redeem=False)
runner, marks = _runner(monkeypatch, client, legacy=_LegacyClient())
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None, None, account, {"uid": UID, "cookie": COOKIE}, {}
)
status, message, _result = marks[-1]
assert status == "error"
assert "积分中心接口无响应" in message
+81 -12
View File
@@ -8,6 +8,8 @@ from typing import TYPE_CHECKING, Any
from sqlalchemy.orm import Session
from core.huya.wss_client import POINTS_REDEEM_GOODS_NUM
from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask
@@ -26,6 +28,7 @@ class GoodsMixin:
def _resolve_uid(self, account_info: dict) -> int: ...
def _push_log(self, level: str, message: str) -> None: ...
def _activity_client(self, uid: int, cookie: str): ...
def _shop_client(self, uid: int, cookie: str): ...
def _mark_task(self, *args: Any, **kwargs: Any) -> None: ...
def _wait_until(self, when: datetime, uid: int) -> bool: ...
def _parse_scheduled_time(self, value: Any) -> datetime | None: ...
@@ -90,16 +93,13 @@ class GoodsMixin:
account_info: dict,
config_info: dict,
):
sid = str(self.payload.get("sid") or config_info.get("sid") or "").strip()
if not sid:
self._mark_task(worker_db, task, "failed", "请先配置虎牙活动 SID")
return
sid_int = self._to_int(sid)
if not sid_int:
self._mark_task(worker_db, task, "failed", f"虎牙活动 SID 无效: {sid}")
return
"""查询积分:主源是积分中心的 shopMiddleUI.getRedeemNeedGoods。
2026-10-15 真机实测:旧活动接口 webActUI.getUserScore 对任何 sid 都返回
603「活动已结束」(该接口已下线,与配置无关);积分中心接口返回
``tag2``=当前积分、``tag3``=兑换门槛积分,另可顺手取 GameBizUI
的成长值/成长等级。
"""
uid = self._resolve_uid(account_info)
if not uid:
self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid")
@@ -110,10 +110,76 @@ class GoodsMixin:
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
return
goods_num = (
self._to_int(self.payload.get("goods_num")) or POINTS_REDEEM_GOODS_NUM
)
client: Any = self._shop_client(uid, cookie)
redeem = client.get_redeem_need_goods(
uid=uid, guid="", cookie=cookie, goods_num=goods_num
)
if redeem is None:
self._query_points_legacy(
worker_db, task, account, uid, cookie, config_info
)
return
result = redeem.to_dict()
result["goods_num"] = goods_num
if redeem.code != 200:
self._mark_task(
worker_db,
task,
"failed",
redeem.message or f"虎牙积分查询失败: {redeem.code}",
result,
)
return
points = self._to_int(redeem.value("tag2"))
result["points"] = points
result["redeem_threshold"] = self._to_int(redeem.value("tag3"))
growth = client.get_grow_level_user_info(uid=uid, guid="", cookie=cookie)
if growth is not None and growth.code == 0:
result["grow_value"] = self._to_int(growth.value("tag1", "tag0"))
result["grow_level"] = self._to_int(growth.value("tag1", "tag1"))
result["nickname"] = growth.value("tag2") or ""
account.points = points
account.status = "points_queried"
account.updated_at = datetime.now(UTC)
self._mark_task(worker_db, task, "success", f"积分: {points}", result)
def _query_points_legacy(
self,
worker_db: Session,
task: HuyaTask,
account: HuyaAccount,
uid: int,
cookie: str,
config_info: dict,
):
"""积分中心接口没有响应时回退旧活动接口,并如实报告旧接口的返回。"""
sid = str(self.payload.get("sid") or config_info.get("sid") or "").strip()
if not sid:
self._mark_task(
worker_db, task, "error", "积分中心接口无响应,且未配置可回退的活动 SID"
)
return
sid_int = self._to_int(sid)
if not sid_int:
self._mark_task(worker_db, task, "failed", f"虎牙活动 SID 无效: {sid}")
return
client: Any = self._activity_client(uid, cookie)
response = client.query_user_score(uid=uid, cookie=cookie, sid=sid_int)
if response is None:
self._mark_task(worker_db, task, "error", "虎牙积分接口无响应")
self._mark_task(
worker_db,
task,
"error",
"虎牙积分接口无响应(积分中心与旧活动接口均失败)",
)
return
result = response.to_dict()
@@ -123,7 +189,8 @@ class GoodsMixin:
worker_db,
task,
"failed",
response.msg or f"虎牙积分查询失败: {response.status}",
f"积分中心接口无响应;旧活动接口(sid={sid_int})返回 "
f"{response.status} {response.msg}".strip(),
result,
)
return
@@ -132,7 +199,9 @@ class GoodsMixin:
account.points = points
account.status = "points_queried"
account.updated_at = datetime.now(UTC)
self._mark_task(worker_db, task, "success", f"积分: {points}", result)
self._mark_task(
worker_db, task, "success", f"积分: {points}(旧活动接口)", result
)
def _execute_query_exchange_records(
self,
+6 -6
View File
@@ -506,12 +506,12 @@ class RechargeMixin:
account.updated_at = datetime.now(UTC)
if payment_status == "paid":
account.status = "recharge_paid"
post_score = client.query_user_score(
uid=uid, cookie=cookie, sid=self._to_int(config_info.get("sid")) or 2203
)
if post_score is not None:
result["post_payment_score"] = post_score.to_dict()
account.points = post_score.available_score
# 支付后刷新积分:旧活动接口 getUserScore 已下线(603 活动已结束),
# 改走积分中心 getRedeemNeedGoods(tag2 = 当前积分)。
post_redeem = client.get_redeem_need_goods(uid=uid, guid="", cookie=cookie)
if post_redeem is not None and post_redeem.code == 200:
result["post_payment_points"] = self._to_int(post_redeem.value("tag2"))
account.points = result["post_payment_points"]
paid_message = f"支付成功: {product_name} x{count} {result['amount_text']}"
if payment_order and payment_order.get("pay_time"):
paid_message += f",支付时间 {self._format_local_time(int(payment_order['pay_time']) // 1000)}"
+5 -3
View File
@@ -1136,9 +1136,11 @@ export default function HuyaTasksPage() {
return <Text type="secondary">-</Text>;
}
const availableScore = value?.available_score;
if (typeof availableScore === 'number') {
return <Tag color="blue">可用积分 {availableScore}</Tag>;
// 积分结果:新版走积分中心(points),旧任务记录里是活动接口的 available_score
const points = typeof value?.points === 'number' ? value.points : value?.available_score;
if (typeof points === 'number') {
const level = value?.grow_level;
return <Tag color="blue">可用积分 {points}{typeof level === 'number' ? ` · 等级 ${level}` : ''}</Tag>;
}
return value ? <Text code style={{ fontSize: 12 }}>{JSON.stringify(value)}</Text> : <Text type="secondary">-</Text>;