Files
laralog/app/Blog/Services/CoverGenerator.php
T
gouki d041622531
Build and Deploy / verify (push) Successful in 10s
Build and Deploy / deploy (push) Successful in 17s
feat: post cover filter + admin AI cover action, restyle page-cache admin
- 抽 CoverGenerator 服务(covers:generate 与后台共用),内部强制同步产出 card 转换
- 文章列表:新增封面状态图标列、「有/无封面/完全无图」筛选、行内「生成封面」AI 操作
- 页面缓存:统计改为原生 StatsOverviewWidget(缓存页数/总大小/命中率/时长),
  缓存明细表重排(表头、悬停、URL 截断、图标删除按钮、空状态)
2026-10-14 23:48:57 +00:00

95 lines
3.2 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
declare(strict_types=1);
namespace App\Blog\Services;
use App\Models\Post;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
/**
* AI 封面生成:复用「AI 设置」的 LLM 配置调 OpenAI 兼容 /image_generation 接口
* (如 MiniMax image-01),图片存入 media cover 集合(singleFile,重复生成自动替换)。
*/
class CoverGenerator
{
public function __construct(private LlmClient $llm) {}
public function isConfigured(): bool
{
return $this->llm->isConfigured();
}
public function model(?string $override = null): string
{
return $override ?: (string) blog_setting('llm_image_model', 'image-01');
}
public function generateFor(Post $post, ?string $model = null, ?string $style = null): void
{
if (! $this->llm->isConfigured()) {
throw new \RuntimeException('LLM 未配置:请先在后台「AI 设置」配置 Base URL / API Key');
}
// 站点无常驻 queue worker:同步产出 card 转换,避免缩略图永远卡在队列里。
config(['media-library.queue_conversions_by_default' => false]);
$response = Http::timeout(120)
->withToken($this->llm->apiKey())
->post($this->llm->baseUrl().'/image_generation', [
'model' => $this->model($model),
'prompt' => $this->buildPrompt($post, $style),
'aspect_ratio' => '16:9',
'response_format' => 'url',
'n' => 1,
'prompt_optimizer' => true,
]);
if ($response->failed()) {
throw new \RuntimeException('HTTP '.$response->status().':'.Str::limit($response->body(), 200));
}
$url = $response->json('data.image_urls.0');
if (! is_string($url) || $url === '') {
$reason = $response->json('base_resp.status_msg') ?: $response->body();
throw new \RuntimeException('接口未返回图片:'.Str::limit((string) $reason, 200));
}
$image = Http::timeout(60)->get($url)->throw();
$ext = match (strtolower(explode(';', (string) $image->header('Content-Type'))[0])) {
'image/png' => 'png',
'image/webp' => 'webp',
default => 'jpg',
};
$tmp = tempnam(sys_get_temp_dir(), 'cover_');
file_put_contents($tmp, $image->body());
try {
$post->addMedia($tmp)
->usingFileName('cover-'.$post->id.'.'.$ext)
->toMediaCollection('cover');
} finally {
@unlink($tmp);
}
}
public function buildPrompt(Post $post, ?string $style = null): string
{
$style = trim((string) ($style
?: blog_setting('cover_prompt_style', '扁平插画风格,色调柔和,构图简洁')));
$excerpt = Str::limit(trim(strip_tags((string) $post->excerpt)), 60, '');
$parts = ["为博客文章《{$post->title}》生成一张封面配图"];
if ($excerpt !== '') {
$parts[] = "内容大意:{$excerpt}";
}
$parts[] = $style;
$parts[] = '画面中不要出现任何文字';
return implode('。', $parts);
}
}