ht32-code-mcp 0.4.5__tar.gz → 0.5.0__tar.gz

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 (36) hide show
  1. ht32_code_mcp-0.5.0/DEPLOY.md +279 -0
  2. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/PKG-INFO +1 -1
  3. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/components.example.json +7 -0
  4. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/pyproject.toml +1 -1
  5. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/component_exporter.py +1 -0
  6. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/component_source.py +66 -14
  7. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/components.py +11 -2
  8. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/server.py +30 -2
  9. ht32_code_mcp-0.5.0/tests/test_component_redact.py +125 -0
  10. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/uv.lock +1 -1
  11. ht32_code_mcp-0.4.5/DEPLOY.md +0 -494
  12. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/.gitignore +0 -0
  13. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/.gitkeep +0 -0
  14. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/.pytest_base_final_run.log +0 -0
  15. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/README.md +0 -0
  16. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/e2e_scenario_a.py +0 -0
  17. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/e2e_scenario_b.py +0 -0
  18. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/smoke_http.py +0 -0
  19. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/smoke_stdio.py +0 -0
  20. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/__init__.py +0 -0
  21. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/__main__.py +0 -0
  22. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/exporter.py +0 -0
  23. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/index.py +0 -0
  24. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/knowledge.py +0 -0
  25. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/materializer.py +0 -0
  26. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/release.py +0 -0
  27. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/src/ht32_code_mcp/usage.py +0 -0
  28. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/conftest.py +0 -0
  29. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/test_component_export.py +0 -0
  30. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/test_component_source.py +0 -0
  31. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/test_component_tools.py +0 -0
  32. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/test_components.py +0 -0
  33. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/test_export_cloud_mode.py +0 -0
  34. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/test_knowledge_grounding.py +0 -0
  35. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/test_recipe_claims.py +0 -0
  36. {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.5.0}/tests/test_tools.py +0 -0
