diff --git a/.env b/.env index b96e1f03..62277e29 100644 --- a/.env +++ b/.env @@ -19,9 +19,19 @@ client_id = 616562 client_secret = CfMDrllyqBTFKrUkO2XaE7OmWTYqP3yd hmac = 8aU8WnITYhwaGTXH +[base] +model_url=http://chat.taimed.cn +model_url1=http://125.39.141.154:10002/v1/chat/completions +model=DeepSeek-Coder-V2-Instruct + +[user_field_ai] +; 留空则依次用 promotion PROMOTION_LLM_URL、citation 等;仅写根地址时会自动补 /v1/chat/completions +;chat_url=http://chat.taimed.cn/v1/chat/completions +;chat_model=DeepSeek-Coder-V2-Instruct + [promotion] PROMOTION_LLM_URL=http://chat.taimed.cn/v1/chat/completions -PROMOTION_LLM_MODEL=your-model-name +PROMOTION_LLM_MODEL=DeepSeek-Coder-V2-Instruct PROMOTION_LLM_TIMEOUT=30 PROMOTION_LLM_FALLBACK="We would like to cordially invite you to consider submitting a manuscript to {{journal_name}}." PROMOTION_LLM_ADVISED_FALLBACK="" diff --git a/application/api/controller/EmailClient.php b/application/api/controller/EmailClient.php index e1142acf..63841fc0 100644 --- a/application/api/controller/EmailClient.php +++ b/application/api/controller/EmailClient.php @@ -1406,8 +1406,8 @@ class EmailClient extends Base return jsonError('Factory is disabled'); } $expertType = intval($factory['expert_type']); - if (!in_array($expertType, [2, 3, 5], true)) { - return jsonError('Only expert_type=2(Editorial Board), 3(Young Editorial Board) or 5(Expert pool) is supported currently'); + if (!in_array($expertType, [2, 3, 4, 5, 6], true)) { + return jsonError('Unsupported expert_type; supported: 2=编委, 3=青年编委, 4=作者, 5=专家库, 6=往期青年编委'); } $journalId = intval($factory['journal_id']); @@ -2260,7 +2260,7 @@ class EmailClient extends Base * 每日自动生成推广任务(由 Linux crontab 调用) * * 逻辑: - * 1. 查询所有 state=0 的任务工厂(支持 expert_type=2 编委 / =5 expert 库;其他类型预留) + * 1. 查询所有 state=0 的任务工厂(支持 expert_type=2/3/4/5/6) * 2. JOIN journal 确认期刊有效(state=0, start_promotion=1) * 3. 按 factory_id + send_date 检查去重 * 4. template/style: 工厂 > 0 用工厂的,否则用期刊默认 @@ -2282,7 +2282,7 @@ class EmailClient extends Base ->alias('f') ->join('t_journal j', 'j.journal_id = f.journal_id', 'inner') ->where('f.state', 0) - ->where('f.expert_type', 'in', [2, 3, 5]) + ->where('f.expert_type', 'in', [2, 3, 4, 5, 6]) ->where('j.state', 0) ->where('f.start_promotion', 1) ->field('f.*, j.title as journal_title, j.default_template_id, j.default_style_id') @@ -2684,6 +2684,7 @@ class EmailClient extends Base 3 => 'Young Editorial Board', 4 => 'Author', 5 => 'Expert Pool', + 6 => 'Past Young Editorial Board', ]; return isset($map[intval($t)]) ? $map[intval($t)] : 'Unknown'; } @@ -2694,7 +2695,7 @@ class EmailClient extends Base * - expert_type = 5:从 t_expert 库选人(按领域 / 国家 / 频次) * 频次:e.ltime(成功发送后回写)+ t_promotion_email_log 中「待发送 state=0 的入队时间 ctime」 * (避免「今日生成任务明日发送」时 ltime 未变导致连续两天选到同一拨人) - * - expert_type ∈ {1,2,3,4}:从系统内部表选人(主编/编委/青年编委/作者),fields 与国家筛选忽略; + * - expert_type ∈ {1,2,3,4,6}:从系统内部表选人(主编/编委/青年编委/作者/往期青年编委),fields 与国家筛选忽略; * 频次按 t_promotion_email_log:已发/退信用 send_time;待发送队列用 ctime(同上) * * 返回行 shape 已对齐: @@ -2782,7 +2783,7 @@ class EmailClient extends Base * * 频次:扣除「同 expert_type 下,no_repeat_days 内 (1) 已发出或退信,或 (2) 仍在队列待发送(state=0,按 ctime)」的人 * - * @param int $expertType 1=主编 2=编委 3=青年编委 4=作者 + * @param int $expertType 1=主编 2=编委 3=青年编委 4=作者 6=往期青年编委 * @param int $journalId * @param int $noRepeatDays * @param int $limit @@ -2817,11 +2818,12 @@ class EmailClient extends Base break; case 1: // 主编(预留,本期不实现) - break; - case 4: // 作者(预留) - Db::name("article_author")->alias('aa') + return []; + + case 4: // 作者:该刊投稿作者(按邮箱关联 t_user) + $query = Db::name('article_author')->alias('aa') ->join('t_user u', 'u.email = aa.email', 'inner') - ->join("t_article a","a.article_id = aa.article_id","left") + ->join('t_article a', 'a.article_id = aa.article_id', 'inner') ->join('t_user_reviewer_info uri', 'uri.reviewer_id = u.user_id', 'left') ->where('a.journal_id', $journalId) ->where('u.email', '<>', '') @@ -2843,6 +2845,10 @@ class EmailClient extends Base return []; } + if (!isset($query)) { + return []; + } + if ($noRepeatDays > 0) { $cutoff = intval(time() - ($noRepeatDays * 86400)); $expertTypeSafe = intval($expertType); diff --git a/application/common/UserFieldAiService.php b/application/common/UserFieldAiService.php index 672e43ed..03560fbf 100644 --- a/application/common/UserFieldAiService.php +++ b/application/common/UserFieldAiService.php @@ -166,7 +166,7 @@ class UserFieldAiService while (true) { $query = Db::name('user')->alias('u') - ->leftJoin('t_user_reviewer_info uri', 'uri.reviewer_id = u.user_id') + ->join('t_user_reviewer_info uri', 'uri.reviewer_id = u.user_id',"left") ->where('u.user_id', '>', $cursor); if (!$force) { $query->where(function ($q) { @@ -270,13 +270,70 @@ class UserFieldAiService return array_values(array_unique(array_filter(array_map('trim', $titles)))); } + /** + * 解析 OpenAI 兼容 chat/completions 完整 URL。 + * base.model_url 常为站点根(如 http://chat.taimed.cn),直接 POST 会 404。 + */ + private function resolveLlmChatUrl() + { + $candidates = [ +// Env::get('user_field_ai.chat_url', ''), +// Env::get('promotion.promotion_llm_url', ''), +// Env::get('expert_country_chat_url', ''), +// Env::get('citation_chat_url', ''), + Env::get('base.model_url1', ''), + ]; + foreach ($candidates as $u) { + $u = trim((string) $u); + if ($u === '') { + continue; + } + $normalized = $this->normalizeChatCompletionsUrl($u); + if ($normalized !== '') { + return $normalized; + } + } + return ''; + } + + private function normalizeChatCompletionsUrl($url) + { + $url = trim((string) $url); + if ($url === '') { + return ''; + } + if (stripos($url, 'chat/completions') !== false) { + return $url; + } + return rtrim($url, '/') . '/v1/chat/completions'; + } + + private function resolveLlmModel() + { + $candidates = [ + Env::get('user_field_ai.chat_model', ''), + Env::get('base.model', ''), + Env::get('promotion.promotion_llm_model', ''), + Env::get('expert_country_chat_model', ''), + Env::get('citation_chat_model', ''), + 'gpt-4.1', + ]; + foreach ($candidates as $m) { + $m = trim((string) $m); + if ($m !== '' && strtolower($m) !== 'your-model-name') { + return $m; + } + } + return ''; + } + private function summarizeWithLlm(array $context) { - $url = trim((string) Env::get('user_field_ai.chat_url', Env::get('expert_country_chat_url', Env::get('citation_chat_url', '')))); - $model = trim((string) Env::get('user_field_ai.chat_model', Env::get('expert_country_chat_model', Env::get('citation_chat_model', 'gpt-4.1')))); + $url = $this->resolveLlmChatUrl(); + $model = $this->resolveLlmModel(); $apiKey = trim((string) Env::get('user_field_ai.chat_api_key', Env::get('expert_country_chat_api_key', Env::get('citation_chat_api_key', '')))); if ($url === '' || $model === '') { - throw new Exception('user_field_ai chat not configured (chat_url / chat_model)'); + throw new Exception('user_field_ai chat not configured (set user_field_ai.chat_url or promotion PROMOTION_LLM_URL / base.model_url)'); } $payloadJson = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); @@ -321,7 +378,10 @@ class UserFieldAiService throw new Exception('LLM curl error: ' . $err); } if ($code < 200 || $code >= 300) { - throw new Exception('LLM HTTP ' . $code . ': ' . mb_substr((string) $raw, 0, 400)); + $hint = ($code === 404 && stripos($url, 'chat/completions') === false) + ? ' (chat_url may be missing /v1/chat/completions)' + : ''; + throw new Exception('LLM HTTP ' . $code . $hint . ': ' . mb_substr((string) $raw, 0, 400)); } $data = json_decode($raw, true); @@ -378,7 +438,6 @@ class UserFieldAiService } Db::name('user_reviewer_info')->insert([ 'reviewer_id' => $userId, - 'ctime' => time(), 'state' => 0, ]); }