beacon-mfg-mcp 1.12.2 → 1.12.3

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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/package.json +1 -1
  3. package/server.py +56 -19
package/README.md CHANGED
@@ -60,6 +60,19 @@ linked_entity 温州市亿鸿装饰工程有限公司 ← 营业执照
60
60
  登记机关),只有政府公开数据来源的主体才有,且**只读不改**;`linked_entity` 是**场所与主体的
61
61
  关联边**,可撤销、可人工修正。
62
62
 
63
+ ## 工艺证据等级(search_vendors 的 `process_evidence`)
64
+
65
+ 工艺码有**三种来源**,混为一谈会把「这一类大概都做」当成「这家确实做」:
66
+
67
+ | 值 | 含义 | 该不该当事实 |
68
+ |----|------|--------------|
69
+ | `null` | 能力卡 / 企业自述登记的事实工艺(或类默认但只有 1~3 个码) | 可当事实,仍建议 `get_capability_card` 复核 |
70
+ | `inferred:A1` ~ `inferred:B0` | 零采集推断,字母是证据分档(A1 三路一致 … B0 单路有信号) | 推断不是核实,严苛场景要追问 |
71
+ | `class_default` | 该记录挂了 ≥4 个「国标小类默认工艺码」,是同类企业的众数,**不是这家企业的工艺事实** | ❌ 必须降权,不要据此下单 |
72
+
73
+ `class_default` 覆盖约 1.3 万条。工艺码一个没删(按工艺检索的召回不受影响),
74
+ 只是把证据等级如实标出来 —— 让 Agent 少做一轮二次筛选。
75
+
63
76
  ## 身份与额度(不需要注册)
64
77
 
65
78
  首次运行会在 `~/.beacon-mfg/` 生成一个匿名身份(机器本地 UUID → 单向哈希),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beacon-mfg-mcp",
3
- "version": "1.12.2",
3
+ "version": "1.12.3",
4
4
  "mcpName": "io.github.eiry16/beacon-mfg-mcp",
5
5
  "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).",