@@ -0,0 +1,279 @@
1
+ # MCP Server 部署指南(ht32-code)
2
+
3
+ > 本文只写**当前**部署形态。历史演进看 git log;文档与 compose 冲突时**以 compose 为准**。
4
+ > 代码在 `.work/mcp-server/`,已发布到 PyPI(`ht32-code-mcp`)。
5
+
6
+ ## 部署信息
7
+
8
+ ```
9
+ MCP Server 地址(Streamable HTTP): https://mcp.s621.work/ht32-code
10
+ PyPI 包: https://pypi.org/project/ht32-code-mcp/(uvx ht32-code-mcp)
11
+ Bundle 下载地址: https://mcp.s621.work/ht32-bundles/<token>.zip
12
+ 本地 Gitea: http://100.66.1.6:3000/S621/ht32-code.git
13
+ MCP Server 代码: .work/mcp-server/
14
+ ```
15
+
16
+ ## 当前拓扑
17
+
18
+ MCP 与 bundle 都跑在应用机(Linux 本地机),云端 OpenResty 只做反向代理:
19
+
20
+ ```
21
+ 客户端 → https://mcp.s621.work(云端 OpenResty)
22
+ → 节点小宝 P2P 组网(100.66.1.6)
23
+ → 应用机容器
24
+ ht32-code-mcp 宿主 18081 → 容器 8001 /ht32-code
25
+ ht32-bundles-web 宿主 18082 → 容器 80 /ht32-bundles
26
+ ```
27
+
28
+ 反代配置原文在**工作区** `maintenance/openresty/`(`ht32-code.conf` / `ht32-bundles.conf` / `default.conf`),
29
+ 手工贴进 1Panel 站点 `mcp.s621.work` 的同一个 server 块。`/ht32-bundles` **必须反代**
30
+ 到 `100.66.1.6:18082`,且 `proxy_pass` 不能带尾斜杠。
31
+
32
+ ## 运行形态
33
+
34
+ ```
35
+ PyPI 包 → FastMCP 直接提供 Streamable HTTP(常驻单进程)
36
+ ```
37
+
38
+ - 容器直接执行 `ht32-code-mcp`,`MCP_TRANSPORT=streamable-http`。
39
+ - `MCP_STATELESS_HTTP=0`:同一进程内多会话,不再一会话一 Python 子进程。
40
+ - 内存:单进程约 0.1–0.3GB;`export_project_bundle` 并发由应用层信号量限制为 2。
41
+ - 冷启动:进程启动时加载一次代码索引;新会话不再重付。
42
+
43
+ ### 镜像与端点
44
+
45
+ - 镜像 `ht32-code-mcp:0.5.0`:`ghcr.io/astral-sh/uv:python3.12-bookworm-slim` +
46
+ `git/curl/ca-certificates`,`uv tool install ht32-code-mcp==0.5.0`,入口直跑
47
+ `ht32-code-mcp`(私有组件能力仍需服务端 git)。由 `.work/docker/Dockerfile` 构建。
48
+ - 传输:生产 `MCP_TRANSPORT=streamable-http`,`path=/ht32-code`(`MCP_PATH` 注入);
49
+ 本地调试保持默认 `/mcp`。`stdio` 仅供冒烟测试。
50
+ - `MCP_JSON_RESPONSE=0`:保持 SSE,兼容现有客户端。
51
+ - 健康检查:`GET /health` → `{"status":"ok","version":"<包版本>"}`,不触发检索。
52
+ - 资源:`mem_limit: 1g` / `cpus: 1.0` / `pids_limit: 512`(常驻单进程与多会话对象);
53
+ 不再使用 V1 按「一会话一进程」算出的 `4g` / `1024` 熔断值。
54
+
55
+ ## compose 关键配置
56
+
57
+ 文件:`.work/docker/docker-compose.yml`(项目名 `ht32-code`)。
58
+
59
+ | 项 | 值 |
60
+ |---|---|
61
+ | 端口 | `18081:8001` |
62
+ | `CODE_ROOT` | `/app/code`(挂载 `/vol1/1000/SSD/docker/ht32-code`) |
63
+ | `FEEDBACK_ROOT` | `/app/feedback`(挂载 `/vol1/1000/SSD/docker/ht32-mcp-feedback`) |
64
+ | `BUNDLE_DIR` | `/app/bundles`(挂载 `/vol1/1000/SSD/docker/ht32-web/ht32-bundles`) |
65
+ | `BUNDLE_BASE_URL` | `https://mcp.s621.work/ht32-bundles` |
66
+ | `COMPONENTS_CONFIG` | `/app/code/.work/components/components.json` |
67
+ | `COMPONENTS_STATE_FILE` | `/app/feedback/component-tag-lock.json` |
68
+ | 凭据 | `.work/docker/.env` 的 `COMPONENTS_GIT_AUTH`(base64 用户名:只读 token),已 gitignore |
69
+ | `MCP_TRANSPORT` | `streamable-http`(不经 supergateway) |
70
+ | `MCP_HOST` / `MCP_PORT` | `0.0.0.0` / `8001` |
71
+ | `MCP_PATH` | `/ht32-code`(须与反代 location 一致) |
72
+ | `MCP_STATELESS_HTTP` | `0`(P0 保留进程内有状态会话) |
73
+ | `MCP_JSON_RESPONSE` | `0`(保持 SSE 响应) |
74
+ | 资源 | `mem_limit: 1g` / `cpus: 1.0` / `pids_limit: 512` |
75
+ | 健康检查 | `curl -fsS http://127.0.0.1:8001/health`,30s 间隔,`start_period: 60s` |
76
+
77
+ > `CODE_ROOT` 挂载**不加 `:ro`**:`index/code_index.json` 已 gitignore,容器内可能需重建。
78
+
79
+ ## 云端 zip 下载通道
80
+
81
+ `export_project_bundle` 有两种交付形态,**由服务端环境变量决定,调用方无需判断**:
82
+
83
+ | 模式 | 触发条件 | 行为 |
84
+ |---|---|---|
85
+ | 云端 zip | `BUNDLE_DIR` 与 `BUNDLE_BASE_URL` **同时非空** | 打包到 `BUNDLE_DIR`,返回 `<BUNDLE_BASE_URL>/<token>.zip` + SHA256 + 过期时间 |
86
+ | 本地目录 | 二者任一为空(默认) | 物化到 `out_dir`(须为空目录/不存在) |
87
+
88
+ | 变量 | 默认 | 说明 |
89
+ |------|------|------|
90
+ | `BUNDLE_DIR` | 空(= 关闭云端模式) | 容器内 bundle 输出目录,须可写 |
91
+ | `BUNDLE_BASE_URL` | 空(= 关闭云端模式) | 公网下载前缀 |
92
+ | `BUNDLE_TTL_SECONDS` | `3600` | zip 有效期;每次 export 前顺带清理过期 zip |
93
+
94
+ 实现要点:
95
+
96
+ - **token**:`secrets.token_urlsafe(24)`(192 bit 熵),文件名即 token,不可枚举
97
+ - **物化在系统临时目录**:bundle 目录里只出现最终 zip,失败不留公开半成品
98
+ - **清理**:每次 export 按 mtime `sweep_expired()`;TTL 到点不主动删,
99
+ 靠下次 export 或同步脚本兜底;过期后重新调用工具即可拿新 URL
100
+
101
+ ### 服务器侧落地
102
+
103
+ 两个容器通过应用机上的同一目录交换文件:
104
+
105
+ ```
106
+ 应用机 /vol1/1000/SSD/docker/ht32-web/ht32-bundles/
107
+ ├─ ht32-code-mcp 容器 看到 /app/bundles(按 TTL 写 zip)
108
+ └─ ht32-bundles-web 容器 看到 /usr/share/nginx/html/ht32-bundles(18082 → 80)
109
+ 云端 OpenResty /ht32-bundles → proxy_pass http://100.66.1.6:18082
110
+ 公网 URL https://mcp.s621.work/ht32-bundles/<token>.zip
111
+ ```
112
+
113
+ compose 已包含两个服务,无需界面手工加挂载。云端 OpenResty 必须有:
114
+
115
+ ```nginx
116
+ location ^~ /ht32-bundles {
117
+ proxy_pass http://100.66.1.6:18082;
118
+ proxy_http_version 1.1;
119
+ proxy_set_header Host $host;
120
+ proxy_read_timeout 300s;
121
+ proxy_buffering on;
122
+ }
123
+ ```
124
+
125
+ 漏了这条 location,`/ht32-bundles/` 会被 `default.conf` 兜底成 404。
126
+
127
+ ### 验证
128
+
129
+ 1. `get_code_version` 出现
130
+ `云端 zip 下载:启用(TTL 3600s,https://mcp.s621.work/ht32-bundles)`;
131
+ 仍显示「未配置」→ 环境变量没注入(查变量名 / 容器是否重建)。
132
+ 2. `export_project_bundle(chip="HT32F52352")` 返回下载 URL、有效期、大小、SHA256。
133
+ 3. curl 拉该 URL:约 1.5 MB zip,`sha256sum` 与报告一致,解压后 UV4 编译 0E0W。
134
+ 4. `ls /vol1/1000/SSD/docker/ht32-web/ht32-bundles` 只有 `*.zip`(无残留临时目录)。
135
+
136
+ ## 反馈与用量日志
137
+
138
+ 与 ht32-wiki 使用**同一套实现**(`usage.py`,两仓库逐字节相同)、同一套目录约定
139
+ (同根 + 服务子目录)、同一套字段。本服务写 `<FEEDBACK_ROOT>/code/`。
140
+
141
+ | 通道 | 文件 | 触发 | 内容 |
142
+ |------|------|------|------|
143
+ | 用量日志 | `usage_*.jsonl` | 服务端自动(middleware) | 工具名、参数原文、耗时、成败、session |
144
+ | 反馈 | `feedback_*.jsonl` | Agent 主动(`submit_feedback`) | 语义性问题 |
145
+
146
+ - `FEEDBACK_ROOT` 为空 = 整体关闭(本地开发默认不落盘);必须填**绝对路径**。
147
+ - `ok` 字段口径是「工具是否完成了它的工作」:`错误:` / `导出失败` / `请至少提供` 记失败;
148
+ `未找到` / `不在支持列表` 记成功("库里没有"是补内容的最强信号)。
149
+ - 清理由同步脚本负责(`LOG_KEEP_DAYS=30`)。
150
+
151
+ ### 目录布局与验证
152
+
153
+ ```
154
+ /vol1/1000/SSD/docker/ht32-mcp-feedback/ ← 宿主机单一入口,两容器挂同一卷
155
+ ├─ wiki/ ← ht32-wiki 容器写
156
+ └─ code/ ← ht32-code 容器写
157
+ ```
158
+
159
+ `get_code_version` 应显示 `反馈与用量日志:启用(/app/feedback/code)`;
160
+ `tail -3 /vol1/1000/SSD/docker/ht32-mcp-feedback/code/usage_$(date -u +%Y-%m-%d).jsonl` 能看到记录。
161
+ 合并分析用 ht32-skills 的 `scripts/analyze_feedback.py`。
162
+
163
+ ## 私有组件
164
+
165
+ 让 MCP 用户发现私有 Git 组件(BSP / 中间件 / 驱动包),检索/读取源码,并把选定 tag 的
166
+ 源码快照并入导出工程。
167
+
168
+ ```
169
+ 用户 → MCP: list_components / get_component_info / search_component_code / read_component_file
170
+ export_project_bundle(components=["<id>@<tag>"])
171
+ MCP → 仓库内登记表(.work/components/components.json)→ 私有 Git
172
+ ```
173
+
174
+ | 变量 | 默认 | 说明 |
175
+ |------|------|------|
176
+ | `COMPONENTS_CONFIG` | `.work/components/components.json`(文件存在即启用) | 组件目录;配置无效时**服务启动失败**并定位到字段 |
177
+ | `COMPONENTS_STATE_FILE` | `<临时目录>/ht32-component-cache/tag-lock.json` | tag→commit 台账(检测 tag 被强制移动) |
178
+ | `COMPONENT_GIT_TIMEOUT` | `60` | 单次 Git 操作超时(秒) |
179
+
180
+ 运行时依赖 `git` + 私有仓库凭据,缺一即 `[COMPONENT_REMOTE_UNAVAILABLE]`:
181
+
182
+ 1. **镜像**:`ht32-code-mcp:0.4.5-git` 已补 git;`docker exec ht32-code-mcp git --version` 应有输出。
183
+ 2. **凭据**:部署机 `.work/docker/.env` 写
184
+ `COMPONENTS_GIT_AUTH=<base64("用户名:只读token")>`;compose 以
185
+ `GIT_CONFIG_*` 注入 `http.extraheader`,不写入仓库、日志或 MCP 响应。轮换后改 `.env` 并重建容器。
186
+ 3. **验证**:`docker exec ht32-code-mcp git ls-remote http://100.66.1.6:3000/S621/LwFSM.git | tail -2`
187
+ 能列出 `refs/tags/V2.0`。
188
+
189
+ ### 源码裁剪(`export.redact`)
190
+
191
+ 面向"只交付 `.lib`、不给源码"的组件。不执行仓库里的 `script/submodule.bat`,
192
+ 改为快照层内置规则:
193
+
194
+ | 模式 | 行为 |
195
+ |---|---|
196
+ | `none`(默认) | 不裁剪 |
197
+ | `auto` | 仅当仓库含 `script/submodule.bat` 时按 `lib-only` 规则裁剪 |
198
+ | `lib-only` | 无条件裁剪 |
199
+
200
+ `lib-only` 规则:移除根 `project/`;`lib/<name>.lib` 存在则移除 `src/<name>.c`;
201
+ `tools/InfoProcess.exe` 存在则移除 `tools/InfoProcess.c`;`script/` 保留。
202
+ 裁剪发生在快照构建阶段,`read_component_file` / `search_component_code` / 导出均看不到被移除文件;
203
+ `manifest/components-lock.json` 记录 `redaction.mode` 与逐条 `removed`。
204
+
205
+ ### 登记表位置
206
+
207
+ 放在 ht32-code 仓库 `.work/components/components.json`,随仓库版本化:
208
+
209
+ - 新增组件 = 一次 commit;服务器 `git reset --hard` 后自动生效;**不需要发 MCP 版本**
210
+ - **不进 PyPI**:wheel 只含 `src/`;sdist 已排除 `.work/components`
211
+ - **前提:仓库保持私有**。转公开前必须先把登记表移出
212
+ - `catalog_sha256` / 仓库 `commit` / `catalog_version` 一起出现在
213
+ `get_code_version` / `list_components` / `components-lock.json`,用于反查登记表版本
214
+ - 字段要点:`series` 按族前缀/通配匹配,`["*"]` 表示覆盖全型号;`chips` 只用于展示已验证型号
215
+ - 加载器支持 `//` 与 `/* */` 注释(不支持尾随逗号),注释计入哈希
216
+
217
+ ### 日常运维
218
+
219
+ | 操作 | 做法 |
220
+ |---|---|
221
+ | 新增组件 | 改登记表 → commit/push → 同步脚本或手动 reset + 重建 |
222
+ | 刷新可用 tag | 组件维护者打 tag 即可,查询时自动出现 |
223
+ | 下架/暂停 | `"status": "disabled"` → commit → 同步 |
224
+ | 回滚登记表 | `git checkout <commit> -- .work/components/components.json` → commit/push |
225
+ | tag 漂移 | 同 tag 指向新 commit 返回 `[COMPONENT_TAG_DRIFT]`,核实后删台账该行重试 |
226
+ | 查历史 | `git log -p -- .work/components/components.json`;工程侧看 `components-lock.json` |
227
+ | 不要在服务器上直接改 | 更新脚本 `git reset --hard` 会覆盖手改 |
228
+
229
+ ### 隐私检查
230
+
231
+ ```bash
232
+ uv build && tar -tzf dist/*.tar.gz | grep -i "\.work/components" && echo "泄漏!" || echo "OK"
233
+ git grep -nE "PRIVATE-TOKEN|password=|ssh://.*@" -- .work/components
234
+ ```
235
+
236
+ ## 更新与发布
237
+
238
+ **服务器侧更新**(改 compose / 路径 / 端口 push 后,脚本一跑即生效):
239
+
240
+ ```bash
241
+ cd /vol1/1000/SSD/docker/ht32-code
242
+ git fetch origin && git reset --hard origin/master
243
+ docker compose -f .work/docker/docker-compose.yml up -d --force-recreate --build
244
+ ```
245
+
246
+ 或触发 1Panel 计划任务 `ht32-code-mcp-update`(每天 02:30):同步 → HEAD 变化才删旧索引 →
247
+ 重建容器 → 清理过期 bundle / 日志。
248
+
249
+ **PyPI 发布**(改了 `.work/mcp-server/src/` 就必须发版):
250
+
251
+ ```bash
252
+ cd .work/mcp-server
253
+ uv run --extra dev pytest
254
+ python smoke_stdio.py
255
+ uv lock && uv build && uv publish # token 在仓库根 .env 的 UV_PUBLISH_TOKEN
256
+ ```
257
+
258
+ - 两处版本号必须同步:`pyproject.toml` 的 `version` 与 `VERSION` 末行 `MCP Server:`
259
+ - 发布前清理 `dist/`,否则会把旧包一起上传
260
+ - 发布后 `get_code_version` 应返回新版本;仍是旧版则查服务器 `git pull` 与容器重建
261
+
262
+ > 线上跑的是 **PyPI 包**,`/vol1/1000/SSD/docker/ht32-code` 只是被检索的数据源。
263
+ > 改仓库文件本身不生效——必须发版 + 重建容器。
264
+
265
+ ## 排查
266
+
267
+ ```bash
268
+ docker logs --tail 100 ht32-code-mcp # 启动参数、组件目录校验、请求日志都在这里
269
+ docker exec -it ht32-code-mcp sh # 进容器
270
+ curl -s http://127.0.0.1:8001/health # 容器内健康检查(宿主 18081)
271
+ curl -s -o /dev/null -w "%{http_code}" https://mcp.s621.work/ht32-code # 400 = 服务在(正常,缺 MCP 头)
272
+ ```
273
+
274
+ - 启动日志应含 `启动服务: Streamable HTTP 0.0.0.0:8001/ht32-code(stateless=False, json=False)`;
275
+ 若仍见 supergateway / `--stateful`,说明服务器 compose 未同步或镜像未重建
276
+ - 协议版本被拒 → 旧镜像或旧客户端;本版直接由 FastMCP 4.x 提供,不再经 supergateway
277
+ - `/health` 404 → 进程是旧版(`MCP_PATH` 之前的版本没有该路由)
278
+ - 容器 126 退出 → 旧 compose 的 `$$(...)` 命令替换被吃掉;新版无 `command`,重新 `git pull` 即可
279
+ - 首会话超时 → 冷启动(索引加载);容器健康检查 `start_period: 60s` 已留余量
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ht32-code-mcp
3
- Version: 0.4.5
3
+ Version: 0.5.0
4
4
  Summary: HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出
