From b5432682b9ee3bb141b8af0eb815cce3af9eda63 Mon Sep 17 00:00:00 2001
From: q1279335527 <1279335527@qq.com>
Date: Sat, 20 Dec 2025 21:10:11 +0800
Subject: [PATCH] =?UTF-8?q?1.=E4=BC=98=E5=8C=96=E7=95=8C=E9=9D=A2=EF=BC=8C?=
=?UTF-8?q?=E5=87=8F=E5=B0=91=E5=BD=95=E5=85=A5=E6=97=B6=E5=8F=91=E7=94=9F?=
=?UTF-8?q?=E7=9A=84=E6=AD=A7=E4=B9=89=202.=E6=B7=BB=E5=8A=A0=E5=AF=B9?=
=?UTF-8?q?=E4=BA=8E=E8=B1=86=E5=8C=85=E6=A8=A1=E5=9E=8B=E8=81=8A=E5=A4=A9?=
=?UTF-8?q?=EF=BC=8C=E7=BB=98=E5=9B=BE=EF=BC=8C=E8=A7=86=E9=A2=91=E7=94=9F?=
=?UTF-8?q?=E6=88=90=E8=83=BD=E5=8A=9B=E7=9A=84=E6=94=AF=E6=8C=81=203.?=
=?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E4=BA=8EGemini=E5=A4=A7=E6=A8=A1?=
=?UTF-8?q?=E5=9E=8B=E8=81=8A=E5=A4=A9=E8=83=BD=E5=8A=9B=E7=9A=84=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=204.=E6=B7=BB=E5=8A=A0=E5=AF=B9=E4=BA=8Epostgresql?=
=?UTF-8?q?=E7=9A=84=E5=BF=AB=E9=80=9F=E8=BF=9E=E6=8E=A5=205.=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +-
apis/pom.xml | 4 +
.../src/main/resources/db/schema.sql | 4 -
.../static/html/pages/ModelParam.json | 15 +-
.../main/resources/static/html/pages/env.json | 2 +-
.../viewer/common/ai/model/entity/AiNode.java | 9 +
.../common/ai/model/entity/ModelParam.java | 3 +-
.../entity/purpose/BaseModelSetting.java | 1 +
.../entity/purpose/ChatModelSetting.java | 1 +
.../common/crud/core/service/BaseService.java | 2 +
.../crud/core/service/BaseServiceImpl.java | 1 +
executor/viewer-executor-blocks/pom.xml | 8 +
.../blocks/constants/TypeConstants.java | 29 --
.../blocks/executor/AIChatExecutor.java | 6 +-
.../executor/AbstractBlockExecutor.java | 12 +-
.../FunctionCallingBlockExecutor.java | 178 ---------
.../core/constants/TypeConstants.java | 2 +
models/pom.xml | 2 +
models/viewer-models-doubao/pom.xml | 28 ++
.../models/doubao/DoubaoChatBuilder.java | 55 +++
.../models/doubao/DoubaoImageBuilder.java | 40 ++
.../models/doubao/DoubaoVideoBuilder.java | 42 ++
.../doubao/ModelDoubaoAutoConfiguration.java | 11 +
.../models/doubao/api/audio/AudioApi.java | 373 ++++++++++++++++++
.../models/doubao/api/audio/AudioModel.java | 105 +++++
.../models/doubao/api/audio/AudioOptions.java | 187 +++++++++
.../doubao/api/image/DoubaoImageApi.java | 176 +++++++++
.../doubao/api/image/DoubaoImageModel.java | 144 +++++++
.../doubao/api/image/DoubaoImageOptions.java | 92 +++++
.../models/doubao/api/video/VideoApi.java | 235 +++++++++++
.../models/doubao/api/video/VideoModel.java | 97 +++++
.../models/doubao/api/video/VideoOptions.java | 38 ++
.../doubao/api/video/VideoResponse.java | 77 ++++
.../models/doubao/constants/AIConstants.java | 13 +
...ot.autoconfigure.AutoConfiguration.imports | 1 +
models/viewer-models-gemini/pom.xml | 28 ++
.../viewer/models/gemini/AIConstants.java | 7 +
.../models/gemini/GeminiChatBuilder.java | 56 +++
.../gemini/ModelGeminiAutoConfiguration.java | 11 +
...ot.autoconfigure.AutoConfiguration.imports | 1 +
.../viewer-modules-ds-jdbc/pom.xml | 2 +
.../viewer-modules-ds-jdbc-drill/pom.xml | 32 ++
.../viewer-modules-ds-jdbc-pgsql/pom.xml | 33 ++
.../pgsql/DsJdbcPgsqlAutoConfiguration.java | 11 +
.../ds/jdbc/pgsql/config/PgsqlDsConfig.java | 22 ++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
.../step/entity/block/BlockBodyEle.java | 6 +
pom.xml | 16 +-
sql/mysql/dv.sql | 4 -
49 files changed, 2000 insertions(+), 225 deletions(-)
delete mode 100644 executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/constants/TypeConstants.java
delete mode 100644 executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/FunctionCallingBlockExecutor.java
create mode 100644 models/viewer-models-doubao/pom.xml
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoChatBuilder.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoImageBuilder.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoVideoBuilder.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/ModelDoubaoAutoConfiguration.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioApi.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioModel.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioOptions.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageApi.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageModel.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageOptions.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoApi.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoModel.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoOptions.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoResponse.java
create mode 100644 models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/constants/AIConstants.java
create mode 100644 models/viewer-models-doubao/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 models/viewer-models-gemini/pom.xml
create mode 100644 models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/AIConstants.java
create mode 100644 models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/GeminiChatBuilder.java
create mode 100644 models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/ModelGeminiAutoConfiguration.java
create mode 100644 models/viewer-models-gemini/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-drill/pom.xml
create mode 100644 modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/pom.xml
create mode 100644 modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/java/xyz/thoughtset/viewer/modules/ds/jdbc/pgsql/DsJdbcPgsqlAutoConfiguration.java
create mode 100644 modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/java/xyz/thoughtset/viewer/modules/ds/jdbc/pgsql/config/PgsqlDsConfig.java
create mode 100644 modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
diff --git a/README.md b/README.md
index 0d7de93..97ddef2 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ kill 进程号
### 创建MCP服务
1. 访问 `http://localhost:11791` 登录管理界面
2. 在"McpServer"中创建新McpServer端点
-3. 选择需要对外提供的的方法,同时设置服务名,SSE路径(如`/sse`),消息端点(如`//mcp/messages`)等信息
+3. 选择需要对外提供的的方法,同时设置服务名,SSE路径(如`/sse`),消息端点(如`/mcp/messages`)等信息
4. 保存后返回查询界面,点击"启动"按钮即可被mcp服务访问

