AI代码生成工具
This commit is contained in:
230
app/Service/common/ai/AiGenerationLogService.php
Normal file
230
app/Service/common/ai/AiGenerationLogService.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user