5
5
  License-Expression: MIT
6
6
  Keywords: firmware,holtek,ht32,mcp,mcu
@@ -54,6 +54,13 @@
54
54
  "exclude": ["docs/", "tests/"],
55
55
  // 非空时只允许这些后缀,如 [".c", ".h"];空 = 不限
56
56
  "allowed_suffixes": [],
57
+ // 源码裁剪(不执行仓库脚本):none / auto / lib-only
58
+ // none = 不裁剪(默认)
59
+ // auto = 仅当仓库含 script/submodule.bat 时按下面的内置规则裁剪
60
+ // lib-only= 无条件裁剪:移除 project/、删除有同名 lib/*.lib 的 src/*.c、
61
+ // 删除与 tools/InfoProcess.exe 同名的 .c;script/ 保留
62
+ // 裁剪在快照层生效,read/search/export 都看不到被移除的源码
63
+ "redact": "none",
57
64
  // 容量上限:文件数 / 单文件字节 / 总字节
58
65
  "max_files": 2000,
59
66
  "max_file_bytes": 2097152,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ht32-code-mcp"
7
- version = "0.4.5"
7
+ version = "0.5.0"
8
8
  description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
9
9
  requires-python = ">=3.11"
10
10
  license = "MIT"
@@ -64,6 +64,7 @@ class PreparedComponents:
64
64
  "files": [{"path": f.rel, "bytes": f.size, "sha256": f.sha256}
65
65
  for f in snap.files],
66
66
  "excluded": snap.excluded,
67
+ "redaction": {"mode": snap.redact_mode, "removed": snap.redacted},
67
68
  "license": item.entry.license,
68
69
  "maintainer": item.entry.maintainer,
69
70
  })
@@ -72,7 +72,8 @@ class Snapshot:
72
72
 
73
73
  def __init__(self, entry: ComponentEntry, tag: str, commit: str,
74
74
  workdir: Path, archive: Path, files: list[SnapshotFile],
75
- excluded: list[dict]) -> None:
75
+ excluded: list[dict], redact_mode: str = "none",
76
+ redacted: list[dict] | None = None) -> None:
76
77
  self.entry = entry
77
78
  self.tag = tag
78
79
  self.commit = commit
@@ -80,6 +81,8 @@ class Snapshot:
80
81
  self.archive = archive
81
82
  self.files = files
82
83
  self.excluded = excluded
84
+ self.redact_mode = redact_mode
85
+ self.redacted = redacted or []
83
86
 
84
87
  @property
85
88
  def total_bytes(self) -> int:
@@ -285,19 +288,20 @@ class ComponentSource:
285
288
  "只接受发布 tag 指向的提交")
286
289
  archive = workdir / "tree.zip"
287
290
  self._git(["archive", "--format=zip", f"--output={archive}", head], cwd=repo)
288
- files, excluded = self._plan(archive, info.tag)
289
- return Snapshot(self.entry, info.tag, info.commit, workdir, archive, files, excluded)
291
+ files, excluded, redact_mode, redacted = self._plan(archive, info.tag)
292
+ return Snapshot(self.entry, info.tag, info.commit, workdir, archive,
293
+ files, excluded, redact_mode, redacted)
290
294
  except BaseException:
291
295
  _rmtree(workdir)
292
296
  raise
293
297
 
294
298
  # ---------------------------------------------------------- 文件计划
295
- def _plan(self, archive: Path, tag: str) -> tuple[list[SnapshotFile], list[dict]]:
299
+ def _plan(self, archive: Path, tag: str
300
+ ) -> tuple[list[SnapshotFile], list[dict], str, list[dict]]:
296
301
  export = self.entry.export
297
302
  root_prefix = f"{export.root}/" if export.root else ""
298
- files: list[SnapshotFile] = []
303
+ candidates: list[SnapshotFile] = []
299
304
  excluded: list[dict] = []
300
- total = 0
301
305
  with zipfile.ZipFile(archive) as zf:
302
306
  for info in zf.infolist():
303
307
  if info.is_dir():
@@ -323,20 +327,39 @@ class ComponentSource:
323
327
  if export.allowed_suffixes and not name.lower().endswith(export.allowed_suffixes):
324
328
  raise _path_rejected(
325
329
  f"组件文件类型不在登记允许列表({list(export.allowed_suffixes)}):{name}")
