value)); $adminId = (int) ($params['admin_id'] ?? $this->userId); return (int) AiGenerationModel::insertGetId([ 'admin_id' => $adminId, 'scene' => $scene, 'name' => mb_substr(trim((string) ($params['name'] ?? '')), 0, 128), 'provider' => trim((string) ($params['provider'] ?? '')), 'model' => trim((string) ($params['model'] ?? '')), 'status' => AiGenerationStatusEnum::RUNNING->value, 'api_key_id' => (int) ($params['api_key_id'] ?? 0), 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0, 'usage_json' => null, 'started_at' => $now, 'finished_at' => 0, 'duration_ms' => 0, 'error_msg' => '', 'input_snapshot' => $this->encodeJson($params['input_snapshot'] ?? null), 'result_json' => null, 'raw_response' => null, 'created_at' => $now, 'updated_at' => 0, 'deleted_at' => 0, ]); } /** * 标记成功并写入结果 / token * * @param array{ * provider?:string, * model?:string, * api_key_id?:int, * usage?:array, * result_json?:array|null, * raw_response?:string|null, * name?:string * } $payload */ public function markSuccess(int $id, array $payload = []): bool { if ($id <= 0) { return false; } $row = AiGenerationModel::where('id', $id)->where('deleted_at', 0)->first(); if (!$row) { return false; } $usage = is_array($payload['usage'] ?? null) ? $payload['usage'] : []; $finishedAt = time(); $startedAt = (int) ($row->getRawOriginal('started_at') ?? 0); $data = [ 'status' => AiGenerationStatusEnum::SUCCESS->value, 'finished_at' => $finishedAt, 'duration_ms' => $this->calcDurationMs($startedAt, $finishedAt), 'prompt_tokens' => (int) ($usage['prompt_tokens'] ?? 0), 'completion_tokens' => (int) ($usage['completion_tokens'] ?? 0), 'total_tokens' => (int) ($usage['total_tokens'] ?? 0), 'usage_json' => $this->encodeJson($usage ?: null), 'result_json' => $this->encodeJson($payload['result_json'] ?? null), 'raw_response' => $this->clipText((string) ($payload['raw_response'] ?? ''), 65000), 'error_msg' => '', 'updated_at' => $finishedAt, ]; if (!empty($payload['provider'])) { $data['provider'] = (string) $payload['provider']; } if (!empty($payload['model'])) { $data['model'] = (string) $payload['model']; } if (array_key_exists('api_key_id', $payload)) { $data['api_key_id'] = (int) $payload['api_key_id']; } if (!empty($payload['name'])) { $data['name'] = mb_substr(trim((string) $payload['name']), 0, 128); } return (bool) AiGenerationModel::where('id', $id)->update($data); } /** * 标记失败 */ public function markFail(int $id, string $errorMsg, array $payload = []): bool { if ($id <= 0) { return false; } $row = AiGenerationModel::where('id', $id)->where('deleted_at', 0)->first(); if (!$row) { return false; } $finishedAt = time(); $startedAt = (int) ($row->getRawOriginal('started_at') ?? 0); $data = [ 'status' => AiGenerationStatusEnum::FAIL->value, 'finished_at' => $finishedAt, 'duration_ms' => $this->calcDurationMs($startedAt, $finishedAt), 'error_msg' => mb_substr(trim($errorMsg), 0, 1000), 'raw_response' => $this->clipText((string) ($payload['raw_response'] ?? ''), 65000), 'updated_at' => $finishedAt, ]; if (!empty($payload['provider'])) { $data['provider'] = (string) $payload['provider']; } if (!empty($payload['model'])) { $data['model'] = (string) $payload['model']; } if (array_key_exists('api_key_id', $payload)) { $data['api_key_id'] = (int) $payload['api_key_id']; } $usage = is_array($payload['usage'] ?? null) ? $payload['usage'] : []; if (!empty($usage)) { $data['prompt_tokens'] = (int) ($usage['prompt_tokens'] ?? 0); $data['completion_tokens'] = (int) ($usage['completion_tokens'] ?? 0); $data['total_tokens'] = (int) ($usage['total_tokens'] ?? 0); $data['usage_json'] = $this->encodeJson($usage); } return (bool) AiGenerationModel::where('id', $id)->update($data); } /** * 分页列表(管理端生成记录) */ public function getPageListForAdmin(): array { $this->model = AiGenerationModel::class; $this->selectField = [ 'id', 'admin_id', 'scene', 'name', 'provider', 'model', 'status', 'api_key_id', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'started_at', 'finished_at', 'duration_ms', 'error_msg', 'created_at', ]; $this->queryField = [ 'scene' => '=', 'status' => '=', 'provider' => '=', 'name' => 'like', ]; $this->orderBy = [ 'name' => 'id', 'sort' => 'desc', ]; // 重置 where,避免单例污染 $this->where = [['deleted_at', '=', 0]]; $list = $this->getPageList(); foreach ($list['items'] as &$item) { $status = (int) ($item['status'] ?? 0); $scene = (string) ($item['scene'] ?? ''); $item['status_text'] = AiGenerationStatusEnum::tryFrom($status)?->description() ?? '未知'; $item['scene_text'] = AiGenerationSceneEnum::tryFrom($scene)?->description() ?? $scene; } unset($item); return $list; } /** * 详情(含 input/result,不含超大 raw 时可按需) */ public function getDetailForAdmin(int $id): array { $row = AiGenerationModel::where('id', $id)->where('deleted_at', 0)->first(); if (!$row) { $this->utils->notFound('记录不存在'); } $data = $row->toArray(); $status = (int) ($data['status'] ?? 0); $scene = (string) ($data['scene'] ?? ''); $data['status_text'] = AiGenerationStatusEnum::tryFrom($status)?->description() ?? '未知'; $data['scene_text'] = AiGenerationSceneEnum::tryFrom($scene)?->description() ?? $scene; return $data; } private function calcDurationMs(int $startedAt, int $finishedAt): int { if ($startedAt <= 0 || $finishedAt <= 0 || $finishedAt < $startedAt) { return 0; } return ($finishedAt - $startedAt) * 1000; } private function encodeJson(mixed $value): ?string { if ($value === null) { return null; } if (is_string($value)) { return $value; } return json_encode($value, JSON_UNESCAPED_UNICODE); } private function clipText(string $text, int $max): string { if ($text === '' || mb_strlen($text) <= $max) { return $text; } return mb_substr($text, 0, $max); } }