Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1a56ca8
upgrade erupt-web
erupts Jan 4, 2026
7032a10
add erupt-print module
erupts Jan 5, 2026
9b5ba8a
Merge remote-tracking branch 'origin/develop' into develop
erupts Jan 5, 2026
46d8393
add erupt-print module
erupts Jan 5, 2026
0034b05
remove sample h2 pwd
erupts Jan 5, 2026
c0571a5
Init print
erupts Jan 5, 2026
2923aa4
Fix job init
erupts Jan 6, 2026
f86d667
@ChoiceType.trigger repeat to onchange
erupts Jan 6, 2026
872ac82
大模型名称调整 QWen 调整为 Qwen 更新后需要在大模型管理中手动修改
erupts Jan 6, 2026
bef3b45
后续版本会删除 ChoiceTrigger 请使用 onchange 代替
erupts Jan 6, 2026
26c6574
optimize erupt-print
erupts Jan 6, 2026
a871525
upgrade erupt-web
erupts Jan 6, 2026
2b19b37
upgrade erupt-web
erupts Jan 6, 2026
67f3255
erupt-ai 支持渲染数学公式与图
erupts Jan 6, 2026
ad4d7a5
优化 LocalDateTime 类型的 Gson 适配,支持读写 LocalDate 类型的数据
erupts Jan 7, 2026
1658ded
Merge remote-tracking branch 'origin/develop' into develop
erupts Jan 7, 2026
749509e
Choice 组件支持配置下拉框选项的颜色
erupts Jan 7, 2026
b074ee2
Fix @Dynamic validate bug
erupts Jan 8, 2026
1bdc716
add print api
erupts Jan 12, 2026
f7dcb31
add print api
erupts Jan 12, 2026
a0d8eb5
add print var support
erupts Jan 13, 2026
906dbbb
add print var support
erupts Jan 13, 2026
fb03a1d
update config
erupts Jan 14, 2026
3dd42e1
remove EruptPrintBind, enhance EruptPrintTpl, and add API for templat…
erupts Jan 15, 2026
9935895
upgrade to 1.13.3
erupts Jan 15, 2026
d31e382
Replace JavaScript chunks with updated versions
erupts Jan 15, 2026
1c1dcb4
update application.yml: add datasource credentials and system-prompt …
erupts Jan 16, 2026
c4ee3b7
update application.yml: add datasource credentials and system-prompt …
erupts Jan 16, 2026
56ede45
Optimize notice web
erupts Jan 17, 2026
1efe5cc
Add `sense` filter support to `/messages` API in `EruptNoticeController`
erupts Jan 19, 2026
cbffa77
Add `selectByPath` method to `EruptLambdaQuery` and `/scenes` API to …
erupts Jan 19, 2026
8578a50
Update notice scene naming and parameter handling
erupts Jan 19, 2026
68fb3cb
Add `NoticeUrlOpenWay` enum and `urlOpenWay` field to `NoticeMessage`
erupts Jan 19, 2026
ebb8dde
Update erupt-web
erupts Jan 19, 2026
3cf59f4
Update template path handling and support ROUTER mode
erupts Jan 20, 2026
f853c09
Optimize code
erupts Jan 21, 2026
dea3b1e
Refactor and enhance Erupt Print and Cube modules
erupts Jan 21, 2026
8f37d14
Add `enableFunctionCall` property and conditional checks in `LLMService`
erupts Jan 22, 2026
3450b32
Refactor Erupt Print to add variable replacement logic and integrate …
erupts Jan 23, 2026
06b6382
Fix @VL color mission bug
erupts Jan 24, 2026
5962dfa
Fix LLM message miss bug
erupts Jan 24, 2026
87da3ca
Optimize erupt-ai
erupts Jan 24, 2026
5d075d5
Refactor Erupt Print module to replace `repeatVar` logic with Velocit…
erupts Jan 25, 2026
9b04102
update erupt-web
erupts Jan 25, 2026
59654e1
init print menu
erupts Jan 26, 2026
cb984c5
Optimize flow-print
erupts Jan 27, 2026
74872cf
Optimize flow-print
erupts Jan 27, 2026
660fc5b
Optimize Cube Operator
erupts Jan 30, 2026
c48fa3c
Add erupt print config model
erupts Feb 1, 2026
a5173f7
Merge remote-tracking branch 'origin/develop' into develop
erupts Feb 1, 2026
973503a
update erupt-web
erupts Feb 2, 2026
c1a5c0c
Optimize erupt cube
erupts Feb 3, 2026
9152883
add maven-release.sh
erupts Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deploy/erupt-cloud-server-docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# 默认使用阿里云镜像
FROM openjdk:11-jre-slim
FROM eclipse-temurin:17-jre

