beacon-mfg-mcp 1.11.1 → 1.11.2
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.
- package/package.json +1 -1
- package/server.py +43 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beacon-mfg-mcp",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.2",
|
|
4
4
|
"description": "Read-only MCP server exposing the BeaconMFG灯塔 directory of business entities across ALL GB/T 4754 industry divisions (manufacturing, services, retail, logistics, catering, IT, and more) to any MCP-compatible agent. No signup and no API key: an anonymous identity is generated on first run, phone binding is optional (only needed for L2 capability cards).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
package/server.py
CHANGED
|
@@ -220,7 +220,7 @@ def _recall_for_sourcing(text: str, top_k: int = 200, pack_id: str | None = None
|
|
|
220
220
|
# 都对齐到这个值,`scripts/check_version.py` 在发布前校验它们没有漂移。
|
|
221
221
|
# 读不到 package.json 时的兜底值 —— 改版本时**必须**和 package.json 一起改,
|
|
222
222
|
# 否则 check_version.py 会报错(这就是它的用途)。
|
|
223
|
-
VERSION_FALLBACK = "1.11.
|
|
223
|
+
VERSION_FALLBACK = "1.11.2"
|
|
224
224
|
|
|
225
225
|
|
|
226
226
|
def _pkg_version() -> str:
|
|
@@ -414,6 +414,30 @@ def _header_value(v: Any) -> str:
|
|
|
414
414
|
return urllib.parse.quote(s, safe=",._-")
|
|
415
415
|
|
|
416
416
|
|
|
417
|
+
def _quote_path(relpath: str) -> str:
|
|
418
|
+
"""把可能含非 ASCII 的相对路径转成**可放进 HTTP 请求行**的 ASCII 串。
|
|
419
|
+
|
|
420
|
+
⚠ 这是与 `_header_value()` 并列的**第二处编码边界**,漏了同样致命:
|
|
421
|
+
`http.client` 组装请求行时按 `'ascii'` 编码,路径里出现原始 UTF-8 字节
|
|
422
|
+
就在**建连之前**抛 `UnicodeEncodeError: 'ascii' codec can't encode ...`。
|
|
423
|
+
|
|
424
|
+
2026-10-24 实测路径:`_city_postings()` 拼 `skills/registry/index/city/贵阳.json`,
|
|
425
|
+
城市名是发布侧的**原始中文文件名**(391 个城市全是中文名,没有哈希化)——
|
|
426
|
+
中文城市 + 冷缓存首个查询必定在这一跳失败。
|
|
427
|
+
|
|
428
|
+
为什么不在 `_city_postings()` 里就地 quote:`fetch_text()` 会**先用 relpath
|
|
429
|
+
读本地工作树/dirgit HEAD**(本地仓库模式),把 relpath 提前编码会让本地读
|
|
430
|
+
全部落空。所以编码只发生在「真正要发 HTTP」这一层,relpath 本身保持原样。
|
|
431
|
+
|
|
432
|
+
只对含非 ASCII 的路径处理:纯 ASCII 路径(绝大多数分片、索引桶)走原样,
|
|
433
|
+
零行为变化,也不会把已带 `%` 的路径二次编码。
|
|
434
|
+
"""
|
|
435
|
+
if relpath.isascii():
|
|
436
|
+
return relpath
|
|
437
|
+
return urllib.parse.quote(relpath, safe="/")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
417
441
|
def _beacon_headers() -> Dict[str, str]:
|
|
418
442
|
"""随请求带出的头。
|
|
419
443
|
|
|
@@ -568,8 +592,16 @@ def _http_diagnose(relpath: str, last_err: str = "") -> str:
|
|
|
568
592
|
网络不可达」,而 1.4.x~1.11.0 的真实故障是请求头里的中文 token 触发
|
|
569
593
|
`UnicodeEncodeError` —— 让人白去查代理/VPN(2026-10-24 用户实测反馈)。
|
|
570
594
|
"""
|
|
571
|
-
#
|
|
595
|
+
# 编码问题:与网络、代理、CDN 全都无关,必须单独说清楚。
|
|
596
|
+
# 且必须区分错在**请求行(URL)还是请求头**——两者排查方向完全不同:
|
|
597
|
+
# 'ascii' codec → 路径里混进了非 ASCII(如 city/贵阳.json)
|
|
598
|
+
# 'latin-1' codec → 请求头里混进了非 ASCII(如 X-Beacon-Tokens: 海伦)
|
|
599
|
+
# 早期把两者都写成「请求头」,用户按那个方向去查会一无所获(2026-10-24 反馈)。
|
|
572
600
|
if "UnicodeEncodeError" in last_err or "latin-1" in last_err:
|
|
601
|
+
if "'ascii'" in last_err:
|
|
602
|
+
return ("请求路径 `%s` 含非 ASCII 字符,无法编码成 HTTP 请求行"
|
|
603
|
+
"(**本地编码问题,不是网络问题**);最后错误: %s"
|
|
604
|
+
% (relpath, last_err))
|
|
573
605
|
return ("请求头包含非 ASCII 字符,无法编码成 HTTP 头(**本地编码问题,"
|
|
574
606
|
"不是网络问题**);最后错误: " + last_err)
|
|
575
607
|
hints = []
|
|
@@ -626,7 +658,7 @@ def _http_get(relpath: str) -> Tuple[Optional[str], Optional[str]]:
|
|
|
626
658
|
last_err = ""
|
|
627
659
|
_fatal = False
|
|
628
660
|
for base in _http_bases():
|
|
629
|
-
url = base + "/" + relpath
|
|
661
|
+
url = base + "/" + _quote_path(relpath)
|
|
630
662
|
cap = _cache_path(url)
|
|
631
663
|
etag = None
|
|
632
664
|
if os.path.exists(cap):
|
|
@@ -2946,6 +2978,7 @@ def _city_postings(city: str) -> Optional[Dict[str, int]]:
|
|
|
2946
2978
|
if not city:
|
|
2947
2979
|
return {}
|
|
2948
2980
|
safe = city.replace("/", "").replace("\\", "")
|
|
2981
|
+
err_before = _HTTP_LAST_ERROR[0] # 记住进入前的错误,供兜底成功后撤销
|
|
2949
2982
|
txt = _index_text(INDEX_DIR + "/city/" + safe + ".json")
|
|
2950
2983
|
if txt:
|
|
2951
2984
|
try:
|
|
@@ -2957,6 +2990,13 @@ def _city_postings(city: str) -> Optional[Dict[str, int]]:
|
|
|
2957
2990
|
txt = _index_text(INDEX_DIR + "/city.json")
|
|
2958
2991
|
if not txt:
|
|
2959
2992
|
return None
|
|
2993
|
+
# 走到这里说明**全表兜底取到了**。单城文件只是「省流量」的可选快路径,
|
|
2994
|
+
# 它失败(4xx / 编码错误 / 该城市无文件)不影响结果完整性 —— 所以不能让它
|
|
2995
|
+
# 冒到 `fetch_error` 里。实测代价:0 条结果时返回体夹一条「请求头编码错误」,
|
|
2996
|
+
# 调用方会判定「工具坏了」,而真实结论只是「库里确实没有这家企业」。
|
|
2997
|
+
# 只撤销**本函数内新产生**的错误,外层(manifest / 分片哈希不符)照旧保留。
|
|
2998
|
+
if _HTTP_LAST_ERROR[0] != err_before:
|
|
2999
|
+
_HTTP_LAST_ERROR[0] = err_before
|
|
2960
3000
|
try:
|
|
2961
3001
|
full = json.loads(txt)
|
|
2962
3002
|
except Exception:
|