4. Konfiguration
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