Artikel

4. Konfiguration

KILinuxMarketingPython

Die Konfiguration trennt Code und Betriebsregeln. Plattformen, Accounts, Limits, Zeitfenster und Sicherheitsparameter gehören nicht hart in Python-Dateien, sondern in eine zentrale Konfigurationsdatei.

Für das MVP reicht eine Datei:

config.yaml

Die .env enthält nur sensible oder installationsabhängige Werte:

API-Keys
Passwörter
Umgebungsmodus
globale Schalter

Die config.yaml enthält fachliche Betriebsregeln:

Plattformen
Accounts
Limits
Themen
Zeitfenster
Sicherheitsparameter
Dateipfade

4.1 Plattform-Konfiguration

Jede Plattform erhält einen eigenen Block.

Minimal:

platforms:
  linkedin:
    enabled: true
    adapter: "linkedin"
    session_path: "storage/sessions/linkedin.json"
    screenshots_path: "storage/screenshots/linkedin"
    default_post_type: "text"

Wichtige Felder:

enabled

Aktiviert oder deaktiviert die Plattform.

adapter

Name des Plattformadapters im Code.

session_path

Pfad zur gespeicherten Playwright-Session.

screenshots_path

Pfad für Screenshots.

default_post_type

Standardtyp für Beiträge.

Beispiel für mehrere Plattformen:

platforms:
  linkedin:
    enabled: true
    adapter: "linkedin"
    session_path: "storage/sessions/linkedin.json"
    screenshots_path: "storage/screenshots/linkedin"
    default_post_type: "text"

  instagram:
    enabled: false
    adapter: "instagram"
    session_path: "storage/sessions/instagram.json"
    screenshots_path: "storage/screenshots/instagram"
    default_post_type: "image"

  facebook:
    enabled: false
    adapter: "facebook"
    session_path: "storage/sessions/facebook.json"
    screenshots_path: "storage/screenshots/facebook"
    default_post_type: "text"

Für den Anfang sollte nur eine Plattform aktiv sein.

platforms:
  linkedin:
    enabled: true

  instagram:
    enabled: false

  facebook:
    enabled: false

Das verhindert, dass Fehler auf mehreren Plattformen gleichzeitig entstehen.

4.2 Account-Konfiguration

Die Account-Konfiguration beschreibt, welcher Account oder welche Seite genutzt wird.

Keine Passwörter in config.yaml.

Passwörter, Tokens oder API-Keys gehören in .env.

Beispiel:

accounts:
  linkedin:
    account_name: "XYZ Verlag"
    account_type: "personal"
    profile_url: "https://www.linkedin.com/in/example"

  facebook:
    account_name: XYZ Verlag"
    account_type: "page"
    page_name: "XYZ Verlag"

  instagram:
    account_name: "xyz"
    account_type: "business"

Mögliche Account-Typen:

personal
page
business

Für LinkedIn ist wichtig, ob im persönlichen Profil oder auf einer Unternehmensseite veröffentlicht wird.

accounts:
  linkedin:
    account_type: "personal"

oder:

accounts:
  linkedin:
    account_type: "company_page"
    page_name: "xyz Verlag"

Für das MVP ist ein persönliches LinkedIn-Profil technisch einfacher als eine Unternehmensseite. Die Architektur sollte aber beide Varianten unterscheiden können.

Accountdaten in .env:

LINKEDIN_LOGIN_EMAIL=
LINKEDIN_LOGIN_PASSWORD=
FACEBOOK_LOGIN_EMAIL=
FACEBOOK_LOGIN_PASSWORD=
INSTAGRAM_LOGIN_USERNAME=
INSTAGRAM_LOGIN_PASSWORD=

Diese Werte werden nur für den manuellen Login oder eine spätere Login-Hilfe verwendet. Der reguläre Betrieb sollte über gespeicherte Sessions laufen.

4.3 Posting-Limits

Limits verhindern, dass das System zu viel veröffentlicht oder bei Fehlern mehrfach agiert.

Globale Limits:

limits:
  max_posts_per_day: 1
  max_comments_per_day: 3
  max_likes_per_day: 10
  max_failures_per_day: 3