6
6
  "keywords": [
package/server.py CHANGED
@@ -341,7 +341,7 @@ def _recall_for_sourcing(text: str, top_k: int = 200, pack_id: str | None = None
341
341
  # 都对齐到这个值,`scripts/check_version.py` 在发布前校验它们没有漂移。
342
342
  # 读不到 package.json 时的兜底值 —— 改版本时**必须**和 package.json 一起改,
343
343
  # 否则 check_version.py 会报错(这就是它的用途)。
344
- VERSION_FALLBACK = "1.12.2"
344
+ VERSION_FALLBACK = "1.12.3"
345
345
 
346
346
 
347
347
  def _pkg_version() -> str:
@@ -3275,12 +3275,20 @@ def _rec_summary(rec: Dict[str, Any]) -> Dict[str, Any]:
3275
3275
  "score": rec.get("sc"),
3276
3276
  "has_phone": bool(rec.get("tel")),
3277
3277
  "process": rec.get("proc", []),
3278
- # 工艺来源(2026-11-06):None = 能力卡/企业自述/按国标码推导的类默认;
3279
- # "inferred:A1" = 零采集推断,字母是分档(A1 三路一致 99.3% / A2 97.4% /
3280
- # A3 94.3% / B0 单路 96.0%,见 docs/PROCESS_INFERENCE.md)。
3281
- # 为什么要暴露:推断不是核实,"这家能做"和"这家这一类大概都做"必须可区分 ——
3282
- # 客户在严苛场景(超公差、医疗件、指定牌号)里要能据此追加追问,而不是直接下单。
3283
- "process_evidence": ("inferred:" + rec["pinf"]) if rec.get("pinf") else None,
3278
+ # 工艺来源(2026-11-06,2026-11-13 补第三档):
3279
+ # None = 能力卡 / 企业自述 / 类默认但工艺码少于 4 个
3280
+ # "inferred:A1" = 零采集推断,字母是分档(A1 三路一致 99.3% / A2 97.4% /
3281
+ # A3 94.3% / B0 单路 96.0%,见 docs/PROCESS_INFERENCE.md)
3282
+ # "class_default" = 该记录挂了 ≥4 个「国标小类默认工艺码」,不是这家企业
3283
+ # 的事实工艺,只是同类众数。全库 12,988 条属此类(其中
3284
+ # 12,970 条 pv=derived)。必须让 Agent 看得见:否则它会把
3285
+ # 「这一类大概都做」当成「这家确实做」,严苛场景直接下单。
3286
+ # 红线:工艺码一个不动(召回不变),只加证据标签 —— 不编造也不减召回。
3287
+ # 12,988 条里只有 838 条有推断候选,其余 12,150 条没有任何强证据可换。
3288
+ "process_evidence": (
3289
+ "inferred:" + rec["pinf"] if rec.get("pinf")
3290
+ else ("class_default" if (rec.get("pv") == "derived"
3291
+ and len(rec.get("proc") or []) >= 4) else None)),
3284
3292
  # 业务能力码(2026-11-06):非制造业门类的「能干什么」,如 fmt_bar 酒吧 /
3285
3293
  # edu_vocational 职业技能培训 / con_steel 钢结构。此前它只参与匹配不返回,
3286
3294
  # 于是「搜酒吧」的结果里看不出哪家真是酒吧。现在一并返回,agent 可据此
@@ -4486,11 +4494,27 @@ def search_vendors(query: str = "", city: str = "", gb: str = "",
4486
4494
  # `alias_scanned` 仍按**记录数**计,与改前口径一致。
4487
4495
  if _via_server:
4488
4496
  def _alias_recs():
4489
- for _code in alias_by_code:
4490
- _r = _server_search(q="", city=city, gb=_code, et=et_want,
4491
- limit=SERVER_SEARCH_WINDOW)
4492
- if _r:
4493
- yield from _r["records"]
4497
+ codes = list(alias_by_code)[:6]
4498
+ if not codes:
4499
+ return
4500
+ # 2026-11-13 多码列举:一次请求把全部国标码交给 worker。
4501
+ # 旧 worker 不认 gbs:它只按首码 gb 应答、且不回 gbs_supported,
4502
+ # 于是这里退回逐码补发其余码 —— 绝不出现「取首个码漏掉另外两类」。
4503
+ _r = _server_search(q="", city=city, gb=codes, et=et_want,
4504
+ limit=SERVER_SEARCH_WINDOW)
4505
+ done = set()
4506
+ if _r:
4507
+ yield from _r["records"]
4508
+ if _r.get("gbs_supported") and len(codes) > 1:
4509
+ return
4510
+ done.add(codes[0])
4511
+ for _code in codes:
4512
+ if _code in done:
4513
+ continue
4514
+ _r2 = _server_search(q="", city=city, gb=_code, et=et_want,
4515
+ limit=SERVER_SEARCH_WINDOW)
4516
+ if _r2:
4517
+ yield from _r2["records"]
4494
4518
  _alias_iter = _alias_recs()
4495
4519
  else:
4496
4520
  _alias_iter = _records_from_paths(alias_paths or [])
@@ -4832,7 +4856,7 @@ _SEARCH_PROBE_UNTIL = [0.0]
4832
4856
  SERVER_SEARCH_WINDOW = 200
4833
4857
 
4834
4858
 
4835
- def _server_search(q: str = "", city: str = "", gb: str = "", et: int = 0,
4859
+ def _server_search(q: str = "", city: str = "", gb: Any = "", et: int = 0,
4836
4860
  limit: int = SERVER_SEARCH_WINDOW,
4837
4861
  cap: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
4838
4862
  """POST /v1/search —— 服务端检索(「按条来拉」的检索面,2026-11-03)。
@@ -4849,8 +4873,13 @@ def _server_search(q: str = "", city: str = "", gb: str = "", et: int = 0,
4849
4873
  return None
4850
4874
  if time.time() < _SEARCH_PROBE_UNTIL[0]:
4851
4875
  return None
4852
- if not (str(q or "").strip() or str(city or "").strip()
4853
- or str(gb or "").strip() or cap):
4876
+ # 2026-11-13:gb 可为多个国标码(别名「注塑」→2929/3525/3399)。
4877
+ # 一次请求全交给 worker,别再逐码各发一次 —— 额度与 RTT 都按请求数翻。
4878
+ if isinstance(gb, (list, tuple, set)):
4879
+ gb_list = [str(x).strip() for x in gb if str(x).strip()][:6]
4880
+ else:
4881
+ gb_list = [str(gb).strip()] if str(gb or "").strip() else []
4882
+ if not (str(q or "").strip() or str(city or "").strip() or gb_list or cap):
4854
4883
  return None
4855
4884
  try:
4856
4885
  import identity as _idn
@@ -4860,8 +4889,11 @@ def _server_search(q: str = "", city: str = "", gb: str = "", et: int = 0,
4860
4889
  payload: Dict[str, Any] = {"q": str(q or ""), "limit": int(limit), "offset": 0}
4861
4890
  if str(city or "").strip():
4862
4891
  payload["city"] = str(city).strip()
4863
- if str(gb or "").strip():
4864
- payload["gb"] = str(gb).strip()
4892
+ if gb_list:
4893
+ # 旧 worker 只认单码 gb:首码照传,保证线上还没部署新 worker 时
4894
+ # 至少这一个码仍生效;新 worker 见到 gbs 数组会改用多码列举。
4895
+ payload["gb"] = gb_list[0]
4896
+ payload["gbs"] = gb_list
4865
4897
  if et:
4866
4898
  payload["et"] = int(et)
4867
4899
  if cap:
@@ -4893,7 +4925,8 @@ def _server_search(q: str = "", city: str = "", gb: str = "", et: int = 0,
4893
4925
  if isinstance(hits, list):
4894
4926
  return {"total": int(o.get("total_matched") or 0),
4895
4927
  "cap_matched": int(o.get("cap_matched") or 0),
4896
- "records": hits}
4928
+ "records": hits,
4929
+ "gbs_supported": bool(o.get("gbs_supported"))}
4897
4930
  continue
4898
4931
  except _ue.HTTPError as e:
4899
4932
  st[0] = e.code
@@ -4904,7 +4937,8 @@ def _server_search(q: str = "", city: str = "", gb: str = "", et: int = 0,
4904
4937
  try:
4905
4938
  _o = json.loads(e.read().decode("utf-8"))
4906
4939
  if _o.get("error") == "bad_gb":
4907
- return {"total": 0, "records": []}
4940
+ return {"total": 0, "records": [],
4941
+ "gbs_supported": False}
4908
4942
  except Exception:
4909
4943
  pass
4910
4944
  continue
@@ -5478,6 +5512,9 @@ TOOLS = [
5478
5512
  "加 radius_km 只留圈内(地标名需调用方先解析成坐标)。"
5479
5513
  "返回精简档案(id/名称/城市/区县/国标码/认证等级/"
5480
5514
  "工艺/材料/认证/是否含电话/连锁门店数/距离)。"
5515
+ "每条带 process_evidence:null=有事实工艺;inferred:A1~B0=零采集"
5516
+ "推断(分档非核实);class_default=挂的是国标小类默认工艺码(≥4个),"
5517
+ "不是这家企业的事实工艺,严苛场景请降权或追问核实。"
5481
5518
  "⚠ 关键词是**字面子串**匹配:用户的口语词若与名录写法不同(如「机加工」"
5482
5519
  "vs 归档用的「机械零部件加工」、「面馆」vs「苏式面馆」),命中会偏少。"
5483
5520
  "此时不要直接报「没有」——看返回体里的 hint / suggested_gb,"