Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b76fbaf17 | |||
| f4c21c825c | |||
| 3fa7543477 | |||
| 064451dc7c | |||
| c76914444c | |||
| 09b19f9ced | |||
| 9f76064826 | |||
| 49a986fa65 | |||
| 2c39e5b50b | |||
| 26b58f2909 | |||
| d9528486ef | |||
| 525899557b | |||
| 03339885ff | |||
| 8fa42206aa | |||
| be12045fa8 | |||
| 7c0aaf5392 | |||
| 55a95889b0 | |||
| 8029f7fb2d | |||
| 2fa4ff2781 | |||
| aadbaab321 | |||
| 54f0d074ba | |||
| 642f2bba6a | |||
| 77cbb1d352 | |||
| 8e686b1ca3 | |||
| d30ceeda6b | |||
| 0168a8b0a5 | |||
| 260b6c3a8a |
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Проверка наличия пользователя в группе 2FA через Active Directory.
|
||||||
|
Использует credentials и настройки из config/config.py.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python check_ad_2fa.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from ldap3 import Server, Connection, ALL, SUBTREE
|
||||||
|
|
||||||
|
# Подтягиваем конфиг
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASES
|
||||||
|
from utils.ad_search import search_by_filter as ad_search_all_bases
|
||||||
|
|
||||||
|
|
||||||
|
def find_user(login: str) -> dict | None:
|
||||||
|
"""Найти пользователя в AD по логину (sAMAccountName или mail).
|
||||||
|
Ищет по всем OU из AD_BASES."""
|
||||||
|
filter_login = login.lower().replace("\\", "\\5c").replace("*", "\\2a").replace("(", "\\28").replace(")", "\\29")
|
||||||
|
|
||||||
|
if "@" in filter_login:
|
||||||
|
ldap_filter = "(|{}{})".format("(mail={})".format(filter_login), "(sAMAccountName={})".format(filter_login))
|
||||||
|
else:
|
||||||
|
ldap_filter = "(|{}{})".format("(sAMAccountName={})".format(filter_login), "(mail={})".format(filter_login))
|
||||||
|
|
||||||
|
entries = ad_search_all_bases(ldap_filter, ["cn", "sAMAccountName", "memberOf"])
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
|
||||||
|
entry = entries[0]
|
||||||
|
return {
|
||||||
|
"dn": entry.entry_dn,
|
||||||
|
"cn": str(entry.cn),
|
||||||
|
"sam": str(entry.sAMAccountName),
|
||||||
|
"groups": [str(g) for g in entry.memberOf],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_2fa_group(login: str) -> dict:
|
||||||
|
"""Проверить, входит ли пользователь в группу 2FA."""
|
||||||
|
user = find_user(login)
|
||||||
|
if user is None:
|
||||||
|
return {"found": False, "error": f"Пользователь '{login}' не найден в AD."}
|
||||||
|
|
||||||
|
# Ищем группу, содержащую "2FA" в имени
|
||||||
|
groups_2fa = [g for g in user["groups"] if "2FA" in g.upper()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"found": True,
|
||||||
|
"user": user,
|
||||||
|
"in_2fa_group": len(groups_2fa) > 0,
|
||||||
|
"matching_groups": groups_2fa,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(" Проверка пользователя в группе 2FA (Active Directory)")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"\n Сервер AD: {AD_SERVER}")
|
||||||
|
print(f" Базы поиска: {config.AD_BASES}")
|
||||||
|
print(f" Подключено как: {AD_USER}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
login = input(" Введите логин (sAMAccountName или email) или 'q' для выхода:\n > ").strip()
|
||||||
|
if not login or login.lower() == "q":
|
||||||
|
print("\n До свидания!\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
result = check_2fa_group(login)
|
||||||
|
|
||||||
|
if not result["found"]:
|
||||||
|
print(f"\n ❌ {result['error']}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
u = result["user"]
|
||||||
|
print(f"\n ✅ Пользователь найден:")
|
||||||
|
print(f" CN: {u['cn']}")
|
||||||
|
print(f" sAM: {u['sam']}")
|
||||||
|
print(f" DN: {u['dn']}")
|
||||||
|
print(f" Групп: {len(u['groups'])}")
|
||||||
|
|
||||||
|
if result["in_2fa_group"]:
|
||||||
|
print(f"\n 🟢 Пользователь ВХОДИТ в группу 2FA:")
|
||||||
|
for g in result["matching_groups"]:
|
||||||
|
print(f" {g}")
|
||||||
|
else:
|
||||||
|
print(f"\n 🔴 Пользователь НЕ найден в группе 2FA")
|
||||||
|
if u["groups"]:
|
||||||
|
print(f"\n Все группы пользователя ({len(u['groups'])}):")
|
||||||
|
for g in u["groups"]:
|
||||||
|
print(f" {g}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n Прервано пользователем.\n")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n ❌ Ошибка: {e}")
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Проверочный скрипт: user_id -> CARD_ID -> телефон.
|
||||||
|
|
||||||
|
Интерактивный режим: после каждого запроса спрашивает,
|
||||||
|
хочешь ли продолжить.
|
||||||
|
|
||||||
|
config.py сам подтягивает все секреты из Passwork при импорте.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# config.py при импорте заполняет globals():
|
||||||
|
# AD_USER, AD_PASSWORD, AD_SERVER, AD_BASES
|
||||||
|
# DB_USER, DB_PASSWORD, SQL_SERVER, SQL_DB_NAME
|
||||||
|
# ============================================================
|
||||||
|
sys.path.insert(0, "/opt/trueconf_bot")
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# AD: user_id -> CARD_ID
|
||||||
|
# ============================================================
|
||||||
|
def get_card_id_from_ad(user_id: str) -> str:
|
||||||
|
"""Ищем пользователя в AD по всем OU из AD_BASES, возвращаем extensionAttribute2 (CARD_ID)."""
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
|
|
||||||
|
entries = search_by_user_id(user_id, ["extensionAttribute2"])
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
raise Exception("Пользователь не найден в AD")
|
||||||
|
|
||||||
|
entry = entries[0]
|
||||||
|
|
||||||
|
if "extensionAttribute2" in entry and entry.extensionAttribute2.value:
|
||||||
|
card_id = str(entry.extensionAttribute2.value).strip()
|
||||||
|
log.info("CARD_ID: {}".format(card_id))
|
||||||
|
return card_id
|
||||||
|
|
||||||
|
raise Exception("extensionAttribute2 пуст для этого пользователя в AD")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# SQL: CARD_ID -> телефон в PR_CARD
|
||||||
|
# ============================================================
|
||||||
|
def get_phone_from_db(card_id: str) -> str:
|
||||||
|
"""Ищем сотрудника в UOV_SELFSERVICE_PR_CARD по CARD_ID."""
|
||||||
|
import pymssql
|
||||||
|
|
||||||
|
conn = pymssql.connect(
|
||||||
|
server=config.SQL_SERVER,
|
||||||
|
user=config.DB_USER,
|
||||||
|
password=config.DB_PASSWORD,
|
||||||
|
database=config.SQL_DB_NAME,
|
||||||
|
charset="cp1251",
|
||||||
|
)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT c.ID, c.PHONE, c.F_NAME, c.L_NAME, c.M_NAME
|
||||||
|
FROM UOV_SELFSERVICE_PR_CARD c
|
||||||
|
WHERE c.ID = %s
|
||||||
|
"""
|
||||||
|
cursor.execute(query, (card_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise Exception("CARD_ID {} не найден в PR_CARD".format(card_id))
|
||||||
|
|
||||||
|
card_id_db = row[0]
|
||||||
|
phone = str(row[1]) if row[1] else "Не указан"
|
||||||
|
f_name = str(row[2]) if row[2] else ""
|
||||||
|
l_name = str(row[3]) if row[3] else ""
|
||||||
|
m_name = str(row[4]) if row[4] else ""
|
||||||
|
|
||||||
|
log.info("PR_CARD.ID: {}".format(card_id_db))
|
||||||
|
log.info("ФИО: {} {} {}".format(l_name, f_name, m_name))
|
||||||
|
log.info("PHONE: {}".format(phone))
|
||||||
|
return phone
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================
|
||||||
|
def run_one():
|
||||||
|
"""Выполнить один запрос: ввод user_id -> CARD_ID -> телефон."""
|
||||||
|
user_id = input("\nВведите user_id (email или sAMAccountName): ").strip()
|
||||||
|
if not user_id:
|
||||||
|
print("user_id пустой, выход.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("[1] Поиск CARD_ID в Active Directory")
|
||||||
|
print(" Сервер: {}".format(config.AD_SERVER))
|
||||||
|
print(" Базы поиска: {}".format(config.AD_BASES))
|
||||||
|
print(" user_id: {}".format(user_id))
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
card_id = get_card_id_from_ad(user_id)
|
||||||
|
except Exception as e:
|
||||||
|
print("Ошибка AD: {}".format(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("[2] Поиск телефона в базе BossCopy")
|
||||||
|
print(" Сервер: {}".format(config.SQL_SERVER))
|
||||||
|
print(" CARD_ID: {}".format(card_id))
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
phone = get_phone_from_db(card_id)
|
||||||
|
except Exception as e:
|
||||||
|
print("Ошибка SQL: {}".format(e))
|
||||||
|
return card_id
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print(" ИТОГ")
|
||||||
|
print("=" * 70)
|
||||||
|
print(" user_id: {}".format(user_id))
|
||||||
|
print(" CARD_ID: {}".format(card_id))
|
||||||
|
print(" Телефон: {}".format(phone))
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
return card_id
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print(" Проверка: user_id -> CARD_ID -> телефон")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
run_one()
|
||||||
|
|
||||||
|
again = input("Продолжить? (y/n): ").strip().lower()
|
||||||
|
if again not in ("y", "yes", "д", "да"):
|
||||||
|
print("Готово.")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Clean and chunk parsed PDF data from unstructured-api.
|
|
||||||
Filters noise, preserves structure, chunks to 1500-1800 chars.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
def load_parsed_data(path):
|
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def clean_text(text):
|
|
||||||
if not text:
|
|
||||||
return ""
|
|
||||||
text = re.sub(r'\s+', ' ', text.strip())
|
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
def is_noise(text):
|
|
||||||
"""Check if text is noise."""
|
|
||||||
text = clean_text(text)
|
|
||||||
if not text:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Company name header/footer
|
|
||||||
if text == "АО «ХК «Сибцем»":
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Page numbers: "Стр. 2 из 10"
|
|
||||||
if re.match(r'^Стр\.\s+\d+\s+из\s+\d+$', text):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Standalone numbers: "4."
|
|
||||||
if re.match(r'^\d+\.$', text):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Table of contents
|
|
||||||
lines = text.split('\n')
|
|
||||||
if len(lines) > 3:
|
|
||||||
page_pattern = re.compile(r'\d+\s*$')
|
|
||||||
page_lines = sum(1 for l in lines if page_pattern.search(l.strip()))
|
|
||||||
if page_lines > len(lines) * 0.5:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Specific noise
|
|
||||||
noise_patterns = [
|
|
||||||
r'^Оглавление$',
|
|
||||||
r'^Содержание$',
|
|
||||||
r'^Редакция \d+$',
|
|
||||||
r'^Тип документа:',
|
|
||||||
r'^Наименование процесса:',
|
|
||||||
r'^Ведущее подразделение:',
|
|
||||||
r'^Дата утверждения:',
|
|
||||||
]
|
|
||||||
for pattern in noise_patterns:
|
|
||||||
if re.search(pattern, text):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_section_title(text):
|
|
||||||
"""Check if text is a section title like '4. Термины, определения и сокращения'."""
|
|
||||||
text = clean_text(text)
|
|
||||||
return bool(re.match(r'^\d+\.\s+\w', text))
|
|
||||||
|
|
||||||
def is_section_subtitle(text):
|
|
||||||
"""Check if text is a section subtitle like '6.7. Электронные...'."""
|
|
||||||
text = clean_text(text)
|
|
||||||
return bool(re.match(r'^\d+\.\d+\.\s+\w', text))
|
|
||||||
|
|
||||||
def is_section_header(text):
|
|
||||||
"""Check if text starts with a section/subsection number."""
|
|
||||||
text = clean_text(text)
|
|
||||||
return bool(re.match(r'^\d+(?:\.\d+)*[\.\s]', text))
|
|
||||||
|
|
||||||
def merge_elements(elements):
|
|
||||||
"""
|
|
||||||
Merge short fragments with context.
|
|
||||||
Strategy: merge consecutive non-title elements into text blocks.
|
|
||||||
Keep titles and tables as separate blocks.
|
|
||||||
"""
|
|
||||||
if not elements:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Filter noise first
|
|
||||||
filtered = [el for el in elements if not is_noise(el.get('text', ''))]
|
|
||||||
|
|
||||||
# Separate into blocks: text blocks, title blocks, table blocks
|
|
||||||
blocks = []
|
|
||||||
current_text_block = []
|
|
||||||
|
|
||||||
for el in filtered:
|
|
||||||
text = el.get('text', '')
|
|
||||||
el_type = el.get('type', '')
|
|
||||||
|
|
||||||
# Skip Image (OCR noise from cover page)
|
|
||||||
if el_type == 'Image':
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If this is a title/section header, flush text block and add title
|
|
||||||
if el_type in ('Title',) and is_section_header(text):
|
|
||||||
if current_text_block:
|
|
||||||
blocks.append({
|
|
||||||
'text': ' '.join(clean_text(e['text']) for e in current_text_block),
|
|
||||||
'type': 'NarrativeText',
|
|
||||||
'metadata': current_text_block[-1].get('metadata', {})
|
|
||||||
})
|
|
||||||
current_text_block = []
|
|
||||||
blocks.append({
|
|
||||||
'text': text,
|
|
||||||
'type': 'Title',
|
|
||||||
'metadata': el.get('metadata', {})
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If this is a table, flush text block and add table
|
|
||||||
if el_type == 'Table':
|
|
||||||
if current_text_block:
|
|
||||||
blocks.append({
|
|
||||||
'text': ' '.join(clean_text(e['text']) for e in current_text_block),
|
|
||||||
'type': 'NarrativeText',
|
|
||||||
'metadata': current_text_block[-1].get('metadata', {})
|
|
||||||
})
|
|
||||||
current_text_block = []
|
|
||||||
blocks.append({
|
|
||||||
'text': text,
|
|
||||||
'type': 'Table',
|
|
||||||
'metadata': el.get('metadata', {})
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Otherwise, accumulate into text block
|
|
||||||
current_text_block.append(el)
|
|
||||||
|
|
||||||
# Flush remaining text block
|
|
||||||
if current_text_block:
|
|
||||||
blocks.append({
|
|
||||||
'text': ' '.join(clean_text(e['text']) for e in current_text_block),
|
|
||||||
'type': 'NarrativeText',
|
|
||||||
'metadata': current_text_block[-1].get('metadata', {})
|
|
||||||
})
|
|
||||||
|
|
||||||
# Now merge short text blocks with adjacent content
|
|
||||||
# Specifically: merge short blocks that are continuations of previous sections
|
|
||||||
result = []
|
|
||||||
for i, block in enumerate(blocks):
|
|
||||||
text = block['text']
|
|
||||||
el_type = block['type']
|
|
||||||
clean = clean_text(text)
|
|
||||||
|
|
||||||
# If this is a short text block, try to merge with previous
|
|
||||||
if len(clean) < 60 and el_type == 'NarrativeText' and result:
|
|
||||||
# Check if it looks like a continuation (section subtitle or fragment)
|
|
||||||
if is_section_subtitle(clean) or not clean[0].isdigit():
|
|
||||||
# Merge with previous block
|
|
||||||
result[-1]['text'] = f"{result[-1]['text']} {clean}"
|
|
||||||
continue
|
|
||||||
|
|
||||||
result.append(block)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def chunk_text(text, max_chunk_size=1800):
|
|
||||||
"""Split text into chunks of ~1500-1800 characters."""
|
|
||||||
if len(text) <= max_chunk_size:
|
|
||||||
return [text]
|
|
||||||
|
|
||||||
# Try to split by paragraphs first
|
|
||||||
paragraphs = re.split(r'\n+', text)
|
|
||||||
if len(paragraphs) > 1:
|
|
||||||
chunks = []
|
|
||||||
current = ""
|
|
||||||
for para in paragraphs:
|
|
||||||
para = para.strip()
|
|
||||||
if not para:
|
|
||||||
continue
|
|
||||||
if len(current) + len(para) + 1 <= max_chunk_size:
|
|
||||||
current = f"{current}\n{para}"
|
|
||||||
else:
|
|
||||||
if current:
|
|
||||||
chunks.append(current)
|
|
||||||
current = para
|
|
||||||
if current:
|
|
||||||
chunks.append(current)
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
# Split by sentences
|
|
||||||
sentences = re.split(r'(?<=[.!?])\s+', text)
|
|
||||||
chunks = []
|
|
||||||
current = ""
|
|
||||||
for sentence in sentences:
|
|
||||||
if len(current) + len(sentence) + 1 <= max_chunk_size:
|
|
||||||
current = f"{current} {sentence}"
|
|
||||||
else:
|
|
||||||
if current:
|
|
||||||
chunks.append(current)
|
|
||||||
current = sentence
|
|
||||||
if current:
|
|
||||||
chunks.append(current)
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
def process_elements(elements, max_chunk_size=1800):
|
|
||||||
"""Process elements into chunks."""
|
|
||||||
# Merge blocks
|
|
||||||
merged = merge_elements(elements)
|
|
||||||
|
|
||||||
# Create chunks
|
|
||||||
chunks = []
|
|
||||||
chunk_index = 0
|
|
||||||
|
|
||||||
for el in merged:
|
|
||||||
text = el.get('text', '')
|
|
||||||
el_type = el.get('type', 'Unknown')
|
|
||||||
page = el.get('metadata', {}).get('page_number', '?')
|
|
||||||
|
|
||||||
# Skip very short chunks
|
|
||||||
if len(text.strip()) < 10:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Split long texts
|
|
||||||
parts = chunk_text(text, max_chunk_size)
|
|
||||||
|
|
||||||
for part in parts:
|
|
||||||
chunk_index += 1
|
|
||||||
chunks.append({
|
|
||||||
'index': chunk_index,
|
|
||||||
'type': el_type,
|
|
||||||
'page': page,
|
|
||||||
'text': part,
|
|
||||||
'size': len(part)
|
|
||||||
})
|
|
||||||
|
|
||||||
return chunks, merged
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parsed_path = '/tmp/pol177_parsed.json'
|
|
||||||
output_path = '/tmp/pol177_clean_chunk.json'
|
|
||||||
|
|
||||||
print("Loading parsed data...")
|
|
||||||
elements = load_parsed_data(parsed_path)
|
|
||||||
print(f"Loaded {len(elements)} elements")
|
|
||||||
|
|
||||||
print("Processing...")
|
|
||||||
chunks, merged = process_elements(elements)
|
|
||||||
|
|
||||||
print(f"Generated {len(chunks)} chunks")
|
|
||||||
|
|
||||||
# Stats
|
|
||||||
if chunks:
|
|
||||||
sizes = [c['size'] for c in chunks]
|
|
||||||
print(f"Chunk sizes: min={min(sizes)}, max={max(sizes)}, avg={sum(sizes)/len(sizes):.0f}")
|
|
||||||
|
|
||||||
# Type distribution
|
|
||||||
types = {}
|
|
||||||
for c in chunks:
|
|
||||||
types[c['type']] = types.get(c['type'], 0) + 1
|
|
||||||
print(f"Chunk types: {types}")
|
|
||||||
|
|
||||||
# Show all chunks
|
|
||||||
print("\n=== All chunks ===")
|
|
||||||
for c in chunks:
|
|
||||||
print(f"[{c['index']}] {c['type']} (page {c['page']}, {c['size']} chars)")
|
|
||||||
print(f" {c['text'][:120]}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
+5
-1
@@ -85,6 +85,7 @@ ENABLE_SERVICE_DESK = True # Модуль создания заявок в
|
|||||||
ENABLE_PHOTO_BOT = True # Модуль обработки фото/сканов
|
ENABLE_PHOTO_BOT = True # Модуль обработки фото/сканов
|
||||||
ENABLE_TRANSCRIPTION = True # Модуль расшифровки голосовых сообщений (Whisper)
|
ENABLE_TRANSCRIPTION = True # Модуль расшифровки голосовых сообщений (Whisper)
|
||||||
ENABLE_SEARCH_BOT = True # Модуль интеллектуального поиска по регламентам (RAG)
|
ENABLE_SEARCH_BOT = True # Модуль интеллектуального поиска по регламентам (RAG)
|
||||||
|
ENABLE_INSTRUCT = True # Модуль инструкций (Trueconf, Почта)
|
||||||
ENABLE_LK = True # Личный кабинет сотрудника (отпуска, расчетные листки, справки)
|
ENABLE_LK = True # Личный кабинет сотрудника (отпуска, расчетные листки, справки)
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@@ -103,7 +104,10 @@ ALLOWED_USERS = [ # Список TrueConf ID разрешенны
|
|||||||
TC_SERVER = os.getenv("TC_SERVER", "") # Адрес сервера TrueConf
|
TC_SERVER = os.getenv("TC_SERVER", "") # Адрес сервера TrueConf
|
||||||
|
|
||||||
AD_SERVER = "ldap://172.16.20.20" # Адрес контроллера домена Active Directory
|
AD_SERVER = "ldap://172.16.20.20" # Адрес контроллера домена Active Directory
|
||||||
AD_BASE = "OU=-Пользователи,DC=sibcem,DC=ru" # Базовый путь поиска пользователей в AD
|
AD_BASES = ( # Базовые пути поиска пользователей в AD (несколько OU)
|
||||||
|
"OU=-Пользователи,DC=sibcem,DC=ru",
|
||||||
|
"OU=Планшеты,OU=enabled,OU=БезКомпьютеров,DC=sibcem,DC=ru",
|
||||||
|
)
|
||||||
|
|
||||||
SQL_SERVER = "SRVKEM-MOBILEIN.sibcem.ru" # Сервер MS SQL для работы Личного кабинета
|
SQL_SERVER = "SRVKEM-MOBILEIN.sibcem.ru" # Сервер MS SQL для работы Личного кабинета
|
||||||
SQL_DB_NAME = "BossCopy" # База данных с информацией по кадрам и ЗП
|
SQL_DB_NAME = "BossCopy" # База данных с информацией по кадрам и ЗП
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Debug: print the exact LDAP filter that works in lk/handlers.py"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "/opt/trueconf_bot")
|
||||||
|
from config.config import AD_USER, AD_PASSWORD, AD_SERVER, AD_BASES
|
||||||
|
from utils.ad_search import search_by_filter as ad_search_all_bases
|
||||||
|
|
||||||
|
from ldap3.utils.conv import escape_filter_chars
|
||||||
|
|
||||||
|
# Simulate lk/handlers.py filter
|
||||||
|
user_id = "ds.krivochenko@tcs.sibcem.ru"
|
||||||
|
search_id = user_id.replace("@tcs.sibcem.ru", "@sibcem.ru")
|
||||||
|
short_username = search_id.split("@")[0]
|
||||||
|
safe_user_id = escape_filter_chars(search_id)
|
||||||
|
|
||||||
|
print(f"user_id: {user_id}")
|
||||||
|
print(f"search_id: {search_id}")
|
||||||
|
print(f"short_username: {short_username}")
|
||||||
|
print(f"safe_user_id: {safe_user_id}")
|
||||||
|
|
||||||
|
# lk/handlers.py filter
|
||||||
|
filter_lk = "(&(objectClass=user)(|(mail={})(userPrincipalName={})(userPrincipalName={}{}{}))({}))".format(
|
||||||
|
safe_user_id,
|
||||||
|
safe_user_id,
|
||||||
|
escape_filter_chars(short_username),
|
||||||
|
"@sibcem.ru",
|
||||||
|
"",
|
||||||
|
escape_filter_chars(short_username),
|
||||||
|
)
|
||||||
|
print(f"\nFilter (lk format):")
|
||||||
|
print(filter_lk)
|
||||||
|
|
||||||
|
# Count parens
|
||||||
|
opens = filter_lk.count('(')
|
||||||
|
closes = filter_lk.count(')')
|
||||||
|
print(f"\nOpen parens: {opens}, Close parens: {closes}")
|
||||||
|
|
||||||
|
# Now test with the connection
|
||||||
|
print(f"\nSearching with filter across all AD_BASES...")
|
||||||
|
entries = ad_search_all_bases(filter_lk, ["cn", "sAMAccountName", "mail", "userPrincipalName"])
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
entry = entries[0]
|
||||||
|
print(f"FOUND: cn={entry.cn}, mail={entry.mail if 'mail' in entry else 'N/A'}, sam={entry.sAMAccountName if 'sAMAccountName' in entry else 'N/A'}")
|
||||||
|
else:
|
||||||
|
print("NOT FOUND in any AD_BASE")
|
||||||
|
|
||||||
|
# Try root
|
||||||
|
root_base = "DC=sibcem,DC=ru"
|
||||||
|
print(f"\nSearching in {root_base}...")
|
||||||
|
from utils.ad_search import search_by_filter
|
||||||
|
entries2 = search_by_filter(filter_lk, ["cn", "sAMAccountName", "mail", "userPrincipalName"])
|
||||||
|
|
||||||
|
if entries2:
|
||||||
|
entry = entries2[0]
|
||||||
|
print(f"FOUND: cn={entry.cn}, mail={entry.mail if 'mail' in entry else 'N/A'}, sam={entry.sAMAccountName if 'sAMAccountName' in entry else 'N/A'}")
|
||||||
|
else:
|
||||||
|
print("NOT FOUND in root")
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
from utils.texts import (
|
||||||
|
UNKNOWN_MAIN_CMD_TEXT,
|
||||||
|
EMOJI_DIGITS,
|
||||||
|
INSTRUCT_MAIN_MENU_TEXT,
|
||||||
|
INSTRUCT_TRUECONF_TEXT,
|
||||||
|
INSTRUCT_TRUECONF_WITH_AD,
|
||||||
|
INSTRUCT_EMAIL_TEXT,
|
||||||
|
)
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
from trueconf import Router, Message
|
||||||
|
from trueconf.types import FSInputFile
|
||||||
|
|
||||||
|
from utils.states import get_state, set_state, clear_state
|
||||||
|
|
||||||
|
# 🔌 Импортируем централизованную функцию сбора статистики из main
|
||||||
|
from utils.stats_logger import log_menu_stats
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
# Путь к файлу инструкции Trueconf
|
||||||
|
TRUECONF_INSTRUCT_PATH = os.path.join(os.path.dirname(__file__), "Проверка_наличия_и_авторизация_на_мобильном_устройстве_Trueconf.docx")
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_cn(user_id: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Получить CN пользователя из AD по его user_id (email или логин).
|
||||||
|
user_id из TrueConf — это обычно email, например 'ds.krivochenko@tcs.sibcem.ru'
|
||||||
|
Заменяет tcs.sibcem.ru на sibcem.ru для поиска в AD.
|
||||||
|
Ищет по всем OU из AD_BASES.
|
||||||
|
"""
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Заменяем tcs.sibcem.ru на sibcem.ru
|
||||||
|
search_id = user_id.replace("@tcs.sibcem.ru", "@sibcem.ru")
|
||||||
|
short_username = search_id.split("@")[0] if "@" in search_id else user_id
|
||||||
|
|
||||||
|
logger.info(f"AD lookup: user_id={user_id}, search_id={search_id}, short={short_username}")
|
||||||
|
|
||||||
|
entries = search_by_user_id(search_id, ["cn", "sAMAccountName", "mail", "userPrincipalName"])
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
entry = entries[0]
|
||||||
|
cn = str(entry.cn)
|
||||||
|
mail = str(entry.mail) if 'mail' in entry else "N/A"
|
||||||
|
sam = str(entry.sAMAccountName) if 'sAMAccountName' in entry else "N/A"
|
||||||
|
upn = str(entry.userPrincipalName) if 'userPrincipalName' in entry else "N/A"
|
||||||
|
logger.info(f"AD found: cn={cn}, mail={mail}, sam={sam}, upn={upn}")
|
||||||
|
return cn
|
||||||
|
|
||||||
|
logger.warning(f"AD not found for user_id={user_id}, search_id={search_id}, short={short_username}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения CN из AD для {user_id}: {e}", exc_info=True)
|
||||||
|
return user_id # fallback — используем user_id как CN
|
||||||
|
|
||||||
|
|
||||||
|
@router.message()
|
||||||
|
async def instruct_router_handler(msg: Message):
|
||||||
|
user_id = msg.from_user.id
|
||||||
|
user_name = getattr(msg.from_user, 'name', 'N/A')
|
||||||
|
user_username = getattr(msg.from_user, 'username', 'N/A')
|
||||||
|
|
||||||
|
logger.info(f"Instruct: user_id={user_id}, name={user_name}, username={user_username}")
|
||||||
|
|
||||||
|
current_state = get_state(user_id)
|
||||||
|
|
||||||
|
if not msg.text:
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = msg.text.strip().lower()
|
||||||
|
|
||||||
|
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
||||||
|
if cmd == emoji_num:
|
||||||
|
cmd = raw_num
|
||||||
|
break
|
||||||
|
|
||||||
|
if current_state is None:
|
||||||
|
from utils.menu import MENU_MAP
|
||||||
|
if MENU_MAP.get(cmd) == "INSTRUCT":
|
||||||
|
msg.handled = True
|
||||||
|
set_state(user_id, "INSTRUCT_MODE")
|
||||||
|
await msg.answer(INSTRUCT_MAIN_MENU_TEXT, parse_mode="html")
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
if current_state == "INSTRUCT_MODE":
|
||||||
|
msg.handled = True
|
||||||
|
if cmd == "0":
|
||||||
|
log_menu_stats(user_id, "Инструкции", "Выход в главное меню")
|
||||||
|
clear_state(user_id)
|
||||||
|
from utils.menu import MENU_TEXT
|
||||||
|
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||||
|
elif cmd == "1":
|
||||||
|
log_menu_stats(user_id, "Инструкции", "Trueconf")
|
||||||
|
set_state(user_id, "INSTRUCT_TRUECONF_VIEW")
|
||||||
|
cn = get_user_cn(user_id)
|
||||||
|
text = INSTRUCT_TRUECONF_WITH_AD(EMOJI_DIGITS, cn)
|
||||||
|
await msg.answer(text, parse_mode="html")
|
||||||
|
if os.path.isfile(TRUECONF_INSTRUCT_PATH):
|
||||||
|
await msg.answer_document(FSInputFile(TRUECONF_INSTRUCT_PATH))
|
||||||
|
elif cmd == "2":
|
||||||
|
log_menu_stats(user_id, "Инструкции", "Почта")
|
||||||
|
set_state(user_id, "INSTRUCT_EMAIL_VIEW")
|
||||||
|
await msg.answer(INSTRUCT_EMAIL_TEXT, parse_mode="html")
|
||||||
|
else:
|
||||||
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
|
|
||||||
|
elif current_state in ["INSTRUCT_TRUECONF_VIEW", "INSTRUCT_EMAIL_VIEW"]:
|
||||||
|
msg.handled = True
|
||||||
|
if cmd == "0":
|
||||||
|
clear_state(user_id)
|
||||||
|
from utils.menu import MENU_TEXT
|
||||||
|
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||||
|
elif cmd == "9":
|
||||||
|
set_state(user_id, "INSTRUCT_MODE")
|
||||||
|
await msg.answer(INSTRUCT_MAIN_MENU_TEXT, parse_mode="html")
|
||||||
|
else:
|
||||||
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
Binary file not shown.
Binary file not shown.
+10
-11
@@ -129,19 +129,18 @@ async def handle_lk_error(msg: Message, user_id: str, action: str, error_details
|
|||||||
# ИНТЕГРАЦИЯ С ACTIVE DIRECTORY ЧЕРЕЗ extensionAttribute2
|
# ИНТЕГРАЦИЯ С ACTIVE DIRECTORY ЧЕРЕЗ extensionAttribute2
|
||||||
# =========================================================
|
# =========================================================
|
||||||
def get_card_id_from_ad(user_id: str) -> str:
|
def get_card_id_from_ad(user_id: str) -> str:
|
||||||
|
# Поиск CARD_ID по всем OU из AD_BASES
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
try:
|
try:
|
||||||
server = Server(config.AD_SERVER, get_info=ALL)
|
entries = search_by_user_id(user_id, ['extensionAttribute2'])
|
||||||
conn = Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True)
|
if not entries:
|
||||||
safe_user_id = escape_filter_chars(user_id)
|
raise Exception("Пользователь не найден в AD.")
|
||||||
short_username = user_id.split('@')[0]
|
entry = entries[0]
|
||||||
search_filter = f"(&(objectClass=user)(|(mail={safe_user_id})(userPrincipalName={safe_user_id})(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})(sAMAccountName={escape_filter_chars(short_username)})))"
|
if 'extensionAttribute2' in entry and entry.extensionAttribute2.value:
|
||||||
|
return str(entry.extensionAttribute2.value).strip()
|
||||||
conn.search(config.AD_BASE, search_filter, attributes=['extensionAttribute2'])
|
|
||||||
|
|
||||||
if conn.entries and 'extensionAttribute2' in conn.entries[0] and conn.entries[0].extensionAttribute2.value:
|
|
||||||
return str(conn.entries[0].extensionAttribute2.value).strip()
|
|
||||||
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
||||||
except Exception as e: raise Exception(f"AD Card Query Error: {e}")
|
except Exception as e:
|
||||||
|
raise Exception("AD Card Query Error: {}".format(e))
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# МОДНОЕ УНИВЕРСАЛЬНОЕ ЯДРО ПАГИНАЦИИ СПИСКОВ ДЛЯ ЧАТА
|
# МОДНОЕ УНИВЕРСАЛЬНОЕ ЯДРО ПАГИНАЦИИ СПИСКОВ ДЛЯ ЧАТА
|
||||||
|
|||||||
+18
-9
@@ -13,7 +13,10 @@ from ldap3.utils.conv import escape_filter_chars
|
|||||||
TEST_USER = "man.bogov@tcs.sibcem.ru"
|
TEST_USER = "man.bogov@tcs.sibcem.ru"
|
||||||
|
|
||||||
AD_SERVER = "ldap://172.16.20.20"
|
AD_SERVER = "ldap://172.16.20.20"
|
||||||
AD_BASE = "OU=-Пользователи,DC=sibcem,DC=ru"
|
AD_BASES = (
|
||||||
|
"OU=-Пользователи,DC=sibcem,DC=ru",
|
||||||
|
"OU=Планшеты,OU=enabled,OU=БезКомпьютеров,DC=sibcem,DC=ru",
|
||||||
|
)
|
||||||
|
|
||||||
AD_USER = "sdesk-mail"
|
AD_USER = "sdesk-mail"
|
||||||
AD_PASSWORD = "fne?e!q.m8phcrGVAcqr"
|
AD_PASSWORD = "fne?e!q.m8phcrGVAcqr"
|
||||||
@@ -32,7 +35,6 @@ DB_PASSWORD = "Bav:fX#UwH8%atv4"
|
|||||||
def get_phone_from_ad(user_id: str) -> str:
|
def get_phone_from_ad(user_id: str) -> str:
|
||||||
|
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
server = Server(AD_SERVER, get_info=ALL)
|
||||||
|
|
||||||
conn = Connection(
|
conn = Connection(
|
||||||
server,
|
server,
|
||||||
user=AD_USER,
|
user=AD_USER,
|
||||||
@@ -41,22 +43,29 @@ def get_phone_from_ad(user_id: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
safe_user_id = escape_filter_chars(user_id)
|
safe_user_id = escape_filter_chars(user_id)
|
||||||
|
|
||||||
short_username = user_id.split("@")[0]
|
short_username = user_id.split("@")[0]
|
||||||
|
|
||||||
search_filter = (
|
search_filter = (
|
||||||
f"(&(objectClass=user)"
|
"(&(objectClass=user)"
|
||||||
f"(|(mail={safe_user_id})"
|
"(|(mail={})"
|
||||||
f"(userPrincipalName={safe_user_id})"
|
"(userPrincipalName={})"
|
||||||
f"(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})"
|
"(userPrincipalName={}))"
|
||||||
f"(sAMAccountName={escape_filter_chars(short_username)})))"
|
"(sAMAccountName={}))".format(
|
||||||
|
safe_user_id,
|
||||||
|
safe_user_id,
|
||||||
|
escape_filter_chars(short_username) + "@sibcem.ru",
|
||||||
|
escape_filter_chars(short_username),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for base in AD_BASES:
|
||||||
conn.search(
|
conn.search(
|
||||||
AD_BASE,
|
base,
|
||||||
search_filter,
|
search_filter,
|
||||||
attributes=['mobile', 'telephoneNumber']
|
attributes=['mobile', 'telephoneNumber']
|
||||||
)
|
)
|
||||||
|
if conn.entries:
|
||||||
|
break
|
||||||
|
|
||||||
if not conn.entries:
|
if not conn.entries:
|
||||||
raise Exception("Пользователь не найден в AD")
|
raise Exception("Пользователь не найден в AD")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
import time
|
import time
|
||||||
@@ -18,13 +17,6 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logging.getLogger('trueconf.client.chatbot').propagate = False
|
logging.getLogger('trueconf.client.chatbot').propagate = False
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
sd_workflow_logger = logging.getLogger('sd_workflow')
|
|
||||||
sd_workflow_logger.setLevel(logging.INFO)
|
|
||||||
if not sd_workflow_logger.handlers:
|
|
||||||
handler = RotatingFileHandler(os.path.join(os.path.dirname(__file__), 'logs', 'sd_workflow.log'), maxBytes=10*1024*1024, backupCount=5)
|
|
||||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
sd_workflow_logger.addHandler(handler)
|
|
||||||
|
|
||||||
# Добавляем путь к проекту в sys.path
|
# Добавляем путь к проекту в sys.path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
@@ -79,7 +71,7 @@ log_ram_usage("Старт скрипта (базовый вес)")
|
|||||||
from photo_bot.handlers import router as photo_router, PHOTO_MAIN_MENU_TEXT
|
from photo_bot.handlers import router as photo_router, PHOTO_MAIN_MENU_TEXT
|
||||||
log_ram_usage("После импорта photo_bot")
|
log_ram_usage("После импорта photo_bot")
|
||||||
|
|
||||||
from service_desk.handlers import router as sd_router, SD_MAIN_MENU_TEXT, sd_sessions
|
from service_desk.handlers import router as sd_router, SD_MAIN_MENU_TEXT
|
||||||
log_ram_usage("После импорта service_desk")
|
log_ram_usage("После импорта service_desk")
|
||||||
|
|
||||||
from transcription_bot.handlers import router as transcription_router, TRANSCRIPTION_MAIN_MENU_TEXT
|
from transcription_bot.handlers import router as transcription_router, TRANSCRIPTION_MAIN_MENU_TEXT
|
||||||
@@ -88,6 +80,9 @@ log_ram_usage("После импорта transcription_router (Виспер)")
|
|||||||
from search_bot.handlers import router as search_router, SEARCH_MAIN_MENU_TEXT
|
from search_bot.handlers import router as search_router, SEARCH_MAIN_MENU_TEXT
|
||||||
log_ram_usage("После импорта search_router")
|
log_ram_usage("После импорта search_router")
|
||||||
|
|
||||||
|
from instruct.handlers import router as instruct_router
|
||||||
|
log_ram_usage("После импорта instruct_router")
|
||||||
|
|
||||||
from lk.handlers import router as lk_router
|
from lk.handlers import router as lk_router
|
||||||
log_ram_usage("После импорта lk_router (Личный кабинет)")
|
log_ram_usage("После импорта lk_router (Личный кабинет)")
|
||||||
|
|
||||||
@@ -120,68 +115,7 @@ async def main_menu(msg: Message):
|
|||||||
USER_MENU_SHOWN[user_id] = True
|
USER_MENU_SHOWN[user_id] = True
|
||||||
set_state(user_id, "SD_MODE")
|
set_state(user_id, "SD_MODE")
|
||||||
log_menu_stats(user_id, "Service Desk", "Вход")
|
log_menu_stats(user_id, "Service Desk", "Вход")
|
||||||
# Помечаем сообщение как обработанное, чтобы sd_module_handler не перезаписал
|
|
||||||
msg.handled = True
|
|
||||||
# Инициализируем сессию для service_desk
|
|
||||||
if user_id not in sd_sessions:
|
|
||||||
sd_sessions[user_id] = {"step": "choose_template", "files_queue": [], "post_create_queue": [], "templates": [], "template_names": {}, "template_menu_text": ""}
|
|
||||||
# Загружаем шаблоны и встраиваем их в текст
|
|
||||||
try:
|
|
||||||
import json, asyncio, httpx, urllib3
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
||||||
# Импортируем SD_URL и SD_TOKEN из config
|
|
||||||
import importlib.util
|
|
||||||
spec = importlib.util.spec_from_file_location("custom_config", os.path.join(os.path.dirname(os.path.abspath(__file__)), "config", "config.py"))
|
|
||||||
custom_config = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(custom_config)
|
|
||||||
SD_URL = custom_config.SD_URL
|
|
||||||
SD_TOKEN = custom_config.SD_TOKEN
|
|
||||||
headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json"}
|
|
||||||
input_data = {"list_info": {"row_count": 100, "start_index": 1}}
|
|
||||||
params = {"input_data": json.dumps(input_data)}
|
|
||||||
url = f"{SD_URL}/api/v3/request_templates"
|
|
||||||
async with httpx.AsyncClient(verify=False) as client:
|
|
||||||
resp = await client.get(url, headers=headers, params=params, timeout=30.0)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
data = resp.json()
|
|
||||||
all_templates = data.get("request_templates", [])
|
|
||||||
pre_filtered = [
|
|
||||||
t for t in all_templates
|
|
||||||
if t.get("status") == "ACTIVE"
|
|
||||||
and not t.get("inactive", False)
|
|
||||||
and str(t.get("id")) != "2"
|
|
||||||
]
|
|
||||||
async def check_template(t, cl):
|
|
||||||
try:
|
|
||||||
t_resp = await client.get(url + "/" + str(t["id"]), headers=headers, timeout=15.0)
|
|
||||||
if t_resp.status_code == 200:
|
|
||||||
detailed = t_resp.json().get("request_template", {})
|
|
||||||
return t if detailed.get("show_to_requester") is True else None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
tasks = [check_template(t, client) for t in pre_filtered]
|
|
||||||
results = await asyncio.gather(*tasks)
|
|
||||||
templates = [t for t in results if t is not None]
|
|
||||||
template_lines = ""
|
|
||||||
for idx, t in enumerate(templates):
|
|
||||||
num = idx + 1
|
|
||||||
digit = f"{num}⃣" if num <= 9 else "0⃣" if num == 10 else str(num)
|
|
||||||
template_lines += f"{digit} - {t['name']}\n"
|
|
||||||
# Сохраняем шаблоны в sd_sessions для sd_module_handler
|
|
||||||
sd_sessions[user_id]["templates"] = templates
|
|
||||||
sd_sessions[user_id]["template_names"] = {str(i+1): str(t["id"]) for i, t in enumerate(templates)}
|
|
||||||
sd_sessions[user_id]["template_menu_text"] = template_lines.strip()
|
|
||||||
template_text = f"🎫 Режим создания заявки в Service Desk\n\nВыберите, пожалуйста, шаблон подходящий под вашу проблему\n\n{template_lines}"
|
|
||||||
await msg.answer(template_text, parse_mode="html")
|
|
||||||
msg.handled = True
|
|
||||||
else:
|
|
||||||
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
|
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
|
||||||
msg.handled = True
|
|
||||||
except Exception as e:
|
|
||||||
sd_workflow_logger.error(f"Ошибка шаблонов: {e}")
|
|
||||||
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
|
|
||||||
msg.handled = True
|
|
||||||
return
|
return
|
||||||
elif action == "PHOTO":
|
elif action == "PHOTO":
|
||||||
USER_MENU_SHOWN[user_id] = True
|
USER_MENU_SHOWN[user_id] = True
|
||||||
@@ -208,6 +142,18 @@ async def main_menu(msg: Message):
|
|||||||
from lk.handlers import send_dynamic_lk_main_menu
|
from lk.handlers import send_dynamic_lk_main_menu
|
||||||
await send_dynamic_lk_main_menu(msg, user_id)
|
await send_dynamic_lk_main_menu(msg, user_id)
|
||||||
return
|
return
|
||||||
|
elif action == "INSTRUCT":
|
||||||
|
USER_MENU_SHOWN[user_id] = True
|
||||||
|
set_state(user_id, "INSTRUCT_MODE")
|
||||||
|
log_menu_stats(user_id, "Инструкции", "Вход")
|
||||||
|
await msg.answer(
|
||||||
|
"📋 <b>Выберите инструкцию:</b>\n\n"
|
||||||
|
"1⃣ — 🖥 Trueconf\n"
|
||||||
|
"2⃣ — 📧 Почта\n\n"
|
||||||
|
"<i>0⃣ — В главное меню</i>",
|
||||||
|
parse_mode="html"
|
||||||
|
)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
if not menu_shown:
|
if not menu_shown:
|
||||||
USER_MENU_SHOWN[user_id] = True
|
USER_MENU_SHOWN[user_id] = True
|
||||||
@@ -225,6 +171,7 @@ async def main():
|
|||||||
dp.include_router(sd_router)
|
dp.include_router(sd_router)
|
||||||
dp.include_router(transcription_router)
|
dp.include_router(transcription_router)
|
||||||
dp.include_router(search_router)
|
dp.include_router(search_router)
|
||||||
|
dp.include_router(instruct_router)
|
||||||
dp.include_router(lk_router)
|
dp.include_router(lk_router)
|
||||||
dp.include_router(main_router)
|
dp.include_router(main_router)
|
||||||
bot = Bot.from_credentials(server=TC_SERVER, username=TC_LOGIN, password=TC_PASSWORD, dispatcher=dp, verify_ssl=False)
|
bot = Bot.from_credentials(server=TC_SERVER, username=TC_LOGIN, password=TC_PASSWORD, dispatcher=dp, verify_ssl=False)
|
||||||
|
|||||||
-100
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Парсинг регламента через Unstructured API.
|
|
||||||
Делает один проход (strategy=hi_res) и разделяет элементы на текст и таблицы.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# === НАСТРОЙКИ ===
|
|
||||||
UNSTRUCTURED_API = "http://192.168.1.103:8005/general/v0/general"
|
|
||||||
PDF_PATH = "/opt/documents_xk/АОХКСибцем/07 Служба Вице-президента по экономике и финансам/ДИТ/02 Регламенты деятельности/08 Положение Управление ИТ-инцидентами от 20.02.2024 № ПОЛ-123-2.pdf"
|
|
||||||
OUTPUT_PATH = "/opt/okf-regulations/concepts/pol123_parsed.json"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_document(pdf_path: str) -> dict:
|
|
||||||
"""Отправляет PDF в API (hi_res) и возвращает рассортированные данные."""
|
|
||||||
print(f" Отправка файла {Path(pdf_path).name} в Unstructured API (hi_res)...")
|
|
||||||
|
|
||||||
with open(pdf_path, "rb") as f:
|
|
||||||
files = {"files": (Path(pdf_path).name, f, "application/pdf")}
|
|
||||||
data = {
|
|
||||||
"strategy": "hi_res",
|
|
||||||
"coordinates": "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(UNSTRUCTURED_API, files=files, data=data, timeout=300)
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f" ❌ Ошибка Unstructured API: {e}")
|
|
||||||
return {"text": [], "tables": []}
|
|
||||||
|
|
||||||
elements = response.json()
|
|
||||||
if isinstance(elements, dict):
|
|
||||||
elements = elements.get("elements", [])
|
|
||||||
|
|
||||||
text_elements = []
|
|
||||||
real_tables = []
|
|
||||||
|
|
||||||
print(" Сортировка и фильтрация элементов...")
|
|
||||||
for el in elements:
|
|
||||||
el_type = el.get("type")
|
|
||||||
|
|
||||||
if el_type == "Table":
|
|
||||||
meta = el.get("metadata", {})
|
|
||||||
html = meta.get("text_as_html", "")
|
|
||||||
|
|
||||||
# Строгий фильтр на наличие HTML-таблицы
|
|
||||||
if html and "<table" in html.lower():
|
|
||||||
row_count = len(re.findall(r'<tr[^>]*>', html, re.IGNORECASE)) if html else None
|
|
||||||
col_count = len(re.findall(r'<td[^>]*>|<th[^>]*>', html[:500], re.IGNORECASE)) if html else None
|
|
||||||
|
|
||||||
real_tables.append({
|
|
||||||
"page": meta.get("page_number"),
|
|
||||||
"text": el.get("text", ""),
|
|
||||||
"html": html,
|
|
||||||
"row_count": row_count,
|
|
||||||
"col_count": col_count,
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
# Если таблица оказалась фейковой, отправляем её текст в общий котел
|
|
||||||
text_elements.append(el)
|
|
||||||
else:
|
|
||||||
# Всё остальное (Title, NarrativeText, ListItem и т.д.)
|
|
||||||
text_elements.append(el)
|
|
||||||
|
|
||||||
return {"text": text_elements, "tables": real_tables}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"📄 Парсинг документа: {Path(PDF_PATH).name}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
parsed_data = parse_document(PDF_PATH)
|
|
||||||
|
|
||||||
print(f"\n[Итоги]")
|
|
||||||
print(f" ✅ Получено текстовых элементов: {len(parsed_data['text'])}")
|
|
||||||
print(f" ✅ Найдено валидных таблиц: {len(parsed_data['tables'])}")
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"document": "ПОЛ-123",
|
|
||||||
"title": "Положение Управление ИТ-инцидентами от 20.02.2024 № ПОЛ-123-2",
|
|
||||||
"text": parsed_data["text"],
|
|
||||||
"tables": parsed_data["tables"],
|
|
||||||
"metadata": {
|
|
||||||
"unstructured_api": UNSTRUCTURED_API,
|
|
||||||
"strategy": "hi_res",
|
|
||||||
"timestamp": "2026-07-06",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Path(OUTPUT_PATH).write_text(json.dumps(result, ensure_ascii=False, indent=2))
|
|
||||||
print(f"\n✅ Сохранено в {OUTPUT_PATH}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+15
-13
@@ -5,37 +5,39 @@ from ldap3 import Server, Connection, ALL, MODIFY_REPLACE
|
|||||||
|
|
||||||
# Подтягиваем конфиг
|
# Подтягиваем конфиг
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASE
|
from config.config import AD_SERVER, AD_USER, AD_PASSWORD
|
||||||
|
from utils.ad_search import search_by_login as ad_search_all_bases
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def upload_photo_to_ad(target_user: str, photo_path: str) -> dict:
|
def upload_photo_to_ad(target_user: str, photo_path: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Читает готовое фото с диска и записывает его в атрибут thumbnailPhoto в AD.
|
Читает готовое фото с диска и записывает его в атрибут thumbnailPhoto в AD.
|
||||||
|
Ищет пользователя по всем OU из AD_BASES.
|
||||||
"""
|
"""
|
||||||
|
from utils.ad_search import search_by_filter
|
||||||
try:
|
try:
|
||||||
# Читаем сырые байты картинки
|
# Читаем сырые байты картинки
|
||||||
with open(photo_path, "rb") as f:
|
with open(photo_path, "rb") as f:
|
||||||
photo_bytes = f.read()
|
photo_bytes = f.read()
|
||||||
|
|
||||||
# Формируем фильтр для поиска
|
# Ищем пользователя в AD по всем OU
|
||||||
if '@' in target_user:
|
if '@' in target_user:
|
||||||
ldap_filter = f"(mail={target_user})"
|
ldap_filter = "(mail={})".format(target_user)
|
||||||
else:
|
else:
|
||||||
ldap_filter = f"(sAMAccountName={target_user})"
|
ldap_filter = "(sAMAccountName={})".format(target_user)
|
||||||
|
|
||||||
# Подключаемся к AD
|
entries = search_by_filter(ldap_filter, ["cn"])
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
return {"success": False, "error": "Пользователь {} не найден в AD.".format(target_user)}
|
||||||
|
|
||||||
|
user_dn = entries[0].entry_dn
|
||||||
|
|
||||||
|
# Подключаемся для модификации
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
server = Server(AD_SERVER, get_info=ALL)
|
||||||
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
||||||
|
|
||||||
# Находим пользователя в дереве (нам нужен его точный путь - DN)
|
|
||||||
conn.search(search_base=AD_BASE, search_filter=ldap_filter, attributes=["cn"])
|
|
||||||
|
|
||||||
if not conn.entries:
|
|
||||||
return {"success": False, "error": f"Пользователь {target_user} не найден в AD."}
|
|
||||||
|
|
||||||
user_dn = conn.entries[0].entry_dn
|
|
||||||
|
|
||||||
# 🪄 МАГИЯ: Перезаписываем атрибут thumbnailPhoto нашими байтами
|
# 🪄 МАГИЯ: Перезаписываем атрибут thumbnailPhoto нашими байтами
|
||||||
conn.modify(user_dn, {'thumbnailPhoto': [(MODIFY_REPLACE, [photo_bytes])]})
|
conn.modify(user_dn, {'thumbnailPhoto': [(MODIFY_REPLACE, [photo_bytes])]})
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from PIL import Image
|
|||||||
|
|
||||||
# Подтягиваем ваши настройки из конфига
|
# Подтягиваем ваши настройки из конфига
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASE
|
from config.config import AD_SERVER, AD_USER, AD_PASSWORD
|
||||||
|
from utils.ad_search import search_by_filter as ad_search_all_bases
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
|
||||||
@@ -21,22 +22,14 @@ def check_user_photo(target_user: str):
|
|||||||
ldap_filter = f"(sAMAccountName={target_user})"
|
ldap_filter = f"(sAMAccountName={target_user})"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Подключаемся к AD
|
# Подключаемся к AD и ищем по всем OU из AD_BASES
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
entries = ad_search_all_bases(ldap_filter, ["cn", "thumbnailPhoto"])
|
||||||
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
|
||||||
|
|
||||||
# Ищем пользователя и просим вернуть атрибуты: имя (cn) и фото (thumbnailPhoto)
|
if not entries:
|
||||||
conn.search(
|
|
||||||
search_base=AD_BASE,
|
|
||||||
search_filter=ldap_filter,
|
|
||||||
attributes=["cn", "thumbnailPhoto"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not conn.entries:
|
|
||||||
print("❌ Пользователь с такими данными не найден в AD.")
|
print("❌ Пользователь с такими данными не найден в AD.")
|
||||||
return
|
return
|
||||||
|
|
||||||
user = conn.entries[0]
|
user = entries[0]
|
||||||
user_name = user.cn.value if 'cn' in user else target_user
|
user_name = user.cn.value if 'cn' in user else target_user
|
||||||
print(f"👤 Найден сотрудник: {user_name}")
|
print(f"👤 Найден сотрудник: {user_name}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,289 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Универсальный скрипт для улучшения MD-файлов документов АО «ХК «Сибцем».
|
|
||||||
|
|
||||||
Применяет паттерны:
|
|
||||||
1. Удаляет футеры страниц
|
|
||||||
2. Форматирует блок "УТВЕРЖДАЮ"
|
|
||||||
3. Выравнивает иерархию заголовков
|
|
||||||
4. Объединяет таблицы терминов
|
|
||||||
5. Добавляет разделители между разделами
|
|
||||||
6. Приводит таблицы к единому виду
|
|
||||||
|
|
||||||
Использование:
|
|
||||||
python3 optimize_document.py <путь_к_оригиналу_на_сервере>
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
python3 optimize_document.py "/opt/documents_xk/АОХКСибцем/07 Служба.../08 Положение Управление ИТ-инцидентами от 20.02.2024 № ПОЛ-123-2.md"
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
# Конфигурация
|
|
||||||
SSH_KEY = "/home/hermes/.ssh/192.168.1.106"
|
|
||||||
SSH_USER = "administrator@192.168.1.106"
|
|
||||||
SERVER_BASE = "/opt/documents_xk/АОХКСибцем"
|
|
||||||
FILEBROWSER_URL = "https://smb.dddennnisss.ru"
|
|
||||||
FILEBROWSER_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGaWxlQnJvd3NlciBRdWFudHVtIiwiZXhwIjoyMzAyNzM1NzA3LCJpYXQiOjE3ODQzMzU3MDcsImJlbG9uZ3NUbyI6MSwiUGVybWlzc2lvbnMiOnsiYXBpIjp0cnVlLCJhZG1pbiI6dHJ1ZSwibW9kaWZ5Ijp0cnVlLCJzaGFyZSI6dHJ1ZSwicmVhbHRpbWUiOmZhbHNlLCJkZWxldGUiOnRydWUsImNyZWF0ZSI6dHJ1ZSwiZG93bmxvYWQiOnRydWV9fQ.QBKlh3NQJeh4aeMiKLkhg0tF8hxk-Oh2KLsXLSbuvm8"
|
|
||||||
SOURCE_NAME = "SSD-Storage"
|
|
||||||
|
|
||||||
|
|
||||||
def run_ssh(command):
|
|
||||||
"""Выполнить команду на сервере через SSH."""
|
|
||||||
result = subprocess.run(
|
|
||||||
["ssh", "-i", SSH_KEY, "-o", "StrictHostKeyChecking=no",
|
|
||||||
SSH_USER, command],
|
|
||||||
capture_output=True, text=True, timeout=30
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def scp_download(remote_path, local_path):
|
|
||||||
"""Скачать файл с сервера."""
|
|
||||||
result = subprocess.run(
|
|
||||||
["scp", "-i", SSH_KEY, "-o", "StrictHostKeyChecking=no",
|
|
||||||
f"{SSH_USER}:{remote_path}", local_path],
|
|
||||||
capture_output=True, text=True, timeout=60
|
|
||||||
)
|
|
||||||
return result.returncode == 0
|
|
||||||
|
|
||||||
|
|
||||||
def fix_headings(content):
|
|
||||||
"""Выровнять иерархию заголовков: все на уровень ##."""
|
|
||||||
content = re.sub(r'^(#+)\s+(.+)$', r'## \2', content, flags=re.MULTILINE)
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def add_dividers(content):
|
|
||||||
"""Добавить разделители между основными разделами."""
|
|
||||||
# Разделитель перед Оглавлением
|
|
||||||
content = re.sub(
|
|
||||||
r'(\n)(## )(Оглавление)',
|
|
||||||
r'\1\n---\n\1\2\3',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
# Разделители перед разделами 1-9
|
|
||||||
content = re.sub(
|
|
||||||
r'(\n)(## )(\s*(?:1\.|2\.|3\.|4\.|5\.|6\.|7\.|8\.|9\.))',
|
|
||||||
r'\1\n---\n\1\2\3',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
# Разделители перед Приложениями
|
|
||||||
content = re.sub(
|
|
||||||
r'(\n)(## )(Приложение)',
|
|
||||||
r'\1\n---\n\1\2\3',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
# Разделитель перед Листом согласований
|
|
||||||
content = re.sub(
|
|
||||||
r'(\n)(## )(Лист согласований)',
|
|
||||||
r'\1\n---\n\1\2\3',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def fix_tables(content):
|
|
||||||
"""Привести все таблицы к единому виду с thead/tbody."""
|
|
||||||
def fix_table(table):
|
|
||||||
if '<thead>' in table and '<tbody>' in table:
|
|
||||||
return table
|
|
||||||
rows = re.findall(r'<tr>(.*?)</tr>', table, re.DOTALL)
|
|
||||||
if not rows:
|
|
||||||
return table
|
|
||||||
|
|
||||||
header = '<thead><tr>' + rows[0] + '</tr></thead><tbody>'
|
|
||||||
body = ''
|
|
||||||
for row in rows[1:]:
|
|
||||||
body += '<tr>' + row + '</tr>'
|
|
||||||
body += '</tbody>'
|
|
||||||
|
|
||||||
return '<table>' + header + body + '</table>'
|
|
||||||
|
|
||||||
return re.sub(r'<table>.*?</table>', lambda m: fix_table(m.group(0)), content, flags=re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_footers(content):
|
|
||||||
"""Удалить футеры страниц."""
|
|
||||||
# Удалить "АО «ХК «Сибцем»\nТип документа: ...\nНаименование документа: ..."
|
|
||||||
content = re.sub(
|
|
||||||
r'АО «ХК «Сибцем»\nТип документа:.*?\nНаименование документа:.*?\n\n',
|
|
||||||
'',
|
|
||||||
content,
|
|
||||||
flags=re.DOTALL
|
|
||||||
)
|
|
||||||
# Удалить "Стр. X из Y"
|
|
||||||
content = re.sub(r'Стр\. \d+ из \d+\n\n', '', content)
|
|
||||||
# Удалить "Дата утверждения: DD.MM.YYYY"
|
|
||||||
content = re.sub(r'Дата утверждения: \d{2}\.\d{2}\.\d{4}\n\n', '', content)
|
|
||||||
# Удалить "Ведущее подразделение: ..."
|
|
||||||
content = re.sub(r'Ведущее подразделение:.*?\n\n', '', content)
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def fix_approval_block(content):
|
|
||||||
"""Форматировать блок УТВЕРЖДАЮ."""
|
|
||||||
# Паттерн 1: "УТВЕРЖДАЮ\nПрезидент\n\nО.В. Шарыкин\n\n«XX» месяц год г."
|
|
||||||
approval_patterns = [
|
|
||||||
r'(УТВЕРЖДАУ\nПрезидент\n\nО\.В\. Шарыкин\n\n«\d+» \w+ \d{4} г\.)',
|
|
||||||
r'(УТВЕРЖДАУ\nПрезидент\n\nО\.В\. Шарыкин\n\n«\d+» \w+ \d{4} г\.)',
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in approval_patterns:
|
|
||||||
match = re.search(pattern, content)
|
|
||||||
if match:
|
|
||||||
old_text = match.group(0)
|
|
||||||
new_text = f'''<div align="right">
|
|
||||||
<strong>УТВЕРЖДАУ</strong><br>
|
|
||||||
Президент<br>
|
|
||||||
О.В. Шарыкин<br>
|
|
||||||
{old_text.split("«")[1].split("г.")[0]} г.
|
|
||||||
</div>'''
|
|
||||||
content = content.replace(old_text, new_text)
|
|
||||||
break
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def merge_terms_tables(content):
|
|
||||||
"""Объединить разбитые таблицы терминов."""
|
|
||||||
# Поиск таблиц терминов
|
|
||||||
terms_pattern = r'(<table>.*?Термины.*?</table>)'
|
|
||||||
matches = re.findall(terms_pattern, content, re.DOTALL)
|
|
||||||
|
|
||||||
if len(matches) > 1:
|
|
||||||
# Объединяем все таблицы терминов в одну
|
|
||||||
merged = '<table><thead><tr><th>Термин</th><th>Определение</th></tr></thead><tbody>'
|
|
||||||
for table in matches:
|
|
||||||
rows = re.findall(r'<tr>(.*?)</tr>', table, re.DOTALL)
|
|
||||||
for row in rows[1:]: # Пропускаем заголовок
|
|
||||||
merged += row
|
|
||||||
merged += '</tbody></table>'
|
|
||||||
content = content.replace(matches[0], merged)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def optimize_document(md_path):
|
|
||||||
"""Основная функция оптимизации документа."""
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
print(f"📄 Обработка документа: {md_path}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Шаг 1: Скачать MD с сервера
|
|
||||||
print("\n📥 Шаг 1: Скачивание MD...")
|
|
||||||
result = run_ssh(f"cat '{md_path}'")
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"✗ Ошибка при скачивании: {result.stderr}")
|
|
||||||
return
|
|
||||||
|
|
||||||
md_content = result.stdout
|
|
||||||
print(f"✓ MD скачан: {len(md_content)} символов")
|
|
||||||
|
|
||||||
# Шаг 2: Применить паттерны
|
|
||||||
print("\n🔧 Шаг 2: Применение паттернов...")
|
|
||||||
|
|
||||||
# 2.1 Удалить футеры
|
|
||||||
content = remove_footers(md_content)
|
|
||||||
print(" ✓ Футеры удалены")
|
|
||||||
|
|
||||||
# 2.2 Выровнять иерархию заголовков
|
|
||||||
content = fix_headings(content)
|
|
||||||
print(" ✓ Иерархия заголовков выровнена")
|
|
||||||
|
|
||||||
# 2.3 Добавить разделители
|
|
||||||
content = add_dividers(content)
|
|
||||||
print(" ✓ Разделители добавлены")
|
|
||||||
|
|
||||||
# 2.4 Привести таблицы к единому виду
|
|
||||||
content = fix_tables(content)
|
|
||||||
print(" ✓ Таблицы приведены к единому виду")
|
|
||||||
|
|
||||||
# 2.5 Объединить таблицы терминов
|
|
||||||
content = merge_terms_tables(content)
|
|
||||||
print(" ✓ Таблицы терминов объединены")
|
|
||||||
|
|
||||||
# 2.6 Форматировать блок УТВЕРЖДАЮ
|
|
||||||
content = fix_approval_block(content)
|
|
||||||
print(" ✓ Блок УТВЕРЖДАЮ отформатирован")
|
|
||||||
|
|
||||||
# Сохранить улучшенную версию
|
|
||||||
improved_path = "/tmp/improved_document.md"
|
|
||||||
with open(improved_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print(f"\n📊 Результаты:")
|
|
||||||
print(f" Исходный размер: {len(md_content)} символов")
|
|
||||||
print(f" Улучшенный размер: {len(content)} символов")
|
|
||||||
|
|
||||||
# Показать структуру
|
|
||||||
sections = re.findall(r'^(##)\s+(.+)$', content, re.MULTILINE)
|
|
||||||
print(f" Разделов: {len(sections)}")
|
|
||||||
|
|
||||||
tables = re.findall(r'<table>', content)
|
|
||||||
print(f" Таблиц: {len(tables)}")
|
|
||||||
|
|
||||||
# Показать первые 300 символов
|
|
||||||
print(f"\n📝 Начало файла:")
|
|
||||||
print(content[:300].replace('\n', '\\n'))
|
|
||||||
|
|
||||||
# Загрузить в filebrowser
|
|
||||||
print("\n📤 Шаг 3: Загрузка в filebrowser...")
|
|
||||||
upload_to_filebrowser(content, md_path)
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
print(f"\n{'=' * 60}")
|
|
||||||
print(f"✅ Готово! Время выполнения: {elapsed:.1f} сек")
|
|
||||||
print(f"{'=' * 60}")
|
|
||||||
|
|
||||||
|
|
||||||
def upload_to_filebrowser(content, md_path):
|
|
||||||
"""Загрузить файл в filebrowser."""
|
|
||||||
# Извлечь имя файла из пути
|
|
||||||
filename = md_path.split('/')[-1]
|
|
||||||
# Добавить "_improved" перед расширением
|
|
||||||
if filename.endswith('.md'):
|
|
||||||
improved_filename = filename[:-3] + '_improved.md'
|
|
||||||
else:
|
|
||||||
improved_filename = filename + '_improved'
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
session.cookies.set("filebrowser_quantum_jwt", FILEBROWSER_TOKEN, domain="smb.dddennnisss.ru")
|
|
||||||
|
|
||||||
url = f"{FILEBROWSER_URL}/api/resources"
|
|
||||||
params = {"path": improved_filename, "source": SOURCE_NAME}
|
|
||||||
|
|
||||||
r = session.put(url, params=params, data=content)
|
|
||||||
|
|
||||||
if r.status_code == 200:
|
|
||||||
print(f"✓ Файл загружен: {improved_filename}")
|
|
||||||
|
|
||||||
# Проверить
|
|
||||||
r = session.get(f"{FILEBROWSER_URL}/api/resources", params={"path": "/", "source": SOURCE_NAME})
|
|
||||||
if r.status_code == 200:
|
|
||||||
data = r.json()
|
|
||||||
items = data.get("files", []) + data.get("folders", [])
|
|
||||||
for item in items:
|
|
||||||
if improved_filename in item["name"]:
|
|
||||||
print(f"✓ Размер: {item['size']} байт")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print(f"✗ Ошибка загрузки: {r.status_code} - {r.text[:200]}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Использование:")
|
|
||||||
print(" python3 optimize_document.py <путь_к_оригиналу_на_сервере>")
|
|
||||||
print("\nПример:")
|
|
||||||
print(" python3 optimize_document.py \"/opt/documents_xk/АОХКСибцем/07 Служба.../08 Положение Управление ИТ-инцидентами от 20.02.2024 № ПОЛ-123-2.md\"")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
md_path = sys.argv[1]
|
|
||||||
optimize_document(md_path)
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Универсальный скрипт для улучшения MD-файлов документов АО «ХК «Сибцем».
|
|
||||||
|
|
||||||
Выполняется на сервере 192.168.1.106.
|
|
||||||
Применяет паттерны:
|
|
||||||
1. Удаляет футеры страниц
|
|
||||||
2. Форматирует блок "УТВЕРЖДАЮ"
|
|
||||||
3. Выравнивает иерархию заголовков
|
|
||||||
4. Объединяет таблицы терминов
|
|
||||||
5. Добавляет разделители между разделами
|
|
||||||
6. Приводит таблицы к единому виду
|
|
||||||
|
|
||||||
Использование:
|
|
||||||
python3 /opt/trueconf_bot/search_bot/optimize_documents.py <путь_к_каталогу>
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
python3 /opt/trueconf_bot/search_bot/optimize_documents.py "/opt/documents_xk/ready_md/АОХКСибцем/07 Служба Вице-президента по экономике и финансам/ДИТ/02 Регламенты деятельности/"
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Конфигурация
|
|
||||||
FILEBROWSER_URL = "https://smb.dddennnisss.ru"
|
|
||||||
FILEBROWSER_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGaWxlQnJvd3NlciBRdWFudHVtIiwiZXhwIjoyMzAyNzM1NzA3LCJpYXQiOjE3ODQzMzU3MDcsImJlbG9uZ3NUbyI6MSwiUGVybWlzc2lvbnMiOnsiYXBpIjp0cnVlLCJhZG1pbiI6dHJ1ZSwibW9kaWZ5Ijp0cnVlLCJzaGFyZSI6dHJ1ZSwicmVhbHRpbWUiOmZhbHNlLCJkZWxldGUiOnRydWUsImNyZWF0ZSI6dHJ1ZSwiZG93bmxvYWQiOnRydWV9fQ.QBKlh3NQJeh4aeMiKLkhg0tF8hxk-Oh2KLsXLSbuvm8"
|
|
||||||
SOURCE_NAME = "SSD-Storage"
|
|
||||||
|
|
||||||
|
|
||||||
def remove_footers(content):
|
|
||||||
"""Удалить футеры страниц."""
|
|
||||||
# Удалить "АО «ХК «Сибцем»\nТип документа: ...\nНаименование документа: ..."
|
|
||||||
content = re.sub(
|
|
||||||
r'АО «ХК «Сибцем»\nТип документа:.*?\nНаименование документа:.*?\n\n',
|
|
||||||
'',
|
|
||||||
content,
|
|
||||||
flags=re.DOTALL
|
|
||||||
)
|
|
||||||
# Удалить "Стр. X из Y"
|
|
||||||
content = re.sub(r'Стр\. \d+ из \d+\n\n', '', content)
|
|
||||||
# Удалить "Дата утверждения: DD.MM.YYYY"
|
|
||||||
content = re.sub(r'Дата утверждения: \d{2}\.\d{2}\.\d{4}\n\n', '', content)
|
|
||||||
# Удалить "Ведущее подразделение: ..."
|
|
||||||
content = re.sub(r'Ведущее подразделение:.*?\n\n', '', content)
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def fix_headings(content):
|
|
||||||
"""Выровнять иерархию заголовков: все на уровень ##."""
|
|
||||||
content = re.sub(r'^(#+)\s+(.+)$', r'## \2', content, flags=re.MULTILINE)
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def add_dividers(content):
|
|
||||||
"""Добавить разделители между основными разделами."""
|
|
||||||
# Разделитель перед Оглавлением
|
|
||||||
content = re.sub(
|
|
||||||
r'(\n)(## )(Оглавление)',
|
|
||||||
r'\1\n---\n\1\2\3',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
# Разделители перед разделами 1-9
|
|
||||||
content = re.sub(
|
|
||||||
r'(\n)(## )(\s*(?:1\.|2\.|3\.|4\.|5\.|6\.|7\.|8\.|9\.))',
|
|
||||||
r'\1\n---\n\1\2\3',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
# Разделители перед Приложениями
|
|
||||||
content = re.sub(
|
|
||||||
r'(\n)(## )(Приложение)',
|
|
||||||
r'\1\n---\n\1\2\3',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
# Разделитель перед Листом согласований
|
|
||||||
content = re.sub(
|
|
||||||
r'(\n)(## )(Лист согласований)',
|
|
||||||
r'\1\n---\n\1\2\3',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def fix_tables(content):
|
|
||||||
"""Привести все таблицы к единому виду с thead/tbody."""
|
|
||||||
def fix_table(table):
|
|
||||||
if '<thead>' in table and '<tbody>' in table:
|
|
||||||
return table
|
|
||||||
rows = re.findall(r'<tr>(.*?)</tr>', table, re.DOTALL)
|
|
||||||
if not rows:
|
|
||||||
return table
|
|
||||||
|
|
||||||
header = '<thead><tr>' + rows[0] + '</tr></thead><tbody>'
|
|
||||||
body = ''
|
|
||||||
for row in rows[1:]:
|
|
||||||
body += '<tr>' + row + '</tr>'
|
|
||||||
body += '</tbody>'
|
|
||||||
|
|
||||||
return '<table>' + header + body + '</table>'
|
|
||||||
|
|
||||||
return re.sub(r'<table>.*?</table>', lambda m: fix_table(m.group(0)), content, flags=re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
def merge_terms_tables(content):
|
|
||||||
"""Объединить разбитые таблицы терминов."""
|
|
||||||
# Поиск таблиц терминов
|
|
||||||
terms_pattern = r'(<table>.*?Термины.*?</table>)'
|
|
||||||
matches = re.findall(terms_pattern, content, re.DOTALL)
|
|
||||||
|
|
||||||
if len(matches) > 1:
|
|
||||||
# Объединяем все таблицы терминов в одну
|
|
||||||
merged = '<table><thead><tr><th>Термин</th><th>Определение</th></tr></thead><tbody>'
|
|
||||||
for table in matches:
|
|
||||||
rows = re.findall(r'<tr>(.*?)</tr>', table, re.DOTALL)
|
|
||||||
for row in rows[1:]: # Пропускаем заголовок
|
|
||||||
merged += row
|
|
||||||
merged += '</tbody></table>'
|
|
||||||
content = content.replace(matches[0], merged)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def fix_approval_block(content):
|
|
||||||
"""Форматировать блок УТВЕРЖДАЮ."""
|
|
||||||
# Паттерн 1: "УТВЕРЖДАЮ\nПрезидент\n\nО.В. Шарыкин\n\n«XX» месяц год г."
|
|
||||||
approval_patterns = [
|
|
||||||
r'(УТВЕРЖДАУ\nПрезидент\n\nО\.В\. Шарыкин\n\n«\d+» \w+ \d{4} г\.)',
|
|
||||||
r'(УТВЕРЖДАУ\nПрезидент\n\nО\.В\. Шарыкин\n\n«\d+» \w+ \d{4} г\.)',
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in approval_patterns:
|
|
||||||
match = re.search(pattern, content)
|
|
||||||
if match:
|
|
||||||
old_text = match.group(0)
|
|
||||||
new_text = f'''<div align="right">
|
|
||||||
<strong>УТВЕРЖДАУ</strong><br>
|
|
||||||
Президент<br>
|
|
||||||
О.В. Шарыкин<br>
|
|
||||||
{old_text.split("«")[1].split("г.")[0]} г.
|
|
||||||
</div>'''
|
|
||||||
content = content.replace(old_text, new_text)
|
|
||||||
break
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def upload_to_filebrowser(content, filename):
|
|
||||||
"""Загрузить файл в filebrowser."""
|
|
||||||
# Добавить "_improved" перед расширением
|
|
||||||
if filename.endswith('.md'):
|
|
||||||
improved_filename = filename[:-3] + '_improved.md'
|
|
||||||
else:
|
|
||||||
improved_filename = filename + '_improved'
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
session.cookies.set("filebrowser_quantum_jwt", FILEBROWSER_TOKEN, domain="smb.dddennnisss.ru")
|
|
||||||
|
|
||||||
url = f"{FILEBROWSER_URL}/api/resources"
|
|
||||||
params = {"path": improved_filename, "source": SOURCE_NAME}
|
|
||||||
|
|
||||||
r = session.put(url, params=params, data=content)
|
|
||||||
|
|
||||||
if r.status_code == 200:
|
|
||||||
print(f"✓ Файл загружен: {improved_filename}")
|
|
||||||
|
|
||||||
# Проверить
|
|
||||||
r = session.get(f"{FILEBROWSER_URL}/api/resources", params={"path": "/", "source": SOURCE_NAME})
|
|
||||||
if r.status_code == 200:
|
|
||||||
data = r.json()
|
|
||||||
items = data.get("files", []) + data.get("folders", [])
|
|
||||||
for item in items:
|
|
||||||
if improved_filename in item["name"]:
|
|
||||||
print(f"✓ Размер: {item['size']} байт")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print(f"✗ Ошибка загрузки: {r.status_code} - {r.text[:200]}")
|
|
||||||
|
|
||||||
return improved_filename
|
|
||||||
|
|
||||||
|
|
||||||
def optimize_document(md_path):
|
|
||||||
"""Основная функция оптимизации документа."""
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
print(f"\n📄 Обработка документа: {md_path.split('/')[-1]}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Шаг 1: Читать MD файл
|
|
||||||
print("\n📥 Шаг 1: Чтение MD...")
|
|
||||||
try:
|
|
||||||
with open(md_path, 'r', encoding='utf-8') as f:
|
|
||||||
md_content = f.read()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Ошибка при чтении: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"✓ MD прочитан: {len(md_content)} символов")
|
|
||||||
|
|
||||||
# Шаг 2: Применить паттерны
|
|
||||||
print("\n🔧 Шаг 2: Применение паттернов...")
|
|
||||||
|
|
||||||
# 2.1 Удалить футеры
|
|
||||||
content = remove_footers(md_content)
|
|
||||||
print(" ✓ Футеры удалены")
|
|
||||||
|
|
||||||
# 2.2 Выровнять иерархию заголовков
|
|
||||||
content = fix_headings(content)
|
|
||||||
print(" ✓ Иерархия заголовков выровнена")
|
|
||||||
|
|
||||||
# 2.3 Добавить разделители
|
|
||||||
content = add_dividers(content)
|
|
||||||
print(" ✓ Разделители добавлены")
|
|
||||||
|
|
||||||
# 2.4 Привести таблицы к единому виду
|
|
||||||
content = fix_tables(content)
|
|
||||||
print(" ✓ Таблицы приведены к единому виду")
|
|
||||||
|
|
||||||
# 2.5 Объединить таблицы терминов
|
|
||||||
content = merge_terms_tables(content)
|
|
||||||
print(" ✓ Таблицы терминов объединены")
|
|
||||||
|
|
||||||
# 2.6 Форматировать блок УТВЕРЖДАЮ
|
|
||||||
content = fix_approval_block(content)
|
|
||||||
print(" ✓ Блок УТВЕРЖДАУ отформатирован")
|
|
||||||
|
|
||||||
# Сохранить улучшенную версию
|
|
||||||
improved_path = md_path.replace('.md', '_improved.md')
|
|
||||||
with open(improved_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print(f"\n📊 Результаты:")
|
|
||||||
print(f" Исходный размер: {len(md_content)} символов")
|
|
||||||
print(f" Улучшенный размер: {len(content)} символов")
|
|
||||||
|
|
||||||
# Показать структуру
|
|
||||||
sections = re.findall(r'^(##)\s+(.+)$', content, re.MULTILINE)
|
|
||||||
print(f" Разделов: {len(sections)}")
|
|
||||||
|
|
||||||
tables = re.findall(r'<table>', content)
|
|
||||||
print(f" Таблиц: {len(tables)}")
|
|
||||||
|
|
||||||
# Показать первые 300 символов
|
|
||||||
print(f"\n📝 Начало файла:")
|
|
||||||
print(content[:300].replace('\n', '\\n'))
|
|
||||||
|
|
||||||
# Загрузить в filebrowser
|
|
||||||
print("\n📤 Шаг 3: Загрузка в filebrowser...")
|
|
||||||
filename = md_path.split('/')[-1]
|
|
||||||
upload_to_filebrowser(content, filename)
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
print(f"\n{'=' * 60}")
|
|
||||||
print(f"✅ Готово! Время выполнения: {elapsed:.1f} сек")
|
|
||||||
print(f"{'=' * 60}")
|
|
||||||
|
|
||||||
|
|
||||||
def process_directory(directory):
|
|
||||||
"""Обработать все MD файлы в каталоге."""
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
print(f"📂 Обработка каталога: {directory}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Найти все MD файлы
|
|
||||||
md_files = []
|
|
||||||
for root, dirs, files in os.walk(directory):
|
|
||||||
for file in files:
|
|
||||||
if file.endswith('.md'):
|
|
||||||
md_files.append(os.path.join(root, file))
|
|
||||||
|
|
||||||
print(f"Найдено {len(md_files)} MD файлов")
|
|
||||||
|
|
||||||
# Обработать каждый файл
|
|
||||||
for md_path in md_files:
|
|
||||||
try:
|
|
||||||
optimize_document(md_path)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Ошибка при обработке {md_path}: {e}")
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
print(f"\n{'=' * 60}")
|
|
||||||
print(f"✅ Обработка каталога завершена! Общее время: {elapsed:.1f} сек")
|
|
||||||
print(f"{'=' * 60}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Использование:")
|
|
||||||
print(" python3 optimize_documents.py <путь_к_каталогу>")
|
|
||||||
print("\nПример:")
|
|
||||||
print(' python3 optimize_documents.py "/opt/documents_xk/ready_md/АОХКСибцем/07 Служба Вице-президента по экономике и финансам/ДИТ/02 Регламенты деятельности/"')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
directory = sys.argv[1]
|
|
||||||
process_directory(directory)
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Комбинированный парсинг ПОЛ-177:
|
|
||||||
- Текст: Unstructured API (192.168.1.103:8005)
|
|
||||||
- Таблицы: pypdf (локально)
|
|
||||||
|
|
||||||
Сохраняет результат в /opt/okf-regulations/concepts/pol177_parsed.json
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# === НАСТРОЙКИ ===
|
|
||||||
UNSTRUCTURED_API = "http://192.168.1.103:8005/general/v0/general"
|
|
||||||
PDF_PATH = "/opt/documents_xk/АОХКСибцем/07 Служба Вице-президента по экономике и финансам/ДИТ/02 Регламенты деятельности/10 Положение Хранение электронных документов от 03.04.2024 № ПОЛ-177.pdf"
|
|
||||||
OUTPUT_PATH = "/opt/okf-regulations/concepts/pol177_parsed.json"
|
|
||||||
|
|
||||||
def fetch_unstructured(pdf_path: str) -> list[dict]:
|
|
||||||
"""Получаем элементы текста из Unstructured API (103)."""
|
|
||||||
cmd = [
|
|
||||||
"curl", "-X", "POST", UNSTRUCTURED_API,
|
|
||||||
"-F", f"files=@{pdf_path}",
|
|
||||||
"-F", "strategy=fast",
|
|
||||||
"-F", "output_format=json",
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"❌ Unstructured API error: {result.stderr}")
|
|
||||||
return []
|
|
||||||
return json.loads(result.stdout)
|
|
||||||
|
|
||||||
def extract_tables_pypdf(pdf_path: str) -> list[dict]:
|
|
||||||
"""Извлекаем таблицы через pypdf (локально на 106)."""
|
|
||||||
try:
|
|
||||||
from pypdf import PdfReader
|
|
||||||
except ImportError:
|
|
||||||
import subprocess
|
|
||||||
subprocess.run(["pip3", "install", "pypdf", "--user"], check=True)
|
|
||||||
from pypdf import PdfReader
|
|
||||||
|
|
||||||
reader = PdfReader(pdf_path)
|
|
||||||
tables = []
|
|
||||||
for i, page in enumerate(reader.pages):
|
|
||||||
text = page.extract_text()
|
|
||||||
if text and ("таблица" in text.lower() or "стр" in text.lower()):
|
|
||||||
tables.append({"page": i + 1, "text": text})
|
|
||||||
return tables
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print("📄 Парсинг ПОЛ-177")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# 1. Текст через Unstructured API
|
|
||||||
print("\n[1/2] Запрос текста через Unstructured API (103)...")
|
|
||||||
elements = fetch_unstructured(PDF_PATH)
|
|
||||||
print(f" ✅ Получено {len(elements)} элементов")
|
|
||||||
|
|
||||||
# 2. Таблицы через pypdf
|
|
||||||
print("\n[2/2] Извлечение таблиц через pypdf (106)...")
|
|
||||||
tables = extract_tables_pypdf(PDF_PATH)
|
|
||||||
print(f" ✅ Найдено {len(tables)} страниц с текстом")
|
|
||||||
|
|
||||||
# 3. Сохраняем комбинированный результат
|
|
||||||
result = {
|
|
||||||
"document": "ПОЛ-177",
|
|
||||||
"title": "Положение Хранение электронных документов от 03.04.2024 № ПОЛ-177",
|
|
||||||
"unstructured": elements,
|
|
||||||
"tables": tables,
|
|
||||||
"metadata": {
|
|
||||||
"unstructured_api": UNSTRUCTURED_API,
|
|
||||||
"strategy": "fast",
|
|
||||||
"timestamp": "2026-07-06",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Path(OUTPUT_PATH).write_text(json.dumps(result, ensure_ascii=False, indent=2))
|
|
||||||
print(f"\n✅ Сохранено в {OUTPUT_PATH}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+13
-107
@@ -67,61 +67,15 @@ logger = logging.getLogger(__name__)
|
|||||||
router = Router()
|
router = Router()
|
||||||
sd_sessions = {}
|
sd_sessions = {}
|
||||||
|
|
||||||
async def get_sd_templates():
|
|
||||||
url = f"{SD_URL}/api/v3/request_templates"
|
|
||||||
headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json", "Content-Type": "application/json"}
|
|
||||||
input_data = {"list_info": {"row_count": 100, "start_index": 1}}
|
|
||||||
params = {"input_data": json.dumps(input_data)}
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient(verify=False) as client:
|
|
||||||
response = await client.get(url, headers=headers, params=params, timeout=30.0)
|
|
||||||
if response.status_code != 200:
|
|
||||||
sd_workflow_logger.error(f"Ошибка API шаблонов: HTTP {response.status_code} - {response.text}")
|
|
||||||
return []
|
|
||||||
data = response.json()
|
|
||||||
all_templates = data.get("request_templates", [])
|
|
||||||
pre_filtered = [
|
|
||||||
t for t in all_templates
|
|
||||||
if t.get("status") == "ACTIVE"
|
|
||||||
and not t.get("inactive", False)
|
|
||||||
and str(t.get("id")) != "2"
|
|
||||||
]
|
|
||||||
sd_workflow_logger.info(f"Шаблонов в системе: {len(all_templates)} | Активных: {len(pre_filtered)}")
|
|
||||||
async def check_template(t):
|
|
||||||
try:
|
|
||||||
t_resp = await client.get(url + "/" + str(t["id"]), headers=headers, timeout=15.0)
|
|
||||||
if t_resp.status_code == 200:
|
|
||||||
detailed = t_resp.json().get("request_template", {})
|
|
||||||
if detailed.get("show_to_requester") is True:
|
|
||||||
sc = detailed.get("service_category", {})
|
|
||||||
t["service_category"] = sc
|
|
||||||
t["request"] = detailed.get("request", {})
|
|
||||||
return t if detailed.get("show_to_requester") is True else None
|
|
||||||
except Exception as e:
|
|
||||||
sd_workflow_logger.warning(f"Ошибка деталей шаблона ID {t['id']}: {e}")
|
|
||||||
return None
|
|
||||||
tasks = [check_template(t) for t in pre_filtered]
|
|
||||||
results = await asyncio.gather(*tasks)
|
|
||||||
active_templates = [t for t in results if t is not None]
|
|
||||||
sd_workflow_logger.info(f"Шаблоны с show_to_requester=True: {len(active_templates)}")
|
|
||||||
for temp in active_templates:
|
|
||||||
sd_workflow_logger.info(f" ID: {temp['id']} | Name: {temp['name']}")
|
|
||||||
return active_templates
|
|
||||||
except Exception as e:
|
|
||||||
sd_workflow_logger.error(f"Ошибка загрузки шаблонов: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
# ================================================
|
# ================================================
|
||||||
# БИЗНЕС-ЛОГИКА
|
# БИЗНЕС-ЛОГИКА
|
||||||
# ================================================
|
# ================================================
|
||||||
def get_ad_user_sync(login: str):
|
def get_ad_user_sync(login: str):
|
||||||
try:
|
try:
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
from utils.ad_search import search_by_login
|
||||||
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
entries = search_by_login(login, ["displayName", "mail", "l", "userAccountControl"])
|
||||||
conn.search(search_base=AD_BASE, search_filter=f"(sAMAccountName={login})", attributes=["displayName", "mail", "l", "userAccountControl"])
|
if entries:
|
||||||
if conn.entries:
|
user = entries[0]
|
||||||
user = conn.entries[0]
|
|
||||||
uac = user.userAccountControl.value if 'userAccountControl' in user else 0
|
uac = user.userAccountControl.value if 'userAccountControl' in user else 0
|
||||||
return {
|
return {
|
||||||
"name": user.displayName.value if 'displayName' in user else login,
|
"name": user.displayName.value if 'displayName' in user else login,
|
||||||
@@ -173,36 +127,18 @@ async def generate_smart_subject(text: str) -> str:
|
|||||||
sd_workflow_logger.error(f"📝 [Subject Gen Error] Exception: {e}\n{traceback.format_exc()}")
|
sd_workflow_logger.error(f"📝 [Subject Gen Error] Exception: {e}\n{traceback.format_exc()}")
|
||||||
return "Заявка из КЛЕВЕР"
|
return "Заявка из КЛЕВЕР"
|
||||||
|
|
||||||
async def create_ticket_in_sd(requester_email: str, subject: str, description: str, city: str, template_id: str | None = None, templates: list | None = None):
|
async def create_ticket_in_sd(requester_email: str, subject: str, description: str, city: str):
|
||||||
headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json", "Content-Type": "application/json"}
|
headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json"}
|
||||||
endpoint = f"{SD_URL}/api/v3/requests"
|
endpoint = f"{SD_URL}/api/v3/requests"
|
||||||
html_desc = f"<p>{description.replace(chr(10), '<br>')}</p><br><hr><p style='color:#555;font-size:12px;'><i>Создано через TrueConf КЛЕВЕР</i></p>"
|
html_desc = f"<p>{description.replace(chr(10), '<br>')}</p><br><hr><p style='color:#555;font-size:12px;'><i>Создано через TrueConf КЛЕВЕР</i></p>"
|
||||||
async def _send_req(email, current_city, template_id, templates):
|
async def _send_req(email, current_city):
|
||||||
req_payload = {"request": {"subject": subject, "description": html_desc, "requester": {"name": email}}}
|
payload = {"request": {"subject": subject, "description": html_desc, "requester": {"email_id": email}, "udf_fields": {"udf_pick_301": current_city}}}
|
||||||
if template_id:
|
|
||||||
req_payload["request"]["template"] = {"id": template_id}
|
|
||||||
# Найти service_category по шаблону
|
|
||||||
for t in templates:
|
|
||||||
if str(t.get("id")) == str(template_id):
|
|
||||||
sc = t.get("service_category", {})
|
|
||||||
if sc:
|
|
||||||
req_payload["request"]["service_category"] = sc
|
|
||||||
break
|
|
||||||
if "udf_pick_301" in req_payload["request"]:
|
|
||||||
req_payload["request"]["udf_fields"] = {"udf_pick_301": current_city}
|
|
||||||
elif "udf_fields" in req_payload["request"] and current_city:
|
|
||||||
req_payload["request"]["udf_fields"]["udf_pick_301"] = current_city
|
|
||||||
else:
|
|
||||||
req_payload["request"]["udf_fields"] = {"udf_pick_301": current_city}
|
|
||||||
async with httpx.AsyncClient(verify=False) as client:
|
async with httpx.AsyncClient(verify=False) as client:
|
||||||
resp = await client.post(endpoint, headers=headers, params={"input_data": json.dumps(req_payload)}, timeout=15.0)
|
return await client.post(endpoint, headers=headers, data={"input_data": json.dumps(payload)}, timeout=15.0)
|
||||||
if resp.status_code not in [200, 201]:
|
resp = await _send_req(requester_email, city)
|
||||||
sd_workflow_logger.error(f"API Error: status={resp.status_code} body={resp.text}")
|
|
||||||
return resp
|
|
||||||
resp = await _send_req(requester_email, city, template_id, templates)
|
|
||||||
data = resp.json() if resp.status_code in [200, 201] else {}
|
data = resp.json() if resp.status_code in [200, 201] else {}
|
||||||
if data.get("response_status", {}).get("status_code") != 2000:
|
if data.get("response_status", {}).get("status_code") != 2000:
|
||||||
resp = await _send_req(requester_email, "Кемерово", template_id, templates)
|
resp = await _send_req(requester_email, "Кемерово")
|
||||||
data = resp.json() if resp.status_code in [200, 201] else {}
|
data = resp.json() if resp.status_code in [200, 201] else {}
|
||||||
if data.get("response_status", {}).get("status_code") == 2000:
|
if data.get("response_status", {}).get("status_code") == 2000:
|
||||||
return data.get("request", {}).get("id")
|
return data.get("request", {}).get("id")
|
||||||
@@ -283,8 +219,6 @@ def _extract_attachments_from_content(content, is_attachment_type=False):
|
|||||||
async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login):
|
async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login):
|
||||||
"""Логика генерации темы, отправки в SD и загрузки всех очередей вложений."""
|
"""Логика генерации темы, отправки в SD и загрузки всех очередей вложений."""
|
||||||
session["step"] = "creating_ticket"
|
session["step"] = "creating_ticket"
|
||||||
template_id = session.get("template_id")
|
|
||||||
sd_workflow_logger.info(f"[Create] Template ID: {template_id}")
|
|
||||||
try:
|
try:
|
||||||
ad_user = await asyncio.to_thread(get_ad_user_sync, login)
|
ad_user = await asyncio.to_thread(get_ad_user_sync, login)
|
||||||
sd_workflow_logger.info(f"👤 [AD Lookup] User: {login} -> Found: {ad_user is not None}")
|
sd_workflow_logger.info(f"👤 [AD Lookup] User: {login} -> Found: {ad_user is not None}")
|
||||||
@@ -293,7 +227,7 @@ async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login
|
|||||||
subject = await generate_smart_subject(msg_text)
|
subject = await generate_smart_subject(msg_text)
|
||||||
sd_workflow_logger.info(f"📝 [Subject Gen] Text: {msg_text[:50]}... -> Subject: {subject}")
|
sd_workflow_logger.info(f"📝 [Subject Gen] Text: {msg_text[:50]}... -> Subject: {subject}")
|
||||||
|
|
||||||
ticket_id = await create_ticket_in_sd(sender_email, subject, msg_text, city, template_id, session.get("templates", []))
|
ticket_id = await create_ticket_in_sd(sender_email, subject, msg_text, city)
|
||||||
sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}")
|
sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}")
|
||||||
|
|
||||||
if ticket_id:
|
if ticket_id:
|
||||||
@@ -380,39 +314,11 @@ async def sd_module_handler(msg: Message):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if user_id not in sd_sessions:
|
if user_id not in sd_sessions:
|
||||||
sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": [], "templates": [], "template_names": {}}
|
sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": []}
|
||||||
session = sd_sessions[user_id]
|
session = sd_sessions[user_id]
|
||||||
|
|
||||||
|
|
||||||
# --- ШАГ 0: ВЫБОР ШАБЛОНА ---
|
|
||||||
if session["step"] == "choose_template":
|
|
||||||
if cmd in ["skip", "пропустить", "без шаблона"]:
|
|
||||||
sd_workflow_logger.info("[Template] User skipped template selection")
|
|
||||||
session["step"] = "need_text"
|
|
||||||
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
|
|
||||||
return
|
|
||||||
if cmd in session.get("template_names", {}):
|
|
||||||
template_id = session["template_names"][cmd]
|
|
||||||
template_name = next((t["name"] for t in session.get("templates", []) if str(t["id"]) == str(template_id)), "Неизвестно")
|
|
||||||
sd_workflow_logger.info(f"[Template] User selected: {template_id} ({template_name})")
|
|
||||||
session["template_id"] = template_id
|
|
||||||
session["step"] = "need_text"
|
|
||||||
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await msg.answer(
|
|
||||||
f"Неверный номер. Выберите из списка:\n\n"
|
|
||||||
f"{session.get('template_menu_text', '')}",
|
|
||||||
parse_mode="html"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА ---
|
# --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА ---
|
||||||
if session["step"] == "need_text":
|
if session["step"] == "need_text":
|
||||||
# Пользователь только что вошёл в SD_MODE — показываем меню
|
|
||||||
if len(msg_text.strip()) == 0 and not inline_attachments:
|
|
||||||
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
|
|
||||||
return
|
|
||||||
if not msg_text.strip() and not inline_attachments:
|
if not msg_text.strip() and not inline_attachments:
|
||||||
await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html")
|
await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -45,11 +45,10 @@ ALLOWED_EXTENSIONS = [
|
|||||||
# --- Функция получения Email из Active Directory ---
|
# --- Функция получения Email из Active Directory ---
|
||||||
def get_user_email_sync(login: str) -> str:
|
def get_user_email_sync(login: str) -> str:
|
||||||
try:
|
try:
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
from utils.ad_search import search_by_login
|
||||||
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
entries = search_by_login(login, ["mail"])
|
||||||
conn.search(search_base=AD_BASE, search_filter=f"(sAMAccountName={login})", attributes=["mail"])
|
if entries and 'mail' in entries[0] and entries[0].mail.value:
|
||||||
if conn.entries and 'mail' in conn.entries[0] and conn.entries[0].mail.value:
|
return str(entries[0].mail.value)
|
||||||
return str(conn.entries[0].mail.value)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка поиска email в AD: {e}")
|
logger.error(f"Ошибка поиска email в AD: {e}")
|
||||||
return DEFAULT_REQUESTER
|
return DEFAULT_REQUESTER
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# /opt/trueconf_bot/utils/ad_checker.py
|
||||||
|
# Проверка членства пользователя в группах Active Directory
|
||||||
|
|
||||||
|
|
||||||
|
from ldap3 import Server, Connection, ALL
|
||||||
|
from utils.ad_search import search_by_filter as ad_search_all_bases
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_groups(cn: str) -> list[str] | None:
|
||||||
|
"""
|
||||||
|
Найти пользователя по CN и вернуть список групп.
|
||||||
|
Ищет по всем OU из AD_BASES.
|
||||||
|
Возвращает None если пользователь не найден.
|
||||||
|
"""
|
||||||
|
entries = ad_search_all_bases("(cn={})".format(cn), ["cn", "sAMAccountName", "memberOf"])
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
entry = entries[0]
|
||||||
|
return [str(g) for g in entry.memberOf]
|
||||||
|
|
||||||
|
|
||||||
|
def check_group_membership(cn: str, group_pattern: str = "2FA") -> dict:
|
||||||
|
"""
|
||||||
|
Проверить членство пользователя в группе.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cn: CN пользователя (например 'Krivochenko_Denis_Sergeevich')
|
||||||
|
group_pattern: подстрока для поиска в имени группы (по умолчанию '2FA')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict с полями:
|
||||||
|
found: bool — найден ли пользователь
|
||||||
|
cn: str | None — CN пользователя
|
||||||
|
sam: str | None — sAMAccountName
|
||||||
|
in_group: bool — в группе ли
|
||||||
|
groups: list[str] — все группы
|
||||||
|
matching_groups: list[str] — группы с паттерном
|
||||||
|
"""
|
||||||
|
groups = get_user_groups(cn)
|
||||||
|
if groups is None:
|
||||||
|
return {
|
||||||
|
"found": False,
|
||||||
|
"cn": None,
|
||||||
|
"sam": None,
|
||||||
|
"in_group": False,
|
||||||
|
"groups": [],
|
||||||
|
"matching_groups": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
matching = [g for g in groups if group_pattern.upper() in g.upper()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"found": True,
|
||||||
|
"cn": cn,
|
||||||
|
"sam": groups[0].split(",")[0].split("=")[-1], # грубо из DN
|
||||||
|
"in_group": len(matching) > 0,
|
||||||
|
"groups": groups,
|
||||||
|
"matching_groups": matching,
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# /opt/trueconf_bot/utils/ad_search.py
|
||||||
|
# Универсальный поиск пользователей по нескольким OU в AD.
|
||||||
|
# Каждая функция принимает user_id/email/логин и ищет по всем OU из AD_BASES.
|
||||||
|
# Возвращает первую найденную запись (ldap3 Entry) или None.
|
||||||
|
|
||||||
|
from ldap3 import Server, Connection, ALL
|
||||||
|
from ldap3.utils.conv import escape_filter_chars
|
||||||
|
import config.config as config
|
||||||
|
|
||||||
|
|
||||||
|
def _get_conn():
|
||||||
|
"""Создать соединение с AD."""
|
||||||
|
server = Server(config.AD_SERVER, get_info=ALL)
|
||||||
|
return Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True)
|
||||||
|
|
||||||
|
|
||||||
|
def search_by_login(login: str, attributes: list[str] | None = None) -> list:
|
||||||
|
"""
|
||||||
|
Найти пользователя по sAMAccountName во всех OU из AD_BASES.
|
||||||
|
Возвращает список Entry (обычно 0 или 1).
|
||||||
|
"""
|
||||||
|
attrs = attributes or ["cn", "sAMAccountName"]
|
||||||
|
conn = _get_conn()
|
||||||
|
for base in config.AD_BASES:
|
||||||
|
conn.search(
|
||||||
|
search_base=base,
|
||||||
|
search_filter="({}={})".format("sAMAccountName", login),
|
||||||
|
attributes=attrs,
|
||||||
|
)
|
||||||
|
if conn.entries:
|
||||||
|
return list(conn.entries)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def search_by_user_id(user_id: str, attributes: list[str] | None = None) -> list:
|
||||||
|
"""
|
||||||
|
Найти пользователя по email/UPN/sAMAccountName во всех OU из AD_BASES.
|
||||||
|
Возвращает список Entry (обычно 0 или 1).
|
||||||
|
"""
|
||||||
|
attrs = attributes or ["cn", "sAMAccountName", "mail", "userPrincipalName", "extensionAttribute2"]
|
||||||
|
safe_id = escape_filter_chars(user_id)
|
||||||
|
short_username = user_id.split("@")[0] if "@" in user_id else user_id
|
||||||
|
safe_sam = escape_filter_chars(short_username)
|
||||||
|
|
||||||
|
# Строим LDAP filter: (&(objectClass=user)(|(mail=...)(userPrincipalName=...)(sAMAccountName=...)))
|
||||||
|
or_parts = [
|
||||||
|
"(mail={})".format(safe_id),
|
||||||
|
"(userPrincipalName={})".format(safe_id),
|
||||||
|
"(userPrincipalName={})".format(short_username + "@sibcem.ru"),
|
||||||
|
"(sAMAccountName={})".format(safe_sam),
|
||||||
|
]
|
||||||
|
or_filter = "(|{})".format("".join(or_parts))
|
||||||
|
search_filter = "(&{}{})".format("(objectClass=user)", or_filter)
|
||||||
|
|
||||||
|
conn = _get_conn()
|
||||||
|
for base in config.AD_BASES:
|
||||||
|
conn.search(search_base=base, search_filter=search_filter, attributes=attrs)
|
||||||
|
if conn.entries:
|
||||||
|
return list(conn.entries)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def search_by_filter(search_filter: str, attributes: list[str]) -> list:
|
||||||
|
"""
|
||||||
|
Найти по произвольному LDAP-фильтру во всех OU из AD_BASES.
|
||||||
|
Возвращает список Entry.
|
||||||
|
"""
|
||||||
|
conn = _get_conn()
|
||||||
|
for base in config.AD_BASES:
|
||||||
|
conn.search(search_base=base, search_filter=search_filter, attributes=attributes)
|
||||||
|
if conn.entries:
|
||||||
|
return list(conn.entries)
|
||||||
|
return []
|
||||||
@@ -62,6 +62,13 @@ def build_dynamic_menu():
|
|||||||
dynamic_items.append(f"{num_char} — Поиск по регламентам")
|
dynamic_items.append(f"{num_char} — Поиск по регламентам")
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
|
if getattr(config, 'ENABLE_INSTRUCT', False):
|
||||||
|
num_char = emoji_digits.get(counter, str(counter))
|
||||||
|
MENU_MAP[str(counter)] = "INSTRUCT"
|
||||||
|
MENU_MAP[num_char] = "INSTRUCT"
|
||||||
|
dynamic_items.append(f"{num_char} — 📋 Инструкции")
|
||||||
|
counter += 1
|
||||||
|
|
||||||
if counter == 1:
|
if counter == 1:
|
||||||
dynamic_items.append("<i>В данный момент бот находится на техническом обслуживании.</i>")
|
dynamic_items.append("<i>В данный момент бот находится на техническом обслуживании.</i>")
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -63,18 +63,16 @@ async def send_otp_via_api(phone: str, code: str) -> tuple:
|
|||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
def get_card_id_from_ad(user_id: str) -> str:
|
def get_card_id_from_ad(user_id: str) -> str:
|
||||||
"""Синхронно вытягивает CARD_ID сотрудника из Active Directory (extensionAttribute2)"""
|
"""Синхронно вытягивает CARD_ID сотрудника из Active Directory (extensionAttribute2).
|
||||||
|
Ищет по всем OU из AD_BASES."""
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
try:
|
try:
|
||||||
server = Server(config.AD_SERVER, get_info=ALL)
|
entries = search_by_user_id(user_id, ["extensionAttribute2"])
|
||||||
conn = Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True)
|
if not entries:
|
||||||
safe_user_id = escape_filter_chars(user_id)
|
raise Exception("Пользователь не найден в AD.")
|
||||||
short_username = user_id.split('@')[0]
|
entry = entries[0]
|
||||||
|
if 'extensionAttribute2' in entry and entry.extensionAttribute2.value:
|
||||||
search_filter = f"(&(objectClass=user)(|(mail={safe_user_id})(userPrincipalName={safe_user_id})(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})(sAMAccountName={escape_filter_chars(short_username)})))"
|
return str(entry.extensionAttribute2.value).strip()
|
||||||
conn.search(config.AD_BASE, search_filter, attributes=['extensionAttribute2'])
|
|
||||||
|
|
||||||
if conn.entries and 'extensionAttribute2' in conn.entries[0] and conn.entries[0].extensionAttribute2.value:
|
|
||||||
return str(conn.entries[0].extensionAttribute2.value).strip()
|
|
||||||
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка получения CARD_ID из AD для {user_id}: {e}")
|
logger.error(f"Ошибка получения CARD_ID из AD для {user_id}: {e}")
|
||||||
|
|||||||
+80
-6
@@ -170,11 +170,6 @@ SD_UNKNOWN_CMD_TEXT = _sd_unknown(EMOJI_DIGITS)
|
|||||||
SD_TEXT_REQUIRED = _sd_text_required(EMOJI_DIGITS)
|
SD_TEXT_REQUIRED = _sd_text_required(EMOJI_DIGITS)
|
||||||
SD_CREATING = _sd_creating()
|
SD_CREATING = _sd_creating()
|
||||||
|
|
||||||
def _sd_ticket_create_error(emojis: dict) -> str:
|
|
||||||
return (
|
|
||||||
f"❌ <b>Не удалось создать заявку.</b>\n\n" f"Пожалуйста, повторите попытку позже.\n\n" f"{emojis['0']} — В главное меню"
|
|
||||||
)
|
|
||||||
|
|
||||||
def sd_ticket_create_error() -> str:
|
def sd_ticket_create_error() -> str:
|
||||||
return _sd_ticket_create_error(EMOJI_DIGITS)
|
return _sd_ticket_create_error(EMOJI_DIGITS)
|
||||||
|
|
||||||
@@ -400,7 +395,7 @@ def _lk_payslip_loading(emojis: dict) -> str:
|
|||||||
return "⏳ <i>Формирую архив расчетных листков...</i>"
|
return "⏳ <i>Формирую архив расчетных листков...</i>"
|
||||||
|
|
||||||
def _lk_vacation_loading(emojis: dict) -> str:
|
def _lk_vacation_loading(emojis: dict) -> str:
|
||||||
return "⏳ <i>Формирую справку по отпусках...</i>"
|
return "⏳ <i>Формирую справку по отпускам...</i>"
|
||||||
|
|
||||||
def _lk_sick_loading(emojis: dict) -> str:
|
def _lk_sick_loading(emojis: dict) -> str:
|
||||||
return "⏳ <i>Загружаю историю больничных листов...</i>"
|
return "⏳ <i>Загружаю историю больничных листов...</i>"
|
||||||
@@ -494,6 +489,7 @@ LK_UNKNOWN_CMD_TEXT = (
|
|||||||
f"<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
f"<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def lk_error_handling(emojis: dict) -> str:
|
def lk_error_handling(emojis: dict) -> str:
|
||||||
return _lk_error_handling(emojis)
|
return _lk_error_handling(emojis)
|
||||||
|
|
||||||
@@ -574,3 +570,81 @@ def lk_nav_text(emojis: dict) -> str:
|
|||||||
|
|
||||||
def lk_years_menu(current_year: int, emojis: dict) -> str:
|
def lk_years_menu(current_year: int, emojis: dict) -> str:
|
||||||
return lk_years_menu(current_year, emojis)
|
return lk_years_menu(current_year, emojis)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# INSTRUCT BOT (Инструкции)
|
||||||
|
# =========================================================
|
||||||
|
# Модуль инструкций
|
||||||
|
|
||||||
|
# --- Private functions (Internal logic) ---
|
||||||
|
|
||||||
|
def _instruct_main_menu(emojis: dict) -> str:
|
||||||
|
return (
|
||||||
|
f"📋 <b>Выберите инструкцию:</b>\n\n"
|
||||||
|
f"{emojis['1']} — 🖥 Trueconf\n"
|
||||||
|
f"{emojis['2']} — 📧 Почта\n\n"
|
||||||
|
f"<i>{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
def _instruct_trueconf(emojis: dict) -> str:
|
||||||
|
"""Статический текст Trueconf — без проверки группы."""
|
||||||
|
return (
|
||||||
|
"🖥 <b>Инструкция по Trueconf</b>\n\n"
|
||||||
|
"Данная функция находится в разработке.\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _instruct_trueconf_with_ad(emojis: dict, cn: str) -> str:
|
||||||
|
"""
|
||||||
|
Текст Trueconf с проверкой группы 2FA.
|
||||||
|
cn — CN пользователя из AD.
|
||||||
|
"""
|
||||||
|
from utils.ad_checker import check_group_membership
|
||||||
|
|
||||||
|
result = check_group_membership(cn, "2FA")
|
||||||
|
|
||||||
|
if not result["found"]:
|
||||||
|
return (
|
||||||
|
f"🖥 <b>Инструкция по Trueconf</b>\n\n"
|
||||||
|
f"Пользователь {cn} не найден в Active Directory.\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["in_group"]:
|
||||||
|
body = (
|
||||||
|
f"🖥 <b>Инструкция по Trueconf</b>\n\n"
|
||||||
|
f"✅ <b>Доступ к Trueconf разрешён.</b>\n\n"
|
||||||
|
f"У вас есть доступ к функциям Trueconf.\n\n"
|
||||||
|
f"Детали:\n"
|
||||||
|
f"• CN: {result['cn']}\n"
|
||||||
|
f"• Группы 2FA: {', '.join(result['matching_groups'])}\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
body = (
|
||||||
|
f"🖥 <b>Инструкция по Trueconf</b>\n\n"
|
||||||
|
f"❌ <b>Доступ к Trueconf запрещён.</b>\n\n"
|
||||||
|
f"У вас нет группы 2FA в Active Directory.\n\n"
|
||||||
|
f"Детали:\n"
|
||||||
|
f"• CN: {result['cn']}\n"
|
||||||
|
f"• Групп 2FA: 0\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def _instruct_email(emojis: dict) -> str:
|
||||||
|
return (
|
||||||
|
"📧 <b>Инструкция по Почте</b>\n\n"
|
||||||
|
"Данная функция находится в разработке.\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Public constants and functions ---
|
||||||
|
|
||||||
|
INSTRUCT_MAIN_MENU_TEXT = _instruct_main_menu(EMOJI_DIGITS)
|
||||||
|
INSTRUCT_TRUECONF_TEXT = _instruct_trueconf(EMOJI_DIGITS)
|
||||||
|
INSTRUCT_TRUECONF_WITH_AD = _instruct_trueconf_with_ad
|
||||||
|
INSTRUCT_EMAIL_TEXT = _instruct_email(EMOJI_DIGITS)
|
||||||
|
|||||||
Reference in New Issue
Block a user