Fix: korrekte Credit-Verrechnung + Duplikat-Schutz

- Credits nur bei status='success': Konflikt/Fehler/Failed → 0 Credits
- Multi-Action: Credits proportional zu erfolgreichen Aktionen
- AgentActionService: Duplikat-Check vor Event/Task/Notiz-Anlage
- Multi-Action-Status: 'success'/'partial'/'failed' statt immer 'success'
- Gilt für Web (Livewire/Agent/Index) und API (AgentChatController)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-04-19 07:31:36 +02:00
parent 24f43be627
commit 68aa62db6f
3 changed files with 48 additions and 11 deletions

View File

@ -137,9 +137,13 @@ class AgentChatController extends Controller
foreach ($parsed['_multi'] as $action) { foreach ($parsed['_multi'] as $action) {
$results[] = $actionService->handle($user, $action); $results[] = $actionService->handle($user, $action);
} }
$successCount = collect($results)->where('status', 'success')->count();
$totalCount = max(1, count($results));
$actionResult = [ $actionResult = [
'status' => collect($results)->every(fn ($r) => $r['status'] === 'success') ? 'success' : 'partial', 'status' => $successCount === count($results) ? 'success' : ($successCount > 0 ? 'partial' : 'failed'),
'results' => $results, 'results' => $results,
'success_count' => $successCount,
'total_count' => $totalCount,
]; ];
} elseif (isset($parsed['type']) && $parsed['type'] !== 'chat') { } elseif (isset($parsed['type']) && $parsed['type'] !== 'chat') {
$actionService = new AgentActionService(); $actionService = new AgentActionService();
@ -180,9 +184,16 @@ class AgentChatController extends Controller
$shouldLog = true; $shouldLog = true;
if ($isAction) { if ($isAction) {
$credits = (($actionResult['status'] ?? '') === 'error') $status = $actionResult['status'] ?? 'error';
? 0 if ($status !== 'success' && $status !== 'partial') {
: $this->calculateCredits($usage, $aiConfig, $type); $credits = 0;
} elseif (isset($actionResult['success_count'], $actionResult['total_count'])) {
// Multi-Action: proportional zu erfolgreichen Aktionen
$full = $this->calculateCredits($usage, $aiConfig, $type);
$credits = (int) ceil($full * $actionResult['success_count'] / $actionResult['total_count']);
} else {
$credits = $this->calculateCredits($usage, $aiConfig, $type);
}
} elseif ($historyCount === 0) { } elseif ($historyCount === 0) {
$credits = 5; $credits = 5;
} else { } else {

View File

@ -129,12 +129,17 @@ class Index extends Component
} }
$duration = round((microtime(true) - $startTime) * 1000); $duration = round((microtime(true) - $startTime) * 1000);
$credits = $this->calculateCredits(['type' => 'multi'], $duration, $usage); $successCount = collect($results)->where('status', 'success')->count();
$totalCount = max(1, count($results));
$fullCredits = $this->calculateCredits(['type' => 'multi'], $duration, $usage);
$credits = $successCount > 0
? (int) ceil($fullCredits * $successCount / $totalCount)
: 0;
$combinedResult = [ $combinedResult = [
'status' => 'success', 'status' => $successCount === count($results) ? 'success' : ($successCount > 0 ? 'partial' : 'failed'),
'message' => implode(' | ', $messages) ?: 'Erledigt!', 'message' => implode(' | ', $messages) ?: 'Erledigt!',
'meta' => ['actions' => count($actions)], 'meta' => ['actions' => count($actions)],
]; ];
$this->logConversationAction($userMessage, ['type' => 'multi'], $combinedResult, $model, $duration, $credits); $this->logConversationAction($userMessage, ['type' => 'multi'], $combinedResult, $model, $duration, $credits);
@ -209,7 +214,9 @@ class Index extends Component
$this->dispatch('agent:sent'); $this->dispatch('agent:sent');
} else { } else {
$credits = $this->calculateCredits($parsed, $duration, $usage); $credits = ($result['status'] === 'success')
? $this->calculateCredits($parsed, $duration, $usage)
: 0;
$this->logConversationAction($userMessage, $parsed, $result, $model, $duration, $credits); $this->logConversationAction($userMessage, $parsed, $result, $model, $duration, $credits);
if ($result['status'] === 'success' && ($parsed['type'] ?? '') === 'event') { if ($result['status'] === 'success' && ($parsed['type'] ?? '') === 'event') {

View File

@ -48,9 +48,14 @@ class AgentActionService
} }
if (!empty($data['start']) && !empty($data['end'])) { if (!empty($data['start']) && !empty($data['end'])) {
$title = $data['title'] ?? 'Termin';
$dateStr = Carbon::parse($data['start'])->toDateString();
if (Event::where('user_id', $user->id)->where('title', $title)->whereDate('starts_at', $dateStr)->exists()) {
return ['status' => 'failed', 'message' => "Termin \"{$title}\" existiert an diesem Tag bereits.", 'meta' => []];
}
return $planner->plan($user, array_merge($data, [ return $planner->plan($user, array_merge($data, [
'title' => $data['title'] ?? 'Termin', 'title' => $title,
'start' => $data['start'], 'start' => $data['start'],
'end' => $data['end'], 'end' => $data['end'],
'duration_minutes' => Carbon::parse($data['start']) 'duration_minutes' => Carbon::parse($data['start'])
@ -67,9 +72,14 @@ class AgentActionService
*/ */
if (!empty($data['datetime'])) { if (!empty($data['datetime'])) {
$title = $data['title'] ?? 'Termin';
$dateStr = Carbon::parse($data['datetime'])->toDateString();
if (Event::where('user_id', $user->id)->where('title', $title)->whereDate('starts_at', $dateStr)->exists()) {
return ['status' => 'failed', 'message' => "Termin \"{$title}\" existiert an diesem Tag bereits.", 'meta' => []];
}
return $planner->plan($user, array_merge($data, [ return $planner->plan($user, array_merge($data, [
'title' => $data['title'] ?? 'Termin', 'title' => $title,
'start' => $data['datetime'], 'start' => $data['datetime'],
'duration_minutes' => $data['duration_minutes'] ?? null, 'duration_minutes' => $data['duration_minutes'] ?? null,
'ai_duration' => $data['ai_duration'] ?? null, 'ai_duration' => $data['ai_duration'] ?? null,
@ -92,6 +102,11 @@ class AgentActionService
protected static function handleNote(User $user, array $data): array protected static function handleNote(User $user, array $data): array
{ {
$content = $data['content'] ?? $data['text'] ?? '';
if ($content && Note::where('user_id', $user->id)->where('content', $content)->whereDate('created_at', today())->exists()) {
return ['status' => 'failed', 'message' => 'Diese Notiz wurde heute bereits angelegt.', 'meta' => []];
}
$note = Note::create([ $note = Note::create([
'user_id' => $user->id, 'user_id' => $user->id,
'title' => $data['title'] ?? null, 'title' => $data['title'] ?? null,
@ -127,6 +142,10 @@ class AgentActionService
{ {
$title = $data['title'] ?? $data['description'] ?? 'Aufgabe'; $title = $data['title'] ?? $data['description'] ?? 'Aufgabe';
if (Task::where('user_id', $user->id)->where('title', $title)->whereDate('created_at', today())->exists()) {
return ['status' => 'failed', 'message' => "Aufgabe \"{$title}\" wurde heute bereits angelegt.", 'meta' => []];
}
// due_at: AI schickt due_at, due_date oder datetime // due_at: AI schickt due_at, due_date oder datetime
$dueAt = null; $dueAt = null;
if (!empty($data['due_at'])) { if (!empty($data['due_at'])) {