Plattformbezogene Limits:

platform_limits:
  linkedin:
    max_posts_per_day: 1
    max_comments_per_day: 3
    max_likes_per_day: 5

  instagram:
    max_posts_per_day: 1
    max_comments_per_day: 0
    max_likes_per_day: 0

  facebook:
    max_posts_per_day: 1
    max_comments_per_day: 2
    max_likes_per_day: 5

Für einen Verlag reicht normalerweise:

ein eigener Beitrag pro Tag
wenige Kommentare
kaum oder keine automatischen Likes
keine automatischen Follows

Zusätzlich sinnvoll:

limits:
  min_minutes_between_posts: 240
  min_minutes_between_comments: 30
  max_same_topic_posts_per_week: 2
  max_retry_attempts: 2

Damit wird verhindert, dass mehrere Beiträge kurz hintereinander veröffentlicht werden.

4.4 Themen-Limits

Themen-Limits verhindern monotone Inhalte.

Beispiel:

topic_limits:
  max_posts_per_topic_per_week: 2
  max_posts_per_author_per_week: 2
  max_book_promotions_per_week: 3
  max_workshop_posts_per_week: 2
  max_ai_posts_per_week: 2

Mögliche Themen:

topics:
  allowed:
    - "klassiker"
    - "gemeinfreie-literatur"
    - "verlagswerkstatt"
    - "ebook-produktion"
    - "ki-im-verlag"
    - "buchvorstellung"
    - "blogartikel"

  blocked:
    - "parteipolitik"
    - "privates"
    - "religionsstreit"
    - "tagesaufreger"

Für OpenAI können die Themen zusätzlich als Steuerung genutzt werden.

content_mix:
  linkedin:
    workshop: 0.4
    ai_publishing: 0.3
    book_intro: 0.2
    blog_summary: 0.1

  instagram:
    quote_card: 0.5
    book_visual: 0.3
    author_note: 0.2

  facebook:
    book_intro: 0.5
    blog_summary: 0.3
    quote: 0.2

Das System muss diese Gewichtung nicht sofort mathematisch exakt umsetzen. Für das MVP reicht es, beim Generieren die erlaubten Beitragstypen zu prüfen.

4.5 Zeitfenster

Zeitfenster legen fest, wann veröffentlicht werden darf.

Beispiel:

posting_windows:
  linkedin:
    - start: "08:30"
      end: "10:30"
    - start: "16:00"
      end: "18:00"

  instagram:
    - start: "11:00"
      end: "13:00"
    - start: "18:00"
      end: "20:00"

  facebook:
    - start: "09:00"
      end: "11:00"
    - start: "17:00"
      end: "19:00"

Globale Zeitzone:

timezone: "Europe/Berlin"

Wochentage:

posting_days:
  linkedin:
    - "monday"
    - "wednesday"
    - "friday"

  instagram:
    - "tuesday"
    - "thursday"
    - "saturday"

  facebook:
    - "monday"
    - "friday"

Ruhezeiten:

quiet_hours:
  start: "21:00"
  end: "07:00"

Während der Ruhezeit wird nicht gepostet, auch wenn ein Scheduler versehentlich läuft.

Zusätzlich sinnvoll:

blackout_dates:
  - "2026-01-01"
  - "2026-12-24"
  - "2026-12-25"

Für ein technisches MVP reicht aber zunächst:

timezone: "Europe/Berlin"

posting_windows:
  linkedin:
    - start: "08:30"
      end: "10:30"

4.6 Sicherheitsparameter

Sicherheitsparameter definieren harte Stop-Regeln.

safety:
  dry_run_required_for_new_platforms: true
  stop_on_captcha: true
  stop_on_login_required: true
  stop_on_platform_warning: true
  stop_on_unexpected_redirect: true
  stop_after_consecutive_failures: 2
  disable_platform_after_daily_failures: 3
  require_screenshot_after_publish: true
  require_screenshot_on_error: true

Wichtig ist ein globaler Kill-Switch.

kill_switch:
  enabled: false
  reason: ""

