ht32-code-mcp 0.2.6__tar.gz → 0.3.1__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.
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/PKG-INFO +1 -1
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/e2e_scenario_a.py +2 -2
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/pyproject.toml +1 -1
- ht32_code_mcp-0.3.1/src/ht32_code_mcp/exporter.py +181 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/src/ht32_code_mcp/index.py +5 -4
- ht32_code_mcp-0.3.1/src/ht32_code_mcp/materializer.py +927 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/src/ht32_code_mcp/server.py +82 -47
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/tests/test_knowledge_grounding.py +2 -2
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/tests/test_tools.py +219 -314
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/uv.lock +1 -1
- ht32_code_mcp-0.2.6/src/ht32_code_mcp/exporter.py +0 -472
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/.gitignore +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/.gitkeep +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/.pytest_base_final_run.log +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/DEPLOY.md +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/README.md +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/e2e_scenario_b.py +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/smoke_http.py +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/smoke_stdio.py +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/src/ht32_code_mcp/__init__.py +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/src/ht32_code_mcp/__main__.py +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/src/ht32_code_mcp/knowledge.py +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/src/ht32_code_mcp/release.py +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/src/ht32_code_mcp/usage.py +0 -0
- {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.1}/tests/conftest.py +0 -0
|
@@ -69,8 +69,8 @@ def wiki_transport() -> StdioTransport:
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def devices_entry(chip: str) -> dict:
|
|
72
|
-
"""该型号在
|
|
73
|
-
for dj in sorted((CODE_ROOT / "
|
|
72
|
+
"""该型号在 templates/*/devices.json 中的参数条目(用于核对导出产物)。"""
|
|
73
|
+
for dj in sorted((CODE_ROOT / "templates").glob("*/devices.json")):
|
|
74
74
|
for d in json.loads(dj.read_text(encoding="utf-8")).get("devices", []):
|
|
75
75
|
if d.get("device", "").upper() == chip.upper():
|
|
76
76
|
return d
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""export_project_bundle:v2 契约导出(recipe 物化 + 原子发布 + manifest)。
|
|
2
|
+
|
|
3
|
+
v2 变更(相对旧"复制模板 + 改少量字段"的实现):
|
|
4
|
+
- 入口支持显式 config_id / device+board / clock_profile / retarget_profile;
|
|
5
|
+
旧 chip 参数走只读适配层(v1-adapter.json),保持旧默认并给弃用警告;
|
|
6
|
+
- 物化由 materializer.py 完成:先校验后 staging、XML 声明式改写(命中数约束)、
|
|
7
|
+
内存一处裁决多处一致、manifest 四件套、staging 原子重命名、ZIP .part 原子发布;
|
|
8
|
+
- 响应字段:contract_version/config_id/recipe_revision/device/board/compiler/
|
|
9
|
+
profiles/project_relative_path/manifest_relative_path/artifact_sha256/
|
|
10
|
+
artifact_size/requires/warnings/validation。
|
|
11
|
+
|
|
12
|
+
模板(templates/)是派生快照(replay_templates.py 可重建);唯一事实源是
|
|
13
|
+
.work/template-recipes/(schema + recipes + mapping + inventory)。
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import dataclasses
|
|
18
|
+
import shutil
|
|
19
|
+
import tempfile
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from .materializer import (CONTRACT_VERSION, ContractError, Materializer, Resolved,
|
|
24
|
+
zip_tree_atomic)
|
|
25
|
+
|
|
26
|
+
DEFAULT_BUNDLE_TTL = 3600
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass
|
|
30
|
+
class ExportResult:
|
|
31
|
+
"""一次导出的结果(本地目录/云端 zip 共用)。"""
|
|
32
|
+
|
|
33
|
+
text: str
|
|
34
|
+
config_id: str
|
|
35
|
+
device: str
|
|
36
|
+
board: str
|
|
37
|
+
compiler: str
|
|
38
|
+
profiles: dict
|
|
39
|
+
project_relative_path: str
|
|
40
|
+
manifest_relative_path: str
|
|
41
|
+
artifact_sha256: str
|
|
42
|
+
artifact_size: int
|
|
43
|
+
requires: dict
|
|
44
|
+
warnings: list
|
|
45
|
+
validation: dict
|
|
46
|
+
out_dir: Path | None = None
|
|
47
|
+
zip_path: Path | None = None
|
|
48
|
+
download_url: str = ""
|
|
49
|
+
expires_at: float = 0.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProjectExporter:
|
|
53
|
+
"""v2 导出器:recipe 选择 -> 物化 -> 原子发布。"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, code_root: Path) -> None:
|
|
56
|
+
self.root = Path(code_root)
|
|
57
|
+
recipes_dir = self.root / ".work" / "template-recipes" / "recipes"
|
|
58
|
+
self.materializer = Materializer(self.root, recipes_dir)
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------ 解析
|
|
61
|
+
def resolve(self, *, config_id: str = "", chip: str = "", device: str = "",
|
|
62
|
+
board: str = "", clock_profile: str = "",
|
|
63
|
+
retarget_profile: str = "") -> Resolved:
|
|
64
|
+
return self.materializer.resolve(
|
|
65
|
+
config_id=config_id, chip=chip, device=device,
|
|
66
|
+
board=board, clock_profile=clock_profile, retarget_profile=retarget_profile)
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------ 本地目录
|
|
69
|
+
def export_dir(self, *, out_dir: str, project_name: str = "", config_id: str = "",
|
|
70
|
+
chip: str = "", device: str = "", board: str = "",
|
|
71
|
+
clock_profile: str = "", retarget_profile: str = "",
|
|
72
|
+
include_host_tools: bool = False) -> ExportResult:
|
|
73
|
+
r = self.resolve(config_id=config_id, chip=chip, device=device, board=board,
|
|
74
|
+
clock_profile=clock_profile, retarget_profile=retarget_profile)
|
|
75
|
+
res = self.materializer.materialize(
|
|
76
|
+
r, out_dir, project_name=project_name, include_host_tools=include_host_tools)
|
|
77
|
+
return self._result(r, res, artifact_kind="directory")
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------ 云端 zip
|
|
80
|
+
def export_zip(self, *, bundle_dir: str, ttl_seconds: int = DEFAULT_BUNDLE_TTL,
|
|
81
|
+
project_name: str = "", config_id: str = "", chip: str = "",
|
|
82
|
+
device: str = "", board: str = "", clock_profile: str = "",
|
|
83
|
+
retarget_profile: str = "",
|
|
84
|
+
include_host_tools: bool = False) -> ExportResult:
|
|
85
|
+
r = self.resolve(config_id=config_id, chip=chip, device=device, board=board,
|
|
86
|
+
clock_profile=clock_profile, retarget_profile=retarget_profile)
|
|
87
|
+
bdir = Path(bundle_dir).expanduser().resolve()
|
|
88
|
+
bdir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
tmp = Path(tempfile.mkdtemp(prefix="ht32-bundle-", dir=str(bdir)))
|
|
90
|
+
try:
|
|
91
|
+
res = self.materializer.materialize(
|
|
92
|
+
r, tmp / "bundle", project_name=project_name,
|
|
93
|
+
include_host_tools=include_host_tools)
|
|
94
|
+
import secrets
|
|
95
|
+
token = secrets.token_urlsafe(24)
|
|
96
|
+
zip_path = bdir / f"{token}.zip"
|
|
97
|
+
size, digest = zip_tree_atomic(tmp / "bundle", zip_path)
|
|
98
|
+
result = self._result(r, res, artifact_kind="zip")
|
|
99
|
+
result.zip_path = zip_path
|
|
100
|
+
result.artifact_sha256 = digest
|
|
101
|
+
result.artifact_size = size
|
|
102
|
+
result.expires_at = time.time() + ttl_seconds
|
|
103
|
+
return result
|
|
104
|
+
finally:
|
|
105
|
+
if tmp.exists():
|
|
106
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------ 结果文本
|
|
109
|
+
def _result(self, r: Resolved, res, *, artifact_kind: str) -> ExportResult:
|
|
110
|
+
cfg = r.config
|
|
111
|
+
lock = res.out_dir / "manifest" / "project-lock.json"
|
|
112
|
+
manifest_rel = "manifest/project-lock.json"
|
|
113
|
+
requires = {
|
|
114
|
+
"pack": r.recipe["pack"]["id"],
|
|
115
|
+
"pack_note": r.recipe["pack"]["device_macro_note"],
|
|
116
|
+
"keil": "MDK-ARM 5.x (AC5)",
|
|
117
|
+
}
|
|
118
|
+
validation = dict(cfg["validation"])
|
|
119
|
+
validation["static"] = "pass"
|
|
120
|
+
result = ExportResult(
|
|
121
|
+
text="", config_id=r.config_id, device=cfg["device"],
|
|
122
|
+
board=cfg["board"]["id"], compiler=cfg["compiler"]["id"],
|
|
123
|
+
profiles={"clock": r.clock_profile, "retarget": r.retarget_profile},
|
|
124
|
+
project_relative_path=res.project_file,
|
|
125
|
+
manifest_relative_path=manifest_rel,
|
|
126
|
+
artifact_sha256=res.sha256, artifact_size=res.bytes,
|
|
127
|
+
requires=requires, warnings=list(r.warnings), validation=validation,
|
|
128
|
+
out_dir=res.out_dir)
|
|
129
|
+
result.text = self._report(r, res, artifact_kind)
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
def _report(self, r: Resolved, res, artifact_kind: str) -> str:
|
|
133
|
+
cfg = r.config
|
|
134
|
+
mem = cfg["memory"]
|
|
135
|
+
lines = [
|
|
136
|
+
f"自包含 Keil 工程已导出(v2 契约,{artifact_kind}):",
|
|
137
|
+
f"- contract_version: {CONTRACT_VERSION}",
|
|
138
|
+
f"- config_id: {r.config_id}"
|
|
139
|
+
+ ("(非默认 profile 组合的派生 id)" if r.derived_config_id else ""),
|
|
140
|
+
f"- recipe_revision: {r.recipe['recipe_revision']}"
|
|
141
|
+
f"(materializer {r.recipe['materializer']['id']}"
|
|
142
|
+
f" r{r.recipe['materializer']['revision']})",
|
|
143
|
+
f"- device: {cfg['device']}|board: {cfg['board']['id']}"
|
|
144
|
+
f"|compiler: {cfg['compiler']['id']}",
|
|
145
|
+
f"- profiles: clock={r.clock_profile}, retarget={r.retarget_profile}",
|
|
146
|
+
f"- 工程文件: {res.project_file}",
|
|
147
|
+
f"- manifest: manifest/project-lock.json(另有 files/deviations/validation)",
|
|
148
|
+
f"- 内存(一处裁决): Flash {mem['flash'][0]['size']}"
|
|
149
|
+
f" @ {mem['flash'][0]['base']};RAM {mem['ram'][0]['size']}"
|
|
150
|
+
f" @ {mem['ram'][0]['base']}"
|
|
151
|
+
+ (f";冲突记录 {len(mem['conflicts'])} 条" if mem["conflicts"] else ""),
|
|
152
|
+
f"- 文件数/字节: {res.files} / {res.bytes}",
|
|
153
|
+
f"- 树 SHA256: {res.sha256}",
|
|
154
|
+
f"- 前置: {r.recipe['pack']['device_macro_note']}",
|
|
155
|
+
]
|
|
156
|
+
if r.retarget_profile == "retarget":
|
|
157
|
+
claims = cfg["retarget"]["uart_pin_claims"]
|
|
158
|
+
lines.append(f"- 串口占用: {claims[0] if claims else 'COM1'}")
|
|
159
|
+
if r.warnings:
|
|
160
|
+
lines.append("- 警告:")
|
|
161
|
+
lines.extend(f" - {w}" for w in r.warnings)
|
|
162
|
+
v = cfg["validation"]
|
|
163
|
+
lines.append(f"- validation: static=pass, compile={v['compile']}, "
|
|
164
|
+
f"gui={v['gui']}, hardware={v['hardware']}"
|
|
165
|
+
"(gui/hardware 未验证项以 not-tested 明示)")
|
|
166
|
+
lines.append("")
|
|
167
|
+
lines.append("后续:UV4 -b 基线编译(目标 0 Error 0 Warning);"
|
|
168
|
+
"换芯片请用显式 config_id 重新导出,不要手改 Device/Cpu/宏。")
|
|
169
|
+
return "\n".join(lines)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# 旧兼容入口(v1 chip 参数只读适配;调用方逐步迁移到 config_id)
|
|
173
|
+
def export_project_bundle_legacy(exporter: ProjectExporter, chip: str, out_dir: str,
|
|
174
|
+
project_name: str = "") -> str:
|
|
175
|
+
try:
|
|
176
|
+
result = exporter.export_dir(out_dir=out_dir, project_name=project_name, chip=chip)
|
|
177
|
+
except ContractError as e:
|
|
178
|
+
return f"导出失败:[{e.code}] {e.message}"
|
|
179
|
+
except OSError as e:
|
|
180
|
+
return f"导出失败(写盘/打包失败,检查目录权限与空间):{e}"
|
|
181
|
+
return result.text
|
|
@@ -166,14 +166,15 @@ class CodeIndex:
|
|
|
166
166
|
raw_dir = self.root / "raw"
|
|
167
167
|
if raw_dir.is_dir():
|
|
168
168
|
data["fwlibs"] = [self._scan_fwlib(p) for p in sorted(raw_dir.iterdir())
|
|
169
|
-
if p.is_dir()
|
|
170
|
-
templates_dir = raw_dir / "templates"
|
|
171
|
-
data["templates"] = [self._scan_template(p) for p in sorted(templates_dir.iterdir())
|
|
172
|
-
if p.is_dir()] if templates_dir.is_dir() else []
|
|
169
|
+
if p.is_dir()]
|
|
173
170
|
data["files"] = sorted(
|
|
174
171
|
_rel(f.relative_to(self.root).as_posix())
|
|
175
172
|
for f in raw_dir.rglob("*") if f.is_file()
|
|
176
173
|
)
|
|
174
|
+
templates_dir = self.root / "templates"
|
|
175
|
+
if templates_dir.is_dir():
|
|
176
|
+
data["templates"] = [self._scan_template(p) for p in sorted(templates_dir.iterdir())
|
|
177
|
+
if p.is_dir()]
|
|
177
178
|
data["knowledge_pages"] = self._scan_knowledge()
|
|
178
179
|
fw = data["fwlibs"]
|
|
179
180
|
data["stats"] = {
|