490 lines
17 KiB
Python
Executable File
490 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
KI-Einstiegsscript für EspoCRM Projekt
|
||
======================================
|
||
Gibt einen vollständigen Überblick über das Projekt aus:
|
||
- README.md Inhalt
|
||
- Automatisch ermittelte Projektstruktur
|
||
- Entitäten und ihre Felder
|
||
- Beziehungen zwischen Entitäten
|
||
- Custom PHP Klassen
|
||
- Workflows
|
||
- Frontend Anpassungen
|
||
|
||
Ziel: KI erhält aktuellen Informationsstand für die Programmierung
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
from collections import defaultdict
|
||
from typing import Dict, List, Set
|
||
|
||
# Basis-Pfad des Projekts
|
||
BASE_PATH = Path("/var/lib/docker/volumes/vmh-espocrm_espocrm/_data")
|
||
CUSTOM_PATH = BASE_PATH / "custom/Espo/Custom"
|
||
README_PATH = BASE_PATH / "README.md"
|
||
|
||
|
||
def print_section(title: str, symbol: str = "="):
|
||
"""Gibt eine formatierte Section-Überschrift aus."""
|
||
print(f"\n{symbol * 80}")
|
||
print(f"{title.center(80)}")
|
||
print(f"{symbol * 80}\n")
|
||
|
||
|
||
def print_subsection(title: str):
|
||
"""Gibt eine Unterüberschrift aus."""
|
||
print(f"\n{'─' * 80}")
|
||
print(f"► {title}")
|
||
print(f"{'─' * 80}")
|
||
|
||
|
||
def read_readme():
|
||
"""Hinweis auf README.md - wird nicht direkt ausgegeben."""
|
||
# README.md wird nicht ausgegeben - zu lang für direkte Ausgabe
|
||
# Hinweis erfolgt am Ende des Scripts
|
||
pass
|
||
|
||
|
||
def analyze_entities():
|
||
"""Analysiert alle Custom Entitäten und ihre Definitionen."""
|
||
print_section("ENTITÄTEN ANALYSE", "=")
|
||
|
||
entity_defs_path = CUSTOM_PATH / "Resources/metadata/entityDefs"
|
||
scopes_path = CUSTOM_PATH / "Resources/metadata/scopes"
|
||
|
||
if not entity_defs_path.exists():
|
||
print("⚠️ Keine Custom Entitäten gefunden!")
|
||
return {}
|
||
|
||
entities = {}
|
||
|
||
# Alle entityDefs Dateien durchgehen
|
||
for entity_file in sorted(entity_defs_path.glob("*.json")):
|
||
entity_name = entity_file.stem
|
||
|
||
try:
|
||
with open(entity_file, 'r', encoding='utf-8') as f:
|
||
entity_def = json.load(f)
|
||
|
||
# Scope-Informationen laden
|
||
scope_info = {}
|
||
scope_file = scopes_path / f"{entity_name}.json"
|
||
if scope_file.exists():
|
||
with open(scope_file, 'r', encoding='utf-8') as f:
|
||
scope_info = json.load(f)
|
||
|
||
entities[entity_name] = {
|
||
'def': entity_def,
|
||
'scope': scope_info,
|
||
'file': entity_file
|
||
}
|
||
|
||
print_subsection(f"Entität: {entity_name}")
|
||
|
||
# Scope Informationen
|
||
if scope_info:
|
||
print("\n📋 Scope:")
|
||
for key, value in scope_info.items():
|
||
if key in ['entity', 'module', 'object', 'tab', 'acl', 'customizable',
|
||
'stream', 'disabled', 'type', 'isCustom']:
|
||
print(f" • {key}: {value}")
|
||
|
||
# Felder
|
||
if 'fields' in entity_def and entity_def['fields']:
|
||
print(f"\n🔧 Felder ({len(entity_def['fields'])}):")
|
||
for field_name, field_def in sorted(entity_def['fields'].items()):
|
||
field_type = field_def.get('type', 'unknown')
|
||
required = " [REQUIRED]" if field_def.get('required') else ""
|
||
disabled = " [DISABLED]" if field_def.get('disabled') else ""
|
||
isCustom = " [CUSTOM]" if field_def.get('isCustom') else ""
|
||
|
||
extra_info = []
|
||
if field_type == 'enum':
|
||
options = field_def.get('options', [])
|
||
extra_info.append(f"options: {len(options)}")
|
||
elif field_type == 'link':
|
||
entity = field_def.get('entity', 'unknown')
|
||
extra_info.append(f"→ {entity}")
|
||
elif field_type in ['varchar', 'text']:
|
||
if 'maxLength' in field_def:
|
||
extra_info.append(f"max: {field_def['maxLength']}")
|
||
elif field_type == 'currency':
|
||
extra_info.append("€")
|
||
|
||
extra_str = f" ({', '.join(extra_info)})" if extra_info else ""
|
||
print(f" • {field_name}: {field_type}{extra_str}{required}{disabled}{isCustom}")
|
||
|
||
# Beziehungen (Links)
|
||
if 'links' in entity_def and entity_def['links']:
|
||
print(f"\n🔗 Beziehungen ({len(entity_def['links'])}):")
|
||
for link_name, link_def in sorted(entity_def['links'].items()):
|
||
link_type = link_def.get('type', 'unknown')
|
||
foreign_entity = link_def.get('entity', 'unknown')
|
||
foreign_link = link_def.get('foreign', 'N/A')
|
||
relation_name = link_def.get('relationName', '')
|
||
disabled = " [DISABLED]" if link_def.get('disabled') else ""
|
||
|
||
relation_info = f" (relationName: {relation_name})" if relation_name else ""
|
||
print(f" • {link_name} [{link_type}] → {foreign_entity}.{foreign_link}{relation_info}{disabled}")
|
||
|
||
# Formula Scripts
|
||
formula_file = CUSTOM_PATH / f"Resources/metadata/formula/{entity_name}.json"
|
||
if formula_file.exists():
|
||
with open(formula_file, 'r', encoding='utf-8') as f:
|
||
formula_def = json.load(f)
|
||
print(f"\n⚡ Formula Scripts:")
|
||
for script_type in ['beforeSaveScript', 'beforeSaveApiScript', 'afterSaveScript']:
|
||
if script_type in formula_def:
|
||
script = formula_def[script_type]
|
||
lines = script.count('\n') + 1
|
||
print(f" • {script_type}: {lines} Zeilen")
|
||
|
||
print()
|
||
|
||
except json.JSONDecodeError as e:
|
||
print(f"❌ Fehler beim Parsen von {entity_file.name}: {e}")
|
||
except Exception as e:
|
||
print(f"❌ Fehler bei {entity_file.name}: {e}")
|
||
|
||
return entities
|
||
|
||
|
||
def analyze_relationships(entities: Dict):
|
||
"""Analysiert Beziehungen zwischen Entitäten."""
|
||
print_section("BEZIEHUNGSGRAPH", "=")
|
||
|
||
# Sammle alle Beziehungen
|
||
relationships = defaultdict(list)
|
||
|
||
for entity_name, entity_data in entities.items():
|
||
entity_def = entity_data['def']
|
||
if 'links' not in entity_def:
|
||
continue
|
||
|
||
for link_name, link_def in entity_def['links'].items():
|
||
if link_def.get('disabled'):
|
||
continue
|
||
|
||
target_entity = link_def.get('entity')
|
||
link_type = link_def.get('type', 'unknown')
|
||
|
||
if target_entity:
|
||
relationships[entity_name].append({
|
||
'link_name': link_name,
|
||
'type': link_type,
|
||
'target': target_entity,
|
||
'foreign': link_def.get('foreign', 'N/A')
|
||
})
|
||
|
||
# Gib Beziehungsgraph aus
|
||
for entity_name in sorted(relationships.keys()):
|
||
links = relationships[entity_name]
|
||
if links:
|
||
print(f"\n{entity_name}:")
|
||
for link in links:
|
||
arrow = "→" if link['type'] in ['belongsTo', 'hasOne'] else "⇄"
|
||
print(f" {arrow} {link['link_name']} [{link['type']}] → {link['target']}")
|
||
|
||
|
||
def analyze_custom_classes():
|
||
"""Analysiert Custom PHP Klassen."""
|
||
print_section("CUSTOM PHP KLASSEN", "=")
|
||
|
||
classes_path = CUSTOM_PATH / "Classes"
|
||
|
||
if not classes_path.exists():
|
||
print("ℹ️ Keine Custom PHP Klassen gefunden.")
|
||
return
|
||
|
||
php_files = list(classes_path.rglob("*.php"))
|
||
|
||
if not php_files:
|
||
print("ℹ️ Keine Custom PHP Klassen gefunden.")
|
||
return
|
||
|
||
# Gruppiere nach Typ
|
||
by_type = defaultdict(list)
|
||
|
||
for php_file in php_files:
|
||
relative_path = php_file.relative_to(classes_path)
|
||
parts = relative_path.parts
|
||
|
||
if len(parts) > 0:
|
||
class_type = parts[0]
|
||
by_type[class_type].append(relative_path)
|
||
|
||
for class_type in sorted(by_type.keys()):
|
||
print(f"\n📦 {class_type}:")
|
||
for file_path in sorted(by_type[class_type]):
|
||
print(f" • {file_path}")
|
||
|
||
|
||
def analyze_workflows():
|
||
"""Analysiert Workflows."""
|
||
print_section("WORKFLOWS", "=")
|
||
|
||
workflows_path = BASE_PATH / "custom/workflows"
|
||
|
||
if not workflows_path.exists():
|
||
print("ℹ️ Keine Workflows gefunden.")
|
||
return
|
||
|
||
workflow_files = list(workflows_path.glob("*.json"))
|
||
|
||
if not workflow_files:
|
||
print("ℹ️ Keine Workflows gefunden.")
|
||
return
|
||
|
||
for workflow_file in sorted(workflow_files):
|
||
try:
|
||
with open(workflow_file, 'r', encoding='utf-8') as f:
|
||
workflow = json.load(f)
|
||
|
||
name = workflow.get('name', workflow_file.stem)
|
||
entity = workflow.get('entityType', 'N/A')
|
||
is_active = workflow.get('isActive', False)
|
||
status = "✓ AKTIV" if is_active else "✗ INAKTIV"
|
||
|
||
print(f"\n📋 {name} ({workflow_file.name})")
|
||
print(f" Entität: {entity}")
|
||
print(f" Status: {status}")
|
||
|
||
# Trigger-Typ
|
||
if 'type' in workflow:
|
||
print(f" Trigger: {workflow['type']}")
|
||
|
||
# Aktionen
|
||
if 'actions' in workflow:
|
||
actions = workflow['actions']
|
||
print(f" Aktionen ({len(actions)}):")
|
||
for action in actions[:5]: # Erste 5 Aktionen
|
||
action_type = action.get('type', 'unknown')
|
||
print(f" • {action_type}")
|
||
if len(actions) > 5:
|
||
print(f" ... und {len(actions) - 5} weitere")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Fehler beim Lesen von {workflow_file.name}: {e}")
|
||
|
||
|
||
def analyze_frontend():
|
||
"""Analysiert Frontend Anpassungen."""
|
||
print_section("FRONTEND ANPASSUNGEN", "=")
|
||
|
||
# JavaScript
|
||
js_path = BASE_PATH / "client/custom/src"
|
||
if js_path.exists():
|
||
js_files = list(js_path.rglob("*.js"))
|
||
if js_files:
|
||
print_subsection("JavaScript Files")
|
||
for js_file in sorted(js_files):
|
||
relative = js_file.relative_to(js_path)
|
||
print(f" • {relative}")
|
||
|
||
# CSS
|
||
css_path = BASE_PATH / "client/custom/css"
|
||
if css_path.exists():
|
||
css_files = list(css_path.glob("*.css"))
|
||
if css_files:
|
||
print_subsection("CSS Files")
|
||
for css_file in sorted(css_files):
|
||
print(f" • {css_file.name}")
|
||
|
||
# App Client Config
|
||
client_config = CUSTOM_PATH / "Resources/metadata/app/client.json"
|
||
if client_config.exists():
|
||
print_subsection("App Client Config")
|
||
try:
|
||
with open(client_config, 'r', encoding='utf-8') as f:
|
||
config = json.load(f)
|
||
|
||
if 'cssList' in config:
|
||
print(" CSS List:")
|
||
for css in config['cssList']:
|
||
if css != "__APPEND__":
|
||
print(f" • {css}")
|
||
|
||
if 'scriptList' in config:
|
||
print(" Script List:")
|
||
for script in config['scriptList']:
|
||
if script != "__APPEND__":
|
||
print(f" • {script}")
|
||
|
||
except Exception as e:
|
||
print(f" ❌ Fehler: {e}")
|
||
|
||
|
||
def analyze_layouts():
|
||
"""Analysiert Custom Layouts."""
|
||
print_section("CUSTOM LAYOUTS", "=")
|
||
|
||
layouts_path = CUSTOM_PATH / "Resources/metadata/layouts"
|
||
|
||
if not layouts_path.exists():
|
||
print("ℹ️ Keine Custom Layouts gefunden.")
|
||
return
|
||
|
||
# Gruppiere nach Entität
|
||
entities = defaultdict(list)
|
||
|
||
for layout_file in layouts_path.rglob("*.json"):
|
||
relative = layout_file.relative_to(layouts_path)
|
||
entity = relative.parts[0] if len(relative.parts) > 1 else "unknown"
|
||
layout_type = relative.stem
|
||
entities[entity].append(layout_type)
|
||
|
||
for entity in sorted(entities.keys()):
|
||
layouts = sorted(entities[entity])
|
||
print(f"\n{entity}:")
|
||
print(f" Layouts: {', '.join(layouts)}")
|
||
|
||
|
||
def analyze_i18n():
|
||
"""Analysiert Internationalisierung."""
|
||
print_section("INTERNATIONALISIERUNG (i18n)", "=")
|
||
|
||
i18n_path = CUSTOM_PATH / "Resources/i18n"
|
||
|
||
if not i18n_path.exists():
|
||
print("ℹ️ Keine i18n Dateien gefunden.")
|
||
return
|
||
|
||
languages = [d.name for d in i18n_path.iterdir() if d.is_dir()]
|
||
|
||
print(f"Unterstützte Sprachen: {', '.join(sorted(languages))}")
|
||
|
||
for lang in sorted(languages):
|
||
lang_path = i18n_path / lang
|
||
json_files = list(lang_path.glob("*.json"))
|
||
|
||
if json_files:
|
||
print(f"\n{lang}:")
|
||
print(f" Übersetzungsdateien: {len(json_files)}")
|
||
|
||
# Zähle Labels
|
||
total_labels = 0
|
||
for json_file in json_files:
|
||
try:
|
||
with open(json_file, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
# Rekursiv Labels zählen
|
||
def count_labels(obj):
|
||
if isinstance(obj, dict):
|
||
return sum(count_labels(v) for v in obj.values())
|
||
elif isinstance(obj, str):
|
||
return 1
|
||
return 0
|
||
total_labels += count_labels(data)
|
||
except:
|
||
pass
|
||
|
||
print(f" Geschätzte Labels: ~{total_labels}")
|
||
|
||
|
||
def print_quick_stats():
|
||
"""Gibt eine Schnellübersicht aus."""
|
||
print_section("SCHNELLÜBERSICHT", "=")
|
||
|
||
stats = {}
|
||
|
||
# Entitäten
|
||
entity_defs_path = CUSTOM_PATH / "Resources/metadata/entityDefs"
|
||
if entity_defs_path.exists():
|
||
stats['Entities'] = len(list(entity_defs_path.glob("*.json")))
|
||
|
||
# PHP Klassen
|
||
classes_path = CUSTOM_PATH / "Classes"
|
||
if classes_path.exists():
|
||
stats['PHP Classes'] = len(list(classes_path.rglob("*.php")))
|
||
|
||
# Workflows
|
||
workflows_path = BASE_PATH / "custom/workflows"
|
||
if workflows_path.exists():
|
||
stats['Workflows'] = len(list(workflows_path.glob("*.json")))
|
||
|
||
# JS Files
|
||
js_path = BASE_PATH / "client/custom/src"
|
||
if js_path.exists():
|
||
stats['JavaScript Files'] = len(list(js_path.rglob("*.js")))
|
||
|
||
# CSS Files
|
||
css_path = BASE_PATH / "client/custom/css"
|
||
if css_path.exists():
|
||
stats['CSS Files'] = len(list(css_path.glob("*.css")))
|
||
|
||
# Layouts
|
||
layouts_path = CUSTOM_PATH / "Resources/metadata/layouts"
|
||
if layouts_path.exists():
|
||
stats['Custom Layouts'] = len(list(layouts_path.rglob("*.json")))
|
||
|
||
# i18n
|
||
i18n_path = CUSTOM_PATH / "Resources/i18n"
|
||
if i18n_path.exists():
|
||
languages = [d.name for d in i18n_path.iterdir() if d.is_dir()]
|
||
stats['Languages'] = len(languages)
|
||
|
||
print("📊 Projekt-Statistiken:\n")
|
||
for key, value in stats.items():
|
||
print(f" • {key:<20} {value:>5}")
|
||
|
||
|
||
def main():
|
||
"""Hauptfunktion - führt alle Analysen aus."""
|
||
print("\n" + "=" * 80)
|
||
print("KI-EINSTIEGSSCRIPT FÜR ESPOCRM PROJEKT".center(80))
|
||
print("Automatische Projekt-Analyse für KI-basierte Programmierung".center(80))
|
||
print("=" * 80)
|
||
|
||
# 1. Schnellübersicht
|
||
print_quick_stats()
|
||
|
||
# 2. README.md
|
||
read_readme()
|
||
|
||
# 3. Entitäten analysieren
|
||
entities = analyze_entities()
|
||
|
||
# 4. Beziehungsgraph
|
||
if entities:
|
||
analyze_relationships(entities)
|
||
|
||
# 5. Custom Layouts
|
||
analyze_layouts()
|
||
|
||
# 6. Custom PHP Klassen
|
||
analyze_custom_classes()
|
||
|
||
# 7. Workflows
|
||
analyze_workflows()
|
||
|
||
# 8. Frontend
|
||
analyze_frontend()
|
||
|
||
# 9. i18n
|
||
analyze_i18n()
|
||
|
||
# Abschluss
|
||
print_section("ANALYSE ABGESCHLOSSEN", "=")
|
||
print("\n✅ Die KI hat jetzt einen vollständigen Überblick über das Projekt!")
|
||
print(" Alle Entitäten, Beziehungen, Custom Klassen und Frontend-Anpassungen wurden erfasst.\n")
|
||
|
||
# Hinweis auf README.md
|
||
print("=" * 80)
|
||
print("📖 WICHTIG: Bitte jetzt die README.md lesen!")
|
||
print("=" * 80)
|
||
print(f"\n Pfad: {README_PATH}")
|
||
print(f"\n Die README.md enthält {README_PATH.stat().st_size // 1024}KB detaillierte Projektdokumentation:")
|
||
print(" • Struktur und Funktionsweise")
|
||
print(" • Rebuild-Prozess")
|
||
print(" • Workflow-Verwaltung")
|
||
print(" • Formula-Scripts und Custom PHP")
|
||
print(" • Internationalisierung")
|
||
print(" • Troubleshooting")
|
||
print("\n 📌 UNBEDINGT LESEN für vollständiges Projektverständnis!\n")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|