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) {
$results[] = $actionService->handle($user, $action);
}
$successCount = collect($results)->where('status', 'success')->count();
$totalCount = max(1, count($results));
$actionResult = [
'status' => collect($results)->every(fn ($r) => $r['status'] === 'success') ? 'success' : 'partial',
'status' => $successCount === count($results) ? 'success' : ($successCount > 0 ? 'partial' : 'failed'),
'results' => $results,
'success_count' => $successCount,
'total_count' => $totalCount,
];
} elseif (isset($parsed['type']) && $parsed['type'] !== 'chat') {
$actionService = new AgentActionService();
@ -180,9 +184,16 @@ class AgentChatController extends Controller
$shouldLog = true;
if ($isAction) {
$credits = (($actionResult['status'] ?? '') === 'error')
? 0
: $this->calculateCredits($usage, $aiConfig, $type);
$status = $actionResult['status'] ?? 'error';
if ($status !== 'success' && $status !== 'partial') {
$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) {
$credits = 5;
} else {

View File

@ -129,10 +129,15 @@ class Index extends Component
}
$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 = [
'status' => 'success',
'status' => $successCount === count($results) ? 'success' : ($successCount > 0 ? 'partial' : 'failed'),
'message' => implode(' | ', $messages) ?: 'Erledigt!',
'meta' => ['actions' => count($actions)],
];
@ -209,7 +214,9 @@ class Index extends Component
$this->dispatch('agent:sent');
} 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);
if ($result['status'] === 'success' && ($parsed['type'] ?? '') === 'event') {

View File

@ -48,9 +48,14 @@ class AgentActionService
}
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, [
'title' => $data['title'] ?? 'Termin',
'title' => $title,
'start' => $data['start'],
'end' => $data['end'],
'duration_minutes' => Carbon::parse($data['start'])
@ -67,9 +72,14 @@ class AgentActionService
*/
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, [
'title' => $data['title'] ?? 'Termin',
'title' => $title,
'start' => $data['datetime'],
'duration_minutes' => $data['duration_minutes'] ?? null,
'ai_duration' => $data['ai_duration'] ?? null,
@ -92,6 +102,11 @@ class AgentActionService
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([
'user_id' => $user->id,
'title' => $data['title'] ?? null,
@ -127,6 +142,10 @@ class AgentActionService
{
$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
$dueAt = null;
if (!empty($data['due_at'])) {