修改自动推广的相关任务
This commit is contained in:
176
application/common/PromotionLlmService.php
Normal file
176
application/common/PromotionLlmService.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
use think\Env;
|
||||
|
||||
/**
|
||||
* 推广邮件 LLM 服务
|
||||
*
|
||||
* 作用:基于 expert 的代表作 (paper_title) + 期刊信息 (name / scope 等),
|
||||
* 生成一段 2-3 句的个性化描述,用于邮件模板变量 {{llm_description}}。
|
||||
*
|
||||
* 配置(.env 的 [promotion] 段):
|
||||
* PROMOTION_LLM_URL chat/completions 接口地址
|
||||
* PROMOTION_LLM_MODEL 模型名
|
||||
* PROMOTION_LLM_TIMEOUT 超时时间(秒),默认 30
|
||||
* PROMOTION_LLM_FALLBACK 兜底描述(LLM 不可用 / 调用失败时使用)
|
||||
*
|
||||
* 返回约定:
|
||||
* ['status' => 1|2|3, 'text' => string]
|
||||
* 1 = LLM 成功生成
|
||||
* 2 = LLM 调用失败,text = 兜底文案
|
||||
* 3 = 跳过(缺少代表作等前置条件),text = 兜底文案
|
||||
*/
|
||||
class PromotionLlmService
|
||||
{
|
||||
private $url;
|
||||
private $model;
|
||||
private $timeout;
|
||||
private $apiKey;
|
||||
private $fallback;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->url = trim((string)Env::get('promotion.promotion_llm_url', ''));
|
||||
$this->model = trim((string)Env::get('promotion.promotion_llm_model', ''));
|
||||
$this->timeout = max(5, intval(Env::get('promotion.promotion_llm_timeout', 30)));
|
||||
$this->apiKey = trim((string)Env::get('promotion.promotion_llm_api_key', ''));
|
||||
$this->fallback = trim((string)Env::get('promotion.promotion_llm_fallback',
|
||||
'Your recent work aligns closely with the scope of our journal, and we would be honored to consider a contribution from you.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成个性化描述
|
||||
*
|
||||
* @param array $expert 至少包含 name / representative_work_title / fields / affiliation
|
||||
* @param array $journal 至少包含 title (name) / aims (可选) / databases (可选)
|
||||
* @return array ['status' => 1|2|3, 'text' => string]
|
||||
*/
|
||||
public function generateDescription(array $expert, array $journal): array
|
||||
{
|
||||
$paperTitle = trim((string)($expert['representative_work_title'] ?? ''));
|
||||
$expertName = trim((string)($expert['name'] ?? ''));
|
||||
$journalName = trim((string)($journal['title'] ?? ''));
|
||||
|
||||
if ($paperTitle === '' || $journalName === '') {
|
||||
return ['status' => 3, 'text' => $this->fallback];
|
||||
}
|
||||
|
||||
if ($this->url === '' || $this->model === '') {
|
||||
return ['status' => 2, 'text' => $this->fallback];
|
||||
}
|
||||
|
||||
$expertField = trim((string)($expert['fields'] ?? ($expert['field'] ?? '')));
|
||||
$journalAims = trim((string)($journal['aims'] ?? ''));
|
||||
$journalDbs = trim((string)($journal['databases'] ?? ''));
|
||||
|
||||
$system = 'You are an academic editorial assistant. '
|
||||
. 'Write a short, warm, professional English paragraph (2-3 sentences, <=50 words) '
|
||||
. 'that: (1) briefly appreciates the author\'s recent paper, '
|
||||
. '(2) explains why it fits the journal\'s scope, '
|
||||
. '(3) gently invites a future submission. '
|
||||
. 'Do NOT use placeholders, do NOT add greetings, do NOT add signatures, '
|
||||
. 'output plain text only (no markdown).';
|
||||
|
||||
$userLines = [];
|
||||
$userLines[] = 'Author name: ' . ($expertName !== '' ? $expertName : '(unknown)');
|
||||
if ($expertField !== '') {
|
||||
$userLines[] = 'Author research field: ' . $expertField;
|
||||
}
|
||||
$userLines[] = 'Recent paper title: ' . $paperTitle;
|
||||
$userLines[] = 'Target journal: ' . $journalName;
|
||||
if ($journalAims !== '') {
|
||||
$userLines[] = 'Journal aims & scope: ' . mb_substr($journalAims, 0, 500);
|
||||
}
|
||||
if ($journalDbs !== '') {
|
||||
$userLines[] = 'Journal indexing: ' . mb_substr($journalDbs, 0, 200);
|
||||
}
|
||||
$userLines[] = 'Return only the paragraph, nothing else.';
|
||||
$user = implode("\n", $userLines);
|
||||
|
||||
$payload = [
|
||||
'model' => $this->model,
|
||||
'temperature' => 0.4,
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $system],
|
||||
['role' => 'user', 'content' => $user],
|
||||
],
|
||||
];
|
||||
|
||||
$content = $this->postChat($payload);
|
||||
if ($content === null) {
|
||||
return ['status' => 2, 'text' => $this->fallback];
|
||||
}
|
||||
|
||||
$content = $this->cleanContent($content);
|
||||
if ($content === '') {
|
||||
return ['status' => 2, 'text' => $this->fallback];
|
||||
}
|
||||
|
||||
return ['status' => 1, 'text' => $content];
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 chat/completions 接口,返回 content 字符串;失败返回 null。
|
||||
*/
|
||||
private function postChat(array $payload)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $this->url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, min(10, $this->timeout));
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
||||
|
||||
$headers = ['Content-Type: application/json'];
|
||||
if ($this->apiKey !== '') {
|
||||
$headers[] = 'Authorization: Bearer ' . $this->apiKey;
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$raw = curl_exec($ch);
|
||||
if ($raw === false) {
|
||||
curl_close($ch);
|
||||
return null;
|
||||
}
|
||||
$httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
|
||||
curl_close($ch);
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) return null;
|
||||
|
||||
if (isset($data['choices'][0]['message']['content'])) {
|
||||
return (string)$data['choices'][0]['message']['content'];
|
||||
}
|
||||
if (isset($data['content'])) {
|
||||
return (string)$data['content'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗 LLM 输出:去除 markdown 包裹、多余空白、首尾引号、过长截断。
|
||||
*/
|
||||
private function cleanContent(string $text): string
|
||||
{
|
||||
$text = trim($text);
|
||||
$text = preg_replace('/^```[a-zA-Z]*\s*|```$/m', '', $text);
|
||||
$text = trim($text);
|
||||
$text = trim($text, "\"' \t\n\r\0\x0B");
|
||||
$text = preg_replace('/\s+/', ' ', $text);
|
||||
if (mb_strlen($text) > 800) {
|
||||
$text = mb_substr($text, 0, 800);
|
||||
}
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
public function getFallback(): string
|
||||
{
|
||||
return $this->fallback;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user