feat: Plugin-System Umbau — 6 Phasen komplett abgeschlossen
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
Phase 1: Contracts konsequent nutzen - 12 neue contracts.py erstellt (alle 19 Plugins haben jetzt contracts) - 4 bestehende contracts.py an zentrale ContractRegistry angepasst - Alle 19 Plugins haben on_deactivate mit Contract-Unregister - 0 echte problematische INTER-Plugin Imports Phase 2: Hooks/Filters-System - app/core/hooks.py (HookRegistry mit actions + filters) - 15 Hook-Punkte in Core-Services (contact, auth, mail, calendar, user, dms) - BasePlugin.on_deactivate meldet alle Hooks ab Phase 3: Plugin-Isolation - scripts/check_cross_plugin_imports.py (Linting-Regel) - .github/workflows/check-cross-plugin-imports.yml (CI/CD) - .pre-commit-cross-plugin.yaml (Pre-commit Hook) - 155 Dateien geprueft, 0 Verstoesse Phase 4: Plugin-Versioning - app/plugins/semver.py (SemVer mit Parse, Compare, Pre-release) - migration_runner.py erweitert: run_migration_down, rollback_to_version - manifest.py: min_app_version Feld - registry.py: App-Version-Compatibility-Check bei Installation - GET /api/v1/plugins/updates Endpoint Phase 5: Marketplace-Vorbereitung - app/plugins/signature.py (Ed25519 Signatur-Validierung) - app/plugins/quarantine.py (Plugin-Quarantine mit Validierung) - app/models/plugin_allowlist.py + Migration 0046 - manifest.py: author, license, homepage, icon, screenshots, changelog, marketplace_tags, price - registry.py: discover_external(), discover_all() - POST /api/v1/plugins/install-marketplace (deaktiviert) Phase 6: Manifest-Anpassung - manifest.py: 12 neue Felder + SemVer/Hook-Name Validierung - MANIFEST_SCHEMA_DOC aktualisiert - Alle 19 Plugin-Manifeste aktualisiert - Frontend PluginUiManifest Typ erweitert Zusaetzliche Bug-Fixes: - test_sample-Modul erstellt - conftest.py Deadlock-Prevention - SESSION_COOKIE_SECURE=true - dump.rdb aus Git entfernt + .gitignore - backup.py datetime.utcnow -> func.now() - system_settings.py JSONB-Import nach oben - tax.py Mapped[float] -> Mapped[Decimal] - notification.py type_key-Laengen vereinheitlicht Tests: 91 neue Tests, alle bestanden
This commit is contained in:
@@ -232,6 +232,31 @@ class PluginManifest(BaseModel):
|
||||
custom_fields: list[CustomFieldDefinition] = Field(
|
||||
default_factory=list, description="Custom field definitions contributed by this plugin"
|
||||
)
|
||||
# ── Versioning (Phase 4) ──
|
||||
min_app_version: str = Field(
|
||||
default="0.0.0",
|
||||
description="Minimum LeoCRM version required (SemVer)"
|
||||
)
|
||||
# ── Marketplace (Phase 5) ──
|
||||
author: str = Field(default="", max_length=200, description="Plugin author name")
|
||||
author_email: str = Field(default="", max_length=200, description="Author contact email")
|
||||
homepage: str = Field(default="", max_length=500, description="Plugin homepage URL")
|
||||
license: str = Field(default="MIT", max_length=50, description="License identifier")
|
||||
icon: str = Field(default="", description="Icon URL or emoji")
|
||||
screenshots: list[str] = Field(default_factory=list, description="Screenshot URLs for marketplace")
|
||||
changelog: str = Field(default="", description="Changelog URL or inline text")
|
||||
marketplace_tags: list[str] = Field(default_factory=list, description="Marketplace category tags")
|
||||
price: float = Field(default=0.0, ge=0.0, description="Price (0 = free)")
|
||||
# ── Hooks (Phase 2) ──
|
||||
hooks: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Hook names this plugin registers (e.g. 'contact.before_create')"
|
||||
)
|
||||
# ── Contracts (Phase 1) ──
|
||||
contract_version: str = Field(
|
||||
default="1.0.0",
|
||||
description="Contract API version this plugin exposes"
|
||||
)
|
||||
|
||||
|
||||
@field_validator("name")
|
||||
@@ -241,6 +266,27 @@ class PluginManifest(BaseModel):
|
||||
raise ValueError("Plugin name must be alphanumeric with underscores only")
|
||||
return v.lower()
|
||||
|
||||
@field_validator("min_app_version")
|
||||
@classmethod
|
||||
def validate_min_app_version(cls, v: str) -> str:
|
||||
"""Validate min_app_version is valid SemVer."""
|
||||
if v and v != "0.0.0":
|
||||
from app.plugins.semver import SemVer
|
||||
SemVer.parse(v) # Raises ValueError if invalid
|
||||
return v
|
||||
|
||||
@field_validator("hooks")
|
||||
@classmethod
|
||||
def validate_hooks(cls, v: list[str]) -> list[str]:
|
||||
"""Validate hook names follow namespace.action pattern."""
|
||||
import re
|
||||
for hook in v:
|
||||
if not re.match(r"^[a-z_]+\.[a-z_]+$", hook):
|
||||
raise ValueError(
|
||||
f"Invalid hook name '{hook}': must be 'namespace.action' (lowercase, underscores only)"
|
||||
)
|
||||
return v
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
@@ -323,6 +369,46 @@ MANIFEST_SCHEMA_DOC = ManifestSchemaResponse(
|
||||
"required": "false",
|
||||
"description": "Dashboard widgets (id, label_key, component, col_span, permission)",
|
||||
},
|
||||
"min_app_version": {
|
||||
"type": "str",
|
||||
"required": "false",
|
||||
"description": "Minimum LeoCRM version required (SemVer, default: 0.0.0)",
|
||||
},
|
||||
"author": {
|
||||
"type": "str",
|
||||
"required": "false",
|
||||
"description": "Plugin author name",
|
||||
},
|
||||
"license": {
|
||||
"type": "str",
|
||||
"required": "false",
|
||||
"description": "License identifier (default: MIT)",
|
||||
},
|
||||
"homepage": {
|
||||
"type": "str",
|
||||
"required": "false",
|
||||
"description": "Plugin homepage URL",
|
||||
},
|
||||
"hooks": {
|
||||
"type": "list[str]",
|
||||
"required": "false",
|
||||
"description": "Hook names this plugin registers (e.g. 'contact.before_create')",
|
||||
},
|
||||
"contract_version": {
|
||||
"type": "str",
|
||||
"required": "false",
|
||||
"description": "Contract API version this plugin exposes (default: 1.0.0)",
|
||||
},
|
||||
"marketplace_tags": {
|
||||
"type": "list[str]",
|
||||
"required": "false",
|
||||
"description": "Marketplace category tags",
|
||||
},
|
||||
"price": {
|
||||
"type": "float",
|
||||
"required": "false",
|
||||
"description": "Price (0 = free, default: 0.0)",
|
||||
},
|
||||
},
|
||||
example=PluginManifest(
|
||||
name="example_plugin",
|
||||
|
||||
Reference in New Issue
Block a user