苍穹外卖新增Ai功能终章

苍穹外卖新增Ai功能终章

接上一篇章后续

新增功能

sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" :is-collapse="isCollapse" /> 

通过源码我们发现 左侧菜单栏是按照路由表来循环遍历的

所以我们在路由表中新增

跳转地址

在views/ai-service下新建一个index.vue

Index代码如下:

<template> <div> <div> <h3>AI智能客服</h3> <el-button icon="el-icon-close" @click="closeService">关闭</el-button> </div> <div> <!-- 统一循环 --> <div v-for="msg in messages" :key="msg.id" :class="msg.role" > <!-- Bot/Human 头像在左侧 --> <img v-if="msg.role !== 'user'" :src="msg.role === 'human' ? humanAvatar : aiAvatar" /> <!-- 消息内容 --> <div>{{ msg.content }}</div> <!-- User 头像在右侧 --> <img v-if="msg.role === 'user'" src="@/assets/user-avatar.png" /> </div> <!-- 等待动画 --> <div v-if="loading"> <img src="@/assets/ai-avatar.png" alt="AI客服" /> <div> <div v-for="(_, i) in 3" :key="i"></div> </div> </div> </div> 

后端修改篇

前端传过来的数据为:

{ "question": "你好" } 

所以我们定义后端返回的数据为

{ "code": 1, "msg": null, "data": "您好呀!欢迎来咨询,请问有什么我能帮您的吗?" } 

根据这个我们来改写代码:

首先在Controller中创建

AiController

@PostMapping public Result<String> getAnswer(@RequestBody Map<String, String> payload) { String question = payload.get("question"); return Result.success(aiService.getAnswer(question)); } 

然后根据大模型给的api调用格式

