ht32-code-mcp 0.1.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.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ index/
6
+ dist/
7
+ build/
8
+ uv.lock.tmp
File without changes
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.5
2
+ Name: ht32-code-mcp
3
+ Version: 0.1.0
4
+ Summary: HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出
5
+ License-Expression: MIT
6
+ Keywords: firmware,holtek,ht32,mcp,mcu
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Topic :: Software Development :: Embedded Systems
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: fastmcp>=0.5.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.0; extra == 'dev'
@@ -0,0 +1,5 @@
1
+ # .work/mcp-server/
2
+
3
+ MCP server 代码目录(Python/FastMCP),P0-2 在此实现 7 工具(本地 stdio)。
4
+
5
+ 当前为占位:仓库初始化(P0-1)时建立骨架,保证目录结构随 git 入库。
@@ -0,0 +1,142 @@
1
+ """P0-4 场景 a 端到端驱动:双 MCP 本地 stdio 真实子进程跑通新建工程链路。
2
+
3
+ 流程(对应 ht32-skills/ht32-project-create):
4
+ wiki-mcp: get_model + get_chip_info(HT32F52352 规格核对)
5
+ code-mcp: tools/list(7 工具)→ get_project_manifest → export_project_bundle → 落盘核对
6
+
7
+ 用法:
8
+ <code-venv-python> e2e_scenario_a.py [--chip HT32F52352]
9
+ 输出:
10
+ %TEMP%/ht32_p0_4_e2e/wiki_chip_info.txt | manifest.txt | export.txt
11
+ %TEMP%/ht32_p0_4_e2e/stage/<chip>_demo/ 导出产物(编译对象)
12
+ 退出码 0 全通过;1 失败。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import asyncio
18
+ import os
19
+ import shutil
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ from fastmcp import Client
24
+ from fastmcp.client.transports import StdioTransport
25
+
26
+ CODE_MCP_DIR = Path(__file__).resolve().parent
27
+ WIKI_MCP_DIR = Path(r"d:\HoltekWork\WorkDesktop\ht32_ai_mcp\ht32-wiki\.work\mcp-server")
28
+ WIKI_ROOT = WIKI_MCP_DIR.parents[1]
29
+
30
+ CODE_PY = CODE_MCP_DIR / ".venv" / "Scripts" / "python.exe"
31
+ WIKI_PY = WIKI_MCP_DIR / ".venv" / "Scripts" / "python.exe"
32
+
33
+ EXPECTED_TOOLS = {
34
+ "get_code_version", "list_libraries", "find_example", "get_file",
35
+ "search_code_knowledge", "get_project_manifest", "export_project_bundle",
36
+ }
37
+
38
+ WORK = Path(os.environ.get("TEMP", ".")) / "ht32_p0_4_e2e"
39
+
40
+ failed = 0
41
+
42
+
43
+ def check(name: str, cond: bool, detail: str = "") -> bool:
44
+ global failed
45
+ suffix = f" — {detail}" if detail else ""
46
+ print(f"{'✅' if cond else '❌'} [{name}]{suffix}")
47
+ if not cond:
48
+ failed += 1
49
+ return cond
50
+
51
+
52
+ async def call_text(client: Client, tool: str, args: dict) -> str:
53
+ r = await client.call_tool(tool, args)
54
+ parts = [c.text for c in r.content if getattr(c, "text", None)]
55
+ return "\n".join(parts)
56
+
57
+
58
+ def wiki_transport() -> StdioTransport:
59
+ env = dict(os.environ)
60
+ env.update({"WIKI_ROOT": str(WIKI_ROOT), "MCP_TRANSPORT": "stdio",
61
+ "MCP_WARMUP": "1", "DS_RAG_AUTOBUILD": "0"})
62
+ for key in ("EMBED_API_KEY", "SILICONFLOW_API_KEY", "RERANK_API_KEY"):
63
+ env.pop(key, None)
64
+ return StdioTransport(str(WIKI_PY), ["-m", "ht32_wiki_mcp"],
65
+ env=env, cwd=str(WIKI_MCP_DIR))
66
+
67
+
68
+ def code_transport() -> StdioTransport:
69
+ env = dict(os.environ)
70
+ env["CODE_ROOT"] = str(CODE_MCP_DIR.parents[1])
71
+ return StdioTransport(str(CODE_PY), ["-m", "ht32_code_mcp"],
72
+ env=env, cwd=str(CODE_MCP_DIR))
73
+
74
+
75
+ async def phase_wiki(chip: str) -> str:
76
+ print("\n========== 阶段 1 · wiki-mcp 规格核对 ==========")
77
+ async with Client(wiki_transport()) as client:
78
+ model_text = await call_text(client, "get_model", {"model": chip})
79
+ check("get_model 返回系列映射", "系列:" in model_text,
80
+ model_text.splitlines()[0] if model_text else "(空)")
81
+ chip_text = await call_text(client, "get_chip_info", {"model": chip})
82
+ check("get_chip_info 命中系列页", "未找到" not in chip_text and len(chip_text) > 200,
83
+ f"{len(chip_text)} 字符")
84
+ (WORK / "wiki_chip_info.txt").write_text(
85
+ f"== get_model({chip}) ==\n{model_text}\n\n== get_chip_info({chip}) ==\n{chip_text}",
86
+ encoding="utf-8")
87
+ print(f"规格核对全文已存 {WORK / 'wiki_chip_info.txt'}")
88
+ return chip_text
89
+
90
+
91
+ async def phase_code(chip: str) -> Path:
92
+ print("\n========== 阶段 2 · code-mcp manifest + export ==========")
93
+ stage = WORK / "stage" / f"{chip.lower()}_demo"
94
+ if stage.exists():
95
+ shutil.rmtree(stage)
96
+ stage.parent.mkdir(parents=True, exist_ok=True)
97
+
98
+ async with Client(code_transport()) as client:
99
+ tools = {t.name for t in await client.list_tools()}
100
+ check("7 工具注册齐全", EXPECTED_TOOLS <= tools,
101
+ f"{len(tools)} 个" if EXPECTED_TOOLS <= tools else f"缺 {EXPECTED_TOOLS - tools}")
102
+
103
+ manifest = await call_text(client, "get_project_manifest", {})
104
+ check("manifest 返回模板清单", "工程模板清单" in manifest,
105
+ manifest.splitlines()[0] if manifest else "(空)")
106
+ (WORK / "manifest.txt").write_text(manifest, encoding="utf-8")
107
+
108
+ export = await call_text(client, "export_project_bundle",
109
+ {"chip": chip, "out_dir": str(stage)})
110
+ check("export_project_bundle 成功", "导出失败" not in export and stage.is_dir(),
111
+ export.splitlines()[0] if export else "(空)")
112
+ (WORK / "export.txt").write_text(export, encoding="utf-8")
113
+
114
+ uvprojx = list(stage.rglob("*.uvprojx"))
115
+ check("落盘核对:.uvprojx 存在", len(uvprojx) == 1, str(uvprojx))
116
+ files = [p for p in stage.rglob("*") if p.is_file()]
117
+ check("落盘核对:工程文件数 > 20", len(files) > 20, f"{len(files)} 个文件")
118
+ print(f"导出产物:{stage}")
119
+ return stage
120
+
121
+
122
+ async def main() -> int:
123
+ parser = argparse.ArgumentParser()
124
+ parser.add_argument("--chip", default="HT32F52352")
125
+ args = parser.parse_args()
126
+ WORK.mkdir(parents=True, exist_ok=True)
127
+ if not (WIKI_PY.is_file() and CODE_PY.is_file()):
128
+ print("❌ 找不到 venv 解释器", file=sys.stderr)
129
+ return 1
130
+
131
+ await phase_wiki(args.chip)
132
+ stage = await phase_code(args.chip)
133
+
134
+ print("\n========== 摘要 ==========")
135
+ print(f"工作目录: {WORK}")
136
+ print(f"导出工程: {stage}")
137
+ print(f"结果: {'✅ 全部通过' if failed == 0 else f'❌ {failed} 项失败'}")
138
+ return 0 if failed == 0 else 1
139
+
140
+
141
+ if __name__ == "__main__":
142
+ raise SystemExit(asyncio.run(main()))
@@ -0,0 +1,76 @@
1
+ """P0-4 场景 b 冒烟:检索辅助写码三工具各一次,确认返回真实库内容。
2
+
3
+ search_code_knowledge("UART 用法") → 知识页命中
4
+ find_example(peripheral="UART", chip="HT32F52352") → 官方例程路径
5
+ get_file(例程 main.c) → 官方库真实源码
6
+
7
+ 用法:
8
+ <code-venv-python> e2e_scenario_b.py
9
+ 退出码 0 全通过;1 失败。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import os
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from fastmcp import Client
20
+ from fastmcp.client.transports import StdioTransport
21
+
22
+ CODE_MCP_DIR = Path(__file__).resolve().parent
23
+ CODE_PY = CODE_MCP_DIR / ".venv" / "Scripts" / "python.exe"
24
+
25
+ failed = 0
26
+
27
+
28
+ def check(name: str, cond: bool, detail: str = "") -> bool:
29
+ global failed
30
+ suffix = f" — {detail}" if detail else ""
31
+ print(f"{'✅' if cond else '❌'} [{name}]{suffix}")
32
+ if not cond:
33
+ failed += 1
34
+ return cond
35
+
36
+
37
+ async def call_text(client: Client, tool: str, args: dict) -> str:
38
+ r = await client.call_tool(tool, args)
39
+ return "\n".join(c.text for c in r.content if getattr(c, "text", None))
40
+
41
+
42
+ async def main() -> int:
43
+ env = dict(os.environ)
44
+ env["CODE_ROOT"] = str(CODE_MCP_DIR.parents[1])
45
+ transport = StdioTransport(str(CODE_PY), ["-m", "ht32_code_mcp"],
46
+ env=env, cwd=str(CODE_MCP_DIR))
47
+
48
+ async with Client(transport) as client:
49
+ print("== 场景 b 冒烟(检索辅助写码)==")
50
+
51
+ s = await call_text(client, "search_code_knowledge", {"query": "UART 用法 中断 发送", "limit": 3})
52
+ check("search_code_knowledge 命中知识页", "找到" in s and "entities/" in s,
53
+ s.splitlines()[0] if s else "(空)")
54
+ print("\n".join(s.splitlines()[:12]), "…\n")
55
+
56
+ # 官方例程目录名为 USART(无 UART 目录):find_example 词法匹配不含 uart→usart 别名
57
+ f = await call_text(client, "find_example",
58
+ {"peripheral": "USART", "chip": "HT32F52352", "limit": 3})
59
+ check("find_example 返回 USART 例程", "找到" in f and "example" in f,
60
+ f.splitlines()[0] if f else "(空)")
61
+ m = re.search(r"路径: (raw/\S+)", f)
62
+ check("例程含 main.c 路径", bool(m), m.group(1) if m else "(未匹配)")
63
+ print("\n".join(f.splitlines()[:10]), "…\n")
64
+
65
+ if m:
66
+ g = await call_text(client, "get_file", {"path": f"{m.group(1)}/main.c", "max_length": 4000})
67
+ has_api = ("HT_" in g) and ("UART" in g or "USART" in g)
68
+ check("get_file 返回官方库真实源码", has_api, f"{len(g)} 字符")
69
+ print("\n".join(g.splitlines()[:10]), "…")
70
+
71
+ print(f"\n结果: {'✅ 场景 b 冒烟通过' if failed == 0 else f'❌ {failed} 项失败'}")
72
+ return 0 if failed == 0 else 1
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(asyncio.run(main()))
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ht32-code-mcp"
7
+ version = "0.1.0"
8
+ description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
9
+ requires-python = ">=3.11"
10
+ license = "MIT"
11
+ keywords = ["ht32", "mcu", "mcp", "firmware", "holtek"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Software Development :: Embedded Systems",
18
+ ]
19
+ dependencies = [
20
+ "fastmcp>=0.5.0",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev = [
25
+ "pytest>=8.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ ht32-code-mcp = "ht32_code_mcp.server:main"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/ht32_code_mcp"]
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
@@ -0,0 +1,58 @@
1
+ """stdio 冒烟测试:以子进程 stdio 启动 server,验证 7 工具全部可调用。
2
+
3
+ 用法(从 .work/mcp-server/ 目录,需已安装依赖):
4
+ python smoke_stdio.py
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ SCRIPT_DIR = Path(__file__).resolve().parent
14
+ SRC = SCRIPT_DIR / "src"
15
+ sys.path.insert(0, str(SRC))
16
+
17
+ from fastmcp import Client # noqa: E402
18
+ from fastmcp.client.transports import StdioTransport # noqa: E402
19
+
20
+ EXPECTED_TOOLS = {
21
+ "get_code_version", "list_libraries", "find_example", "get_file",
22
+ "search_code_knowledge", "get_project_manifest", "export_project_bundle",
23
+ }
24
+
25
+
26
+ async def run() -> int:
27
+ env = dict(os.environ)
28
+ env["PYTHONPATH"] = str(SRC) + os.pathsep + env.get("PYTHONPATH", "")
29
+ transport = StdioTransport(sys.executable, ["-m", "ht32_code_mcp"], env=env)
30
+ async with Client(transport) as client:
31
+ tools = {t.name for t in await client.list_tools()}
32
+ missing = EXPECTED_TOOLS - tools
33
+ print(f"工具清单({len(tools)}):{sorted(tools)}")
34
+ if missing:
35
+ print(f"❌ 缺失工具:{missing}")
36
+ return 1
37
+ print(f"✅ 7 工具注册齐全")
38
+
39
+ r = await client.call_tool("get_code_version", {})
40
+ print("✅ get_code_version:", (r.content[0].text or "").splitlines()[0])
41
+
42
+ r = await client.call_tool("find_example", {"peripheral": "GPIO", "limit": 3})
43
+ text = r.content[0].text or ""
44
+ assert "example/GPIO" in text, text
45
+ print(f"✅ find_example(GPIO): {text.splitlines()[0]}")
46
+
47
+ r = await client.call_tool("get_file", {
48
+ "path": "raw/HT32_STD_5xxxx_FWLib_V1.21.1/library/HT32F5xxxx_Driver/inc/ht32f5xxxx_gpio.h",
49
+ "max_length": -1,
50
+ })
51
+ assert "GPIO_DirectionConfig" in (r.content[0].text or "")
52
+ print("✅ get_file(gpio.h) 内容正确")
53
+ print("✅ stdio 冒烟全部通过")
54
+ return 0
55
+
56
+
57
+ if __name__ == "__main__":
58
+ raise SystemExit(asyncio.run(run()))
@@ -0,0 +1,3 @@
1
+ """ht32-code-mcp:HT32 代码知识库 MCP Server(7 工具,本地 stdio)。"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ """允许 `python -m ht32_code_mcp` 直接启动。"""
2
+ from .server import main
3
+
4
+ main()
@@ -0,0 +1,166 @@
1
+ """export_project_bundle:从模板 + 官方库源码组装自包含可编译 Keil 工程到本地目录。
2
+
3
+ 本阶段(P0-2)不做 token 下载 URL,云端阶段再扩展。
4
+ 参数化依据:raw/templates/ht32f5-keil/devices.json(device/cdef/adef/sys/startup)。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ import shutil
10
+ from pathlib import Path
11
+
12
+ _REDIRECT_RE = re.compile(r"#ifdef\s+(USE_\w+)\s*\n\s*#define\s+(USE_\w+)")
13
+
14
+
15
+ class ExportError(Exception):
16
+ """导出失败(型号未知 / 源文件缺失 / 目标目录非空等)。"""
17
+
18
+
19
+ def _read_text(path: Path) -> str:
20
+ return path.read_text(encoding="utf-8", errors="replace")
21
+
22
+
23
+ class ProjectExporter:
24
+ def __init__(self, code_root: Path) -> None:
25
+ self.root = Path(code_root)
26
+ self.template_dir = self.root / "raw" / "templates" / "ht32f5-keil"
27
+ self.fwlib_dir = self.root / "raw" / "HT32_STD_5xxxx_FWLib_V1.21.1"
28
+
29
+ # ------------------------------------------------------------ 主流程
30
+ def export(self, chip: str, out_dir: str) -> str:
31
+ if not self.template_dir.is_dir():
32
+ raise ExportError(f"模板目录不存在:{self.template_dir}")
33
+ device, entry = self._resolve_device(chip)
34
+
35
+ # 1. 预检目标目录
36
+ out = Path(out_dir).expanduser().resolve()
37
+ if out.exists() and any(out.iterdir()):
38
+ raise ExportError(f"目标目录非空,拒绝覆盖:{out}(请指定新目录或先清空)")
39
+
40
+ # 2. 预检官方源文件(先验证后落盘,失败不产生半成品)
41
+ sys_c = self.fwlib_dir / "library" / "Device" / "Holtek" / "HT32F5xxxx" / "Source" / entry["sys"]
42
+ sys_h = sys_c.parent.parent / "Include" / (sys_c.stem + ".h")
43
+ startup = (self.fwlib_dir / "library" / "Device" / "Holtek" / "HT32F5xxxx"
44
+ / "Source" / "ARM" / entry["startup"])
45
+ libcfg_name = self._resolve_libcfg(entry["cdef"])
46
+ libcfg = (self.fwlib_dir / "library" / "HT32F5xxxx_Driver" / "inc" / libcfg_name
47
+ if libcfg_name else None)
48
+ missing = [str(p) for p in (sys_c, sys_h, startup, libcfg) if p is not None and not p.is_file()]
49
+ if missing:
50
+ raise ExportError("官方库缺少所需源文件:\n- " + "\n- ".join(missing))
51
+
52
+ # 3. 复制模板树
53
+ out.mkdir(parents=True, exist_ok=True)
54
+ shutil.copytree(self.template_dir, out, dirs_exist_ok=True)
55
+
56
+ # 4. 参数化 uvprojx
57
+ uvprojx = out / "MDK-ARM" / "ht32f5-template.uvprojx"
58
+ old_sys_stem = "system_ht32f5xxxx_01"
59
+ old_startup_stem = "startup_ht32f5xxxx_01"
60
+ text = _read_text(uvprojx)
61
+ text = re.sub(r"(<Device>)[^<]*(</Device>)", rf"\g<1>{device}\g<2>", text, count=1)
62
+ text = _replace_define(text, "USE_HT32_DRIVER", entry["cdef"])
63
+ text = _replace_define(text, "USE_HT32_CHIP", entry["adef"])
64
+ text = text.replace(old_sys_stem, sys_c.stem).replace(old_startup_stem, startup.stem)
65
+ with open(uvprojx, "w", encoding="utf-8", newline="") as f:
66
+ f.write(text)
67
+
68
+ # 5. 替换设备相关文件
69
+ replaced: list[str] = []
70
+ for old in out.glob("CMSIS/system_ht32f5xxxx_*.c"):
71
+ old.unlink()
72
+ for old in out.glob("CMSIS/system_ht32f5xxxx_*.h"):
73
+ old.unlink()
74
+ shutil.copyfile(sys_c, out / "CMSIS" / sys_c.name)
75
+ shutil.copyfile(sys_h, out / "CMSIS" / sys_h.name)
76
+ replaced += [f"CMSIS/{sys_c.name}", f"CMSIS/{sys_h.name}"]
77
+
78
+ for old in out.glob("Startup/startup_*.s"):
79
+ old.unlink()
80
+ shutil.copyfile(startup, out / "Startup" / startup.name)
81
+ replaced.append(f"Startup/{startup.name}")
82
+
83
+ if libcfg is not None:
84
+ for old in out.glob("Drivers/*_libcfg.h"):
85
+ old.unlink()
86
+ shutil.copyfile(libcfg, out / "Drivers" / libcfg.name)
87
+ replaced.append(f"Drivers/{libcfg.name}")
88
+
89
+ return self._report(out, device, entry, replaced, libcfg_name)
90
+
91
+ # ------------------------------------------------------------ 辅助
92
+ def _resolve_device(self, chip: str) -> tuple[str, dict]:
93
+ import json
94
+ dj = self.template_dir / "devices.json"
95
+ if not dj.is_file():
96
+ raise ExportError(f"模板型号参数表缺失:{dj}")
97
+ meta = json.loads(dj.read_text(encoding="utf-8"))
98
+ want = chip.strip().upper()
99
+ for d in meta.get("devices", []):
100
+ if d.get("device", "").upper() == want:
101
+ return d["device"], d # 重名取首条(官方板级变体)
102
+ # 宽松匹配:型号主体包含(HT32F52352 含于 HT32F52352_SK 场景已由上面精确处理)
103
+ for d in meta.get("devices", []):
104
+ if want in d.get("device", "").upper():
105
+ return d["device"], d
106
+ names = [d.get("device", "") for d in meta.get("devices", [])][:20]
107
+ raise ExportError(
108
+ f"型号 '{chip}' 不在模板参数表(共 {meta.get('count', '?')} 个型号)。\n"
109
+ f"可用型号(前 20):{', '.join(names)}…\n"
110
+ "完整清单:get_file('raw/templates/ht32f5-keil/devices.json') 或 get_project_manifest()"
111
+ )
112
+
113
+ def _resolve_libcfg(self, cdef: str) -> str | None:
114
+ """从 cdef 宏解析 libcfg 文件名,沿 ht32_config.h 重定向链(USE_HT50F32002 → USE_HT32F50220_30)。"""
115
+ inc_dir = self.fwlib_dir / "library" / "HT32F5xxxx_Driver" / "inc"
116
+ config = (self.template_dir / "CMSIS" / "ht32_config.h")
117
+ redirects: dict[str, str] = {}
118
+ if config.is_file():
119
+ redirects = {m.group(1): m.group(2)
120
+ for m in _REDIRECT_RE.finditer(_read_text(config))}
121
+ for token in [t.strip() for t in cdef.split(",")]:
122
+ if not token.startswith("USE_"):
123
+ continue
124
+ cur, seen = token, set()
125
+ while cur and cur not in seen:
126
+ seen.add(cur)
127
+ candidate = inc_dir / (cur[4:].lower() + "_libcfg.h")
128
+ if candidate.is_file():
129
+ return candidate.name
130
+ cur = redirects.get(cur, "")
131
+ return None
132
+
133
+ def _report(self, out: Path, device: str, entry: dict,
134
+ replaced: list[str], libcfg_name: str | None) -> str:
135
+ n_files = sum(1 for p in out.rglob("*") if p.is_file())
136
+ libcfg_note = (f"Drivers/{libcfg_name}" if libcfg_name
137
+ else "(未解析出 libcfg,保留模板默认,请人工核对)")
138
+ lines = [
139
+ f"自包含 Keil 工程已导出:{out}",
140
+ f"目标芯片:{device}({entry.get('sys')} / {entry.get('startup')})",
141
+ f"C 宏(Cads):{entry['cdef']}",
142
+ f"汇编宏(Aads):{entry['adef']}",
143
+ f"libcfg:{libcfg_note}",
144
+ f"工程文件:MDK-ARM/ht32f5-template.uvprojx(已按上述宏参数化)",
145
+ f"文件总数:{n_files}",
146
+ "",
147
+ "按目标芯片替换/注入的文件:",
148
+ *[f"- {r}" for r in replaced],
149
+ "",
150
+ "目录结构:Application(main/it/conf)、CMSIS(设备头+system)、"
151
+ "Drivers(最小驱动集+libcfg)、Startup、MDK-ARM(工程文件)、README.md、devices.json",
152
+ "",
153
+ "后续步骤:",
154
+ "1. Keil MDK 打开 MDK-ARM/ht32f5-template.uvprojx(需 Holtek HT32_DFP pack ≥1.0.67)",
155
+ "2. 如需精确 IROM/IRAM 内存布局,在 Options → Device 重新选择该型号由 DFP 刷新",
156
+ "3. 基线编译验证(P0-4.2 将用 UV4 命令行复核 0 Error 0 Warning)",
157
+ ]
158
+ return "\n".join(lines)
159
+
160
+
161
+ def _replace_define(text: str, anchor: str, new_value: str) -> str:
162
+ """替换 uvprojx 中包含 anchor 的 <Define> 行内容(Cads/Aads 各一条)。"""
163
+ pattern = re.compile(r"(<Define>)[^<]*" + re.escape(anchor) + r"[^<]*(</Define>)")
164
+ if not pattern.search(text):
165
+ raise ExportError(f"uvprojx 中未找到含 {anchor} 的 <Define>,模板结构可能已变化")
166
+ return pattern.sub(rf"\g<1>{new_value}\g<2>", text, count=1)