beacon-mfg-mcp 1.4.1 → 1.5.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 +143 -99
- package/bin/beacon-mfg-mcp.js +54 -36
- package/bin/selfupdate.js +150 -0
- package/package.json +43 -44
- package/rfq-kernel/demo.py +9 -0
- package/server.py +2552 -2217
- package/test_protocol.py +0 -313
package/RELEASE.md
CHANGED
|
@@ -1,99 +1,143 @@
|
|
|
1
|
-
# Beacon-MFG 只读 MCP 服务 · v1.
|
|
2
|
-
|
|
3
|
-
让任意支持 MCP 的主流 agent(Claude Desktop / Cline / Continue / WorkBuddy 等)能够
|
|
4
|
-
**检索与调用**已发布到 Cloudflare Pages(或 GitHub)的灯塔工厂供应商数据,
|
|
5
|
-
**无需 clone 整个仓库、无需任何密钥**。
|
|
6
|
-
|
|
7
|
-
## 设计边界
|
|
8
|
-
MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流水线**不进 MCP、不暴露、不持密钥**,
|
|
9
|
-
也不会落在任何写路径上。对 Android App 与 cron/GUI 流水线**零影响**(详见 `mcp/README.md`)。
|
|
10
|
-
|
|
11
|
-
## 六个只读 tool
|
|
12
|
-
| tool | 用途 | 关键参数 |
|
|
13
|
-
|------|------|----------|
|
|
14
|
-
| `search_vendors` | 按 关键词 / 城市 / 国标码 检索,返回精简档案 | `query` `city` `gb` `limit` `offset` |
|
|
15
|
-
| `get_vendor` | 按 id(+国标码)取完整中文档案 | `id`(必填) `gb`(可选) |
|
|
16
|
-
| `get_capability_card` | 按 id 取能力卡(工艺/设备/产能/认证) | `id`(必填) |
|
|
17
|
-
| `start_sourcing` | 【客户 agent 自动触发】识别找厂/代工/采购意图 → 品类识别→宽召回→澄清,返回 `session_id` | `demand_text`(必填) `audience_id` |
|
|
18
|
-
| `answer_sourcing` | 续接澄清轮次:写回客户答案,返回下一轮澄清或初选列表 | `session_id`(必填) `answers` |
|
|
19
|
-
| `refine_sourcing` | 推荐轮次交互:`details`/`more`/`best` | `session_id`(必填) `action` `value` |
|
|
20
|
-
|
|
21
|
-
后三个构成「采购寻源」多轮会话链路 `start_sourcing → answer_sourcing → refine_sourcing`,
|
|
22
|
-
依赖仓库平级的 `skills/rfq-kernel` 桥接模块。
|
|
23
|
-
|
|
24
|
-
## 本次更新(v1.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- `BEACON_WORKTREE
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
##
|
|
99
|
-
|
|
1
|
+
# Beacon-MFG 只读 MCP 服务 · v1.5.0
|
|
2
|
+
|
|
3
|
+
让任意支持 MCP 的主流 agent(Claude Desktop / Cline / Continue / WorkBuddy 等)能够
|
|
4
|
+
**检索与调用**已发布到 Cloudflare Pages(或 GitHub)的灯塔工厂供应商数据,
|
|
5
|
+
**无需 clone 整个仓库、无需任何密钥**。
|
|
6
|
+
|
|
7
|
+
## 设计边界
|
|
8
|
+
MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流水线**不进 MCP、不暴露、不持密钥**,
|
|
9
|
+
也不会落在任何写路径上。对 Android App 与 cron/GUI 流水线**零影响**(详见 `mcp/README.md`)。
|
|
10
|
+
|
|
11
|
+
## 六个只读 tool
|
|
12
|
+
| tool | 用途 | 关键参数 |
|
|
13
|
+
|------|------|----------|
|
|
14
|
+
| `search_vendors` | 按 关键词 / 城市 / 国标码 检索,返回精简档案 | `query` `city` `gb` `limit` `offset` |
|
|
15
|
+
| `get_vendor` | 按 id(+国标码)取完整中文档案 | `id`(必填) `gb`(可选) |
|
|
16
|
+
| `get_capability_card` | 按 id 取能力卡(工艺/设备/产能/认证) | `id`(必填) |
|
|
17
|
+
| `start_sourcing` | 【客户 agent 自动触发】识别找厂/代工/采购意图 → 品类识别→宽召回→澄清,返回 `session_id` | `demand_text`(必填) `audience_id` |
|
|
18
|
+
| `answer_sourcing` | 续接澄清轮次:写回客户答案,返回下一轮澄清或初选列表 | `session_id`(必填) `answers` |
|
|
19
|
+
| `refine_sourcing` | 推荐轮次交互:`details`/`more`/`best` | `session_id`(必填) `action` `value` |
|
|
20
|
+
|
|
21
|
+
后三个构成「采购寻源」多轮会话链路 `start_sourcing → answer_sourcing → refine_sourcing`,
|
|
22
|
+
依赖仓库平级的 `skills/rfq-kernel` 桥接模块。
|
|
23
|
+
|
|
24
|
+
## 本次更新(v1.5.0)
|
|
25
|
+
**`get_vendor` 从 4.5 分钟降到 6 秒(45×)。** 起因是外部实测反馈「HTTP 模式比本地 git 模式慢约 100 倍,
|
|
26
|
+
按 id 取一家企业要 270 秒」。定位结论:慢不在计算,而在**为了找一个 id 而下载整张 18.4 MB 的
|
|
27
|
+
`name-index.jsonl`**——id 只是其中一行。
|
|
28
|
+
|
|
29
|
+
- **idmap 定址分片**:新增 `skills/registry/index/idmap/`,`id → 国标码` 按 id 千位切片(183 片 × 1000 id,
|
|
30
|
+
命中那片约 20 KB)。片名由 id 直接算出(`CN-MFG-0093680` → `93680 // 1000` → `093.jsonl`),
|
|
31
|
+
**不需要目录表、不需要额外请求**。
|
|
32
|
+
- **零 404 的开关设计**:idmap 的元信息放在 `manifest.metadata.idmap` 里——清单本来就要拉,所以是零额外请求。
|
|
33
|
+
清单里**没有**这个块 = 该快照尚未发布 idmap → 整体走老路径。这样「MCP 先升级、数据后发布」的窗口期
|
|
34
|
+
不会变成满屏 404(实测过:一次 404 的冷启动兜底要 44 秒)。
|
|
35
|
+
- **按内容哈希缓存分片**:清单里每片都登记了内容 sha1,用它当缓存键——内容不变则**完全不走网络**,
|
|
36
|
+
取到后顺手校验,不符就不写缓存。
|
|
37
|
+
- **zh 详情分片改并发取**;**HTTP 超时 15→30 s**;**显式声明 gzip 并手动解压**
|
|
38
|
+
(urllib 不会自动解压,JSON 压缩 6~20×,此前一直是裸传)。
|
|
39
|
+
- **实测**(`scripts/bench_get_vendor.py`,缓存目录强制隔离):
|
|
40
|
+
|
|
41
|
+
| 场景 | 冷启动 | 复用缓存 |
|
|
42
|
+
|------|--------|----------|
|
|
43
|
+
| 线上 CDN(本次前) | 270.6 s | — |
|
|
44
|
+
| 线上 CDN(本次后) | **5.95 s** | **1.22 s** |
|
|
45
|
+
| 本地快照 | 1.77 s | 0.80 s |
|
|
46
|
+
|
|
47
|
+
- **恢复 `mcp/test_protocol.py`**:上一个提交误删了发布前必跑的协议回归测试,本次还原(313 行)。
|
|
48
|
+
发布前实测三条链路 **16/16 通过**(HTTP / stdio / 标准分帧适配层 + 死代理容错)。
|
|
49
|
+
- 数据侧(`skills/registry/index/idmap/**` 与含 `metadata.idmap` 的 `data/manifest.json`)已同步发布到 Pages,
|
|
50
|
+
旧版 MCP 不受影响(读不到 idmap 块就走老路径)。
|
|
51
|
+
|
|
52
|
+
## 本次更新(v1.4.2,已发布)
|
|
53
|
+
- **自动升级(自更新)**:新增 `bin/selfupdate.js`,MCP 启动时以**独立 detached 子进程**拉起(绝不阻塞 MCP 启动),
|
|
54
|
+
节流查 npm registry `latest`(默认 24h 一次,可用 `BEACON_MCP_UPDATE_INTERVAL_H` 调),发现新版本即执行
|
|
55
|
+
`npm i -g beacon-mfg-mcp@<新版本>`(Windows 上正确走 `npm.cmd + shell`)。
|
|
56
|
+
- **跳过条件**:设 `BEACON_MCP_NO_UPDATE=1`、或已设 `BEACON_SOURCE` / `BEACON_REPO`(离线 / 本地仓库用户)时**不联网升级**,避免对本地仓库用户添乱。
|
|
57
|
+
- **并发保护**:原子目录锁 + 残留锁 10 分钟过期自愈(踩过死锁坑——残留锁会让之后所有启动永久跳过自更新)。
|
|
58
|
+
- **测试**:`test_update.js` 单元测试 23/23 通过,并以 fake-npm 拦截验证安装命令形态正确(`npm.cmd i -g beacon-mfg-mcp@1.4.2`)。
|
|
59
|
+
- **前向生效说明**:自更新是「向前生效」的——已装的 ≤1.4.1 旧版代码里没有 `selfupdate.js`,需**一次性手动** `npm i -g beacon-mfg-mcp@latest` 进入自动升级基线;之后永久自动。`npx beacon-mfg-mcp` 用户天然永远最新。
|
|
60
|
+
|
|
61
|
+
## 本次更新(v1.4.1,已发布)
|
|
62
|
+
- **直连优先 + 死代理容错**:修复「失效代理导致检索归零」——默认源不可达时直连兜底镜像,且报错信息直指真因(是代理挂了而非数据问题),`npm i -g` 装一次即享受(无需重启客户端)。
|
|
63
|
+
|
|
64
|
+
## 本次更新(v1.4.0,已发布)
|
|
65
|
+
- **标准分帧适配层 `mcp_adapter.py`**:补齐「严格分帧」客户端接入路径,与「换行 stdio」「Streamable HTTP」三条传输链路统一。
|
|
66
|
+
- **协议回归测试 `test_protocol.py`**:一条命令覆盖三条链路(握手 + 6 工具齐全 + 三城市真实检索 `via_index`),发版前必跑。
|
|
67
|
+
|
|
68
|
+
## 本次更新(v1.3.3,已发布)
|
|
69
|
+
- **本地检索免提交、免重启即时可见**:这是 v1.3.1「worktree 兜底」的收尾。v1.3.1 只让**单文件读**走了工作树,
|
|
70
|
+
但关键词/城市检索的**召回**走的是**批量读** `_read_many_text()`——它在 git 模式下恒走 `git archive HEAD`
|
|
71
|
+
(已提交快照),于是**改完数据、重建索引后,关键词检索仍读旧索引、必须 commit 才可见**。
|
|
72
|
+
本次把批量读也改为「**工作树优先 → `git archive HEAD` 兜底 → HTTP**」。
|
|
73
|
+
- **进程内 + 磁盘缓存按文件新鲜度失效**:新增 `_wt_mtime` / `_cache_stale`,各 loader
|
|
74
|
+
(manifest / 别名 / 国标名表 / cap 索引 / city / 索引桶 / fp 分片 / 能力词)改为
|
|
75
|
+
**按工作树文件 mtime 判新**——派生层一重建,MCP 立刻重读,**无需重启进程**;
|
|
76
|
+
磁盘索引缓存(`idx-*`)加 **mtime 印记**(`repr` 精确往返),同秒重写也能识别。
|
|
77
|
+
- **可选 TTL**:`BEACON_CACHE_TTL`(秒,默认 `0`=关闭)可让进程内缓存周期性刷新;
|
|
78
|
+
非工作树模式(`BEACON_WORKTREE=0`)保持旧的「提交后 / 重启后生效」行为不变。
|
|
79
|
+
- 已用**真实注入实验**验证:单进程、不 commit、只 sleep,`data/gb` 新记录经本地重建后
|
|
80
|
+
关键词检索由 `0` 命中变 `1` 命中。
|
|
81
|
+
|
|
82
|
+
## 本次更新(v1.3.2,已发布)
|
|
83
|
+
- **npm 启动器锁定 UTF-8**:`bin/beacon-mfg-mcp.js` 拉起 python 时注入 `PYTHONUTF8=1` / `PYTHONIOENCODING=utf-8`,
|
|
84
|
+
修复中文 Windows 上 python 按 GBK 写 stdout、被客户端按 UTF-8 解码导致的工具描述/结果乱码。
|
|
85
|
+
- **HTTP 兜底镜像**:默认源 `beacon-mfg.pages.dev` 不可达时,自动回退到 GitHub 镜像
|
|
86
|
+
`cdn.jsdelivr.net/gh/eiry16/beacon-mfg@main` 与 `raw.githubusercontent.com/eiry16/beacon-mfg/main`
|
|
87
|
+
(与 Android App 同策略),保证「免 clone 检索」在单一 CDN 失效时仍可工作;可用 `BEACON_MIRRORS` 追加。
|
|
88
|
+
- **serverInfo 版本对齐**:服务端自报版本改为读 `package.json`,不再硬编码,消除与 npm 包版本漂移。
|
|
89
|
+
|
|
90
|
+
## 本次更新(v1.3.1,已发布)
|
|
91
|
+
- **npm 包自带 rfq-kernel**:`prepack` 会把 `skills/rfq-kernel` 打进包内,因此 `npm i -g beacon-mfg-mcp`
|
|
92
|
+
安装后 6 个 tool 全部可用(早于此版本的 npm 包不含该桥接,3 个寻源 tool 会降级)。
|
|
93
|
+
GitHub Release tarball 同步包含 `rfq-kernel/`。
|
|
94
|
+
- **闭合检索缺口**:本地 git 模式默认 `BEACON_WORKTREE=1`,优先读工作树文件(含未提交/刚修改的数据),
|
|
95
|
+
仅在文件缺失或单对象 JSON 解析失败(半成品)时退回 `git show HEAD`,再否则走 HTTP。
|
|
96
|
+
设 `BEACON_WORKTREE=0` 可恢复「只读已提交快照」的严格行为。
|
|
97
|
+
|
|
98
|
+
## 安装(三选一)
|
|
99
|
+
|
|
100
|
+
### 1. 从 GitHub Release 下载(推荐,零依赖)
|
|
101
|
+
下载本 Release 的 `beacon-mfg-mcp-mcp-v1.5.0.tar.gz`,解压后:
|
|
102
|
+
```jsonc
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"beacon-mfg": {
|
|
106
|
+
"command": "python",
|
|
107
|
+
"args": ["/解压目录/server.py"],
|
|
108
|
+
"env": { "BEACON_REPO": "/你的/beacon-mfg仓库路径" } // 可选:离线 + 隐私安全
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. npm(需先 `npm login`,仓库已配 `NPM_TOKEN` 后自动发布)
|
|
115
|
+
```bash
|
|
116
|
+
npm install -g beacon-mfg-mcp
|
|
117
|
+
# 或一次性运行:
|
|
118
|
+
npx beacon-mfg-mcp
|
|
119
|
+
```
|
|
120
|
+
装好后客户端配置:
|
|
121
|
+
```jsonc
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"beacon-mfg": { "command": "beacon-mfg-mcp", "args": [], "env": { "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8" } }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. 从源码(git clone)
|
|
130
|
+
```bash
|
|
131
|
+
git clone https://github.com/eiry16/beacon-mfg.git
|
|
132
|
+
# 客户端 command 指向 beacon-mfg/mcp/server.py
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 环境变量
|
|
136
|
+
- `BEACON_REPO`:本地 git 仓库路径;设了就用 `git show HEAD:` + 工作树 读(推荐:离线 + 掩码态隐私安全)。
|
|
137
|
+
- `BEACON_SOURCE`:HTTP 基址,默认 `https://beacon-mfg.pages.dev`(可改自定义域名)。
|
|
138
|
+
- `BEACON_WORKTREE`:本地模式是否优先读工作树(默认 `1`;设 `0` 恢复「只读已提交快照」)。
|
|
139
|
+
- `BEACON_MIRRORS`:HTTP 模式追加的兜底镜像基址(逗号分隔);默认源不可达时除内置的 jsDelivr/raw 外,可在此追加自有镜像。
|
|
140
|
+
- `HTTPS_PROXY` / `HTTP_PROXY`:python 子进程的代理;在中国大陆等直连 GitHub 困难的网络,设 `HTTPS_PROXY=http://127.0.0.1:7890` 可让镜像链路走代理。
|
|
141
|
+
|
|
142
|
+
## 许可
|
|
143
|
+
MIT
|
package/bin/beacon-mfg-mcp.js
CHANGED
|
@@ -1,36 +1,54 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
// beacon-mfg-mcp 启动器:本 MCP 服务是纯 Python 标准库实现,这里只负责
|
|
4
|
-
// 在用户机器上找到 Python 解释器并拉起 server.py(stdio
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// beacon-mfg-mcp 启动器:本 MCP 服务是纯 Python 标准库实现,这里只负责
|
|
4
|
+
// ① 在用户机器上找到 Python 解释器并拉起 server.py(stdio 协议)
|
|
5
|
+
// ② 顺手踢一个**独立进程**去做节流的自更新检查
|
|
6
|
+
const { spawnSync, spawn } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const serverPy = path.join(__dirname, '..', 'server.py');
|
|
10
|
+
const selfUpdateJs = path.join(__dirname, 'selfupdate.js');
|
|
11
|
+
|
|
12
|
+
// 踢起自更新检查:detached + unref,主进程既不等它也不被它拖住。
|
|
13
|
+
// 为什么必须是独立进程而不是本进程里的 async 函数:
|
|
14
|
+
// 下面 spawnSync 是**同步阻塞**的,会冻结整个事件循环 —— async 的自更新
|
|
15
|
+
// 在 server 运行期间推不动,等它能跑时本进程已经 process.exit() 了,
|
|
16
|
+
// 更新逻辑永远跑不完(实测 stamp 根本不写)。独立进程不受此影响。
|
|
17
|
+
// 失败、离线、没权限一律静默:它只影响「下次启动是不是最新版」。
|
|
18
|
+
try {
|
|
19
|
+
const u = spawn(process.execPath, [selfUpdateJs], {
|
|
20
|
+
stdio: 'ignore',
|
|
21
|
+
detached: true,
|
|
22
|
+
windowsHide: true,
|
|
23
|
+
});
|
|
24
|
+
u.on('error', () => { /* 没有 node?不可能,自身就是 node 起的 */ });
|
|
25
|
+
u.unref();
|
|
26
|
+
} catch (_) { /* 自更新是加分项,绝不能影响 MCP 启动 */ }
|
|
27
|
+
|
|
28
|
+
// 探测可用的 Python 解释器(优先 python3,兼容 python / py)
|
|
29
|
+
const candidates = ['python3', 'python', 'py'];
|
|
30
|
+
let chosen = null;
|
|
31
|
+
for (const c of candidates) {
|
|
32
|
+
const r = spawnSync(c, ['--version'], { stdio: 'ignore' });
|
|
33
|
+
if (r.status === 0) {
|
|
34
|
+
chosen = c;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!chosen) {
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
'[beacon-mfg-mcp] 未找到 Python 3。请先安装 Python 3.8+ 并加入 PATH,再重试。\n'
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const child = spawnSync(chosen, [serverPy, ...process.argv.slice(2)], {
|
|
46
|
+
stdio: 'inherit',
|
|
47
|
+
// 显式锁定 UTF-8:中文 Windows 上 python 默认按 GBK 写 stdout,
|
|
48
|
+
// MCP 客户端按 UTF-8 解码会乱码。从启动器层面保证子进程用 UTF-8 输出。
|
|
49
|
+
env: Object.assign({}, process.env, {
|
|
50
|
+
PYTHONUTF8: '1',
|
|
51
|
+
PYTHONIOENCODING: 'utf-8',
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
process.exit(child.status === null ? 1 : child.status);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// beacon-mfg-mcp 自更新检查(独立进程,由 launcher 以 detached 方式拉起)
|
|
4
|
+
//
|
|
5
|
+
// 【为什么必须独立成进程】
|
|
6
|
+
// launcher 用 spawnSync 同步拉起 server.py —— 那会**阻塞 Node 整个事件循环**。
|
|
7
|
+
// 如果把自更新写成 async 函数在主进程里跑,它在 spawnSync 期间推不动,
|
|
8
|
+
// 等 server 退出、事件循环恢复时,主进程紧跟着就 process.exit() 了,
|
|
9
|
+
// 于是更新逻辑永远跑不完(实测:stamp 根本没写)。
|
|
10
|
+
// 独立 detached 子进程不受主进程阻塞与退出影响,是唯一稳的做法。
|
|
11
|
+
//
|
|
12
|
+
// 【三条硬约束】
|
|
13
|
+
// 1. 绝不阻塞 MCP 启动:本进程是 detached + unref 的,主进程不等它。
|
|
14
|
+
// 2. 失败一律静默降级:离线 / 内网 / 没装 npm / 无写权限,都只当没更新过。
|
|
15
|
+
// 3. 节流 + 可关:默认 24h 查一次;BEACON_MCP_NO_UPDATE=1 完全关闭。
|
|
16
|
+
|
|
17
|
+
const { spawnSync, spawn } = require('child_process');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const https = require('https');
|
|
22
|
+
|
|
23
|
+
const PKG = require('../package.json');
|
|
24
|
+
const CURRENT = PKG.version || '0.0.0';
|
|
25
|
+
|
|
26
|
+
const STATE_DIR = path.join(os.homedir() || '.', '.beacon-mfg');
|
|
27
|
+
const STAMP = path.join(STATE_DIR, 'update-check.json');
|
|
28
|
+
const LOCK = path.join(STATE_DIR, 'update.lock');
|
|
29
|
+
const REGISTRY = 'https://registry.npmjs.org/beacon-mfg-mcp/latest';
|
|
30
|
+
const INTERVAL_H = Number(process.env.BEACON_MCP_UPDATE_INTERVAL_H || 24);
|
|
31
|
+
|
|
32
|
+
function note(msg) {
|
|
33
|
+
// 只能写 stderr:stdout 可能是 MCP 协议通道(在独立进程里虽然安全,
|
|
34
|
+
// 但保持一致,将来若被复用也不会误伤)。
|
|
35
|
+
try { process.stderr.write('[beacon-mfg-mcp] ' + msg + '\n'); } catch (_) {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function semverGt(a, b) {
|
|
39
|
+
const pa = String(a).split('.').map((x) => parseInt(x, 10) || 0);
|
|
40
|
+
const pb = String(b).split('.').map((x) => parseInt(x, 10) || 0);
|
|
41
|
+
for (let i = 0; i < 3; i++) {
|
|
42
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
43
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shouldSkip() {
|
|
49
|
+
if (process.env.BEACON_MCP_NO_UPDATE === '1') return true;
|
|
50
|
+
// 离线仓库用户(数据来自本地 git/目录),联网升级只会添乱
|
|
51
|
+
if (process.env.BEACON_SOURCE || process.env.BEACON_REPO) return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readStamp() {
|
|
56
|
+
try { return JSON.parse(fs.readFileSync(STAMP, 'utf8')).ts || 0; } catch (_) { return 0; }
|
|
57
|
+
}
|
|
58
|
+
function writeStamp(extra) {
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
61
|
+
fs.writeFileSync(STAMP, JSON.stringify(
|
|
62
|
+
Object.assign({ ts: Date.now(), version: CURRENT }, extra || {})));
|
|
63
|
+
} catch (_) {}
|
|
64
|
+
}
|
|
65
|
+
// 锁的过期时间:超过就视为持有者已死,直接接管。
|
|
66
|
+
// 为什么必须有:detached 子进程有可能被中途杀掉(launcher 退出、Windows job object),
|
|
67
|
+
// 停在「已加锁、未解锁」之间。实测踩到过一次 —— 残留锁会让**此后所有启动**
|
|
68
|
+
// 永久跳过自更新,而且现场毫无线索。所以锁必须能自愈。
|
|
69
|
+
const LOCK_STALE_MS = Number(process.env.BEACON_MCP_UPDATE_LOCK_STALE_MIN || 10) * 60000;
|
|
70
|
+
|
|
71
|
+
function lockHeld() {
|
|
72
|
+
// flag 'wx' = 原子创建,文件已存在会抛错。用 writeFileSync 覆盖写是**不互斥**的
|
|
73
|
+
// (两个进程都会「拿到锁」然后互相 unlink),并发保护就成了摆设。
|
|
74
|
+
const grab = () => {
|
|
75
|
+
const fd = fs.openSync(LOCK, 'wx');
|
|
76
|
+
fs.writeSync(fd, String(process.pid) + ' ' + Date.now());
|
|
77
|
+
fs.closeSync(fd);
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
81
|
+
grab();
|
|
82
|
+
return true;
|
|
83
|
+
} catch (_) {
|
|
84
|
+
try {
|
|
85
|
+
const st = fs.statSync(LOCK);
|
|
86
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
87
|
+
fs.unlinkSync(LOCK); // 持有者已死,回收
|
|
88
|
+
grab();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
} catch (_) { /* 回收也失败:只当没拿到锁 */ }
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function unlock() {
|
|
96
|
+
try { fs.unlinkSync(LOCK); } catch (_) {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fetchLatest(timeoutMs) {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
let done = false;
|
|
102
|
+
const finish = (v) => { if (!done) { done = true; resolve(v); } };
|
|
103
|
+
let req;
|
|
104
|
+
try {
|
|
105
|
+
req = https.get(REGISTRY, { headers: { Accept: 'application/json' } },
|
|
106
|
+
(r) => {
|
|
107
|
+
if (r.statusCode !== 200) { r.resume(); return finish(null); }
|
|
108
|
+
let b = '';
|
|
109
|
+
r.setEncoding('utf8');
|
|
110
|
+
r.on('data', (c) => { b += c; if (b.length > 20000) r.destroy(); });
|
|
111
|
+
r.on('end', () => {
|
|
112
|
+
try { finish(JSON.parse(b).version || null); } catch (_) { finish(null); }
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
} catch (_) { return finish(null); }
|
|
116
|
+
req.setTimeout(timeoutMs, () => { try { req.destroy(); } catch (_) {} finish(null); });
|
|
117
|
+
req.on('error', () => finish(null));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const IS_WIN = process.platform === 'win32';
|
|
122
|
+
// Windows 上 npm 是 npm.cmd 批处理,必须 shell:true,否则 FileNotFoundError。
|
|
123
|
+
const NPM = IS_WIN ? 'npm.cmd' : 'npm';
|
|
124
|
+
const NPM_OPTS = { stdio: 'ignore', shell: IS_WIN, env: process.env };
|
|
125
|
+
|
|
126
|
+
async function main() {
|
|
127
|
+
if (shouldSkip()) return;
|
|
128
|
+
if (Date.now() - readStamp() < INTERVAL_H * 3600 * 1000) return; // 节流
|
|
129
|
+
if (!lockHeld()) return; // 并发保护
|
|
130
|
+
|
|
131
|
+
const latest = await fetchLatest(5000);
|
|
132
|
+
if (!latest) { writeStamp({ result: 'registry_unreachable' }); unlock(); return; }
|
|
133
|
+
writeStamp({ result: 'checked', latest });
|
|
134
|
+
if (!semverGt(latest, CURRENT)) { unlock(); return; } // 已最新
|
|
135
|
+
|
|
136
|
+
note('发现新版 ' + latest + '(当前 ' + CURRENT + '),正在更新…');
|
|
137
|
+
// 同步等安装结果:detached 进程本身就是后台的,这里等一会儿无副作用,
|
|
138
|
+
// 换来的是能把「成功/失败」明确告诉用户。
|
|
139
|
+
const r = spawnSync(NPM, ['i', '-g', 'beacon-mfg-mcp@' + latest],
|
|
140
|
+
Object.assign({ timeout: 180000 }, NPM_OPTS));
|
|
141
|
+
unlock();
|
|
142
|
+
if (r.status === 0) {
|
|
143
|
+
note('已更新 ' + CURRENT + ' → ' + latest + ',下次启动生效。');
|
|
144
|
+
} else {
|
|
145
|
+
note('自动更新未成功(可能缺权限或网络受限)。'
|
|
146
|
+
+ '可手动执行:npm i -g beacon-mfg-mcp@latest');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main().catch(() => { unlock(); process.exit(0); });
|
package/package.json
CHANGED
|
@@ -1,44 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "beacon-mfg-mcp",
|
|
3
|
-
"version": "1.
|
|
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
|
-
"keywords": [
|
|
6
|
-
"mcp",
|
|
7
|
-
"model-context-protocol",
|
|
8
|
-
"beacon-mfg",
|
|
9
|
-
"suppliers",
|
|
10
|
-
"manufacturing",
|
|
11
|
-
"china",
|
|
12
|
-
"sourcing"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://github.com/eiry16/beacon-mfg#readme",
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/eiry16/beacon-mfg.git"
|
|
18
|
-
},
|
|
19
|
-
"bugs": {
|
|
20
|
-
"url": "https://github.com/eiry16/beacon-mfg/issues"
|
|
21
|
-
},
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"bin": {
|
|
24
|
-
"beacon-mfg-mcp": "bin/beacon-mfg-mcp.js"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"server.py",
|
|
28
|
-
"http_server.py",
|
|
29
|
-
"mcp_adapter.py",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "beacon-mfg-mcp",
|
|
3
|
+
"version": "1.5.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"beacon-mfg",
|
|
9
|
+
"suppliers",
|
|
10
|
+
"manufacturing",
|
|
11
|
+
"china",
|
|
12
|
+
"sourcing"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/eiry16/beacon-mfg#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/eiry16/beacon-mfg.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/eiry16/beacon-mfg/issues"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"bin": {
|
|
24
|
+
"beacon-mfg-mcp": "bin/beacon-mfg-mcp.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"server.py",
|
|
28
|
+
"http_server.py",
|
|
29
|
+
"mcp_adapter.py",
|
|
30
|
+
"bin/",
|
|
31
|
+
"rfq-kernel/",
|
|
32
|
+
"copy-rfk.js",
|
|
33
|
+
"README.md",
|
|
34
|
+
"RELEASE.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepack": "node copy-rfk.js"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=16"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {}
|
|
43
|
+
}
|
package/rfq-kernel/demo.py
CHANGED
|
@@ -9,6 +9,15 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import sys
|
|
12
|
+
# [utf8-guard] Windows 下 stdout/stderr 非 TTY 时按 locale 编码(中文 Windows = cp936/GBK),
|
|
13
|
+
# 而本脚本输出的 ✗ ✓ ⚠ 等符号 GBK 码表里根本没有 → print 当场抛
|
|
14
|
+
# UnicodeEncodeError: 'gbk' codec can't encode character '\u2717'
|
|
15
|
+
# 整条流水线会断在打印那一行。统一 UTF-8 + 兜底替换:宁可显示成 '?',也绝不因打印而崩。
|
|
16
|
+
for _s in (sys.stdout, sys.stderr):
|
|
17
|
+
try:
|
|
18
|
+
_s.reconfigure(encoding="utf-8", errors="replace")
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
12
21
|
from datetime import datetime, timezone
|
|
13
22
|
from pathlib import Path
|
|
14
23
|
|