Wenn enabled auf true steht, darf das System keine Aktion ausführen.

Plattformbezogene Kill-Switches:

platform_kill_switch:
  linkedin:
    enabled: false
    reason: ""

  instagram:
    enabled: false
    reason: ""

  facebook:
    enabled: false
    reason: ""

Sinnvoll sind außerdem verbotene Aktionsarten:

blocked_actions:
  - "auto_follow"
  - "auto_unfollow"
  - "mass_like"
  - "captcha_bypass"
  - "multi_account_rotation"

Kommentar-Sicherheitsregeln:

comment_safety:
  require_manual_approval_for_external_comments: true
  allow_auto_reply_to_own_comments: false
  allow_sales_links_in_comments: false
  max_comment_length: 600
  reject_generic_comments: true

Damit wird technisch festgelegt, dass das System keine unkontrollierte Kommentar-Maschine wird.

4.7 Beispiel: config.yaml

Vollständige MVP-Konfiguration:

app:
  name: "social-publisher"
  timezone: "Europe/Berlin"
  mode: "dry_run"

storage:
  data_path: "data"
  prompts_path: "prompts"
  sessions_path: "storage/sessions"
  screenshots_path: "storage/screenshots"
  images_path: "storage/images"
  logs_path: "storage/logs"

platforms:
  linkedin:
    enabled: true
    adapter: "linkedin"
    session_path: "storage/sessions/linkedin.json"
    screenshots_path: "storage/screenshots/linkedin"
    default_post_type: "text"

  instagram:
    enabled: false
    adapter: "instagram"
    session_path: "storage/sessions/instagram.json"
    screenshots_path: "storage/screenshots/instagram"
    default_post_type: "image"

  facebook:
    enabled: false
    adapter: "facebook"
    session_path: "storage/sessions/facebook.json"
    screenshots_path: "storage/screenshots/facebook"
    default_post_type: "text"

accounts:
  linkedin:
    account_name: "xyz Verlag"
    account_type: "personal"
    profile_url: ""

  instagram:
    account_name: "xyz"
    account_type: "business"

  facebook:
    account_name: "xyz Verlag"
    account_type: "page"
    page_name: "xyz Verlag"

limits:
  max_posts_per_day: 1
  max_comments_per_day: 3
  max_likes_per_day: 10
  max_failures_per_day: 3
  min_minutes_between_posts: 240
  min_minutes_between_comments: 30
  max_retry_attempts: 2

platform_limits:
  linkedin:
    max_posts_per_day: 1
    max_comments_per_day: 3
    max_likes_per_day: 5

  instagram:
    max_posts_per_day: 1
    max_comments_per_day: 0
    max_likes_per_day: 0

  facebook:
    max_posts_per_day: 1
    max_comments_per_day: 2
    max_likes_per_day: 5

topics:
  allowed:
    - "klassiker"
    - "gemeinfreie-literatur"
    - "verlagswerkstatt"
    - "ebook-produktion"
    - "ki-im-verlag"
    - "buchvorstellung"
    - "blogartikel"

  blocked:
    - "parteipolitik"
    - "privates"
    - "religionsstreit"
    - "tagesaufreger"

topic_limits:
  max_posts_per_topic_per_week: 2
  max_posts_per_author_per_week: 2
  max_book_promotions_per_week: 3
  max_workshop_posts_per_week: 2
  max_ai_posts_per_week: 2

content_mix:
  linkedin:
    workshop: 0.4
    ai_publishing: 0.3
    book_intro: 0.2
    blog_summary: 0.1

  instagram:
    quote_card: 0.5
    book_visual: 0.3
    author_note: 0.2

  facebook:
    book_intro: 0.5
    blog_summary: 0.3
    quote: 0.2

posting_days:
  linkedin:
    - "monday"
    - "wednesday"
    - "friday"

  instagram:
    - "tuesday"
    - "thursday"
    - "saturday"

  facebook:
    - "monday"
    - "friday"

