beacon-mfg-mcp 1.5.5 → 1.5.6
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/README.md +7 -1
- package/RELEASE.md +50 -3
- package/package.json +1 -1
- package/server.py +48 -10
package/README.md
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
> 仍按**连续出现**匹配,召回少于 `"精密 加工"`(`city=苏州` 实测 2 条 vs 26 条)。
|
|
25
25
|
> 遇到这种生词,正确做法是**先 `suggest_filters`** 拿候选国标码再改用 `gb=`,
|
|
26
26
|
> 而不是反复试同义词 —— 服务端会在命中 <5 条时用 `hint` / `suggested_gb` 主动提示这条路。
|
|
27
|
+
> - **结果顺序(v1.5.6 起:档内也有序)**:`results` 先按**召回来源**分档
|
|
28
|
+
> (`via=text` 字面命中 → `via=cap_text` 只撞能力词面 → `via=alias` 别名定向 → `via=cap` 能力定向),
|
|
29
|
+
> **同一档内**再按「关键词在**厂名**里**成片**出现」优先。所以问「商务酒店」时,
|
|
30
|
+
> `龙翔商务酒店` 会排在 `某酒店(中央商务区店)` 前面(后者只是括号里的商圈名撞上了「商务」)。
|
|
31
|
+
> **`score` 是能力画像分**(有已发布能力卡才有分,无卡为 0),**不是相关度**,别拿它反推排序;
|
|
32
|
+
> 返回体的 `order_note` 里写死了这个口径。
|
|
27
33
|
> - **检索架构(v1.2.0 起:按需拉取,不再全量冷启动)**:
|
|
28
34
|
> 早期实现每次查询都要把**全部 fp 分片**拉下来在客户端建索引,复杂度 O(数据总量) —— 11.8 万条约 5.8s,线性外推千万级约 **491s / 2.2GB 内存**,不可用。
|
|
29
35
|
> v1.2.0 改为 **O(命中量)**:索引在**发布侧预构建**(`scripts/gen_search_index.py`,随 `postfetch` 的 `searchindex` 步骤产出 `skills/registry/index/`),
|
|
@@ -44,7 +50,7 @@
|
|
|
44
50
|
|
|
45
51
|
所有返回都是 JSON 文本。找不到时返回 `{ "error": ... }` 或 `has_card:false`,不会抛协议错。
|
|
46
52
|
|
|
47
|
-
### 用户原话 → 国标码:给 agent 的调用协议(v1.5.
|
|
53
|
+
### 用户原话 → 国标码:给 agent 的调用协议(v1.5.6)
|
|
48
54
|
|
|
49
55
|
**为什么需要**:`query` 是**字面子串**匹配,而名录按国标小类归档 —— 客户说「机加工」,
|
|
50
56
|
库里写的是「机械零部件加工」,字面不通。实测(`city=嘉兴`):
|
package/RELEASE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Beacon-MFG 只读 MCP 服务 · v1.5.
|
|
1
|
+
# Beacon-MFG 只读 MCP 服务 · v1.5.6
|
|
2
2
|
|
|
3
3
|
让任意支持 MCP 的主流 agent(Claude Desktop / Cline / Continue / WorkBuddy 等)能够
|
|
4
4
|
**检索与调用**已发布到 Cloudflare Pages(或 GitHub)的灯塔工厂供应商数据,
|
|
@@ -23,7 +23,54 @@ MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流
|
|
|
23
23
|
后三个构成「采购寻源」多轮会话链路 `start_sourcing → answer_sourcing → refine_sourcing`,
|
|
24
24
|
依赖仓库平级的 `skills/rfq-kernel` 桥接模块。
|
|
25
25
|
|
|
26
|
-
## 本次更新(v1.5.
|
|
26
|
+
## 本次更新(v1.5.6)
|
|
27
|
+
**修「结果呈现」:同档内排序不再是随机的。无破坏性变更**(入参、三种接入方式、既有返回字段全不变)。
|
|
28
|
+
|
|
29
|
+
### 1. 档内排序 = 分片记录顺序(等于随机)
|
|
30
|
+
|
|
31
|
+
1.5.5 只做了**档位**排序(`text → cap_text → alias → cap`),档内顺序仍是分片里的记录
|
|
32
|
+
顺序。实测「贵阳的商务酒店」把
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
1. 悦庭楠舍酒店(花果园中央商务区店) ← 只是括号里的**商圈名**撞上了「商务」
|
|
36
|
+
2. 小苹果酒店(中央商务区4号店) ← 同上
|
|
37
|
+
3. 龙翔商务酒店(贵阳龙洞堡机场店) ← 厂名里就写着「商务酒店」
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
排成了这样。两家其实都算 `text` 命中,但用户要的是后一种。
|
|
41
|
+
|
|
42
|
+
修法:新增模块级 `_name_fit()`,按「关键词在**厂名**里**成片**出现」记 2、各自出现记 1、
|
|
43
|
+
否则 0,作为**档内**次级键。修后 top-8 **全部**是真·商务酒店(龙翔 / 华宛 / 88城市优品 /
|
|
44
|
+
温馨 / 远瞻 / 新时空 / 五新 / OYO豪维雅)。
|
|
45
|
+
|
|
46
|
+
> 只用「关键词是否出现在厂名里」不够 —— 两家都含「商务」和「酒店」,必须**成片**才分得开。
|
|
47
|
+
|
|
48
|
+
### 2. `suggest_filters` 与 `search_vendors` 的解析回显字段名不一致
|
|
49
|
+
|
|
50
|
+
`search_vendors` 用 `query_parsed`,`suggest_filters` 却用扁平的
|
|
51
|
+
`city_from_query` / `keywords` / `recognized`。agent 从一个工具学到的字段名在另一个里
|
|
52
|
+
找不到;而 `README.md` 与本指南早就统一写 `query_parsed` —— 属实现漂移。
|
|
53
|
+
|
|
54
|
+
修后两个工具的 `query_parsed` 形状**逐字一致**。
|
|
55
|
+
|
|
56
|
+
### 3. `gb=` 路由没走同一套排序
|
|
57
|
+
|
|
58
|
+
`gb=` 只扫一个分片、直接按分片顺序返回;`query` 路由已按档位排。同一批记录经两条路进来
|
|
59
|
+
顺序不同,agent 会以为「换个写法结果就变了」。修后两条路由共用 `_name_fit()`,`gb=` 路由
|
|
60
|
+
补 `order_note`。
|
|
61
|
+
|
|
62
|
+
### 实测对照(原话直传,不手工拆词)
|
|
63
|
+
|
|
64
|
+
| 版本 | 命中 | 城市 | top-8 |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| 1.5.4 | **5418** | ❌ 丢失 | ❌ 全是**杭州**的度假酒店 |
|
|
67
|
+
| 1.5.5 | 401 | ✅ 贵阳 | ⚠ 混入 2 家「(中央商务区店)」假朋友 |
|
|
68
|
+
| **1.5.6** | 401 | ✅ 贵阳 | ✅ 全为真·商务酒店 |
|
|
69
|
+
|
|
70
|
+
回归:协议 16/16、语义路由 14/14、检索哨兵「机加工@嘉兴」643 条、城市正确性 10/10、
|
|
71
|
+
疑似无关 0 条、`gb=` 与 `query` 两路由同码记录 9/9 一致。
|
|
72
|
+
|
|
73
|
+
## 本次更新(v1.5.5,已发布)
|
|
27
74
|
**让「用户原话」真的能用。无破坏性变更:入参、三种接入方式、返回体既有字段全部不变(只增不减)。**
|
|
28
75
|
|
|
29
76
|
### 1. 中文整句直传必然 0 条 —— 关键词切分只有 `q.split()`
|
|
@@ -359,7 +406,7 @@ v1.5.0 的 idmap 有个致命假设:片存在但片里没这条 id ⇒ 判「
|
|
|
359
406
|
## 安装(三选一)
|
|
360
407
|
|
|
361
408
|
### 1. 从 GitHub Release 下载(推荐,零依赖)
|
|
362
|
-
下载本 Release 的 `beacon-mfg-mcp-mcp-v1.5.
|
|
409
|
+
下载本 Release 的 `beacon-mfg-mcp-mcp-v1.5.6.tar.gz`,解压后:
|
|
363
410
|
```jsonc
|
|
364
411
|
{
|
|
365
412
|
"mcpServers": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beacon-mfg-mcp",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.6",
|
|
4
4
|
"description": "Read-only MCP server exposing the BeaconMFG灯塔工厂 supplier directory (search vendors, get vendor detail, get capability card, multi-turn sourcing) to any MCP-compatible agent. No API key required.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
package/server.py
CHANGED
|
@@ -283,9 +283,9 @@ def _pkg_version() -> str:
|
|
|
283
283
|
try:
|
|
284
284
|
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "package.json")
|
|
285
285
|
with open(p, "r", encoding="utf-8") as f:
|
|
286
|
-
return json.load(f).get("version", "1.5.
|
|
286
|
+
return json.load(f).get("version", "1.5.6")
|
|
287
287
|
except Exception:
|
|
288
|
-
return "1.5.
|
|
288
|
+
return "1.5.6"
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
# HTTP 基址列表(含兜底镜像)。默认部署 Cloudflare Pages 不可达时,自动回退到
|
|
@@ -1268,17 +1268,23 @@ def suggest_filters(query: str, city: str = "", limit: int = 8) -> Dict[str, Any
|
|
|
1268
1268
|
res: Dict[str, Any] = {
|
|
1269
1269
|
"query": q,
|
|
1270
1270
|
"city": city or "",
|
|
1271
|
-
# 把「MCP 是怎么理解这句话的」摊开给 agent 看:抽走了哪个城市、剔了哪些停用词、
|
|
1272
|
-
# 认出了哪些词条。不透明的话,agent 无法判断候选为空到底是「没这行业」还是
|
|
1273
|
-
# 「这句话没被读懂」,只能瞎猜着换词重试。
|
|
1274
|
-
"city_from_query": norm.get("city_from_query") or "",
|
|
1275
|
-
"keywords": norm.get("tokens") or [],
|
|
1276
|
-
"recognized": norm.get("segments") or [],
|
|
1277
1271
|
"candidates": out,
|
|
1278
1272
|
"how_to_use": "挑 1~3 个 code,逐个调 search_vendors(gb=<code>, city=<城市>)"
|
|
1279
1273
|
"(给了 gb 只扫对应分片,最快);多个码的结果自行合并去重。"
|
|
1280
1274
|
"候选为空或都不对味时,调 list_industries 看货架自己挑。",
|
|
1281
1275
|
}
|
|
1276
|
+
# 把「MCP 是怎么理解这句话的」摊开给 agent 看:抽走了哪个城市、剔了哪些停用词、
|
|
1277
|
+
# 认出了哪些词条。不透明的话,agent 无法判断候选为空到底是「没这行业」还是
|
|
1278
|
+
# 「这句话没被读懂」,只能瞎猜着换词重试。
|
|
1279
|
+
# 字段形状与 search_vendors 的 query_parsed **逐字一致** —— agent 从任一工具学到的
|
|
1280
|
+
# 字段名在另一个里都找得到,不必记两套形状(此前这里是扁平的 city_from_query /
|
|
1281
|
+
# keywords / recognized,与 search_vendors 的 query_parsed 不一致,属实现漂移)。
|
|
1282
|
+
res["query_parsed"] = {
|
|
1283
|
+
"city": city or "",
|
|
1284
|
+
"city_from_query": norm.get("city_from_query") or "",
|
|
1285
|
+
"keywords": norm.get("tokens") or [],
|
|
1286
|
+
"recognized": norm.get("segments") or [],
|
|
1287
|
+
}
|
|
1282
1288
|
if cap_cands:
|
|
1283
1289
|
res["cap_candidates"] = cap_cands
|
|
1284
1290
|
res["cap_note"] = ("这些是**能力域**(跨门类的工艺/技术标签),不是国标码,"
|
|
@@ -2012,6 +2018,26 @@ def _hay_lit(rec: Dict[str, Any]) -> str:
|
|
|
2012
2018
|
return _hay_parts(rec, False)
|
|
2013
2019
|
|
|
2014
2020
|
|
|
2021
|
+
def _name_fit(rec_or_sum: Dict[str, Any], tokens: List[str]) -> int:
|
|
2022
|
+
"""关键词在**厂名**里的贴合度,0/1/2。用于同一召回档内部的次级排序。
|
|
2023
|
+
|
|
2024
|
+
只有档位是不够的:档内顺序原本就是分片里的记录顺序 —— 等于随机。实测
|
|
2025
|
+
「贵阳的商务酒店」把 悦庭楠舍酒店(花果园**中央商务区**店) 排在了 龙翔商务酒店
|
|
2026
|
+
前面:两者都算 text 命中,但前者只是**括号里的商圈名**撞上了「商务」,后者厂名里
|
|
2027
|
+
就写着「商务酒店」。只用「关键词是否出现在厂名里」也不够(两家都含「商务」和
|
|
2028
|
+
「酒店」),必须是**成片出现**才区分得开。
|
|
2029
|
+
|
|
2030
|
+
`query+city` 与 `gb=` 两条路由共用本函数,保证同一批记录两条路排出来的顺序一致
|
|
2031
|
+
(体检的「C 路由一致性」就是查这个)。
|
|
2032
|
+
"""
|
|
2033
|
+
if not tokens:
|
|
2034
|
+
return 0
|
|
2035
|
+
co = str(rec_or_sum.get("company") or rec_or_sum.get("co") or "").lower()
|
|
2036
|
+
if "".join(tokens) in co:
|
|
2037
|
+
return 2 # 关键词在厂名里成片出现 —— 最强
|
|
2038
|
+
return 1 if all(t in co for t in tokens) else 0
|
|
2039
|
+
|
|
2040
|
+
|
|
2015
2041
|
def _rec_summary(rec: Dict[str, Any]) -> Dict[str, Any]:
|
|
2016
2042
|
return {
|
|
2017
2043
|
"id": rec.get("id"),
|
|
@@ -2571,12 +2597,19 @@ def search_vendors(query: str = "", city: str = "", gb: str = "",
|
|
|
2571
2597
|
s = _rec_summary(rec)
|
|
2572
2598
|
s["via"] = "gb"
|
|
2573
2599
|
matches.append(s)
|
|
2600
|
+
# 与 query 路由同一套档内排序(`gb=` 只走一档,故按厂名贴合度降序即可),
|
|
2601
|
+
# 否则同一批记录经两条路进来顺序不同,agent 会以为「换了个写法结果就变了」。
|
|
2602
|
+
matches.sort(key=lambda x: -_name_fit(x, gen_tokens))
|
|
2574
2603
|
return _attach_routing_hint({
|
|
2575
2604
|
"total_matched": len(matches),
|
|
2576
2605
|
"returned": len(matches[offset:offset + limit]),
|
|
2577
2606
|
"shards_scanned": scanned,
|
|
2578
2607
|
"results": matches[offset:offset + limit],
|
|
2579
2608
|
**parsed,
|
|
2609
|
+
# 同 query 路由的口径:档内按「关键词在厂名里成片出现」优先,score 不是相关度。
|
|
2610
|
+
"order_note": ("results 按「关键词在厂名里成片出现」优先排列;score 是"
|
|
2611
|
+
"**能力画像分**(有已发布能力卡才有分,无卡为 0),"
|
|
2612
|
+
"**不是相关度**,不要拿它反推排序。"),
|
|
2580
2613
|
}, q, city)
|
|
2581
2614
|
|
|
2582
2615
|
# 未给国标码:优先走「预构建倒排索引 → 只拉命中分片」(O(命中量))。
|
|
@@ -2689,7 +2722,10 @@ def search_vendors(query: str = "", city: str = "", gb: str = "",
|
|
|
2689
2722
|
# 为什么必须显式排:cap_text 与 text 是**同一趟**文本召回里 append 进去的,
|
|
2690
2723
|
# 不重排两档会交错,order_note 里「按召回来源分档排列」就成了假承诺。
|
|
2691
2724
|
_RANK = {"text": 0, "city": 0, "cap_text": 1, "alias": 2, "cap": 3}
|
|
2692
|
-
|
|
2725
|
+
|
|
2726
|
+
# 档内再按厂名贴合度排(见 `_name_fit` 的说明)。这是**档内**微调,不影响档间
|
|
2727
|
+
# 次序,也不影响 total_matched。
|
|
2728
|
+
matches.sort(key=lambda x: (_RANK.get(x.get("via"), 9), -_name_fit(x, gen_tokens)))
|
|
2693
2729
|
|
|
2694
2730
|
out = {
|
|
2695
2731
|
"total_matched": len(matches),
|
|
@@ -2704,7 +2740,9 @@ def search_vendors(query: str = "", city: str = "", gb: str = "",
|
|
|
2704
2740
|
# 所以在这里把口径写死,让 agent 自己能分辨「谁更值得先看」。
|
|
2705
2741
|
"order_note": ("results 按召回来源分档排列:via=text(厂名/工艺/材料等**字面**命中) → "
|
|
2706
2742
|
"via=cap_text(厂名里其实没这个词,只是撞上了能力键的中文词面) → "
|
|
2707
|
-
"via=alias(别名定向) → via=cap(能力定向)
|
|
2743
|
+
"via=alias(别名定向) → via=cap(能力定向);**同一档内**再按「关键词在"
|
|
2744
|
+
"厂名里成片出现」优先(所以「商务酒店」会把 龙翔商务酒店 排在 "
|
|
2745
|
+
"某酒店(中央商务区店) 前面);score 是**能力画像分**"
|
|
2708
2746
|
"(有已发布能力卡才有分,无卡为 0),**不是相关度**,不要拿它反推排序。"),
|
|
2709
2747
|
}
|
|
2710
2748
|
if alias_hits:
|