beacon-mfg-mcp 1.7.0 → 1.8.0
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/RELEASE.md +23 -2
- package/package.json +2 -2
- package/server.py +85 -2
package/RELEASE.md
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
|
-
# Beacon-MFG 只读 MCP 服务 · v1.
|
|
1
|
+
# Beacon-MFG 只读 MCP 服务 · v1.8.0
|
|
2
2
|
|
|
3
3
|
面向 Agent 的供应商名录检索服务:**只读**,不参与交易、不写入任何数据、不持有密钥。
|
|
4
4
|
|
|
5
|
-
## 本次更新(v1.
|
|
5
|
+
## 本次更新(v1.8.0)
|
|
6
|
+
|
|
7
|
+
**一句话:口语词不再对不上字——「律所」「饭店」「保洁」直接命中,完全没听过的词
|
|
8
|
+
也会收到货架初筛候选,语义归一这一步正式交给 LLM,但候选来源归 server。**
|
|
9
|
+
|
|
10
|
+
- **L1 高频俗称表**(+151 条):律所→7231 律师及相关法律服务、饭店→6210 正餐服务、
|
|
11
|
+
奶茶→6231/6232、保洁公司→8010/8219、开锁→8290……覆盖餐饮、居民服务、房产、医疗、
|
|
12
|
+
金融、法律、制造、软件、运输、零售、文娱、专业服务十二个方向。每条码都经
|
|
13
|
+
`gb-index.json` 校验 `records>0`——货架上没有的类不上表。
|
|
14
|
+
- **L2 类名机械简称**(+937 条):由 `scripts/gen_class_abbr.py` 从 GB/T 4754 全量类名
|
|
15
|
+
切短生成(法律服务、正餐、快递、网吧、歌舞厅……),作为 alias 机制第三层
|
|
16
|
+
(data > curated > abbr,setdefault 保序,冲突自动让位前两层)。
|
|
17
|
+
- **L3 零命中货架兜底**(`_shelf_recall`):查询词与名录写法完全对不上字时,返回体
|
|
18
|
+
`suggested_gb` 直接带上按 bigram 重合度初筛的货架候选(全灭档启用类名首字单字回溯),
|
|
19
|
+
附 `records` 与门类路径;agent 零额外轮次改传 `gb=`。字面零关联时给的是货架 records
|
|
20
|
+
最高榜,why 字段明码标注「仅供语义参考,不对味就如实告诉用户没收录」。
|
|
21
|
+
- **性能修复(线上已部署)**:Pages 索引落后于数据导致每次查询回退全量扫 460 个分片
|
|
22
|
+
(实测 153.5 秒),已重跑部署修复;索引新鲜时查询回到 0.2~1 秒。
|
|
23
|
+
- **部署洁净度**:`data/` 下一级 `_` 开头的内部产物(剪枝备份等,313 MiB)不再打进
|
|
24
|
+
Pages 发布面。
|
|
25
|
+
|
|
26
|
+
## 本次更新(v1.7.0,已发布)
|
|
6
27
|
|
|
7
28
|
**一句话:从「工厂名录」升级为「全部国标行业门类的主体名录」——服务业、
|
|
8
29
|
流通业主体现在是一等公民,不再被当成「非制造商」反向排除。**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beacon-mfg-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
|
@@ -46,4 +46,4 @@
|
|
|
46
46
|
"node": ">=16"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {}
|
|
49
|
-
}
|
|
49
|
+
}
|
package/server.py
CHANGED
|
@@ -848,9 +848,14 @@ def _load_alias_file(rel: str, txt: Optional[str] = None) -> Dict[str, Any]:
|
|
|
848
848
|
|
|
849
849
|
|
|
850
850
|
def load_gb_alias() -> Dict[str, Any]:
|
|
851
|
-
"""词 →
|
|
851
|
+
"""词 → 条目。三层表合并(data > curated > abbr,setdefault 保序):
|
|
852
|
+
gb-alias.json 数据推导层(企业关键词实际落在哪些小类)
|
|
853
|
+
gb-alias-curated.json 人工策展层(口语俗称:律所、饭店、保洁…)
|
|
854
|
+
gb-class-abbr.json 机械简称层(类名切短:法律服务、正餐、快递…)
|
|
855
|
+
前两层优先级更高:abbr 层显式跳过与前两层冲突的词(见 gen_class_abbr.py)。"""
|
|
852
856
|
global _alias_cache
|
|
853
|
-
_alias_relpaths = ("data/gb-alias.json", "data/gb-alias-curated.json"
|
|
857
|
+
_alias_relpaths = ("data/gb-alias.json", "data/gb-alias-curated.json",
|
|
858
|
+
"data/gb-class-abbr.json")
|
|
854
859
|
if _cache_stale(_alias_cache, _alias_relpaths):
|
|
855
860
|
# ⚡ 两张表互相独立 → 一次并发取齐。逐个 fetch 各付一次 CDN RTT
|
|
856
861
|
# (并发取齐可省下数次串行往返)。
|
|
@@ -1324,6 +1329,65 @@ def suggest_filters(query: str, city: str = "", limit: int = 8) -> Dict[str, Any
|
|
|
1324
1329
|
return res
|
|
1325
1330
|
|
|
1326
1331
|
|
|
1332
|
+
# _shelf_recall 单字回溯用的泛字:这些字在国标类名里随处可见,命中了不说明
|
|
1333
|
+
# 查询词与该类有任何关系(「店」命中正餐服务、咖啡馆服务……全部)。
|
|
1334
|
+
_SHELF_STOP_CHARS = set("服务业公厂商中心部所集团有限材料产品设备加工制造技术"
|
|
1335
|
+
"供销买卖经营承包租赁代理经纪咨询开发建设设计安装"
|
|
1336
|
+
"装修装饰工程施工承包勘察监理检测检验")
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def _shelf_recall(q: str, limit: int = 5) -> List[Dict[str, Any]]:
|
|
1340
|
+
"""L3 兜底:查询词与任何类名/别名都不对字时,把货架初筛递到 agent 手边。
|
|
1341
|
+
|
|
1342
|
+
与 suggest_filters 的分工:那条链路只认「子串完整对上」(ql in nm);
|
|
1343
|
+
这里用 **bigram 重合度** ——「法律咨询」对「律师及相关法律服务」共享
|
|
1344
|
+
bigram「法律」,子串链路全军覆没时这里还能给方向。完全无重合时退化为货架
|
|
1345
|
+
records top(why 里明码标注「字面无关联,仅供语义参考」),让 agent 至少
|
|
1346
|
+
知道名录里有什么、按语义挑一个 gb=,或者如实告诉用户「该查询不在覆盖范围」。
|
|
1347
|
+
"""
|
|
1348
|
+
q = (q or "").strip()
|
|
1349
|
+
if not q:
|
|
1350
|
+
return []
|
|
1351
|
+
ql = q.lower()
|
|
1352
|
+
grams = {g for g in _grams(q, query_mode=True) if len(g) >= 2 and g != ql}
|
|
1353
|
+
# 单字回溯:bigram 全灭时启用(「律政」→ 7231 律师及相关法律服务共享单字「律」)。
|
|
1354
|
+
# 泛字单字(服/务/业/公/司…)在类名里到处都是,命中了也不说明问题 —— 剔除。
|
|
1355
|
+
chars = {c for c in q if "\u4e00" <= c <= "\u9fff" and c not in _SHELF_STOP_CHARS}
|
|
1356
|
+
scored: List[tuple] = []
|
|
1357
|
+
for r in _gb_shelf():
|
|
1358
|
+
nm = (r["name"] or "").lower()
|
|
1359
|
+
path = (r["path"] or "").lower()
|
|
1360
|
+
s = 100 if (ql and ql in nm) else 0
|
|
1361
|
+
bi = sum(1 for g in grams if g in nm) if grams else 0
|
|
1362
|
+
if bi:
|
|
1363
|
+
s += min(bi * 10, 40)
|
|
1364
|
+
elif chars:
|
|
1365
|
+
# 单字回溯分级:类名**首字**命中是强信号(「律师…」对「律政」的律),
|
|
1366
|
+
# 腰部命中降权(「邮政/市政」里的政与「律政」无关)。不叠加,取最高。
|
|
1367
|
+
head = nm[:1] if nm else ""
|
|
1368
|
+
if any(c in chars for c in head):
|
|
1369
|
+
s += 25
|
|
1370
|
+
elif any(c in nm for c in chars):
|
|
1371
|
+
s += 8
|
|
1372
|
+
s += min(r["records"] / 5000.0, 4)
|
|
1373
|
+
if s > 0:
|
|
1374
|
+
scored.append((s, r))
|
|
1375
|
+
scored.sort(key=lambda x: -x[0])
|
|
1376
|
+
out: List[Dict[str, Any]] = []
|
|
1377
|
+
for s, r in scored[:limit]:
|
|
1378
|
+
out.append({"code": r["code"], "name": r["name"], "path": r["path"],
|
|
1379
|
+
"records": r["records"],
|
|
1380
|
+
"why": "货架字面初筛(bigram 重合,非精确子串):请按语义判断是否对口"})
|
|
1381
|
+
if not out:
|
|
1382
|
+
# 完全不对字 → 货架热销榜。给信息但不装懂:why 明说这只是 records 最高。
|
|
1383
|
+
for r in sorted(_gb_shelf(), key=lambda r: -r["records"])[:limit]:
|
|
1384
|
+
out.append({"code": r["code"], "name": r["name"], "path": r["path"],
|
|
1385
|
+
"records": r["records"],
|
|
1386
|
+
"why": "查询词与货架字面零关联 —— 这只是名录里 records 最高的类,"
|
|
1387
|
+
"仅供语义参考,不对味就如实告诉用户没收录"})
|
|
1388
|
+
return out
|
|
1389
|
+
|
|
1390
|
+
|
|
1327
1391
|
def _attach_routing_hint(out: Dict[str, Any], q: str, city: str) -> Dict[str, Any]:
|
|
1328
1392
|
"""命中偏少时,把「怎么把口语词换成国标码」这条链路内联进结果。
|
|
1329
1393
|
|
|
@@ -1365,6 +1429,17 @@ def _attach_routing_hint(out: Dict[str, Any], q: str, city: str) -> Dict[str, An
|
|
|
1365
1429
|
if caps:
|
|
1366
1430
|
_emit_cap()
|
|
1367
1431
|
return out
|
|
1432
|
+
# L3:货架初筛兜底。别名/类名子串都推不出码时,把 bigram 初筛(或热销榜)
|
|
1433
|
+
# 直接递给 agent —— 语义归一这一步归 LLM,但「候选从哪来」不必再让它
|
|
1434
|
+
# 多花一轮 list_industries。
|
|
1435
|
+
shelf = _shelf_recall(q, limit=5)
|
|
1436
|
+
if shelf:
|
|
1437
|
+
out["suggested_gb"] = shelf
|
|
1438
|
+
out["hint"] = ("0 条:你的词与名录里的写法(企业名/国标类名/别名表)都对不上字。"
|
|
1439
|
+
"名录按国标小类归档,我已按字面重合度初筛了货架上最接近的类目"
|
|
1440
|
+
"(见 suggested_gb)—— 请按语义挑 1~3 个 code 改传 gb= 重试;"
|
|
1441
|
+
"都不对味就如实告诉用户该查询不在名录覆盖范围,不要换词硬猜。")
|
|
1442
|
+
return out
|
|
1368
1443
|
out["hint"] = ("关键词与名录里的写法对不上,且没有可推导的国标码。"
|
|
1369
1444
|
"建议调 list_industries 用语义挑码后改传 gb=。")
|
|
1370
1445
|
return out
|
|
@@ -1381,6 +1456,14 @@ def _attach_routing_hint(out: Dict[str, Any], q: str, city: str) -> Dict[str, An
|
|
|
1381
1456
|
if caps:
|
|
1382
1457
|
_emit_cap()
|
|
1383
1458
|
return out
|
|
1459
|
+
# L3 同上:货架初筛兜底(这批词多半只是字面撞词,真正的行业要 agent 按语义挑)。
|
|
1460
|
+
shelf = _shelf_recall(q, limit=5)
|
|
1461
|
+
if shelf:
|
|
1462
|
+
out["suggested_gb"] = shelf
|
|
1463
|
+
out["hint"] = ("命中偏少且推不出国标码:这几条多半只是**字面撞词**,未必真是你要的行业。"
|
|
1464
|
+
"我已按字面重合度初筛了货架上最接近的类目(见 suggested_gb)—— 请按语义挑 "
|
|
1465
|
+
"1~3 个 code 改传 gb= 重试,召回和精度都会好很多。")
|
|
1466
|
+
return out
|
|
1384
1467
|
out["hint"] = ("命中偏少且推不出国标码:这几条多半只是**字面撞词**,未必真是你要的行业。"
|
|
1385
1468
|
"建议调 list_industries(keyword=\"…\") 用语义挑 1~3 个国标小类,"
|
|
1386
1469
|
"再改传 gb= 重试;这样召回和精度都会好很多。")
|