Browse Source

接入火山引擎deepseek

wangyunpeng 1 month ago
parent
commit
5eadf54b0a

+ 22 - 0
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/common/enums/ApiChannelEnum.java

@@ -0,0 +1,22 @@
+package com.tzld.piaoquan.longarticle.common.enums;
+
+public enum ApiChannelEnum {
+    OFFICIAL("official", "DeepSeek 官方 API"),
+    VOLCENGINE("volcengine", "火山引擎方舟 API");
+
+    private final String code;
+    private final String desc;
+
+    ApiChannelEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+}

+ 39 - 0
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/model/dto/kimi/AIOfficialApiResponse.java

@@ -0,0 +1,39 @@
+package com.tzld.piaoquan.longarticle.model.dto.kimi;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AIOfficialApiResponse {
+
+    private String id;
+    private String object;
+    private long created;
+    private String model;
+    private List<Choice> choices;
+    private Usage usage;
+
+
+    @Data
+    public static class Choice {
+        private long index;
+        private Message message;
+        private String finishReason;
+    }
+
+
+    @Data
+    public static class Message {
+        private String role;
+        private String content;
+    }
+
+    @Data
+    public static class Usage {
+        private long promptTokens;
+        private long completionTokens;
+        private long totalTokens;
+    }
+
+}

+ 11 - 0
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/model/dto/kimi/AIResult.java

@@ -0,0 +1,11 @@
+package com.tzld.piaoquan.longarticle.model.dto.kimi;
+
+import lombok.Data;
+
+@Data
+public class AIResult {
+    private boolean success;
+    private AIOfficialApiResponse response;
+    private String failReason;
+    private String responseStr;
+}

+ 10 - 25
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/service/local/impl/KimiServiceImpl.java

@@ -2,11 +2,12 @@ package com.tzld.piaoquan.longarticle.service.local.impl;
 
 import com.alibaba.fastjson.JSONObject;
 import com.tzld.piaoquan.longarticle.dao.mapper.longarticle.LongArticlesTextMapper;
+import com.tzld.piaoquan.longarticle.model.dto.kimi.AIResult;
 import com.tzld.piaoquan.longarticle.model.po.longarticle.LongArticlesText;
 import com.tzld.piaoquan.longarticle.model.po.longarticle.LongArticlesTextExample;
 import com.tzld.piaoquan.longarticle.service.local.KimiService;