public static List<RoleContent> historyList=new ArrayList<>(); // 对话历史存储集合 public static String; // 大模型的答案汇总 // 环境治理的重要性 环保 人口老龄化 我爱我的祖国 public String NewQuestion = "demo"; public static final Gson gson = new Gson(); // 个性化参数 private String userId; private Boolean wsCloseFlag; private static Boolean totalFlag=true; // 控制提示用户是否输入 // 构造函数 public BigModelNew(String userId, Boolean wsCloseFlag) { this.userId = userId; this.wsCloseFlag = wsCloseFlag; } // 主函数 public static void main(String[] args) throws Exception { // 个性化参数入口,如果是并发使用,可以在这里模拟 while (true){ if(totalFlag){ Scanner scanner=new Scanner(System.in); System.out.print("我:"); totalFlag=false; // 构建鉴权url String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret); OkHttpClient client = new OkHttpClient.Builder().build(); String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://"); Request request = new Request.Builder().url(url).build(); for (int i = 0; i < 1; i++) {; WebSocket webSocket = client.newWebSocket(request, new BigModelNew(i + "", false)); } }else{ Thread.sleep(200); } } } public static boolean canAddHistory(){ // 由于历史记录最大上线1.2W左右,需要判断是能能加入历史 int history_length=0; for(RoleContent temp:historyList){ history_length=history_length+temp.content.length(); } if(history_length>12000){ historyList.remove(0); historyList.remove(1); historyList.remove(2); historyList.remove(3); historyList.remove(4); return false; }else{ return true; } } // 线程来发送音频与参数 class MyThread extends Thread { private WebSocket webSocket; public MyThread(WebSocket webSocket) { this.webSocket = webSocket; } public void run() { try { JSONObject requestJson=new JSONObject(); JSONObject header=new JSONObject(); // header参数 header.put("app_id",appid); header.put("uid",UUID.randomUUID().toString().substring(0, 10)); JSONObject parameter=new JSONObject(); // parameter参数 JSONObject chat=new JSONObject(); chat.put("domain",domain); chat.put("temperature",0.5); chat.put("max_tokens",4096); parameter.put("chat",chat); JSONObject payload=new JSONObject(); // payload参数 JSONObject message=new JSONObject(); JSONArray text=new JSONArray(); // 历史问题获取 if(historyList.size()>0){ for(RoleContent tempRoleContent:historyList){ text.add(JSON.toJSON(tempRoleContent)); } } // 最新问题 RoleContent roleContent=new RoleContent(); roleContent.role="user"; roleContent.content=NewQuestion; text.add(JSON.toJSON(roleContent)); historyList.add(roleContent); message.put("text",text); payload.put("message",message); requestJson.put("header",header); requestJson.put("parameter",parameter); requestJson.put("payload",payload); // System.err.println(requestJson); // 可以打印看每次的传参明细 webSocket.send(requestJson.toString()); // 等待服务端返回完毕后关闭 while (true) { // System.err.println(wsCloseFlag + "---"); Thread.sleep(200); if (wsCloseFlag) { break; } } webSocket.close(1000, ""); } catch (Exception e) { e.printStackTrace(); } } } @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); System.out.print("大模型:"); MyThread myThread = new MyThread(webSocket); myThread.start(); } @Override public void onMessage(WebSocket webSocket, String text) { // System.out.println(userId + "用来区分那个用户的结果" + text); JsonParse myJsonParse = gson.fromJson(text, JsonParse.class); if (myJsonParse.header.code != 0) { System.out.println("发生错误,错误码为:" + myJsonParse.header.code); System.out.println("本次请求的sid为:" + myJsonParse.header.sid); webSocket.close(1000, ""); } List<Text> textList = myJsonParse.payload.choices.text; for (Text temp : textList) { System.out.print(temp.content); totalAnswer=totalAnswer+temp.content; } if (myJsonParse.header.status == 2) { // 可以关闭连接,释放资源 System.out.println(); System.out.println("*************************************************************************************"); if(canAddHistory()){ RoleContent roleContent=new RoleContent(); roleContent.setRole("assistant"); roleContent.setContent(totalAnswer); historyList.add(roleContent); }else{ historyList.remove(0); RoleContent roleContent=new RoleContent(); roleContent.setRole("assistant"); roleContent.setContent(totalAnswer); historyList.add(roleContent); } wsCloseFlag = true; totalFlag=true; } } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { super.onFailure(webSocket, t, response); try { if (null != response) { int code = response.code(); System.out.println("onFailure code:" + code); System.out.println("onFailure body:" + response.body().string()); if (101 != code) { System.out.println("connection failed"); System.exit(0); } } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // 鉴权方法 public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception { URL url = new URL(hostUrl); // 时间 SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); format.setTimeZone(TimeZone.getTimeZone("GMT")); String date = format.format(new Date()); // 拼接 String preStr = "host: " + url.getHost() + "\n" + "date: " + date + "\n" + "GET " + url.getPath() + " HTTP/1.1"; // System.err.println(preStr); // SHA256加密 Mac mac = Mac.getInstance("hmacsha256"); SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256"); mac.init(spec); byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); // Base64加密 String sha = Base64.getEncoder().encodeToString(hexDigits); // System.err.println(sha); // 拼接 String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha); // 拼接地址 HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder().// addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))).// addQueryParameter("date", date).// addQueryParameter("host", url.getHost()).// build(); // System.err.println(httpUrl.toString()); return httpUrl.toString(); } //返回的json结果拆解 class JsonParse { Header header; Payload payload; } class Header { int code; int status; String sid; } class Payload { Choices choices; } class Choices { List<Text> text; } class Text { String role; String content; } class RoleContent{ String role; String content; public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } 

然后我们对代码进行改造

先创建

创建aiService

public interface AiService {     public String getAnswer(String question); }

实现Aiservice

然后先把账号内容配置注入到程序

