Files
kompas3d-mcp/tools/kdoc.py
T
mikhail 3a98afc67f chore: initial skeleton + KOMPAS connection spike (S1/S2)
Зафиксирован фундамент проекта MCP-сервера КОМПАС-3D:

- docs/: ARCHITECTURE.md, IMPLEMENTATION_PLAN.md, presentation.html
- tools/kdoc.py: навигатор по справке SDK
- libs/kompas-interop/: вендорские interop-сборки КОМПАС из SDK
- src/Kompas.Mcp.ConnectSpike: net8.0-windows консоль, доказывает
  работу COM-интеропа и attach/launch подключения к КОМПАС
  (IApplication, версия 24.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 23:28:29 +03:00

201 lines
7.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
kdoc — навигация по справке SDK КОМПАС-3D (Help & Manual WebHelp) для агента.
Справка — это ~26 000 HTML-страниц в docs/KOMPAS_SDK_ru-RU/ со «зашумлённой»
вёрсткой (шапка, подвал, JS). Листать их напрямую неэффективно. Этот инструмент:
* build-index — один раз строит компактный индекс docs/kompas_sdk_index.tsv
(файл<TAB>заголовок<TAB>краткое описание) из zoom_pageinfo.js;
* search — ищет страницы по подстроке в заголовке/описании;
* read — печатает чистый текст одной страницы (тело топика без вёрстки).
Типичный сценарий агента:
python tools/kdoc.py search "выдавливание" # найти нужный интерфейс
python tools/kdoc.py read ibossextrusiondefinition.html
Поиск по индексу можно вести и встроенным Grep по docs/kompas_sdk_index.tsv —
формат строки: <имя_файла>\t<заголовок>\t<описание>.
"""
import sys
import os
import re
import json
import html
import argparse
from html.parser import HTMLParser
# На Windows консоль часто cp1251 — принудительно выводим UTF-8,
# чтобы кириллица не искажалась при вызове из агента/любого шелла.
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8")
except (AttributeError, ValueError):
pass
HERE = os.path.dirname(os.path.abspath(__file__))
DOC_DIR = os.environ.get(
"KDOC_DIR", os.path.join(HERE, "..", "docs", "KOMPAS_SDK_ru-RU")
)
DOC_DIR = os.path.abspath(DOC_DIR)
INDEX_PATH = os.path.abspath(os.path.join(DOC_DIR, "..", "kompas_sdk_index.tsv"))
PAGEINFO = os.path.join(DOC_DIR, "zoom_pageinfo.js")
# Текстовый «мусор» навигации/подвала, попадающий в тело топика.
NOISE = {
"please enable javascript to view this site.",
"scroll", "предыдущий", "вверх", "следующий",
"содержание", "содержане", "указатель", "поиск",
"подразделы:", "(отсутствуют)",
}
FOOTER_RE = re.compile(r"©?\s*ООО\s*«?АСКОН")
def build_index() -> int:
"""Разобрать массив pagedata из zoom_pageinfo.js в TSV-индекс."""
if not os.path.isfile(PAGEINFO):
sys.exit(f"Не найден {PAGEINFO}. Проверьте KDOC_DIR / путь к справке.")
with open(PAGEINFO, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
marker = "pagedata = "
pos = text.find(marker)
if pos < 0:
sys.exit("В zoom_pageinfo.js не найден массив pagedata.")
payload = text[pos + len(marker):].strip()
payload = payload.rstrip(";").strip()
data = json.loads(payload) # [["./file.html","title","desc","img"], ...]
rows = []
for entry in data:
if not entry or not entry[0]:
continue
fname = entry[0].lstrip("./").strip()
title = html.unescape((entry[1] or "").strip())
desc = html.unescape((entry[2] or "").strip())
title = re.sub(r"\s+", " ", title)
desc = re.sub(r"\s+", " ", desc)[:300]
rows.append(f"{fname}\t{title}\t{desc}")
with open(INDEX_PATH, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(rows) + "\n")
print(f"Индекс записан: {INDEX_PATH} ({len(rows)} страниц)")
return len(rows)
def search(terms, limit=40):
"""Грубый поиск по индексу: AND по всем подстрокам, без регистра."""
if not os.path.isfile(INDEX_PATH):
print("Индекс отсутствует — строю...", file=sys.stderr)
build_index()
needles = [t.lower() for t in terms if t.strip()]
hits = 0
with open(INDEX_PATH, "r", encoding="utf-8") as f:
for line in f:
low = line.lower()
if all(n in low for n in needles):
parts = line.rstrip("\n").split("\t")
fname = parts[0]
title = parts[1] if len(parts) > 1 else ""
print(f"{fname}\t{title}")
hits += 1
if hits >= limit:
print(f"... (показаны первые {limit}; уточните запрос)")
break
if hits == 0:
print("Ничего не найдено.")
class _TopicExtractor(HTMLParser):
"""Собирает текст из <div id='topicbody'>, пропуская script/style."""
BLOCK = {"p", "div", "br", "tr", "li", "h1", "h2", "h3", "h4",
"table", "pre"}
def __init__(self):
super().__init__(convert_charrefs=True)
self.depth = 0 # глубина вложенности внутри topicbody
self.capturing = False
self.skip = 0 # внутри script/style
self.out = []
def handle_starttag(self, tag, attrs):
attrd = dict(attrs)
if not self.capturing and attrd.get("id") == "topicbody":
self.capturing = True
self.depth = 1
return
if not self.capturing:
return
if tag in ("script", "style"):
self.skip += 1
if tag in self.BLOCK:
self.out.append("\n")
if tag != "br":
self.depth += 1
def handle_endtag(self, tag):
if not self.capturing:
return
if tag in ("script", "style") and self.skip:
self.skip -= 1
if tag != "br":
self.depth -= 1
if self.depth <= 0:
self.capturing = False
def handle_data(self, data):
if self.capturing and not self.skip:
self.out.append(data)
def read(fname):
path = os.path.join(DOC_DIR, os.path.basename(fname))
if not path.endswith(".html"):
path += ".html"
if not os.path.isfile(path):
sys.exit(f"Нет файла: {path}")
with open(path, "r", encoding="utf-8", errors="replace") as f:
raw = f.read()
p = _TopicExtractor()
p.feed(raw)
text = "".join(p.out)
cleaned = []
for line in text.splitlines():
s = re.sub(r"[ \t ]+", " ", line).strip()
if not s:
continue
if s.lower() in NOISE:
continue
if FOOTER_RE.search(s):
break # подвал — дальше только копирайт/телефон
cleaned.append(s)
print(os.path.basename(path))
print("=" * len(os.path.basename(path)))
print("\n".join(cleaned).strip())
def main():
ap = argparse.ArgumentParser(
prog="kdoc", description="Навигация по справке SDK КОМПАС-3D для агента."
)
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("build-index", help="построить docs/kompas_sdk_index.tsv")
sp = sub.add_parser("search", help="искать по заголовку/описанию")
sp.add_argument("terms", nargs="+")
sp.add_argument("--limit", type=int, default=40)
rp = sub.add_parser("read", help="печать чистого текста страницы")
rp.add_argument("file")
args = ap.parse_args()
if args.cmd == "build-index":
build_index()
elif args.cmd == "search":
search(args.terms, args.limit)
elif args.cmd == "read":
read(args.file)
if __name__ == "__main__":
main()