326
- if info.file_size > export.max_file_bytes:
327
- raise ComponentError(
328
- COMPONENT_LIMIT_EXCEEDED,
329
- f"组件 '{self.entry.id}' 的 {name} 为 {info.file_size} 字节,"
330
- f"超过单文件上限 {export.max_file_bytes}")
331
- total += info.file_size
332
- files.append(SnapshotFile(
330
+ candidates.append(SnapshotFile(
333
331
  rel=name, size=info.file_size,
334
332
  sha256=hashlib.sha256(zf.read(info)).hexdigest(), zip_name=info.filename))
333
+ if not candidates:
334
+ raise ComponentError(
335
+ COMPONENT_TAG_NOT_FOUND,
336
+ f"组件 '{self.entry.id}' 在 tag '{tag}' 的导出根 "
337
+ f"'{export.root or '.'}' 下没有可导出文件")
338
+
339
+ redact_mode, removed = _redaction_plan(export.redact, [f.rel for f in candidates])
340
+ files: list[SnapshotFile] = []
341
+ redacted: list[dict] = []
342
+ for item in candidates:
343
+ reason = removed.get(item.rel.lower())
344
+ if reason:
345
+ redacted.append({"path": item.rel, "reason": reason})
346
+ excluded.append({"path": item.rel, "reason": reason})
347
+ else:
348
+ files.append(item)
335
349
  if not files:
336
350
  raise ComponentError(
337
351
  COMPONENT_TAG_NOT_FOUND,
338
352
  f"组件 '{self.entry.id}' 在 tag '{tag}' 的导出根 "
339
353
  f"'{export.root or '.'}' 下没有可导出文件")
354
+
355
+ total = 0
356
+ for item in files:
357
+ if item.size > export.max_file_bytes:
358
+ raise ComponentError(
359
+ COMPONENT_LIMIT_EXCEEDED,
360
+ f"组件 '{self.entry.id}' 的 {item.rel} 为 {item.size} 字节,"
361
+ f"超过单文件上限 {export.max_file_bytes}")
362
+ total += item.size
340
363
  if len(files) > export.max_files:
341
364
  raise ComponentError(
342
365
  COMPONENT_LIMIT_EXCEEDED,
@@ -345,7 +368,36 @@ class ComponentSource:
345
368
  raise ComponentError(
346
369
  COMPONENT_LIMIT_EXCEEDED,
347
370
  f"组件 '{self.entry.id}' 总字节 {total} 超过上限 {export.max_total_bytes}")
348
- return sorted(files, key=lambda f: f.rel), excluded
371
+ return sorted(files, key=lambda f: f.rel), excluded, redact_mode, redacted
372
+
373
+
374
+ def _redaction_plan(mode: str, names: list[str]) -> tuple[str, dict[str, str]]:
375
+ """源码裁剪:等价于组件仓库的 `script/submodule.bat`,但**不执行**任何脚本。
376
+
377
+ 规则(路径相对组件导出根,大小写不敏感):
378
+ - 移除根目录 `project/`(示例/测试工程);
379
+ - `lib/<name>.lib` 存在时,移除 `src/<name>.c`;
380
+ - `tools/InfoProcess.exe` 存在时,移除 `tools/InfoProcess.c`;
381
+ - `script/` 保留。
382
+ `auto` 仅当检测到 `script/submodule.bat` 时生效;`lib-only` 无条件生效。
383
+ 返回 (生效模式, {小写路径: 原因})。
384
+ """
385
+ if mode == "none":
386
+ return "none", {}
387
+ lowered = [n.lower() for n in names]
388
+ if mode == "auto" and "script/submodule.bat" not in lowered:
389
+ return "none", {}
390
+ libs = {Path(n).stem for n in lowered if re.fullmatch(r"lib/[^/]+\.lib", n)}
391
+ has_tool_exe = "tools/infoprocess.exe" in lowered
392
+ removed: dict[str, str] = {}
393
+ for name in lowered:
394
+ if name.startswith("project/"):
395
+ removed[name] = "redact:project-dir"
396
+ elif re.fullmatch(r"src/[^/]+\.c", name) and Path(name).stem in libs:
397
+ removed[name] = "redact:source-replaced-by-lib"
398
+ elif has_tool_exe and name == "tools/infoprocess.c":
399
+ removed[name] = "redact:tool-source"
400
+ return "lib-only", removed
349
401
 
350
402
 
351
403
  def _unsafe_member(name: str) -> bool:
@@ -53,9 +53,11 @@ _COMPONENT_KEYS = {
53
53
  _TAGS_KEYS = {"include", "exclude"}
54
54
  _EXPORT_KEYS = {
55
55
  "root", "target", "exclude", "allowed_suffixes",
56
- "max_files", "max_file_bytes", "max_total_bytes",
56
+ "max_files", "max_file_bytes", "max_total_bytes", "redact",
57
57
  }
58
58
 
59
+ REDACT_MODES = ("none", "auto", "lib-only")
60
+
59
61
  DEFAULT_MAX_FILES = 2000
60
62
  DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024
61
63
  DEFAULT_MAX_TOTAL_BYTES = 64 * 1024 * 1024
@@ -82,6 +84,8 @@ class ExportPolicy:
82
84
  max_files: int = DEFAULT_MAX_FILES
83
85
  max_file_bytes: int = DEFAULT_MAX_FILE_BYTES
84
86
  max_total_bytes: int = DEFAULT_MAX_TOTAL_BYTES
87
+ # 源码裁剪:none / auto(检测到 script/submodule.bat 才裁剪)/ lib-only
88
+ redact: str = "none"
85
89
 
86
90
 
87
91
  @dataclass(frozen=True)
@@ -308,6 +312,10 @@ def _parse_export(data: dict, where: str, component_id: str) -> ExportPolicy:
308
312
  else:
309
313
  target = f"components/{component_id}"
310
314
  excludes = _str_list(block, "exclude", f"{where}.export")
315
+ redact = (_optional_str(block, "redact", f"{where}.export") or "none").strip().lower()
316
+ if redact not in REDACT_MODES:
317
+ raise _fail(f"{where}.export.redact",
318
+ f"只支持 {' / '.join(REDACT_MODES)},当前为 '{redact}'")
311
319
  suffixes = tuple(s if s.startswith(".") else "." + s
312
320
  for s in _str_list(block, "allowed_suffixes", f"{where}.export"))
313
321
  max_files = _int_field(block, "max_files", f"{where}.export", DEFAULT_MAX_FILES)
@@ -319,7 +327,8 @@ def _parse_export(data: dict, where: str, component_id: str) -> ExportPolicy:
319
327
  raise _fail(f"{where}.export", "max_files 超出合理上限(100000)")
320
328
  return ExportPolicy(root=root, target=target, exclude=excludes,
321
329
  allowed_suffixes=suffixes, max_files=max_files,
322
- max_file_bytes=max_file, max_total_bytes=max_total)
330
+ max_file_bytes=max_file, max_total_bytes=max_total,
331
+ redact=redact)
323
332
 
324
333
 
325
334
  def _parse_component(raw: object, index: int, remotes: dict[str, str],
@@ -64,6 +64,11 @@ LOG_DIR = resolve_log_dir(FEEDBACK_ROOT, SERVICE_CODE)
64
64
  # 默认 8101 而非 8100:wiki 占用 8100,两服务常同机部署,避开默认端口冲突
65
65
  HOST = os.environ.get("MCP_HOST", "0.0.0.0")
66
66
  PORT = _env_int("MCP_PORT", 8101)
67
+ # HTTP 端点路径:生产由 compose 注入 /ht32-code;默认 /mcp 保持本地 smoke 测试不变
68
+ MCP_PATH = os.environ.get("MCP_PATH", "/mcp")
69
+ # P0 先保留进程内有状态会话(兼容现有客户端与热身脚本);要多副本时再开 1
70
+ STATELESS_HTTP = os.environ.get("MCP_STATELESS_HTTP", "0") != "0"
71
+ JSON_RESPONSE = os.environ.get("MCP_JSON_RESPONSE", "0") != "0"
67
72
 
68
73
  index = CodeIndex(CODE_ROOT).load()
69
74
  knowledge = KnowledgeSearch(CODE_ROOT)
@@ -171,6 +176,15 @@ M0+(HT32F5 / L5)、M3(HT32F1 / F175x-275x)、M4(F490x1 / F491x3 / F493
171
176
 
172
177
  mcp = FastMCP("ht32-code", instructions=_INSTRUCTIONS)
173
178
 
179
+
180
+ @mcp.custom_route("/health", methods=["GET"])
181
+ async def _health(_request):
182
+ """容器健康检查:不触发检索,不占线程池。"""
183
+ from starlette.responses import JSONResponse
184
+
185
+ return JSONResponse({"status": "ok", "version": _MCP_VERSION})
186
+
187
+
174
188
  # 用量日志:服务端自动记录所有工具调用(FEEDBACK_ROOT 为空则不启用)
175
189
  if LOG_DIR:
176
190
  mcp.add_middleware(UsageMiddleware(
@@ -702,6 +716,9 @@ def get_component_info(component_id: str) -> str:
702
716
  lines.append(f"- tag 规则:include={list(entry.tag_include)},"
703
717
  f"exclude={list(entry.tag_exclude)}")
704
718
  lines.extend(describe_tags(tags, err, limit=50))
719
+ if entry.export.redact != "none":
720
+ lines.append(f"- 源码裁剪:{entry.export.redact}"
721
+ "(移除 project/、有同名 .lib 的 src/*.c、InfoProcess.c;读/搜/导出统一生效)")
705
722
  export = entry.export
706
723
  lines += [
707
724
  f"- 导出落点:{entry.target}/(仓库导出根:{export.root or '.'})",
@@ -942,8 +959,19 @@ def main() -> None:
942
959
  print(f"[ht32-code-mcp] 启动服务: SSE {HOST}:{PORT}", file=sys.stderr)
943
960
  mcp.run(transport="sse", host=HOST, port=PORT)
944
961
  else:
945
- print(f"[ht32-code-mcp] 启动服务: Streamable HTTP {HOST}:{PORT}", file=sys.stderr)
946
- mcp.run(transport="streamable-http", host=HOST, port=PORT)
962
+ print(
963
+ f"[ht32-code-mcp] 启动服务: Streamable HTTP {HOST}:{PORT}{MCP_PATH}"
964
+ f"(stateless={STATELESS_HTTP}, json={JSON_RESPONSE})",
965
+ file=sys.stderr,
966
+ )
967
+ mcp.run(
968
+ transport="streamable-http",
969
+ host=HOST,
970
+ port=PORT,
971
+ path=MCP_PATH,
972
+ stateless_http=STATELESS_HTTP,
973
+ json_response=JSON_RESPONSE,
974
+ )
947
975
 
948
976
 
949
977
  if __name__ == "__main__":
@@ -0,0 +1,125 @@
1
+ """组件源码裁剪:等价 `script/submodule.bat`,但不在服务端执行任何脚本。"""
2
+ from __future__ import annotations
3
+
4
+ import pytest
5
+
6
+ from conftest import catalog_document, commit_all, git, write
7
+ from ht32_code_mcp.component_exporter import ComponentService
8
+ from ht32_code_mcp.component_source import (ComponentSource, TagLockStore,
9
+ _redaction_plan)
10
+ from ht32_code_mcp.components import ComponentError, parse_catalog
11
+
12
+
13
+ @pytest.fixture
14
+ def redact_repo(tmp_path):
15
+ if not __import__("shutil").which("git"):
16
+ pytest.skip("环境无 git,跳过源码裁剪测试")
17
+ repo = tmp_path / "iap-repo"
18
+ repo.mkdir()
19
+ git(repo, "init", "-q")
20
+ write(repo, "README.md", "# HT32 IAP\n")
21
+ write(repo, "inc/ht32_iap.h", "void iap_init(void);\n")
22
+ write(repo, "src/ht32_iap.c", "void iap_init(void) {}\n")
23
+ write(repo, "src/ht32_iap_cache.c", "void cache_init(void) {}\n")
24
+ write(repo, "src/extra.c", "void extra(void) {}\n")
25
+ write(repo, "project/test/main.c", "int main(void) { return 0; }\n")
26
+ write(repo, "tools/InfoProcess.c", "int main(void) { return 0; }\n")
27
+ (repo / "tools" / "InfoProcess.exe").write_bytes(b"MZ")
28
+ (repo / "lib").mkdir()
29
+ (repo / "lib" / "ht32_iap.lib").write_bytes(b"LIB-1")
30
+ (repo / "lib" / "ht32_iap_cache.lib").write_bytes(b"LIB-2")
31
+ write(repo, "script/submodule.bat", "@echo off\r\n")
32
+ commit = commit_all(repo, "iap v1")
33
+ git(repo, "tag", "v1.0.0")
34
+ return {"path": repo, "commit": commit}
35
+
36
+
37
+ def _catalog(redact: str, remote: str = "https://git.example.invalid/org/iap.git"):
38
+ doc = catalog_document(remote)
39
+ doc["components"][0]["export"] = {"target": "", "redact": redact}
40
+ return parse_catalog(doc)
41
+
42
+
43
+ def _source(repo, tmp_path, redact: str) -> ComponentSource:
44
+ catalog = _catalog(redact)
45
+ return ComponentSource(catalog.get("ht32_bsp"), str(repo),
46
+ work_root=tmp_path / "work", timeout=120,
47
+ tag_locks=TagLockStore(None))
48
+
49
+
50
+ def test_redaction_plan_auto_requires_script():
51
+ names = ["src/a.c", "lib/a.lib", "project/main.c"]
52
+ assert _redaction_plan("auto", names) == ("none", {})
53
+
54
+
55
+ def test_redaction_plan_lib_only_rules():
56
+ names = ["src/a.c", "src/b.c", "lib/a.lib", "project/x/main.c",
57
+ "tools/InfoProcess.c", "tools/InfoProcess.exe",
58
+ "script/submodule.bat"]
59
+ mode, removed = _redaction_plan("lib-only", names)
60
+ assert mode == "lib-only"
61
+ assert set(removed) == {"src/a.c", "project/x/main.c", "tools/infoprocess.c"}
62
+ assert removed["src/a.c"] == "redact:source-replaced-by-lib"
63
+
64
+
65
+ def test_snapshot_redacts_sources_but_keeps_lib_header_and_script(redact_repo, tmp_path):
66
+ src = _source(redact_repo["path"], tmp_path, "auto")
67
+ snap = src.snapshot("v1.0.0")
68
+ try:
69
+ rels = {f.rel for f in snap.files}
70
+ assert snap.redact_mode == "lib-only"
71
+ assert "project/test/main.c" not in rels
72
+ assert "src/ht32_iap.c" not in rels
73
+ assert "src/ht32_iap_cache.c" not in rels
74
+ assert "tools/InfoProcess.c" not in rels
75
+ assert "src/extra.c" in rels
76
+ assert "lib/ht32_iap.lib" in rels
77
+ assert "inc/ht32_iap.h" in rels
78
+ assert "tools/InfoProcess.exe" in rels
79
+ assert "script/submodule.bat" in rels
80
+ reasons = {r["path"]: r["reason"] for r in snap.redacted}
81
+ assert reasons["src/ht32_iap.c"] == "redact:source-replaced-by-lib"
82
+ assert reasons["project/test/main.c"] == "redact:project-dir"
83
+ assert reasons["tools/InfoProcess.c"] == "redact:tool-source"
84
+ finally:
85
+ snap.close()
86
+
87
+
88
+ def test_snapshot_redact_none_keeps_everything(redact_repo, tmp_path):
89
+ src = _source(redact_repo["path"], tmp_path, "none")
90
+ snap = src.snapshot("v1.0.0")
91
+ try:
92
+ rels = {f.rel for f in snap.files}
93
+ assert "project/test/main.c" in rels
94
+ assert "src/ht32_iap.c" in rels
95
+ assert snap.redact_mode == "none"
96
+ assert snap.redacted == []
97
+ finally:
98
+ snap.close()
99
+
100
+
101
+ def test_export_lock_records_redaction(redact_repo, tmp_path):
102
+ catalog = _catalog("auto", str(redact_repo["path"]))
103
+ svc = ComponentService(lambda: catalog, work_root=tmp_path / "work", timeout=120)
104
+ prepared = svc.prepare(["ht32_bsp@v1.0.0"])
105
+ try:
106
+ lock = prepared.lock_document()
107
+ row = lock["components"][0]
108
+ assert row["redaction"]["mode"] == "lib-only"
109
+ removed = {r["path"] for r in row["redaction"]["removed"]}
110
+ assert removed == {"project/test/main.c", "src/ht32_iap.c",
111
+ "src/ht32_iap_cache.c", "tools/InfoProcess.c"}
112
+ exported = {f["path"] for f in row["files"]}
113
+ assert "src/ht32_iap.c" not in exported
114
+ assert "lib/ht32_iap.lib" in exported
115
+ finally:
116
+ prepared.close()
117
+
118
+
119
+ def test_registry_rejects_unknown_redact_mode():
120
+ doc = catalog_document("https://git.example.invalid/org/repo.git")
121
+ doc["components"][0]["export"]["redact"] = "all"
122
+ with pytest.raises(ComponentError) as err:
123
+ parse_catalog(doc)
124
+ assert err.value.code == "COMPONENTS_CONFIG_INVALID"
125
+ assert "redact" in err.value.message
@@ -440,7 +440,7 @@ wheels = [
440
440
 
441
441
  [[package]]
442
442
  name = "ht32-code-mcp"
443
- version = "0.4.5"
443
+ version = "0.5.0"
444
444
  source = { editable = "." }
445
445
  dependencies = [
446
446
  { name = "fastmcp" },
@@ -1,494 +0,0 @@
1
- # MCP Server 部署指南(ht32-code)
2
-
3
- HT32 Code MCP Server 的部署、配置与排错。代码在 `.work/mcp-server/`,已发布到 PyPI(`ht32-code-mcp`)。
4
-
5
- ## 部署信息
6
-
7
- ```
8
- MCP Server 地址(Streamable HTTP): https://mcp.s621.work/ht32-code
9
- PyPI 包: https://pypi.org/project/ht32-code-mcp/(uvx ht32-code-mcp 即可运行)
10
- Bundle 下载地址: https://mcp.s621.work/ht32-bundles/<token>.zip
11
- 本地 Gitea: http://100.66.1.6:3000/S621/ht32-code.git
12
- MCP Server 代码: .work/mcp-server/
13
- ```
14
-
15
- ## 部署链路
16
-
17
- ```
18
- PyPI 发布包 → 1Panel 原生 MCP 管理(类型 uvx,启动命令 uvx ht32-code-mcp,输出类型 streamableHttp)
19
- → supergateway 包装 stdio 为 Streamable HTTP(stateful)
20
- → OpenResty 暴露公网
21
- ├─ /ht32-code/* → MCP 协议端点(反向代理到容器)
22
- └─ /ht32-bundles/* → bundle zip 静态目录(P1-3)
23
- ```
24
-
25
- 代码默认走 `stdio`(`MCP_TRANSPORT` 未设置时),supergateway 负责 stdio→Streamable HTTP 转换。
26
- 与 ht32-wiki 同构:SSE 无心跳会被 OpenResty 空闲切断,故走 streamableHttp stateful。
27
-
28
- ## 传输模式(0.2.2+ 支持原生 HTTP)
29
-
30
- `server.py` 按 `MCP_TRANSPORT` 分派三种传输,**与 ht32-wiki 同构**:
31
-
32
- | `MCP_TRANSPORT` | 行为 | 典型用途 |
33
- |---|---|---|
34
- | `stdio`(默认) | 标准输入输出 | 本地开发;或供 supergateway 包装为 Streamable HTTP |
35
- | `streamable-http` | 单端点 HTTP,**默认有状态会话**(FastMCP `stateless_http=False`) | 直接对外暴露,无需 supergateway 这一层 |
36
- | `sse` | 旧式 SSE(无心跳,易被反代空闲切断) | 兼容旧客户端,不推荐 |
37
-
38
- HTTP 模式监听地址由 `MCP_HOST`(默认 `0.0.0.0`)/ `MCP_PORT`(默认 `8101`,
39
- **避开 wiki 的 8100**)控制;端点路径为 FastMCP 默认的 `/mcp`。
40
-
41
- ```bash
42
- # 原生 Streamable HTTP(stateful),供反代直连
43
- MCP_TRANSPORT=streamable-http MCP_HOST=0.0.0.0 MCP_PORT=8101 uvx ht32-code-mcp
44
- ```
45
-
46
- > **stateful 是默认值而非开关**:FastMCP 4.0.5 的 `stateless_http` 默认 `False`,
47
- > 即同一 `mcp-session-id` 复用同一会话,子进程/进程内状态不每次 POST 重建——
48
- > 与 supergateway 的 `--stateful --sessionTimeout 1800000` 语义一致。
49
- > 若将来需要 stateless,须显式改 FastMCP 设置,届时 Wiki/Code 两端要一起对照。
50
-
51
- ### 生产接线:一律用 supergateway 包装(2026-09-23 定)
52
-
53
- 两个 MCP(wiki 与 code)**统一由 supergateway 包装**,不接受「服务自身原生 HTTP」形态:
54
-
55
- - 容器里 `uvx ht32-code-mcp` 保持 stdio,由网关转 Streamable HTTP + stateful
56
- - 好处:**网关这一层统一管协议版本与会话**(`--protocolVersion 2025-06-18`
57
- `--stateful --sessionTimeout 1800000`),云端反代只认固定的容器端口/路径,
58
- 换服务或改内部实现时反代无需跟着动
59
-
60
- 原生 `streamable-http` / `sse` 保留为**能力**(本地直跑、调试、将来去网关时可用),
61
- 但**当前生产不启用**;本地 compose 部署方案见本仓库 `.work/docker/docker-compose.yml`(按此约定编写)。
62
-
63
- 无论走哪种,**监听端口必须绑 `0.0.0.0`**(默认值),否则网关/反代从容器外连不上。
64
- 验证:`ss -ltnp | grep -E '1808[0-2]'` 应显示 `0.0.0.0`,而非 `127.0.0.1`。
65
-
66
- ## 关键配置
67
-
68
- 1Panel MCP 表单:
69
-
70
- - 类型:`uvx`
71
- - 启动命令:`uvx ht32-code-mcp`(**必须带 uvx 前缀**,容器里没装裸命令)
72
- - 输出类型:`streamableHttp`
73
- - 流式传输路径:`/ht32-code`(切输出类型后是**新字段**,默认空,必须手填;必填校验)
74
- - 协议版本:`2025-06-18`(1Panel 预填默认值,勿动;仅用于 supergateway 对下游 stdio 的 auto-init)
75
- - 镜像:`supercorp/supergateway:next-uvx`(**必须覆盖默认的 `:uvx`**——见下方踩坑)
76
- - 参数:`--stateful --sessionTimeout 1800000`(30 分钟;2026-09-23 由 3600000 下调)
77
- - 环境变量:`CODE_ROOT=/app/code`
78
- - 挂载:`/opt/ht32-code:/app/code`(**不用加 `:ro`**——`index/code_index.json`
79
- 已 gitignore 且容器内可能需重建,只读会阻断;容器路径不能有前导空格,否则 `invalid mount path`)
80
-
81
- ### 镜像 tag 踩坑(2026-09-20)
82
-
83
- 默认镜像 `supercorp/supergateway:uvx` 构建时解析到旧版
84
- `@modelcontextprotocol/sdk`(LATEST_PROTOCOL_VERSION=2025-06-18),新版客户端
85
- (Trae 发 `2025-11-25`)initialize 直接被拒:`Bad Request: Unsupported protocol
86
- version`。根因是 supergateway 3.4.3 对 SDK 声明的是浮动依赖 `^1.18.2`,镜像构建
87
- 日期决定行为;`next-uvx`(4.0.0-rc.4)将 SDK **精确锁定 1.30.0**,无此问题。
88
-
89
- ## 云端 zip 下载通道(P1-3,0.1.2+)
90
-
91
- `export_project_bundle` 有两种交付形态,**由服务端环境变量决定,调用方无需判断**:
92
-
93
- | 模式 | 触发条件 | 行为 |
94
- |---|---|---|
95
- | 云端 zip | `BUNDLE_DIR` 与 `BUNDLE_BASE_URL` **同时非空** | 打包到 `BUNDLE_DIR`,返回 `<BUNDLE_BASE_URL>/<token>.zip` + SHA256 + 过期时间 |
96
- | 本地目录 | 二者任一为空(默认) | 物化到 `out_dir`(须为空目录/不存在) |
97
-
98
- ### 环境变量
99
-
100
- | 变量 | 默认 | 说明 |
101
- |------|------|------|
102
- | `BUNDLE_DIR` | 空(= 关闭云端模式) | 容器内 bundle 输出目录,须可写 |
103
- | `BUNDLE_BASE_URL` | 空(= 关闭云端模式) | 公网下载前缀,如 `https://mcp.s621.work/ht32-bundles` |
104
- | `BUNDLE_TTL_SECONDS` | `3600` | zip 有效期;每次 export 前顺带清理过期 zip |
105
-
106
- 非数字的 `BUNDLE_TTL_SECONDS` 会静默回退默认值,不报错。
107
-
108
- ### 实现要点
109
-
110
- - **token**:`secrets.token_urlsafe(24)`(192 bit 熵),文件名即 token;不可枚举、不可猜测
111
- - **物化在系统临时目录**(`tempfile.mkdtemp`)而非 bundle 目录——bundle 目录里只出现
112
- 最终 zip,中途失败的半成品不会暴露成可下载文件
113
- - **清理**:`sweep_expired()` 在每次 export 时按 mtime 删除超期 `*.zip`;
114
- 进程崩溃遗留的临时目录不影响交付(不在 bundle 目录内)
115
- - **TTL 到点不主动删**:靠下次 export 的 sweep 或 P1-2 定时脚本清理;过期后
116
- 重新调用工具即可拿新 URL
117
-
118
- ### 服务器侧落地(3 步)
119
-
120
- 本部署的 OpenResty 是**容器化**的(容器名 `1Panel-openresty-ih7J`),
121
- 已挂载 `WEBSITE_DIR=/opt/1panel/www` → 容器内 `/www`。
122
- 因此 bundle 目录直接落在网站根目录树下,**OpenResty 的 volumes 一行都不用改**——
123
- 两个容器通过宿主机上的同一目录交换文件:
124
-
125
- ```
126
- 宿主机 /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles/ (站点 root 目录下 + /ht32-bundles)
127
- ├─ code-mcp 容器看到 /app/bundles
128
- └─ OpenResty 容器看到 /www/sites/mcp.s621.work/index/ht32-bundles
129
- 公网 URL https://mcp.s621.work/ht32-bundles/<token>.zip
130
- ```
131
-
132
- 站点「网站目录」显示 root = `/opt/1panel/www/sites/mcp.s621.work/index`,
133
- bundle 放其下子目录即被当作静态文件直接访问。
134
-
135
- **第 1 步:宿主机建 bundle 目录**
136
-
137
- ```bash
138
- mkdir -p /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles
139
- chmod 755 /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles
140
- ```
141
-
142
- **第 2 步:1Panel 给 code-mcp 容器补挂载与环境变量**
143
-
144
- | 配置项 | 值 |
145
- |--------|-----|
146
- | 挂载 | `/opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles`(**不要加 `:ro`,需可写**) |
147
- | 环境变量 | `BUNDLE_DIR=/app/bundles` |
148
- | 环境变量 | `BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles` |
149
- | 环境变量 | `BUNDLE_TTL_SECONDS=3600`(可选,默认即 3600) |
150
-
151
- 完整配置(含现有项):
152
-
153
- ```
154
- 类型: uvx
155
- 启动命令: uvx ht32-code-mcp
156
- 输出类型: streamableHttp
157
- 流式传输路径:/ht32-code
158
- 协议版本: 2025-06-18
159
- 镜像: supercorp/supergateway:next-uvx
160
- 参数: --stateful --sessionTimeout 1800000
161
- 环境变量: CODE_ROOT=/app/code
162
- BUNDLE_DIR=/app/bundles
163
- BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles
164
- 挂载: /opt/ht32-code:/app/code
165
- /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles ← 宿主机路径,非容器内路径
166
- ```
167
-
168
- **第 3 步:OpenResty 无需任何配置**
169
-
170
- 站点 root = `/opt/1panel/www/sites/mcp.s621.work/index`,映射到容器内
171
- `/www/sites/mcp.s621.work/index`。bundle 目录落在这个 root 之下子目录,
172
- 静态文件天然可访问,**不用建反向代理、不用手写 location、不用改 compose**。
173
-
174
- > **不要为 bundles 建反向代理条目**:1Panel 反代 UI 表达不了静态 location
175
- > (后端代理地址必填,而静态目录没有后端)。若建了且后端留空,1Panel 生成的
176
- > location 会抢走 `/ht32-bundles/` 却不知从哪读文件 → 404。已有此类条目请删除。
177
-
178
- > **静态 location 无需 `proxy_*` 指令**:文件由 nginx 读盘直接返回,不用
179
- > `proxy_pass`,也不用 `proxy_read_timeout`——这与 code 的代理 location 形态不同属正常。
180
-
181
- > **权限**:1Panel 站点要求 `index` 目录下所有文件/目录属主与属组为 `1000`(见
182
- > 「网站目录」页提示,PHP 容器环境)。nginx worker 需可读该目录;zip 文件 644 /
183
- > 目录 755 正常可读。若验证时拿 403,先查属主属组与 SELinux 上下文。
184
-
185
- 重载(改了容器挂载或环境变量时需重建 code-mcp;仅改文件无需重载 nginx):
186
-
187
- ```bash
188
- docker exec 1Panel-openresty-ih7J nginx -t
189
- ```
190
-
191
- ### 验证生效
192
-
193
- 1. 调 `get_code_version`,应出现:
194
-
195
- ```
196
- 云端 zip 下载:启用(TTL 3600s,https://mcp.s621.work/ht32-bundles)
197
- ```
198
-
199
- 仍显示「未配置」→ 环境变量没注入成功(查变量名 / 容器是否重建)。
200
-
201
- 2. 调 `export_project_bundle(chip="HT32F52352")`,应返回:
202
-
203
- ```
204
- 下载 URL:https://mcp.s621.work/ht32-bundles/<token>.zip
205
- 有效期:3600 秒(至 ...)
206
- 大小:1.53 MB(455 文件)
207
- SHA256:...
208
- ```
209
-
210
- 3. 浏览器/curl 拉该 URL:应得约 1.5 MB zip;`sha256sum` 与报告一致;
211
- 解压后 455 文件、`project/MDK_ARMv5/<工程名>.uvprojx` 在,UV4 编译 0E0W。
212
-
213
- 4. 确认 `ls /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles` 只有 `*.zip`(无残留临时目录)。
214
-
215
- ## 反馈与用量日志(P1-5,0.2.0+)
216
-
217
- > **与 ht32-wiki-mcp 统一**:两服务使用同一套实现(`usage.py`,两仓库逐字节
218
- > 相同的副本)、同一套目录约定(同根 + 服务子目录)、同一套字段。
219
- > 本服务写 `<FEEDBACK_ROOT>/code/`,wiki 写 `<FEEDBACK_ROOT>/wiki/`。
220
-
221
- 两条通道,按 UTC 日期分文件(JSON Lines):
222
-
223
- | 通道 | 文件 | 触发 | 记录内容 |
224
- |------|------|------|----------|
225
- | 用量日志 | `usage_*.jsonl` | **服务端自动**(middleware 拦截所有工具调用) | 工具名、参数原文(剔除 `limit`/`max_length`/`out_dir`)、耗时、成败、错误、客户端 session |
226
- | 反馈 | `feedback_*.jsonl` | **Agent 主动**(`submit_feedback` 工具) | 语义性问题:API 错 / 缺口 / 路径错 / 模板缺陷 |
227
-
228
- 自动通道保证调用事实完整(不依赖 Agent 配合),用于追踪用户需求与生成的模板;
229
- 反馈通道补 middleware 判断不了的语义问题。每条记录都带 `service` 字段
230
- (`code`),便于两服务日志合并分析。
231
-
232
- ### `ok` 字段的口径(0.2.1+)
233
-
234
- 工具用**字符串**返回错误而不抛异常,middleware 拿不到协议级 `is_error`,
235
- 故靠返回值文本判断。口径是**工具是否完成了它的工作**:
236
-
237
- | 返回值 | `ok` | 说明 |
238
- |---|---|---|
239
- | `错误:…` / `导出失败…` | `false` | 参数校验、路径越界、IO 失败、无法产出工程 |
240
- | `未找到…` / `不在支持列表` / `不在覆盖范围` / `暂无模板` | `true` | **"无数据"≠失败** |
241
-
242
- "无数据"刻意记为成功:`用户搜 BLDC 但知识库没有` 正是指导 P2-1 补内容的
243
- 最强信号,若标成失败会被分析工具当作噪声剔除。标记表在 `usage.py` 的
244
- `SOFT_FAILURE_MARKERS`,**改动工具返回值文案时需同步维护**。
245
-
246
- ### 目录布局
247
-
248
- ```
249
- /opt/ht32-mcp-feedback/ ← 宿主机单一入口,两个容器挂同一卷
250
- ├─ wiki/ ← ht32-wiki 容器写这里
251
- └─ code/ ← ht32-code 容器写这里(本服务)
252
- ├─ usage_2026-09-21.jsonl
253
- └─ feedback_2026-09-21.jsonl
254
- ```
255
-
256
- ### 环境变量
257
-
258
- | 变量 | 默认 | 说明 |
259
- |------|------|------|
260
- | `FEEDBACK_ROOT` | 空(= **关闭**,本地开发默认不落盘) | 日志根目录,服务名子目录自动拼接;**必须填绝对路径**(如 `/app/feedback`,不要写 `app/feedback`) |
261
- | `CODE_LOG_DIR` | — | 已废弃,改用 `FEEDBACK_ROOT` |
262
-
263
- > ⚠️ **必须用绝对路径**:`FEEDBACK_ROOT=app/feedback` 会相对容器 WORKDIR
264
- > 解析。实测云端曾因漏写前导 `/`,`get_code_version` 显示为
265
- > `反馈与用量日志:启用(app/feedback/code)`——靠 WORKDIR 恰为 `/` 才侥幸
266
- > 落对位置,改镜像 WORKDIR 后会静默写到别处。验证时看输出路径是否以 `/` 开头。
267
-
268
- > **隐私**:参数按原文记录,`query` / `question` 可能含用户项目信息。
269
- > 该目录只应挂在受控服务器上,**不要提交进仓库**(定时脚本是
270
- > `git reset --hard` 镜像模式,写进仓库也会被覆盖丢失)。
271
-
272
- ### 服务器侧落地
273
-
274
- **第 1 步:宿主机建目录**
275
-
276
- ```bash
277
- mkdir -p /opt/ht32-mcp-feedback
278
- chmod 755 /opt/ht32-mcp-feedback
279
- ```
280
-
281
- **第 2 步:两个 MCP 容器都挂同一卷**
282
-
283
- | 服务 | 挂载 | 环境变量 |
284
- |------|------|----------|
285
- | ht32-wiki-mcp | `/opt/ht32-mcp-feedback:/app/feedback` | `FEEDBACK_ROOT=/app/feedback` |
286
- | ht32-code-mcp | `/opt/ht32-mcp-feedback:/app/feedback` | `FEEDBACK_ROOT=/app/feedback` |
287
-
288
- 完整配置(code 侧,含现有项):
289
-
290
- ```
291
- 类型: uvx
292
- 启动命令: uvx ht32-code-mcp
293
- 输出类型: streamableHttp
294
- 流式传输路径:/ht32-code
295
- 协议版本: 2025-06-18
296
- 镜像: supercorp/supergateway:next-uvx
297
- 参数: --stateful --sessionTimeout 1800000
298
- 环境变量: CODE_ROOT=/app/code
299
- BUNDLE_DIR=/app/bundles
300
- BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles
301
- FEEDBACK_ROOT=/app/feedback
302
- 挂载: /opt/ht32-code:/app/code
303
- /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles
304
- /opt/ht32-mcp-feedback:/app/feedback
305
- ```
306
-
307
- ### 验证
308
-
309
- 1. 调 `get_code_version`,应出现
310
- `反馈与用量日志:启用(/app/feedback/code)`。
311
- 仍显示「未配置」→ 环境变量没注入(查变量名 / 容器是否重建)。
312
-
313
- 2. 随便调几个工具,然后看日志:
314
-
315
- ```bash
316
- tail -3 /opt/ht32-mcp-feedback/code/usage_$(date -u +%Y-%m-%d).jsonl
317
- ```
318
-
319
- 3. 统计被请求的芯片型号(指导 P2-1 排优先级)——用 ht32-skills 的分析工具:
320
-
321
- ```bash
322
- # 克隆 ht32-skills 后(或用本地副本)
323
- python ht32-skills/scripts/analyze_feedback.py /opt/ht32-mcp-feedback
324
- ```
325
-
326
- 该工具(零依赖,仅标准库)合并 wiki + code 两服务日志,输出需求信号、
327
- 调用分布与失败、反馈类型、耗时 p50/p95、最近失败与反馈明细;支持
328
- `--days N` / `--service` / `--json`。详见 ht32-skills 的 README。
329
-
330
- 4. 清理策略由定时同步脚本第 7 步负责(`LOG_KEEP_DAYS=30`,与同步日志同口径,
331
- 目录变量为 `FEEDBACK_ROOT`)。
332
-
333
- ## 私有组件(0.4.5+)
334
-
335
- 让 MCP 用户发现私有 Git 组件(BSP / 中间件 / 驱动包),检索/读取源码,并把**选定 tag 的
336
- 源码快照**并入导出工程。组件对全部 MCP 用户可见、可导出;新增组件只改仓库内登记表,
337
- 不改 MCP 代码或 Skill。
338
-
339
- ```
340
- 用户 → MCP: list_components / get_component_info / search_component_code / read_component_file
341
- export_project_bundle(components=["<id>@<tag>"])
342
- MCP 服务端 → 仓库内登记表(默认 .work/components/components.json)→ remotes 映射 → 私有 Git(本机路径或私有网络)
343
- ```
344
-
345
- ### 环境变量
346
-
347
- | 变量 | 默认 | 说明 |
348
- |------|------|------|
349
- | `COMPONENTS_CONFIG` | `.work/components/components.json`(文件存在即启用) | 组件目录 JSON 路径;显式配置优先。文件存在但配置无效时**服务启动失败**(错误定位到组件 ID/字段) |
350
- | `COMPONENTS_STATE_FILE` | `<临时目录>/ht32-component-cache/tag-lock.json` | tag→commit 台账(检测 tag 被强制移动);生产建议放持久卷(compose 已指向 `/app/feedback/component-tag-lock.json`) |
351
- | `COMPONENT_GIT_TIMEOUT` | `60` | 单次 Git 操作超时(秒) |
352
-
353
- **运行时依赖 `git`**:组件工具通过服务端 `git` 读取远端。部署前先确认容器内可用
354
- (`docker exec ht32-code-mcp git --version`);不可用时组件工具返回
355
- `[COMPONENT_REMOTE_UNAVAILABLE] 服务端未安装 git`,需自建带 git 的镜像或改用带 git 的运行时。
356
- 纯检索/导出模板不依赖 git,不受影响。
357
-
358
- ### 组件登记表放在哪里
359
-
360
- **放在 ht32-code 仓库里**:`.work/components/components.json`,随仓库提交、回滚、blame。
361
-
362
- ```
363
- ht32-code(私有仓库,服务器 /vol1/1000/SSD/docker/ht32-code → 容器 /app/code)
364
- └─ .work/components/components.json ← 登记表(真实 remote 地址在这里)
365
- ```
366
-
367
- - **好处**:新增组件 = 一次 commit;服务器更新脚本 `git fetch + git reset --hard origin/master`
368
- 后自动生效;历史用 `git log -p .work/components/components.json` 查,回滚用
369
- `git checkout <commit> -- .work/components/components.json`;**不需要发 MCP 版本**。
370
- - **不进 PyPI**:wheel 只打包 `src/ht32_code_mcp`;sdist 已在
371
- `pyproject.toml` 的 `[tool.hatch.build.targets.sdist].exclude` 排除 `.work/components`。
372
- 发布前可 `uv build` 后检查 `tar -tzf dist/*.tar.gz | grep components` 应为空。
373
- - **前提:仓库必须保持私有**。若 ht32-code 将来转为公开,必须先把登记表移出仓库
374
- (改回私有目录挂载或独立私有配置仓库),否则组件清单与内网地址会随仓库泄露。
375
- - 登记表可选写 `catalog_version`(人工标签);服务端另算 `catalog_sha256`(文件内容哈希)
376
- 与所在仓库 `commit`,三者一起出现在 `get_code_version` / `list_components` /
377
- `components-lock.json`,用于反查“这次调用是哪一版登记表”。
378
- - 登记字段要点:`series` 按**族前缀**匹配(登记 `HT32F5` 即覆盖 `HT32F50241`
379
- 等同族型号,也支持 `HT32F49*` 通配);`["*"]` 表示已确认覆盖全部型号;
380
- `chips` 只用于展示“已验证型号”,不必穷举同族全部 IC;`list_components(query=...)`
381
- 也接受型号。
382
- - 加载器**支持 `//` 与 `/* */` 注释**(字符串里的 `//` 不受影响;不支持尾随逗号);
383
- 逐字段说明见带注释的 `components.example.json`,**正式登记表建议保持纯 JSON**。
384
- 注释计入文件内容哈希——改注释等同于换了一版登记表。
385
-
386
- ### 服务器侧落地
387
-
388
- **第 1 步:在仓库里提交登记表**
389
-
390
- ```powershell
391
- # 维护者本地(ht32-code 仓库)
392
- mkdir -Path .work\components
393
- # 写入 components.json(结构见 components.example.json)
394
- git add .work/components/components.json
395
- git commit -m "feat(components): 登记 HT32_BSP 与 LwFSM"
396
- git push
397
- ```
398
-
399
- **第 2 步:服务器同步 + 重建**
400
-
401
- ```bash
402
- # 手动:仓库已挂载到 /app/code,git reset 后重启容器即可
403
- cd /vol1/1000/SSD/docker/ht32-code && git fetch && git reset --hard origin/master
404
- docker compose -f .work/docker/docker-compose.yml up -d --force-recreate
405
- # 或等 1Panel 计划任务里的 ht32-code-mcp-update.md(每天 02:30)自动执行
406
- ```
407
-
408
- 不需要新建 `/app/components`、不需要额外挂载;`COMPONENTS_CONFIG` 已在 compose 指向
409
- `/app/code/.work/components/components.json`(即使不配,服务也会在默认位置发现它)。
410
-
411
- **第 3 步:验证**
412
-
413
- ```bash
414
- docker logs ht32-code-mcp | grep "私有组件目录"
415
- # [ht32-code-mcp] 私有组件目录: 2 个组件(catalog 3f9a1c2b / commit 7f43dfc4 / tag 台账 ...)
416
- ```
417
-
418
- 配置无效时进程以退出码 2 结束,日志给出 `[COMPONENTS_CONFIG_INVALID] <字段>`。
419
-
420
- **第 4 步:工具验证**
421
-
422
- 1. `get_code_version` 出现 `私有组件目录:已登记 N 个组件(catalog … / commit …)`。
423
- 2. `list_components()` 列出登记组件与可用 tag(远端不可用时显示
424
- `远端不可用([COMPONENT_REMOTE_UNAVAILABLE] …)`,不回退 branch/HEAD)。
425
- 3. `export_project_bundle(config_id=..., components=["<id>@<tag>"])` 的产物含
426
- `components/<id>/` 与 `manifest/components-lock.json`(tag/commit/逐文件 SHA256,
427
- 以及 `catalog.sha256/commit/version` 登记表来源)。
428
-
429
- ### 日常运维
430
-
431
- | 操作 | 做法 |
432
- |---|---|
433
- | 新增组件 | 编辑 `.work/components/components.json` → commit/push → 同步脚本(或手动 reset + 重建);**不改 Skills、不发 MCP 版本** |
434
- | 刷新可用 tag | 组件维护者打 tag 即可;登记规则匹配的新 tag 自动出现(`git ls-remote` 每次查询) |
435
- | 下架/暂停组件 | 该条 `"status": "disabled"` → commit → 同步;列表显示为已停用,读取/导出明确拒绝 |
436
- | 回滚登记表 | `git checkout <commit> -- .work/components/components.json` → commit/push → 同步;或 `git revert <commit>` |
437
- | tag 漂移 | 同 tag 指向新 commit 时返回 `[COMPONENT_TAG_DRIFT]`(阻断);维护者核实后删除台账里该行再重试 |
438
- | 查历史 | `git log -p -- .work/components/components.json`;工程侧看 `components-lock.json` 的 `catalog.commit` |
439
- | **不要**在服务器上直接改 | 更新脚本是 `git reset --hard`,手改会被覆盖;所有变更走仓库提交 |
440
- | 临时目录 | 每次读取/导出的浅克隆在系统临时目录,成功后立即清理 |
441
- | Git 不可达 | 短超时 + `[COMPONENT_REMOTE_UNAVAILABLE]`(错误信息已脱敏,不含 remote URL) |
442
-
443
- ### 隐私/安全检查(发布或改配置后)
444
-
445
- ```bash
446
- # 1) 公开产物(PyPI 包)不得含登记表
447
- uv build && tar -tzf dist/*.tar.gz | grep -i "\.work/components" && echo "泄漏!" || echo "OK"
448
- # wheel 只含 src/,可用 unzip -l dist/*.whl | grep components 复核
449
-
450
- # 2) 登记表本身不得含凭据(URL 可以,token/密码不行)
451
- git grep -nE "PRIVATE-TOKEN|password=|ssh://.*@" -- .work/components
452
- ```
453
-
454
- ## PyPI 包发布流程
455
-
456
- ```bash
457
- # token 在 ht32-code 根目录 .env 的 UV_PUBLISH_TOKEN
458
- uv lock && uv build && uv publish
459
- ```
460
-
461
- 发布后同步 `VERSION` 文件末行:`MCP Server: <版本>(deployed, PyPI, <日期>)`。
462
-
463
- ## 踩过的坑
464
-
465
- ### 1. 云端跑的是 PyPI 包,不是 /opt 仓库(P0-1)
466
-
467
- - **症状**:改了仓库代码、云端 MCP 行为不变
468
- - **根因**:`uvx ht32-code-mcp` 从 PyPI 拉包,`/opt/ht32-code` 只是被检索的数据源
469
- - **修复**:改代码必须**发版 + 重建容器**,改仓库文件本身不生效
470
-
471
- ### 2. 索引只读挂载
472
-
473
- - **症状**:容器启动报索引无法写入/重建失败
474
- - **根因**:`/opt/ht32-code:/app/code:ro` 时 `index/code_index.json` 不可写
475
- - **修复**:挂载去掉 `:ro`
476
-
477
- ### 3. 镜像 tag 导致协议版本被拒
478
-
479
- 见上方「镜像 tag 踩坑」。
480
-
481
- ## 当前部署状态
482
-
483
- | # | 任务 | 说明 | 状态 |
484
- |---|------|------|------|
485
- | 3.1 | MCP 工具实现(7 检索/导出 + 4 私有组件 + 版本检查 + 1 反馈) | `.work/mcp-server/src/ht32_code_mcp/`(FastMCP) | ✅ |
486
- | 3.2 | 工具级测试 | `pytest` 46 用例全绿 | ✅ |
487
- | 3.3 | PyPI 发布(hatchling) | `ht32-code-mcp` 0.2.3(2026-09-24,含全系列固件库覆盖 + 6 模板多模板导出) | ✅ |
488
- | 3.4 | 云端 git clone + 挂载 | `/opt/ht32-code:/app/code` | ✅ |
489
- | 3.5 | 1Panel 部署(uvx + supergateway → streamableHttp stateful,镜像 next-uvx) | `https://mcp.s621.work/ht32-code` | ✅ |
490
- | 3.6 | OpenResty 超时修复(`proxy_read_timeout 3600s`) | 修复 60s 空闲断连;2026-09-21 复核两端空闲 100s 后调用仍 200,未被 1Panel 回退 | ✅ |
491
- | 3.7 | 云端 zip 下载通道(P1-3) | 服务器侧已上线:下载 1608157 字节 / SHA256 一致 / 解压 455 文件 / UV4 0E0W | ✅ |
492
- | 3.8 | 定时同步脚本含 bundle 清理(P1-2) | `ht32-code-mcp-update.md` 第 6 步 `find … -mmin +60 -delete` | ✅ |
493
- | 3.9 | 反馈与用量日志(P1-5,与 wiki 统一) | 0.2.1;云端已启用,但 `FEEDBACK_ROOT` 漏写前导 `/`(显示 `app/feedback/code`),需改绝对路径 | ⚠️ |
494
- | 3.10 | 私有组件目录(0.4.5) | 登记表随仓库版本化(`.work/components/components.json`,无额外挂载);容器内需有 `git` | ⏳ 待用户部署 |
File without changes
File without changes
File without changes