修改自动推广的相关任务
This commit is contained in:
1
.env
1
.env
@@ -24,6 +24,7 @@ PROMOTION_LLM_URL=http://chat.taimed.cn/v1/chat/completions
|
|||||||
PROMOTION_LLM_MODEL=your-model-name
|
PROMOTION_LLM_MODEL=your-model-name
|
||||||
PROMOTION_LLM_TIMEOUT=30
|
PROMOTION_LLM_TIMEOUT=30
|
||||||
PROMOTION_LLM_FALLBACK=""
|
PROMOTION_LLM_FALLBACK=""
|
||||||
|
PROMOTION_LLM_ADVISED_FALLBACK=""
|
||||||
|
|
||||||
[journal]
|
[journal]
|
||||||
;官网服务器地址
|
;官网服务器地址
|
||||||
|
|||||||
@@ -7,20 +7,24 @@ use think\Env;
|
|||||||
/**
|
/**
|
||||||
* 推广邮件 LLM 服务
|
* 推广邮件 LLM 服务
|
||||||
*
|
*
|
||||||
* 作用:基于 expert 的代表作 (paper_title) + 期刊信息 (name / scope 等),
|
* 作用:基于 expert 的代表作 (paper_title)、expert 的研究领域、期刊信息 (name / scope / fields 等),
|
||||||
* 生成一段 2-3 句的个性化描述,用于邮件模板变量 {{llm_description}}。
|
* 通过一次 LLM 调用同时生成两段邮件内容:
|
||||||
|
* - description 个性化赞美 + 邀稿段落(对应模板变量 {{ai_content_analysis}} / {{llm_description}})
|
||||||
|
* - advised_topics "我们特别关注 X / Y / Z 领域" 段落(对应模板变量 {{ai_advised_topics}})
|
||||||
|
*
|
||||||
|
* 单次调用返回 JSON,避免两次 LLM 请求导致的延迟翻倍;任一段失败可独立兜底。
|
||||||
*
|
*
|
||||||
* 配置(.env 的 [promotion] 段):
|
* 配置(.env 的 [promotion] 段):
|
||||||
* PROMOTION_LLM_URL chat/completions 接口地址
|
* PROMOTION_LLM_URL chat/completions 接口地址
|
||||||
* PROMOTION_LLM_MODEL 模型名
|
* PROMOTION_LLM_MODEL 模型名
|
||||||
* PROMOTION_LLM_TIMEOUT 超时时间(秒),默认 30
|
* PROMOTION_LLM_TIMEOUT 超时时间(秒),默认 30
|
||||||
* PROMOTION_LLM_FALLBACK 兜底描述(LLM 不可用 / 调用失败时使用)
|
* PROMOTION_LLM_FALLBACK description 兜底文案
|
||||||
|
* PROMOTION_LLM_ADVISED_FALLBACK advised_topics 兜底文案
|
||||||
*
|
*
|
||||||
* 返回约定:
|
* 状态约定:
|
||||||
* ['status' => 1|2|3, 'text' => string]
|
* 1 = LLM 成功生成
|
||||||
* 1 = LLM 成功生成
|
* 2 = LLM 调用失败 / 解析失败,使用兜底
|
||||||
* 2 = LLM 调用失败,text = 兜底文案
|
* 3 = 前置条件不足(缺代表作、缺交集等),使用兜底
|
||||||
* 3 = 跳过(缺少代表作等前置条件),text = 兜底文案
|
|
||||||
*/
|
*/
|
||||||
class PromotionLlmService
|
class PromotionLlmService
|
||||||
{
|
{
|
||||||
@@ -29,6 +33,7 @@ class PromotionLlmService
|
|||||||
private $timeout;
|
private $timeout;
|
||||||
private $apiKey;
|
private $apiKey;
|
||||||
private $fallback;
|
private $fallback;
|
||||||
|
private $advisedFallback;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -38,27 +43,54 @@ class PromotionLlmService
|
|||||||
$this->apiKey = trim((string)Env::get('promotion.promotion_llm_api_key', ''));
|
$this->apiKey = trim((string)Env::get('promotion.promotion_llm_api_key', ''));
|
||||||
$this->fallback = trim((string)Env::get('promotion.promotion_llm_fallback',
|
$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.'));
|
'Your recent work aligns closely with the scope of our journal, and we would be honored to consider a contribution from you.'));
|
||||||
|
$this->advisedFallback = trim((string)Env::get('promotion.promotion_llm_advised_fallback',
|
||||||
|
'We are especially interested in the research directions that align with your expertise, and warmly welcome your future submissions in these areas.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成个性化描述
|
* 一次 LLM 调用生成邮件两段内容。
|
||||||
*
|
*
|
||||||
* @param array $expert 至少包含 name / representative_work_title / fields / affiliation
|
* @param array $expert expert 行(含 name / representative_work_title / fields 等)
|
||||||
* @param array $journal 至少包含 title (name) / aims (可选) / databases (可选)
|
* @param array $journal journal 行(含 title / aims / databases 等)
|
||||||
* @return array ['status' => 1|2|3, 'text' => string]
|
* @param array $overlapFields 预先计算的 expert & journal 领域交集(可能为空)
|
||||||
|
* @param array $journalFields journal(或 task/工厂)的目标领域
|
||||||
|
* @param array $expertFields expert 的研究领域
|
||||||
|
* @return array [
|
||||||
|
* 'description' => string,
|
||||||
|
* 'description_status' => 1|2|3,
|
||||||
|
* 'advised_topics' => string,
|
||||||
|
* 'advised_topics_status' => 1|2|3,
|
||||||
|
* ]
|
||||||
*/
|
*/
|
||||||
public function generateDescription(array $expert, array $journal): array
|
public function generateEmailContent(
|
||||||
{
|
array $expert,
|
||||||
|
array $journal,
|
||||||
|
array $overlapFields = [],
|
||||||
|
array $journalFields = [],
|
||||||
|
array $expertFields = []
|
||||||
|
): array {
|
||||||
$paperTitle = trim((string)($expert['representative_work_title'] ?? ''));
|
$paperTitle = trim((string)($expert['representative_work_title'] ?? ''));
|
||||||
$expertName = trim((string)($expert['name'] ?? ''));
|
$expertName = trim((string)($expert['name'] ?? ''));
|
||||||
$journalName = trim((string)($journal['title'] ?? ''));
|
$journalName = trim((string)($journal['title'] ?? ''));
|
||||||
|
|
||||||
if ($paperTitle === '' || $journalName === '') {
|
$overlapList = $this->cleanList($overlapFields);
|
||||||
return ['status' => 3, 'text' => $this->fallback];
|
$journalList = $this->cleanList($journalFields);
|
||||||
|
$expertList = $this->cleanList($expertFields);
|
||||||
|
|
||||||
|
$hasDescInput = ($paperTitle !== '' && $journalName !== '');
|
||||||
|
$hasAdvisedInput = (!empty($overlapList) || (!empty($journalList) && !empty($expertList)));
|
||||||
|
|
||||||
|
// 两段都缺输入时直接走双兜底
|
||||||
|
if (!$hasDescInput && !$hasAdvisedInput) {
|
||||||
|
return $this->allFallback(3, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LLM 未配置
|
||||||
if ($this->url === '' || $this->model === '') {
|
if ($this->url === '' || $this->model === '') {
|
||||||
return ['status' => 2, 'text' => $this->fallback];
|
return $this->allFallback(
|
||||||
|
$hasDescInput ? 2 : 3,
|
||||||
|
$hasAdvisedInput ? 2 : 3
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$expertField = trim((string)($expert['fields'] ?? ($expert['field'] ?? '')));
|
$expertField = trim((string)($expert['fields'] ?? ($expert['field'] ?? '')));
|
||||||
@@ -66,27 +98,48 @@ class PromotionLlmService
|
|||||||
$journalDbs = trim((string)($journal['databases'] ?? ''));
|
$journalDbs = trim((string)($journal['databases'] ?? ''));
|
||||||
|
|
||||||
$system = 'You are an academic editorial assistant. '
|
$system = 'You are an academic editorial assistant. '
|
||||||
. 'Write a short, warm, professional English paragraph (2-3 sentences, <=50 words) '
|
. 'You will receive context about an author, their recent paper, and a target journal, '
|
||||||
. 'that: (1) briefly appreciates the author\'s recent paper, '
|
. 'and you must produce TWO English paragraphs for an invitation email. '
|
||||||
. '(2) explains why it fits the journal\'s scope, '
|
. 'Output STRICT MINIFIED JSON ONLY with exactly these keys:'
|
||||||
. '(3) gently invites a future submission. '
|
. '{"description":"...","advised_topics":"..."} '
|
||||||
. 'Do NOT use placeholders, do NOT add greetings, do NOT add signatures, '
|
. 'Rules for "description": 2-3 sentences, <=50 words, warm and professional; '
|
||||||
. 'output plain text only (no markdown).';
|
. '(a) briefly appreciate the author\'s recent paper, '
|
||||||
|
. '(b) explain why it fits the journal\'s scope, '
|
||||||
|
. '(c) gently invite a future submission. '
|
||||||
|
. 'Rules for "advised_topics": 1-2 sentences, <=40 words; '
|
||||||
|
. 'emphasize that the journal is particularly interested in the research directions '
|
||||||
|
. 'where the journal\'s focus and the author\'s work overlap; '
|
||||||
|
. 'mention the overlapping topics explicitly (use the provided overlap list when non-empty, '
|
||||||
|
. 'otherwise choose the best semantic overlap between journal focus and author fields); '
|
||||||
|
. 'end by inviting contributions leaning toward those directions. '
|
||||||
|
. 'No greetings, no signatures, no placeholders, no markdown, no code fences. '
|
||||||
|
. 'If a section genuinely cannot be produced, return an empty string for that key.';
|
||||||
|
|
||||||
$userLines = [];
|
$userLines = [];
|
||||||
$userLines[] = 'Author name: ' . ($expertName !== '' ? $expertName : '(unknown)');
|
$userLines[] = 'Author name: ' . ($expertName !== '' ? $expertName : '(unknown)');
|
||||||
if ($expertField !== '') {
|
if ($expertField !== '') {
|
||||||
$userLines[] = 'Author research field: ' . $expertField;
|
$userLines[] = 'Author research field (raw): ' . $expertField;
|
||||||
|
}
|
||||||
|
if (!empty($expertList)) {
|
||||||
|
$userLines[] = 'Author research fields (list): ' . implode(', ', $expertList);
|
||||||
|
}
|
||||||
|
$userLines[] = 'Recent paper title: ' . ($paperTitle !== '' ? $paperTitle : '(none)');
|
||||||
|
$userLines[] = 'Target journal: ' . ($journalName !== '' ? $journalName : '(unknown)');
|
||||||
|
if (!empty($journalList)) {
|
||||||
|
$userLines[] = 'Journal focus fields: ' . implode(', ', $journalList);
|
||||||
|
}
|
||||||
|
if (!empty($overlapList)) {
|
||||||
|
$userLines[] = 'Overlap topics (exact match): ' . implode(', ', $overlapList);
|
||||||
|
} else {
|
||||||
|
$userLines[] = 'Overlap topics (exact match): (none, infer semantically from the two field lists above)';
|
||||||
}
|
}
|
||||||
$userLines[] = 'Recent paper title: ' . $paperTitle;
|
|
||||||
$userLines[] = 'Target journal: ' . $journalName;
|
|
||||||
if ($journalAims !== '') {
|
if ($journalAims !== '') {
|
||||||
$userLines[] = 'Journal aims & scope: ' . mb_substr($journalAims, 0, 500);
|
$userLines[] = 'Journal aims & scope: ' . mb_substr($journalAims, 0, 500);
|
||||||
}
|
}
|
||||||
if ($journalDbs !== '') {
|
if ($journalDbs !== '') {
|
||||||
$userLines[] = 'Journal indexing: ' . mb_substr($journalDbs, 0, 200);
|
$userLines[] = 'Journal indexing: ' . mb_substr($journalDbs, 0, 200);
|
||||||
}
|
}
|
||||||
$userLines[] = 'Return only the paragraph, nothing else.';
|
$userLines[] = 'Return only minified JSON {"description":"...","advised_topics":"..."}.';
|
||||||
$user = implode("\n", $userLines);
|
$user = implode("\n", $userLines);
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
@@ -100,15 +153,55 @@ class PromotionLlmService
|
|||||||
|
|
||||||
$content = $this->postChat($payload);
|
$content = $this->postChat($payload);
|
||||||
if ($content === null) {
|
if ($content === null) {
|
||||||
return ['status' => 2, 'text' => $this->fallback];
|
return $this->allFallback(
|
||||||
|
$hasDescInput ? 2 : 3,
|
||||||
|
$hasAdvisedInput ? 2 : 3
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = $this->cleanContent($content);
|
$parsed = $this->parseJson($content);
|
||||||
if ($content === '') {
|
if ($parsed === null) {
|
||||||
return ['status' => 2, 'text' => $this->fallback];
|
return $this->allFallback(
|
||||||
|
$hasDescInput ? 2 : 3,
|
||||||
|
$hasAdvisedInput ? 2 : 3
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['status' => 1, 'text' => $content];
|
$desc = $this->cleanContent((string)($parsed['description'] ?? ''));
|
||||||
|
$advised = $this->cleanContent((string)($parsed['advised_topics'] ?? ''));
|
||||||
|
|
||||||
|
$descStatus = 1;
|
||||||
|
if ($desc === '') {
|
||||||
|
$desc = $this->fallback;
|
||||||
|
$descStatus = $hasDescInput ? 2 : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
$advisedStatus = 1;
|
||||||
|
if ($advised === '') {
|
||||||
|
$advised = $this->advisedFallback;
|
||||||
|
$advisedStatus = $hasAdvisedInput ? 2 : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'description' => $desc,
|
||||||
|
'description_status' => $descStatus,
|
||||||
|
'advised_topics' => $advised,
|
||||||
|
'advised_topics_status' => $advisedStatus,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容旧接口:单段 description 生成(仍走 generateEmailContent,取第一段)
|
||||||
|
*
|
||||||
|
* @return array ['status' => 1|2|3, 'text' => string]
|
||||||
|
*/
|
||||||
|
public function generateDescription(array $expert, array $journal): array
|
||||||
|
{
|
||||||
|
$r = $this->generateEmailContent($expert, $journal);
|
||||||
|
return [
|
||||||
|
'status' => intval($r['description_status']),
|
||||||
|
'text' => (string)$r['description'],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,6 +246,28 @@ class PromotionLlmService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从模型原始输出里提取 JSON 对象;失败返回 null。
|
||||||
|
*/
|
||||||
|
private function parseJson(string $raw)
|
||||||
|
{
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') return null;
|
||||||
|
$raw = preg_replace('/^```[a-zA-Z]*\s*|```$/m', '', $raw);
|
||||||
|
$raw = trim($raw);
|
||||||
|
|
||||||
|
// 直接 decode
|
||||||
|
$obj = json_decode($raw, true);
|
||||||
|
if (is_array($obj)) return $obj;
|
||||||
|
|
||||||
|
// 抓出第一个 {...} 块再 decode
|
||||||
|
if (preg_match('/\{.*\}/s', $raw, $m)) {
|
||||||
|
$obj = json_decode($m[0], true);
|
||||||
|
if (is_array($obj)) return $obj;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清洗 LLM 输出:去除 markdown 包裹、多余空白、首尾引号、过长截断。
|
* 清洗 LLM 输出:去除 markdown 包裹、多余空白、首尾引号、过长截断。
|
||||||
*/
|
*/
|
||||||
@@ -169,8 +284,41 @@ class PromotionLlmService
|
|||||||
return trim($text);
|
return trim($text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对领域列表做 trim / 去空 / 去重,保留首次出现顺序。
|
||||||
|
*/
|
||||||
|
private function cleanList(array $list): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($list as $item) {
|
||||||
|
$v = trim((string)$item);
|
||||||
|
if ($v === '') continue;
|
||||||
|
$key = strtolower($v);
|
||||||
|
if (isset($seen[$key])) continue;
|
||||||
|
$seen[$key] = true;
|
||||||
|
$out[] = $v;
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function allFallback(int $descStatus, int $advisedStatus): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'description' => $this->fallback,
|
||||||
|
'description_status' => $descStatus,
|
||||||
|
'advised_topics' => $this->advisedFallback,
|
||||||
|
'advised_topics_status' => $advisedStatus,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function getFallback(): string
|
public function getFallback(): string
|
||||||
{
|
{
|
||||||
return $this->fallback;
|
return $this->fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAdvisedFallback(): string
|
||||||
|
{
|
||||||
|
return $this->advisedFallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,17 @@ class PromotionService
|
|||||||
}
|
}
|
||||||
$expert['fields'] = implode(',', $fieldSet);
|
$expert['fields'] = implode(',', $fieldSet);
|
||||||
$expert['representative_work_title'] = $matchedTitle !== '' ? $matchedTitle : $fallbackTitle;
|
$expert['representative_work_title'] = $matchedTitle !== '' ? $matchedTitle : $fallbackTitle;
|
||||||
|
|
||||||
|
// 现场发送路径:没有提前准备 LLM,退回 .env 兜底文案
|
||||||
|
try {
|
||||||
|
$llmSvc = new PromotionLlmService();
|
||||||
|
$expert['llm_description'] = $llmSvc->getFallback();
|
||||||
|
$expert['ai_advised_topics'] = $llmSvc->getAdvisedFallback();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$expert['llm_description'] = '';
|
||||||
|
$expert['ai_advised_topics'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
$expertVars = $this->buildExpertVars($expert);
|
$expertVars = $this->buildExpertVars($expert);
|
||||||
$journalVars = $this->buildJournalVars($journal);
|
$journalVars = $this->buildJournalVars($journal);
|
||||||
$vars = array_merge($journalVars, $expertVars);
|
$vars = array_merge($journalVars, $expertVars);
|
||||||
@@ -312,21 +323,60 @@ class PromotionService
|
|||||||
$expert['fields'] = implode(',', $fieldSet);
|
$expert['fields'] = implode(',', $fieldSet);
|
||||||
$expert['representative_work_title'] = $matchedTitle !== '' ? $matchedTitle : $fallbackTitle;
|
$expert['representative_work_title'] = $matchedTitle !== '' ? $matchedTitle : $fallbackTitle;
|
||||||
|
|
||||||
|
// 领域交集(大小写不敏感,保留 expert 原字面值用于展示)
|
||||||
|
$overlapFields = [];
|
||||||
|
if (!empty($taskFieldLower)) {
|
||||||
|
foreach ($fieldSet as $fn) {
|
||||||
|
if (isset($taskFieldLower[strtolower($fn)])) {
|
||||||
|
$overlapFields[] = $fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$journal = Db::name('journal')->where('journal_id', $task['journal_id'])->find();
|
$journal = Db::name('journal')->where('journal_id', $task['journal_id'])->find();
|
||||||
|
|
||||||
// 调用 LLM 生成个性化描述(失败/缺条件时回退到兜底文案)
|
// 一次 LLM 调用生成两段内容(description + advised_topics)
|
||||||
$llmResult = ['status' => 0, 'text' => ''];
|
$llmResult = [
|
||||||
|
'description' => '',
|
||||||
|
'description_status' => 0,
|
||||||
|
'advised_topics' => '',
|
||||||
|
'advised_topics_status' => 0,
|
||||||
|
];
|
||||||
try {
|
try {
|
||||||
$llm = new PromotionLlmService();
|
$llm = new PromotionLlmService();
|
||||||
$llmResult = $llm->generateDescription($expert, $journal ?: []);
|
$llmResult = $llm->generateEmailContent(
|
||||||
|
$expert,
|
||||||
|
$journal ?: [],
|
||||||
|
$overlapFields,
|
||||||
|
$taskFields,
|
||||||
|
$fieldSet
|
||||||
|
);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$llmResult = ['status' => 2, 'text' => ''];
|
// 兜底双占位($llm 实例可能未成功构建,单独拿一个尝试)
|
||||||
|
$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());
|
$this->log("prepareSingleEmail log_id={$logId} llm_exception=" . $e->getMessage());
|
||||||
}
|
}
|
||||||
$llmText = (string)$llmResult['text'];
|
$llmText = (string)$llmResult['description'];
|
||||||
$llmStatus = intval($llmResult['status']);
|
$llmStatus = intval($llmResult['description_status']);
|
||||||
|
$advisedText = (string)$llmResult['advised_topics'];
|
||||||
|
$advisedStatus = intval($llmResult['advised_topics_status']);
|
||||||
|
|
||||||
$expert['llm_description'] = $llmText;
|
$expert['llm_description'] = $llmText;
|
||||||
|
$expert['ai_advised_topics'] = $advisedText;
|
||||||
|
|
||||||
$expertVars = $this->buildExpertVars($expert);
|
$expertVars = $this->buildExpertVars($expert);
|
||||||
$journalVars = $this->buildJournalVars($journal);
|
$journalVars = $this->buildJournalVars($journal);
|
||||||
@@ -341,27 +391,41 @@ class PromotionService
|
|||||||
$now = time();
|
$now = time();
|
||||||
if ($rendered['code'] !== 0) {
|
if ($rendered['code'] !== 0) {
|
||||||
Db::name('promotion_email_log')->where('log_id', $logId)->update([
|
Db::name('promotion_email_log')->where('log_id', $logId)->update([
|
||||||
'state' => 2,
|
'state' => 2,
|
||||||
'error_msg' => 'Prepare failed: ' . $rendered['msg'],
|
'error_msg' => 'Prepare failed: ' . $rendered['msg'],
|
||||||
'llm_description' => mb_substr($llmText, 0, 2000),
|
'llm_description' => mb_substr($llmText, 0, 2000),
|
||||||
'llm_status' => $llmStatus,
|
'llm_status' => $llmStatus,
|
||||||
'send_time' => $now,
|
'llm_advised_topics' => mb_substr($advisedText, 0, 2000),
|
||||||
|
'llm_advised_topics_status' => $advisedStatus,
|
||||||
|
'send_time' => $now,
|
||||||
]);
|
]);
|
||||||
$this->tryFinalizeTask($task['task_id']);
|
$this->tryFinalizeTask($task['task_id']);
|
||||||
return ['code' => 1, 'msg' => $rendered['msg'], 'llm_status' => $llmStatus];
|
return [
|
||||||
|
'code' => 1,
|
||||||
|
'msg' => $rendered['msg'],
|
||||||
|
'llm_status' => $llmStatus,
|
||||||
|
'llm_advised_topics_status' => $advisedStatus,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Db::name('promotion_email_log')->where('log_id', $logId)->update([
|
Db::name('promotion_email_log')->where('log_id', $logId)->update([
|
||||||
'subject_prepared' => mb_substr($rendered['data']['subject'], 0, 512),
|
'subject_prepared' => mb_substr($rendered['data']['subject'], 0, 512),
|
||||||
'body_prepared' => $rendered['data']['body'],
|
'body_prepared' => $rendered['data']['body'],
|
||||||
'llm_description' => mb_substr($llmText, 0, 2000),
|
'llm_description' => mb_substr($llmText, 0, 2000),
|
||||||
'llm_status' => $llmStatus,
|
'llm_status' => $llmStatus,
|
||||||
'prepared_at' => $now,
|
'llm_advised_topics' => mb_substr($advisedText, 0, 2000),
|
||||||
|
'llm_advised_topics_status' => $advisedStatus,
|
||||||
|
'prepared_at' => $now,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->tryFinalizeTask($task['task_id']);
|
$this->tryFinalizeTask($task['task_id']);
|
||||||
|
|
||||||
return ['code' => 0, 'msg' => 'ok', 'llm_status' => $llmStatus];
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => 'ok',
|
||||||
|
'llm_status' => $llmStatus,
|
||||||
|
'llm_advised_topics_status' => $advisedStatus,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -725,16 +789,19 @@ class PromotionService
|
|||||||
|
|
||||||
public function buildExpertVars($expert)
|
public function buildExpertVars($expert)
|
||||||
{
|
{
|
||||||
$llm = $expert['llm_description'] ?? '';
|
$llm = $expert['llm_description'] ?? '';
|
||||||
|
$advised = $expert['ai_advised_topics'] ?? '';
|
||||||
return [
|
return [
|
||||||
'expert_title' => "Ph.D",
|
'expert_title' => "Ph.D",
|
||||||
'expert_name' => $expert['name'] ?? '',
|
'expert_name' => $expert['name'] ?? '',
|
||||||
'expert_email' => $expert['email'] ?? '',
|
'expert_email' => $expert['email'] ?? '',
|
||||||
'expert_affiliation' => $expert['affiliation'] ?? '',
|
'expert_affiliation' => $expert['affiliation'] ?? '',
|
||||||
'expert_field' => $expert['fields'] ?? ($expert['field'] ?? ''),
|
'expert_field' => $expert['fields'] ?? ($expert['field'] ?? ''),
|
||||||
'representative_work_title' => $expert['representative_work_title'] ?? '',
|
'representative_work_title' => $expert['representative_work_title'] ?? '',
|
||||||
'llm_description' => $llm,
|
'llm_description' => $llm,
|
||||||
'ai_content_analysis' => $llm,
|
'ai_content_analysis' => $llm,
|
||||||
|
'ai_advised_topics' => $advised,
|
||||||
|
'llm_advised_topics' => $advised,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
sql/add_llm_advised_topics_to_promotion_email_log.sql
Normal file
7
sql/add_llm_advised_topics_to_promotion_email_log.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- 为 t_promotion_email_log 增加第二段 AI 生成字段(推荐投稿方向)
|
||||||
|
-- llm_advised_topics: 大模型生成的"我们特别关注 X/Y/Z 领域"段落,对应模板变量 {{ai_advised_topics}}
|
||||||
|
-- llm_advised_topics_status: 0未处理 1成功 2失败兜底 3跳过(无可用领域)
|
||||||
|
|
||||||
|
ALTER TABLE `t_promotion_email_log`
|
||||||
|
ADD COLUMN `llm_advised_topics` TEXT NULL COMMENT '大模型生成的推荐投稿方向文案' AFTER `llm_description`,
|
||||||
|
ADD COLUMN `llm_advised_topics_status` TINYINT NOT NULL DEFAULT 0 COMMENT 'AI 推荐方向状态: 0未处理 1成功 2失败兜底 3跳过' AFTER `llm_advised_topics`;
|
||||||
Reference in New Issue
Block a user