// 配置参数 @Value("${ai.hostUrl}") private String; @Value("${ai.domain}") private String; @Value("${ai.appid}") private String; @Value("${ai.apiSecret}") private String; @Value("${ai.apiKey}") private String; 
// 对话历史存储集合 private final List<RoleContent> historyList = new ArrayList<>(); // 大模型的答案汇总 private StringBuilder totalAnswer = new StringBuilder(); // Gson实例,用于JSON解析 private static final Gson gson = new Gson(); 

根据科大讯飞的api的调用文档 我们发现 可以设置对话背景

因为我们是商家的客服 所以我们压入一个对话背景

RoleContent roleSystem = new RoleContent(); roleSystem.setRole("system"); roleSystem.setContent("你是一个智能客服,接下来你要用客服的语气和我对话"); text.add(JSON.toJSON(roleSystem)); 

总体代码如下:

public class AiServiceImpl implements AiService { // 对话历史存储集合 private final List<RoleContent> historyList = new ArrayList<>(); // 大模型的答案汇总 private StringBuilder totalAnswer = new StringBuilder(); // Gson实例,用于JSON解析 private static final Gson gson = new Gson(); // 配置参数 @Value("${ai.hostUrl}") private String; @Value("${ai.domain}") private String; @Value("${ai.appid}") private String; @Value("${ai.apiSecret}") private String; @Value("${ai.apiKey}") private String; @Override public String getAnswer(String question) { if (question == null || question.trim().isEmpty()) { return "请输入有效问题"; } try { // 构建鉴权URL String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret); OkHttpClient client = new OkHttpClient.Builder().build(); String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://"); Request request = new Request.Builder().url(url).build(); // 使用CountDownLatch等待响应 CountDownLatch latch = new CountDownLatch(1); WebSocket webSocket = client.newWebSocket(request, new AiWebSocketListener(latch, question)); // 等待响应完成或超时 boolean completed = latch.await(30, TimeUnit.SECONDS); if (!completed) { webSocket.close(1000, "请求超时"); return "请求超时,请重试"; } log.info("AI回答:" + totalAnswer); return totalAnswer.toString(); } catch (Exception e) { e.printStackTrace(); return "处理请求时出错: " + e.getMessage(); } } // WebSocket监听器,处理连接和消息 class AiWebSocketListener extends WebSocketListener { private final CountDownLatch latch; private final String question; private final AtomicBoolean isResponseComplete = new AtomicBoolean(false); public AiWebSocketListener(CountDownLatch latch, String question) { this.latch = latch; this.question = question; } @Override public void onOpen(WebSocket webSocket, Response response) { totalAnswer.setLength(0); super.onOpen(webSocket, response); // 发送请求参数 sendRequest(webSocket, question); } @Override public void onMessage(WebSocket webSocket, String text) { try { // 解析响应 JsonParse myJsonParse = gson.fromJson(text, JsonParse.class); if (myJsonParse.header.code != 0) { System.err.println("发生错误,错误码为:" + myJsonParse.header.code); System.err.println("本次请求的sid为:" + myJsonParse.header.sid); webSocket.close(1000, "Error code: " + myJsonParse.header.code); latch.countDown(); return; } // 处理回答内容 List<Text> textList = myJsonParse.payload.choices.text; for (Text temp : textList) { totalAnswer.append(temp.content); } // 判断是否为完整响应(根据API规范调整) if (myJsonParse.header.status == 2) { // 添加到历史记录 addToHistory(question, totalAnswer.toString()); // 标记响应完成 isResponseComplete.set(true); latch.countDown(); } } catch (Exception e) { e.printStackTrace(); latch.countDown(); } } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { super.onFailure(webSocket, t, response); try { System.err.println("WebSocket连接失败"); if (response != null) { System.err.println("错误码: " + response.code()); if (response.body() != null) { System.err.println("错误信息: " + response.body().string()); } } t.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { latch.countDown(); } } @Override public void onClosed(WebSocket webSocket, int code, String reason) { super.onClosed(webSocket, code, reason); System.out.println("WebSocket连接已关闭,代码: " + code + ", 原因: " + reason); if (!isResponseComplete.get()) { latch.countDown(); } } // 发送请求参数 private void sendRequest(WebSocket webSocket, String question) { try { JSONObject requestJson = new JSONObject(); // 构造请求头 JSONObject header = new JSONObject(); header.put("app_id", appid); header.put("uid", UUID.randomUUID().toString().substring(0, 10)); // 构造请求参数 JSONObject parameter = new JSONObject(); JSONObject chat = new JSONObject(); chat.put("domain", domain); chat.put("temperature", 0.5); chat.put("max_tokens", 4096); parameter.put("chat", chat); // 构造payload JSONObject payload = new JSONObject(); JSONObject message = new JSONObject(); JSONArray text = new JSONArray(); // 添加系统提示 RoleContent roleSystem = new RoleContent(); roleSystem.setRole("system"); roleSystem.setContent("你是一个智能客服,接下来你要用客服的语气和我对话"); text.add(JSON.toJSON(roleSystem)); // 添加历史记录 for (RoleContent history : historyList) { text.add(JSON.toJSON(history)); } // 添加当前问题 RoleContent userQuestion = new RoleContent(); userQuestion.setRole("user"); userQuestion.setContent(question); text.add(JSON.toJSON(userQuestion)); message.put("text", text); payload.put("message", message); requestJson.put("header", header); requestJson.put("parameter", parameter); requestJson.put("payload", payload); webSocket.send(requestJson.toString()); } catch (Exception e) { e.printStackTrace(); latch.countDown(); } } } // 添加到对话历史 private void addToHistory(String question, String answer) { // 限制历史记录长度(示例:最多保留10条) if (historyList.size() >= 10) { historyList.remove(0); } // 添加用户问题 RoleContent userContent = new RoleContent(); userContent.setRole("user"); userContent.setContent(question); historyList.add(userContent); // 添加AI回答 RoleContent aiContent = new RoleContent(); aiContent.setRole("assistant"); aiContent.setContent(answer); historyList.add(aiContent); } // 鉴权方法 private String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception { URL url = new URL(hostUrl); // 时间 SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); format.setTimeZone(TimeZone.getTimeZone("GMT")); String date = format.format(new Date()); // 拼接 String preStr = "host: " + url.getHost() + "\n" + "date: " + date + "\n" + "GET " + url.getPath() + " HTTP/1.1"; // HMAC-SHA256加密 Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(spec); byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); // Base64加密 String sha = Base64.getEncoder().encodeToString(hexDigits); // 拼接Authorization String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha); // 拼接URL HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder() .addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))) .addQueryParameter("date", date) .addQueryParameter("host", url.getHost()) .build(); return httpUrl.toString(); } // 返回的JSON结果拆解类 class JsonParse { Header header; Payload payload; } class Header { int code; int status; String sid; } class Payload { Choices choices; } class Choices { List<Text> text; } class Text { String role; String content; } class RoleContent { String role; String content; public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } 

至此功能开发完成:

测试

需要源代码的可以评论区留邮箱 我会发代码到邮箱

工作了 好久不看博客了 这个技术栈有点落后了 建议大家不要学了 这个是我当时学java的时候搞的

开源到gitee了 需要自取https://gitee.com/JavaScript_Python_3b77/sky-delivery-change.git

Read more

如何降低AIGC总体疑似度?7个实用技巧+专业工具真实案例分享

如何降低AIGC总体疑似度?7个实用技巧+专业工具真实案例分享

为什么你的论文总是被标为AIGC疑似? 近年来,随着AI写作工具的普及,一个让无数研究者头疼的问题出现了——AIGC总体疑似度过高。根据各大高校的最新规定,如果论文的AIGC率超过30%,很可能被判定为AI代写,直接取消答辩资格! 根据高校规定,AIGC率超过30%可能被判定为学术不端,面临取消答辩资格的风险。 许多同学反映:"我只是用AI辅助写作,怎么就被判定为学术不端了?" 这背后的原因是AI生成内容具有特定的规律性特征,如固定句式、高频词汇组合等,这些"数字指纹"很容易被检测系统识别。 7个实用降重技巧,亲测有效! 1. 变换表达,重构句式 避免使用AI常见的短句结构,如"首先,"、"综上,"等。将这些碎片化表达整合成完整句子。 示例对比: * 改前:综上所述,研究者们普遍认为企业偿债能力是一个多维度的概念。 * 改后:总之研究人员普遍认同企业偿债能力这一多维度概念。 2. 引入具体数据和案例 通过添加真实的研究数据、

2026新手小白AI创业变现指南(二)- AI写作辅助平台

2026新手小白AI创业变现指南(二)- AI写作辅助平台

刚刚更新了2026新手小白AI创业变现指南l列表,新增加了测试过的炼字工坊、蛙蛙写作、笔杆平台(学术论文平台,非通用写作平台)。想简单介绍下,详情请点击2026新手小白AI创业变现指南(一)中平台列表中平台名称看详细介绍。 一、炼字工坊 平台基础信息 项目内容平台名称炼字工坊官方网址https://lianzigongfang.com平台介绍专为网文/剧本/漫剧作者设计的AI创作平台,帮你把精力花在“故事和表达”上,把重复、耗时、卡壳的部分交给AI。相比通用AI,炼字工坊在长篇稳定性上有明显优势。它用「问答+抽卡」帮你定题材卖点,用「设定库」自动归档世界观和角色,用「分层大纲」把控剧情节奏,用「续写润色」解决卡文问题。最重要的是:你的作品不会用于AI训练,版权完全归你。核心定位长篇创作的全流程辅助,从灵感、设定到续写、润色,让你专注创作本身。 🎯 它和通用AI(如DeepSeek、千问)

【论文阅读】ColorFlow: Retrieval-Augmented Image Sequence Colorization

【论文阅读】ColorFlow: Retrieval-Augmented Image Sequence Colorization

基于检索增强的漫画/图片序列上色任务。 intro 现有问题:不是把一张黑白图随便上色,而是要在同一角色跨多张分镜/多帧时,尽量保持发色、衣服配色等“身份颜色(ID color)”一致,而且还要让操作流程“像工具”一样好用:不需要为每个角色单独 finetune,也不强行抽取显式的 ID embedding。论文把整个方案拆成三个阶段:RAP(检索增强)、ICP(in-context 扩散上色)、GSRP(引导式超分复原)。 任务设定:Reference-based Image Sequence Colorization * 输入:一张待上色的黑白图(来自漫画/分镜序列中的某一帧)+ 一个“参考图池”(同章节或同序列里若干张已经有颜色的图)。 * 输出:一张彩色结果,要求在序列层面尽量保持角色/物体的颜色身份一致(例如同一角色的头发颜色在多帧一致)。 * 关键难点:参考池里信息多、分镜构图变化大、同角色会变形/

OpenClaw之Memory配置成本地模式,Ubuntu+CUDA+cuDNN+llama.cpp

文章目录 * 背景:Memory不生效的问题 * OpenClaw的Memory配置 * Ubuntu24.04安装CUDA和cuDNN * 编译llama.cpp * 验证方案1: * 验证方案2:下载并运行Llama-2 7B模型 * 安装node-llama-cpp * 验证Memory * sqlite-vec unavailable * 踩过的坑 * 安装node-llama-cpp的一些提示 * 安装node-llama-cpp的前置条件 * Using `node-llama-cpp` With Vulkan 承接上文:Windows11基于WSL2首次运行Openclaw,并对接飞书应用,我已经在电脑上安装了OpenClaw,接下来解决Memory问题。走了很多弯路,下面主要讲我总结的正确的安装过程。 总结来说:针对Memory不生效的问题,又不想用OpenAI或Gemini,或者只想单纯的节省token,可以按照如下的方式,设置为local模式: * 修改openclaw.json配置 * 安装CUDA和cu