Livia arbeitet nicht mehr neben dem Leadbestand, sondern in ihn hinein. - Systemzugänge tragen Titel, Beschreibung und Adresse; die Liste zeigt je Eintrag nur noch Aktiv/Inaktiv und Lesen/Schreiben — kein Berechtigungsbalken, kein Auge-Icon, keine Lese-/Schreibgruppen - Die Quellenregistry ist dynamisch: Der Lauf liest genau die aktiven Lesezugänge mit Adresse aus Livias Personalblatt. Für die drei gepflegten Quellen greift weiterhin ihr eigener Leser, für alles andere ein allgemeiner — eine Übersichtsseite, bis zu zehn Unterseiten, kein Spider. Freie Adressen werden auf http/https geprüft und gegen interne Netze gesperrt - Manuell abgelegte PDF- und Word-Dokumente fliessen in denselben Lauf. Text wird beim Ablegen nativ extrahiert (ZIP + Flate über DecompressionStream, ohne neue Abhängigkeit), Datei und Text liegen in IndexedDB - Neuer harter Filter «Zeitliche Relevanz»: das Modell beurteilt die Ereigniszeit der Veränderung, nicht das Publikationsdatum. «unknown» fällt bewusst nicht durch — ein erfundenes Datum wäre die schlechtere Antwort - Quellentypen und Beobachtungsraum sind Chips mit Freitext; jeder Wert lässt sich einzeln entfernen, auch die vorgegebenen - Info-Knopf bei «Zeitliche Relevanz» und «Mindestrelevanz» erklärt, wie der Wert entsteht — ohne erfundene Prozentgewichte - Gefundene Leads gehen in den zentralen Signalbestand statt in eine zweite Ergebnisliste; die Herkunft steht am Signal (origin: RESEARCH_RUN) - Der Lauf erzeugt einen echten Protokolleintrag mit den Zahlen des Laufs - Lead-Detail: zeitliche Einordnung mit Fundstelle, semantisches Suchprofil bei Chancen mit «Matching starten», und bei Risiko ausdrücklich «Kein internes Objekt eindeutig zugeordnet» statt einer beliebigen Mock-Immobilie - Aufgabenliste nach §11 bereinigt, «Unsichere Signale in Review Queue legen» von Nora übernommen Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
/**
|
||
* Property On — «Teamübersicht»: digitale Mitarbeiter.
|
||
*
|
||
* Namensgebung: Der Entitätstyp heisst `TeamAgent`, nicht `Agent`. Im Repo existiert
|
||
* bereits `PowerOn` (AI-Backend-Proxy) und Claude-Code legt Worktrees unter `agent-*`
|
||
* ab — ein nackter Typ `Agent` wäre in Suchen praktisch nicht auffindbar. Alle
|
||
* Satellitentypen tragen deshalb das Präfix `Agent*`; generische Namen wie `Status`,
|
||
* `Priority` oder `Channel` sind tabu, weil `src/domain/index.ts` per `export *`
|
||
* bündelt und dort bereits `ReviewStatus`, `SourceType`, `RiskLevel` u. a. liegen.
|
||
*
|
||
* Fachliche Wahrheit ist der Agenten-Katalog: Rollen, Personalnummern und
|
||
* Zuständigkeiten der fünf Kernagenten werden nicht verändert.
|
||
*/
|
||
|
||
// ── Status & Autonomie ────────────────────────────────────────────────────────
|
||
|
||
export const AgentStatus = {
|
||
ACTIVE: 'ACTIVE',
|
||
BUILDING: 'BUILDING',
|
||
PAUSED: 'PAUSED',
|
||
} as const
|
||
export type AgentStatus = typeof AgentStatus[keyof typeof AgentStatus]
|
||
|
||
export const AgentAutonomyLevel = {
|
||
AUTONOMOUS: 'AUTONOMOUS',
|
||
APPROVAL_REQUIRED: 'APPROVAL_REQUIRED',
|
||
PROPOSAL_ONLY: 'PROPOSAL_ONLY',
|
||
} as const
|
||
export type AgentAutonomyLevel = typeof AgentAutonomyLevel[keyof typeof AgentAutonomyLevel]
|
||
|
||
// ── Kanäle ────────────────────────────────────────────────────────────────────
|
||
|
||
export const AgentChannelType = {
|
||
EMAIL: 'EMAIL',
|
||
TEAMS: 'TEAMS',
|
||
WHATSAPP: 'WHATSAPP',
|
||
WORKSPACE_CHAT: 'WORKSPACE_CHAT',
|
||
CALENDAR: 'CALENDAR',
|
||
WEB_FORM: 'WEB_FORM',
|
||
PORTAL_LEAD_MAIL: 'PORTAL_LEAD_MAIL',
|
||
PHONE_VOICE: 'PHONE_VOICE',
|
||
PHONE_NOTE: 'PHONE_NOTE',
|
||
AUDIO_OUTPUT: 'AUDIO_OUTPUT',
|
||
STRUCTURED_MAIL_FALLBACK: 'STRUCTURED_MAIL_FALLBACK',
|
||
} as const
|
||
export type AgentChannelType = typeof AgentChannelType[keyof typeof AgentChannelType]
|
||
|
||
export const AgentChannelDirection = {
|
||
INBOUND: 'INBOUND',
|
||
OUTBOUND: 'OUTBOUND',
|
||
BOTH: 'BOTH',
|
||
} as const
|
||
export type AgentChannelDirection = typeof AgentChannelDirection[keyof typeof AgentChannelDirection]
|
||
|
||
export const AgentConnectionStatus = {
|
||
CONNECTED: 'CONNECTED',
|
||
DISCONNECTED: 'DISCONNECTED',
|
||
ROADMAP: 'ROADMAP',
|
||
} as const
|
||
export type AgentConnectionStatus = typeof AgentConnectionStatus[keyof typeof AgentConnectionStatus]
|
||
|
||
/** Simulierte Kanalkonfiguration — es werden nie echte Zugangsdaten erfasst. */
|
||
export interface AgentChannelConfig {
|
||
displayName: string
|
||
senderAddress?: string
|
||
inboxAddress?: string
|
||
defaultRecipients: string[]
|
||
autoReplyEnabled: boolean
|
||
}
|
||
|
||
export interface AgentChannel {
|
||
id: string
|
||
type: AgentChannelType
|
||
direction: AgentChannelDirection
|
||
description: string
|
||
status: AgentConnectionStatus
|
||
enabled: boolean
|
||
/** Kennzeichnet im Katalog als «optional» geführte Kanäle. */
|
||
optional?: boolean
|
||
config: AgentChannelConfig
|
||
}
|
||
|
||
// ── Systeme ───────────────────────────────────────────────────────────────────
|
||
|
||
export const AgentSystemType = {
|
||
M365_EXCHANGE: 'M365_EXCHANGE',
|
||
OUTLOOK: 'OUTLOOK',
|
||
SHAREPOINT: 'SHAREPOINT',
|
||
ONEDRIVE: 'ONEDRIVE',
|
||
FILESHARE: 'FILESHARE',
|
||
DMS: 'DMS',
|
||
CALENDAR: 'CALENDAR',
|
||
IMMOTOP2: 'IMMOTOP2',
|
||
ABACUS_ABAIMMO: 'ABACUS_ABAIMMO',
|
||
RIMO_R5: 'RIMO_R5',
|
||
GARAIO_REM: 'GARAIO_REM',
|
||
CRM: 'CRM',
|
||
ZEFIX: 'ZEFIX',
|
||
SHAB: 'SHAB',
|
||
PUBLIC_WEB: 'PUBLIC_WEB',
|
||
PORTAL_LEAD_INBOX: 'PORTAL_LEAD_INBOX',
|
||
WORKSPACE: 'WORKSPACE',
|
||
} as const
|
||
export type AgentSystemType = typeof AgentSystemType[keyof typeof AgentSystemType]
|
||
|
||
export const AgentAccessLevel = {
|
||
READ: 'READ',
|
||
WRITE: 'WRITE',
|
||
READ_WRITE: 'READ_WRITE',
|
||
} as const
|
||
export type AgentAccessLevel = typeof AgentAccessLevel[keyof typeof AgentAccessLevel]
|
||
|
||
export interface AgentSystem {
|
||
id: string
|
||
type: AgentSystemType
|
||
access: AgentAccessLevel
|
||
status: AgentConnectionStatus
|
||
/**
|
||
* Ob der Mitarbeiter diesen Zugang derzeit nutzt. Optional, weil der Katalog
|
||
* die Zugänge ohne Schalter führt — fehlt das Feld, gilt der Zugang als aktiv.
|
||
*/
|
||
enabled?: boolean
|
||
/**
|
||
* Eigener Titel der Quelle (Runde 10, §2.1). Der Katalog benennt seine
|
||
* Zugänge über `type`; selbst angebundene Quellen haben keinen passenden
|
||
* Typ und tragen ihren Namen deshalb hier. Fehlt der Titel, gilt das Label
|
||
* des Typs — so bleiben die Katalogeinträge unverändert.
|
||
*/
|
||
title?: string
|
||
/**
|
||
* Adresse der Quelle. Bei einer aktiven Lesequelle ist das die Seite, die
|
||
* Livias Recherchelauf tatsächlich abruft — der angezeigte Link und der
|
||
* gelesene Link sind damit derselbe Wert.
|
||
*/
|
||
url?: string
|
||
/** Wofür der Agent dieses System nutzt — in Alltagssprache, ohne KI-Jargon. */
|
||
usage: string
|
||
/** Klartext zur Berechtigung, z. B. «Schreibt erst nach Freigabe». */
|
||
permissionNote: string
|
||
}
|
||
|
||
// ── Aufgaben ──────────────────────────────────────────────────────────────────
|
||
|
||
export interface AgentTask {
|
||
id: string
|
||
title: string
|
||
description: string
|
||
enabled: boolean
|
||
/** Zeitplan oder auslösendes Ereignis, z. B. «täglich 06:00» oder «bei Eingang». */
|
||
schedule: string
|
||
requiresApproval: boolean
|
||
dependsOnChannel?: AgentChannelType
|
||
dependsOnSystem?: AgentSystemType
|
||
}
|
||
|
||
// ── Einstellungen ─────────────────────────────────────────────────────────────
|
||
|
||
export const AgentSettingKind = {
|
||
TEXT: 'TEXT',
|
||
NUMBER: 'NUMBER',
|
||
SELECT: 'SELECT',
|
||
MULTI_SELECT: 'MULTI_SELECT',
|
||
BOOLEAN: 'BOOLEAN',
|
||
TIME: 'TIME',
|
||
/**
|
||
* Mehrfachauswahl als Chips, jeder einzeln entfernbar (Runde 10, §§2.7–2.9).
|
||
*
|
||
* Der Unterschied zu `MULTI_SELECT` ist nicht bloss die Darstellung: Ein
|
||
* Auswahlfeld kann nur anbieten, was jemand vorher als Option hinterlegt
|
||
* hat. Quellentypen und Beobachtungsräume sind aber genau das, was sich von
|
||
* Haus zu Haus unterscheidet — sie brauchen ein Feld, in das man schreiben
|
||
* kann, was man meint.
|
||
*/
|
||
CHIPS: 'CHIPS',
|
||
/** Abgelegte Dokumente als Datenbasis der Recherche (Runde 10, §2.6). */
|
||
DOCUMENTS: 'DOCUMENTS',
|
||
} as const
|
||
export type AgentSettingKind = typeof AgentSettingKind[keyof typeof AgentSettingKind]
|
||
|
||
export const AgentSettingGroup = {
|
||
GENERAL: 'GENERAL',
|
||
APPROVAL: 'APPROVAL',
|
||
DELIVERY: 'DELIVERY',
|
||
SPECIFIC: 'SPECIFIC',
|
||
} as const
|
||
export type AgentSettingGroup = typeof AgentSettingGroup[keyof typeof AgentSettingGroup]
|
||
|
||
export interface AgentSettingOption {
|
||
value: string
|
||
label: string
|
||
}
|
||
|
||
export interface AgentSetting {
|
||
id: string
|
||
label: string
|
||
description?: string
|
||
kind: AgentSettingKind
|
||
group: AgentSettingGroup
|
||
value: string | number | boolean | string[]
|
||
options?: AgentSettingOption[]
|
||
unit?: string
|
||
min?: number
|
||
max?: number
|
||
/** Fachlich nicht abschaltbar — z. B. Sinas Quellenzwang. */
|
||
locked?: boolean
|
||
lockedReason?: string
|
||
/**
|
||
* Erklärt auf Knopfdruck, wie ein Wert zustande kommt (Runde 10, §2.10).
|
||
*
|
||
* Gehört ans Datenmodell und nicht in die Komponente: Was ein Schwellenwert
|
||
* bedeutet, weiss das Personalblatt, nicht das Eingabefeld. Und ein Text,
|
||
* der eine Berechnung beschreibt, muss neben der Berechnung stehen, damit
|
||
* beide zusammen geändert werden.
|
||
*/
|
||
infoText?: string
|
||
/**
|
||
* Nur bei `CHIPS`: ob eigene Werte erfasst werden dürfen. Bei einer
|
||
* fachlich abgeschlossenen Liste — etwa den drei Zeitfenstern — bleibt es
|
||
* aus, sonst entstünde ein vierter Wert, den niemand auswerten kann.
|
||
*/
|
||
allowCustom?: boolean
|
||
}
|
||
|
||
// ── Kennzahlen & Profil ───────────────────────────────────────────────────────
|
||
|
||
export interface AgentMetric {
|
||
id: string
|
||
label: string
|
||
value: string
|
||
hint?: string
|
||
}
|
||
|
||
/** Informationsbox unter dem Kopfbereich des Personalblatts (§7.6). */
|
||
export interface AgentProfile {
|
||
purpose: string
|
||
input: string[]
|
||
coreFlow: string[]
|
||
output: string[]
|
||
}
|
||
|
||
// ── Entität ───────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Farbton des Avatars. Bewusst ein Token-Schlüssel statt eines Hex-Werts —
|
||
* `npm run check:tokens` liegt über dem Schwellwert und darf nicht wachsen.
|
||
*/
|
||
export const AgentAvatarTone = {
|
||
BRAND: 'BRAND',
|
||
SUCCESS: 'SUCCESS',
|
||
INFO: 'INFO',
|
||
SIGNAL: 'SIGNAL',
|
||
WARNING: 'WARNING',
|
||
GOLD: 'GOLD',
|
||
SLATE: 'SLATE',
|
||
} as const
|
||
export type AgentAvatarTone = typeof AgentAvatarTone[keyof typeof AgentAvatarTone]
|
||
|
||
export interface TeamAgent {
|
||
id: string
|
||
name: string
|
||
role: string
|
||
personnelNumber: string
|
||
department: string
|
||
email: string
|
||
avatarTone: AgentAvatarTone
|
||
status: AgentStatus
|
||
autonomyLevel: AgentAutonomyLevel
|
||
/** Ein Satz für Karte und Listeneintrag. */
|
||
shortDescription: string
|
||
/** Freitext zur Autonomie, wo der Katalog differenziert («autonom beim Erinnern, Freigabe bei Eskalationen»). */
|
||
autonomyNote: string
|
||
profile: AgentProfile
|
||
responsibilities: string[]
|
||
tasks: AgentTask[]
|
||
channels: AgentChannel[]
|
||
systems: AgentSystem[]
|
||
settings: AgentSetting[]
|
||
metrics: AgentMetric[]
|
||
/** Kurzkennzahl für die Agentenkarte auf der Teamübersicht. */
|
||
headlineMetricId: string
|
||
lastRun?: string
|
||
}
|
||
|
||
// ── Schreib-Inputs ────────────────────────────────────────────────────────────
|
||
|
||
export interface UpdateTeamAgentInput {
|
||
status?: AgentStatus
|
||
tasks?: AgentTask[]
|
||
channels?: AgentChannel[]
|
||
/** Systeme sind seit Runde 10 (§2) genauso bearbeitbar wie Aufgaben und Kanäle. */
|
||
systems?: AgentSystem[]
|
||
settings?: AgentSetting[]
|
||
}
|