cams_charge_get_user_stub_groups 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cams_charge_get_user_stub_groups-0.1.0/.gitignore +7 -0
- cams_charge_get_user_stub_groups-0.1.0/PKG-INFO +10 -0
- cams_charge_get_user_stub_groups-0.1.0/curl.sh +13 -0
- cams_charge_get_user_stub_groups-0.1.0/pyproject.toml +19 -0
- cams_charge_get_user_stub_groups-0.1.0/src/cams_charge_get_user_stub_groups/__init__.py +1 -0
- cams_charge_get_user_stub_groups-0.1.0/src/cams_charge_get_user_stub_groups/client.py +304 -0
- cams_charge_get_user_stub_groups-0.1.0/src/cams_charge_get_user_stub_groups/server.py +29 -0
- cams_charge_get_user_stub_groups-0.1.0/tests/test_tool.py +32 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cams_charge_get_user_stub_groups
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Charging assistant MCP: get_user_stub_groups
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: fastmcp<4,>=3.4
|
|
7
|
+
Requires-Dist: requests>=2.31
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
10
|
+
Requires-Dist: responses>=0.25; extra == 'dev'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 测试 get_user_stub_groups: GET <BASE>/mts/ai/getUserStubGroups?userId=${USER_ID}&lng=${LNG}&lat=${LAT}
|
|
3
|
+
# 用法: CHARGE_BACKEND_BASE_URL=http://<后端host> bash curl.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
BASE="${CHARGE_BACKEND_BASE_URL:-http://localhost:8080}"
|
|
6
|
+
USER_ID="${USER_ID:-77d12b73-7f5a-4abb-97e2-63a13d3d2732}"
|
|
7
|
+
STUB_ID="${STUB_ID:-2023100909}"
|
|
8
|
+
LAT="${LAT:-31.641665}"
|
|
9
|
+
LNG="${LNG:-119.889879}"
|
|
10
|
+
CHARGE_ID="${CHARGE_ID:-<从 exec_start_charging 返回取>}"
|
|
11
|
+
STATION_ID="${STATION_ID:-<从 get_nearby_station 返回取>}"
|
|
12
|
+
echo "-> GET ${BASE}/mts/ai/getUserStubGroups?userId=${USER_ID}&lng=${LNG}&lat=${LAT}"
|
|
13
|
+
curl -sS "${BASE}/mts/ai/getUserStubGroups?userId=${USER_ID}&lng=${LNG}&lat=${LAT}" | python3 -m json.tool
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cams_charge_get_user_stub_groups"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Charging assistant MCP: get_user_stub_groups"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = ["fastmcp>=3.4,<4", "requests>=2.31"]
|
|
7
|
+
|
|
8
|
+
[project.scripts]
|
|
9
|
+
cams-charge-get-user-stub-groups = "cams_charge_get_user_stub_groups.server:main"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/cams_charge_get_user_stub_groups"]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = ["pytest>=8", "responses>=0.25"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""cams_charge_get_user_stub_groups MCP server."""
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Vendored charging-backend HTTP client + response normalizer.
|
|
2
|
+
|
|
3
|
+
CANONICAL COPY: mcps/common/client.py
|
|
4
|
+
Copied verbatim into every cams_charge_* package at src/<pkg>/client.py.
|
|
5
|
+
Edit here, then re-copy into each package (keep them byte-identical).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# 桩状态码 → 中文标签(对齐后端 StubStatusMenu.java)。00=空闲是唯一可启动状态。
|
|
16
|
+
STATUS_LABELS: dict[str, str] = {
|
|
17
|
+
"00": "空闲", "01": "充电中", "02": "故障", "03": "车位占用",
|
|
18
|
+
"04": "维护中", "10": "维修中", "05": "离线", "06": "在建中",
|
|
19
|
+
"07": "桩程序升级", "08": "预约等待充电中", "09": "储能中",
|
|
20
|
+
"99": "删除", "ff": "锁定不允许充电", "0A": "离线启动",
|
|
21
|
+
"0B": "已插枪", "0D": "已插枪", "0C": "在线预约中",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# 后端成功 code(字符串 "200",非 0)
|
|
25
|
+
_OK_CODES = {"200", "0"}
|
|
26
|
+
|
|
27
|
+
# 预约订单状态(UserStubReservationStatus.java):0 已取消 / 1 预约中 / 2 预约保留中 / 3 预约中超时 / 9 已充电
|
|
28
|
+
RESERVATION_STATUS = {"0": "已取消", "1": "预约中", "2": "预约保留中", "3": "预约中超时", "9": "已充电"}
|
|
29
|
+
_RESERVING = {"1", "2"} # judgeIsReserving:进行中
|
|
30
|
+
|
|
31
|
+
# 地锁物理状态(ParkinglockStatusEnum.java,stubInfo.lockStatus)
|
|
32
|
+
LOCK_STATUS = {"0": "已降下", "1": "已升起", "2": "故障", "3": "已锁车", "4": "已休眠", "5": "未知", "6": "离网", "9": "占用中"}
|
|
33
|
+
_LOCK_FREE = {"1", "4"} # checkIsFree:地锁空闲(不挡车)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _base_url() -> str:
|
|
37
|
+
val = os.environ.get("CHARGE_BACKEND_BASE_URL", "").strip().rstrip("/")
|
|
38
|
+
if not val:
|
|
39
|
+
raise RuntimeError("CHARGE_BACKEND_BASE_URL 未设置")
|
|
40
|
+
return val
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _servlet() -> str:
|
|
44
|
+
return os.environ.get("CHARGE_BACKEND_SERVLET", "mts").strip().strip("/")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _timeout() -> float:
|
|
48
|
+
try:
|
|
49
|
+
return float(os.environ.get("CHARGE_BACKEND_TIMEOUT", "10"))
|
|
50
|
+
except ValueError:
|
|
51
|
+
return 10.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _token() -> str | None:
|
|
55
|
+
return os.environ.get("CHARGE_BACKEND_TOKEN") or None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolved_method(default: str) -> str:
|
|
59
|
+
return os.environ.get("CHARGE_METHOD_NAME") or default
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _coerce_form(form: Mapping[str, Any] | None) -> dict[str, str]:
|
|
63
|
+
out: dict[str, str] = {}
|
|
64
|
+
if not form:
|
|
65
|
+
return out
|
|
66
|
+
for k, v in form.items():
|
|
67
|
+
if v is None:
|
|
68
|
+
continue
|
|
69
|
+
out[k] = v if isinstance(v, str) else str(v)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def request_platform_api(
|
|
74
|
+
default_method_name: str,
|
|
75
|
+
form: Mapping[str, Any] | None = None,
|
|
76
|
+
*,
|
|
77
|
+
method_name_override: str | None = None,
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""调用一个充电后台 methodName。绝不向调用方抛异常;始终返回结果 dict。
|
|
80
|
+
|
|
81
|
+
返回:
|
|
82
|
+
{"ok": True, "data": <data>} 成功(code=="200")
|
|
83
|
+
{"ok": False, "code": str, "msg": str, "suggestion": str} 业务失败(code!=200)
|
|
84
|
+
{"ok": False, "error": str, "suggestion": str} 网络/解析错误
|
|
85
|
+
|
|
86
|
+
请求方式: GET, 参数以 query 串传递 (application/x-www-form-urlencoded)。
|
|
87
|
+
后端信封: {"code":"200","text":...,"data":...,"pageLimit":null},成功 code="200"。
|
|
88
|
+
"""
|
|
89
|
+
method_name = method_name_override or _resolved_method(default_method_name)
|
|
90
|
+
url = f"{_base_url()}/{_servlet()}/{method_name.lstrip('/')}"
|
|
91
|
+
payload = _coerce_form(form)
|
|
92
|
+
headers = {}
|
|
93
|
+
token = _token()
|
|
94
|
+
if token:
|
|
95
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
resp = requests.get(url, params=payload, headers=headers, timeout=_timeout())
|
|
99
|
+
except requests.RequestException as exc:
|
|
100
|
+
return {"ok": False, "error": f"网络请求失败: {exc}",
|
|
101
|
+
"suggestion": "请稍后重试;持续失败请联系平台排查后端连通性。"}
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
body = resp.json()
|
|
105
|
+
except ValueError:
|
|
106
|
+
return {"ok": False, "error": f"后端返回非JSON (HTTP {resp.status_code})",
|
|
107
|
+
"suggestion": "后端可能返回HTML错误页;请检查 methodName 与 servlet 配置。"}
|
|
108
|
+
|
|
109
|
+
if isinstance(body, dict) and "code" in body:
|
|
110
|
+
code = body.get("code")
|
|
111
|
+
msg = body.get("text") or body.get("msg", "")
|
|
112
|
+
if str(code) in _OK_CODES:
|
|
113
|
+
return {"ok": True, "data": body.get("data")}
|
|
114
|
+
return {"ok": False, "code": code, "msg": msg,
|
|
115
|
+
"suggestion": _biz_suggestion(code, msg)}
|
|
116
|
+
return {"ok": True, "data": body}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _biz_suggestion(code: Any, msg: str) -> str:
|
|
120
|
+
return f"业务失败 code={code}: {msg}。可引导用户重试或更换目标桩/场站。"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ───────────────────────── 归一化(精简字段,去噪) ─────────────────────────
|
|
124
|
+
|
|
125
|
+
def status_label(code: Any) -> str:
|
|
126
|
+
if code is None:
|
|
127
|
+
return "未知"
|
|
128
|
+
return STATUS_LABELS.get(str(code), str(code))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def is_idle(code: Any) -> bool:
|
|
132
|
+
return str(code) == "00"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _norm_stub(s: Any) -> Any:
|
|
136
|
+
if not isinstance(s, dict):
|
|
137
|
+
return s
|
|
138
|
+
status = s.get("status")
|
|
139
|
+
pl = s.get("parkingLockId")
|
|
140
|
+
si = s.get("stubInfo") if isinstance(s.get("stubInfo"), dict) else {}
|
|
141
|
+
lock_status = si.get("lockStatus")
|
|
142
|
+
lock_s = str(lock_status) if lock_status not in (None, "") else None
|
|
143
|
+
return {
|
|
144
|
+
"stubId": s.get("id"),
|
|
145
|
+
"name": s.get("name"),
|
|
146
|
+
"kw": s.get("kw"),
|
|
147
|
+
"chargingMode": s.get("chargingMode"),
|
|
148
|
+
"status": status,
|
|
149
|
+
"statusLabel": status_label(status),
|
|
150
|
+
"idle": is_idle(status),
|
|
151
|
+
"reservable": str(s.get("reservable")) in ("1", "true", "True"),
|
|
152
|
+
"hasLock": bool(pl),
|
|
153
|
+
"parkingLockId": pl,
|
|
154
|
+
"lockStatus": lock_status,
|
|
155
|
+
"lockStatusLabel": LOCK_STATUS.get(lock_s, lock_s) if lock_s else None,
|
|
156
|
+
"lockFree": lock_s in _LOCK_FREE,
|
|
157
|
+
"isVinPlugAndCharge": bool(s.get("isVinPlugAndCharge")),
|
|
158
|
+
"totalFee": s.get("totalFee"),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _norm_stub_detail(s: Any) -> Any:
|
|
163
|
+
"""桩详情(get_stub_info,单桩):在 _norm_stub 基础上补场站上下文 + 充电订单号/开始时间。"""
|
|
164
|
+
base = _norm_stub(s)
|
|
165
|
+
if not isinstance(base, dict):
|
|
166
|
+
return base
|
|
167
|
+
si = s.get("stubInfo") if isinstance(s.get("stubInfo"), dict) else {}
|
|
168
|
+
base["stubGroupId"] = s.get("stubGroupId")
|
|
169
|
+
base["stubGroupName"] = s.get("stubGroupName")
|
|
170
|
+
base["stubGroupAddress"] = s.get("stubGroupAddress")
|
|
171
|
+
base["chargeOrderId"] = si.get("orderId") # 充电中(status=01)时的充电订单号,可作 exec_stop 的 chargeId 兜底
|
|
172
|
+
base["chargeStartTime"] = si.get("chargeStartTime")
|
|
173
|
+
return base
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _norm_station(s: Any) -> Any:
|
|
177
|
+
if not isinstance(s, dict):
|
|
178
|
+
return s
|
|
179
|
+
stubs = s.get("stubList") or []
|
|
180
|
+
reservable_raw = s.get("reservable")
|
|
181
|
+
if reservable_raw is None:
|
|
182
|
+
reservable_raw = s.get("isReservable") # nearby/user_stub_groups 用 isReservable
|
|
183
|
+
price = s.get("currentPrice")
|
|
184
|
+
return {
|
|
185
|
+
"stationId": s.get("id"),
|
|
186
|
+
"name": s.get("name"),
|
|
187
|
+
"address": s.get("address"),
|
|
188
|
+
"lat": s.get("gisGcj02Lat"),
|
|
189
|
+
"lng": s.get("gisGcj02Lng"),
|
|
190
|
+
"distance": s.get("distance"),
|
|
191
|
+
"serviceTime": s.get("serviceTime"),
|
|
192
|
+
"parkingFree": s.get("parkingFree"),
|
|
193
|
+
"parkingFeeInfo": s.get("parkingFeeInfo"),
|
|
194
|
+
"reservable": reservable_raw in (True, "true", "1", 1),
|
|
195
|
+
"isFavorite": bool(s.get("isFavorite")),
|
|
196
|
+
"notice": s.get("stubGroupInfo") or s.get("noticInfo"),
|
|
197
|
+
"currentPrice": price.get("value") if isinstance(price, dict) else None,
|
|
198
|
+
"totalFee": s.get("totalFee"),
|
|
199
|
+
"placeholderInfo": s.get("placeholderInfo"),
|
|
200
|
+
"stubCount": len(stubs),
|
|
201
|
+
"stubs": [_norm_stub(x) for x in stubs],
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _norm_station_list(data: Any) -> Any:
|
|
206
|
+
"""场站列表归一;null/非列表 → [](getUserStubGroups 的 userPreferStubGroupList 可为 null)。"""
|
|
207
|
+
if isinstance(data, list):
|
|
208
|
+
return [_norm_station(s) for s in data]
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _norm_user_stub_groups(data: Any) -> Any:
|
|
213
|
+
if not isinstance(data, dict):
|
|
214
|
+
return data
|
|
215
|
+
return {
|
|
216
|
+
"near": _norm_station_list(data.get("nearStubGroupList")),
|
|
217
|
+
"favorite": _norm_station_list(data.get("favoriteStubGroupList")),
|
|
218
|
+
"prefer": _norm_station_list(data.get("userPreferStubGroupList")),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _norm_exec(data: Any) -> Any:
|
|
223
|
+
"""exec_* 成功返回的订单对象:best-effort 提取 charge/order id(字段名待成功样本确认)。"""
|
|
224
|
+
if isinstance(data, dict):
|
|
225
|
+
for k in ("chargeId", "chargeNo", "orderId", "orderNo", "chargeOrderId", "id"):
|
|
226
|
+
if data.get(k):
|
|
227
|
+
return {"id": data.get(k), "raw": data}
|
|
228
|
+
return data
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _norm_reservation(data: Any) -> Any:
|
|
232
|
+
"""预约订单(get_reserve_order,单个对象或 null):提取关键字段 + 状态映射。"""
|
|
233
|
+
if not isinstance(data, dict):
|
|
234
|
+
return data
|
|
235
|
+
status = data.get("status")
|
|
236
|
+
status_s = str(status) if status is not None else None
|
|
237
|
+
return {
|
|
238
|
+
"reservationId": data.get("id"),
|
|
239
|
+
"userId": data.get("accountId"),
|
|
240
|
+
"stubId": data.get("stubId"),
|
|
241
|
+
"stubName": data.get("stubName"),
|
|
242
|
+
"stubGroupId": data.get("stubGroupId"),
|
|
243
|
+
"stubGroupName": data.get("stubGroupName"),
|
|
244
|
+
"address": data.get("address"),
|
|
245
|
+
"carId": data.get("carId"),
|
|
246
|
+
"licencePlate": data.get("licencePlate"),
|
|
247
|
+
"brandName": data.get("brandName"),
|
|
248
|
+
"status": status,
|
|
249
|
+
"statusLabel": RESERVATION_STATUS.get(status_s, status_s) if status_s else "未知",
|
|
250
|
+
"active": status_s in _RESERVING,
|
|
251
|
+
"reservateTime": data.get("reservateTime"),
|
|
252
|
+
"reserveEndTime": data.get("reserveEndTime"),
|
|
253
|
+
"reserveMinutes": data.get("reserveMinutes"),
|
|
254
|
+
"unlockTime": data.get("unlockTime"),
|
|
255
|
+
"cancelTime": data.get("cancelTime"),
|
|
256
|
+
"lat": data.get("gisGcj02Lat"),
|
|
257
|
+
"lng": data.get("gisGcj02Lng"),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _norm_parking_order(data: Any) -> Any:
|
|
262
|
+
"""地锁订单(ParkingOrder):提取关键字段。"""
|
|
263
|
+
if not isinstance(data, dict):
|
|
264
|
+
return data
|
|
265
|
+
return {
|
|
266
|
+
"parkingOrderId": data.get("parkingOrderId"),
|
|
267
|
+
"id": data.get("id"),
|
|
268
|
+
"chargeOrderId": data.get("chargeOrderId"),
|
|
269
|
+
"stubId": data.get("stubId"),
|
|
270
|
+
"userId": data.get("userId"),
|
|
271
|
+
"startTime": data.get("startTime"),
|
|
272
|
+
"endTime": data.get("endTime"),
|
|
273
|
+
"status": data.get("status"),
|
|
274
|
+
"createTime": data.get("createTime"),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _norm_lock_orders(data: Any) -> Any:
|
|
279
|
+
"""get_lock_order:ParkingOrder 列表归一;空列表([])表示无进行中地锁订单。"""
|
|
280
|
+
if isinstance(data, list):
|
|
281
|
+
return [_norm_parking_order(x) for x in data]
|
|
282
|
+
return data
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def normalize(tool: str, data: Any) -> Any:
|
|
286
|
+
"""按工具名把后端原始 data 归一为精简结构。未知/空样本透传。"""
|
|
287
|
+
if data is None:
|
|
288
|
+
return None
|
|
289
|
+
if tool == "get_nearby_station":
|
|
290
|
+
return _norm_station_list(data)
|
|
291
|
+
if tool == "get_user_stub_groups":
|
|
292
|
+
return _norm_user_stub_groups(data)
|
|
293
|
+
if tool == "get_station_detail":
|
|
294
|
+
return _norm_station(data)
|
|
295
|
+
if tool == "get_stub_info":
|
|
296
|
+
return _norm_stub_detail(data)
|
|
297
|
+
if tool in ("exec_reserve_stub", "exec_unlock_ground",
|
|
298
|
+
"exec_start_charging", "exec_stop_charging"):
|
|
299
|
+
return _norm_exec(data)
|
|
300
|
+
if tool == "get_reserve_order":
|
|
301
|
+
return _norm_reservation(data)
|
|
302
|
+
if tool == "get_lock_order":
|
|
303
|
+
return _norm_lock_orders(data)
|
|
304
|
+
return data
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from fastmcp import FastMCP
|
|
2
|
+
|
|
3
|
+
from .client import request_platform_api, normalize
|
|
4
|
+
|
|
5
|
+
mcp = FastMCP("get-user-stub-groups")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_user_stub_groups(user_id: str, user_lat: float, user_lng: float) -> dict:
|
|
9
|
+
"""工具名称: get_user_stub_groups
|
|
10
|
+
核心功能: 查询用户电站:一次返回附近/收藏/常用三个列表(每个场站含桩列表 stubs[])。用户说「我收藏的」「常去」「我的电站」时调用。
|
|
11
|
+
后端接口: GET {BASE}/{servlet}/ai/getUserStubGroups query: userId, lng, lat
|
|
12
|
+
入参: user_id: str, user_lat: float, user_lng: float
|
|
13
|
+
返回: {"ok":True,"data":<归一化结果>} 或 {"ok":False,"code":..,"msg":..,"suggestion":..} / {"ok":False,"error":..,"suggestion":..}
|
|
14
|
+
"""
|
|
15
|
+
r = request_platform_api("ai/getUserStubGroups", form={"userId": user_id, "lng": user_lng, "lat": user_lat})
|
|
16
|
+
if r.get("ok"):
|
|
17
|
+
r["data"] = normalize("get_user_stub_groups", r.get("data"))
|
|
18
|
+
return r
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
mcp.tool(name="get_user_stub_groups", description=get_user_stub_groups.__doc__)(get_user_stub_groups)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
mcp.run()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import responses
|
|
2
|
+
|
|
3
|
+
from cams_charge_get_user_stub_groups.server import get_user_stub_groups
|
|
4
|
+
|
|
5
|
+
BASE = "https://charge.test"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _query(call) -> str:
|
|
9
|
+
return call.request.url
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@responses.activate
|
|
13
|
+
def test_ok(monkeypatch):
|
|
14
|
+
monkeypatch.setenv("CHARGE_BACKEND_BASE_URL", BASE)
|
|
15
|
+
monkeypatch.setenv("CHARGE_BACKEND_SERVLET", "mts")
|
|
16
|
+
responses.add(
|
|
17
|
+
responses.GET, f"{BASE}/mts/ai/getUserStubGroups",
|
|
18
|
+
json={"code": "200", "text": None, "data": {"ok": True}}, status=200)
|
|
19
|
+
r = get_user_stub_groups(user_id="77d12b73-7f5a-4abb-97e2-63a13d3d2732", user_lat=31.641665, user_lng=119.889879)
|
|
20
|
+
assert r["ok"] is True
|
|
21
|
+
q = _query(responses.calls[0])
|
|
22
|
+
for f in ['userId=77d12b73-7f5a-4abb-97e2-63a13d3d2732', 'lng=119.889879', 'lat=31.641665']:
|
|
23
|
+
assert f in q, f"missing query param {f} in {q!r}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@responses.activate
|
|
27
|
+
def test_biz_error(monkeypatch):
|
|
28
|
+
monkeypatch.setenv("CHARGE_BACKEND_BASE_URL", BASE)
|
|
29
|
+
responses.add(responses.GET, f"{BASE}/mts/ai/getUserStubGroups",
|
|
30
|
+
json={"code": "010212", "text": "该终端暂不可用"}, status=200)
|
|
31
|
+
r = get_user_stub_groups(user_id="77d12b73-7f5a-4abb-97e2-63a13d3d2732", user_lat=31.641665, user_lng=119.889879)
|
|
32
|
+
assert r["ok"] is False and r["code"] == "010212"
|