Files
nl-admin-api/app/Service/common/ai/AiGenerationLogService.php
2026-08-10 15:51:00 +08:00

231 lines
8.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Service\common\ai;
use App\BaseApp\BaseService;
use App\Enum\AiGenerationSceneEnum;
use App\Enum\AiGenerationStatusEnum;
use App\Models\ai\AiGenerationModel;
/**
* AI 生成历史落库:开始 / 成功 / 失败
* 为什么单独抽:多场景复用同一流水表,业务只关心 begin/finish
*/
class AiGenerationLogService extends BaseService
{
/**
* 创建一条「进行中」记录,返回主键 ID
*
* @param array{
* scene?:string,
* name?:string,
* provider?:string,
* model?:string,
* api_key_id?:int,
* input_snapshot?:array|null,
* admin_id?:int
* } $params
*/
public function begin(array $params): int
{
$now = time();
$scene = trim((string) ($params['scene'] ?? AiGenerationSceneEnum::CODE_GENERATION->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);
}
}