diff --git a/application/api/job/UserFieldAiFill.php b/application/api/job/UserFieldAiFill.php index deb1b447..acb0dcd6 100644 --- a/application/api/job/UserFieldAiFill.php +++ b/application/api/job/UserFieldAiFill.php @@ -30,6 +30,6 @@ class UserFieldAiFill $job->delete(); $delay = max(0, (int) (isset($data['delay']) ? $data['delay'] : 1)); -// $svc->enqueueNextFieldAi($delay, $queue, $userId, $force); + $svc->enqueueNextFieldAi($delay, $queue, $userId, $force); } } diff --git a/application/common/PromotionService.php b/application/common/PromotionService.php index d958bed2..c5b1ad2e 100644 --- a/application/common/PromotionService.php +++ b/application/common/PromotionService.php @@ -509,47 +509,66 @@ class PromotionService } } - // 一次 LLM 调用生成两段内容(description + advised_topics) - $llmResult = [ - 'description' => '', - 'description_status' => 0, - 'advised_topics' => '', - 'advised_topics_status' => 0, - ]; - try { - $llm = new PromotionLlmService(); - $llmResult = $llm->generateEmailContent( - $expert, - $journal ?: [], - $overlapFields, - $taskFields, - $fieldSet - ); - } catch (\Exception $e) { - $fbDesc = ''; - $fbAdvised = ''; - try { - $fbSvc = isset($llm) ? $llm : new PromotionLlmService(); - $fbDesc = $fbSvc->getFallback(); - $fbAdvised = $fbSvc->getAdvisedFallback(); - } catch (\Exception $ignore) { - } - $llmResult = [ - 'description' => $fbDesc, - 'description_status' => 2, - 'advised_topics' => $fbAdvised, - 'advised_topics_status' => 2, - ]; - $this->log("prepareSingleEmail log_id={$logId} llm_exception=" . $e->getMessage()); - } - $llmText = (string)$llmResult['description']; - $llmStatus = intval($llmResult['description_status']); - $advisedText = (string)$llmResult['advised_topics']; - $advisedStatus = intval($llmResult['advised_topics_status']); + // 仅当模板真正引用了 LLM 占位符(llm_description / ai_content_analysis / + // ai_advised_topics / llm_advised_topics)时才调用 LLM,避免无谓的请求与开销。 + $llmNeed = $this->detectLlmTemplateNeed( + $task['template_id'], + $task['journal_id'], + intval($task['style_id'] ?? 0) + ); - $expert['llm_description'] = $llmText; - $expert['ai_advised_topics'] = $advisedText; - $expert['role'] = $this->mapExpertTypeRole($expertType); + if (!$llmNeed['need']) { + $llmText = ''; + $llmStatus = 0; + $advisedText = ''; + $advisedStatus = 0; + $expert['llm_description'] = ''; + $expert['ai_advised_topics'] = ''; + $expert['role'] = $this->mapExpertTypeRole($expertType); + $this->log("prepareSingleEmail log_id={$logId} skip_llm (template has no llm placeholders)"); + } else { + // 一次 LLM 调用生成两段内容(description + advised_topics) + $llmResult = [ + 'description' => '', + 'description_status' => 0, + 'advised_topics' => '', + 'advised_topics_status' => 0, + ]; + try { + $llm = new PromotionLlmService(); + $llmResult = $llm->generateEmailContent( + $expert, + $journal ?: [], + $overlapFields, + $taskFields, + $fieldSet + ); + } catch (\Exception $e) { + $fbDesc = ''; + $fbAdvised = ''; + try { + $fbSvc = isset($llm) ? $llm : new PromotionLlmService(); + $fbDesc = $fbSvc->getFallback(); + $fbAdvised = $fbSvc->getAdvisedFallback(); + } catch (\Exception $ignore) { + } + $llmResult = [ + 'description' => $fbDesc, + 'description_status' => 2, + 'advised_topics' => $fbAdvised, + 'advised_topics_status' => 2, + ]; + $this->log("prepareSingleEmail log_id={$logId} llm_exception=" . $e->getMessage()); + } + $llmText = (string)$llmResult['description']; + $llmStatus = intval($llmResult['description_status']); + $advisedText = (string)$llmResult['advised_topics']; + $advisedStatus = intval($llmResult['advised_topics_status']); + + $expert['llm_description'] = $llmText; + $expert['ai_advised_topics'] = $advisedText; + $expert['role'] = $this->mapExpertTypeRole($expertType); + } } $expertVars = $this->buildExpertVars($expert); @@ -935,6 +954,73 @@ class PromotionService // ==================== Template Rendering ==================== + /** + * 检测邮件模板(含 style 头尾)是否包含需要 LLM 生成的占位符。 + * + * @return array{need:bool,need_description:bool,need_advised_topics:bool,tags:array} + */ + public function detectLlmTemplateNeed($templateId, $journalId, $styleId = 0) + { + $descriptionTags = ['llm_description', 'ai_content_analysis']; + $advisedTags = ['ai_advised_topics', 'llm_advised_topics']; + $allTags = array_merge($descriptionTags, $advisedTags); + + $parts = []; + $tpl = Db::name('mail_template') + ->where('template_id', $templateId) + ->where('journal_id', $journalId) + ->where('state', 0) + ->find(); + if ($tpl) { + $parts[] = (string)($tpl['subject'] ?? ''); + $parts[] = (string)($tpl['body_html'] ?? ''); + } + $styleId = intval($styleId); + if ($styleId > 0) { + $style = Db::name('mail_style')->where('style_id', $styleId)->where('state', 0)->find(); + if ($style) { + $parts[] = (string)($style['header_html'] ?? ''); + $parts[] = (string)($style['footer_html'] ?? ''); + } + } + + $haystack = implode("\n", $parts); + $found = []; + foreach ($allTags as $tag) { + if ($this->templateContainsVar($haystack, $tag)) { + $found[] = $tag; + } + } + + $needDesc = (bool) array_intersect($found, $descriptionTags); + $needAdvised = (bool) array_intersect($found, $advisedTags); + + return [ + 'need' => !empty($found), + 'need_description' => $needDesc, + 'need_advised_topics' => $needAdvised, + 'tags' => $found, + ]; + } + + /** + * 模板是否包含某变量占位符(支持 {{ var }} 与 {var})。 + */ + protected function templateContainsVar($haystack, $varName) + { + if (!is_string($haystack) || $haystack === '' || $varName === '') { + return false; + } + $quoted = preg_quote($varName, '/'); + if (preg_match('/\{\{\s*' . $quoted . '\s*\}\}/', $haystack)) { + return true; + } + if (strpos($haystack, '{' . $varName . '}') !== false) { + return true; + } + return false; + } + public function renderFromTemplate($templateId, $journalId, $varsJson, $styleId = 0) { $tpl = Db::name('mail_template')->where('template_id', $templateId)->where('journal_id', $journalId)->where('state', 0)->find();