- 抽 CoverGenerator 服务(covers:generate 与后台共用),内部强制同步产出 card 转换 - 文章列表:新增封面状态图标列、「有/无封面/完全无图」筛选、行内「生成封面」AI 操作 - 页面缓存:统计改为原生 StatsOverviewWidget(缓存页数/总大小/命中率/时长), 缓存明细表重排(表头、悬停、URL 截断、图标删除按钮、空状态)
88 lines
2.8 KiB
PHP
88 lines
2.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Blog\Services\CoverGenerator;
|
||
use App\Models\Post;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
/**
|
||
* 批量为文章生成 AI 封面(默认只补完全没有任何图片的文章)。
|
||
*/
|
||
class GenerateCovers extends Command
|
||
{
|
||
protected $signature = 'covers:generate
|
||
{--all : 全部重新生成(默认只处理没有任何封面图的文章)}
|
||
{--limit= : 最多处理多少篇}
|
||
{--post= : 只处理指定文章 ID,逗号分隔}
|
||
{--model= : 图片模型,覆盖 llm_image_model 设置}
|
||
{--style= : 附加到提示词的风格描述}
|
||
{--dry-run : 只列出将处理的文章,不调用接口}';
|
||
|
||
protected $description = '用 AI 为文章批量生成封面图(存入 media cover 集合)';
|
||
|
||
public function handle(CoverGenerator $covers): int
|
||
{
|
||
if (! $covers->isConfigured()) {
|
||
$this->error('LLM 未配置:请先在后台「AI 设置」或 .env 配置 LLM_BASE_URL / LLM_API_KEY');
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$model = $covers->model($this->option('model') ?: null);
|
||
$style = $this->option('style') ?: null;
|
||
|
||
$query = Post::query()->published()->orderedForList();
|
||
|
||
if ($ids = trim((string) $this->option('post'))) {
|
||
$query->whereIn('id', array_filter(array_map('intval', explode(',', $ids))));
|
||
}
|
||
|
||
$todo = $query->get()
|
||
->filter(fn (Post $post) => $this->option('all') || $post->coverOrFirstImageUrl() === null)
|
||
->values();
|
||
|
||
if ($limit = (int) $this->option('limit')) {
|
||
$todo = $todo->take($limit)->values();
|
||
}
|
||
|
||
if ($todo->isEmpty()) {
|
||
$this->info('没有需要生成封面的文章。');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$this->info("待处理 {$todo->count()} 篇,模型 {$model}");
|
||
|
||
if ($this->option('dry-run')) {
|
||
foreach ($todo as $post) {
|
||
$this->line("[{$post->id}] {$post->title}");
|
||
}
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$ok = 0;
|
||
$fail = 0;
|
||
|
||
foreach ($todo as $post) {
|
||
try {
|
||
$covers->generateFor($post, $model, $style);
|
||
$ok++;
|
||
$this->info("✔ [{$post->id}] {$post->title}");
|
||
} catch (\Throwable $e) {
|
||
$fail++;
|
||
$this->warn("✘ [{$post->id}] {$post->title}:{$e->getMessage()}");
|
||
Log::warning('covers:generate 失败', ['post_id' => $post->id, 'error' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
$this->info("完成:成功 {$ok},失败 {$fail}");
|
||
|
||
return $ok === 0 && $fail > 0 ? self::FAILURE : self::SUCCESS;
|
||
}
|
||
}
|