posting_windows:
  linkedin:
    - start: "08:30"
      end: "10:30"
    - start: "16:00"
      end: "18:00"

  instagram:
    - start: "11:00"
      end: "13:00"
    - start: "18:00"
      end: "20:00"

  facebook:
    - start: "09:00"
      end: "11:00"
    - start: "17:00"
      end: "19:00"

quiet_hours:
  start: "21:00"
  end: "07:00"

blackout_dates:
  - "2026-01-01"
  - "2026-12-24"
  - "2026-12-25"

safety:
  dry_run_required_for_new_platforms: true
  stop_on_captcha: true
  stop_on_login_required: true
  stop_on_platform_warning: true
  stop_on_unexpected_redirect: true
  stop_after_consecutive_failures: 2
  disable_platform_after_daily_failures: 3
  require_screenshot_after_publish: true
  require_screenshot_on_error: true

kill_switch:
  enabled: false
  reason: ""

platform_kill_switch:
  linkedin:
    enabled: false
    reason: ""

  instagram:
    enabled: false
    reason: ""

  facebook:
    enabled: false
    reason: ""

blocked_actions:
  - "auto_follow"
  - "auto_unfollow"
  - "mass_like"
  - "captcha_bypass"
  - "multi_account_rotation"

comment_safety:
  require_manual_approval_for_external_comments: true
  allow_auto_reply_to_own_comments: false
  allow_sales_links_in_comments: false
  max_comment_length: 600
  reject_generic_comments: true

openai:
  default_model: "gpt-4.1-mini"
  temperature: 0.4
  max_output_tokens: 900
  prompt_version: "1.0.0"

Beispiel: Konfiguration laden

Datei:

src/config.py
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import yaml
from dotenv import load_dotenv


@dataclass(frozen=True)
class RuntimeConfig:
    root_dir: Path
    app_name: str
    timezone: str
    mode: str
    raw: dict[str, Any]


class ConfigLoader:
    def __init__(self, root_dir: Path) -> None:
        self.root_dir = root_dir

    def load(self) -> RuntimeConfig:
        load_dotenv(self.root_dir / ".env")

        config_file = self.root_dir / "config.yaml"

        if config_file.exists() is False:
            raise RuntimeError("Missing config.yaml.")

        with config_file.open("r", encoding="utf-8") as file:
            loaded_config = yaml.safe_load(file)

        if isinstance(loaded_config, dict) is False:
            raise RuntimeError("config.yaml must contain a YAML object.")

        app_config = loaded_config.get("app")

        if isinstance(app_config, dict) is False:
            raise RuntimeError("Missing app configuration.")

        app_name = app_config.get("name")
        timezone = app_config.get("timezone")
        mode = app_config.get("mode")

        if isinstance(app_name, str) is False:
            raise RuntimeError("app.name must be a string.")

        if isinstance(timezone, str) is False:
            raise RuntimeError("app.timezone must be a string.")

        if isinstance(mode, str) is False:
            raise RuntimeError("app.mode must be a string.")

        if mode != "dry_run" and mode != "live":
            raise RuntimeError("app.mode must be dry_run or live.")

        return RuntimeConfig(
            root_dir=self.root_dir,
            app_name=app_name,
            timezone=timezone,
            mode=mode,
            raw=loaded_config,
        )


class PlatformConfigReader:
    def __init__(self, raw_config: dict[str, Any]) -> None:
        self.raw_config = raw_config

    def is_platform_enabled(self, platform_name: str) -> bool:
        platforms = self.raw_config.get("platforms")

        if isinstance(platforms, dict) is False:
            raise RuntimeError("Missing platforms configuration.")

        platform_config = platforms.get(platform_name)

        if isinstance(platform_config, dict) is False:
            raise RuntimeError(f"Missing platform configuration: {platform_name}")

        enabled = platform_config.get("enabled")

        if isinstance(enabled, bool) is False:
            raise RuntimeError(f"platforms.{platform_name}.enabled must be a boolean.")

        return enabled


