mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
Introduces SpaceType ('personal' | 'brand' | 'club' | 'family' | 'team' |
'practice') and SPACE_MODULE_ALLOWLIST as the shared-branding primitives
for the Spaces refactor that replaces the user-vs-org polymorphy with a
single tenancy primitive (Notion/Linear pattern).
Pure additive — no runtime behaviour change yet. Better Auth config,
Dexie migration, scope wrapper and rolling module migration follow in
separate commits.
Plan: docs/plans/spaces-foundation.md
Social-relay plan now defers brand storage to the Spaces primitive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
269 lines
14 KiB
Markdown
269 lines
14 KiB
Markdown
# Social Relay — Module Plan
|
||
|
||
## Status (2026-04-20)
|
||
|
||
**Ideenphase, wartet auf Spaces-Foundation.** Noch kein Code. Nach Einführung der Spaces-Primitive (siehe [`spaces-foundation.md`](./spaces-foundation.md)) wird Edisconet ein Space vom Typ `brand` — der frühere `brands/`-Modul-Vorschlag entfällt. OAuth-Token, Voice-Doc und AI-Persona hängen am Space. Dieser Plan wird nach Phase-2 der Foundation überarbeitet und präzisiert; bis dahin nur konzeptuell gültig.
|
||
|
||
---
|
||
|
||
## Ziel
|
||
|
||
Ausgewählte LinkedIn-Posts von **Howspace** (Quelle) werden auf Deutsch übersetzt, für **Edisconet** (Ziel, Company Page ID 88888092) tonal angepasst und — **nach menschlichem Review** — auf der Edisconet-Company-Page veröffentlicht. Ursprung (Howspace) bleibt im Post sichtbar.
|
||
|
||
Kernfrage: *"Welcher HS-Post lohnt sich für Edisconet, wie klingt er auf Deutsch in unserem Ton, und wie kommt er mit korrekter Attribution raus — ohne dass ich jedes Mal 20 Minuten copy-paste-übersetze?"*
|
||
|
||
**Nicht im Scope (bewusst):**
|
||
- Automatisches Scraping von LinkedIn (ToS-Verstoß, Ban-Risiko für den Company-Account)
|
||
- Vollautomatisches Posten ohne Approval (Reputationsrisiko, KI-Halluzination bei Fachbegriffen)
|
||
- Multi-Source / Multi-Target (erst HS→Edisconet, später generalisieren)
|
||
- Bild-/Video-Assets aus HS-Posts übernehmen (Copyright) — wir erzeugen eigene Visuals oder postieren Text-only
|
||
|
||
## Abgrenzung zu bestehenden Modulen
|
||
|
||
| Modul | Unterschied |
|
||
|-------|-------------|
|
||
| `mana-research` | Research extrahiert Fakten aus vielen Quellen, Social Relay transformiert *einen* spezifischen Post für Republishing |
|
||
| `mana-ai` (Mission Runner) | Wird als Execution-Layer genutzt (Queue, Retry, Cron), nicht dupliziert |
|
||
| `mana-landing-builder` | Erzeugt Landing Pages, nicht Social-Posts |
|
||
| `news-research` | RSS-Feed-Discovery für Consumer, nicht B2B-Social-Republishing |
|
||
|
||
---
|
||
|
||
## Architektur-Entscheidung: Weg 3 (Eigenbau, manueller Intake)
|
||
|
||
Gegen Scraping, gegen Fire-and-forget. Begründung:
|
||
|
||
1. **ToS-konform**: Kein Crawling fremder LinkedIn-Seiten. User pastet Post-URL oder Text manuell.
|
||
2. **Review-Gate**: Draft landet in der Mana-UI, geht erst nach Approve live. Schützt vor KI-Fehlern und falscher Tonalität.
|
||
3. **Wiederverwendung**: `mana-llm` (Übersetzung+Adaption), `mana-ai` (Queue), `mana-auth` (OAuth-Token-Storage) existieren bereits.
|
||
4. **Erweiterbar**: Später zweite Quelle (X, Bluesky, Blog-RSS) oder zweite Ziel-Page ohne Umbau.
|
||
|
||
### Komponenten-Übersicht
|
||
|
||
```
|
||
┌─────────────────────┐ paste URL/text ┌──────────────────────┐
|
||
│ Mana Web Module │ ───────────────────▶ │ apps/api/social-relay│
|
||
│ (social-relay/) │ │ Hono routes │
|
||
└─────────────────────┘ └──────────┬────────────┘
|
||
▲ │
|
||
│ Draft-Review, Approve ▼
|
||
│ ┌──────────────────────┐
|
||
│ │ mana-llm │
|
||
│ │ (translate + adapt) │
|
||
│ └──────────┬────────────┘
|
||
│ │
|
||
│ ┌──────────▼────────────┐
|
||
│ │ services/ │
|
||
│ │ mana-social-relay │ ◀── cron: daily digest?
|
||
│ │ (publish queue) │
|
||
│ └──────────┬────────────┘
|
||
│ │
|
||
│ webhook: published ▼
|
||
│ ┌──────────────────────┐
|
||
└────────────────────────────────────│ LinkedIn UGC API │
|
||
│ (Edisconet page) │
|
||
└──────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Intake: wie kommen HS-Posts rein?
|
||
|
||
Drei Wege, priorisiert:
|
||
|
||
1. **Post-URL paste** (primär): User kopiert `https://www.linkedin.com/posts/howspace_...` → Server holt OpenGraph-Meta (Titel/Beschreibung/Hero-Image URL, Post-Text oft im `og:description`) via `apps/api`-Fetch. Ausreichend für die meisten Text-Posts.
|
||
2. **Manueller Text-Paste** (Fallback): Wenn OG-Extraktion scheitert oder der Post Mehrwert im Carousel/Video hat, fügt der User den Text selbst ein. Screenshot optional als Referenz.
|
||
3. **Watchlist** (später, Phase 2): User trägt Howspace-URL ein, täglicher Cron fragt die OG-Daten der Company-Page an (oder einen HS-eigenen RSS-Feed/Newsletter, falls verfügbar) und zeigt neue Posts als **Vorschlagsliste** — kein Auto-Pull in die Queue.
|
||
|
||
Kein `puppeteer`/`playwright`-Scraping gegen linkedin.com in Phase 1.
|
||
|
||
---
|
||
|
||
## Processing: `mana-llm` Pipeline
|
||
|
||
Ein neuer Prompt-Template in `services/mana-llm/prompts/social-relay.ts`:
|
||
|
||
```ts
|
||
// Inputs: sourcePostText (EN), brandVoice ("edisconet"), sourceUrl
|
||
// Outputs: { germanPost, suggestedHashtags, attributionFooter, rationale }
|
||
```
|
||
|
||
Drei Teilschritte innerhalb *eines* LLM-Calls (günstiger als drei Calls):
|
||
|
||
1. **Übersetzung** EN → DE mit B2B-Ton
|
||
2. **Adaption**: Howspace-spezifische Referenzen (ihre Produktnamen, ihre Cases) werden generalisiert oder auf Edisconet-Kontext gemappt. Claim-Übernahmen (Statistiken, Kundennamen) werden als `[originalzitat]` markiert, nicht umformuliert.
|
||
3. **Attribution-Footer erzwingen**, Template:
|
||
> `Inspiriert von einem Post von @Howspace: [sourceUrl]`
|
||
Harter System-Prompt-Check + Post-Processing-Assertion: wenn der Footer fehlt, Fehler werfen, nicht stillschweigend posten.
|
||
|
||
Model: `claude-sonnet-4-6` (Qualität > Geschwindigkeit, 1–2 Posts/Tag). Temperatur niedrig (0.3) für Konsistenz. Prompt-Cache auf den Brand-Voice-Block, damit wiederholte Läufe günstig bleiben.
|
||
|
||
---
|
||
|
||
## Publishing: LinkedIn Community Management API
|
||
|
||
**Offizielle API, nicht inoffiziell.** Endpoints:
|
||
|
||
- `POST /rest/posts` — Text-Post als Organization
|
||
- OAuth 2.0, Scope `w_organization_social`
|
||
- Access-Token-Lifetime: 60 Tage, Refresh nötig
|
||
|
||
**Onboarding (einmalig):**
|
||
1. LinkedIn Developer App erstellen, Edisconet-Company-Page als Organization verifizieren
|
||
2. App-Review durchlaufen für `w_organization_social` (2–4 Wochen Bearbeitungszeit — **blockt Go-Live, früh starten**)
|
||
3. OAuth-Flow in `services/mana-auth` ergänzen: `auth/linkedin-org/callback` → speichert Token verschlüsselt in `mana_platform.linkedin_tokens` (neue pgSchema)
|
||
4. Refresh-Cron in `mana-social-relay` (alle 50 Tage)
|
||
|
||
**Kein Draft-Endpoint in der API** — LinkedIn bietet keine server-seitigen Drafts für Organizations. Unser Review-Gate läuft deshalb komplett in Mana: `status='approved'` → erst dann schlägt `publish` zu.
|
||
|
||
---
|
||
|
||
## Modul-Struktur (Frontend)
|
||
|
||
```
|
||
apps/mana/apps/web/src/lib/modules/social-relay/
|
||
├── types.ts # SourcePost, Draft, PublishedPost, Status
|
||
├── collections.ts # Dexie: socialRelayDrafts
|
||
├── queries.ts # useDrafts, useDraft(id), usePublished
|
||
├── stores/
|
||
│ └── drafts.svelte.ts # ingestUrl, regenerate, approve, reject, publish
|
||
├── api.ts # Client: /api/v1/social-relay/*
|
||
├── components/
|
||
│ ├── IngestForm.svelte # URL-Paste + Manual-Text-Fallback
|
||
│ ├── DraftCard.svelte # Listeneintrag (Quelle + DE-Preview + Status)
|
||
│ ├── DraftDetail.svelte # Seitenweise Review, Edit-in-place, Regenerate-Button
|
||
│ ├── PublishButton.svelte # approve + publish (mit Bestätigung)
|
||
│ └── HistoryView.svelte # veröffentlichte Posts + LinkedIn-Permalinks
|
||
└── routes/
|
||
├── +page.svelte # Liste aller Drafts
|
||
└── draft/[id]/+page.svelte # Detail + Review
|
||
```
|
||
|
||
## Backend-Routes (`apps/api`)
|
||
|
||
```
|
||
apps/api/src/modules/social-relay/routes.ts
|
||
├── POST /ingest # { sourceUrl | sourceText } → Draft (kein LLM-Call yet)
|
||
├── POST /drafts/:id/generate # triggert mana-llm → schreibt germanPost in Draft
|
||
├── PATCH /drafts/:id # User-Edits am germanPost
|
||
├── POST /drafts/:id/approve # status='approved'
|
||
├── POST /drafts/:id/publish # queue in mana-social-relay → LinkedIn
|
||
├── GET /drafts # Liste (paginated)
|
||
└── GET /published # Liste veröffentlichter Posts + Permalinks
|
||
```
|
||
|
||
Auth: nur `role='founder'` (Admin). Kein Public-Tier.
|
||
|
||
## Neuer Service `services/mana-social-relay`
|
||
|
||
Analog zu `mana-ai` Mission-Runner, aber klein:
|
||
|
||
- Port (neu, in `docs/PORT_SCHEMA.md` eintragen)
|
||
- Queue (Redis) für `publish`-Jobs
|
||
- LinkedIn-API-Client + Token-Refresh-Cron
|
||
- Webhook-Dispatcher zurück an `apps/api` bei Erfolg/Fehler
|
||
- Retry mit exponential backoff (LinkedIn API kann 429en)
|
||
|
||
Alternative: **kein eigener Service**, Publishing direkt in `apps/api` mit `mana-ai`-Mission als Queue. Erspart Container + Port. Entscheidung aufschieben bis M2 — wenn die Publish-Logik unter ~100 Zeilen bleibt, gewinnt Alternative.
|
||
|
||
---
|
||
|
||
## Data Model
|
||
|
||
**Dexie (`apps/mana/apps/web/src/lib/data/database.ts`) — v27:**
|
||
|
||
```ts
|
||
socialRelayDrafts: Dexie.Table<LocalSocialRelayDraft, string>
|
||
|
||
type LocalSocialRelayDraft = {
|
||
id: string
|
||
sourceUrl: string // encrypted
|
||
sourceText: string // encrypted (OG-extracted or manual)
|
||
sourceAuthor: string // "Howspace" — plaintext (brand name)
|
||
germanPost: string // encrypted (LLM output, user-editable)
|
||
suggestedHashtags: string[] // plaintext
|
||
attributionFooter: string // encrypted
|
||
status: 'draft' | 'generating' | 'ready' | 'approved' | 'publishing' | 'published' | 'failed' | 'rejected'
|
||
linkedInPostId: string | null // plaintext, nach publish
|
||
linkedInPermalink: string | null
|
||
failureReason: string | null // encrypted
|
||
createdAt: number
|
||
updatedAt: number
|
||
publishedAt: number | null
|
||
}
|
||
```
|
||
|
||
Encryption-Registry-Entry in `apps/mana/apps/web/src/lib/data/crypto/registry.ts`: Felder mit `encrypted` oben.
|
||
|
||
**Postgres (`mana_platform.social_relay`):**
|
||
|
||
```sql
|
||
CREATE SCHEMA social_relay;
|
||
CREATE TABLE social_relay.drafts ( ... ); -- sync target
|
||
CREATE TABLE social_relay.linkedin_tokens ( -- OAuth-Tokens
|
||
organization_id TEXT PRIMARY KEY,
|
||
access_token_encrypted BYTEA NOT NULL,
|
||
refresh_token_encrypted BYTEA NOT NULL,
|
||
expires_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
```
|
||
|
||
Sync via `mana-sync` wie alle anderen Module; `appId='social-relay'`.
|
||
|
||
---
|
||
|
||
## Milestones
|
||
|
||
### M1 — Skelett + Intake (1–2 Tage)
|
||
- Modul registriert in `mana-apps.ts` (`tier: 'founder'`)
|
||
- Route `/social-relay`, leere Liste
|
||
- Dexie v27 + Encryption-Registry
|
||
- `POST /ingest` mit OG-Extraction (ohne LLM)
|
||
- Manuelle Text-Paste als Fallback
|
||
- Draft-Liste zeigt Quelle, noch kein generiertes Deutsch
|
||
|
||
### M2 — LLM-Pipeline (2–3 Tage)
|
||
- `services/mana-llm`: `social-relay.ts` Prompt-Template
|
||
- `POST /drafts/:id/generate`
|
||
- DraftDetail-View mit Side-by-Side EN/DE, Regenerate-Button
|
||
- Attribution-Footer-Check (Assertion im Backend)
|
||
- Edit-in-place
|
||
|
||
### M3 — LinkedIn OAuth + Publish (Dauer hängt von App-Review ab)
|
||
- LinkedIn Developer App einrichten
|
||
- **App-Review-Antrag stellen → läuft 2–4 Wochen, blockt M3** (parallel zu M1/M2 starten)
|
||
- OAuth-Flow in `mana-auth`
|
||
- `POST /drafts/:id/publish` → direkter API-Call (noch ohne Queue)
|
||
- Erfolg → Permalink in Draft, Status='published'
|
||
|
||
### M4 — Queue + Retry (1 Tag)
|
||
- Entscheidung: eigener Service vs. `mana-ai`-Mission
|
||
- Retry mit backoff
|
||
- History-View mit allen Posts + LinkedIn-Permalinks
|
||
|
||
### M5 — Watchlist / Vorschläge (optional, Phase 2)
|
||
- Howspace-Watchlist-Eintrag (URL der Company-Page)
|
||
- Täglicher Cron → OG-Scrape der öffentlichen Übersicht, Diff gegen bekannte Post-URLs
|
||
- Neue Posts als **Vorschläge** in der UI (1-Tap Ingest), kein Auto-Pull
|
||
|
||
---
|
||
|
||
## Offene Fragen
|
||
|
||
- **LinkedIn App-Review**: Wie streng ist `w_organization_social`-Approval für kleine Firmen? Erfordert u.U. eine Produkt-Demo. → Früh klären, ggf. parallel Weg-1-Workaround (Make.com + LinkedIn-Integration von Make, die eigenen OAuth-Scope mitbringt) als Übergangslösung für M3.
|
||
- **Bilder**: HS-Posts haben oft ein Hero-Image. Copyright → wir nutzen sie *nicht*. Eigenes Visual? Dann braucht M2 einen `mana-image-gen`-Hook oder manuellen Upload via `uload`. Phase-1-Entscheidung: **Text-only**, Bildoption in M5.
|
||
- **Brand Voice**: Braucht ein Edisconet-Voice-Dokument (Tonalität, Claims, verbotene Wörter) als System-Prompt-Kontext. Vor M2 zu liefern. Ablage: `services/mana-llm/prompts/voices/edisconet.md`.
|
||
- **Language-Fallback**: Was wenn HS-Post bereits Deutsch ist? → Erkennen (franc oder LLM selbst), dann nur Adaption, keine Übersetzung.
|
||
- **Eigener Service vs. inline in `apps/api`**: In M4 entscheiden.
|
||
- **Rate-Limiting**: LinkedIn erlaubt ~25 Posts/Tag/Page. Irrelevant für unseren Use-Case (1–5/Woche), aber in Queue einbauen.
|
||
- **Compliance-Footer**: Reicht "Inspiriert von" oder brauchen wir expliziteres "Basierend auf [URL]"? Rechtsmeinung bei Content-Ownership einholen, sobald M2 läuft.
|
||
|
||
---
|
||
|
||
## Zusammenfassung für "Was zuerst?"
|
||
|
||
Zwei Dinge **parallel** starten, weil LinkedIn App-Review lange dauert:
|
||
|
||
1. **Tag 1**: LinkedIn Developer App anlegen, Edisconet-Page verifizieren, App-Review beantragen.
|
||
2. **Tag 1–4**: M1 + M2 bauen (Intake + LLM-Pipeline). Ergebnis ist bereits nutzbar: Draft → Copy-to-Clipboard → manuell auf LinkedIn posten. Spart schon ~80% der Zeit gegenüber Vollmanuell.
|
||
3. **Sobald Review durch**: M3 (Auto-Publish) aktivieren.
|