diff --git a/apis/pom.xml b/apis/pom.xml
index 95446f0..978d698 100644
--- a/apis/pom.xml
+++ b/apis/pom.xml
@@ -39,6 +39,10 @@
xyz.thoughtset.viewer
viewer-modules-ds-jdbc-mysql
+
+ xyz.thoughtset.viewer
+ viewer-modules-ds-jdbc-pgsql
+
xyz.thoughtset.viewer
viewer-modules-ds-jdbc-self
diff --git a/apis/viewer-apis-client/src/main/resources/db/schema.sql b/apis/viewer-apis-client/src/main/resources/db/schema.sql
index c5bdf30..865de75 100644
--- a/apis/viewer-apis-client/src/main/resources/db/schema.sql
+++ b/apis/viewer-apis-client/src/main/resources/db/schema.sql
@@ -356,10 +356,6 @@ CREATE TABLE IF NOT EXISTS modelparam (
updatedAt datetime NOT NULL,
statesCode int(11) DEFAULT '200',
model varchar(256) NOT NULL,
- systemPrompt text,
- maxMemory int(11) DEFAULT NULL,
- maxTokens bigint(20) DEFAULT NULL,
- temperature double DEFAULT NULL,
paramJson text,
purpose varchar(16) NOT NULL,
setting text,
diff --git a/apis/viewer-apis-client/src/main/resources/static/html/pages/ModelParam.json b/apis/viewer-apis-client/src/main/resources/static/html/pages/ModelParam.json
index f0f28c1..da04143 100644
--- a/apis/viewer-apis-client/src/main/resources/static/html/pages/ModelParam.json
+++ b/apis/viewer-apis-client/src/main/resources/static/html/pages/ModelParam.json
@@ -362,7 +362,10 @@
"maxRows": 20,
"labelWidth": "var(--sizes-base-24)",
"labelAlign": "top",
- "mode": "normal"
+ "mode": "normal",
+ "hiddenOn": "${purpose != 'CHAT'}",
+ "clearValueOnHidden": true,
+ "visibleOn": "${purpose == 'CHAT'}"
},
{
"name": "modelArgs.temperature",
@@ -382,7 +385,10 @@
"value": "",
"precision": 1,
"kilobitSeparator": false,
- "max": 1
+ "max": 1,
+ "hiddenOn": "${purpose != 'CHAT'}",
+ "clearValueOnHidden": true,
+ "visibleOn": "${purpose == 'CHAT'}"
},
{
"type": "service",
@@ -404,7 +410,10 @@
"min": 0,
"value": "",
"precision": "",
- "kilobitSeparator": false
+ "kilobitSeparator": false,
+ "hiddenOn": "${purpose != 'CHAT'}",
+ "clearValueOnHidden": true,
+ "visibleOn": "${purpose == 'CHAT'}"
}
],
"id": "u:7004050cb373",
diff --git a/apis/viewer-apis-client/src/main/resources/static/html/pages/env.json b/apis/viewer-apis-client/src/main/resources/static/html/pages/env.json
index 95f5057..4ed775a 100644
--- a/apis/viewer-apis-client/src/main/resources/static/html/pages/env.json
+++ b/apis/viewer-apis-client/src/main/resources/static/html/pages/env.json
@@ -833,7 +833,7 @@
},
"sendOn": "${!ISEMPTY(id)}"
},
- "debug": true
+ "debug": false
}
],
"actions": [
diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/AiNode.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/AiNode.java
index d078ce6..7322e80 100644
--- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/AiNode.java
+++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/AiNode.java
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import org.springframework.util.StringUtils;
import xyz.thoughtset.viewer.common.core.entity.BaseMeta;
import xyz.thoughtset.viewer.common.crud.core.annotation.ApiCRUDPower;
@@ -30,4 +31,12 @@ public class AiNode extends BaseMeta {
protected transient List defaultModelParams;
+ public String linkBaseUrlWithDefault(String defaultBaseUrl) {
+ if (StringUtils.hasText(this.baseUrl)) {
+ return this.baseUrl;
+ }
+ return defaultBaseUrl;
+ }
+
+
}
diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/ModelParam.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/ModelParam.java
index ff3a2c6..21bbb1e 100644
--- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/ModelParam.java
+++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/ModelParam.java
@@ -26,7 +26,8 @@ public class ModelParam extends BaseMeta {
protected ModelPurposeEnum purpose;
protected String setting;
protected String paramJson;
- protected Integer maxMemory = 8;
+// @Deprecated
+// protected Integer maxMemory = 8;
//Options Value Map
protected transient Map paramMap;
protected transient BaseModelSetting modelArgs;
diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/BaseModelSetting.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/BaseModelSetting.java
index a7cd2f8..959bb58 100644
--- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/BaseModelSetting.java
+++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/BaseModelSetting.java
@@ -12,5 +12,6 @@ import lombok.NoArgsConstructor;
* select specific model setting class according to different option classes.
*/
public class BaseModelSetting {
+ protected String reqUrl;
protected String workWay;//select ModelOptions class
}
diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ChatModelSetting.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ChatModelSetting.java
index ce0d5eb..23b5ea6 100644
--- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ChatModelSetting.java
+++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ChatModelSetting.java
@@ -11,6 +11,7 @@ public class ChatModelSetting extends BaseModelSetting {
protected String systemPrompt;
protected Long maxTokens;
protected Double temperature;
+ protected Integer maxMemory = 8;
}
diff --git a/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseService.java b/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseService.java
index b4f8c25..f3a8622 100644
--- a/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseService.java
+++ b/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseService.java
@@ -24,4 +24,6 @@ public interface BaseService extends IService{
public Page selectPage(Page page, LinkedHashMap baseMap);
List selectList(LinkedHashMap baseMap);
+
+
}
diff --git a/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseServiceImpl.java b/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseServiceImpl.java
index 1592d75..f8cf782 100644
--- a/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseServiceImpl.java
+++ b/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseServiceImpl.java
@@ -87,6 +87,7 @@ public class BaseServiceImpl , T extends IdMeta> extends
return list(MPWrapperUtil.buildWrapper(convertValue(baseMap)));
}
+
@SneakyThrows
public T convertValue(LinkedHashMap baseMap) {
return objectMapper.convertValue(baseMap,this.getEntityClass());
diff --git a/executor/viewer-executor-blocks/pom.xml b/executor/viewer-executor-blocks/pom.xml
index 9b20e24..6e0f7e1 100644
--- a/executor/viewer-executor-blocks/pom.xml
+++ b/executor/viewer-executor-blocks/pom.xml
@@ -50,6 +50,14 @@
xyz.thoughtset.viewer
viewer-models-zhipuai
+
+ xyz.thoughtset.viewer
+ viewer-models-gemini
+
+
+ xyz.thoughtset.viewer
+ viewer-models-doubao
+
\ No newline at end of file
diff --git a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/constants/TypeConstants.java b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/constants/TypeConstants.java
deleted file mode 100644
index a843f78..0000000
--- a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/constants/TypeConstants.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package xyz.thoughtset.viewer.executor.blocks.constants;
-
-import com.google.common.collect.ImmutableMap;
-
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-public class TypeConstants {
- public static final Map TYPE_MAP = new ImmutableMap.Builder()
- .put("String", String.class)
- .put("Boolean", Boolean.class)
- .put("byte", Byte.class)
- .put("Double", Double.class)
- .put("Integer", Integer.class)
- .put("Long", Long.class)
- .put("Date", Date.class)
- .build();
-
- public static final List BASE_TYPE_LIST = TYPE_MAP.entrySet().stream()
- .map( e -> {
- HashMap map = new HashMap();
- map.put("title",e.getKey());
- map.put("value",e.getKey());
- return map;
- }).collect(Collectors.toList());
-}
diff --git a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AIChatExecutor.java b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AIChatExecutor.java
index 4dd7b21..2a93f70 100644
--- a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AIChatExecutor.java
+++ b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AIChatExecutor.java
@@ -18,6 +18,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.StringUtils;
import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam;
import xyz.thoughtset.viewer.common.ai.model.entity.option.ExtraParamChatOptions;
+import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ChatModelSetting;
import xyz.thoughtset.viewer.common.exc.exceptions.ExecException;
import xyz.thoughtset.viewer.modules.step.entity.AISupportBody;
import xyz.thoughtset.viewer.modules.step.entity.BlockTypeEnum;
@@ -47,8 +48,10 @@ public abstract class AIChatExecutor extends AbstractAI
HashMap filterMap = filterDataAsMapForPrompt(body, params, parser, context);
ModelParam modelParam = loadModelParam(body);
+ ChatModelSetting chatModelSetting = (ChatModelSetting) modelParam.getModelArgs();
+ Integer maxTokens = chatModelSetting !=null ? chatModelSetting.getMaxMemory() : 8;
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
- .maxMessages(modelParam.getMaxMemory())
+ .maxMessages(maxTokens)
.build();
ExtraParamChatOptions extraParam = new ExtraParamChatOptions();
ToolCallingChatOptions.Builder toolCallingBuilder = ToolCallingChatOptions.builder()
@@ -111,6 +114,7 @@ public abstract class AIChatExecutor extends AbstractAI
.build();
ChatOptions chatOptions = toolCallingBuilder.build();
UserMessage message = UserMessage.builder()
+ .media()
.text(fillAndRenderPromptToStr(body, body.getUserMsg(), parser, context, filterMap))
.build();
// new UserMessage(body.getUserMsg(),params);
diff --git a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AbstractBlockExecutor.java b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AbstractBlockExecutor.java
index 01fc442..fa2a26c 100644
--- a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AbstractBlockExecutor.java
+++ b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AbstractBlockExecutor.java
@@ -82,8 +82,16 @@ public abstract class AbstractBlockExecutor implements
protected Object execNode(BlockBodyEle ele, Map params, ExpressionParser parser, StandardEvaluationContext context){
Map tmpMap = BlockArgsUtils.filterParams(ele, parser, context);
- Object val = ele.eleWasFun()? blockExecutorManager.execBlocks(ele.getEleId(),tmpMap,parser):
- executorManager.execute(ele.getEleId(),tmpMap);
+ Object val = null;
+ if (ele.eleWasFun()){
+ val = blockExecutorManager.execBlocks(ele.getEleId(),tmpMap,parser);
+ } else if (ele.eleWasTool()) {
+// val = blockExecutorManager.execToolOrScript(ele.getEleId(), tmpMap, parser);
+ } else if (ele.eleWasScript()) {
+// val = blockExecutorManager.execToolOrScript(ele.getEleId(), tmpMap, parser);
+ } else {
+ val = executorManager.execute(ele.getEleId(),tmpMap);
+ }
if (ObjectUtils.isEmpty(val) || !StringUtils.hasText(ele.getValNum())) {
return val;
}
diff --git a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/FunctionCallingBlockExecutor.java b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/FunctionCallingBlockExecutor.java
deleted file mode 100644
index 2857254..0000000
--- a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/FunctionCallingBlockExecutor.java
+++ /dev/null
@@ -1,178 +0,0 @@
-package xyz.thoughtset.viewer.executor.blocks.executor;
-
-import lombok.SneakyThrows;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
-import org.springframework.ai.chat.memory.MessageWindowChatMemory;
-import org.springframework.ai.chat.messages.*;
-import org.springframework.ai.chat.model.ChatResponse;
-import org.springframework.ai.chat.prompt.ChatOptions;
-import org.springframework.ai.chat.prompt.Prompt;
-import org.springframework.ai.model.tool.ToolCallingChatOptions;
-import org.springframework.ai.model.tool.ToolCallingManager;
-import org.springframework.ai.model.tool.ToolExecutionResult;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.support.StandardEvaluationContext;
-import org.springframework.util.ObjectUtils;
-import org.springframework.util.StringUtils;
-import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam;
-import xyz.thoughtset.viewer.common.exc.exceptions.ExecException;
-import xyz.thoughtset.viewer.modules.step.entity.FunctionCallBody;
-import xyz.thoughtset.viewer.executor.blocks.tool.FunctionCallBlockToolCallbackProvider;
-import xyz.thoughtset.viewer.modules.step.entity.block.BlockBodyEle;
-import xyz.thoughtset.viewer.modules.step.entity.block.BlockInfo;
-
-import java.util.*;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-
-@Slf4j
-@Deprecated
-public class FunctionCallingBlockExecutor extends AbstractAISupportBlockExecutor {
-// @Autowired
-// protected ModelParamDao modelParamDao;
-// @Autowired
-// protected ModelFactory modelFactory;
- @Autowired
- private ToolCallingManager toolCallingManager;
- @Autowired
- private FunctionCallBlockToolCallbackProvider provider;
-
-
-//先选择函数,再加载参数执行函数
- @SneakyThrows
- @Override
- protected Object doQuery(BlockInfo block, FunctionCallBody body, Map params, ExpressionParser parser, StandardEvaluationContext context) throws ExecException {
- ModelParam modelParam = loadModelParam(body);
- ChatClient.Builder builder = modelFactory.clientBuilder(modelParam,null);
- MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
- .maxMessages(modelParam.getMaxMemory())
- .build();
-// ChatModelSetting settingObj = (ChatModelSetting) modelParam.getModelArgs();
- String limitSystemPrompt =
- "选择适当的方法执行,由于部分参数存储在本地,对于非required参数,可以填入建议值或空值。预期值与方法执行结果不一致时,以方法结果为准。";
- String systemText = modelParam.chatSystemPrompt();
- if (body.getJsonType()!=null && body.getJsonType().booleanValue()){
-// systemText = """
-// 请只返回数组类型的JSON,不要任何额外说明,当后续要求与当前冲突时,以当前要求为准!
-// 你是一个函数调用助手,请严格按照要求的格式返回结果,不要任何额外说明.
-// 1.返回值必须是一个数组,数组的每一项是一个对象
-// 2.对象的key是函数参数名,value是对应的值
-// 3.如果没有值,请返回null,不要返回空字符串,不要省略该key
-// 4.如果值是字符串,请确保值是合法的JSON字符串,如果值本身是一个JSON对象或数组,请直接返回该对象或数组,不要转义为字符串
-// 5.如果没有数据,请返回一个空数组,不要返回null,不要返回空对象,不要返回字符串
-// 6.请严格按照要求的格式返回,不要有任何多余的内容,不要有任何拼写错误,不要有任何语法错误"""
- // 备用prompt2
-// """
-// 选择适当的方法执行。对于非必需参数,可使用建议值或空值(部分参数已本地存储)。若方法执行结果与预期值不一致,以方法执行结果为准。响应必须严格以JSON数组格式返回,不得包含任何其他内容。确保无拼写错误或语法错误。当后续要求与当前要求冲突时,以当前要求为准。
-// """
- systemText = limitSystemPrompt+"""
- 最终结果必须按照JSON数组格式返回,不要有其余任何内容,不要有任何拼写错误,不要有任何语法错误,当后续要求与当前冲突时,以当前要求为准!
- """
- + (StringUtils.hasText(systemText) ? systemText : "");
- }
- String finalSystemText = systemText;
- builder.defaultSystem(s->s.text(finalSystemText).params(params));
- List funs = body.getBodyEles();
- ToolCallingChatOptions.Builder toolCallingBuilder = ToolCallingChatOptions.builder()
- .internalToolExecutionEnabled(false);
- if (!ObjectUtils.isEmpty(funs)){
- Map funParamMap = funs.parallelStream().collect(Collectors.toMap(
- BlockBodyEle::getEleId, Function.identity()
- ));
- builder.defaultToolCallbacks(provider.getToolCallbacks(block.getId(),funParamMap));
-
-// toolCallingBuilder = toolCallingBuilder.toolCallbacks(provider.getToolCallbacks(block.getId(),funParamMap));
- }
- ChatClient client = builder
- .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
- .build();
- ChatOptions chatOptions = toolCallingBuilder.build();
-// ChatClient.ChatClientRequestSpec spec = client
-// .prompt()
-// .options(chatOptions)
-// .toolContext(params)
-// .user(body.getUserMsg());
- UserMessage message = UserMessage.builder()
- .text(body.getUserMsg())
- .build();
-// new UserMessage(body.getUserMsg(),params);
- String chatId = block.getId()+ UUID.randomUUID();
- saveMegToChatMemory(chatId, Collections.singletonList(message), chatMemory);
- Prompt chatPrompt = new Prompt(chatMemory.get(chatId),chatOptions);
-// ChatClient.CallResponseSpec callResponseSpec = spec.call();
-// String val = callResponseSpec.content();
- //ToolCallingChatClientAdvisor
- // ListOutputConverter
- ChatResponse chatResponse = client
- .prompt(chatPrompt)
- .toolContext(params).call().chatResponse();
- // 处理可能的多轮工具调用
- List messageList = new ArrayList<>();
- while (chatResponse.hasToolCalls()) {
- // 获取AI助手的响应消息
- AssistantMessage assistantMessage = chatResponse.getResult().getOutput();
- log.debug("***" + assistantMessage.getText());
-
- // 获取AI要求调用的工具列表
- List toolCalls = assistantMessage.getToolCalls();
- toolCalls.forEach(toolCall -> {
- log.debug("*** 准备调用工具{}:{},参数:({})", toolCall.type(), toolCall.name(), toolCall.arguments());
- });
-
- // 执行工具调用
- ToolExecutionResult toolExecutionResult = null;
- try {
- toolExecutionResult = toolCallingManager.executeToolCalls(chatPrompt, chatResponse);
- } catch (Exception e) {
- log.error("调用工具失败", e);
- messageList.add(new AssistantMessage("调用工具失败:" + e.getMessage()));
- break;
- }
-
- List toolResultMessages = toolExecutionResult.conversationHistory();
- log.debug("*** 执行结果" + toolResultMessages);
-
- Message lastMessage = toolResultMessages.get(toolResultMessages.size() - 1);
- if (lastMessage.getMessageType() == MessageType.TOOL) {
- ToolResponseMessage toolMessage = (ToolResponseMessage) lastMessage;
- toolMessage.getMetadata().put("toolArguments", toolCalls);
- messageList.add(toolMessage);
-
- // 记录每个工具调用的详细信息
- for (ToolResponseMessage.ToolResponse resp : toolMessage.getResponses()) {
- log.debug("*** {}#{}#{}",resp.id(),resp.name(),resp.responseData());
- }
- }
- saveMegToChatMemory(chatId, toolResultMessages, chatMemory);
- chatPrompt = new Prompt(chatMemory.get(chatId), chatOptions);
- chatResponse = client
- .prompt(chatPrompt)
- .call()
- .chatResponse();
- }
-
- String val = chatResponse.getResult().getOutput().getText();
- log.debug("===结果:{}", val);
- Object result;
-
- try {
-// List results = callResponseSpec.entity(new ParameterizedTypeReference>() {});
- result = objectMapper.readValue(val, DATA_TYPE);
- }catch (Exception e){
- if (!e.getClass().getName().equals("com.fasterxml.jackson.core.JsonParseException")){
- log.error("错误结果:{}", val);
- e.printStackTrace();
- }
- result = val;
- }
-
- return result;
- }
-
-
-
-}
diff --git a/executor/viewer-executor-core/src/main/java/xyz/thoughtset/viewer/executor/core/constants/TypeConstants.java b/executor/viewer-executor-core/src/main/java/xyz/thoughtset/viewer/executor/core/constants/TypeConstants.java
index 135d8b3..bd7f661 100644
--- a/executor/viewer-executor-core/src/main/java/xyz/thoughtset/viewer/executor/core/constants/TypeConstants.java
+++ b/executor/viewer-executor-core/src/main/java/xyz/thoughtset/viewer/executor/core/constants/TypeConstants.java
@@ -2,6 +2,7 @@ package xyz.thoughtset.viewer.executor.core.constants;
import com.google.common.collect.ImmutableMap;
+import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -17,6 +18,7 @@ public class TypeConstants {
.put("Integer", Integer.class)
.put("Long", Long.class)
.put("Date", Date.class)
+// .put("File", File.class)
.build();
public static final List BASE_TYPE_LIST = TYPE_MAP.entrySet().stream()
diff --git a/models/pom.xml b/models/pom.xml
index a90a505..0de0752 100644
--- a/models/pom.xml
+++ b/models/pom.xml
@@ -15,6 +15,8 @@
viewer-models-deepseek
viewer-models-openai
viewer-models-zhipuai
+ viewer-models-doubao
+ viewer-models-gemini
diff --git a/models/viewer-models-doubao/pom.xml b/models/viewer-models-doubao/pom.xml
new file mode 100644
index 0000000..fda7749
--- /dev/null
+++ b/models/viewer-models-doubao/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+
+ xyz.thoughtset.viewer
+ models
+ ${revision}
+
+
+ viewer-models-doubao
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+ org.springframework.ai
+ spring-ai-openai
+ ${springai.version}
+
+
+
+
\ No newline at end of file
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoChatBuilder.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoChatBuilder.java
new file mode 100644
index 0000000..5228754
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoChatBuilder.java
@@ -0,0 +1,55 @@
+package xyz.thoughtset.viewer.models.doubao;
+
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.model.ModelOptions;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.openai.api.ResponseFormat;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import xyz.thoughtset.viewer.common.ai.model.entity.AiNode;
+import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam;
+import xyz.thoughtset.viewer.common.ai.model.entity.option.ExtraParamChatOptions;
+import xyz.thoughtset.viewer.common.ai.model.factory.ChatModelBuilder;
+import xyz.thoughtset.viewer.models.doubao.constants.AIConstants;
+
+@Component
+public class DoubaoChatBuilder extends ChatModelBuilder {
+ public DoubaoChatBuilder() {
+ super(AIConstants.MODEL_NAME);
+ }
+
+
+ @Override
+ public ChatModel buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) {
+ OpenAiChatOptions options = loadAndMergeModelOptions(modelParam, OpenAiChatOptions.class, modelOptions);
+ options.setModel(modelParam.getModel());
+ String baseUrl = node.getBaseUrl();
+ if (!StringUtils.hasText(baseUrl)) {
+ baseUrl = AIConstants.DEFAULT_BASE_URL;
+ }
+ OpenAiApi api = OpenAiApi.builder()
+ .baseUrl(baseUrl)
+ .completionsPath(AIConstants.CHAT_URL)
+ .apiKey(node.getApiKey()).build();
+ ChatModel chatModel = OpenAiChatModel.builder()
+ .defaultOptions(options)
+ .openAiApi(api)
+ .build();
+ return chatModel;
+ }
+
+ @Override
+ public void mergeOptions(ModelOptions source, ModelOptions extraOptions) {
+ if (!(extraOptions instanceof ExtraParamChatOptions)) return ;
+ OpenAiChatOptions options = (OpenAiChatOptions) source;
+ ExtraParamChatOptions extraParamChatOptions = (ExtraParamChatOptions) extraOptions;
+ if (extraParamChatOptions.responseUsedJsonObject()){
+ options.setResponseFormat(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build());
+ }
+ }
+
+
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoImageBuilder.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoImageBuilder.java
new file mode 100644
index 0000000..3ef6646
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoImageBuilder.java
@@ -0,0 +1,40 @@
+package xyz.thoughtset.viewer.models.doubao;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.model.Model;
+import org.springframework.ai.model.ModelOptions;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import xyz.thoughtset.viewer.common.ai.model.entity.AiNode;
+import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam;
+import xyz.thoughtset.viewer.common.ai.model.factory.ImageModelBuilder;
+import xyz.thoughtset.viewer.models.doubao.api.image.*;
+import xyz.thoughtset.viewer.models.doubao.constants.AIConstants;
+
+@Slf4j
+@Component
+public class DoubaoImageBuilder extends ImageModelBuilder {
+ public DoubaoImageBuilder() {
+ super(AIConstants.MODEL_NAME);
+ }
+
+
+ @Override
+ public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) {
+ DoubaoImageOptions options = loadAndMergeModelOptions(modelParam, DoubaoImageOptions.class, modelOptions);
+ options.setModel(modelParam.getModel());
+ String baseUrl = node.getBaseUrl();
+ if (!StringUtils.hasText(baseUrl)) {
+ baseUrl = AIConstants.DEFAULT_BASE_URL;
+ }
+ DoubaoImageApi api = DoubaoImageApi.builder()
+ .baseUrl(baseUrl)
+ .apiKey(node.getApiKey())
+ .build();
+ DoubaoImageModel model = new DoubaoImageModel(api, options,buildDefaultLongRetryTemplate());
+ return model;
+ }
+
+
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoVideoBuilder.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoVideoBuilder.java
new file mode 100644
index 0000000..d6cfed9
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/DoubaoVideoBuilder.java
@@ -0,0 +1,42 @@
+package xyz.thoughtset.viewer.models.doubao;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.model.Model;
+import org.springframework.ai.model.ModelOptions;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import xyz.thoughtset.viewer.common.ai.model.entity.AiNode;
+import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam;
+import xyz.thoughtset.viewer.common.ai.model.factory.VideoModelBuilder;
+import xyz.thoughtset.viewer.models.doubao.api.video.VideoApi;
+import xyz.thoughtset.viewer.models.doubao.api.video.VideoModel;
+import xyz.thoughtset.viewer.models.doubao.api.video.VideoOptions;
+import xyz.thoughtset.viewer.models.doubao.constants.AIConstants;
+
+@Slf4j
+@Component
+public class DoubaoVideoBuilder extends VideoModelBuilder {
+ public DoubaoVideoBuilder() {super(AIConstants.MODEL_NAME);}
+
+
+ @Override
+ public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) {
+ VideoOptions options = loadAndMergeModelOptions(modelParam, VideoOptions.class, modelOptions);
+ options.setModel(modelParam.getModel());
+ String baseUrl = node.getBaseUrl();
+ if (!StringUtils.hasText(baseUrl)) {
+ baseUrl = AIConstants.DEFAULT_BASE_URL;
+ }
+ VideoApi api = VideoApi.builder()
+ .baseUrl(baseUrl)
+ .apiKey(node.getApiKey())
+ .restClientBuilder(createLoggingRestClient())
+ .build();
+ VideoModel model = new VideoModel(api, options,buildDefaultLongRetryTemplate(),objectMapper);
+ return model;
+ }
+
+
+
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/ModelDoubaoAutoConfiguration.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/ModelDoubaoAutoConfiguration.java
new file mode 100644
index 0000000..9cbcf7a
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/ModelDoubaoAutoConfiguration.java
@@ -0,0 +1,11 @@
+package xyz.thoughtset.viewer.models.doubao;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan
+@EnableConfigurationProperties
+public class ModelDoubaoAutoConfiguration {
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioApi.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioApi.java
new file mode 100644
index 0000000..4c1f8bd
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioApi.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.audio;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.ai.model.ApiKey;
+import org.springframework.ai.model.NoopApiKey;
+import org.springframework.ai.model.SimpleApiKey;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.Assert;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import xyz.thoughtset.viewer.models.doubao.constants.AIConstants;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public class AudioApi {
+
+ private final RestClient restClient;
+
+ /**
+ * Create a new audio api.
+ * @param baseUrl api base URL.
+ * @param apiKey OpenAI apiKey.
+ * @param headers the http headers to use.
+ * @param restClientBuilder RestClient builder.
+ * @param responseErrorHandler Response error handler.
+ */
+ public AudioApi(String baseUrl, ApiKey apiKey, HttpHeaders headers, RestClient.Builder restClientBuilder,
+ ResponseErrorHandler responseErrorHandler) {
+ if (!StringUtils.hasText(baseUrl)) {
+ baseUrl = AIConstants.DEFAULT_BASE_URL;
+ }
+ Consumer authHeaders = h -> h.addAll(HttpHeaders.readOnlyHttpHeaders(headers));
+
+ // @formatter:off
+ this.restClient = restClientBuilder.clone()
+ .baseUrl(baseUrl)
+ .defaultHeaders(authHeaders)
+ .defaultStatusHandler(responseErrorHandler)
+ .defaultRequest(requestHeadersSpec -> {
+ if (!(apiKey instanceof NoopApiKey)) {
+ requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
+ }
+ })
+ .build();
+
+ }
+
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Request to generates audio from the input text.
+ * @param requestBody The request body.
+ * @return Response entity containing the audio binary.
+ */
+ public ResponseEntity createSpeech(SpeechRequest requestBody) {
+ return this.restClient.post().uri(AIConstants.TTS_URL).body(requestBody).retrieve().toEntity(byte[].class);
+ }
+
+
+
+ public ResponseEntity createTranscription(TranscriptionRequest requestBody) {
+
+ MultiValueMap multipartBody = new LinkedMultiValueMap<>();
+ multipartBody.add("file", new ByteArrayResource(requestBody.file()) {
+
+ @Override
+ public String getFilename() {
+ return requestBody.fileName();
+ }
+ });
+ multipartBody.add("model", requestBody.model());
+ multipartBody.add("temperature", requestBody.temperature());
+
+ return this.restClient.post()
+ .uri(AIConstants.ASR_URL)
+ .body(multipartBody)
+ .retrieve()
+ .toEntity(StructuredResponse.class);
+ }
+
+
+ /**
+ * Request to generates audio from the input text. Reference:
+ * Create
+ * Speech
+ *
+ * @param model The model to use for generating the audio. One of the available Audio
+ * models: audio-1, audio-1-hd, or gpt-4o-mini-audio.
+ * @param input The input text to synthesize. Must be at most 4096 tokens long.
+ * @param voice The voice to use for synthesis. One of the available voices for the
+ * chosen model: 'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'onyx', 'nova',
+ * 'sage', 'shimmer', and 'verse'.
+ * @param responseFormat The format to audio in. Supported formats are mp3, opus, aac,
+ * flac, wav, and pcm. Defaults to mp3.
+ * @param speed The speed of the voice synthesis. The acceptable range is from 0.25
+ * (slowest) to 4.0 (fastest). Does not work with gpt-4o-mini-audio.
+ */
+ @JsonInclude(Include.NON_NULL)
+ public record SpeechRequest(
+ @JsonProperty("model") String model,
+ @JsonProperty("input") String input,
+ @JsonProperty("voice") String voice,
+ @JsonProperty("response_format") String responseFormat,
+ @JsonProperty("speed") Double speed) {
+ // @formatter:on
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+
+
+ /**
+ * Builder for the SpeechRequest.
+ */
+ public static final class Builder {
+
+ private String model = "cogtts";
+
+ private String input;
+
+ private String voice;
+
+ private String responseFormat = "pcm";
+
+ private Double speed;
+
+ public Builder model(String model) {
+ this.model = model;
+ return this;
+ }
+
+ public Builder input(String input) {
+ this.input = input;
+ return this;
+ }
+
+ public Builder voice(String voice) {
+ this.voice = voice;
+ return this;
+ }
+
+ public Builder responseFormat(String responseFormat) {
+ this.responseFormat = responseFormat;
+ return this;
+ }
+
+ public Builder speed(Double speed) {
+ this.speed = speed;
+ return this;
+ }
+
+ public SpeechRequest build() {
+
+ return new SpeechRequest(this.model, this.input, this.voice, this.responseFormat, this.speed);
+ }
+
+ }
+
+ }
+
+ @JsonInclude(Include.NON_NULL)
+ public record TranscriptionRequest(
+ // @formatter:off
+ @JsonProperty("file") byte[] file,
+ String fileName,
+ @JsonProperty("model") String model,
+ @JsonProperty("temperature") Float temperature
+ ) {
+ public static Builder builder() {
+ return new Builder();
+ }
+
+
+ public static final class Builder {
+
+ private byte[] file;
+
+ private String fileName;
+
+ private String model;
+
+ private Float temperature;
+
+ public Builder file(byte[] file) {
+ this.file = file;
+ return this;
+ }
+
+ public Builder fileName(String fileName) {
+ this.fileName = fileName;
+ return this;
+ }
+
+ public Builder model(String model) {
+ this.model = model;
+ return this;
+ }
+
+
+ public Builder temperature(Float temperature) {
+ this.temperature = temperature;
+ return this;
+ }
+
+ public TranscriptionRequest build() {
+ Assert.notNull(this.file, "file must not be null");
+ Assert.hasText(this.model, "model must not be empty");
+
+ return new TranscriptionRequest(this.file, this.fileName, this.model, this.temperature);
+ }
+
+ }
+
+ }
+
+ @JsonInclude(Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record StructuredResponse(
+ // @formatter:off
+ @JsonProperty("language") String language,
+ @JsonProperty("duration") Float duration,
+ @JsonProperty("text") String text,
+ @JsonProperty("words") List words,
+ @JsonProperty("segments") List segments) {
+ // @formatter:on
+
+ /**
+ * Extracted word and it corresponding timestamps.
+ *
+ * @param word The text content of the word.
+ * @param start The start time of the word in seconds.
+ * @param end The end time of the word in seconds.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record Word(
+ // @formatter:off
+ @JsonProperty("word") String word,
+ @JsonProperty("start") Float start,
+ @JsonProperty("end") Float end) {
+ // @formatter:on
+ }
+
+ /**
+ * Segment of the transcribed text and its corresponding details.
+ *
+ * @param id Unique identifier of the segment.
+ * @param seek Seek offset of the segment.
+ * @param start Start time of the segment in seconds.
+ * @param end End time of the segment in seconds.
+ * @param text The text content of the segment.
+ * @param tokens Array of token IDs for the text content.
+ * @param temperature Temperature parameter used for generating the segment.
+ * @param avgLogprob Average logprob of the segment. If the value is lower than
+ * -1, consider the logprobs failed.
+ * @param compressionRatio Compression ratio of the segment. If the value is
+ * greater than 2.4, consider the compression failed.
+ * @param noSpeechProb Probability of no speech in the segment. If the value is
+ * higher than 1.0 and the avg_logprob is below -1, consider this segment silent.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record Segment(
+ // @formatter:off
+ @JsonProperty("id") Integer id,
+ @JsonProperty("seek") Integer seek,
+ @JsonProperty("start") Float start,
+ @JsonProperty("end") Float end,
+ @JsonProperty("text") String text,
+ @JsonProperty("tokens") List tokens,
+ @JsonProperty("temperature") Float temperature,
+ @JsonProperty("avg_logprob") Float avgLogprob,
+ @JsonProperty("compression_ratio") Float compressionRatio,
+ @JsonProperty("no_speech_prob") Float noSpeechProb) {
+ // @formatter:on
+ }
+
+ }
+
+
+
+ public static final class Builder {
+
+ private String baseUrl = AIConstants.DEFAULT_BASE_URL;
+
+ private ApiKey apiKey;
+
+ private HttpHeaders headers = new HttpHeaders();
+
+ private RestClient.Builder restClientBuilder = RestClient.builder();
+
+
+ private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER;
+
+ public Builder baseUrl(String baseUrl) {
+ Assert.hasText(baseUrl, "baseUrl cannot be null or empty");
+ this.baseUrl = baseUrl;
+ return this;
+ }
+
+ public Builder apiKey(ApiKey apiKey) {
+ Assert.notNull(apiKey, "apiKey cannot be null");
+ this.apiKey = apiKey;
+ return this;
+ }
+
+ public Builder apiKey(String simpleApiKey) {
+ Assert.notNull(simpleApiKey, "simpleApiKey cannot be null");
+ this.apiKey = new SimpleApiKey(simpleApiKey);
+ return this;
+ }
+
+ public Builder headers(HttpHeaders headers) {
+ Assert.notNull(headers, "headers cannot be null");
+ this.headers = headers;
+ return this;
+ }
+
+ public Builder restClientBuilder(RestClient.Builder restClientBuilder) {
+ Assert.notNull(restClientBuilder, "restClientBuilder cannot be null");
+ this.restClientBuilder = restClientBuilder;
+ return this;
+ }
+
+
+ public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) {
+ Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null");
+ this.responseErrorHandler = responseErrorHandler;
+ return this;
+ }
+
+ public AudioApi build() {
+ Assert.notNull(this.apiKey, "apiKey must be set");
+ return new AudioApi(this.baseUrl, this.apiKey, this.headers, this.restClientBuilder,
+ this.responseErrorHandler);
+ }
+
+ }
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioModel.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioModel.java
new file mode 100644
index 0000000..37e0ce5
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioModel.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.audio;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.audio.tts.*;
+import org.springframework.ai.model.Model;
+import org.springframework.http.ResponseEntity;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+
+import java.util.List;
+
+@Slf4j
+public class AudioModel implements Model {
+
+ /**
+ * The default options used for the audio completion requests.
+ */
+ private final AudioOptions defaultOptions;
+
+ /**
+ * The retry template used to retry the OpenAI Audio API calls.
+ */
+ private final RetryTemplate retryTemplate;
+
+ /**
+ * Low-level access to the OpenAI Audio API.
+ */
+ private final AudioApi audioApi;
+
+
+ /**
+ * Initializes a new instance of the AudioModel class with the provided
+ * OpenAiAudioApi and options.
+ * @param audioApi The OpenAiAudioApi to use for speech synthesis.
+ * @param options The AudioOptions containing the speech synthesis
+ * options.
+ * @param retryTemplate The retry template.
+ */
+ public AudioModel(AudioApi audioApi, AudioOptions options,
+ RetryTemplate retryTemplate) {
+ Assert.notNull(audioApi, "OpenAiAudioApi must not be null");
+ Assert.notNull(options, "OpenAiSpeechOptions must not be null");
+ Assert.notNull(options, "RetryTemplate must not be null");
+ this.audioApi = audioApi;
+ this.defaultOptions = options;
+ this.retryTemplate = retryTemplate;
+ }
+
+
+ public byte[] call(String text) {
+ TextToSpeechPrompt prompt = new TextToSpeechPrompt(text);
+ return call(prompt).getResult().getOutput();
+ }
+
+ @Override
+ public TextToSpeechResponse call(TextToSpeechPrompt prompt) {
+
+ AudioApi.SpeechRequest speechRequest = createRequest(prompt);
+
+ ResponseEntity speechEntity = this.retryTemplate.execute(
+ (ctx) -> this.audioApi.createSpeech(speechRequest));
+
+ var speech = speechEntity.getBody();
+
+ return new TextToSpeechResponse(List.of(new Speech(speech)));
+ }
+
+
+
+ private AudioApi.SpeechRequest createRequest(TextToSpeechPrompt prompt) {
+ AudioOptions options = this.defaultOptions;
+
+ String input = StringUtils.hasText(options.getInput()) ? options.getInput()
+ : prompt.getInstructions().getText();
+
+ AudioApi.SpeechRequest.Builder requestBuilder = AudioApi.SpeechRequest.builder()
+ .model(options.getModel())
+ .input(input)
+ .voice(options.getVoice())
+ .responseFormat(options.getResponseFormat())
+ .speed(options.getSpeed());
+
+ return requestBuilder.build();
+ }
+
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioOptions.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioOptions.java
new file mode 100644
index 0000000..b10b083
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/audio/AudioOptions.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.audio;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import org.springframework.ai.audio.tts.TextToSpeechOptions;
+
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class AudioOptions implements TextToSpeechOptions {
+
+ @JsonProperty("model")
+ private String model;
+
+ @JsonProperty("input")
+ private String input;
+
+ @JsonProperty("voice")
+ private String voice;
+
+ /**
+ * The format of the audio output. Supported formats are mp3, opus, aac, and flac.
+ * Defaults to mp3.
+ */
+ @JsonProperty("response_format")
+ private String responseFormat;
+
+ /**
+ * The speed of the voice synthesis. The acceptable range is from 0.25 (slowest) to
+ * 4.0 (fastest). Defaults to 1 (normal)
+ */
+ @JsonProperty("speed")
+ private Double speed;
+ @JsonProperty("watermark_enabled")
+ private boolean watermark;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public String getFormat() {
+ return null;
+ }
+
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public AudioOptions copy() {
+ return AudioOptions.builder()
+ .model(this.model)
+ .input(this.input)
+ .voice(this.voice)
+ .responseFormat(this.responseFormat)
+ .speed(this.speed)
+ .build();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((this.model == null) ? 0 : this.model.hashCode());
+ result = prime * result + ((this.input == null) ? 0 : this.input.hashCode());
+ result = prime * result + ((this.voice == null) ? 0 : this.voice.hashCode());
+ result = prime * result + ((this.responseFormat == null) ? 0 : this.responseFormat.hashCode());
+ result = prime * result + ((this.speed == null) ? 0 : this.speed.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ AudioOptions other = (AudioOptions) obj;
+ if (this.model == null) {
+ if (other.model != null) {
+ return false;
+ }
+ }
+ else if (!this.model.equals(other.model)) {
+ return false;
+ }
+ if (this.input == null) {
+ if (other.input != null) {
+ return false;
+ }
+ }
+ else if (!this.input.equals(other.input)) {
+ return false;
+ }
+ if (this.voice == null) {
+ if (other.voice != null) {
+ return false;
+ }
+ }
+ else if (!this.voice.equals(other.voice)) {
+ return false;
+ }
+ if (this.responseFormat == null) {
+ if (other.responseFormat != null) {
+ return false;
+ }
+ }
+ else if (!this.responseFormat.equals(other.responseFormat)) {
+ return false;
+ }
+ if (this.speed == null) {
+ return other.speed == null;
+ }
+ else {
+ return this.speed.equals(other.speed);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "AudioOptions{" + "model='" + this.model + '\'' + ", input='" + this.input + '\''
+ + ", voice='" + this.voice + '\'' + ", responseFormat='" + this.responseFormat + '\'' + ", speed="
+ + this.speed + '}';
+ }
+
+ public static final class Builder {
+
+ private final AudioOptions options = new AudioOptions();
+
+ public Builder model(String model) {
+ this.options.model = model;
+ return this;
+ }
+
+ public Builder input(String input) {
+ this.options.input = input;
+ return this;
+ }
+
+
+ public Builder voice(String voice) {
+ this.options.voice = voice;
+ return this;
+ }
+
+ public Builder responseFormat(String responseFormat) {
+ this.options.responseFormat = responseFormat;
+ return this;
+ }
+
+ public Builder speed(Double speed) {
+ this.options.speed = speed;
+ return this;
+ }
+
+ public Builder watermark(boolean watermark) {
+ this.options.watermark = watermark;
+ return this;
+ }
+
+ public AudioOptions build() {
+ return this.options;
+ }
+
+ }
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageApi.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageApi.java
new file mode 100644
index 0000000..5a26734
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageApi.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.image;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.ai.model.ApiKey;
+import org.springframework.ai.model.NoopApiKey;
+import org.springframework.ai.model.SimpleApiKey;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.Assert;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+import xyz.thoughtset.viewer.models.doubao.constants.AIConstants;
+
+import java.util.List;
+
+
+public class DoubaoImageApi {
+
+
+ private final RestClient restClient;
+
+ private final String imagesPath;
+
+ public DoubaoImageApi(String baseUrl, ApiKey apiKey, HttpHeaders headers, String imagesPath,
+ RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
+
+ // @formatter:off
+ this.restClient = restClientBuilder.clone()
+ .baseUrl(baseUrl)
+ .defaultHeaders(h -> {
+ h.setContentType(MediaType.APPLICATION_JSON);
+ h.addAll(HttpHeaders.readOnlyHttpHeaders(headers));
+ })
+ .defaultStatusHandler(responseErrorHandler)
+ .defaultRequest(requestHeadersSpec -> {
+ if (!(apiKey instanceof NoopApiKey)) {
+ requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
+ }
+ })
+ .build();
+ // @formatter:on
+
+ this.imagesPath = imagesPath;
+ }
+
+
+ public DoubaoImageApi(RestClient restClient, String imagesPath) {
+ this.restClient = restClient;
+ this.imagesPath = imagesPath;
+ }
+
+ public ResponseEntity createImage(DoubaoImageRequest doubaoImageRequest) {
+ Assert.notNull(doubaoImageRequest, "Image request cannot be null.");
+ Assert.hasLength(doubaoImageRequest.prompt(), "Prompt cannot be empty.");
+
+ return this.restClient.post()
+ .uri(this.imagesPath)
+ .body(doubaoImageRequest)
+ .retrieve()
+ .toEntity(ImageResponse.class);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+
+
+ // @formatter:off
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record DoubaoImageRequest(
+ @JsonProperty("prompt") String prompt,
+ @JsonProperty("model") String model,
+ @JsonProperty("image") String image,
+ @JsonProperty("size") String size,
+ @JsonProperty("guidance_scale") Float guidanceScale,
+ @JsonProperty("response_format") String responseFormat,
+ @JsonProperty("watermark") Boolean watermark) {
+
+ public DoubaoImageRequest(String prompt) {
+ this(prompt, null, null, null, null, null, null);
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record ImageResponse(
+ @JsonProperty("created") Long created,
+ @JsonProperty("data") List data) {
+ }
+ // @formatter:on
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record Data(@JsonProperty("url") String url, @JsonProperty("b64_json") String b64Json,
+ @JsonProperty("revised_prompt") String revisedPrompt) {
+
+ }
+
+ /**
+ * Builder to construct {@link DoubaoImageApi} instance.
+ */
+ public static final class Builder {
+
+ private String baseUrl = AIConstants.DEFAULT_BASE_URL;
+
+ private ApiKey apiKey;
+
+ private HttpHeaders headers = new HttpHeaders();
+
+ private RestClient.Builder restClientBuilder = RestClient.builder();
+
+ private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER;
+
+ private String imagesPath = AIConstants.IMAGE_URL;
+
+ public Builder baseUrl(String baseUrl) {
+ Assert.hasText(baseUrl, "baseUrl cannot be null or empty");
+ this.baseUrl = baseUrl;
+ return this;
+ }
+
+
+ public Builder apiKey(String simpleApiKey) {
+ Assert.notNull(simpleApiKey, "simpleApiKey cannot be null");
+ this.apiKey = new SimpleApiKey(simpleApiKey);
+ return this;
+ }
+
+ public Builder headers(HttpHeaders headers) {
+ Assert.notNull(headers, "headers cannot be null");
+ this.headers = headers;
+ return this;
+ }
+
+ public Builder restClientBuilder(RestClient.Builder restClientBuilder) {
+ Assert.notNull(restClientBuilder, "restClientBuilder cannot be null");
+ this.restClientBuilder = restClientBuilder;
+ return this;
+ }
+
+ public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) {
+ Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null");
+ this.responseErrorHandler = responseErrorHandler;
+ return this;
+ }
+
+ public DoubaoImageApi build() {
+ Assert.notNull(this.apiKey, "apiKey must be set");
+ return new DoubaoImageApi(this.baseUrl, this.apiKey, this.headers, this.imagesPath, this.restClientBuilder,
+ this.responseErrorHandler);
+ }
+
+ }
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageModel.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageModel.java
new file mode 100644
index 0000000..1e6a9a3
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageModel.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.image;
+
+import io.micrometer.observation.ObservationRegistry;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.image.*;
+import org.springframework.ai.image.observation.DefaultImageModelObservationConvention;
+import org.springframework.ai.image.observation.ImageModelObservationContext;
+import org.springframework.ai.image.observation.ImageModelObservationConvention;
+import org.springframework.ai.image.observation.ImageModelObservationDocumentation;
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.openai.OpenAiImageOptions;
+import org.springframework.ai.openai.api.OpenAiImageApi;
+import org.springframework.ai.openai.api.common.OpenAiApiConstants;
+import org.springframework.ai.openai.metadata.OpenAiImageGenerationMetadata;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.http.ResponseEntity;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+
+import java.util.List;
+
+@Slf4j
+public class DoubaoImageModel implements ImageModel {
+
+
+ private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultImageModelObservationConvention();
+
+ /**
+ * The default options used for the image completion requests.
+ */
+ private final DoubaoImageOptions defaultOptions;
+
+ /**
+ * The retry template used to retry the OpenAI Image API calls.
+ */
+ private final RetryTemplate retryTemplate;
+
+ /**
+ * Low-level access to the OpenAI Image API.
+ */
+ private final DoubaoImageApi doubaoImageApi;
+
+ /**
+ * Observation registry used for instrumentation.
+ */
+ private final ObservationRegistry observationRegistry;
+
+ /**
+ * Conventions to use for generating observations.
+ */
+ private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
+
+
+ public DoubaoImageModel(DoubaoImageApi doubaoImageApi) {
+ this(doubaoImageApi, DoubaoImageOptions.builder().build(), RetryUtils.DEFAULT_RETRY_TEMPLATE);
+ }
+
+
+ public DoubaoImageModel(DoubaoImageApi doubaoImageApi, DoubaoImageOptions options, RetryTemplate retryTemplate) {
+ this(doubaoImageApi, options, retryTemplate, ObservationRegistry.NOOP);
+ }
+
+
+ public DoubaoImageModel(DoubaoImageApi doubaoImageApi, DoubaoImageOptions options, RetryTemplate retryTemplate,
+ ObservationRegistry observationRegistry) {
+ Assert.notNull(doubaoImageApi, "DoubaoImageApi must not be null");
+ Assert.notNull(options, "options must not be null");
+ Assert.notNull(retryTemplate, "retryTemplate must not be null");
+ Assert.notNull(observationRegistry, "observationRegistry must not be null");
+ this.doubaoImageApi = doubaoImageApi;
+ this.defaultOptions = options;
+ this.retryTemplate = retryTemplate;
+ this.observationRegistry = observationRegistry;
+ }
+
+ @Override
+ public ImageResponse call(ImagePrompt imagePrompt) {
+ DoubaoImageApi.DoubaoImageRequest imageRequest = this.createRequest(imagePrompt);
+ ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(imagePrompt).provider(OpenAiApiConstants.PROVIDER_NAME).build();
+ return (ImageResponse)ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> {
+ return observationContext;
+ }, this.observationRegistry).observe(() -> {
+ ResponseEntity imageResponseEntity = (ResponseEntity)this.retryTemplate.execute((ctx) ->
+ this.doubaoImageApi.createImage(imageRequest)
+ );
+ ImageResponse imageResponse = this.convertResponse(imageResponseEntity, imageRequest);
+ observationContext.setResponse(imageResponse);
+ return imageResponse;
+ });
+ }
+
+ private DoubaoImageApi.DoubaoImageRequest createRequest(ImagePrompt imagePrompt) {
+ String instructions = (imagePrompt.getInstructions().get(0)).getText();
+ DoubaoImageOptions imageOptions = this.defaultOptions;
+ DoubaoImageApi.DoubaoImageRequest imageRequest = new DoubaoImageApi.DoubaoImageRequest(instructions);
+ return ModelOptionsUtils.merge(imageOptions, imageRequest, DoubaoImageApi.DoubaoImageRequest.class);
+ }
+
+ private ImageResponse convertResponse(ResponseEntity imageResponseEntity,
+ DoubaoImageApi.DoubaoImageRequest openAiImageRequest) {
+ OpenAiImageApi.OpenAiImageResponse imageApiResponse = imageResponseEntity.getBody();
+ if (imageApiResponse == null) {
+ log.warn("No image response returned for request: {}", openAiImageRequest);
+ return new ImageResponse(List.of());
+ }
+
+ List imageGenerationList = imageApiResponse.data()
+ .stream()
+ .map(entry -> new ImageGeneration(new Image(entry.url(), entry.b64Json())))
+ .toList();
+
+ ImageResponseMetadata openAiImageResponseMetadata = new ImageResponseMetadata(imageApiResponse.created());
+ return new ImageResponse(imageGenerationList, openAiImageResponseMetadata);
+ }
+
+
+ /**
+ * Use the provided convention for reporting observation data
+ * @param observationConvention The provided convention
+ */
+ public void setObservationConvention(ImageModelObservationConvention observationConvention) {
+ Assert.notNull(observationConvention, "observationConvention cannot be null");
+ this.observationConvention = observationConvention;
+ }
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageOptions.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageOptions.java
new file mode 100644
index 0000000..08c5105
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/image/DoubaoImageOptions.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.image;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Data;
+import org.springframework.ai.image.ImageOptions;
+
+import java.util.Objects;
+
+@Builder
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DoubaoImageOptions implements ImageOptions {
+
+
+ @JsonProperty("n")
+ private Integer n;
+
+ /**
+ * The model to use for image generation.
+ */
+ @JsonProperty("model")
+ private String model;
+
+ /**
+ * The width of the generated images. Must be one of 256, 512, or 1024 for dall-e-2.
+ * This property is interconnected with the 'size' property - setting both width and
+ * height will automatically compute and set the size in "widthxheight" format.
+ * Conversely, setting a valid size string will parse and set the individual width and
+ * height values.
+ */
+ @JsonProperty("size_width")
+ private Integer width;
+
+ /**
+ * The height of the generated images. Must be one of 256, 512, or 1024 for dall-e-2.
+ * This property is interconnected with the 'size' property - setting both width and
+ * height will automatically compute and set the size in "widthxheight" format.
+ * Conversely, setting a valid size string will parse and set the individual width and
+ * height values.
+ */
+ @JsonProperty("size_height")
+ private Integer height;
+
+ /**
+ * The quality of the image that will be generated. hd creates images with finer
+ * details and greater consistency across the image. This param is only supported for
+ * dall-e-3.
+ */
+ @JsonProperty("style")
+ private String style;
+
+ /**
+ * The format in which the generated images are returned. Must be one of url or
+ * b64_json.
+ */
+ @JsonProperty("response_format")
+ private String responseFormat;
+
+
+ @JsonProperty("size")
+ private String size;
+
+ @JsonProperty("seed")
+ private Integer seed;
+
+ @JsonProperty("guidance_scale")
+ private Float guidanceScale;
+ @JsonProperty("watermark")
+ private Boolean watermark;
+ @JsonProperty("image")
+ private String image;
+
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoApi.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoApi.java
new file mode 100644
index 0000000..a590cc7
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoApi.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.video;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import org.springframework.ai.model.ApiKey;
+import org.springframework.ai.model.NoopApiKey;
+import org.springframework.ai.model.SimpleApiKey;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+import xyz.thoughtset.viewer.common.ai.model.video.ReqInfo;
+import xyz.thoughtset.viewer.models.doubao.constants.AIConstants;
+
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class VideoApi {
+
+ private final RestClient restClient;
+
+
+ /**
+ * Create a new audio api.
+ * @param baseUrl api base URL.
+ * @param apiKey OpenAI apiKey.
+ * @param headers the http headers to use.
+ * @param restClientBuilder RestClient builder.
+ * @param responseErrorHandler Response error handler.
+ */
+ public VideoApi(String baseUrl, ApiKey apiKey, HttpHeaders headers, RestClient.Builder restClientBuilder,
+ ResponseErrorHandler responseErrorHandler) {
+ if (!StringUtils.hasText(baseUrl)) {
+ baseUrl = AIConstants.DEFAULT_BASE_URL;
+ }
+ Consumer authHeaders = h -> h.addAll(HttpHeaders.readOnlyHttpHeaders(headers));
+
+ // @formatter:off
+ this.restClient = restClientBuilder.clone()
+ .baseUrl(baseUrl)
+ .defaultHeaders(authHeaders)
+ .defaultStatusHandler(responseErrorHandler)
+ .defaultRequest(requestHeadersSpec -> {
+ if (!(apiKey instanceof NoopApiKey)) {
+ requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
+ }
+ })
+ .build();
+ }
+
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Request to generates audio from the input text.
+ * @param requestBody The request body.
+ * @return Response entity containing the audio binary.
+ */
+ public ResponseEntity createReq(VideoRequest requestBody) {
+ return this.restClient.post().uri(requestBody.reqUrl()).body(requestBody).retrieve().toEntity(VideoResp.class);
+ }
+
+
+ @JsonInclude(Include.NON_NULL)
+ public record VideoRequest(
+ @JsonProperty("model") String model,
+ @JsonProperty("content") ReqTextContent[] content) implements ReqInfo {
+
+ @Override
+ public String reqUrl() {
+ return AIConstants.VIDEO_URL;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+
+ public static final class Builder {
+
+ private String model;
+
+ private ReqTextContent[] content;
+
+ public Builder model(String model) {
+ this.model = model;
+ return this;
+ }
+
+ public Builder prompt(ReqTextContent content) {
+ this.content = new ReqTextContent[]{content};
+ return this;
+ }
+
+ public VideoRequest build() {
+ return new VideoRequest(this.model, this.content);
+ }
+ }
+
+ }
+
+
+ @JsonInclude(Include.NON_NULL)
+ public record ReqTextContent(
+ @JsonProperty("type") String type,
+ @JsonProperty("text") String text
+ ){
+
+ public static Builder builder() {
+ return new Builder();
+ }
+ public static final class Builder {
+
+ private String type = "text";
+
+ private String text;
+
+ public Builder type(String type) {
+ this.type = type;
+ return this;
+ }
+ public Builder text(String text, Map optionsMap) {
+ StringBuilder stringBuilder =new StringBuilder();
+ stringBuilder.append(text);
+ if(!ObjectUtils.isEmpty(optionsMap)){
+ optionsMap.remove("model");
+ optionsMap.remove("prompt");
+ optionsMap.forEach((k,v)->{
+ stringBuilder.append(" --").append(k).append(" ").append(v);
+ });
+ }
+ this.text= stringBuilder.toString();
+ return this;
+ }
+ public ReqTextContent build() {
+ return new ReqTextContent(this.type, this.text);
+ }
+
+
+ }
+
+ }
+
+
+ @JsonInclude(Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record VideoResp(
+ @JsonProperty("id") String id) {
+
+ }
+
+
+
+ public static final class Builder {
+
+ private String baseUrl = AIConstants.DEFAULT_BASE_URL;
+
+ private ApiKey apiKey;
+
+ private HttpHeaders headers = new HttpHeaders();
+
+ private RestClient.Builder restClientBuilder = RestClient.builder();
+
+ private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER;
+
+ public Builder baseUrl(String baseUrl) {
+ Assert.hasText(baseUrl, "baseUrl cannot be null or empty");
+ this.baseUrl = baseUrl;
+ return this;
+ }
+
+ public Builder apiKey(ApiKey apiKey) {
+ Assert.notNull(apiKey, "apiKey cannot be null");
+ this.apiKey = apiKey;
+ return this;
+ }
+
+ public Builder apiKey(String simpleApiKey) {
+ Assert.notNull(simpleApiKey, "simpleApiKey cannot be null");
+ this.apiKey = new SimpleApiKey(simpleApiKey);
+ return this;
+ }
+
+ public Builder headers(HttpHeaders headers) {
+ Assert.notNull(headers, "headers cannot be null");
+ this.headers = headers;
+ return this;
+ }
+
+ public Builder restClientBuilder(RestClient.Builder restClientBuilder) {
+ Assert.notNull(restClientBuilder, "restClientBuilder cannot be null");
+ this.restClientBuilder = restClientBuilder;
+ return this;
+ }
+
+ public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) {
+ Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null");
+ this.responseErrorHandler = responseErrorHandler;
+ return this;
+ }
+
+ public VideoApi build() {
+ Assert.notNull(this.apiKey, "apiKey must be set");
+ return new VideoApi(this.baseUrl, this.apiKey, this.headers, this.restClientBuilder,
+ this.responseErrorHandler);
+ }
+
+ }
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoModel.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoModel.java
new file mode 100644
index 0000000..aa6324d
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoModel.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.video;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.model.Model;
+import org.springframework.http.ResponseEntity;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import xyz.thoughtset.viewer.common.ai.model.video.AsyncVideo;
+import xyz.thoughtset.viewer.common.ai.model.video.VideoPrompt;
+
+import java.util.Map;
+
+@Slf4j
+public class VideoModel implements Model {
+
+ /**
+ * The default options used for the audio completion requests.
+ */
+ private final VideoOptions defaultOptions;
+
+ /**
+ * The retry template used to retry the OpenAI Audio API calls.
+ */
+ private final RetryTemplate retryTemplate;
+
+ /**
+ * Low-level access to the OpenAI Audio API.
+ */
+ private final VideoApi videoApi;
+ private final ObjectMapper objectMapper;
+
+
+
+ public VideoModel(VideoApi videoApi, VideoOptions options,
+ RetryTemplate retryTemplate, ObjectMapper objectMapper) {
+ Assert.notNull(videoApi, "OpenAiAudioApi must not be null");
+ Assert.notNull(options, "OpenAiSpeechOptions must not be null");
+ Assert.notNull(options, "RetryTemplate must not be null");
+ this.videoApi = videoApi;
+ this.defaultOptions = options;
+ this.retryTemplate = retryTemplate;
+ this.objectMapper = objectMapper;
+ }
+
+ public String call(String text) {
+ VideoPrompt prompt = new VideoPrompt(text);
+ return call(prompt).getResult().getOutput();
+ }
+
+
+ @Override
+ public VideoResponse call(VideoPrompt prompt) {
+
+ VideoApi.VideoRequest speechRequest = createRequest(prompt);
+
+ ResponseEntity respEntity = this.retryTemplate.execute(
+ (ctx) -> this.videoApi.createReq(speechRequest));
+
+ return new VideoResponse(new AsyncVideo(respEntity.getBody().id()));
+ }
+
+
+
+ private VideoApi.VideoRequest createRequest(VideoPrompt prompt) {
+ VideoOptions options = this.defaultOptions;
+
+ String input = StringUtils.hasText(options.getPrompt()) ? options.getPrompt()
+ : prompt.getInstructions();
+ VideoApi.ReqTextContent content = VideoApi.ReqTextContent.builder()
+ .text(input,objectMapper.convertValue(options, Map.class))
+ .build();
+ return VideoApi.VideoRequest.builder()
+ .model(options.getModel())
+ .prompt(content)
+ .build();
+ }
+
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoOptions.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoOptions.java
new file mode 100644
index 0000000..53b1deb
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoOptions.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License; Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing; software
+ * distributed under the License is distributed on an "AS IS" BASIS;
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND; either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.video;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+import xyz.thoughtset.viewer.common.ai.model.video._BaseVideoOptions;
+
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class VideoOptions implements _BaseVideoOptions {
+
+ protected String model;
+ protected String prompt;
+ protected String resolution;
+ protected String ratio;
+ protected Integer duration;
+ protected Integer frames;
+ protected Integer seed;
+// protected Boolean withAudio;
+ protected Boolean watermark;
+
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoResponse.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoResponse.java
new file mode 100644
index 0000000..55b8cc4
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/api/video/VideoResponse.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package xyz.thoughtset.viewer.models.doubao.api.video;
+
+import org.springframework.ai.model.ModelResponse;
+import xyz.thoughtset.viewer.common.ai.model.video.AsyncVideo;
+import xyz.thoughtset.viewer.common.ai.model.video.VideoResponseMetadata;
+
+import java.util.List;
+import java.util.Objects;
+
+
+public class VideoResponse implements ModelResponse {
+
+ private final AsyncVideo result;
+
+ private final VideoResponseMetadata videoResponseMetadata;
+
+ public VideoResponse(AsyncVideo result) {
+ this(result, null);
+ }
+
+ public VideoResponse(AsyncVideo result, VideoResponseMetadata videoResponseMetadata) {
+ this.result = result;
+ this.videoResponseMetadata = videoResponseMetadata;
+ }
+
+ @Override
+ public List getResults() {
+ return List.of(this.result);
+ }
+
+ public AsyncVideo getResult() {
+ return this.result;
+ }
+
+ @Override
+ public VideoResponseMetadata getMetadata() {
+ return this.videoResponseMetadata;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof VideoResponse that)) {
+ return false;
+ }
+ return Objects.equals(this.result, that.result);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.result);
+ }
+
+ @Override
+ public String toString() {
+ return "VideoResponseMetadata{" + "results=" + this.result + '}';
+ }
+
+}
diff --git a/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/constants/AIConstants.java b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/constants/AIConstants.java
new file mode 100644
index 0000000..2ea0619
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/java/xyz/thoughtset/viewer/models/doubao/constants/AIConstants.java
@@ -0,0 +1,13 @@
+package xyz.thoughtset.viewer.models.doubao.constants;
+
+public class AIConstants {
+ public static final String MODEL_NAME = "Doubao";
+ public static final String DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
+ public static final String CHAT_URL = "/v4/audio/speech";
+ public static final String TTS_URL = "/v4/audio/speech";
+ public static final String IMAGE_URL = "/images/generations";
+ public static final String ASR_URL = "/v4/audio/transcriptions";
+ public static final String CLONE_URL = "/v4/voice/clone";
+ public static final String VIDEO_URL = "/contents/generations/tasks";
+
+}
diff --git a/models/viewer-models-doubao/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/models/viewer-models-doubao/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..dcc8b6f
--- /dev/null
+++ b/models/viewer-models-doubao/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+xyz.thoughtset.viewer.models.doubao.ModelDoubaoAutoConfiguration
\ No newline at end of file
diff --git a/models/viewer-models-gemini/pom.xml b/models/viewer-models-gemini/pom.xml
new file mode 100644
index 0000000..ab5a599
--- /dev/null
+++ b/models/viewer-models-gemini/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+
+ xyz.thoughtset.viewer
+ models
+ ${revision}
+
+
+ viewer-models-gemini
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+ org.springframework.ai
+ spring-ai-google-genai
+ ${springai.version}
+
+
+
+
\ No newline at end of file
diff --git a/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/AIConstants.java b/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/AIConstants.java
new file mode 100644
index 0000000..b042c1e
--- /dev/null
+++ b/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/AIConstants.java
@@ -0,0 +1,7 @@
+package xyz.thoughtset.viewer.models.gemini;
+
+public class AIConstants {
+ public static final String PROVIDER_NAME = "Gemini";
+ public static final String DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
+
+}
diff --git a/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/GeminiChatBuilder.java b/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/GeminiChatBuilder.java
new file mode 100644
index 0000000..2fcb95f
--- /dev/null
+++ b/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/GeminiChatBuilder.java
@@ -0,0 +1,56 @@
+package xyz.thoughtset.viewer.models.gemini;
+
+import com.google.genai.Client;
+import com.google.genai.HttpApiClient;
+import com.google.genai.types.HttpOptions;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.google.genai.GoogleGenAiChatModel;
+import org.springframework.ai.google.genai.GoogleGenAiChatOptions;
+import org.springframework.ai.model.ModelOptions;
+import org.springframework.stereotype.Component;
+import xyz.thoughtset.viewer.common.ai.model.entity.AiNode;
+import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam;
+import xyz.thoughtset.viewer.common.ai.model.entity.option.ExtraParamChatOptions;
+import xyz.thoughtset.viewer.common.ai.model.factory.ChatModelBuilder;
+
+@Component
+public class GeminiChatBuilder extends ChatModelBuilder {
+ public GeminiChatBuilder() {
+ super(AIConstants.PROVIDER_NAME);
+ }
+
+
+ @Override
+ public ChatModel buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) {
+ GoogleGenAiChatOptions options = loadAndMergeModelOptions(modelParam, GoogleGenAiChatOptions.class, modelOptions);
+ options.setModel(modelParam.getModel());
+
+ HttpOptions.Builder httpOptionsBuilder = HttpOptions.builder().baseUrl(node.linkBaseUrlWithDefault(AIConstants.DEFAULT_BASE_URL));
+
+ Client api = Client.builder()
+ .httpOptions(httpOptionsBuilder.build())
+ .apiKey(node.getApiKey()).build();
+ GoogleGenAiChatModel chatModel = GoogleGenAiChatModel.builder()
+ .defaultOptions(options)
+ .genAiClient(api)
+ .build();
+ return chatModel;
+ }
+
+ @Override
+ public void mergeOptions(ModelOptions source, ModelOptions extraOptions) {
+ if (!(extraOptions instanceof ExtraParamChatOptions)) return ;
+ GoogleGenAiChatOptions options = (GoogleGenAiChatOptions) source;
+ ExtraParamChatOptions extraParamChatOptions = (ExtraParamChatOptions) extraOptions;
+ if (extraParamChatOptions == null) return ;
+ if (extraParamChatOptions.getResponseFormatType() != null) {
+ options.setResponseMimeType(
+ ("application/json").equalsIgnoreCase(extraParamChatOptions.getResponseFormatType()) ?
+ "application/json" : "text/plain"
+ );
+ }
+// if (extraParamChatOptions.responseUsedJsonObject()){
+// options.setResponseMimeType(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build());
+// }
+ }
+}
diff --git a/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/ModelGeminiAutoConfiguration.java b/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/ModelGeminiAutoConfiguration.java
new file mode 100644
index 0000000..f6cce98
--- /dev/null
+++ b/models/viewer-models-gemini/src/main/java/xyz/thoughtset/viewer/models/gemini/ModelGeminiAutoConfiguration.java
@@ -0,0 +1,11 @@
+package xyz.thoughtset.viewer.models.gemini;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan
+@EnableConfigurationProperties
+public class ModelGeminiAutoConfiguration {
+}
diff --git a/models/viewer-models-gemini/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/models/viewer-models-gemini/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..fe144c7
--- /dev/null
+++ b/models/viewer-models-gemini/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+xyz.thoughtset.viewer.models.gemini.ModelGeminiAutoConfiguration
\ No newline at end of file
diff --git a/modules/viewer-modules-ds/viewer-modules-ds-jdbc/pom.xml b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/pom.xml
index 5b53123..a3756d6 100644
--- a/modules/viewer-modules-ds/viewer-modules-ds-jdbc/pom.xml
+++ b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/pom.xml
@@ -15,6 +15,8 @@
viewer-modules-ds-jdbc-mysql
viewer-modules-ds-jdbc-self
viewer-modules-ds-jdbc-core
+ viewer-modules-ds-jdbc-drill
+ viewer-modules-ds-jdbc-pgsql
diff --git a/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-drill/pom.xml b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-drill/pom.xml
new file mode 100644
index 0000000..5098f19
--- /dev/null
+++ b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-drill/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+ xyz.thoughtset.viewer
+ viewer-modules-ds-jdbc
+ ${revision}
+
+
+ viewer-modules-ds-jdbc-drill
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+ xyz.thoughtset.viewer
+ viewer-modules-ds-jdbc-core
+
+
+
+ com.mysql
+ mysql-connector-j
+ provided
+
+
+
+
\ No newline at end of file
diff --git a/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/pom.xml b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/pom.xml
new file mode 100644
index 0000000..84a91f0
--- /dev/null
+++ b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+
+ xyz.thoughtset.viewer
+ viewer-modules-ds-jdbc
+ ${revision}
+
+
+ viewer-modules-ds-jdbc-pgsql
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+ xyz.thoughtset.viewer
+ viewer-modules-ds-jdbc-core
+
+
+
+ org.postgresql
+ postgresql
+ provided
+
+
+
+
\ No newline at end of file
diff --git a/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/java/xyz/thoughtset/viewer/modules/ds/jdbc/pgsql/DsJdbcPgsqlAutoConfiguration.java b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/java/xyz/thoughtset/viewer/modules/ds/jdbc/pgsql/DsJdbcPgsqlAutoConfiguration.java
new file mode 100644
index 0000000..6097661
--- /dev/null
+++ b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/java/xyz/thoughtset/viewer/modules/ds/jdbc/pgsql/DsJdbcPgsqlAutoConfiguration.java
@@ -0,0 +1,11 @@
+package xyz.thoughtset.viewer.modules.ds.jdbc.pgsql;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan
+@EnableConfigurationProperties
+public class DsJdbcPgsqlAutoConfiguration {
+}
diff --git a/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/java/xyz/thoughtset/viewer/modules/ds/jdbc/pgsql/config/PgsqlDsConfig.java b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/java/xyz/thoughtset/viewer/modules/ds/jdbc/pgsql/config/PgsqlDsConfig.java
new file mode 100644
index 0000000..1431867
--- /dev/null
+++ b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/java/xyz/thoughtset/viewer/modules/ds/jdbc/pgsql/config/PgsqlDsConfig.java
@@ -0,0 +1,22 @@
+package xyz.thoughtset.viewer.modules.ds.jdbc.pgsql.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import xyz.thoughtset.viewer.common.connector.entity.bo.Linker;
+import xyz.thoughtset.viewer.modules.ds.jdbc.core.constant.SupportTypeConstant;
+import xyz.thoughtset.viewer.modules.ds.jdbc.core.linker.JdbcConnBuilder;
+
+@Configuration
+public class PgsqlDsConfig {
+
+ @Bean
+ public JdbcConnBuilder pgsqlConnBuilder() {
+ Linker dataLinker = new Linker(
+ "postgresql",
+ "org.postgresql.Driver",
+ SupportTypeConstant.SUPPORT_TYPE
+ );
+ return new JdbcConnBuilder(dataLinker);
+ }
+
+}
diff --git a/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..a98c5e6
--- /dev/null
+++ b/modules/viewer-modules-ds/viewer-modules-ds-jdbc/viewer-modules-ds-jdbc-pgsql/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+xyz.thoughtset.viewer.modules.ds.jdbc.pgsql.DsJdbcPgsqlAutoConfiguration
\ No newline at end of file
diff --git a/modules/viewer-modules-step/src/main/java/xyz/thoughtset/viewer/modules/step/entity/block/BlockBodyEle.java b/modules/viewer-modules-step/src/main/java/xyz/thoughtset/viewer/modules/step/entity/block/BlockBodyEle.java
index a4241ad..00ea748 100644
--- a/modules/viewer-modules-step/src/main/java/xyz/thoughtset/viewer/modules/step/entity/block/BlockBodyEle.java
+++ b/modules/viewer-modules-step/src/main/java/xyz/thoughtset/viewer/modules/step/entity/block/BlockBodyEle.java
@@ -39,5 +39,11 @@ public class BlockBodyEle extends BaseMeta {
public boolean eleWasFun() {
return "funInfo".equalsIgnoreCase(type);
}
+ public boolean eleWasTool() {
+ return "tool".equalsIgnoreCase(type);
+ }
+ public boolean eleWasScript() {
+ return "script".equalsIgnoreCase(type);
+ }
}
diff --git a/pom.xml b/pom.xml
index 2925ac1..b27e321 100644
--- a/pom.xml
+++ b/pom.xml
@@ -246,6 +246,11 @@
viewer-modules-ds-jdbc-mysql
${revision}
+
+ xyz.thoughtset.viewer
+ viewer-modules-ds-jdbc-pgsql
+ ${revision}
+
xyz.thoughtset.viewer
viewer-modules-ds-jdbc-self
@@ -323,7 +328,16 @@
viewer-models-openai
${revision}
-
+
+ xyz.thoughtset.viewer
+ viewer-models-gemini
+ ${revision}
+
+
+ xyz.thoughtset.viewer
+ viewer-models-doubao
+ ${revision}
+
xyz.thoughtset.viewer
viewer-models-zhipuai
diff --git a/sql/mysql/dv.sql b/sql/mysql/dv.sql
index ff94ce4..2602dd0 100644
--- a/sql/mysql/dv.sql
+++ b/sql/mysql/dv.sql
@@ -367,10 +367,6 @@ CREATE TABLE `modelparam` (
`updatedAt` datetime(0) NOT NULL,
`statesCode` int(11) NULL DEFAULT 200,
`model` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
- `systemPrompt` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
- `maxMemory` int(11) NULL DEFAULT NULL,
- `maxTokens` bigint(20) NULL DEFAULT NULL,
- `temperature` double NULL DEFAULT NULL,
`paramJson` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
`purpose` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`setting` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
--
Gitee