class SafetyConfigReader:
    def __init__(self, raw_config: dict[str, Any]) -> None:
        self.raw_config = raw_config

    def assert_global_kill_switch_is_disabled(self) -> None:
        kill_switch = self.raw_config.get("kill_switch")

        if isinstance(kill_switch, dict) is False:
            raise RuntimeError("Missing kill_switch configuration.")

        enabled = kill_switch.get("enabled")
        reason = kill_switch.get("reason")

        if isinstance(enabled, bool) is False:
            raise RuntimeError("kill_switch.enabled must be a boolean.")

        if isinstance(reason, str) is False:
            raise RuntimeError("kill_switch.reason must be a string.")

        if enabled is True:
            raise RuntimeError(f"Global kill-switch is enabled: {reason}")

    def assert_platform_kill_switch_is_disabled(self, platform_name: str) -> None:
        platform_kill_switch = self.raw_config.get("platform_kill_switch")

        if isinstance(platform_kill_switch, dict) is False:
            raise RuntimeError("Missing platform_kill_switch configuration.")

        platform_config = platform_kill_switch.get(platform_name)

        if isinstance(platform_config, dict) is False:
            raise RuntimeError(f"Missing platform kill-switch: {platform_name}")

        enabled = platform_config.get("enabled")
        reason = platform_config.get("reason")

        if isinstance(enabled, bool) is False:
            raise RuntimeError(f"platform_kill_switch.{platform_name}.enabled must be a boolean.")

        if isinstance(reason, str) is False:
            raise RuntimeError(f"platform_kill_switch.{platform_name}.reason must be a string.")

        if enabled is True:
            raise RuntimeError(f"Platform kill-switch is enabled for {platform_name}: {reason}")


class RuntimeSafety:
    def __init__(self, raw_config: dict[str, Any]) -> None:
        self.raw_config = raw_config

    def assert_live_mode_is_allowed(self, platform_name: str) -> None:
        app_config = self.raw_config.get("app")

        if isinstance(app_config, dict) is False:
            raise RuntimeError("Missing app configuration.")

        mode = app_config.get("mode")

        if isinstance(mode, str) is False:
            raise RuntimeError("app.mode must be a string.")

        if mode == "dry_run":
            return

        if mode != "live":
            raise RuntimeError("app.mode must be dry_run or live.")

        safety_config = self.raw_config.get("safety")

        if isinstance(safety_config, dict) is False:
            raise RuntimeError("Missing safety configuration.")

        dry_run_required = safety_config.get("dry_run_required_for_new_platforms")

        if isinstance(dry_run_required, bool) is False:
            raise RuntimeError("safety.dry_run_required_for_new_platforms must be a boolean.")

        if dry_run_required is True:
            platform_config = self._get_platform_config(platform_name)
            live_confirmed = platform_config.get("live_confirmed")

            if live_confirmed is not True:
                raise RuntimeError(f"Live mode is not confirmed for platform: {platform_name}")

    def _get_platform_config(self, platform_name: str) -> dict[str, Any]:
        platforms = self.raw_config.get("platforms")

        if isinstance(platforms, dict) is False:
            raise RuntimeError("Missing platforms configuration.")

        platform_config = platforms.get(platform_name)

        if isinstance(platform_config, dict) is False:
            raise RuntimeError(f"Missing platform configuration: {platform_name}")

        return platform_config

Beispiel: Plattform aktiv prüfen

from typing import Any


class PlatformConfigReader:
    def __init__(self, raw_config: dict[str, Any]) -> None:
        self.raw_config = raw_config

    def is_platform_enabled(self, platform_name: str) -> bool:
        platforms = self.raw_config.get("platforms")

        if isinstance(platforms, dict) is False:
            raise RuntimeError("Missing platforms configuration.")

        platform_config = platforms.get(platform_name)

        if isinstance(platform_config, dict) is False:
            raise RuntimeError(f"Missing platform configuration: {platform_name}")

        enabled = platform_config.get("enabled")

        if isinstance(enabled, bool) is False:
            raise RuntimeError(f"platforms.{platform_name}.enabled must be a boolean.")

        return enabled

Beispiel: Kill-Switch prüfen

from typing import Any


