Files
espocrm/custom/scripts/ki_project_overview.py
bsiggel 552540e214 feat: Add KI-Einstiegsscript for comprehensive project overview
- Introduced `ki_project_overview.py` for automated analysis of EspoCRM project structure, entities, relationships, custom PHP classes, workflows, frontend adjustments, and internationalization.
- Created `ki-overview.sh` wrapper script for executing the Python script with various output options.
- Updated `README.md` to include a quick start section for the new KI entry script.
- Added detailed documentation in `KI_OVERVIEW_README.md` explaining the script's purpose, usage, and output format.
- Summarized the new features and files in `KI_OVERVIEW_SUMMARY.md`.
- Enhanced `.vscode/settings.json` to approve new scripts for execution.
2026-01-25 12:34:46 +01:00

481 lines
16 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
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():
"""Liest und gibt den README.md Inhalt aus."""
print_section("README.md - Projektdokumentation", "=")
if README_PATH.exists():
with open(README_PATH, 'r', encoding='utf-8') as f:
content = f.read()
print(content)
else:
print("⚠️ README.md nicht gefunden!")
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")
if __name__ == "__main__":
main()