# 设置工作目录
WORKDIR /app
Expand Down
2 changes: 1 addition & 1 deletion deploy/erupt-cloud-server-docker/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>xyz.erupt</groupId>
<artifactId>erupt</artifactId>
<version>1.13.2</version>
<version>1.13.3</version>
<relativePath>../../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion erupt-admin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<parent>
<groupId>xyz.erupt</groupId>
<artifactId>erupt</artifactId>
<version>1.13.2</version>
<version>1.13.3</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
1 change: 1 addition & 0 deletions erupt-ai-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"markdown-it-footnote": "^4.0.0",
"markdown-it-highlightjs": "^4.2.0",
"markdown-it-ins": "^4.0.0",
"markdown-it-katex": "^2.0.3",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-mark": "^4.0.0",
"markdown-it-mermaid": "^0.2.5",
Expand Down
1 change: 0 additions & 1 deletion erupt-ai-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ const send = (message: string) => {
eventSource.onmessage = (event) => {
sending.value = false;
sendDisabled.value = true;
console.log(JSON.parse(event.data).text)
accumulatedMarkdown.value += JSON.parse(event.data).text;
let msg = messages.value[messages.value.length - 1];
if (msg.loading) {
Expand Down
5 changes: 5 additions & 0 deletions erupt-ai-web/src/app.less
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,9 @@
background: #ddd;
margin: @base-margin * 2 0;
}

/* 修复 KaTeX 上下标显示问题 */
.katex .vlist {
vertical-align: baseline !important;
}
}
81 changes: 55 additions & 26 deletions erupt-ai-web/src/components/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,37 @@ import abbr from 'markdown-it-abbr';
import deflist from 'markdown-it-deflist';
import linkAttributes from 'markdown-it-link-attributes';
import taskLists from 'markdown-it-task-lists';
import katex from 'katex';
import markdownItKatex from 'markdown-it-katex';
import 'katex/dist/katex.min.css';
import mermaid from 'mermaid';

// 初始化 mermaid
// if (typeof window !== 'undefined') {
// mermaid.initialize({
// startOnLoad: true,
// theme: 'default',
// securityLevel: 'loose'
// });
// }

// 自定义数学公式渲染
// @ts-ignore
const renderMath = (md) => {
const defaultRender = md.renderer.rules.text;
md.renderer.rules.text = (tokens: {
[x: string]: any;
}, idx: string | number, options: any, env: any, self: any) => {
const token = tokens[idx];
const match = token.content.match(/(\$\$?)([\s\S]*?)\1/g);
if (match) {
match.forEach((m: string) => {
const isBlock = m.startsWith('$$');
const tex = m.slice(2, -2);
try {
const html = katex.renderToString(tex, {displayMode: isBlock});
token.content = token.content.replace(m, html);
} catch (e: any) {
token.content = token.content.replace(m, `<span class="error">Error rendering math: ${e.message}</span>`);
}
});
}
return defaultRender(tokens, idx, options, env, self);
};
};
function preprocessLatex(text: string): string {
// 1. 先将 ```latex 代码块的内容提取出来(保留原始内容)
text = text.replace(/```latex\s*([\s\S]*?)```/g, (_match, content) => {
return '\n' + content.trim() + '\n';
});

// 2. 统一将 \[ ... \] 转换为 $$...$$(LaTeX 块级公式)
text = text.replace(/\\\[([\s\S]*?)\\\]/g, (_match, formula) => {
return '$$' + formula.trim() + '$$';
});

// 3. 将 \( ... \) 转换为 $...$(LaTeX 行内公式)
text = text.replace(/\\\(([\s\S]*?)\\\)/g, (_match, formula) => {
return '$' + formula.trim() + '$';
});

return text;
}

// 创建 markdown-it 实例并加载插件
const md = new MarkdownIt({
Expand All @@ -47,6 +50,12 @@ const md = new MarkdownIt({
linkify: true,
typographer: true,
highlight: (str: string, lang: string) => {
// 处理 mermaid 图表
if (lang === 'mermaid') {
return `<div class="mermaid">${str}</div>`;
}

// 处理代码高亮
if (lang && hljs.getLanguage(lang)) {
return `<pre class="hljs"><code>${hljs.highlight(str, {language: lang}).value}</code></pre>`;
}
Expand All @@ -63,6 +72,26 @@ const md = new MarkdownIt({
.use(deflist)
.use(linkAttributes, {attrs: {target: '_blank', rel: 'noopener'}})
.use(taskLists)
.use(renderMath);
.use(markdownItKatex, {
throwOnError: false,
errorColor: '#cc0000'
});

// 重写 render 方法,添加预处理
const originalRender = md.render.bind(md);
md.render = function (text: string, env?: any): string {
const preprocessedText = preprocessLatex(text);
const html = originalRender(preprocessedText, env);

if (html.indexOf('<div class="mermaid">') !== -1) {
setTimeout(() => {
mermaid.run({
querySelector: '.mermaid'
});
}, 0);
}

return html;
};

export default md;
2 changes: 1 addition & 1 deletion erupt-ai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<parent>
<groupId>xyz.erupt</groupId>
<artifactId>erupt</artifactId>
<version>1.13.2</version>
<version>1.13.3</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 2 additions & 0 deletions erupt-ai/src/main/java/xyz/erupt/ai/config/AiProp.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public class AiProp {

private boolean devMode = false;

private boolean enableFunctionCall = true;

}
18 changes: 14 additions & 4 deletions erupt-ai/src/main/java/xyz/erupt/ai/core/OpenAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
Expand All @@ -30,6 +31,14 @@
@Slf4j
public abstract class OpenAi extends LlmCore {

private final OkHttpClient client = new OkHttpClient().newBuilder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.MINUTES) // 流式需要很长!
.writeTimeout(30, TimeUnit.SECONDS)
.pingInterval(10, TimeUnit.SECONDS) // 保持连接心跳
.build();
;

private final Gson gson = new GsonBuilder().create();

public String chatApiPath() {
Expand All @@ -50,7 +59,6 @@ public LlmConfig config() {
public ChatCompletionResponse chat(LlmRequest llmRequest, String userPrompt, List<ChatCompletionMessage> assistantPrompt) {
assistantPrompt.add(new ChatCompletionMessage(MessageRole.user, userPrompt));
ChatCompletion completion = ChatCompletion.builder().model(llmRequest.getModel()).stream(false).messages(assistantPrompt).build();
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(
gson.toJson(completion),
MediaType.parse("application/json; charset=utf-8")
Expand All @@ -77,6 +85,7 @@ public ChatCompletionResponse chat(LlmRequest llmRequest, String userPrompt, Lis
@Override
@SneakyThrows
public void chatSse(LlmRequest llmRequest, String userPrompt, List<ChatCompletionMessage> assistantPrompt, Consumer<SseListener> listener) {
assistantPrompt.removeIf(it -> StringUtils.isBlank(it.getContent()));
ChatCompletion completion = ChatCompletion.builder().model(llmRequest.getModel()).messages(assistantPrompt).stream(true).build();
completion.setResponse_format(new HashMap<>() {{
this.put("type", String.valueOf(llmRequest.getResponseFormat()));
Expand Down Expand Up @@ -118,8 +127,8 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO
String line = source.readUtf8Line();
if (StringUtils.isNotBlank(line)) {
if (!response.isSuccessful()) {
this.onFailure(call, new IOException(response.body().string()));
log.error("Failed to get llm response from server: {}", response.body());
this.onFailure(call, new IOException(response.body().string() + line));
log.error("Failed to get llm response from server: {}", response.body() + " → " + line);
return;
} else {
try {
Expand All @@ -146,7 +155,8 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO
listener.accept(sseListener);
}
} catch (Exception e) {
this.onFailure(call, new IOException(e));
this.onFailure(call, new IOException(e + "→" + line));
break;
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion erupt-ai/src/main/java/xyz/erupt/ai/llm/OpenRouter.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package xyz.erupt.ai.llm;

import org.springframework.stereotype.Component;
import xyz.erupt.ai.core.LlmConfig;
import xyz.erupt.ai.core.OpenAi;

@Component
Expand All @@ -17,4 +16,8 @@ public String api() {
return "https://openrouter.ai";
}

@Override
public String chatApiPath() {
return "/api/v1/chat/completions";
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package xyz.erupt.ai.llm;

import org.springframework.stereotype.Component;
import xyz.erupt.ai.core.LlmConfig;
import xyz.erupt.ai.core.OpenAi;

/**
* @author YuePeng
* date 2025/2/26 22:58
*/
@Component
public class QWen extends OpenAi {
public class Qwen extends OpenAi {

@Override
public String chatApiPath() {
Expand Down
3 changes: 2 additions & 1 deletion erupt-ai/src/main/java/xyz/erupt/ai/model/LLM.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public class LLM extends MetaModelUpdateVo {
type = EditType.CHOICE,
notNull = true,
search = @Search,
choiceType = @ChoiceType(fetchHandler = LlmCore.H.class, trigger = LLMDataProxy.class)
onchange = LLMDataProxy.class,
choiceType = @ChoiceType(fetchHandler = LlmCore.H.class)
)
)
private String llm;
Expand Down
35 changes: 19 additions & 16 deletions erupt-ai/src/main/java/xyz/erupt/ai/model/LLMDataProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
import org.springframework.transaction.annotation.Transactional;
import xyz.erupt.ai.config.AiProp;
import xyz.erupt.ai.core.LlmCore;
import xyz.erupt.annotation.fun.ChoiceTrigger;
import xyz.erupt.annotation.fun.DataProxy;
import xyz.erupt.annotation.fun.OperationHandler;
import xyz.erupt.annotation.sub_erupt.Tpl;
import xyz.erupt.annotation.sub_field.sub_edit.OnChange;
import xyz.erupt.jpa.dao.EruptDao;
import xyz.erupt.linq.lambda.LambdaSee;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -24,7 +23,7 @@
* date 2025/3/1 18:19
*/
@Component
public class LLMDataProxy implements DataProxy<LLM>, Tpl.TplHandler, ChoiceTrigger, OperationHandler<LLM, Void> {
public class LLMDataProxy implements DataProxy<LLM>, Tpl.TplHandler, OnChange<LLM>, OperationHandler<LLM, Void> {

@Resource
private EruptDao eruptDao;
Expand All @@ -44,19 +43,6 @@ public void bindTplData(Map<String, Object> binding, String[] params) {

public static Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();

@Override
public Map<String, Object> trigger(Object code, String[] params) {
if (null != code && !"null".equals(code)) {
Map<String, Object> ret = new HashMap<>();
ret.put(LambdaSee.field(LLM::getModel), LlmCore.getLLM(code.toString()).model());
ret.put(LambdaSee.field(LLM::getApiUrl), LlmCore.getLLM(code.toString()).api());
ret.put(LambdaSee.field(LLM::getApiKey), "");
ret.put(LambdaSee.field(LLM::getConfig), gson.toJson(LlmCore.getLLM(code.toString()).config()));
return ret;
}
return Collections.emptyMap();
}

@Override
@Transactional
public String exec(List<LLM> data, Void o, String[] param) {
Expand All @@ -67,4 +53,21 @@ public String exec(List<LLM> data, Void o, String[] param) {
eruptDao.find(LLM.class, data.get(0).getId()).setDefaultLLM(true);
return "";
}

@Override
public Map<String, Object> populateForm(LLM llm, String[] params) {
if (null != llm.getLlm()) {
Map<String, Object> ret = new HashMap<>();
ret.put(LambdaSee.field(LLM::getModel), LlmCore.getLLM(llm.getLlm()).model());
ret.put(LambdaSee.field(LLM::getApiUrl), LlmCore.getLLM(llm.getLlm()).api());
ret.put(LambdaSee.field(LLM::getConfig), gson.toJson(LlmCore.getLLM(llm.getLlm()).config()));
return ret;
}
return Map.of();
}

@Override
public Map<String, String> buildEditExpr(LLM llm, String[] params) {
return Map.of();
}
}
20 changes: 16 additions & 4 deletions erupt-ai/src/main/java/xyz/erupt/ai/service/LLMService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -83,7 +84,9 @@ public List<ChatCompletionMessage> geneCompletionPrompt(Chat chat, LLMAgent llmA
}
} else {
// Function Call
chatCompletionMessages.add(new ChatCompletionMessage(MessageRole.system, aiFunctionManager.getFunctionCallPrompt()));
if (aiProp.isEnableFunctionCall()) {
chatCompletionMessages.add(new ChatCompletionMessage(MessageRole.system, aiFunctionManager.getFunctionCallPrompt()));
}
}
List<ChatMessage> chatMessages = eruptDao.lambdaQuery(ChatMessage.class)
.eq(ChatMessage::getChatId, chat.getId())
Expand All @@ -110,9 +113,18 @@ public void sendSse(MetaContext metaContext, LLMAgent llmAgent, SseEmitter emitt
llm.chatSse(llmRequest, chatMessage.getContent(), completionMessage, it -> {
if (it.isFinish()) {
String msg = it.getOutput().toString();
if (it.getOutput().toString().length() <= MESSAGE_TS || it.isError()) {
msg = this.sendMessage(emitter, it.getOutput().toString(), llmModal, chatMessage, completionMessage);
// finish 时,无论之前发送了多少,都要确保最后的增量被发送
if (msg.length() > MESSAGE_TS && !it.isError()) {
// 只发送最后的增量部分
if (!StringUtils.isEmpty(it.getCurrMessage())) {
this.sendMessage(emitter, it.getCurrMessage(), llmModal, chatMessage, completionMessage);
}
} else {
// 短消息直接发送完整内容
msg = this.sendMessage(emitter, msg, llmModal, chatMessage, completionMessage);
}

// 保存到数据库时使用完整消息
chatMessage.setTokens((long) it.getUsage().getPrompt_tokens());
eruptDao.mergeAndFlush(chatMessage);
eruptDao.persistAndFlush(ChatMessage.create(chatMessage.getChatId(), llmModal.getLlm(), llmModal.getModel(), ChatSenderType.MODEL, msg, (long) it.getUsage().getCompletion_tokens()));
Expand All @@ -138,7 +150,7 @@ public void sendSse(MetaContext metaContext, LLMAgent llmAgent, SseEmitter emitt
}

private String sendMessage(SseEmitter emitter, String userMessage, LLM llm, ChatMessage chatMessage, List<ChatCompletionMessage> userContext) {
if (aiFunctionManager.exist(userMessage.trim())) {
if (aiProp.isEnableFunctionCall() && aiFunctionManager.exist(userMessage.trim())) {
String functionMessage = aiFunctionManager.call(userMessage, llm, chatMessage.getContent(), userContext);
try {
emitter.send(GsonFactory.getGson().toJson(new SseBody(functionMessage)), MediaType.TEXT_EVENT_STREAM);
Expand Down
Loading
Loading