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 = $this->requestImage($this->buildPrompt($post, $style), $model); // 标题触发内容审核(input new_sensitive):退化为不含标题的通用提示词重试一次。 if (str_contains((string) $response->body(), 'sensitive') && ! is_string($response->json('data.image_urls.0'))) { $response = $this->requestImage($this->neutralPrompt($style), $model); } 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); } /** * 不含标题/摘要的通用封面提示词(标题触发审核时的兜底)。 */ public function neutralPrompt(?string $style = null): string { $style = trim((string) ($style ?: blog_setting('cover_prompt_style', '扁平插画风格,色调柔和,构图简洁'))); return "博客文章封面配图。{$style}。画面中不要出现任何文字"; } private function requestImage(string $prompt, ?string $model = null): \Illuminate\Http\Client\Response { return Http::timeout(120) ->withToken($this->llm->apiKey()) ->post($this->llm->baseUrl().'/image_generation', [ 'model' => $this->model($model), 'prompt' => $prompt, 'aspect_ratio' => '16:9', 'response_format' => 'url', 'n' => 1, 'prompt_optimizer' => true, ]); } }