-import com.tzld.piaoquan.longarticle.utils.other.DeepSeekAPI;
-import com.tzld.piaoquan.longarticle.utils.other.OpenAIUtils;
+import com.tzld.piaoquan.longarticle.service.remote.DeepSeekApiService;
+import com.tzld.piaoquan.longarticle.service.remote.OpenAIUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -23,6 +24,8 @@ public class KimiServiceImpl implements KimiService {
 
     @Autowired
     private LongArticlesTextMapper longArticlesTextMapper;
+    @Autowired
+    private OpenAIUtils openAIUtils;
 
     @Override
     public LongArticlesText getKimiText(String contentId) {
@@ -47,19 +50,20 @@ public class KimiServiceImpl implements KimiService {
         if (longArticlesText.getKimiStatus() == 1) {
             return true;
         }
-        String kimiTitle = processDeepSeekTitle(longArticlesText.getArticleTitle());
+        String kimiTitle = openAIUtils.processDeepSeekTitle(longArticlesText.getArticleTitle());
         if (StringUtils.isEmpty(kimiTitle)) {
             return false;
         }
-        Integer score = OpenAIUtils.getKimiTitleSafeScore(kimiTitle);
+        kimiTitle = kimiTitle.replace("'", "").replace("\"", "").replace("\\", "");
+        Integer score = openAIUtils.getKimiTitleSafeScore(kimiTitle);
         if (score == null || score > SAFE_SCORE) {
-            kimiTitle = OpenAIUtils.makeKimiTitleSafer(longArticlesText.getArticleTitle());
+            kimiTitle = openAIUtils.makeKimiTitleSafer(longArticlesText.getArticleTitle());
         }
         kimiTitle = kimiTitle.replace("'", "").replace("\"", "").replace("\\", "");
         if (StringUtils.isEmpty(kimiTitle)) {
             return false;
         }
-        JSONObject kimiInfo = OpenAIUtils.kimiMining(longArticlesText.getArticleText());
+        JSONObject kimiInfo = openAIUtils.kimiMining(longArticlesText.getArticleText());
         if (kimiInfo == null) {
             return false;
         }
@@ -77,25 +81,6 @@ public class KimiServiceImpl implements KimiService {
         return false;
     }
 
-    public String processDeepSeekTitle(String title) {
-        String single_title_prompt = "请将以上标题改写成适合小程序点击和传播的小程序标题,小程序标题的写作规范如下,请学习后进行小程序标题的编写。直接输出最终的小程序标题\n" +
-                "        小程序标题写作规范:\n" +
-                "        1.要点前置:将最重要的信息放在标题的最前面,以快速吸引读者的注意力。例如,“5月一辈子同学,三辈子亲,送给我的老同学,听哭无数人!”中的“5月”和“一辈子同学,三辈子亲”都是重要的信息点。\n" +
-                "        2.激发情绪:使用能够触动人心的语言,激发读者的情感共鸣。如“只剩两人同学聚会,看后感动落泪。”使用“感动落泪”激发读者的同情和怀旧情绪。\n" +
-                "        3.使用数字和特殊符号:数字可以提供具体性,而特殊符号如“\uD83D\uDD34”、“\uD83D\uDE04”、“\uD83D\uDD25”等可以吸引视觉注意力,增加点击率。\n" +
-                "        4.悬念和好奇心:创建悬念或提出问题,激发读者的好奇心。\n" +
-                "        5.名人效应:如果内容与知名人士相关,提及他们的名字可以增加标题的吸引力。\n" +
-                "        6.社会价值观:触及读者的文化和社会价值观,如家庭、友情、国家荣誉等。\n" +
-                "        7.标点符号的运用:使用感叹号、问号等标点来增强语气和情感表达。\n" +
-                "        8.直接的语言:使用直白、口语化的语言,易于理解,如“狗屁股,笑死我了!”。\n" +
-                "        9.热点人物或事件:提及当前的热点人物或事件,利用热点效应吸引读者。\n" +
-                "        10.字数适中:保持标题在10-20个字之间,既不过长也不过短,确保信息的完整性和吸引力。\n" +
-                "        11.情感或价值诉求:使用如“感动”、“泪目”、“经典”等词汇,直接与读者的情感或价值观产生共鸣。\n" +
-                "        避免误导:确保标题准确反映内容,避免夸大或误导读者。";
-        String prompt = title + "\n" + single_title_prompt;
-        return DeepSeekAPI.chat(prompt);
-    }
-
     public void updateKimiContentStatus(String contentId, Integer status) {
         LongArticlesTextExample example = new LongArticlesTextExample();
         example.createCriteria().andContentIdEqualTo(contentId);

+ 165 - 0
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/service/remote/DeepSeekApiService.java

@@ -0,0 +1,165 @@
+package com.tzld.piaoquan.longarticle.service.remote;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.tzld.piaoquan.longarticle.common.enums.ApiChannelEnum;
+import com.tzld.piaoquan.longarticle.model.dto.kimi.AIOfficialApiResponse;
+import com.tzld.piaoquan.longarticle.model.dto.kimi.AIResult;
+import com.tzld.piaoquan.longarticle.utils.MapBuilder;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.apache.http.util.TextUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@Slf4j
+public class DeepSeekApiService {
+
+    private OkHttpClient client;
+
+    @Value("${deepseek.default.model:deepseek-v4-flash}")
+    private String defaultModel;
+
+    @Value("${deepseek.official.url:https://api.deepseek.com/chat/completions}")
+    private String officialUrl;
+
+    @Value("${deepseek.official.apiKey:sk-62d7b2c37f824735aa4985852c919c1f}")
+    private String officialApiKey;
+
+    @Value("${deepseek.volcengine.url:https://ark.cn-beijing.volces.com/api/v3/chat/completions}")
+    private String volcengineUrl;
+
+    @Value("${deepseek.volcengine.apiKey:ark-b5d6fcbb-14f9-4f70-a92d-605ec6a72c8d-40883}")
+    private String volcengineApiKey;
+
+    @Value("${deepseek.volcengine.model:ep-20250717193758-8gvmz}")
+    private String volcengineModel;
+
+    @PostConstruct
+    public void init() {
+        client = new OkHttpClient().newBuilder()
+                .connectTimeout(5, TimeUnit.MINUTES)
+                .readTimeout(5, TimeUnit.MINUTES)
+                .writeTimeout(5, TimeUnit.MINUTES)
+                .build();
+    }
+
+    /**
+     * 默认调用火山引擎 API
+     */
+    public AIResult request(String prompt) {
+        return request(prompt, null, null, false, ApiChannelEnum.VOLCENGINE);
+    }
+
+    /**
+     * 默认调用火山引擎 API
+     */
+    public AIResult request(String prompt, String model, Double temperature, Boolean isJSON) {
+        return request(prompt, model, temperature, isJSON, ApiChannelEnum.VOLCENGINE);
+    }
+
+    /**
+     * 支持指定渠道的 API 调用
+     */
+    public AIResult request(String prompt, String model, Double temperature, Boolean isJSON, ApiChannelEnum channel) {
+        AIResult result = new AIResult();
+        result.setSuccess(false);
+        if (TextUtils.isBlank(prompt) || TextUtils.isBlank(prompt.trim())) {
+            result.setFailReason("prompt is empty");
+            return result;
+        }
+
+        String url;
+        String apiKey;
+        String defaultChannelModel;
+        String channelName;
+
+        if (channel == ApiChannelEnum.OFFICIAL) {
+            url = officialUrl;
+            apiKey = officialApiKey;
+            defaultChannelModel = defaultModel;
+            channelName = "official";
+        } else {
+            url = volcengineUrl;
+            apiKey = volcengineApiKey;
+            defaultChannelModel = volcengineModel;
+            channelName = "volcengine";
+        }
+
+        if (TextUtils.isBlank(apiKey)) {
+            result.setFailReason(channelName + " apiKey is not configured");
+            return result;
+        }
+
+        try {
+            JSONArray jsonArray = new JSONArray();
+            JSONObject message = new JSONObject();
+            message.put("role", "user");
+            message.put("content", prompt);
+            jsonArray.add(message);
+
+            String useModel = Optional.ofNullable(model).orElse(defaultChannelModel);
+            if (TextUtils.isBlank(useModel)) {
+                result.setFailReason("model is empty");
+                return result;
+            }
+
+            Map<Object, Object> bodyParam = MapBuilder
+                    .builder()
+                    .put("model", useModel)
+                    .put("temperature", Optional.ofNullable(temperature).orElse(0.3))
+                    .put("messages", jsonArray)
+                    .build();
+            if (isJSON) {
+                JSONObject formatJSON = new JSONObject();
+                formatJSON.put("type", "json_object");
+                bodyParam.put("response_format", formatJSON);
+            }
+
+            MediaType mediaType = MediaType.parse("application/json");
+            RequestBody body = RequestBody.create(mediaType, JSONObject.toJSONString(bodyParam));
+            Request request = new Request.Builder()
+                    .url(url)
+                    .method("POST", body)
+                    .addHeader("Content-Type", "application/json")
+                    .addHeader("Accept", "application/json")
+                    .addHeader("Authorization", "Bearer " + apiKey)
+                    .build();
+            Response response = client.newCall(request).execute();
+
+            String responseContent = response.body().string();
+            result.setResponseStr(responseContent);
+            log.info("deepseek {} api responseContent = {}", channelName, responseContent);
+            if (response.isSuccessful()) {
+                AIOfficialApiResponse obj = JSONObject.parseObject(responseContent, AIOfficialApiResponse.class);
+                if (CollectionUtils.isNotEmpty(obj.getChoices())) {
+                    result.setSuccess(true);
+                    result.setResponse(obj);
+                } else {
+                    result.setFailReason("response empty");
+                }
+            } else {
+                JSONObject json = JSONObject.parseObject(responseContent);
+                result.setFailReason("request error code:" + response.code() + " message:" + json.getString("error"));
+            }
+        } catch (Exception e) {
+            log.error("deepseek {} api fail: {}", channelName, e.getMessage());
+            result.setFailReason(e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 调用 DeepSeek 官方 API(向后兼容)
+     */
+    public AIResult requestOfficialApi(String prompt, String model, Double temperature, Boolean isJSON) {
+        return request(prompt, model, temperature, isJSON, ApiChannelEnum.OFFICIAL);
+    }
+}

+ 41 - 65
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/utils/other/OpenAIUtils.java → long-article-server/src/main/java/com/tzld/piaoquan/longarticle/service/remote/OpenAIUtils.java

@@ -1,63 +1,22 @@
-package com.tzld.piaoquan.longarticle.utils.other;
+package com.tzld.piaoquan.longarticle.service.remote;
 
 import com.alibaba.fastjson.JSONObject;
+import com.tzld.piaoquan.longarticle.model.dto.kimi.AIResult;
+import com.tzld.piaoquan.longarticle.utils.JSONUtils;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.math.NumberUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
 
-import java.util.HashMap;
-import java.util.Map;
-
+@Service
+@Slf4j
 public class OpenAIUtils {
 
-    public static void main(String[] args) {
-        System.out.println(getKimiTitleSafeScore("历史上真实的王莽,还真不是传说中的那样"));
-//        System.out.println(makeKimiTitleSafer("历史上真实的王莽,还真不是传说中的那样"));
-//        System.out.println(kimiMining("在这个充满喜悦和庆祝的日子里,我决定为弟弟的婚礼准备一份特别的礼物——30000元作为结婚礼金。\n" +
-//                "这笔钱对我和我的家庭来说不算小数目,但在我看来,能为弟弟的新生活贡献一份力量,这份心意远远超过了金钱的价值。\n" +
-//                "我和丈夫商量过后,都认为这是一个表达我们祝福和支持的好办法。\n" +
-//                "婚礼当天,一切都安排得井井有条。喜悦和幸福的气氛弥漫在空气中,我看着弟弟和他的新娘幸福地站在一起,心中充满了欣慰。\n" +
-//                "然而,在这一天中,我意外听到了一段对话,这段对话让我重新审视了家庭成员之间的关系和支持的真正含义。\n" +
-//                "当我上厕所时,无意中经过一个小房间,听到了里面传出的声音。那是弟弟和母亲的对话。\n" +
-//                "原来,弟弟在担心,担心这份厚重的礼金会给我和我的丈夫带来经济上的负担。\n" +
-//                "他在对母亲说,他宁愿没有这笔钱,也不愿看到我们因此受到影响。这番话让我站在门外的脚步凝固了,心中充满了复杂的情感。\n" +
-//                "\n" +
-//                "我感动于弟弟的体贴和关心,但同时也感到了一丝悲哀。我们之间的爱和支持,难道仅仅局限于物质的给予吗?我对支持的理解,是否太过狭隘,忽略了弟弟真正的感受和需求?\n" +
-//                "泪水在眼眶中打转,我没有进去打扰他们,而是默默地拉起我的丈夫,决定离开宴会,我需要时间来消化这突如其来的情感波动。\n" +
-//                "回到家后,我花了很长时间去思考和反思。我意识到,作为家庭成员,我们之间的支持不应该成为彼此的负担。\n" +
-//                "真正的支持,应该是基于对彼此需求的理解和尊重,而不仅仅是物质上的帮助。\n" +
-//                "\n" +
-//                "于是,我和弟弟进行了一次深入的交谈。我向他解释了我给予礼金的初衷,同时也表达了我对他顾虑的理解和尊重。\n" +
-//                "我们都意识到,家庭成员之间的沟通是多么的重要。只有通过开放和坦诚的沟通,我们才能更好地理解彼此的需求和感受,避免不必要的误解和心理负担。\n" +
-//                "这次事件让我深刻体会到,家庭关系的维护不仅仅需要物质上的支持,更需要心灵上的交流和理解。在未来的日子里,我希望我们每个人都能成为一个倾听者,一个理解者,让我们的家充满爱和支持,而不是压力和负担。\n" +
-//                "家庭的爱,应该是无条件的,它不仅仅存在于物质的传递中,更重要的是心与心之间的连接。\n" +
-//                "通过有效的沟通,我们可以解决误会和矛盾,使得家庭关系变得更加紧密和和谐。\n" +
-//                "我相信,只要我们愿意打开心扉,相互理解和支持,就能构建一个充满爱的家园。\n" +
-//                "\n" +
-//                "这次经历虽然让我一时感到困惑和伤心,但最终却让我获得了更多的成长和领悟。\n" +
-//                "我感谢弟弟无意间给我的这堂课,它让我认识到,在家庭这个小社会中,每个人的感受和需求都是重要的,我们应该学会更加细腻和周到地去关爱彼此。"));
-    }
-
-    public static String getKimiTitle(String title) {
-        String single_title_prompt = "请将以上标题改写成适合小程序点击和传播的小程序标题,小程序标题的写作规范如下,请学习后进行小程序标题的编写。直接输出最终的小程序标题\n" +
-                "        小程序标题写作规范:\n" +
-                "        1.要点前置:将最重要的信息放在标题的最前面,以快速吸引读者的注意力。例如,“5月一辈子同学,三辈子亲,送给我的老同学,听哭无数人!”中的“5月”和“一辈子同学,三辈子亲”都是重要的信息点。\n" +
-                "        2.激发情绪:使用能够触动人心的语言,激发读者的情感共鸣。如“只剩两人同学聚会,看后感动落泪。”使用“感动落泪”激发读者的同情和怀旧情绪。\n" +
-                "        3.使用数字和特殊符号:数字可以提供具体性,而特殊符号如“\uD83D\uDD34”、“\uD83D\uDE04”、“\uD83D\uDD25”等可以吸引视觉注意力,增加点击率。\n" +
-                "        4.悬念和好奇心:创建悬念或提出问题,激发读者的好奇心。例如,“太神奇了!长江水位下降,重庆出现惊奇一幕!”中的“惊奇一幕”就是一个悬念。\n" +
-                "        5.名人效应:如果内容与知名人士相关,提及他们的名字可以增加标题的吸引力。\n" +
-                "        6.社会价值观:触及读者的文化和社会价值观,如家庭、友情、国家荣誉等。\n" +
-                "        7.标点符号的运用:使用感叹号、问号等标点来增强语气和情感表达。\n" +
-                "        8.直接的语言:使用直白、口语化的语言,易于理解,如“狗屁股,笑死我了!”。\n" +
-                "        9.热点人物或事件:提及当前的热点人物或事件,利用热点效应吸引读者。\n" +
-                "        10.字数适中:保持标题在10-20个字之间,既不过长也不过短,确保信息的完整性和吸引力。\n" +
-                "        11.适当的紧迫感:使用“最新”、“首次”、“紧急”等词汇,创造一种紧迫感,促使读者立即行动。\n" +
-                "        12.情感或价值诉求:使用如“感动”、“泪目”、“经典”等词汇,直接与读者的情感或价值观产生共鸣。\n" +
-                "        避免误导:确保标题准确反映内容,避免夸大或误导读者。" +
-                "        输出:只输出标题,不输出其他内容";
-        return KimiAPI.chat(title + "\n" + single_title_prompt);
-    }
+    @Autowired
+    private DeepSeekApiService deepSeekApiService;
 
-    public static Integer getKimiTitleSafeScore(String kimiTitle) {
+    public Integer getKimiTitleSafeScore(String kimiTitle) {
         String prompt = "请你学习一下内容规范,以下标题可能会违反了某条内容规范。请你对标题做一个内容风险评级,1-10分,等级越高内容违规风险越大。 \n" +
                 "        请直接输出内容风险评级的分数,不要输出你的理由、分析等内容。 \n" +
                 "        输出:\n" +
@@ -91,14 +50,16 @@ public class OpenAIUtils {
                 "                (11)不符合《即时通信工具公众信息服务发展管理暂行规定》及遵守法律法规、社会主义制度、国家利益、公民合法利益、公共秩序、社会道德风尚和信息真实性等“七条底线”要求的; \n" +
                 "                (12)含有法律、行政法规禁止的其他内容的。\n" +
                 "        输入的标题是: " + kimiTitle;
-        String res = DeepSeekAPI.chat(prompt);
+
+        AIResult result = deepSeekApiService.request(prompt);
+        String res = result.getResponse().getChoices().get(0).getMessage().getContent();
         if (StringUtils.isNotEmpty(res) && NumberUtils.isParsable(res)) {
             return Integer.parseInt(res);
         }
         return null;
     }
 
-    public static String makeKimiTitleSafer(String title) {
+    public String makeKimiTitleSafer(String title) {
         String prompt = "以下每行为一个文章的标题,请用尽量平实的语言对以上标题进行改写,保持在10~15字左右,请注意:\n" +
                 "            1. 不要虚构或改变标题的含义。\n" +
                 "            2. 不要用笃定的语气描述存疑的可能性,不要将表述可能性的问句改为肯定句。\n" +
@@ -111,15 +72,13 @@ public class OpenAIUtils {
                 "                \"title_v2\": 请填写第二次输出的标题\n" +
                 "                }\n" +
                 "            输入的标题是: " + title;
-        Map<String, String> responseFormat = new HashMap<>();
-        responseFormat.put("type", "json_object");
-        String res = DeepSeekAPI.chat(prompt, responseFormat);
-//        String res = KimiAPI.jsonChat(prompt);
-        JSONObject jsonObject = JSONObject.parseObject(res);
+        AIResult result = deepSeekApiService.request(prompt, null, null, true);
+        String res = result.getResponse().getChoices().get(0).getMessage().getContent();
+        JSONObject jsonObject = JSONUtils.parseAIJsonContent(res);
         return jsonObject.getString("title_v2");
     }
 
-    public static JSONObject kimiMining(String text) {
+    public JSONObject kimiMining(String text) {
         String textPrompt = "请从我给你的文章中挖掘出以下信息并且返回如下结果。\n" +
                 "        你返回的结果是一个 json, 格式如下:\n" +
                 "        {\n" +
@@ -127,15 +86,32 @@ public class OpenAIUtils {
                 "            \"content_title\": 一个总结性的标题,该标题应简洁并能够反映文章的主要内容\n" +
                 "        }\n" +
                 "        你需要处理的文本是:";
-        Map<String, String> responseFormat = new HashMap<>();
-        responseFormat.put("type", "json_object");
-        String content = DeepSeekAPI.chat(textPrompt + text, responseFormat);
-//        String content = KimiAPI.jsonChat(textPrompt + text);
+        AIResult result = deepSeekApiService.request(textPrompt + text, null, null, true);
+        String content = result.getResponse().getChoices().get(0).getMessage().getContent();
         if (StringUtils.isEmpty(content)) {
             return null;
         }
-        return JSONObject.parseObject(content);
+        return JSONUtils.parseAIJsonContent(content);
     }
 
+    public String processDeepSeekTitle(String title) {
+        String single_title_prompt = "请将以上标题改写成适合小程序点击和传播的小程序标题,小程序标题的写作规范如下,请学习后进行小程序标题的编写。直接输出最终的小程序标题\n" +
+                "        小程序标题写作规范:\n" +
+                "        1.要点前置:将最重要的信息放在标题的最前面,以快速吸引读者的注意力。例如,“5月一辈子同学,三辈子亲,送给我的老同学,听哭无数人!”中的“5月”和“一辈子同学,三辈子亲”都是重要的信息点。\n" +
+                "        2.激发情绪:使用能够触动人心的语言,激发读者的情感共鸣。如“只剩两人同学聚会,看后感动落泪。”使用“感动落泪”激发读者的同情和怀旧情绪。\n" +
+                "        3.使用数字和特殊符号:数字可以提供具体性,而特殊符号如“\uD83D\uDD34”、“\uD83D\uDE04”、“\uD83D\uDD25”等可以吸引视觉注意力,增加点击率。\n" +
+                "        4.悬念和好奇心:创建悬念或提出问题,激发读者的好奇心。\n" +
+                "        5.名人效应:如果内容与知名人士相关,提及他们的名字可以增加标题的吸引力。\n" +
+                "        6.社会价值观:触及读者的文化和社会价值观,如家庭、友情、国家荣誉等。\n" +
+                "        7.标点符号的运用:使用感叹号、问号等标点来增强语气和情感表达。\n" +
+                "        8.直接的语言:使用直白、口语化的语言,易于理解,如“狗屁股,笑死我了!”。\n" +
+                "        9.热点人物或事件:提及当前的热点人物或事件,利用热点效应吸引读者。\n" +
+                "        10.字数适中:保持标题在10-20个字之间,既不过长也不过短,确保信息的完整性和吸引力。\n" +
+                "        11.情感或价值诉求:使用如“感动”、“泪目”、“经典”等词汇,直接与读者的情感或价值观产生共鸣。\n" +
+                "        避免误导:确保标题准确反映内容,避免夸大或误导读者。";
+        String prompt = title + "\n" + single_title_prompt;
+        AIResult res = deepSeekApiService.request(prompt);
+        return res.getResponse().getChoices().get(0).getMessage().getContent();
+    }
 
 }

+ 89 - 0
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/utils/JSONUtils.java

@@ -0,0 +1,89 @@
+package com.tzld.piaoquan.longarticle.utils;
+
+import com.alibaba.fastjson.JSONObject;
+import com.google.common.reflect.TypeToken;
+import com.google.gson.GsonBuilder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+@Slf4j
+public class JSONUtils {
+
+
+    public static String toJsonNew(Object obj) {
+        if (obj == null) {
+            return "";
+        }
+        try {
+            return JSONObject.toJSONString(obj);
+        } catch (Exception e) {
+            log.error("toJson exception", e);
+            return "";
+        }
+    }
+
+    public static String toJson(Object obj) {
+        if (obj == null) {
+            return "";
+        }
+        try {
+            return new GsonBuilder().disableHtmlEscaping().create().toJson(obj);
+        } catch (Exception e) {
+            log.error("toJson exception", e);
+            return "";
+        }
+    }
+
+    public static <T> T fromJson(String value, TypeToken<T> typeToken, T defaultValue) {
+
+        if (StringUtils.isBlank(value)) {
+            return defaultValue;
+        }
+        try {
+            return JSONObject.parseObject(value, typeToken.getType());
+        } catch (Exception e) {
+            log.error("parseObject error! value=[{}]", value, e);
+        }
+        return defaultValue;
+    }
+
+    /**
+     * 解析AI返回的JSON内容,自动处理markdown代码块包裹的情况
+     * AI返回的内容可能被 ```json ... ``` 包裹,导致直接解析失败
+     */
+    public static JSONObject parseAIJsonContent(String content) {
+        if (StringUtils.isBlank(content)) {
+            return null;
+        }
+        // 先直接尝试解析
+        try {
+            return JSONObject.parseObject(content);
+        } catch (Exception e) {
+            log.warn("JSON直接解析失败,尝试清理markdown格式后重试, content=[{}]", content);
+        }
+        // 清理markdown代码块标记后重试
+        String cleaned = cleanMarkdownJsonBlock(content);
+        try {
+            return JSONObject.parseObject(cleaned);
+        } catch (Exception e) {
+            log.error("清理markdown格式后JSON解析仍然失败, cleaned=[{}]", cleaned, e);
+            return null;
+        }
+    }
+
+    /**
+     * 清理markdown代码块标记,如 ```json ... ```
+     */
+    public static String cleanMarkdownJsonBlock(String content) {
+        if (StringUtils.isBlank(content)) {
+            return content;
+        }
+        String cleaned = content.trim();
+        // 去除开头的 ```json 或 ```
+        cleaned = cleaned.replaceAll("^```(?:json)?\\s*", "");
+        // 去除结尾的 ```
+        cleaned = cleaned.replaceAll("\\s*```$", "");
+        return cleaned.trim();
+    }
+
+}

+ 50 - 0
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/utils/MapBuilder.java

@@ -0,0 +1,50 @@
+package com.tzld.piaoquan.longarticle.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author: TanJingyu
+ * @create:2023-06-12 13:28:18
+ **/
+public class MapBuilder {
+
+    private MapBuilder() {
+    }
+
+    public static <K, V> Builder<K, V> builder() {
+        return new Builder<>(null);
+    }
+
+    public static <K, V> Builder<K, V> builder(Map<K, V> map) {
+        return new Builder<>(map);
+    }
+
+    public static class Builder<K, V> {
+        private Map<K, V> resultMap = new HashMap<>();
+
+        private Builder(Map<K, V> map) {
+            if (Objects.nonNull(map)) {
+                resultMap = map;
+            }
+        }
+
+        public Builder<K, V> put(K k, V v) {
+            resultMap.put(k, v);
+            return this;
+        }
+
+        public Map<K, V> build() {
+            return resultMap;
+        }
+    }
+
+
+    public static void main(String[] args) {
+        Map<String, Object> build = MapBuilder.<String, Object>builder().put("name", "忘记")
+                .put("age", 23)
+                .build();
+        System.out.println(build);
+    }
+}

+ 0 - 74
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/utils/other/DeepSeek.java

@@ -1,74 +0,0 @@
-package com.tzld.piaoquan.longarticle.utils.other;
-
-import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest;
-import com.volcengine.ark.runtime.model.completion.chat.ChatMessage;
-import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole;
-import com.volcengine.ark.runtime.service.ArkService;
-import lombok.extern.slf4j.Slf4j;
-import okhttp3.ConnectionPool;
-import okhttp3.Dispatcher;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-@Slf4j
-public class DeepSeek {
-
-    // 在类加载时就创建单例实例
-    private static final ArkService service;
-
-    static {
-        String apiKey = "5e275c38-44fd-415f-abcf-4b59f6377f72";
-        ConnectionPool connectionPool = new ConnectionPool(5, 5, TimeUnit.SECONDS);
-        Dispatcher dispatcher = new Dispatcher();
-        service = ArkService.builder()
-                .dispatcher(dispatcher)
-                .connectionPool(connectionPool)
-                .apiKey(apiKey)
-                .build();
-    }
-
-    // 私有构造函数,防止外部实例化
-    private DeepSeek() {
-    }
-
-
-    public static String getTitle(String title) {
-        try {
-            String single_title_prompt = "请将以上标题改写成适合小程序点击和传播的小程序标题,小程序标题的写作规范如下,请学习后进行小程序标题的编写。直接输出最终的小程序标题\n" +
-                    "        小程序标题写作规范:\n" +
-                    "        1.要点前置:将最重要的信息放在标题的最前面,以快速吸引读者的注意力。例如,“5月一辈子同学,三辈子亲,送给我的老同学,听哭无数人!”中的“5月”和“一辈子同学,三辈子亲”都是重要的信息点。\n" +
-                    "        2.激发情绪:使用能够触动人心的语言,激发读者的情感共鸣。如“只剩两人同学聚会,看后感动落泪。”使用“感动落泪”激发读者的同情和怀旧情绪。\n" +
-                    "        3.使用数字和特殊符号:数字可以提供具体性,而特殊符号如“\uD83D\uDD34”、“\uD83D\uDE04”、“\uD83D\uDD25”等可以吸引视觉注意力,增加点击率。\n" +
-                    "        4.悬念和好奇心:创建悬念或提出问题,激发读者的好奇心。\n" +
-                    "        5.名人效应:如果内容与知名人士相关,提及他们的名字可以增加标题的吸引力。\n" +
-                    "        6.社会价值观:触及读者的文化和社会价值观,如家庭、友情、国家荣誉等。\n" +
-                    "        7.标点符号的运用:使用感叹号、问号等标点来增强语气和情感表达。\n" +
-                    "        8.直接的语言:使用直白、口语化的语言,易于理解,如“狗屁股,笑死我了!”。\n" +
-                    "        9.热点人物或事件:提及当前的热点人物或事件,利用热点效应吸引读者。\n" +
-                    "        10.字数适中:保持标题在10-20个字之间,既不过长也不过短,确保信息的完整性和吸引力。\n" +
-                    "        11.情感或价值诉求:使用如“感动”、“泪目”、“经典”等词汇,直接与读者的情感或价值观产生共鸣。\n" +
-                    "        避免误导:确保标题准确反映内容,避免夸大或误导读者。";
-
-            final List<ChatMessage> messages = new ArrayList<>();
-            final ChatMessage systemMessage = ChatMessage.builder()
-                    .role(ChatMessageRole.SYSTEM)
-                    .content("")
-                    .build();
-            final ChatMessage userMessage = ChatMessage.builder().role(ChatMessageRole.USER).content(title + "\n" + single_title_prompt).build();
-            messages.add(systemMessage);
-            messages.add(userMessage);
-
-            ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
-                    .model("ep-20250213194558-rrmr2")
-                    .messages(messages)
-                    .build();
-
-            return (String) service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage().getContent();
-        } catch (Exception e) {
-            log.error("DeepSeek getTitle error:{}", e.getMessage());
-        }
-        return "";
-    }
-}

+ 0 - 85
long-article-server/src/main/java/com/tzld/piaoquan/longarticle/utils/other/DeepSeekAPI.java

@@ -1,85 +0,0 @@
-package com.tzld.piaoquan.longarticle.utils.other;
-
-import cn.hutool.http.HttpRequest;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.util.CollectionUtils;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class DeepSeekAPI {
-
-    /**
-     * 聊天端点
-     */
-    static String chatEndpoint = "https://api.deepseek.com/chat/completions";
-    /**
-     * api密匙
-     */
-    static String apiKey = "Bearer sk-b756bdbc96a04db89076e24a3f663f1e";
-
-    /**
-     * 发送消息
-     *
-     * @param txt 内容
-     * @return {@link String}
-     */
-    public static String chat(String txt) {
-        return chat(txt, null);
-    }
-
-    public static String chat(String txt, Map<String, String> responseFormat) {
-        Map<String, Object> paramMap = new HashMap<>();
-        paramMap.put("model", "deepseek-v4-flash");
-        if (!CollectionUtils.isEmpty(responseFormat)) {
-            paramMap.put("response_format", responseFormat);
-        }
-        List<Map<String, String>> dataList = new ArrayList<>();
-        dataList.add(new HashMap<String, String>() {{
-            put("role", "user");
-            put("content", txt);
-        }});
-        paramMap.put("messages", dataList);
-        JSONObject message = null;
-        try {
-            String body = HttpRequest.post(chatEndpoint)
-                    .header("Authorization", apiKey)
-                    .header("Content-Type", "application/json")
-                    .body(JSONObject.toJSONString(paramMap))
-                    .execute()
-                    .body();
-            JSONObject jsonObject = JSONObject.parseObject(body);
-            JSONArray choices = jsonObject.getJSONArray("choices");
-            JSONObject result = choices.getJSONObject(0);
-            message = result.getJSONObject("message");
-            return message.getString("content");
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        return "";
-    }
-
-    public static void main(String[] args) {
-        String title = "63岁大爷退休金13000,再婚娶37岁女护士,新婚当晚,他看到她的肚子,瞬间晕倒在地";
-        String single_title_prompt = "请将以上标题改写成适合小程序点击和传播的小程序标题,小程序标题的写作规范如下,请学习后进行小程序标题的编写。直接输出最终的小程序标题\n" +
-                "        小程序标题写作规范:\n" +
-                "        1.要点前置:将最重要的信息放在标题的最前面,以快速吸引读者的注意力。例如,“5月一辈子同学,三辈子亲,送给我的老同学,听哭无数人!”中的“5月”和“一辈子同学,三辈子亲”都是重要的信息点。\n" +
-                "        2.激发情绪:使用能够触动人心的语言,激发读者的情感共鸣。如“只剩两人同学聚会,看后感动落泪。”使用“感动落泪”激发读者的同情和怀旧情绪。\n" +
-                "        3.使用数字和特殊符号:数字可以提供具体性,而特殊符号如“\uD83D\uDD34”、“\uD83D\uDE04”、“\uD83D\uDD25”等可以吸引视觉注意力,增加点击率。\n" +
-                "        4.悬念和好奇心:创建悬念或提出问题,激发读者的好奇心。例如,“太神奇了!长江水位下降,重庆出现惊奇一幕!”中的“惊奇一幕”就是一个悬念。\n" +
-                "        5.名人效应:如果内容与知名人士相关,提及他们的名字可以增加标题的吸引力。\n" +
-                "        6.社会价值观:触及读者的文化和社会价值观,如家庭、友情、国家荣誉等。\n" +
-                "        7.标点符号的运用:使用感叹号、问号等标点来增强语气和情感表达。\n" +
-                "        8.直接的语言:使用直白、口语化的语言,易于理解,如“狗屁股,笑死我了!”。\n" +
-                "        9.热点人物或事件:提及当前的热点人物或事件,利用热点效应吸引读者。\n" +
-                "        10.字数适中:保持标题在10-20个字之间,既不过长也不过短,确保信息的完整性和吸引力。\n" +
-                "        11.适当的紧迫感:使用“最新”、“首次”、“紧急”等词汇,创造一种紧迫感,促使读者立即行动。\n" +
-                "        12.情感或价值诉求:使用如“感动”、“泪目”、“经典”等词汇,直接与读者的情感或价值观产生共鸣。\n" +
-                "        避免误导:确保标题准确反映内容,避免夸大或误导读者。";
-        System.out.println(chat(title + "\n" + single_title_prompt));
-    }
-}

+ 2 - 2
long-article-server/src/main/resources/application-test.properties

@@ -25,8 +25,8 @@ apollo.meta: https://apolloconfig-internal.piaoquantv.com
 
 xxl.job.admin.addresses=http://test-xxl-job-internal.piaoquantv.com/xxl-job-admin
 
-download.path=/Users/shimeng/Desktop/download/
+download.path=./download/
 
 aliyun.log.project=long-article-manage
 
-logging.file.path=/Users/shimeng/Desktop/log/${spring.application.name}
+logging.file.path=./log/${spring.application.name}

+ 42 - 19
long-article-server/src/test/java/com/tzld/piaoquan/longarticle/DeepSeekTest.java

@@ -1,33 +1,56 @@
 package com.tzld.piaoquan.longarticle;
 
-import com.tzld.piaoquan.longarticle.utils.other.DeepSeekAPI;
+import com.alibaba.fastjson.JSONObject;
+import com.tzld.piaoquan.longarticle.model.dto.kimi.AIResult;
+import com.tzld.piaoquan.longarticle.service.remote.DeepSeekApiService;
+import com.tzld.piaoquan.longarticle.service.remote.OpenAIUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
 @SpringBootTest(classes = LongArticleServerApplication.class)
 @Slf4j
 public class DeepSeekTest {
 
+    @Autowired
+    private OpenAIUtils openAIUtils;
+
+    @Test
+    public void getKimiTitleSafeScoreTest() {
+        String title = "十位开国大将,只有他一人去世全国降半旗致哀,不是第一大将粟裕";
+        Integer score = openAIUtils.getKimiTitleSafeScore(title);
+        System.out.println(score);
+    }
+
+    @Test
+    public void makeKimiTitleSaferTest() {
+        String title = "十位开国大将,只有他一人去世全国降半旗致哀,不是第一大将粟裕";
+        String res = openAIUtils.makeKimiTitleSafer(title);
+        System.out.println(res);
+    }
+
+    @Test
+    public void kimiMiningTest() {
+        String text = "降半旗致哀是一种国际上用于悼念杰出人物的高规格礼仪。在中国,享受这一待遇的人非常少,通常仅限于对国家做出杰出贡献的领导人。众所周知,周恩来总理去世时,联合国曾为其降半旗致哀,以表彰他为世界和平事业做出的贡献。然而,在中国的历史上,并非所有的开国大将都享有这一殊荣。罗瑞卿大将是唯一一位在去世后,全国为其降半旗致哀的开国大将,这一特殊待遇引起了人们的广泛关注和讨论。\n" +
+                "罗瑞卿,四川南充县人,早年受进步思想影响,积极参与爱国学生运动,走上了革命道路。1926年,年仅20岁的罗瑞卿考入黄埔军校武汉分校,开启了他的军事生涯。次年,他参加了南昌起义,随后加入中国共产党,并被派往闽西组建地方武装,参与中央苏区的反“围剿”作战。在第二次反“围剿”中,他英勇负伤,子弹射穿了他的左腮,但他仍然坚持战斗,这种不屈不挠的精神为他赢得了广泛尊重。\n" +
+                "\n" +
+                "在解放战争时期,虽然罗瑞卿的战功不及被誉为“第一大将”的粟裕,但他在平津战役中全歼国民党35军,为战役的胜利做出了重要贡献。新中国成立后,罗瑞卿成为首任公安部长,并在1955年被授予大将军衔。随后,他担任了解放军总参谋长、副总理等多个重要职务,为国家的安全和军事现代化建设倾尽全力。\n" +
+                "罗瑞卿在政工方面的出色工作使他与粟裕形成鲜明对比。尽管粟裕在军事指挥方面的成就卓越,但罗瑞卿在政府和军队中的职务更为多元化,特别是担任副总理和军委秘书长,这些职务显示了他在政治和军事领域的全面影响力。1975年,罗瑞卿重返工作岗位,继续为国家贡献力量,直到1977年,他被任命为军委秘书长。\n" +
+                "\n" +
+                "1978年,罗瑞卿因公出访西德,不幸去世,享年72岁。他是唯一一位客死海外的开国大将。考虑到他的职务、贡献以及他在中国革命和建设中的特殊地位,国家决定在他去世后全国降半旗致哀。这一仪式不仅是对罗瑞卿个人功绩的高度认可,也是对他为国家和人民所做贡献的深切缅怀。\n" +
+                "降半旗致哀象征着国家对逝者的最高敬意。在中国,除罗瑞卿外,能够享受此待遇的还有毛泽东、周恩来、刘少奇、朱德等党和国家领导人。罗瑞卿的特殊待遇也引发了人们对他生平和贡献的重新审视。他不仅在军事上具有突出作用,而且在新中国的法律和安全体系建设中发挥了关键作用。\n" +
+                "\n" +
+                "罗瑞卿的一生充满了传奇色彩,他不仅是一位杰出的军事家,更是一位优秀的革命家和政治家。他在革命道路上的坚持与付出,为中国的解放和社会主义建设做出了不可磨灭的贡献。罗瑞卿以其坚定的信仰、卓越的才干和不懈的奋斗精神,成为中国革命史上的重要人物。他的去世使得全国上下为之动容,全国降半旗致哀的仪式既是对他个人成就的高度肯定,也是对他为国家所做贡献的深切悼念。\n" +
+                "总之,罗瑞卿以其卓越的贡献和崇高的品质,赢得了全国人民的尊敬和纪念。虽然他已离世多年,但他的精神和业绩仍将长存于世,激励着后人继续为国家的建设和发展而奋斗。全国降半旗致哀不仅是对罗瑞卿个人的悼念,更是对所有为国家事业奉献一生的革命先驱的集体致敬。";
+        JSONObject res = openAIUtils.kimiMining(text);
+        System.out.println(res.toJSONString());
+    }
+
     @Test
-    public void testSyncContentPlatformQwDatastatReplyTotalJob() {
-        String title = "63岁大爷退休金13000,再婚娶37岁女护士,新婚当晚,他看到她的肚子,瞬间晕倒在地";
-        String single_title_prompt = "请将以上标题改写成适合小程序点击和传播的小程序标题,小程序标题的写作规范如下,请学习后进行小程序标题的编写。直接输出最终的小程序标题\n" +
-                "        小程序标题写作规范:\n" +
-                "        1.要点前置:将最重要的信息放在标题的最前面,以快速吸引读者的注意力。例如,“5月一辈子同学,三辈子亲,送给我的老同学,听哭无数人!”中的“5月”和“一辈子同学,三辈子亲”都是重要的信息点。\n" +
-                "        2.激发情绪:使用能够触动人心的语言,激发读者的情感共鸣。如“只剩两人同学聚会,看后感动落泪。”使用“感动落泪”激发读者的同情和怀旧情绪。\n" +
-                "        3.使用数字和特殊符号:数字可以提供具体性,而特殊符号如“\uD83D\uDD34”、“\uD83D\uDE04”、“\uD83D\uDD25”等可以吸引视觉注意力,增加点击率。\n" +
-                "        4.悬念和好奇心:创建悬念或提出问题,激发读者的好奇心。\n" +
-                "        5.名人效应:如果内容与知名人士相关,提及他们的名字可以增加标题的吸引力。\n" +
-                "        6.社会价值观:触及读者的文化和社会价值观,如家庭、友情、国家荣誉等。\n" +
-                "        7.标点符号的运用:使用感叹号、问号等标点来增强语气和情感表达。\n" +
-                "        8.直接的语言:使用直白、口语化的语言,易于理解,如“狗屁股,笑死我了!”。\n" +
-                "        9.热点人物或事件:提及当前的热点人物或事件,利用热点效应吸引读者。\n" +
-                "        10.字数适中:保持标题在10-20个字之间,既不过长也不过短,确保信息的完整性和吸引力。\n" +
-                "        11.情感或价值诉求:使用如“感动”、“泪目”、“经典”等词汇,直接与读者的情感或价值观产生共鸣。\n" +
-                "        避免误导:确保标题准确反映内容,避免夸大或误导读者。";
-        String prompt = title + "\n" + single_title_prompt;
-        String res =  DeepSeekAPI.chat(prompt);
+    public void processDeepSeekTitleTest() {
+        String title = "十位开国大将,只有他一人去世全国降半旗致哀,不是第一大将粟裕";
+        String res = openAIUtils.processDeepSeekTitle(title);
         System.out.println(res);
     }