@
feat(skills): навык kompas-sdk-research + перенос kdoc.py в навык Новый навык kompas-sdk-research делегирует поиск/чтение офлайн-справки КОМПАС SDK субагенту Sonnet (экономит контекст Opus на 26k страниц WebHelp). Навигатор kdoc.py перенесён tools/ → .claude/skills/kompas-sdk-research/ как часть навыка; добавлен автопоиск каталога справки (вверх от CWD/скрипта или через KDOC_DIR), чтобы скрипт работал из любой точки. Индекс остаётся в docs/kompas_sdk_index.tsv. Ссылки обновлены в CLAUDE.md, README, ARCHITECTURE, presentation, .gitignore. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> @
This commit is contained in:
-200
@@ -1,200 +0,0 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user