| New file |
| | |
| | | package com.zy.ai.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | | |
| | | @Data |
| | | @TableName("sys_ai_chat_message") |
| | | public class AiChatMessage implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @TableId(value = "id", type = IdType.AUTO) |
| | | private Long id; |
| | | |
| | | @TableField("chat_id") |
| | | private String chatId; |
| | | |
| | | @TableField("seq_no") |
| | | private Integer seqNo; |
| | | |
| | | private String role; |
| | | |
| | | private String content; |
| | | |
| | | @TableField("create_time") |
| | | private Date createTime; |
| | | } |
| New file |
| | |
| | | package com.zy.ai.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | | |
| | | @Data |
| | | @TableName("sys_ai_chat_session") |
| | | public class AiChatSession implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @TableId(value = "id", type = IdType.AUTO) |
| | | private Long id; |
| | | |
| | | @TableField("chat_id") |
| | | private String chatId; |
| | | |
| | | private String title; |
| | | |
| | | @TableField("prompt_template_id") |
| | | private Long promptTemplateId; |
| | | |
| | | @TableField("prompt_scene_code") |
| | | private String promptSceneCode; |
| | | |
| | | @TableField("prompt_version") |
| | | private Integer promptVersion; |
| | | |
| | | @TableField("prompt_name") |
| | | private String promptName; |
| | | |
| | | @TableField("message_count") |
| | | private Integer messageCount; |
| | | |
| | | @TableField("last_prompt_tokens") |
| | | private Long lastPromptTokens; |
| | | |
| | | @TableField("last_completion_tokens") |
| | | private Long lastCompletionTokens; |
| | | |
| | | @TableField("last_total_tokens") |
| | | private Long lastTotalTokens; |
| | | |
| | | @TableField("last_llm_call_count") |
| | | private Integer lastLlmCallCount; |
| | | |
| | | @TableField("last_token_updated_at") |
| | | private Date lastTokenUpdatedAt; |
| | | |
| | | @TableField("sum_prompt_tokens") |
| | | private Long sumPromptTokens; |
| | | |
| | | @TableField("sum_completion_tokens") |
| | | private Long sumCompletionTokens; |
| | | |
| | | @TableField("sum_total_tokens") |
| | | private Long sumTotalTokens; |
| | | |
| | | @TableField("ask_count") |
| | | private Long askCount; |
| | | |
| | | @TableField("create_time") |
| | | private Date createTime; |
| | | |
| | | @TableField("update_time") |
| | | private Date updateTime; |
| | | } |
| New file |
| | |
| | | package com.zy.ai.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.zy.ai.entity.AiChatMessage; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | @Mapper |
| | | @Repository |
| | | public interface AiChatMessageMapper extends BaseMapper<AiChatMessage> { |
| | | } |
| New file |
| | |
| | | package com.zy.ai.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.zy.ai.entity.AiChatSession; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | @Mapper |
| | | @Repository |
| | | public interface AiChatSessionMapper extends BaseMapper<AiChatSession> { |
| | | } |
| New file |
| | |
| | | package com.zy.ai.service; |
| | | |
| | | import com.zy.ai.entity.AiPromptTemplate; |
| | | import com.zy.ai.entity.ChatCompletionRequest; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | public interface AiChatStoreService { |
| | | |
| | | List<Map<String, Object>> listChats(); |
| | | |
| | | boolean deleteChat(String chatId); |
| | | |
| | | List<ChatCompletionRequest.Message> getChatHistory(String chatId); |
| | | |
| | | void saveConversation(String chatId, |
| | | String title, |
| | | ChatCompletionRequest.Message userMessage, |
| | | ChatCompletionRequest.Message assistantMessage, |
| | | AiPromptTemplate promptTemplate, |
| | | long promptTokens, |
| | | long completionTokens, |
| | | long totalTokens, |
| | | int llmCallCount); |
| | | } |
| | |
| | | import java.util.concurrent.LinkedBlockingQueue; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | import java.util.function.Consumer; |
| | | |
| | | @Slf4j |
| | |
| | | List<ResolvedRoute> routes = resolveRoutes(); |
| | | if (routes.isEmpty()) { |
| | | log.error("调用 LLM 失败: 未配置可用 LLM 路由"); |
| | | recordCall(traceId, scene, false, 1, null, false, null, 0L, req, null, "none", |
| | | recordCall(traceId, scene, false, 1, null, false, null, 0L, req, null, null, "none", |
| | | new RuntimeException("未配置可用 LLM 路由"), "no_route"); |
| | | return null; |
| | | } |
| | |
| | | boolean canSwitch = shouldSwitch(route, false); |
| | | markFailure(route, ex, canSwitch); |
| | | recordCall(traceId, scene, false, i + 1, route, false, callResult.statusCode, |
| | | System.currentTimeMillis() - start, routeReq, callResult.payload, "error", ex, |
| | | System.currentTimeMillis() - start, routeReq, resp, callResult.payload, "error", ex, |
| | | "invalid_completion"); |
| | | if (hasNext && canSwitch) { |
| | | log.warn("LLM 切换到下一路由, current={}, reason={}", route.tag(), ex.getMessage()); |
| | |
| | | } |
| | | markSuccess(route); |
| | | recordCall(traceId, scene, false, i + 1, route, true, callResult.statusCode, |
| | | System.currentTimeMillis() - start, routeReq, buildResponseText(resp, callResult.payload), |
| | | System.currentTimeMillis() - start, routeReq, resp, buildResponseText(resp, callResult.payload), |
| | | "none", null, null); |
| | | return resp; |
| | | } catch (Throwable ex) { |
| | |
| | | boolean canSwitch = shouldSwitch(route, quota); |
| | | markFailure(route, ex, canSwitch); |
| | | recordCall(traceId, scene, false, i + 1, route, false, statusCodeOf(ex), |
| | | System.currentTimeMillis() - start, routeReq, responseBodyOf(ex), |
| | | System.currentTimeMillis() - start, routeReq, null, responseBodyOf(ex), |
| | | quota ? "quota" : "error", ex, null); |
| | | if (hasNext && canSwitch) { |
| | | log.warn("LLM 切换到下一路由, current={}, reason={}", route.tag(), errorText(ex)); |
| | |
| | | req.setMax_tokens(maxTokens != null ? maxTokens : 1024); |
| | | req.setStream(true); |
| | | |
| | | streamWithFailover(req, onChunk, onComplete, onError, "chat_stream"); |
| | | streamWithFailover(req, onChunk, onComplete, onError, null, "chat_stream"); |
| | | } |
| | | |
| | | public void chatStreamWithTools(List<ChatCompletionRequest.Message> messages, |
| | |
| | | List<Object> tools, |
| | | Consumer<String> onChunk, |
| | | Runnable onComplete, |
| | | Consumer<Throwable> onError) { |
| | | Consumer<Throwable> onError, |
| | | Consumer<ChatCompletionResponse.Usage> onUsage) { |
| | | ChatCompletionRequest req = new ChatCompletionRequest(); |
| | | req.setMessages(messages); |
| | | req.setTemperature(temperature != null ? temperature : 0.3); |
| | |
| | | req.setTools(tools); |
| | | req.setTool_choice("auto"); |
| | | } |
| | | streamWithFailover(req, onChunk, onComplete, onError, tools != null && !tools.isEmpty() ? "chat_stream_tools" : "chat_stream"); |
| | | streamWithFailover(req, onChunk, onComplete, onError, onUsage, tools != null && !tools.isEmpty() ? "chat_stream_tools" : "chat_stream"); |
| | | } |
| | | |
| | | private void streamWithFailover(ChatCompletionRequest req, |
| | | Consumer<String> onChunk, |
| | | Runnable onComplete, |
| | | Consumer<Throwable> onError, |
| | | Consumer<ChatCompletionResponse.Usage> onUsage, |
| | | String scene) { |
| | | String traceId = nextTraceId(); |
| | | List<ResolvedRoute> routes = resolveRoutes(); |
| | | if (routes.isEmpty()) { |
| | | recordCall(traceId, scene, true, 1, null, false, null, 0L, req, null, "none", |
| | | recordCall(traceId, scene, true, 1, null, false, null, 0L, req, null, null, "none", |
| | | new RuntimeException("未配置可用 LLM 路由"), "no_route"); |
| | | if (onError != null) onError.accept(new RuntimeException("未配置可用 LLM 路由")); |
| | | return; |
| | | } |
| | | attemptStream(routes, 0, req, onChunk, onComplete, onError, traceId, scene); |
| | | attemptStream(routes, 0, req, onChunk, onComplete, onError, onUsage, traceId, scene); |
| | | } |
| | | |
| | | private void attemptStream(List<ResolvedRoute> routes, |
| | |
| | | Consumer<String> onChunk, |
| | | Runnable onComplete, |
| | | Consumer<Throwable> onError, |
| | | Consumer<ChatCompletionResponse.Usage> onUsage, |
| | | String traceId, |
| | | String scene) { |
| | | if (index >= routes.size()) { |
| | |
| | | AtomicBoolean doneSeen = new AtomicBoolean(false); |
| | | AtomicBoolean errorSeen = new AtomicBoolean(false); |
| | | AtomicBoolean emitted = new AtomicBoolean(false); |
| | | AtomicReference<ChatCompletionResponse.Usage> usageRef = new AtomicReference<>(); |
| | | LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); |
| | | |
| | | Thread drain = new Thread(() -> { |
| | |
| | | drain.setDaemon(true); |
| | | drain.start(); |
| | | |
| | | Flux<String> streamSource = streamFluxWithSpringAi(route, routeReq); |
| | | Flux<String> streamSource = streamFluxWithSpringAi(route, routeReq, usageRef::set); |
| | | streamSource.subscribe(payload -> { |
| | | if (payload == null || payload.isEmpty()) return; |
| | | queue.offer(payload); |
| | |
| | | boolean canSwitch = shouldSwitch(route, quota); |
| | | markFailure(route, err, canSwitch); |
| | | recordCall(traceId, scene, true, index + 1, route, false, statusCodeOf(err), |
| | | System.currentTimeMillis() - start, routeReq, outputBuffer.toString(), |
| | | System.currentTimeMillis() - start, routeReq, usageResponse(usageRef.get()), outputBuffer.toString(), |
| | | quota ? "quota" : "error", err, "emitted=" + emitted.get()); |
| | | if (!emitted.get() && canSwitch && index < routes.size() - 1) { |
| | | log.warn("LLM 路由失败,自动切换,current={}, reason={}", route.tag(), errorText(err)); |
| | | attemptStream(routes, index + 1, req, onChunk, onComplete, onError, traceId, scene); |
| | | attemptStream(routes, index + 1, req, onChunk, onComplete, onError, onUsage, traceId, scene); |
| | | return; |
| | | } |
| | | if (onError != null) onError.accept(err); |
| | | }, () -> { |
| | | markSuccess(route); |
| | | if (onUsage != null && usageRef.get() != null) { |
| | | try { |
| | | onUsage.accept(usageRef.get()); |
| | | } catch (Exception ignore) { |
| | | } |
| | | } |
| | | recordCall(traceId, scene, true, index + 1, route, true, 200, |
| | | System.currentTimeMillis() - start, routeReq, outputBuffer.toString(), |
| | | System.currentTimeMillis() - start, routeReq, usageResponse(usageRef.get()), outputBuffer.toString(), |
| | | "none", null, null); |
| | | doneSeen.set(true); |
| | | }); |
| | | } |
| | | |
| | | private Flux<String> streamFluxWithSpringAi(ResolvedRoute route, ChatCompletionRequest req) { |
| | | return llmSpringAiClientService.streamCompletion(route.baseUrl, route.apiKey, req) |
| | | private Flux<String> streamFluxWithSpringAi(ResolvedRoute route, |
| | | ChatCompletionRequest req, |
| | | Consumer<ChatCompletionResponse.Usage> usageConsumer) { |
| | | return llmSpringAiClientService.streamCompletion(route.baseUrl, route.apiKey, req, usageConsumer) |
| | | .doOnError(ex -> log.error("调用 Spring AI 流式失败, route={}", route.tag(), ex)); |
| | | } |
| | | |
| | |
| | | Integer httpStatus, |
| | | long latencyMs, |
| | | ChatCompletionRequest req, |
| | | ChatCompletionResponse responseObj, |
| | | String response, |
| | | String switchMode, |
| | | Throwable err, |
| | |
| | | item.setResponseContent(cut(response, LOG_TEXT_LIMIT)); |
| | | item.setErrorType(cut(safeName(err), 128)); |
| | | item.setErrorMessage(err == null ? null : cut(errorText(err), 1024)); |
| | | item.setExtra(cut(extra, 512)); |
| | | item.setExtra(cut(buildExtraPayload(responseObj == null ? null : responseObj.getUsage(), extra), 512)); |
| | | item.setCreateTime(new Date()); |
| | | llmCallLogService.saveIgnoreError(item); |
| | | } |
| | | |
| | | private ChatCompletionResponse usageResponse(ChatCompletionResponse.Usage usage) { |
| | | if (usage == null) { |
| | | return null; |
| | | } |
| | | ChatCompletionResponse response = new ChatCompletionResponse(); |
| | | response.setUsage(usage); |
| | | return response; |
| | | } |
| | | |
| | | private String buildExtraPayload(ChatCompletionResponse.Usage usage, String extra) { |
| | | if (usage == null && isBlank(extra)) { |
| | | return null; |
| | | } |
| | | HashMap<String, Object> payload = new HashMap<>(); |
| | | if (usage != null) { |
| | | if (usage.getPromptTokens() != null) { |
| | | payload.put("promptTokens", usage.getPromptTokens()); |
| | | } |
| | | if (usage.getCompletionTokens() != null) { |
| | | payload.put("completionTokens", usage.getCompletionTokens()); |
| | | } |
| | | if (usage.getTotalTokens() != null) { |
| | | payload.put("totalTokens", usage.getTotalTokens()); |
| | | } |
| | | } |
| | | if (!isBlank(extra)) { |
| | | payload.put("note", extra); |
| | | } |
| | | return payload.isEmpty() ? null : JSON.toJSONString(payload); |
| | | } |
| | | |
| | | private static class CompletionCallResult { |
| | | private final int statusCode; |
| | | private final String payload; |
| | |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | import java.util.function.Consumer; |
| | | |
| | | @Service |
| | | public class LlmSpringAiClientService { |
| | |
| | | legacy); |
| | | } |
| | | |
| | | public Flux<String> streamCompletion(String baseUrl, String apiKey, ChatCompletionRequest req) { |
| | | public Flux<String> streamCompletion(String baseUrl, String apiKey, ChatCompletionRequest req, Consumer<ChatCompletionResponse.Usage> usageConsumer) { |
| | | OpenAiApi api = buildOpenAiApi(baseUrl, apiKey); |
| | | OpenAiApi.ChatCompletionRequest springReq = buildSpringAiRequest(req, true); |
| | | return api.chatCompletionStream(springReq) |
| | | .flatMapIterable(chunk -> chunk == null || chunk.choices() == null |
| | | ? List.<OpenAiApi.ChatCompletionChunk.ChunkChoice>of() |
| | | : chunk.choices()) |
| | | .map(OpenAiApi.ChatCompletionChunk.ChunkChoice::delta) |
| | | .filter(delta -> delta != null) |
| | | .handle((delta, sink) -> { |
| | | String text = extractSpringAiContent(delta); |
| | | if (text != null && !text.isEmpty()) { |
| | | sink.next(text); |
| | | .handle((chunk, sink) -> { |
| | | if (chunk == null) { |
| | | return; |
| | | } |
| | | if (chunk.usage() != null && usageConsumer != null) { |
| | | ChatCompletionResponse.Usage usage = new ChatCompletionResponse.Usage(); |
| | | usage.setPromptTokens(chunk.usage().promptTokens()); |
| | | usage.setCompletionTokens(chunk.usage().completionTokens()); |
| | | usage.setTotalTokens(chunk.usage().totalTokens()); |
| | | usageConsumer.accept(usage); |
| | | } |
| | | List<OpenAiApi.ChatCompletionChunk.ChunkChoice> choices = chunk.choices(); |
| | | if (choices == null || choices.isEmpty()) { |
| | | return; |
| | | } |
| | | for (OpenAiApi.ChatCompletionChunk.ChunkChoice choice : choices) { |
| | | if (choice == null || choice.delta() == null) { |
| | | continue; |
| | | } |
| | | String text = extractSpringAiContent(choice.delta()); |
| | | if (text != null && !text.isEmpty()) { |
| | | sink.next(text); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | |
| | | import com.zy.ai.entity.WcsDiagnosisRequest; |
| | | import com.zy.ai.enums.AiPromptScene; |
| | | import com.zy.ai.mcp.service.SpringAiMcpToolManager; |
| | | import com.zy.ai.service.AiPromptTemplateService; |
| | | import com.zy.ai.utils.AiUtils; |
| | | import com.zy.common.utils.RedisUtil; |
| | | import com.zy.core.enums.RedisKeyType; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
| | | import lombok.RequiredArgsConstructor; |
| | |
| | | @Slf4j |
| | | public class WcsDiagnosisService { |
| | | |
| | | private static final long CHAT_TTL_SECONDS = 7L * 24 * 3600; |
| | | |
| | | @Autowired |
| | | private LlmChatService llmChatService; |
| | | @Autowired |
| | | private RedisUtil redisUtil; |
| | | @Autowired |
| | | private AiUtils aiUtils; |
| | | @Autowired |
| | | private SpringAiMcpToolManager mcpToolManager; |
| | | @Autowired |
| | | private AiPromptTemplateService aiPromptTemplateService; |
| | | @Autowired |
| | | private AiChatStoreService aiChatStoreService; |
| | | |
| | | public void diagnoseStream(WcsDiagnosisRequest request, SseEmitter emitter) { |
| | | List<ChatCompletionRequest.Message> messages = new ArrayList<>(); |
| | |
| | | SseEmitter emitter) { |
| | | List<ChatCompletionRequest.Message> messages = new ArrayList<>(); |
| | | |
| | | List<ChatCompletionRequest.Message> history = null; |
| | | String historyKey = null; |
| | | String metaKey = null; |
| | | if (chatId != null && !chatId.isEmpty()) { |
| | | historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; |
| | | metaKey = RedisKeyType.AI_CHAT_META.key + chatId; |
| | | if (reset) { |
| | | redisUtil.del(historyKey, metaKey); |
| | | aiChatStoreService.deleteChat(chatId); |
| | | } |
| | | List<Object> stored = redisUtil.lGet(historyKey, 0, -1); |
| | | if (stored != null && !stored.isEmpty()) { |
| | | history = new ArrayList<>(stored.size()); |
| | | for (Object o : stored) { |
| | | ChatCompletionRequest.Message m = convertToMessage(o); |
| | | if (m != null) history.add(m); |
| | | } |
| | | if (!history.isEmpty()) messages.addAll(history); |
| | | } else { |
| | | history = new ArrayList<>(); |
| | | List<ChatCompletionRequest.Message> history = aiChatStoreService.getChatHistory(chatId); |
| | | if (history != null && !history.isEmpty()) { |
| | | messages.addAll(history); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | ChatCompletionRequest.Message mcpUser = new ChatCompletionRequest.Message(); |
| | | mcpUser.setRole("user"); |
| | | mcpUser.setContent("【用户提问】\n" + (prompt == null ? "" : prompt)); |
| | | mcpUser.setContent(prompt == null ? "" : prompt); |
| | | |
| | | runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, promptTemplate, 0.3, 2048, emitter, finalChatId); |
| | | } |
| | | |
| | | public List<Map<String, Object>> listChats() { |
| | | java.util.Set<String> keys = redisUtil.scanKeys(RedisKeyType.AI_CHAT_META.key, 1000); |
| | | List<Map<String, Object>> resp = new ArrayList<>(); |
| | | if (keys != null) { |
| | | for (String key : keys) { |
| | | Map<Object, Object> m = redisUtil.hmget(key); |
| | | if (m != null && !m.isEmpty()) { |
| | | java.util.HashMap<String, Object> item = new java.util.HashMap<>(); |
| | | for (Map.Entry<Object, Object> e : m.entrySet()) { |
| | | item.put(String.valueOf(e.getKey()), e.getValue()); |
| | | } |
| | | String chatId = String.valueOf(item.get("chatId")); |
| | | String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; |
| | | item.put("size", redisUtil.lGetListSize(historyKey)); |
| | | resp.add(item); |
| | | } |
| | | } |
| | | } |
| | | return resp; |
| | | return aiChatStoreService.listChats(); |
| | | } |
| | | |
| | | public boolean deleteChat(String chatId) { |
| | | if (chatId == null || chatId.isEmpty()) return false; |
| | | String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; |
| | | String metaKey = RedisKeyType.AI_CHAT_META.key + chatId; |
| | | redisUtil.del(historyKey, metaKey); |
| | | return true; |
| | | return aiChatStoreService.deleteChat(chatId); |
| | | } |
| | | |
| | | public List<ChatCompletionRequest.Message> getChatHistory(String chatId) { |
| | | if (chatId == null || chatId.isEmpty()) return java.util.Collections.emptyList(); |
| | | String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; |
| | | List<Object> stored = redisUtil.lGet(historyKey, 0, -1); |
| | | List<ChatCompletionRequest.Message> result = new ArrayList<>(); |
| | | if (stored != null) { |
| | | for (Object o : stored) { |
| | | ChatCompletionRequest.Message m = convertToMessage(o); |
| | | if (m != null) result.add(m); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private ChatCompletionRequest.Message convertToMessage(Object o) { |
| | | if (o instanceof ChatCompletionRequest.Message) { |
| | | return (ChatCompletionRequest.Message) o; |
| | | } |
| | | if (o instanceof Map) { |
| | | Map<?, ?> map = (Map<?, ?>) o; |
| | | ChatCompletionRequest.Message m = new ChatCompletionRequest.Message(); |
| | | Object role = map.get("role"); |
| | | Object content = map.get("content"); |
| | | m.setRole(role == null ? null : String.valueOf(role)); |
| | | m.setContent(content == null ? null : String.valueOf(content)); |
| | | return m; |
| | | } |
| | | return null; |
| | | return aiChatStoreService.getChatHistory(chatId); |
| | | } |
| | | |
| | | private String buildTitleFromPrompt(String prompt) { |
| | |
| | | if (tools.isEmpty()) { |
| | | throw new IllegalStateException("No MCP tools registered"); |
| | | } |
| | | AgentUsageStats usageStats = new AgentUsageStats(); |
| | | |
| | | baseMessages.add(systemPrompt); |
| | | baseMessages.add(userQuestion); |
| | |
| | | if (resp == null || resp.getChoices() == null || resp.getChoices().isEmpty() || resp.getChoices().get(0).getMessage() == null) { |
| | | throw new IllegalStateException("LLM returned empty response"); |
| | | } |
| | | usageStats.add(resp.getUsage()); |
| | | |
| | | ChatCompletionRequest.Message assistant = resp.getChoices().get(0).getMessage(); |
| | | messages.add(assistant); |
| | |
| | | } catch (Exception ignore) {} |
| | | }, () -> { |
| | | try { |
| | | emitTokenUsage(emitter, usageStats); |
| | | sse(emitter, "\\n\\n【AI】运行已停止(正常结束)\\n\\n"); |
| | | log.info("AI MCP diagnose stopped: final end"); |
| | | emitter.complete(); |
| | | |
| | | if (chatId != null) { |
| | | String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; |
| | | String metaKey = RedisKeyType.AI_CHAT_META.key + chatId; |
| | | |
| | | ChatCompletionRequest.Message a = new ChatCompletionRequest.Message(); |
| | | a.setRole("assistant"); |
| | | a.setContent(assistantBuffer.toString()); |
| | | redisUtil.lSet(historyKey, userQuestion); |
| | | redisUtil.lSet(historyKey, a); |
| | | redisUtil.expire(historyKey, CHAT_TTL_SECONDS); |
| | | Map<Object, Object> old = redisUtil.hmget(metaKey); |
| | | Long createdAt = old != null && old.get("createdAt") != null ? |
| | | (old.get("createdAt") instanceof Number ? ((Number) old.get("createdAt")).longValue() : Long.valueOf(String.valueOf(old.get("createdAt")))) |
| | | : System.currentTimeMillis(); |
| | | Map<String, Object> meta = new java.util.HashMap<>(); |
| | | meta.put("chatId", chatId); |
| | | meta.put("title", buildTitleFromPrompt(userQuestion.getContent())); |
| | | if (promptTemplate != null) { |
| | | meta.put("promptTemplateId", promptTemplate.getId()); |
| | | meta.put("promptSceneCode", promptTemplate.getSceneCode()); |
| | | meta.put("promptVersion", promptTemplate.getVersion()); |
| | | meta.put("promptName", promptTemplate.getName()); |
| | | } |
| | | meta.put("createdAt", createdAt); |
| | | meta.put("updatedAt", System.currentTimeMillis()); |
| | | redisUtil.hmset(metaKey, meta, CHAT_TTL_SECONDS); |
| | | aiChatStoreService.saveConversation(chatId, |
| | | buildTitleFromPrompt(userQuestion.getContent()), |
| | | userQuestion, |
| | | a, |
| | | promptTemplate, |
| | | usageStats.getPromptTokens(), |
| | | usageStats.getCompletionTokens(), |
| | | usageStats.getTotalTokens(), |
| | | usageStats.getLlmCallCount()); |
| | | } |
| | | } catch (Exception ignore) {} |
| | | }, e -> { |
| | | try { |
| | | emitTokenUsage(emitter, usageStats); |
| | | sse(emitter, "\\n\\n【AI】分析出错,运行已停止(异常)\\n\\n"); |
| | | log.error("AI MCP diagnose stopped: stream error", e); |
| | | emitter.complete(); |
| | | } catch (Exception ignore) {} |
| | | }); |
| | | }, usageStats::add); |
| | | } catch (Exception e) { |
| | | try { |
| | | sse(emitter, "\\n\\n【AI】运行已停止(异常)\\n\\n"); |
| | |
| | | } catch (Exception e) { |
| | | log.warn("SSE send failed", e); |
| | | } |
| | | } |
| | | |
| | | private void emitTokenUsage(SseEmitter emitter, AgentUsageStats usageStats) { |
| | | if (emitter == null || usageStats == null || usageStats.getTotalTokens() <= 0) { |
| | | return; |
| | | } |
| | | try { |
| | | emitter.send(SseEmitter.event() |
| | | .name("token_usage") |
| | | .data(JSON.toJSONString(buildTokenUsagePayload(usageStats)))); |
| | | } catch (Exception e) { |
| | | log.warn("SSE token usage send failed", e); |
| | | } |
| | | } |
| | | |
| | | private Map<String, Object> buildTokenUsagePayload(AgentUsageStats usageStats) { |
| | | java.util.LinkedHashMap<String, Object> payload = new java.util.LinkedHashMap<>(); |
| | | payload.put("promptTokens", usageStats.getPromptTokens()); |
| | | payload.put("completionTokens", usageStats.getCompletionTokens()); |
| | | payload.put("totalTokens", usageStats.getTotalTokens()); |
| | | payload.put("llmCallCount", usageStats.getLlmCallCount()); |
| | | return payload; |
| | | } |
| | | |
| | | private void sendLargeText(SseEmitter emitter, String text) { |
| | |
| | | } |
| | | } |
| | | |
| | | private static class AgentUsageStats { |
| | | private long promptTokens; |
| | | private long completionTokens; |
| | | private long totalTokens; |
| | | private int llmCallCount; |
| | | |
| | | void add(ChatCompletionResponse.Usage usage) { |
| | | if (usage == null) { |
| | | return; |
| | | } |
| | | promptTokens += usage.getPromptTokens() == null ? 0L : usage.getPromptTokens(); |
| | | completionTokens += usage.getCompletionTokens() == null ? 0L : usage.getCompletionTokens(); |
| | | totalTokens += usage.getTotalTokens() == null ? 0L : usage.getTotalTokens(); |
| | | llmCallCount++; |
| | | } |
| | | |
| | | long getPromptTokens() { |
| | | return promptTokens; |
| | | } |
| | | |
| | | long getCompletionTokens() { |
| | | return completionTokens; |
| | | } |
| | | |
| | | long getTotalTokens() { |
| | | return totalTokens; |
| | | } |
| | | |
| | | int getLlmCallCount() { |
| | | return llmCallCount; |
| | | } |
| | | } |
| | | |
| | | private boolean isConclusionText(String content) { |
| | | if (content == null) return false; |
| | | String c = content; |
| New file |
| | |
| | | package com.zy.ai.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.zy.ai.entity.AiChatMessage; |
| | | import com.zy.ai.entity.AiChatSession; |
| | | import com.zy.ai.entity.AiPromptTemplate; |
| | | import com.zy.ai.entity.ChatCompletionRequest; |
| | | import com.zy.ai.mapper.AiChatMessageMapper; |
| | | import com.zy.ai.mapper.AiChatSessionMapper; |
| | | import com.zy.ai.service.AiChatStoreService; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Date; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class AiChatStoreServiceImpl implements AiChatStoreService { |
| | | |
| | | private final AiChatSessionMapper aiChatSessionMapper; |
| | | private final AiChatMessageMapper aiChatMessageMapper; |
| | | |
| | | @Override |
| | | public List<Map<String, Object>> listChats() { |
| | | List<AiChatSession> sessions = aiChatSessionMapper.selectList(new QueryWrapper<AiChatSession>() |
| | | .orderByDesc("update_time") |
| | | .orderByDesc("id")); |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (AiChatSession session : sessions) { |
| | | if (session == null) { |
| | | continue; |
| | | } |
| | | LinkedHashMap<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("chatId", session.getChatId()); |
| | | item.put("title", session.getTitle()); |
| | | item.put("size", session.getMessageCount()); |
| | | item.put("promptTemplateId", session.getPromptTemplateId()); |
| | | item.put("promptSceneCode", session.getPromptSceneCode()); |
| | | item.put("promptVersion", session.getPromptVersion()); |
| | | item.put("promptName", session.getPromptName()); |
| | | item.put("lastPromptTokens", session.getLastPromptTokens()); |
| | | item.put("lastCompletionTokens", session.getLastCompletionTokens()); |
| | | item.put("lastTotalTokens", session.getLastTotalTokens()); |
| | | item.put("lastLlmCallCount", session.getLastLlmCallCount()); |
| | | item.put("sumPromptTokens", session.getSumPromptTokens()); |
| | | item.put("sumCompletionTokens", session.getSumCompletionTokens()); |
| | | item.put("sumTotalTokens", session.getSumTotalTokens()); |
| | | item.put("askCount", session.getAskCount()); |
| | | item.put("createdAt", toEpochMilli(session.getCreateTime())); |
| | | item.put("updatedAt", toEpochMilli(session.getUpdateTime())); |
| | | item.put("lastTokenUpdatedAt", toEpochMilli(session.getLastTokenUpdatedAt())); |
| | | result.add(item); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean deleteChat(String chatId) { |
| | | if (isBlank(chatId)) { |
| | | return false; |
| | | } |
| | | aiChatMessageMapper.delete(new QueryWrapper<AiChatMessage>().eq("chat_id", chatId)); |
| | | aiChatSessionMapper.delete(new QueryWrapper<AiChatSession>().eq("chat_id", chatId)); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public List<ChatCompletionRequest.Message> getChatHistory(String chatId) { |
| | | if (isBlank(chatId)) { |
| | | return java.util.Collections.emptyList(); |
| | | } |
| | | List<AiChatMessage> rows = aiChatMessageMapper.selectList(new QueryWrapper<AiChatMessage>() |
| | | .eq("chat_id", chatId) |
| | | .orderByAsc("seq_no") |
| | | .orderByAsc("id")); |
| | | List<ChatCompletionRequest.Message> result = new ArrayList<>(rows.size()); |
| | | for (AiChatMessage row : rows) { |
| | | if (row == null) { |
| | | continue; |
| | | } |
| | | ChatCompletionRequest.Message message = new ChatCompletionRequest.Message(); |
| | | message.setRole(row.getRole()); |
| | | message.setContent(row.getContent()); |
| | | result.add(message); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void saveConversation(String chatId, |
| | | String title, |
| | | ChatCompletionRequest.Message userMessage, |
| | | ChatCompletionRequest.Message assistantMessage, |
| | | AiPromptTemplate promptTemplate, |
| | | long promptTokens, |
| | | long completionTokens, |
| | | long totalTokens, |
| | | int llmCallCount) { |
| | | if (isBlank(chatId) || userMessage == null || assistantMessage == null) { |
| | | return; |
| | | } |
| | | synchronized (("ai_chat_store_" + chatId).intern()) { |
| | | AiChatSession session = aiChatSessionMapper.selectOne(new QueryWrapper<AiChatSession>() |
| | | .eq("chat_id", chatId) |
| | | .last("limit 1")); |
| | | Date now = new Date(); |
| | | int nextSeq = 1; |
| | | if (session == null) { |
| | | session = new AiChatSession(); |
| | | session.setChatId(chatId); |
| | | session.setCreateTime(now); |
| | | session.setMessageCount(0); |
| | | session.setSumPromptTokens(0L); |
| | | session.setSumCompletionTokens(0L); |
| | | session.setSumTotalTokens(0L); |
| | | session.setAskCount(0L); |
| | | } else { |
| | | Integer maxSeq = maxSeqNo(chatId); |
| | | nextSeq = maxSeq == null ? 1 : (maxSeq + 1); |
| | | } |
| | | |
| | | session.setTitle(cut(title, 255)); |
| | | if (promptTemplate != null) { |
| | | session.setPromptTemplateId(promptTemplate.getId()); |
| | | session.setPromptSceneCode(cut(promptTemplate.getSceneCode(), 64)); |
| | | session.setPromptVersion(promptTemplate.getVersion()); |
| | | session.setPromptName(cut(promptTemplate.getName(), 255)); |
| | | } else { |
| | | session.setPromptTemplateId(null); |
| | | session.setPromptSceneCode(null); |
| | | session.setPromptVersion(null); |
| | | session.setPromptName(null); |
| | | } |
| | | session.setLastPromptTokens(promptTokens); |
| | | session.setLastCompletionTokens(completionTokens); |
| | | session.setLastTotalTokens(totalTokens); |
| | | session.setLastLlmCallCount(llmCallCount); |
| | | session.setLastTokenUpdatedAt(now); |
| | | session.setMessageCount((session.getMessageCount() == null ? 0 : session.getMessageCount()) + 2); |
| | | session.setSumPromptTokens((session.getSumPromptTokens() == null ? 0L : session.getSumPromptTokens()) + promptTokens); |
| | | session.setSumCompletionTokens((session.getSumCompletionTokens() == null ? 0L : session.getSumCompletionTokens()) + completionTokens); |
| | | session.setSumTotalTokens((session.getSumTotalTokens() == null ? 0L : session.getSumTotalTokens()) + totalTokens); |
| | | session.setAskCount((session.getAskCount() == null ? 0L : session.getAskCount()) + 1); |
| | | |
| | | if (session.getId() == null) { |
| | | aiChatSessionMapper.insert(session); |
| | | } else { |
| | | aiChatSessionMapper.updateById(session); |
| | | } |
| | | |
| | | insertMessage(chatId, nextSeq, userMessage, now); |
| | | insertMessage(chatId, nextSeq + 1, assistantMessage, now); |
| | | } |
| | | } |
| | | |
| | | private void insertMessage(String chatId, int seqNo, ChatCompletionRequest.Message source, Date now) { |
| | | AiChatMessage row = new AiChatMessage(); |
| | | row.setChatId(chatId); |
| | | row.setSeqNo(seqNo); |
| | | row.setRole(cut(source.getRole(), 32)); |
| | | row.setContent(source.getContent()); |
| | | row.setCreateTime(now); |
| | | aiChatMessageMapper.insert(row); |
| | | } |
| | | |
| | | private Integer maxSeqNo(String chatId) { |
| | | AiChatMessage last = aiChatMessageMapper.selectOne(new QueryWrapper<AiChatMessage>() |
| | | .eq("chat_id", chatId) |
| | | .orderByDesc("seq_no") |
| | | .orderByDesc("id") |
| | | .last("limit 1")); |
| | | return last == null ? null : last.getSeqNo(); |
| | | } |
| | | |
| | | private long toEpochMilli(Date date) { |
| | | return date == null ? 0L : date.getTime(); |
| | | } |
| | | |
| | | private boolean isBlank(String text) { |
| | | return text == null || text.trim().isEmpty(); |
| | | } |
| | | |
| | | private String cut(String text, int maxLen) { |
| | | if (text == null) { |
| | | return null; |
| | | } |
| | | return text.length() > maxLen ? text.substring(0, maxLen) : text; |
| | | } |
| | | } |
| | |
| | | CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"), |
| | | ASYNC_WMS_IN_TASK_REQUEST("async_wms_in_task_request_"), |
| | | ASYNC_WMS_IN_TASK_RESPONSE("async_wms_in_task_response_"), |
| | | AI_CHAT_HISTORY("ai_chat_history_"), |
| | | AI_CHAT_META("ai_chat_meta_"), |
| | | MAIN_PROCESS_PSEUDOCODE("main_process_pseudocode"), |
| | | PLANNER_SCHEDULE("planner_schedule_"), |
| | | ; |
| New file |
| | |
| | | CREATE TABLE IF NOT EXISTS `sys_ai_chat_session` ( |
| | | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', |
| | | `chat_id` VARCHAR(64) NOT NULL COMMENT '会话ID', |
| | | `title` VARCHAR(255) DEFAULT NULL COMMENT '会话标题', |
| | | `prompt_template_id` BIGINT DEFAULT NULL COMMENT 'Prompt模板ID', |
| | | `prompt_scene_code` VARCHAR(64) DEFAULT NULL COMMENT 'Prompt场景', |
| | | `prompt_version` INT DEFAULT NULL COMMENT 'Prompt版本', |
| | | `prompt_name` VARCHAR(255) DEFAULT NULL COMMENT 'Prompt名称', |
| | | `message_count` INT NOT NULL DEFAULT 0 COMMENT '消息数', |
| | | `last_prompt_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '最近一次输入tokens', |
| | | `last_completion_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '最近一次输出tokens', |
| | | `last_total_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '最近一次总tokens', |
| | | `last_llm_call_count` INT NOT NULL DEFAULT 0 COMMENT '最近一次模型调用轮次', |
| | | `last_token_updated_at` DATETIME DEFAULT NULL COMMENT '最近一次tokens更新时间', |
| | | `sum_prompt_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '累计输入tokens', |
| | | `sum_completion_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '累计输出tokens', |
| | | `sum_total_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '累计总tokens', |
| | | `ask_count` BIGINT NOT NULL DEFAULT 0 COMMENT '累计提问次数', |
| | | `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
| | | `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_sys_ai_chat_session_chat_id` (`chat_id`), |
| | | KEY `idx_sys_ai_chat_session_update_time` (`update_time`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI聊天会话表'; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `sys_ai_chat_message` ( |
| | | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', |
| | | `chat_id` VARCHAR(64) NOT NULL COMMENT '会话ID', |
| | | `seq_no` INT NOT NULL COMMENT '顺序号', |
| | | `role` VARCHAR(32) NOT NULL COMMENT '角色:user/assistant', |
| | | `content` LONGTEXT COMMENT '消息内容', |
| | | `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
| | | PRIMARY KEY (`id`), |
| | | KEY `idx_sys_ai_chat_message_chat_seq` (`chat_id`, `seq_no`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI聊天消息表'; |
| | |
| | | <footer class="composer-panel"> |
| | | <div class="composer-head"> |
| | | <div><strong>向 AI 助手提问</strong></div> |
| | | <div>{{ currentChatId ? '会话已绑定' : '临时会话' }}</div> |
| | | <div>{{ currentRunTokenSummary || currentChatTokenSummary || (currentChatId ? '会话已绑定' : '临时会话') }}</div> |
| | | </div> |
| | | <el-input |
| | | v-model="userInput" |
| | |
| | | lastRenderTs: 0, |
| | | renderIntervalMs: 120, |
| | | stepChars: 6, |
| | | runTokenUsage: null, |
| | | userInput: '', |
| | | autoScrollThreshold: 80, |
| | | chats: [], |
| | |
| | | var current = this.findChat(this.currentChatId); |
| | | if (!current && this.resetting) return '新建会话,等待首条消息'; |
| | | if (!current) return '会话 ' + this.currentChatId; |
| | | return this.chatLabel(current); |
| | | var tokenText = this.tokenSummaryText(current); |
| | | return tokenText ? (this.chatLabel(current) + ' · ' + tokenText) : this.chatLabel(current); |
| | | }, |
| | | currentChatTokenSummary: function() { |
| | | var current = this.findChat(this.currentChatId); |
| | | return current ? this.tokenSummaryText(current) : ''; |
| | | }, |
| | | currentRunTokenSummary: function() { |
| | | return this.runTokenUsage ? this.tokenSummaryText(this.runTokenUsage, '本次') : ''; |
| | | }, |
| | | inlinePrompts: function() { |
| | | return this.promptPresets.slice(1); |
| | |
| | | }, |
| | | chatOptionLabel: function(chat) { |
| | | if (!chat) return '未命名会话'; |
| | | return this.chatLabel(chat) + ' · ' + (chat.size || 0) + ' 条 · ' + this.chatUpdatedAt(chat); |
| | | var suffix = this.tokenSummaryText(chat); |
| | | return this.chatLabel(chat) + ' · ' + (chat.size || 0) + ' 条 · ' + this.chatUpdatedAt(chat) + (suffix ? (' · ' + suffix) : ''); |
| | | }, |
| | | numericValue: function(value) { |
| | | if (value === null || value === undefined || value === '') return 0; |
| | | var num = Number(value); |
| | | return isNaN(num) ? 0 : num; |
| | | }, |
| | | tokenSummaryText: function(source, prefix) { |
| | | if (!source) return ''; |
| | | var total = this.numericValue(source.totalTokens != null ? source.totalTokens : source.lastTotalTokens); |
| | | if (!total) return ''; |
| | | var prompt = this.numericValue(source.promptTokens != null ? source.promptTokens : source.lastPromptTokens); |
| | | var completion = this.numericValue(source.completionTokens != null ? source.completionTokens : source.lastCompletionTokens); |
| | | var label = prefix || '上次'; |
| | | return label + ' tokens ' + total + '(输' + prompt + ' / 出' + completion + ')'; |
| | | }, |
| | | chatUpdatedAt: function(chat) { |
| | | if (!chat || !chat.updatedAt) return '刚刚创建'; |
| | |
| | | openChat: function(chatId) { |
| | | if (!chatId || this.streaming) return; |
| | | this.currentChatId = chatId; |
| | | this.runTokenUsage = null; |
| | | this.switchChat(); |
| | | }, |
| | | switchChat: function() { |
| | |
| | | if (this.streaming) return; |
| | | this.currentChatId = Date.now() + '_' + Math.random().toString(36).substr(2, 8); |
| | | this.resetting = true; |
| | | this.runTokenUsage = null; |
| | | this.clear(); |
| | | }, |
| | | deleteChat: function() { |
| | |
| | | if (!message) return; |
| | | this.loading = true; |
| | | this.streaming = true; |
| | | this.runTokenUsage = null; |
| | | this.messages.push({ role: 'user', text: message, ts: this.nowStr() }); |
| | | this.appendAssistantPlaceholder(); |
| | | this.scrollToBottom(true); |
| | |
| | | this.source.onopen = function() { |
| | | self.loading = false; |
| | | }; |
| | | this.source.addEventListener('token_usage', function(e) { |
| | | if (!e || !e.data) return; |
| | | try { |
| | | self.runTokenUsage = JSON.parse(e.data); |
| | | } catch (ignore) {} |
| | | }); |
| | | this.source.onmessage = function(e) { |
| | | if (!e || !e.data) return; |
| | | var chunk = (e.data || '').replace(/\\n/g, '\n'); |
| | |
| | | this.clear(); |
| | | this.loading = true; |
| | | this.streaming = true; |
| | | this.runTokenUsage = null; |
| | | this.appendAssistantPlaceholder(); |
| | | this.scrollToBottom(true); |
| | | |
| | |
| | | this.source.onopen = function() { |
| | | self.loading = false; |
| | | }; |
| | | this.source.addEventListener('token_usage', function(e) { |
| | | if (!e || !e.data) return; |
| | | try { |
| | | self.runTokenUsage = JSON.parse(e.data); |
| | | } catch (ignore) {} |
| | | }); |
| | | this.source.onmessage = function(e) { |
| | | if (!e || !e.data) return; |
| | | var chunk = (e.data || '').replace(/\\n/g, '\n'); |
| | |
| | | </el-table-column> |
| | | <el-table-column prop="httpStatus" label="状态码" width="90"></el-table-column> |
| | | <el-table-column prop="latencyMs" label="耗时(ms)" width="95"></el-table-column> |
| | | <el-table-column label="Tokens" width="140"> |
| | | <template slot-scope="scope"> |
| | | <div>{{ logTotalTokens(scope.row) }}</div> |
| | | <div style="color:#909399;font-size:12px;">输{{ logPromptTokens(scope.row) }} / 出{{ logCompletionTokens(scope.row) }}</div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="traceId" label="TraceId" width="230"></el-table-column> |
| | | <el-table-column label="错误" min-width="220"> |
| | | <template slot-scope="scope"> |
| | |
| | | + '模型: ' + (row.model || '-') + '\n' |
| | | + '状态码: ' + (row.httpStatus != null ? row.httpStatus : '-') + '\n' |
| | | + '耗时: ' + (row.latencyMs != null ? row.latencyMs : '-') + ' ms\n' |
| | | + 'Tokens: ' + this.logTotalTokens(row) + '(输' + this.logPromptTokens(row) + ' / 出' + this.logCompletionTokens(row) + ')\n' |
| | | + '结果: ' + (row.success === 1 ? '成功' : '失败') + '\n' |
| | | + '错误: ' + (row.errorMessage || '-') + '\n\n' |
| | | + '请求:\n' + (row.requestContent || '-') + '\n\n' |
| | |
| | | this.logDetailText = text; |
| | | this.logDetailVisible = true; |
| | | }, |
| | | parseLogExtra: function(row) { |
| | | if (!row || !row.extra) return {}; |
| | | if (typeof row.extra === 'object') return row.extra; |
| | | try { |
| | | return JSON.parse(row.extra); |
| | | } catch (e) { |
| | | return {}; |
| | | } |
| | | }, |
| | | logPromptTokens: function(row) { |
| | | var extra = this.parseLogExtra(row); |
| | | return extra && extra.promptTokens != null ? extra.promptTokens : '-'; |
| | | }, |
| | | logCompletionTokens: function(row) { |
| | | var extra = this.parseLogExtra(row); |
| | | return extra && extra.completionTokens != null ? extra.completionTokens : '-'; |
| | | }, |
| | | logTotalTokens: function(row) { |
| | | var extra = this.parseLogExtra(row); |
| | | return extra && extra.totalTokens != null ? extra.totalTokens : '-'; |
| | | }, |
| | | deleteLog: function(row) { |
| | | var self = this; |
| | | if (!row || !row.id) return; |