class SafetyConfigReader:
    def __init__(self, raw_config: dict[str, Any]) -> None:
        self.raw_config = raw_config

    def assert_global_kill_switch_is_disabled(self) -> None:
        kill_switch = self.raw_config.get("kill_switch")

        if isinstance(kill_switch, dict) is False:
            raise RuntimeError("Missing kill_switch configuration.")

        enabled = kill_switch.get("enabled")
        reason = kill_switch.get("reason")

        if isinstance(enabled, bool) is False:
            raise RuntimeError("kill_switch.enabled must be a boolean.")

        if isinstance(reason, str) is False:
            raise RuntimeError("kill_switch.reason must be a string.")

        if enabled is True:
            raise RuntimeError(f"Global kill-switch is enabled: {reason}")

    def assert_platform_kill_switch_is_disabled(self, platform_name: str) -> None:
        platform_kill_switch = self.raw_config.get("platform_kill_switch")

        if isinstance(platform_kill_switch, dict) is False:
            raise RuntimeError("Missing platform_kill_switch configuration.")

        platform_config = platform_kill_switch.get(platform_name)

        if isinstance(platform_config, dict) is False:
            raise RuntimeError(f"Missing platform kill-switch: {platform_name}")

        enabled = platform_config.get("enabled")
        reason = platform_config.get("reason")

        if isinstance(enabled, bool) is False:
            raise RuntimeError(f"platform_kill_switch.{platform_name}.enabled must be a boolean.")

        if isinstance(reason, str) is False:
            raise RuntimeError(f"platform_kill_switch.{platform_name}.reason must be a string.")

        if enabled is True:
            raise RuntimeError(f"Platform kill-switch is enabled for {platform_name}: {reason}")

Beispiel: Dry-Run erzwingen

from typing import Any


class RuntimeSafety:
    def __init__(self, raw_config: dict[str, Any]) -> None:
        self.raw_config = raw_config

    def assert_live_mode_is_allowed(self, platform_name: str) -> None:
        app_config = self.raw_config.get("app")

        if isinstance(app_config, dict) is False:
            raise RuntimeError("Missing app configuration.")

        mode = app_config.get("mode")

        if isinstance(mode, str) is False:
            raise RuntimeError("app.mode must be a string.")

        if mode == "dry_run":
            return

        if mode != "live":
            raise RuntimeError("app.mode must be dry_run or live.")

        safety_config = self.raw_config.get("safety")

        if isinstance(safety_config, dict) is False:
            raise RuntimeError("Missing safety configuration.")

        dry_run_required = safety_config.get("dry_run_required_for_new_platforms")

        if isinstance(dry_run_required, bool) is False:
            raise RuntimeError("safety.dry_run_required_for_new_platforms must be a boolean.")

        if dry_run_required is True:
            platform_config = self._get_platform_config(platform_name)
            live_confirmed = platform_config.get("live_confirmed")

            if live_confirmed is not True:
                raise RuntimeError(f"Live mode is not confirmed for platform: {platform_name}")

    def _get_platform_config(self, platform_name: str) -> dict[str, Any]:
        platforms = self.raw_config.get("platforms")

        if isinstance(platforms, dict) is False:
            raise RuntimeError("Missing platforms configuration.")

        platform_config = platforms.get(platform_name)

        if isinstance(platform_config, dict) is False:
            raise RuntimeError(f"Missing platform configuration: {platform_name}")

        return platform_config

Dazu kann in config.yaml pro Plattform später ergänzt werden:

platforms:
  linkedin:
    enabled: true
    live_confirmed: false
    adapter: "linkedin"
    session_path: "storage/sessions/linkedin.json"
    screenshots_path: "storage/screenshots/linkedin"
    default_post_type: "text"

Ergebnis dieses Kapitels

Die Konfiguration definiert jetzt:

aktive Plattformen
Account-Kontext
Posting-Limits
Themensteuerung
Zeitfenster
Kill-Switches
Sicherheitsregeln
OpenAI-Grundeinstellungen
Speicherorte

Damit kann die Anwendung ohne Codeänderung zwischen Dry-Run, Testbetrieb und späterem Produktivbetrieb wechseln.

0 Kommentare