Compare commits
11 Commits
07d8193e80
...
b6213d0933
| Author | SHA1 | Date | |
|---|---|---|---|
| b6213d0933 | |||
| 22ceabff96 | |||
| f49d94e2fe | |||
| aaf4c61ff8 | |||
| b777cb3641 | |||
| c78db961c5 | |||
| da536ea742 | |||
| 799174406e | |||
| 6e1bd73cd8 | |||
| d510034abb | |||
| b1a16ccf18 |
17
README.md
17
README.md
@@ -28,9 +28,14 @@ EasyFlow 是一个面向企业场景的 Java AI 应用开发平台,提供智
|
||||
## 快速启动(开发环境)
|
||||
|
||||
### 1. 初始化数据库
|
||||
在 MySQL 中导入:
|
||||
- `sql/01-easyflow-v2.ddl.sql`
|
||||
- `sql/02-easyflow-v2.data.sql`
|
||||
只需要提前创建空库,例如:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE easyflow CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
```
|
||||
|
||||
表结构、Quartz 表和基础数据会在后端启动时由 Flyway 自动迁移完成。
|
||||
当前初始化脚本仅保留 3 段:`V1__quartz.sql`、`V2__easyflow_schema.sql`、`V3__easyflow_seed.sql`。
|
||||
|
||||
### 2. 启动后端
|
||||
在项目根目录执行:
|
||||
@@ -42,6 +47,8 @@ java -jar easyflow-starter/easyflow-starter-all/target/easyflow-starter-all-0.0.
|
||||
|
||||
默认端口:`8111`(见 `easyflow-starter/easyflow-starter-all/src/main/resources/application.yml`)。
|
||||
|
||||
首次连接空库时,应用会自动执行 `db/migration` 下的迁移脚本并初始化默认管理员、菜单、角色和模型服务商数据。
|
||||
|
||||
### 3. 启动前端
|
||||
管理后台:
|
||||
|
||||
@@ -59,7 +66,7 @@ pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
默认测试账号:`admin / 123456`
|
||||
默认测试账号:`admin / Easy@2026`
|
||||
|
||||
## 后端 Jar 包构建与部署
|
||||
|
||||
@@ -78,6 +85,8 @@ mvn -DskipTests -Dmaven.javadoc.skip=true clean package
|
||||
java -jar easyflow-starter/easyflow-starter-all/target/easyflow-starter-all-0.0.1.jar --spring.profiles.active=prod
|
||||
```
|
||||
|
||||
生产环境同样只需要保证目标数据库已创建为空库,Flyway 会在应用启动时自动完成迁移。
|
||||
|
||||
可通过环境变量覆盖关键配置(示例):
|
||||
|
||||
```bash
|
||||
|
||||
@@ -13,18 +13,17 @@ services:
|
||||
- --max_connections=500
|
||||
- --sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-easyflow}
|
||||
MYSQL_USER: ${MYSQL_USER:-easyflow}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-123456}
|
||||
TZ: Asia/Shanghai
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: easyflow
|
||||
MYSQL_USER: easyflow
|
||||
MYSQL_PASSWORD: "123456"
|
||||
ports:
|
||||
- "${MYSQL_PORT:-3306}:3306"
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- ./data/mysql:/var/lib/mysql
|
||||
- ./easyflow/sql/initdb:/docker-entrypoint-initdb.d:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -34,13 +33,13 @@ services:
|
||||
image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/kubesphere/redis:7.2.4-alpine-linuxarm64
|
||||
container_name: easyflow-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-123456}"]
|
||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "123456"]
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-123456}", "ping"]
|
||||
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -51,7 +50,7 @@ services:
|
||||
container_name: easyflow-etcd
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
TZ: Asia/Shanghai
|
||||
ETCD_AUTO_COMPACTION_MODE: revision
|
||||
ETCD_AUTO_COMPACTION_RETENTION: "1000"
|
||||
ETCD_QUOTA_BACKEND_BYTES: "4294967296"
|
||||
@@ -70,12 +69,12 @@ services:
|
||||
restart: unless-stopped
|
||||
command: server /data --address ":9000" --console-address ":9001"
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-easyflowadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-easyflowadmin123}
|
||||
TZ: Asia/Shanghai
|
||||
MINIO_ROOT_USER: easyflowadmin
|
||||
MINIO_ROOT_PASSWORD: easyflowadmin123
|
||||
ports:
|
||||
- "${MINIO_PORT:-9000}:9000"
|
||||
- "${MINIO_CONSOLE_PORT:-9001}:9001"
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./data/minio:/data
|
||||
|
||||
@@ -86,12 +85,12 @@ services:
|
||||
depends_on:
|
||||
- minio
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-easyflowadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-easyflowadmin123}
|
||||
MINIO_ENDPOINT: ${MINIO_ENDPOINT:-http://minio:9000}
|
||||
MINIO_BUCKETS: ${MINIO_BUCKETS:-easyflow,milvus}
|
||||
MINIO_PUBLIC_BUCKETS: ${MINIO_PUBLIC_BUCKETS:-easyflow}
|
||||
MINIO_ALIAS: ${MINIO_ALIAS:-local}
|
||||
MINIO_ROOT_USER: easyflowadmin
|
||||
MINIO_ROOT_PASSWORD: easyflowadmin123
|
||||
MINIO_ENDPOINT: http://minio:9000
|
||||
MINIO_BUCKETS: easyflow,milvus
|
||||
MINIO_PUBLIC_BUCKETS: easyflow
|
||||
MINIO_ALIAS: local
|
||||
volumes:
|
||||
- ./scripts/minio/init-minio.sh:/scripts/init-minio.sh:ro
|
||||
entrypoint: ["/bin/sh", "/scripts/init-minio.sh"]
|
||||
@@ -106,11 +105,11 @@ services:
|
||||
environment:
|
||||
ETCD_ENDPOINTS: etcd:2379
|
||||
COMMON_STORAGETYPE: minio
|
||||
MINIO_ADDRESS: ${MILVUS_MINIO_ADDRESS:-minio:9000}
|
||||
MINIO_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-easyflowadmin}
|
||||
MINIO_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:-easyflowadmin123}
|
||||
MINIO_ADDRESS: minio:9000
|
||||
MINIO_ACCESS_KEY_ID: easyflowadmin
|
||||
MINIO_SECRET_ACCESS_KEY: easyflowadmin123
|
||||
MINIO_USE_SSL: "false"
|
||||
MINIO_BUCKET_NAME: ${MILVUS_MINIO_BUCKET:-milvus}
|
||||
MINIO_BUCKET_NAME: milvus
|
||||
depends_on:
|
||||
etcd:
|
||||
condition: service_started
|
||||
@@ -119,7 +118,7 @@ services:
|
||||
minio-init:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "${MILVUS_GRPC_PORT:-19530}:19530"
|
||||
- "${MILVUS_HTTP_PORT:-9091}:9091"
|
||||
- "19530:19530"
|
||||
- "9091:9091"
|
||||
volumes:
|
||||
- ./data/milvus:/var/lib/milvus
|
||||
|
||||
@@ -61,10 +61,6 @@ services:
|
||||
MYSQL_DATABASE: easyflow
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- ./sql:/docker-entrypoint-initdb.d
|
||||
# 这个命令在默认 entrypoint 运行前修复权限
|
||||
entrypoint: sh -c "chown -R mysql:mysql /docker-entrypoint-initdb.d && docker-entrypoint.sh mysqld"
|
||||
networks:
|
||||
- easyflow-net
|
||||
healthcheck:
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.ai.entity.BotCategory;
|
||||
import tech.easyflow.ai.service.BotCategoryService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* bot分类 控制层。
|
||||
@@ -17,7 +26,24 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/v1/botCategory")
|
||||
@UsePermission(moduleName = "/api/v1/bot")
|
||||
public class BotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public BotCategoryController(BotCategoryService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
@GetMapping("visibleList")
|
||||
public Result<List<BotCategory>> visibleList(BotCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
|
||||
if (access.isRestricted()) {
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
queryWrapper.in(BotCategory::getId, access.getCategoryIds());
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.easyagents.core.model.chat.ChatModel;
|
||||
import com.easyagents.core.model.chat.ChatOptions;
|
||||
import com.alicp.jetcache.Cache;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -25,6 +27,8 @@ import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
@@ -34,6 +38,8 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.BotTableDef.BOT;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
*
|
||||
@@ -55,6 +61,8 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
private Cache<String, Object> cache;
|
||||
@Resource
|
||||
private AudioServiceManager audioServiceManager;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
||||
@@ -164,7 +172,11 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
@GetMapping("getDetail")
|
||||
@SaIgnore
|
||||
public Result<Bot> getDetail(String id) {
|
||||
return Result.ok(botService.getDetail(id));
|
||||
Bot bot = botService.getDetail(id);
|
||||
if (bot != null && StpUtil.isLogin()) {
|
||||
categoryPermissionService.assertCategoryResourceVisible("BOT", bot.getCreatedBy(), bot.getCategoryId(), "无权限访问聊天助手");
|
||||
}
|
||||
return Result.ok(bot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -174,6 +186,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
if (data == null) {
|
||||
return Result.ok(data);
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
categoryPermissionService.assertCategoryResourceVisible("BOT", data.getCreatedBy(), data.getCategoryId(), "无权限访问聊天助手");
|
||||
}
|
||||
|
||||
Map<String, Object> llmOptions = data.getModelOptions();
|
||||
if (llmOptions == null) {
|
||||
@@ -205,6 +220,32 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
applyCategoryPermission(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
|
||||
if (!access.isRestricted()) {
|
||||
return;
|
||||
}
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.eq(Bot::getCreatedBy, access.getAccountId());
|
||||
return;
|
||||
}
|
||||
queryWrapper.and(BOT.CREATED_BY.eq(access.getAccountId()).or(BOT.CATEGORY_ID.in(access.getCategoryIds())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot;
|
||||
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
@@ -11,8 +15,13 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -25,6 +34,13 @@ import java.util.List;
|
||||
@RequestMapping("/api/v1/botKnowledge")
|
||||
@UsePermission(moduleName = "/api/v1/bot")
|
||||
public class BotDocumentCollectionController extends BaseCurdController<BotDocumentCollectionService, BotDocumentCollection> {
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
public BotDocumentCollectionController(BotDocumentCollectionService service) {
|
||||
super(service);
|
||||
}
|
||||
@@ -35,11 +51,31 @@ public class BotDocumentCollectionController extends BaseCurdController<BotDocum
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
List<BotDocumentCollection> botDocumentCollections = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
return Result.ok(botDocumentCollections);
|
||||
List<BotDocumentCollection> visibleList = new ArrayList<>();
|
||||
KnowledgeReadAccessSnapshot snapshot = knowledgeVisibilityQueryHelper.getCurrentReadSnapshot();
|
||||
for (BotDocumentCollection relation : botDocumentCollections) {
|
||||
DocumentCollection knowledge = relation.getKnowledge();
|
||||
if (knowledge == null || knowledgeVisibilityQueryHelper.canRead(knowledge, snapshot)) {
|
||||
visibleList.add(relation);
|
||||
}
|
||||
}
|
||||
return Result.ok(visibleList);
|
||||
}
|
||||
|
||||
@PostMapping("updateBotKnowledgeIds")
|
||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("knowledgeIds") BigInteger [] knowledgeIds) {
|
||||
if (knowledgeIds != null) {
|
||||
for (BigInteger knowledgeId : knowledgeIds) {
|
||||
if (knowledgeId == null) {
|
||||
continue;
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(knowledgeId);
|
||||
if (collection == null) {
|
||||
continue;
|
||||
}
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.READ, "无权限绑定知识库");
|
||||
}
|
||||
}
|
||||
service.saveBotAndKnowledge(botId, knowledgeIds);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@@ -3,19 +3,25 @@ package tech.easyflow.admin.controller.ai;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.BotPlugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.ai.service.BotPluginService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -35,6 +41,12 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
|
||||
@Resource
|
||||
private BotPluginService botPluginService;
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
@Resource
|
||||
private PluginService pluginService;
|
||||
@Resource
|
||||
private PluginVisibilityService pluginVisibilityService;
|
||||
|
||||
@GetMapping("list")
|
||||
public Result<List<BotPlugin>> list(BotPlugin entity, Boolean asTree, String sortKey, String sortType){
|
||||
@@ -43,15 +55,29 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
|
||||
List<BotPlugin> botPlugins = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
List<BotPlugin> visibleList = new ArrayList<>();
|
||||
for (BotPlugin relation : botPlugins) {
|
||||
Plugin plugin = relation.getAiPlugin();
|
||||
if (plugin == null || pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
visibleList.add(relation);
|
||||
}
|
||||
}
|
||||
|
||||
List<BotPlugin> list = Tree.tryToTree(botPlugins, asTree);
|
||||
List<BotPlugin> list = Tree.tryToTree(visibleList, asTree);
|
||||
|
||||
return Result.ok(list);
|
||||
}
|
||||
|
||||
@PostMapping("/getList")
|
||||
public Result<List<Plugin>> getList(@JsonBody(value = "botId", required = true) String botId){
|
||||
return Result.ok(botPluginService.getList(botId));
|
||||
List<Plugin> plugins = botPluginService.getList(botId);
|
||||
List<Plugin> visibleList = new ArrayList<>();
|
||||
for (Plugin plugin : plugins) {
|
||||
if (plugin == null || pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
visibleList.add(plugin);
|
||||
}
|
||||
}
|
||||
return Result.ok(visibleList);
|
||||
}
|
||||
|
||||
@PostMapping("/getBotPluginToolIds")
|
||||
@@ -67,6 +93,23 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
|
||||
@PostMapping("updateBotPluginToolIds")
|
||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("pluginToolIds") BigInteger [] pluginToolIds) {
|
||||
if (pluginToolIds != null) {
|
||||
for (BigInteger pluginToolId : pluginToolIds) {
|
||||
if (pluginToolId == null) {
|
||||
continue;
|
||||
}
|
||||
PluginItem pluginItem = pluginItemService.getById(pluginToolId);
|
||||
if (pluginItem == null) {
|
||||
continue;
|
||||
}
|
||||
if (pluginItem.getPluginId() != null) {
|
||||
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||
if (plugin != null) {
|
||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定插件");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
service.saveBotAndPluginTool(botId, pluginToolIds);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.ai.entity.BotWorkflow;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.permission.WorkflowReadAccessSnapshot;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
@@ -12,10 +16,16 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
*
|
||||
@@ -26,6 +36,13 @@ import java.util.List;
|
||||
@RequestMapping("/api/v1/botWorkflow")
|
||||
@UsePermission(moduleName = "/api/v1/bot")
|
||||
public class BotWorkflowController extends BaseCurdController<BotWorkflowService, BotWorkflow> {
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
public BotWorkflowController(BotWorkflowService service) {
|
||||
super(service);
|
||||
}
|
||||
@@ -36,12 +53,32 @@ public class BotWorkflowController extends BaseCurdController<BotWorkflowService
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
List<BotWorkflow> botWorkflows = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
List<BotWorkflow> list = Tree.tryToTree(botWorkflows, asTree);
|
||||
List<BotWorkflow> visibleList = new ArrayList<>();
|
||||
WorkflowReadAccessSnapshot snapshot = workflowVisibilityQueryHelper.getCurrentReadSnapshot();
|
||||
for (BotWorkflow botWorkflow : botWorkflows) {
|
||||
Workflow workflow = botWorkflow.getWorkflow();
|
||||
if (workflow == null || workflowVisibilityQueryHelper.canRead(workflow, snapshot)) {
|
||||
visibleList.add(botWorkflow);
|
||||
}
|
||||
}
|
||||
List<BotWorkflow> list = Tree.tryToTree(visibleList, asTree);
|
||||
return Result.ok(list);
|
||||
}
|
||||
|
||||
@PostMapping("updateBotWorkflowIds")
|
||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("workflowIds") BigInteger [] workflowIds) {
|
||||
if (workflowIds != null) {
|
||||
for (BigInteger workflowId : workflowIds) {
|
||||
if (workflowId == null) {
|
||||
continue;
|
||||
}
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
continue;
|
||||
}
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.READ, "无权限绑定工作流");
|
||||
}
|
||||
}
|
||||
service.saveBotAndWorkflowTool(botId, workflowIds);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
@@ -16,9 +17,15 @@ import com.easyagents.core.document.Document;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
@@ -51,8 +58,29 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
||||
super(service);
|
||||
}
|
||||
|
||||
@GetMapping("page")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.DOCUMENT_ID,
|
||||
idExpr = "#request.getParameter('documentId')",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
@Override
|
||||
public Result<Page<DocumentChunk>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||
return super.page(request, sortKey, sortType, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.DOCUMENT_CHUNK_ID,
|
||||
idExpr = "#documentChunk.id",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> update(@JsonBody DocumentChunk documentChunk) {
|
||||
boolean success = service.updateById(documentChunk);
|
||||
if (success){
|
||||
@@ -87,6 +115,13 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
||||
|
||||
@PostMapping("removeChunk")
|
||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.DOCUMENT_CHUNK_ID,
|
||||
idExpr = "#chunkId",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) BigInteger chunkId) {
|
||||
DocumentChunk docChunk = documentChunkService.getById(chunkId);
|
||||
if (docChunk == null) {
|
||||
|
||||
@@ -3,17 +3,17 @@ package tech.easyflow.admin.controller.ai;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollectionCategory;
|
||||
import tech.easyflow.ai.entity.WorkflowCategory;
|
||||
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
|
||||
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.WorkflowCategoryService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
@@ -34,6 +34,8 @@ public class DocumentCollectionCategoryController extends BaseCurdController<Doc
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionMapper documentCollectionMapper;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public DocumentCollectionCategoryController(DocumentCollectionCategoryService service) {
|
||||
super(service);
|
||||
@@ -51,4 +53,18 @@ public class DocumentCollectionCategoryController extends BaseCurdController<Doc
|
||||
|
||||
return super.onRemoveBefore(ids);
|
||||
}
|
||||
|
||||
@GetMapping("visibleList")
|
||||
public Result<List<DocumentCollectionCategory>> visibleList(DocumentCollectionCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("KNOWLEDGE");
|
||||
if (access.isRestricted()) {
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
queryWrapper.in(DocumentCollectionCategory::getId, access.getCategoryIds());
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,19 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.easyagents.core.document.Document;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
@@ -17,6 +22,13 @@ import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
@@ -41,6 +53,10 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
|
||||
@Resource
|
||||
private BotDocumentCollectionService botDocumentCollectionService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||
|
||||
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
||||
super(service);
|
||||
@@ -50,6 +66,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
|
||||
normalizeVisibilityScope(entity, isSave);
|
||||
if (!isSave && entity.getId() != null) {
|
||||
DocumentCollection existed = requireKnowledge(String.valueOf(entity.getId()));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, existed, ResourceAction.MANAGE, "无权限管理知识库");
|
||||
}
|
||||
|
||||
String alias = entity.getAlias();
|
||||
String collectionType = entity.getCollectionType();
|
||||
@@ -96,6 +117,13 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
|
||||
@GetMapping("search")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#knowledgeId",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public Result<List<Document>> search(@RequestParam BigInteger knowledgeId, @RequestParam String keyword) {
|
||||
return Result.ok(service.search(knowledgeId, keyword));
|
||||
}
|
||||
@@ -103,6 +131,10 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
|
||||
@Override
|
||||
protected Result<Void> onRemoveBefore(Collection<Serializable> ids) {
|
||||
for (Serializable id : ids) {
|
||||
DocumentCollection collection = requireKnowledge(String.valueOf(id));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.MANAGE, "无权限管理知识库");
|
||||
}
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids);
|
||||
@@ -116,7 +148,90 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID_OR_SLUG,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public Result<DocumentCollection> detail(String id) {
|
||||
return Result.ok(service.getDetail(id));
|
||||
DocumentCollection detail = service.getDetail(id);
|
||||
return Result.ok(detail);
|
||||
}
|
||||
|
||||
@GetMapping("modelList")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||
return Result.ok(llmService.listSelectableModels(entity, asTree, sortKey, sortType));
|
||||
}
|
||||
|
||||
@PostMapping("splitterProfile/save")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#request.knowledgeId",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<Boolean> saveSplitterProfile(@JsonBody DocumentImportDtos.SplitterProfileSaveRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库ID不能为空");
|
||||
}
|
||||
DocumentCollection collection = service.getById(request.getKnowledgeId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
if (collection.isFaqCollection()) {
|
||||
throw new BusinessException("FAQ知识库不支持文档导入策略");
|
||||
}
|
||||
Map<String, Object> options = collection.getOptions() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(collection.getOptions());
|
||||
options.put(DocumentCollection.KEY_SPLITTER_DEFAULT_STRATEGY, request.getDefaultStrategyCode());
|
||||
options.put(DocumentCollection.KEY_SPLITTER_AUTO_RECOMMEND_ENABLED, request.getAutoRecommendEnabled());
|
||||
options.put(DocumentCollection.KEY_SPLITTER_FALLBACK_STRATEGY, request.getFallbackStrategyCode());
|
||||
options.put(DocumentCollection.KEY_SPLITTER_STRATEGY_PROFILES, request.getStrategyProfiles());
|
||||
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(collection.getId());
|
||||
update.setOptions(options);
|
||||
return Result.ok(service.updateById(update));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<DocumentCollection>> list(DocumentCollection entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<DocumentCollection> queryPage(Page<DocumentCollection> page, QueryWrapper queryWrapper) {
|
||||
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.hasLength(entity.getVisibilityScope())) {
|
||||
if (isSave) {
|
||||
entity.setVisibilityScope(VisibilityScope.PRIVATE.name());
|
||||
}
|
||||
return;
|
||||
}
|
||||
entity.setVisibilityScope(VisibilityScope.from(entity.getVisibilityScope()).name());
|
||||
}
|
||||
|
||||
private DocumentCollection requireKnowledge(String idOrAlias) {
|
||||
DocumentCollection collection = service.getDetail(idOrAlias);
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
||||
@@ -24,11 +27,21 @@ import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -58,6 +71,11 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
@Autowired
|
||||
private RedisLockExecutor redisLockExecutor;
|
||||
|
||||
@Resource(name = "default")
|
||||
private FileStorageService storageService;
|
||||
|
||||
@Autowired
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
@Value("${easyflow.storage.local.root:}")
|
||||
private String fileUploadPath;
|
||||
@@ -73,6 +91,8 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
@Transactional
|
||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) String id) {
|
||||
Document document = requireDocument(new BigInteger(id));
|
||||
getDocumentCollection(document.getCollectionId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
List<Serializable> ids = Collections.singletonList(id);
|
||||
Result<?> result = onRemoveBefore(ids);
|
||||
if (result != null) return result;
|
||||
@@ -104,7 +124,7 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
|
||||
DocumentCollection knowledge = getDocumentCollection(kbSlug);
|
||||
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(Document::getCollectionId, knowledge.getId());
|
||||
@@ -121,11 +141,33 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
if (StringUtil.noText(kbSlug)) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
DocumentCollection knowledge = getDocumentCollection(kbSlug);
|
||||
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
|
||||
Page<Document> documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName);
|
||||
return Result.ok(documentList);
|
||||
}
|
||||
|
||||
@GetMapping("download")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.DOCUMENT_ID,
|
||||
idExpr = "#documentId",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public void download(@RequestParam BigInteger documentId, HttpServletResponse response) throws IOException {
|
||||
Document document = requireDocument(documentId);
|
||||
String fileName = resolveDownloadFileName(document);
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName);
|
||||
try (InputStream inputStream = storageService.readStream(document.getDocumentPath())) {
|
||||
IoUtil.copy(inputStream, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String getDefaultOrderBy() {
|
||||
@@ -138,6 +180,11 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
@Transactional
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<Boolean> update(@JsonBody Document entity) {
|
||||
if (entity.getId() == null) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
Document current = requireDocument(entity.getId());
|
||||
getDocumentCollection(current.getCollectionId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
super.update(entity);
|
||||
return Result.ok(updatePosition(entity));
|
||||
}
|
||||
@@ -152,10 +199,40 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
if (documentCollectionSplitParams.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(documentCollectionSplitParams.getKnowledgeId().toString());
|
||||
getDocumentCollection(documentCollectionSplitParams.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.textSplit(documentCollectionSplitParams);
|
||||
}
|
||||
|
||||
@PostMapping("import/analyze")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(@JsonBody DocumentImportDtos.AnalyzeRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.analyzeImport(request);
|
||||
}
|
||||
|
||||
@PostMapping("import/preview")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.PreviewResponse> previewImport(@JsonBody DocumentImportDtos.PreviewRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.previewImport(request);
|
||||
}
|
||||
|
||||
@PostMapping("import/commit")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.CommitResponse> commitImport(@JsonBody DocumentImportDtos.CommitRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.commitImport(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 entity
|
||||
*
|
||||
@@ -219,17 +296,42 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentCollection getDocumentCollection(String idOrSlug) {
|
||||
private DocumentCollection getDocumentCollection(String idOrSlug, ResourceAction action, String denyMessage) {
|
||||
DocumentCollection knowledge = StringUtil.isNumeric(idOrSlug)
|
||||
? knowledgeService.getById(idOrSlug)
|
||||
: knowledgeService.getOne(QueryWrapper.create().eq(DocumentCollection::getSlug, idOrSlug));
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, action, denyMessage);
|
||||
if (knowledge.isFaqCollection()) {
|
||||
throw new BusinessException("FAQ知识库不支持文档操作");
|
||||
}
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
private Document requireDocument(BigInteger documentId) {
|
||||
if (documentId == null) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
Document document = service.getById(documentId);
|
||||
if (document == null) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
private String resolveDownloadFileName(Document document) {
|
||||
String fileName = document.getTitle();
|
||||
if (!StringUtil.hasText(fileName)) {
|
||||
String path = document.getDocumentPath();
|
||||
if (!StringUtil.hasText(path)) {
|
||||
return "document";
|
||||
}
|
||||
int slashIndex = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return slashIndex >= 0 ? path.substring(slashIndex + 1) : path;
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
@@ -29,6 +33,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
||||
@Override
|
||||
@GetMapping("list")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#entity == null ? null : #entity.collectionId",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public Result<List<FaqCategory>> list(FaqCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
|
||||
if (collectionId == null) {
|
||||
@@ -40,6 +51,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
||||
@Override
|
||||
@PostMapping("save")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#entity.collectionId",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> save(@JsonBody FaqCategory entity) {
|
||||
return Result.ok(service.saveCategory(entity));
|
||||
}
|
||||
@@ -47,6 +65,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
||||
@Override
|
||||
@PostMapping("update")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.FAQ_CATEGORY_ID,
|
||||
idExpr = "#entity.id",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> update(@JsonBody FaqCategory entity) {
|
||||
return Result.ok(service.updateCategory(entity));
|
||||
}
|
||||
@@ -54,6 +79,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
||||
@Override
|
||||
@PostMapping("remove")
|
||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.FAQ_CATEGORY_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
return Result.ok(service.removeCategory(new BigInteger(String.valueOf(id))));
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ import tech.easyflow.common.vo.UploadResVo;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
@@ -67,13 +71,31 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
@Override
|
||||
@GetMapping("list")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#entity == null ? null : #entity.collectionId",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public Result<java.util.List<FaqItem>> list(FaqItem entity, Boolean asTree, String sortKey, String sortType) {
|
||||
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
|
||||
if (collectionId == null) {
|
||||
throw new BusinessException("知识库ID不能为空");
|
||||
}
|
||||
return super.list(entity, asTree, sortKey, sortType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping("page")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#request.getParameter('collectionId')",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public Result<Page<FaqItem>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||
if (pageNumber == null || pageNumber < 1) {
|
||||
pageNumber = 1L;
|
||||
@@ -123,6 +145,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
@Override
|
||||
@GetMapping("detail")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.FAQ_ITEM_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public Result<FaqItem> detail(String id) {
|
||||
return super.detail(id);
|
||||
}
|
||||
@@ -130,6 +159,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
@Override
|
||||
@PostMapping("save")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#entity.collectionId",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> save(@JsonBody FaqItem entity) {
|
||||
return Result.ok(service.saveFaqItem(entity));
|
||||
}
|
||||
@@ -137,6 +173,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
@Override
|
||||
@PostMapping("update")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.FAQ_ITEM_ID,
|
||||
idExpr = "#entity.id",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> update(@JsonBody FaqItem entity) {
|
||||
return Result.ok(service.updateFaqItem(entity));
|
||||
}
|
||||
@@ -144,12 +187,26 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
@Override
|
||||
@PostMapping("remove")
|
||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.FAQ_ITEM_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
return Result.ok(service.removeFaqItem(new java.math.BigInteger(String.valueOf(id))));
|
||||
}
|
||||
|
||||
@PostMapping(value = "uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#collectionId",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<UploadResVo> uploadImage(MultipartFile file, BigInteger collectionId) {
|
||||
if (collectionId == null) {
|
||||
throw new BusinessException("知识库ID不能为空");
|
||||
@@ -180,12 +237,26 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
|
||||
@PostMapping(value = "importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#collectionId",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<FaqImportResultVo> importExcel(MultipartFile file, BigInteger collectionId) {
|
||||
return Result.ok(service.importFromExcel(collectionId, file));
|
||||
}
|
||||
|
||||
@GetMapping("downloadImportTemplate")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#collectionId",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public void downloadImportTemplate(BigInteger collectionId, HttpServletResponse response) throws Exception {
|
||||
if (collectionId == null) {
|
||||
throw new BusinessException("知识库ID不能为空");
|
||||
@@ -206,6 +277,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
|
||||
@GetMapping("exportExcel")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#collectionId",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public void exportExcel(BigInteger collectionId, HttpServletResponse response) throws Exception {
|
||||
if (collectionId == null) {
|
||||
throw new BusinessException("知识库ID不能为空");
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.ai.dto.ModelInvokeConfigDtos;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
import tech.easyflow.ai.entity.table.ModelTableDef;
|
||||
@@ -69,6 +70,12 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
return Result.ok(modelService.getList(entity));
|
||||
}
|
||||
|
||||
@GetMapping("invokeList")
|
||||
@SaCheckPermission("/api/v1/model/query")
|
||||
public Result<List<Model>> invokeList() {
|
||||
return Result.ok(modelService.listInvokeModels());
|
||||
}
|
||||
|
||||
@PostMapping("/addAiLlm")
|
||||
@SaCheckPermission("/api/v1/model/save")
|
||||
public Result<Boolean> addAiLlm(Model entity) {
|
||||
@@ -92,6 +99,31 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/updateInvokeConfig")
|
||||
@SaCheckPermission("/api/v1/model/save")
|
||||
public Result<Model> updateInvokeConfig(@RequestBody ModelInvokeConfigDtos.UpdateRequest request) {
|
||||
return Result.ok(modelService.updateInvokeConfig(
|
||||
request.getId(),
|
||||
request.getInvokeCode(),
|
||||
request.getPublishEnabled()
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/batchUpdateInvokePublishStatus")
|
||||
@SaCheckPermission("/api/v1/model/save")
|
||||
public Result<List<Model>> batchUpdateInvokePublishStatus(@RequestBody ModelInvokeConfigDtos.BatchPublishRequest request) {
|
||||
return Result.ok(modelService.batchUpdateInvokePublishStatus(
|
||||
request.getIds(),
|
||||
request.getPublishEnabled()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(Model entity, boolean isSave) {
|
||||
modelService.validateForSaveOrUpdate(entity, isSave);
|
||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||
}
|
||||
|
||||
@GetMapping("/selectLlmByProviderCategory")
|
||||
@SaCheckPermission("/api/v1/model/query")
|
||||
public Result<Map<String, List<Model>>> selectLlmByProviderCategory(Model entity, String sortKey, String sortType) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -10,9 +11,13 @@ import tech.easyflow.ai.service.PluginCategoryService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -30,6 +35,8 @@ public class PluginCategoryController extends BaseCurdController<PluginCategoryS
|
||||
|
||||
@Resource
|
||||
private PluginCategoryService pluginCategoryService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@GetMapping("/doRemoveCategory")
|
||||
@SaCheckPermission("/api/v1/plugin/remove")
|
||||
@@ -37,4 +44,18 @@ public class PluginCategoryController extends BaseCurdController<PluginCategoryS
|
||||
|
||||
return Result.ok(pluginCategoryService.doRemoveCategory(id));
|
||||
}
|
||||
|
||||
@GetMapping("/visibleList")
|
||||
public Result<List<PluginCategory>> visibleList(PluginCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
|
||||
if (access.isRestricted()) {
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
queryWrapper.in(PluginCategory::getId, access.getCategoryIds());
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,25 @@ import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.PluginTableDef.PLUGIN;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -32,6 +41,12 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
|
||||
@Resource
|
||||
PluginService pluginService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private PluginVisibilityService pluginVisibilityService;
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
||||
@@ -40,7 +55,7 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
|
||||
@PostMapping("/plugin/save")
|
||||
@SaCheckPermission("/api/v1/plugin/save")
|
||||
public Result<Boolean> savePlugin(@JsonBody Plugin plugin){
|
||||
public Result<Plugin> savePlugin(@JsonBody Plugin plugin){
|
||||
|
||||
return Result.ok(pluginService.savePlugin(plugin));
|
||||
}
|
||||
@@ -62,7 +77,9 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
@PostMapping("/getList")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<Plugin>> getList(){
|
||||
return Result.ok(pluginService.getList());
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().select();
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return Result.ok(service.getMapper().selectListByQuery(queryWrapper));
|
||||
}
|
||||
|
||||
@GetMapping("/pageByCategory")
|
||||
@@ -76,6 +93,7 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
}
|
||||
if (category == 0){
|
||||
QueryWrapper queryWrapper = buildQueryWrapper(request);
|
||||
applyCategoryPermission(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
||||
} else {
|
||||
@@ -83,8 +101,41 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/modelList")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||
return Result.ok(modelService.listSelectableModels(entity, asTree, sortKey, sortType));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return service.getMapper().paginateWithRelations(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Plugin> detail(String id) {
|
||||
Plugin plugin = service.getById(id);
|
||||
if (plugin != null) {
|
||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
||||
}
|
||||
return Result.ok(plugin);
|
||||
}
|
||||
|
||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
|
||||
if (!access.isRestricted()) {
|
||||
return;
|
||||
}
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.eq(Plugin::getCreatedBy, access.getAccountIdAsLong());
|
||||
return;
|
||||
}
|
||||
Set<BigInteger> pluginIds = pluginVisibilityService.getCurrentVisiblePluginIds();
|
||||
if (pluginIds.isEmpty()) {
|
||||
queryWrapper.eq(Plugin::getCreatedBy, access.getAccountIdAsLong());
|
||||
return;
|
||||
}
|
||||
queryWrapper.and(PLUGIN.CREATED_BY.eq(access.getAccountIdAsLong()).or(PLUGIN.ID.in(pluginIds)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import tech.easyflow.ai.entity.ResourceCategory;
|
||||
import tech.easyflow.ai.service.ResourceCategoryService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 素材分类
|
||||
@@ -14,9 +23,24 @@ import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
@RequestMapping("/api/v1/resourceCategory")
|
||||
@UsePermission(moduleName = "/api/v1/resource")
|
||||
public class ResourceCategoryController extends BaseCurdController<ResourceCategoryService, ResourceCategory> {
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public ResourceCategoryController(ResourceCategoryService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
@GetMapping("visibleList")
|
||||
public Result<List<ResourceCategory>> visibleList(ResourceCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
||||
if (access.isRestricted()) {
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
queryWrapper.in(ResourceCategory::getId, access.getCategoryIds());
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,15 @@ import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
|
||||
|
||||
/**
|
||||
* 素材库
|
||||
@@ -26,6 +31,9 @@ import java.util.Date;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource")
|
||||
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
|
||||
@javax.annotation.Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public ResourceController(ResourceService service) {
|
||||
super(service);
|
||||
}
|
||||
@@ -50,7 +58,36 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
||||
|
||||
@Override
|
||||
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
|
||||
queryWrapper.eq(Resource::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<Resource>> list(Resource entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
applyCategoryPermission(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Resource> detail(String id) {
|
||||
Resource resource = service.getById(id);
|
||||
if (resource != null) {
|
||||
categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材");
|
||||
}
|
||||
return Result.ok(resource);
|
||||
}
|
||||
|
||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
||||
if (!access.isRestricted()) {
|
||||
return;
|
||||
}
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.eq(Resource::getCreatedBy, access.getAccountId());
|
||||
return;
|
||||
}
|
||||
queryWrapper.and(RESOURCE.CREATED_BY.eq(access.getAccountId()).or(RESOURCE.CATEGORY_ID.in(access.getCategoryIds())));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.ai.entity.WorkflowCategory;
|
||||
import tech.easyflow.ai.service.WorkflowCategoryService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
*
|
||||
@@ -17,9 +26,24 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/v1/workflowCategory")
|
||||
@UsePermission(moduleName = "/api/v1/workflow")
|
||||
public class WorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public WorkflowCategoryController(WorkflowCategoryService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
@GetMapping("visibleList")
|
||||
public Result<List<WorkflowCategory>> visibleList(WorkflowCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("WORKFLOW");
|
||||
if (access.isRestricted()) {
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
queryWrapper.in(WorkflowCategory::getId, access.getCategoryIds());
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
@@ -12,6 +13,7 @@ import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
||||
@@ -30,6 +32,12 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -67,6 +75,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
private CodeEngineCapabilityService codeEngineCapabilityService;
|
||||
@Resource
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||
|
||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||
super(service);
|
||||
@@ -78,6 +90,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
*/
|
||||
@PostMapping("/singleRun")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.USE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#workflowId",
|
||||
denyMessage = "无权限运行工作流"
|
||||
)
|
||||
public Result<?> singleRun(
|
||||
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
||||
@JsonBody(value = "nodeId", required = true) String nodeId,
|
||||
@@ -96,6 +115,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
*/
|
||||
@PostMapping("/runAsync")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.USE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限运行工作流"
|
||||
)
|
||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||
@JsonBody("variables") Map<String, Object> variables) {
|
||||
if (variables == null) {
|
||||
@@ -117,6 +143,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
* 获取工作流运行状态 - v2
|
||||
*/
|
||||
@PostMapping("/getChainStatus")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.USE,
|
||||
lookup = ResourceLookup.EXEC_KEY,
|
||||
idExpr = "#executeId",
|
||||
denyMessage = "无权限访问该执行记录"
|
||||
)
|
||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||
@@ -128,6 +161,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
*/
|
||||
@PostMapping("/resume")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.USE,
|
||||
lookup = ResourceLookup.EXEC_KEY,
|
||||
idExpr = "#executeId",
|
||||
denyMessage = "无权限恢复工作流执行"
|
||||
)
|
||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||
@@ -137,6 +177,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
@PostMapping("/importWorkFlow")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<Void> importWorkFlow(Workflow workflow, MultipartFile jsonFile) throws Exception {
|
||||
if (workflow.getId() != null) {
|
||||
Workflow sourceWorkflow = requireWorkflow(String.valueOf(workflow.getId()));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, sourceWorkflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
InputStream is = jsonFile.getInputStream();
|
||||
String content = IoUtil.read(is, StandardCharsets.UTF_8);
|
||||
workflow.setContent(content);
|
||||
@@ -147,13 +191,30 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
|
||||
@GetMapping("/exportWorkFlow")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限访问工作流"
|
||||
)
|
||||
public Result<String> exportWorkFlow(BigInteger id) {
|
||||
Workflow workflow = service.getById(id);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
return Result.ok("", workflow.getContent());
|
||||
}
|
||||
|
||||
@GetMapping("getRunningParameters")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限访问工作流"
|
||||
)
|
||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
||||
Workflow workflow = service.getById(id);
|
||||
|
||||
@@ -186,6 +247,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
public Result<WorkflowCheckResult> check(@JsonBody("id") BigInteger id,
|
||||
@JsonBody("content") String content,
|
||||
@JsonBody(value = "stage", required = true) String stage) {
|
||||
if (id != null) {
|
||||
Workflow workflow = requireWorkflow(String.valueOf(id));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
WorkflowCheckStage checkStage = WorkflowCheckStage.from(stage);
|
||||
WorkflowCheckResult checkResult;
|
||||
if (StringUtils.hasLength(content)) {
|
||||
@@ -199,6 +264,14 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping("detail")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限访问工作流"
|
||||
)
|
||||
public Result<Workflow> detail(String id) {
|
||||
Workflow workflow = service.getDetail(id);
|
||||
return Result.ok(workflow);
|
||||
@@ -206,9 +279,19 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
|
||||
@GetMapping("/copy")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限访问工作流"
|
||||
)
|
||||
public Result<Void> copy(BigInteger id) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
Workflow workflow = service.getById(id);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
workflow.setId(null);
|
||||
workflow.setAlias(IdUtil.fastSimpleUUID());
|
||||
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
|
||||
@@ -218,6 +301,11 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
|
||||
normalizeVisibilityScope(entity, isSave);
|
||||
if (!isSave && entity.getId() != null) {
|
||||
Workflow existed = requireWorkflow(String.valueOf(entity.getId()));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, existed, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
if (StringUtils.hasLength(entity.getContent())) {
|
||||
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
|
||||
}
|
||||
@@ -241,8 +329,26 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<Workflow>> list(Workflow entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
||||
for (Serializable id : ids) {
|
||||
Workflow workflow = requireWorkflow(String.valueOf(id));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in("workflow_id", ids);
|
||||
boolean exists = botWorkflowService.exists(queryWrapper);
|
||||
@@ -251,4 +357,25 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void normalizeVisibilityScope(Workflow entity, boolean isSave) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.hasLength(entity.getVisibilityScope())) {
|
||||
if (isSave) {
|
||||
entity.setVisibilityScope(VisibilityScope.PRIVATE.name());
|
||||
}
|
||||
return;
|
||||
}
|
||||
entity.setVisibilityScope(VisibilityScope.from(entity.getVisibilityScope()).name());
|
||||
}
|
||||
|
||||
private Workflow requireWorkflow(String idOrAlias) {
|
||||
Workflow workflow = service.getDetail(idOrAlias);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package tech.easyflow.admin.controller.dashboard;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||
import tech.easyflow.admin.service.dashboard.DashboardService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
/**
|
||||
* 管理端工作台统计接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dashboard")
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
public DashboardController(DashboardService dashboardService) {
|
||||
this.dashboardService = dashboardService;
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
@SaCheckPermission("/api/v1/dashboard/query")
|
||||
public Result<DashboardOverviewVo> overview(DashboardOverviewQuery query) {
|
||||
return Result.ok(dashboardService.getOverview(SaTokenUtil.getLoginAccount(), query));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.log.annotation.LogRecord;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||
@@ -180,6 +181,28 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/removeBatchWithResult")
|
||||
@SaCheckPermission("/api/v1/sysAccount/remove")
|
||||
@LogRecord("批量删除用户")
|
||||
public Result<SysAccountBatchActionResultVo> removeBatchWithResult(
|
||||
@JsonBody(value = "ids", required = true) List<BigInteger> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return Result.fail("ids不能为空", null);
|
||||
}
|
||||
return Result.ok(service.removeBatchWithResult(ids));
|
||||
}
|
||||
|
||||
@PostMapping("/resetPasswordBatch")
|
||||
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||
@LogRecord("批量重置用户密码")
|
||||
public Result<SysAccountBatchActionResultVo> resetPasswordBatch(
|
||||
@JsonBody(value = "ids", required = true) List<BigInteger> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return Result.fail("ids不能为空", null);
|
||||
}
|
||||
return Result.ok(service.resetPasswordBatch(ids, SaTokenUtil.getLoginAccount().getId()));
|
||||
}
|
||||
|
||||
@PostMapping("/importExcel")
|
||||
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||
public Result<SysAccountImportResultVo> importExcel(MultipartFile file) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package tech.easyflow.admin.controller.system;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeDetailVo;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysRoleCategoryScopeService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sysRoleCategoryScope")
|
||||
public class SysRoleCategoryScopeController {
|
||||
|
||||
@Resource
|
||||
private SysRoleCategoryScopeService sysRoleCategoryScopeService;
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@GetMapping("/detail")
|
||||
@SaCheckPermission("/api/v1/sysRole/query")
|
||||
public Result<SysRoleCategoryScopeDetailVo> detail(BigInteger roleId) {
|
||||
SysRoleCategoryScopeDetailVo detail = sysRoleCategoryScopeService.getRoleScopeDetail(roleId);
|
||||
detail.setEditable(categoryPermissionService.isCurrentSuperAdmin());
|
||||
return Result.ok(detail);
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@SaCheckPermission("/api/v1/sysRole/save")
|
||||
public Result<Void> save(@JsonBody SysRoleCategoryScopeDetailVo request) {
|
||||
assertSuperAdmin();
|
||||
if (request == null || request.getRoleId() == null) {
|
||||
throw new BusinessException("角色ID不能为空");
|
||||
}
|
||||
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||
sysRoleCategoryScopeService.saveRoleScopes(request.getRoleId(), request.getScopes(), operatorId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private void assertSuperAdmin() {
|
||||
if (!categoryPermissionService.isCurrentSuperAdmin()) {
|
||||
throw new BusinessException("仅超级管理员可配置分类权限");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,13 +80,13 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
|
||||
*/
|
||||
@PostMapping("saveRole")
|
||||
@SaCheckPermission("/api/v1/sysRole/save")
|
||||
public Result<Void> saveRole(@JsonBody SysRole entity) {
|
||||
public Result<BigInteger> saveRole(@JsonBody SysRole entity) {
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
if (entity.getId() == null) {
|
||||
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
|
||||
}
|
||||
service.saveRole(entity);
|
||||
return Result.ok();
|
||||
return Result.ok(entity.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
/**
|
||||
* 工作台分布/排行项。
|
||||
*/
|
||||
public class DashboardDistributionItemVo {
|
||||
|
||||
private String key;
|
||||
|
||||
private String label;
|
||||
|
||||
private Long value;
|
||||
|
||||
private Long userTotal;
|
||||
|
||||
private Long activeUserTotal;
|
||||
|
||||
private Long botTotal;
|
||||
|
||||
private Long workflowTotal;
|
||||
|
||||
private Long knowledgeBaseTotal;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Long getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(Long value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Long getUserTotal() {
|
||||
return userTotal;
|
||||
}
|
||||
|
||||
public void setUserTotal(Long userTotal) {
|
||||
this.userTotal = userTotal;
|
||||
}
|
||||
|
||||
public Long getActiveUserTotal() {
|
||||
return activeUserTotal;
|
||||
}
|
||||
|
||||
public void setActiveUserTotal(Long activeUserTotal) {
|
||||
this.activeUserTotal = activeUserTotal;
|
||||
}
|
||||
|
||||
public Long getBotTotal() {
|
||||
return botTotal;
|
||||
}
|
||||
|
||||
public void setBotTotal(Long botTotal) {
|
||||
this.botTotal = botTotal;
|
||||
}
|
||||
|
||||
public Long getWorkflowTotal() {
|
||||
return workflowTotal;
|
||||
}
|
||||
|
||||
public void setWorkflowTotal(Long workflowTotal) {
|
||||
this.workflowTotal = workflowTotal;
|
||||
}
|
||||
|
||||
public Long getKnowledgeBaseTotal() {
|
||||
return knowledgeBaseTotal;
|
||||
}
|
||||
|
||||
public void setKnowledgeBaseTotal(Long knowledgeBaseTotal) {
|
||||
this.knowledgeBaseTotal = knowledgeBaseTotal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
/**
|
||||
* 工作台统计查询参数。
|
||||
*/
|
||||
public class DashboardOverviewQuery {
|
||||
|
||||
private String range;
|
||||
|
||||
public String getRange() {
|
||||
return range;
|
||||
}
|
||||
|
||||
public void setRange(String range) {
|
||||
this.range = range;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工作台总览返回对象。
|
||||
*/
|
||||
public class DashboardOverviewVo {
|
||||
|
||||
private DashboardSummaryVo summary;
|
||||
|
||||
private List<DashboardTrendItemVo> trends;
|
||||
|
||||
private List<DashboardDistributionItemVo> distribution;
|
||||
|
||||
private DashboardOverviewQuery query;
|
||||
|
||||
private Date updatedAt;
|
||||
|
||||
public DashboardSummaryVo getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(DashboardSummaryVo summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public List<DashboardTrendItemVo> getTrends() {
|
||||
return trends;
|
||||
}
|
||||
|
||||
public void setTrends(List<DashboardTrendItemVo> trends) {
|
||||
this.trends = trends;
|
||||
}
|
||||
|
||||
public List<DashboardDistributionItemVo> getDistribution() {
|
||||
return distribution;
|
||||
}
|
||||
|
||||
public void setDistribution(List<DashboardDistributionItemVo> distribution) {
|
||||
this.distribution = distribution;
|
||||
}
|
||||
|
||||
public DashboardOverviewQuery getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public void setQuery(DashboardOverviewQuery query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public Date getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Date updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
/**
|
||||
* 工作台汇总指标。
|
||||
*/
|
||||
public class DashboardSummaryVo {
|
||||
|
||||
private Long userTotal;
|
||||
|
||||
private Long activeUserTotal;
|
||||
|
||||
private Long botTotal;
|
||||
|
||||
private Long workflowTotal;
|
||||
|
||||
private Long knowledgeBaseTotal;
|
||||
|
||||
public Long getUserTotal() {
|
||||
return userTotal;
|
||||
}
|
||||
|
||||
public void setUserTotal(Long userTotal) {
|
||||
this.userTotal = userTotal;
|
||||
}
|
||||
|
||||
public Long getActiveUserTotal() {
|
||||
return activeUserTotal;
|
||||
}
|
||||
|
||||
public void setActiveUserTotal(Long activeUserTotal) {
|
||||
this.activeUserTotal = activeUserTotal;
|
||||
}
|
||||
|
||||
public Long getBotTotal() {
|
||||
return botTotal;
|
||||
}
|
||||
|
||||
public void setBotTotal(Long botTotal) {
|
||||
this.botTotal = botTotal;
|
||||
}
|
||||
|
||||
public Long getWorkflowTotal() {
|
||||
return workflowTotal;
|
||||
}
|
||||
|
||||
public void setWorkflowTotal(Long workflowTotal) {
|
||||
this.workflowTotal = workflowTotal;
|
||||
}
|
||||
|
||||
public Long getKnowledgeBaseTotal() {
|
||||
return knowledgeBaseTotal;
|
||||
}
|
||||
|
||||
public void setKnowledgeBaseTotal(Long knowledgeBaseTotal) {
|
||||
this.knowledgeBaseTotal = knowledgeBaseTotal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
/**
|
||||
* 工作台趋势项。
|
||||
*/
|
||||
public class DashboardTrendItemVo {
|
||||
|
||||
private String key;
|
||||
|
||||
private String label;
|
||||
|
||||
private Long activeUserTotal;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Long getActiveUserTotal() {
|
||||
return activeUserTotal;
|
||||
}
|
||||
|
||||
public void setActiveUserTotal(Long activeUserTotal) {
|
||||
this.activeUserTotal = activeUserTotal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package tech.easyflow.admin.service.dashboard;
|
||||
|
||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
|
||||
/**
|
||||
* 工作台统计服务。
|
||||
*/
|
||||
public interface DashboardService {
|
||||
|
||||
DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query);
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package tech.easyflow.admin.service.dashboard.impl;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainStatus;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.row.Db;
|
||||
import com.mybatisflex.core.row.Row;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
|
||||
import tech.easyflow.admin.service.dashboard.DashboardService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccountRole;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.service.SysAccountRoleService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 工作台统计服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class DashboardServiceImpl implements DashboardService {
|
||||
|
||||
private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
|
||||
@Resource
|
||||
private SysAccountRoleService sysAccountRoleService;
|
||||
@Resource
|
||||
private SysRoleService sysRoleService;
|
||||
|
||||
@Override
|
||||
public DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query) {
|
||||
DashboardQueryContext context = buildContext(loginAccount, query);
|
||||
|
||||
DashboardSummaryVo summary = buildSummary(context);
|
||||
List<DashboardTrendItemVo> trends = buildTrends(context);
|
||||
List<DashboardDistributionItemVo> distribution = buildDistribution(context, summary);
|
||||
|
||||
DashboardOverviewVo result = new DashboardOverviewVo();
|
||||
result.setSummary(summary);
|
||||
result.setTrends(trends);
|
||||
result.setDistribution(distribution);
|
||||
|
||||
DashboardOverviewQuery normalizedQuery = new DashboardOverviewQuery();
|
||||
normalizedQuery.setRange(context.range);
|
||||
result.setQuery(normalizedQuery);
|
||||
result.setUpdatedAt(new Date());
|
||||
return result;
|
||||
}
|
||||
|
||||
private DashboardSummaryVo buildSummary(DashboardQueryContext context) {
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
summary.setUserTotal(countScopedTable("tb_sys_account", "a", true, context));
|
||||
summary.setActiveUserTotal(countActiveUsers(context));
|
||||
summary.setBotTotal(countScopedTable("tb_bot", "b", false, context));
|
||||
summary.setWorkflowTotal(countScopedTable("tb_workflow", "w", false, context));
|
||||
summary.setKnowledgeBaseTotal(countScopedTable("tb_document_collection", "d", false, context));
|
||||
return summary;
|
||||
}
|
||||
|
||||
private List<DashboardTrendItemVo> buildTrends(DashboardQueryContext context) {
|
||||
List<TimeBucket> buckets = buildBuckets(context.range);
|
||||
String bucketFormat = "today".equals(context.range) ? "%Y-%m-%d %H:00:00" : "%Y-%m-%d";
|
||||
|
||||
Map<String, Long> activeUserMap = queryActiveUserTrend(context, bucketFormat);
|
||||
|
||||
List<DashboardTrendItemVo> items = new ArrayList<>(buckets.size());
|
||||
for (TimeBucket bucket : buckets) {
|
||||
long activeUserTotal = activeUserMap.getOrDefault(bucket.key, 0L);
|
||||
|
||||
DashboardTrendItemVo item = new DashboardTrendItemVo();
|
||||
item.setKey(bucket.key);
|
||||
item.setLabel(bucket.label);
|
||||
item.setActiveUserTotal(activeUserTotal);
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private List<DashboardDistributionItemVo> buildDistribution(DashboardQueryContext context, DashboardSummaryVo summary) {
|
||||
return buildResourceDistribution(summary);
|
||||
}
|
||||
|
||||
private List<DashboardDistributionItemVo> buildResourceDistribution(DashboardSummaryVo summary) {
|
||||
List<DashboardDistributionItemVo> items = new ArrayList<>();
|
||||
items.add(buildPlatformItem("userTotal", "用户总量", summary.getUserTotal()));
|
||||
items.add(buildPlatformItem("activeUserTotal", "活跃用户", summary.getActiveUserTotal()));
|
||||
items.add(buildPlatformItem("botTotal", "助手数量", summary.getBotTotal()));
|
||||
items.add(buildPlatformItem("workflowTotal", "工作流数量", summary.getWorkflowTotal()));
|
||||
items.add(buildPlatformItem("knowledgeBaseTotal", "知识库数量", summary.getKnowledgeBaseTotal()));
|
||||
return items;
|
||||
}
|
||||
|
||||
private DashboardDistributionItemVo buildPlatformItem(String key, String label, Long value) {
|
||||
DashboardDistributionItemVo item = new DashboardDistributionItemVo();
|
||||
item.setKey(key);
|
||||
item.setLabel(label);
|
||||
item.setValue(defaultLong(value));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Long> queryActiveUserTrend(DashboardQueryContext context, String bucketFormat) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT DATE_FORMAT(l.created, '").append(bucketFormat).append("') AS bucket_key, ")
|
||||
.append("COUNT(DISTINCT l.account_id) AS total ")
|
||||
.append("FROM tb_sys_log l ")
|
||||
.append("INNER JOIN tb_sys_account a ON a.id = l.account_id AND a.is_deleted IS NULL ")
|
||||
.append("WHERE l.created >= ? AND l.created < ? ");
|
||||
params.add(toDate(context.startTime));
|
||||
params.add(toDate(context.endTime));
|
||||
appendOptionalTenantFilter(sql, params, context.tenantFilterId, "a.tenant_id");
|
||||
appendOptionalDeptFilter(sql, params, context.deptFilterId, "a.dept_id");
|
||||
sql.append("GROUP BY bucket_key ORDER BY bucket_key ASC");
|
||||
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
for (Row row : Db.selectListBySql(sql.toString(), params.toArray())) {
|
||||
data.put(asString(row.get("bucket_key")), asLong(row.get("total")));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private long countScopedTable(String tableName, String alias, boolean containsLogicDelete, DashboardQueryContext context) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT COUNT(*) AS total FROM ").append(tableName).append(" ").append(alias).append(" WHERE 1 = 1 ");
|
||||
if (containsLogicDelete) {
|
||||
sql.append("AND ").append(alias).append(".is_deleted IS NULL ");
|
||||
}
|
||||
appendOptionalTenantFilter(sql, params, context.tenantFilterId, alias + ".tenant_id");
|
||||
appendOptionalDeptFilter(sql, params, context.deptFilterId, alias + ".dept_id");
|
||||
return queryForLong(sql.toString(), params);
|
||||
}
|
||||
|
||||
private long countActiveUsers(DashboardQueryContext context) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT COUNT(DISTINCT l.account_id) AS total ")
|
||||
.append("FROM tb_sys_log l ")
|
||||
.append("INNER JOIN tb_sys_account a ON a.id = l.account_id AND a.is_deleted IS NULL ")
|
||||
.append("WHERE l.created >= ? AND l.created < ? ");
|
||||
params.add(toDate(context.startTime));
|
||||
params.add(toDate(context.endTime));
|
||||
appendOptionalTenantFilter(sql, params, context.tenantFilterId, "a.tenant_id");
|
||||
appendOptionalDeptFilter(sql, params, context.deptFilterId, "a.dept_id");
|
||||
return queryForLong(sql.toString(), params);
|
||||
}
|
||||
|
||||
private long queryForLong(String sql, List<Object> params) {
|
||||
Object result = Db.selectObject(sql, params.toArray());
|
||||
return asLong(result);
|
||||
}
|
||||
|
||||
private void appendOptionalTenantFilter(StringBuilder sql, List<Object> params, BigInteger tenantId, String columnName) {
|
||||
if (tenantId != null) {
|
||||
sql.append(" AND ").append(columnName).append(" = ? ");
|
||||
params.add(tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendOptionalDeptFilter(StringBuilder sql, List<Object> params, BigInteger deptId, String columnName) {
|
||||
if (deptId != null) {
|
||||
sql.append(" AND ").append(columnName).append(" = ? ");
|
||||
params.add(deptId);
|
||||
}
|
||||
}
|
||||
|
||||
private DashboardQueryContext buildContext(LoginAccount loginAccount, DashboardOverviewQuery query) {
|
||||
DashboardQueryContext context = new DashboardQueryContext();
|
||||
context.range = normalizeRange(query == null ? null : query.getRange());
|
||||
context.superAdmin = isSuperAdmin(loginAccount);
|
||||
|
||||
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
||||
if ("today".equals(context.range)) {
|
||||
context.startTime = LocalDateTime.of(today, LocalTime.MIN);
|
||||
context.endTime = context.startTime.plusDays(1);
|
||||
} else if ("7d".equals(context.range)) {
|
||||
context.startTime = LocalDateTime.of(today.minusDays(6), LocalTime.MIN);
|
||||
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
||||
} else {
|
||||
context.startTime = LocalDateTime.of(today.minusDays(29), LocalTime.MIN);
|
||||
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
||||
}
|
||||
|
||||
context.tenantFilterId = context.superAdmin ? null : loginAccount.getTenantId();
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(LoginAccount loginAccount) {
|
||||
if (loginAccount == null || loginAccount.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
QueryWrapper roleMappingWrapper = QueryWrapper.create();
|
||||
roleMappingWrapper.eq(SysAccountRole::getAccountId, loginAccount.getId());
|
||||
List<BigInteger> roleIds = sysAccountRoleService.list(roleMappingWrapper)
|
||||
.stream()
|
||||
.map(SysAccountRole::getRoleId)
|
||||
.collect(Collectors.toList());
|
||||
if (roleIds.isEmpty()) {
|
||||
return Constants.SUPER_ADMIN_ID.equals(loginAccount.getId());
|
||||
}
|
||||
|
||||
QueryWrapper roleWrapper = QueryWrapper.create();
|
||||
roleWrapper.in(SysRole::getId, roleIds);
|
||||
roleWrapper.eq(SysRole::getRoleKey, Constants.SUPER_ADMIN_ROLE_CODE);
|
||||
return sysRoleService.count(roleWrapper) > 0;
|
||||
}
|
||||
|
||||
private String normalizeRange(String range) {
|
||||
if (!StringUtils.hasText(range)) {
|
||||
return "7d";
|
||||
}
|
||||
if ("today".equals(range) || "7d".equals(range) || "30d".equals(range)) {
|
||||
return range;
|
||||
}
|
||||
throw new BusinessException("不支持的时间范围: " + range);
|
||||
}
|
||||
|
||||
private List<TimeBucket> buildBuckets(String range) {
|
||||
List<TimeBucket> buckets = new ArrayList<>();
|
||||
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
||||
|
||||
if ("today".equals(range)) {
|
||||
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00");
|
||||
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("HH:00");
|
||||
LocalDateTime start = LocalDateTime.of(today, LocalTime.MIN);
|
||||
for (int hour = 0; hour < 24; hour++) {
|
||||
LocalDateTime current = start.plusHours(hour);
|
||||
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
int days = "7d".equals(range) ? 7 : 30;
|
||||
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM-dd");
|
||||
LocalDate start = today.minusDays(days - 1L);
|
||||
for (int i = 0; i < days; i++) {
|
||||
LocalDate current = start.plusDays(i);
|
||||
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
private Date toDate(LocalDateTime dateTime) {
|
||||
return Date.from(dateTime.atZone(DEFAULT_ZONE_ID).toInstant());
|
||||
}
|
||||
|
||||
private long defaultLong(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
private String asString(Object value) {
|
||||
return value == null ? "" : String.valueOf(value);
|
||||
}
|
||||
|
||||
private long asLong(Object value) {
|
||||
if (value == null) {
|
||||
return 0L;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
return Long.parseLong(String.valueOf(value));
|
||||
}
|
||||
|
||||
private static class DashboardQueryContext {
|
||||
private String range;
|
||||
private BigInteger tenantFilterId;
|
||||
private BigInteger deptFilterId;
|
||||
private boolean superAdmin;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
}
|
||||
|
||||
private static class TimeBucket {
|
||||
private final String key;
|
||||
private final String label;
|
||||
|
||||
private TimeBucket(String key, String label) {
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package tech.easyflow.publicapi.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
|
||||
import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiErrorResponse;
|
||||
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
|
||||
import tech.easyflow.ai.invoke.service.UnifiedModelInvokeService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
|
||||
@SaIgnore
|
||||
@RestController
|
||||
@RequestMapping("/v1")
|
||||
public class PublicModelChatController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PublicModelChatController.class);
|
||||
private static final long SSE_TIMEOUT = Duration.ofMinutes(10).toMillis();
|
||||
|
||||
@Resource
|
||||
private SysApiKeyService sysApiKeyService;
|
||||
@Resource
|
||||
private UnifiedModelInvokeService unifiedModelInvokeService;
|
||||
@Resource
|
||||
private OpenAiProtocolMapper openAiProtocolMapper;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 统一模型调用(OpenAI Chat Completions)
|
||||
*/
|
||||
@PostMapping(
|
||||
value = "/chat/completions",
|
||||
produces = {
|
||||
MediaType.APPLICATION_JSON_VALUE,
|
||||
MediaType.TEXT_EVENT_STREAM_VALUE
|
||||
}
|
||||
)
|
||||
public Object chatCompletions(@RequestBody String rawBody, HttpServletRequest request) {
|
||||
try {
|
||||
String apiKey = resolveApiKey(request.getHeader(HttpHeaders.AUTHORIZATION));
|
||||
sysApiKeyService.checkApikeyPermission(apiKey, request.getRequestURI());
|
||||
|
||||
OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.readRequest(rawBody);
|
||||
UnifiedChatRequest unifiedRequest = openAiProtocolMapper.toUnifiedRequest(openAiRequest);
|
||||
if (Boolean.TRUE.equals(unifiedRequest.getStream())) {
|
||||
return createStreamEmitter(unifiedRequest);
|
||||
}
|
||||
UnifiedChatResponse response = unifiedModelInvokeService.chat(unifiedRequest);
|
||||
return buildJsonResponse(openAiProtocolMapper.toOpenAiResponse(response));
|
||||
} catch (ModelInvokeException e) {
|
||||
return buildErrorResponse(e);
|
||||
} catch (BusinessException e) {
|
||||
return buildErrorResponse(mapBusinessException(e));
|
||||
} catch (Exception e) {
|
||||
log.error("chatCompletions unexpected error: {}", e.getMessage(), e);
|
||||
return buildErrorResponse(new ModelInvokeException(
|
||||
500,
|
||||
"统一模型调用失败: " + e.getMessage(),
|
||||
"api_error",
|
||||
null,
|
||||
"internal_error"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private SseEmitter createStreamEmitter(UnifiedChatRequest unifiedRequest) {
|
||||
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
|
||||
unifiedModelInvokeService.chatStream(unifiedRequest, new UnifiedChatChunkObserver() {
|
||||
@Override
|
||||
public void onChunk(UnifiedChatChunk chunk) {
|
||||
try {
|
||||
String payload = objectMapper.writeValueAsString(openAiProtocolMapper.toOpenAiChunk(chunk));
|
||||
emitter.send(SseEmitter.event().data(payload));
|
||||
} catch (IOException e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().data("[DONE]"));
|
||||
emitter.complete();
|
||||
} catch (IOException e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
log.error("chatCompletions stream error: {}", throwable.getMessage(), throwable);
|
||||
emitter.completeWithError(throwable);
|
||||
}
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private ResponseEntity<String> buildJsonResponse(Object body) {
|
||||
try {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(objectMapper.writeValueAsString(body));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("序列化响应失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<String> buildErrorResponse(ModelInvokeException e) {
|
||||
OpenAiErrorResponse response = new OpenAiErrorResponse();
|
||||
OpenAiErrorResponse.Error error = new OpenAiErrorResponse.Error();
|
||||
error.setMessage(e.getMessage());
|
||||
error.setType(e.getType());
|
||||
error.setParam(e.getParam());
|
||||
error.setCode(e.getCode());
|
||||
response.setError(error);
|
||||
return ResponseEntity.status(e.getStatus())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(writeJson(response));
|
||||
}
|
||||
|
||||
private String writeJson(Object body) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(body);
|
||||
} catch (Exception ex) {
|
||||
log.error("writeJson error: {}", ex.getMessage(), ex);
|
||||
return "{\"error\":{\"message\":\"响应序列化失败\",\"type\":\"api_error\",\"code\":\"serialization_error\"}}";
|
||||
}
|
||||
}
|
||||
|
||||
private ModelInvokeException mapBusinessException(BusinessException e) {
|
||||
String message = StrUtil.blankToDefault(e.getMessage(), "访问令牌校验失败");
|
||||
if (StrUtil.containsAnyIgnoreCase(message, "apikey 不存在", "apikey 已过期", "已禁用")) {
|
||||
return ModelInvokeException.unauthorized(message);
|
||||
}
|
||||
if (StrUtil.containsAnyIgnoreCase(message, "无权限", "接口不存在")) {
|
||||
return ModelInvokeException.forbidden(message);
|
||||
}
|
||||
return ModelInvokeException.badRequest(message);
|
||||
}
|
||||
|
||||
private String resolveApiKey(String authorizationHeader) {
|
||||
if (StrUtil.isBlank(authorizationHeader)) {
|
||||
throw ModelInvokeException.unauthorized("Authorization 不能为空");
|
||||
}
|
||||
String trimmed = authorizationHeader.trim();
|
||||
if (StrUtil.startWithIgnoreCase(trimmed, "Bearer ")) {
|
||||
trimmed = trimmed.substring(7).trim();
|
||||
}
|
||||
if (StrUtil.isBlank(trimmed)) {
|
||||
throw ModelInvokeException.unauthorized("Authorization 无效");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
package tech.easyflow.usercenter.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import tech.easyflow.ai.entity.BotCategory;
|
||||
import tech.easyflow.ai.service.BotCategoryService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* bot分类 控制层。
|
||||
@@ -17,7 +26,24 @@ import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
@RequestMapping("/userCenter/botCategory")
|
||||
@UsePermission(moduleName = "/api/v1/bot")
|
||||
public class UcBotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public UcBotCategoryController(BotCategoryService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
@GetMapping("visibleList")
|
||||
public Result<List<BotCategory>> visibleList(BotCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
|
||||
if (access.isRestricted()) {
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
queryWrapper.in(BotCategory::getId, access.getCategoryIds());
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package tech.easyflow.usercenter.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.alicp.jetcache.Cache;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.slf4j.Logger;
|
||||
@@ -25,6 +27,8 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
@@ -34,6 +38,8 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.BotTableDef.BOT;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
*
|
||||
@@ -70,6 +76,8 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
||||
private BotPluginService botPluginService;
|
||||
@Resource
|
||||
private BotConversationService conversationMessageService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@GetMapping("/generateConversationId")
|
||||
public Result<Long> generateConversationId() {
|
||||
@@ -188,7 +196,11 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
||||
@GetMapping("getDetail")
|
||||
@SaIgnore
|
||||
public Result<Bot> getDetail(String id) {
|
||||
return Result.ok(botService.getDetail(id));
|
||||
Bot bot = botService.getDetail(id);
|
||||
if (bot != null && StpUtil.isLogin()) {
|
||||
categoryPermissionService.assertCategoryResourceVisible("BOT", bot.getCreatedBy(), bot.getCategoryId(), "无权限访问聊天助手");
|
||||
}
|
||||
return Result.ok(bot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -198,6 +210,9 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
||||
if (data == null) {
|
||||
return Result.ok(data);
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
categoryPermissionService.assertCategoryResourceVisible("BOT", data.getCreatedBy(), data.getCategoryId(), "无权限访问聊天助手");
|
||||
}
|
||||
|
||||
Map<String, Object> llmOptions = data.getModelOptions();
|
||||
if (llmOptions == null) {
|
||||
@@ -229,6 +244,32 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
applyCategoryPermission(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
|
||||
if (!access.isRestricted()) {
|
||||
return;
|
||||
}
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.eq(Bot::getCreatedBy, access.getAccountId());
|
||||
return;
|
||||
}
|
||||
queryWrapper.and(BOT.CREATED_BY.eq(access.getAccountId()).or(BOT.CATEGORY_ID.in(access.getCategoryIds())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {
|
||||
|
||||
|
||||
@@ -13,10 +13,15 @@ import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
|
||||
|
||||
/**
|
||||
* 素材库
|
||||
@@ -28,6 +33,9 @@ import java.util.Date;
|
||||
@RequestMapping("/userCenter/resource")
|
||||
@UsePermission(moduleName = "/api/v1/resource")
|
||||
public class UcResourceController extends BaseCurdController<ResourceService, Resource> {
|
||||
@javax.annotation.Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public UcResourceController(ResourceService service) {
|
||||
super(service);
|
||||
}
|
||||
@@ -52,7 +60,36 @@ public class UcResourceController extends BaseCurdController<ResourceService, Re
|
||||
|
||||
@Override
|
||||
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
|
||||
queryWrapper.eq(Resource::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<Resource>> list(Resource entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
applyCategoryPermission(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Resource> detail(String id) {
|
||||
Resource resource = service.getById(id);
|
||||
if (resource != null) {
|
||||
categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材");
|
||||
}
|
||||
return Result.ok(resource);
|
||||
}
|
||||
|
||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
||||
if (!access.isRestricted()) {
|
||||
return;
|
||||
}
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.eq(Resource::getCreatedBy, access.getAccountId());
|
||||
return;
|
||||
}
|
||||
queryWrapper.and(RESOURCE.CREATED_BY.eq(access.getAccountId()).or(RESOURCE.CATEGORY_ID.in(access.getCategoryIds())));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
package tech.easyflow.usercenter.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import tech.easyflow.ai.entity.WorkflowCategory;
|
||||
import tech.easyflow.ai.service.WorkflowCategoryService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工作流分类
|
||||
@@ -21,6 +28,8 @@ import java.util.Collection;
|
||||
@RequestMapping("/userCenter/workflowCategory")
|
||||
@UsePermission(moduleName = "/api/v1/workflow")
|
||||
public class UcWorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
public UcWorkflowCategoryController(WorkflowCategoryService service) {
|
||||
super(service);
|
||||
@@ -35,4 +44,18 @@ public class UcWorkflowCategoryController extends BaseCurdController<WorkflowCat
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
return Result.fail("-");
|
||||
}
|
||||
|
||||
@GetMapping("visibleList")
|
||||
public Result<List<WorkflowCategory>> visibleList(WorkflowCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("WORKFLOW");
|
||||
if (access.isRestricted()) {
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
queryWrapper.in(WorkflowCategory::getId, access.getCategoryIds());
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,14 @@ package tech.easyflow.usercenter.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
@@ -20,6 +23,10 @@ import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
@@ -45,6 +52,8 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
private TinyFlowService tinyFlowService;
|
||||
@Resource
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
@Resource
|
||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||
|
||||
public UcWorkflowController(WorkflowService service) {
|
||||
super(service);
|
||||
@@ -55,6 +64,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
*/
|
||||
@PostMapping("/singleRun")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.USE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#workflowId",
|
||||
denyMessage = "无权限运行工作流"
|
||||
)
|
||||
public Result<?> singleRun(
|
||||
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
||||
@JsonBody(value = "nodeId", required = true) String nodeId,
|
||||
@@ -73,6 +89,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
*/
|
||||
@PostMapping("/runAsync")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.USE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限运行工作流"
|
||||
)
|
||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||
@JsonBody("variables") Map<String, Object> variables) {
|
||||
if (variables == null) {
|
||||
@@ -94,6 +117,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
* 获取工作流运行状态 - v2
|
||||
*/
|
||||
@PostMapping("/getChainStatus")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.USE,
|
||||
lookup = ResourceLookup.EXEC_KEY,
|
||||
idExpr = "#executeId",
|
||||
denyMessage = "无权限访问该执行记录"
|
||||
)
|
||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||
@@ -105,6 +135,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
*/
|
||||
@PostMapping("/resume")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.USE,
|
||||
lookup = ResourceLookup.EXEC_KEY,
|
||||
idExpr = "#executeId",
|
||||
denyMessage = "无权限恢复工作流执行"
|
||||
)
|
||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||
@@ -116,6 +153,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
*/
|
||||
@GetMapping("getRunningParameters")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限访问工作流"
|
||||
)
|
||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
||||
Workflow workflow = service.getById(id);
|
||||
|
||||
@@ -146,4 +190,32 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
return Result.fail("-");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<Workflow>> list(Workflow entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping("detail")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限访问工作流"
|
||||
)
|
||||
public Result<Workflow> detail(String id) {
|
||||
Workflow workflow = service.getDetail(id);
|
||||
return Result.ok(workflow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<!--使用
|
||||
enjoy 模板引擎-->
|
||||
<dependency>
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
package tech.easyflow.ai.documentimport;
|
||||
|
||||
import com.easyagents.rag.core.RagChunk;
|
||||
import com.easyagents.rag.ingestion.model.AnalysisResult;
|
||||
import com.easyagents.rag.ingestion.model.StrategyConfig;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class DocumentImportDtos {
|
||||
|
||||
private DocumentImportDtos() {
|
||||
}
|
||||
|
||||
public static class FileItem implements Serializable {
|
||||
private String filePath;
|
||||
private String fileName;
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AnalyzeRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private List<FileItem> files = new ArrayList<FileItem>();
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public List<FileItem> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public void setFiles(List<FileItem> files) {
|
||||
this.files = files;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PreviewFileRequest implements Serializable {
|
||||
private String filePath;
|
||||
private String fileName;
|
||||
private StrategyConfig strategyConfig = StrategyConfig.defaults();
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public StrategyConfig getStrategyConfig() {
|
||||
return strategyConfig;
|
||||
}
|
||||
|
||||
public void setStrategyConfig(StrategyConfig strategyConfig) {
|
||||
this.strategyConfig = strategyConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PreviewRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private List<PreviewFileRequest> files = new ArrayList<PreviewFileRequest>();
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public List<PreviewFileRequest> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public void setFiles(List<PreviewFileRequest> files) {
|
||||
this.files = files;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommitRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private List<String> previewSessionIds = new ArrayList<String>();
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public List<String> getPreviewSessionIds() {
|
||||
return previewSessionIds;
|
||||
}
|
||||
|
||||
public void setPreviewSessionIds(List<String> previewSessionIds) {
|
||||
this.previewSessionIds = previewSessionIds;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SplitterProfileSaveRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private String defaultStrategyCode;
|
||||
private Boolean autoRecommendEnabled;
|
||||
private String fallbackStrategyCode;
|
||||
private Map<String, Object> strategyProfiles = new LinkedHashMap<String, Object>();
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getDefaultStrategyCode() {
|
||||
return defaultStrategyCode;
|
||||
}
|
||||
|
||||
public void setDefaultStrategyCode(String defaultStrategyCode) {
|
||||
this.defaultStrategyCode = defaultStrategyCode;
|
||||
}
|
||||
|
||||
public Boolean getAutoRecommendEnabled() {
|
||||
return autoRecommendEnabled;
|
||||
}
|
||||
|
||||
public void setAutoRecommendEnabled(Boolean autoRecommendEnabled) {
|
||||
this.autoRecommendEnabled = autoRecommendEnabled;
|
||||
}
|
||||
|
||||
public String getFallbackStrategyCode() {
|
||||
return fallbackStrategyCode;
|
||||
}
|
||||
|
||||
public void setFallbackStrategyCode(String fallbackStrategyCode) {
|
||||
this.fallbackStrategyCode = fallbackStrategyCode;
|
||||
}
|
||||
|
||||
public Map<String, Object> getStrategyProfiles() {
|
||||
return strategyProfiles;
|
||||
}
|
||||
|
||||
public void setStrategyProfiles(Map<String, Object> strategyProfiles) {
|
||||
this.strategyProfiles = strategyProfiles;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AnalyzeItem implements Serializable {
|
||||
private String filePath;
|
||||
private String fileName;
|
||||
private AnalysisResult analysis;
|
||||
private StrategyConfig strategyConfig = StrategyConfig.defaults();
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public AnalysisResult getAnalysis() {
|
||||
return analysis;
|
||||
}
|
||||
|
||||
public void setAnalysis(AnalysisResult analysis) {
|
||||
this.analysis = analysis;
|
||||
}
|
||||
|
||||
public StrategyConfig getStrategyConfig() {
|
||||
return strategyConfig;
|
||||
}
|
||||
|
||||
public void setStrategyConfig(StrategyConfig strategyConfig) {
|
||||
this.strategyConfig = strategyConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AnalyzeResponse implements Serializable {
|
||||
private Integer total;
|
||||
private List<AnalyzeItem> items = new ArrayList<AnalyzeItem>();
|
||||
|
||||
public Integer getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(Integer total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public List<AnalyzeItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<AnalyzeItem> items) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PreviewFileResult implements Serializable {
|
||||
private String previewSessionId;
|
||||
private String filePath;
|
||||
private String fileName;
|
||||
private String strategyCode;
|
||||
private String strategyLabel;
|
||||
private AnalysisResult analysis;
|
||||
private Integer totalChunks;
|
||||
private Integer totalWarnings;
|
||||
private List<RagChunk> chunks = new ArrayList<RagChunk>();
|
||||
|
||||
public String getPreviewSessionId() {
|
||||
return previewSessionId;
|
||||
}
|
||||
|
||||
public void setPreviewSessionId(String previewSessionId) {
|
||||
this.previewSessionId = previewSessionId;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getStrategyCode() {
|
||||
return strategyCode;
|
||||
}
|
||||
|
||||
public void setStrategyCode(String strategyCode) {
|
||||
this.strategyCode = strategyCode;
|
||||
}
|
||||
|
||||
public String getStrategyLabel() {
|
||||
return strategyLabel;
|
||||
}
|
||||
|
||||
public void setStrategyLabel(String strategyLabel) {
|
||||
this.strategyLabel = strategyLabel;
|
||||
}
|
||||
|
||||
public AnalysisResult getAnalysis() {
|
||||
return analysis;
|
||||
}
|
||||
|
||||
public void setAnalysis(AnalysisResult analysis) {
|
||||
this.analysis = analysis;
|
||||
}
|
||||
|
||||
public Integer getTotalChunks() {
|
||||
return totalChunks;
|
||||
}
|
||||
|
||||
public void setTotalChunks(Integer totalChunks) {
|
||||
this.totalChunks = totalChunks;
|
||||
}
|
||||
|
||||
public Integer getTotalWarnings() {
|
||||
return totalWarnings;
|
||||
}
|
||||
|
||||
public void setTotalWarnings(Integer totalWarnings) {
|
||||
this.totalWarnings = totalWarnings;
|
||||
}
|
||||
|
||||
public List<RagChunk> getChunks() {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
public void setChunks(List<RagChunk> chunks) {
|
||||
this.chunks = chunks;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PreviewResponse implements Serializable {
|
||||
private Integer totalFiles;
|
||||
private Integer totalChunks;
|
||||
private List<PreviewFileResult> items = new ArrayList<PreviewFileResult>();
|
||||
|
||||
public Integer getTotalFiles() {
|
||||
return totalFiles;
|
||||
}
|
||||
|
||||
public void setTotalFiles(Integer totalFiles) {
|
||||
this.totalFiles = totalFiles;
|
||||
}
|
||||
|
||||
public Integer getTotalChunks() {
|
||||
return totalChunks;
|
||||
}
|
||||
|
||||
public void setTotalChunks(Integer totalChunks) {
|
||||
this.totalChunks = totalChunks;
|
||||
}
|
||||
|
||||
public List<PreviewFileResult> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<PreviewFileResult> items) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommitFileResult implements Serializable {
|
||||
private String previewSessionId;
|
||||
private String fileName;
|
||||
private Boolean success;
|
||||
private String reason;
|
||||
private BigInteger documentId;
|
||||
private Integer chunkCount;
|
||||
|
||||
public String getPreviewSessionId() {
|
||||
return previewSessionId;
|
||||
}
|
||||
|
||||
public void setPreviewSessionId(String previewSessionId) {
|
||||
this.previewSessionId = previewSessionId;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public Boolean getSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(Boolean success) {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
|
||||
public Integer getChunkCount() {
|
||||
return chunkCount;
|
||||
}
|
||||
|
||||
public void setChunkCount(Integer chunkCount) {
|
||||
this.chunkCount = chunkCount;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommitResponse implements Serializable {
|
||||
private Integer totalFiles;
|
||||
private Integer successCount;
|
||||
private Integer errorCount;
|
||||
private List<CommitFileResult> results = new ArrayList<CommitFileResult>();
|
||||
|
||||
public Integer getTotalFiles() {
|
||||
return totalFiles;
|
||||
}
|
||||
|
||||
public void setTotalFiles(Integer totalFiles) {
|
||||
this.totalFiles = totalFiles;
|
||||
}
|
||||
|
||||
public Integer getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public void setSuccessCount(Integer successCount) {
|
||||
this.successCount = successCount;
|
||||
}
|
||||
|
||||
public Integer getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
public void setErrorCount(Integer errorCount) {
|
||||
this.errorCount = errorCount;
|
||||
}
|
||||
|
||||
public List<CommitFileResult> getResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
public void setResults(List<CommitFileResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PreviewSession implements Serializable {
|
||||
private String sessionId;
|
||||
private BigInteger knowledgeId;
|
||||
private String filePath;
|
||||
private String fileName;
|
||||
private String sourceFormat;
|
||||
private StrategyConfig strategyConfig;
|
||||
private AnalysisResult analysis;
|
||||
private Document document;
|
||||
private List<DocumentChunk> documentChunks = new ArrayList<DocumentChunk>();
|
||||
private List<RagChunk> previewChunks = new ArrayList<RagChunk>();
|
||||
private Date createdAt;
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getSourceFormat() {
|
||||
return sourceFormat;
|
||||
}
|
||||
|
||||
public void setSourceFormat(String sourceFormat) {
|
||||
this.sourceFormat = sourceFormat;
|
||||
}
|
||||
|
||||
public StrategyConfig getStrategyConfig() {
|
||||
return strategyConfig;
|
||||
}
|
||||
|
||||
public void setStrategyConfig(StrategyConfig strategyConfig) {
|
||||
this.strategyConfig = strategyConfig;
|
||||
}
|
||||
|
||||
public AnalysisResult getAnalysis() {
|
||||
return analysis;
|
||||
}
|
||||
|
||||
public void setAnalysis(AnalysisResult analysis) {
|
||||
this.analysis = analysis;
|
||||
}
|
||||
|
||||
public Document getDocument() {
|
||||
return document;
|
||||
}
|
||||
|
||||
public void setDocument(Document document) {
|
||||
this.document = document;
|
||||
}
|
||||
|
||||
public List<DocumentChunk> getDocumentChunks() {
|
||||
return documentChunks;
|
||||
}
|
||||
|
||||
public void setDocumentChunks(List<DocumentChunk> documentChunks) {
|
||||
this.documentChunks = documentChunks;
|
||||
}
|
||||
|
||||
public List<RagChunk> getPreviewChunks() {
|
||||
return previewChunks;
|
||||
}
|
||||
|
||||
public void setPreviewChunks(List<RagChunk> previewChunks) {
|
||||
this.previewChunks = previewChunks;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package tech.easyflow.ai.documentimport;
|
||||
|
||||
public final class DocumentImportKeys {
|
||||
|
||||
private DocumentImportKeys() {
|
||||
}
|
||||
|
||||
public static final String CACHE_KEY_PREFIX = "easyflow:document:import:preview:";
|
||||
|
||||
public static final String KEY_SPLITTER_DEFAULT_STRATEGY = "splitter.defaultStrategyCode";
|
||||
public static final String KEY_SPLITTER_AUTO_RECOMMEND_ENABLED = "splitter.autoRecommendEnabled";
|
||||
public static final String KEY_SPLITTER_FALLBACK_STRATEGY = "splitter.fallbackStrategyCode";
|
||||
public static final String KEY_SPLITTER_STRATEGY_PROFILES = "splitter.strategyProfiles";
|
||||
|
||||
public static final String KEY_DOCUMENT_STRATEGY_CODE = "splitter.strategyCode";
|
||||
public static final String KEY_DOCUMENT_STRATEGY_LABEL = "splitter.strategyLabel";
|
||||
public static final String KEY_DOCUMENT_STRATEGY_SNAPSHOT = "splitter.strategySnapshot";
|
||||
public static final String KEY_DOCUMENT_ANALYSIS_SUMMARY = "splitter.analysisSummary";
|
||||
public static final String KEY_DOCUMENT_SOURCE_FILE_EXT = "splitter.sourceFileExt";
|
||||
public static final String KEY_DOCUMENT_PREVIEW_VERSION = "splitter.previewVersion";
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.documentimport;
|
||||
|
||||
import com.alicp.jetcache.Cache;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class DocumentImportPreviewService {
|
||||
|
||||
private static final Duration SESSION_TTL = Duration.ofMinutes(30);
|
||||
|
||||
private final Cache<String, Object> defaultCache;
|
||||
|
||||
public DocumentImportPreviewService(@Qualifier("defaultCache") Cache<String, Object> defaultCache) {
|
||||
this.defaultCache = defaultCache;
|
||||
}
|
||||
|
||||
public String put(DocumentImportDtos.PreviewSession session) {
|
||||
String sessionId = UUID.randomUUID().toString().replace("-", "");
|
||||
session.setSessionId(sessionId);
|
||||
defaultCache.put(buildKey(sessionId), session, SESSION_TTL.toMinutes(), TimeUnit.MINUTES);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public DocumentImportDtos.PreviewSession getRequired(String sessionId) {
|
||||
Object cached = defaultCache.get(buildKey(sessionId));
|
||||
if (!(cached instanceof DocumentImportDtos.PreviewSession)) {
|
||||
throw new BusinessException("预览会话已失效,请重新生成预览");
|
||||
}
|
||||
return (DocumentImportDtos.PreviewSession) cached;
|
||||
}
|
||||
|
||||
public void remove(String sessionId) {
|
||||
defaultCache.remove(buildKey(sessionId));
|
||||
}
|
||||
|
||||
private String buildKey(String sessionId) {
|
||||
return DocumentImportKeys.CACHE_KEY_PREFIX + sessionId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package tech.easyflow.ai.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
public class ModelInvokeConfigDtos {
|
||||
|
||||
public static class UpdateRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private BigInteger id;
|
||||
private String invokeCode;
|
||||
private Boolean publishEnabled;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getInvokeCode() {
|
||||
return invokeCode;
|
||||
}
|
||||
|
||||
public void setInvokeCode(String invokeCode) {
|
||||
this.invokeCode = invokeCode;
|
||||
}
|
||||
|
||||
public Boolean getPublishEnabled() {
|
||||
return publishEnabled;
|
||||
}
|
||||
|
||||
public void setPublishEnabled(Boolean publishEnabled) {
|
||||
this.publishEnabled = publishEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BatchPublishRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private List<BigInteger> ids;
|
||||
private Boolean publishEnabled;
|
||||
|
||||
public List<BigInteger> getIds() {
|
||||
return ids;
|
||||
}
|
||||
|
||||
public void setIds(List<BigInteger> ids) {
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
public Boolean getPublishEnabled() {
|
||||
return publishEnabled;
|
||||
}
|
||||
|
||||
public void setPublishEnabled(Boolean publishEnabled) {
|
||||
this.publishEnabled = publishEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import tech.easyflow.ai.entity.base.DocumentCollectionBase;
|
||||
import tech.easyflow.common.util.PropertiesUtil;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
@@ -32,7 +33,7 @@ import java.util.Map;
|
||||
*/
|
||||
|
||||
@Table("tb_document_collection")
|
||||
public class DocumentCollection extends DocumentCollectionBase {
|
||||
public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource {
|
||||
|
||||
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
||||
public static final String TYPE_FAQ = "FAQ";
|
||||
@@ -71,6 +72,10 @@ public class DocumentCollection extends DocumentCollectionBase {
|
||||
* 是否启用重排模型
|
||||
*/
|
||||
public static final String KEY_RERANK_ENABLE = "rerankEnable";
|
||||
public static final String KEY_SPLITTER_DEFAULT_STRATEGY = "splitter.defaultStrategyCode";
|
||||
public static final String KEY_SPLITTER_AUTO_RECOMMEND_ENABLED = "splitter.autoRecommendEnabled";
|
||||
public static final String KEY_SPLITTER_FALLBACK_STRATEGY = "splitter.fallbackStrategyCode";
|
||||
public static final String KEY_SPLITTER_STRATEGY_PROFILES = "splitter.strategyProfiles";
|
||||
|
||||
public DocumentStore toDocumentStore() {
|
||||
String storeType = this.getVectorStoreType();
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||
import tech.easyflow.ai.entity.base.WorkflowBase;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
/**
|
||||
* 实体类。
|
||||
@@ -13,7 +14,7 @@ import tech.easyflow.ai.entity.base.WorkflowBase;
|
||||
*/
|
||||
|
||||
@Table("tb_workflow")
|
||||
public class Workflow extends WorkflowBase {
|
||||
public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||
|
||||
public Tool toFunction(boolean needEnglishName) {
|
||||
return new WorkflowTool(this, needEnglishName);
|
||||
|
||||
@@ -3,8 +3,10 @@ package tech.easyflow.ai.entity.base;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class DocumentChunkBase implements Serializable {
|
||||
@@ -38,6 +40,12 @@ public class DocumentChunkBase implements Serializable {
|
||||
@Column(comment = "分割顺序")
|
||||
private Integer sorting;
|
||||
|
||||
/**
|
||||
* 扩展元信息
|
||||
*/
|
||||
@Column(typeHandler = FastjsonTypeHandler.class, comment = "扩展元信息")
|
||||
private Map<String, Object> options;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -78,4 +86,12 @@ public class DocumentChunkBase implements Serializable {
|
||||
this.sorting = sorting;
|
||||
}
|
||||
|
||||
public Map<String, Object> getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public void setOptions(Map<String, Object> options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -160,6 +160,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
|
||||
@Column(comment = "分类ID")
|
||||
private BigInteger categoryId;
|
||||
|
||||
/**
|
||||
* 可见范围
|
||||
*/
|
||||
@Column(comment = "可见范围")
|
||||
private String visibilityScope;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -352,4 +358,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
|
||||
this.categoryId = categoryId;
|
||||
}
|
||||
|
||||
public String getVisibilityScope() {
|
||||
return visibilityScope;
|
||||
}
|
||||
|
||||
public void setVisibilityScope(String visibilityScope) {
|
||||
this.visibilityScope = visibilityScope;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -152,6 +152,18 @@ public class ModelBase implements Serializable {
|
||||
@Column(comment = "是否支持tool消息")
|
||||
private Boolean supportToolMessage;
|
||||
|
||||
/**
|
||||
* 统一模型调用对外标识
|
||||
*/
|
||||
@Column(comment = "统一模型调用对外标识")
|
||||
private String invokeCode;
|
||||
|
||||
/**
|
||||
* 是否开启统一模型调用发布
|
||||
*/
|
||||
@Column(comment = "是否开启统一模型调用发布")
|
||||
private Boolean publishEnabled;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -336,4 +348,20 @@ public class ModelBase implements Serializable {
|
||||
this.supportToolMessage = supportToolMessage;
|
||||
}
|
||||
|
||||
public String getInvokeCode() {
|
||||
return invokeCode;
|
||||
}
|
||||
|
||||
public void setInvokeCode(String invokeCode) {
|
||||
this.invokeCode = invokeCode;
|
||||
}
|
||||
|
||||
public Boolean getPublishEnabled() {
|
||||
return publishEnabled;
|
||||
}
|
||||
|
||||
public void setPublishEnabled(Boolean publishEnabled) {
|
||||
this.publishEnabled = publishEnabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
|
||||
@Column(comment = "分类ID")
|
||||
private BigInteger categoryId;
|
||||
|
||||
/**
|
||||
* 可见范围
|
||||
*/
|
||||
@Column(comment = "可见范围")
|
||||
private String visibilityScope;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -223,4 +229,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
|
||||
this.categoryId = categoryId;
|
||||
}
|
||||
|
||||
public String getVisibilityScope() {
|
||||
return visibilityScope;
|
||||
}
|
||||
|
||||
public void setVisibilityScope(String visibilityScope) {
|
||||
this.visibilityScope = visibilityScope;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package tech.easyflow.ai.invoke.exception;
|
||||
|
||||
public class ModelInvokeException extends RuntimeException {
|
||||
|
||||
private final int status;
|
||||
private final String type;
|
||||
private final String param;
|
||||
private final String code;
|
||||
|
||||
public ModelInvokeException(int status, String message, String type, String param, String code) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.type = type;
|
||||
this.param = param;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getParam() {
|
||||
return param;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static ModelInvokeException badRequest(String message) {
|
||||
return badRequest(message, null, "invalid_request_error");
|
||||
}
|
||||
|
||||
public static ModelInvokeException badRequest(String message, String param, String code) {
|
||||
return new ModelInvokeException(400, message, "invalid_request_error", param, code);
|
||||
}
|
||||
|
||||
public static ModelInvokeException unauthorized(String message) {
|
||||
return new ModelInvokeException(401, message, "authentication_error", null, "unauthorized");
|
||||
}
|
||||
|
||||
public static ModelInvokeException forbidden(String message) {
|
||||
return new ModelInvokeException(403, message, "permission_error", null, "forbidden");
|
||||
}
|
||||
|
||||
public static ModelInvokeException notFound(String message) {
|
||||
return new ModelInvokeException(404, message, "not_found_error", "model", "model_not_found");
|
||||
}
|
||||
|
||||
public static ModelInvokeException badGateway(String message) {
|
||||
return new ModelInvokeException(502, message, "api_error", null, "upstream_bad_gateway");
|
||||
}
|
||||
|
||||
public static ModelInvokeException serviceUnavailable(String message) {
|
||||
return new ModelInvokeException(503, message, "service_unavailable_error", null, "upstream_unavailable");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
package tech.easyflow.ai.invoke.mapper;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChoice;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedContentPart;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedImageUrl;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedMessage;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedResponseFormat;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedTool;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedToolCall;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedToolCallFunction;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedToolFunction;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedUsage;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionChunkResponse;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class OpenAiProtocolMapper {
|
||||
|
||||
private static final Set<String> ROOT_FIELDS = Set.of(
|
||||
"model", "messages", "stream", "temperature", "top_p", "max_tokens",
|
||||
"seed", "tools", "tool_choice", "response_format"
|
||||
);
|
||||
private static final Set<String> MESSAGE_FIELDS = Set.of(
|
||||
"role", "content", "name", "tool_call_id", "tool_calls"
|
||||
);
|
||||
private static final Set<String> CONTENT_PART_FIELDS = Set.of("type", "text", "image_url");
|
||||
private static final Set<String> IMAGE_URL_FIELDS = Set.of("url", "detail");
|
||||
private static final Set<String> TOOL_FIELDS = Set.of("type", "function");
|
||||
private static final Set<String> TOOL_FUNCTION_FIELDS = Set.of("name", "description", "parameters");
|
||||
private static final Set<String> RESPONSE_FORMAT_FIELDS = Set.of("type", "json_schema");
|
||||
private static final Set<String> TOOL_CHOICE_FIELDS = Set.of("type", "function");
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public OpenAiProtocolMapper(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public OpenAiChatCompletionRequest readRequest(String rawBody) {
|
||||
JsonNode rootNode;
|
||||
try {
|
||||
rootNode = objectMapper.readTree(rawBody);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw ModelInvokeException.badRequest("请求体不是合法 JSON", null, "invalid_json");
|
||||
}
|
||||
|
||||
if (rootNode == null || !rootNode.isObject()) {
|
||||
throw ModelInvokeException.badRequest("请求体必须为 JSON 对象", null, "invalid_json");
|
||||
}
|
||||
validateAllowedFields(rootNode, ROOT_FIELDS, null);
|
||||
validateMessages(rootNode.path("messages"));
|
||||
validateTools(rootNode.path("tools"));
|
||||
validateToolChoice(rootNode.path("tool_choice"));
|
||||
validateResponseFormat(rootNode.path("response_format"));
|
||||
|
||||
try {
|
||||
OpenAiChatCompletionRequest request = buildRequest(rootNode);
|
||||
if (StrUtil.isBlank(request.getModel())) {
|
||||
throw ModelInvokeException.badRequest("model 不能为空", "model", "model_required");
|
||||
}
|
||||
if (request.getMessages() == null || request.getMessages().isEmpty()) {
|
||||
throw ModelInvokeException.badRequest("messages 不能为空", "messages", "messages_required");
|
||||
}
|
||||
return request;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ModelInvokeException.badRequest(e.getMessage(), "messages", "invalid_message_content");
|
||||
}
|
||||
}
|
||||
|
||||
private OpenAiChatCompletionRequest buildRequest(JsonNode rootNode) {
|
||||
OpenAiChatCompletionRequest request = new OpenAiChatCompletionRequest();
|
||||
request.setModel(textValue(rootNode, "model"));
|
||||
request.setMessages(buildMessages(rootNode.path("messages")));
|
||||
if (rootNode.has("stream") && !rootNode.get("stream").isNull()) {
|
||||
request.setStream(rootNode.get("stream").asBoolean());
|
||||
}
|
||||
if (rootNode.has("temperature") && !rootNode.get("temperature").isNull()) {
|
||||
request.setTemperature(rootNode.get("temperature").asDouble());
|
||||
}
|
||||
if (rootNode.has("top_p") && !rootNode.get("top_p").isNull()) {
|
||||
request.setTopP(rootNode.get("top_p").asDouble());
|
||||
}
|
||||
if (rootNode.has("max_tokens") && !rootNode.get("max_tokens").isNull()) {
|
||||
request.setMaxTokens(rootNode.get("max_tokens").asInt());
|
||||
}
|
||||
if (rootNode.has("seed") && !rootNode.get("seed").isNull()) {
|
||||
request.setSeed(rootNode.get("seed").asLong());
|
||||
}
|
||||
if (rootNode.has("tools") && rootNode.get("tools").isArray()) {
|
||||
request.setTools(objectMapper.convertValue(
|
||||
rootNode.get("tools"),
|
||||
new TypeReference<>() {
|
||||
}
|
||||
));
|
||||
}
|
||||
if (rootNode.has("tool_choice") && !rootNode.get("tool_choice").isNull()) {
|
||||
request.setToolChoice(rootNode.get("tool_choice"));
|
||||
}
|
||||
if (rootNode.has("response_format") && rootNode.get("response_format").isObject()) {
|
||||
request.setResponseFormat(objectMapper.convertValue(
|
||||
rootNode.get("response_format"),
|
||||
OpenAiChatCompletionRequest.ResponseFormat.class
|
||||
));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private List<OpenAiChatCompletionRequest.Message> buildMessages(JsonNode messagesNode) {
|
||||
List<OpenAiChatCompletionRequest.Message> messages = new ArrayList<>();
|
||||
for (JsonNode messageNode : messagesNode) {
|
||||
OpenAiChatCompletionRequest.Message message = new OpenAiChatCompletionRequest.Message();
|
||||
message.setRole(textValue(messageNode, "role"));
|
||||
if (messageNode.has("content")) {
|
||||
message.setContentNode(messageNode.get("content"));
|
||||
}
|
||||
message.setName(textValue(messageNode, "name"));
|
||||
message.setToolCallId(textValue(messageNode, "tool_call_id"));
|
||||
if (messageNode.has("tool_calls") && messageNode.get("tool_calls").isArray()) {
|
||||
message.setToolCalls(objectMapper.convertValue(
|
||||
messageNode.get("tool_calls"),
|
||||
new TypeReference<>() {
|
||||
}
|
||||
));
|
||||
}
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
public UnifiedChatRequest toUnifiedRequest(OpenAiChatCompletionRequest request) {
|
||||
UnifiedChatRequest unifiedRequest = new UnifiedChatRequest();
|
||||
unifiedRequest.setModel(request.getModel());
|
||||
unifiedRequest.setMessages(toUnifiedMessages(request.getMessages()));
|
||||
unifiedRequest.setStream(Boolean.TRUE.equals(request.getStream()));
|
||||
unifiedRequest.setTemperature(request.getTemperature());
|
||||
unifiedRequest.setTopP(request.getTopP());
|
||||
unifiedRequest.setMaxTokens(request.getMaxTokens());
|
||||
unifiedRequest.setSeed(request.getSeed());
|
||||
unifiedRequest.setTools(toUnifiedTools(request.getTools()));
|
||||
unifiedRequest.setToolChoice(request.getToolChoice());
|
||||
unifiedRequest.setResponseFormat(toUnifiedResponseFormat(request.getResponseFormat()));
|
||||
return unifiedRequest;
|
||||
}
|
||||
|
||||
public OpenAiChatCompletionRequest toOpenAiRequest(UnifiedChatRequest request) {
|
||||
OpenAiChatCompletionRequest openAiRequest = new OpenAiChatCompletionRequest();
|
||||
openAiRequest.setModel(request.getModel());
|
||||
openAiRequest.setMessages(toOpenAiMessages(request.getMessages()));
|
||||
openAiRequest.setStream(request.getStream());
|
||||
openAiRequest.setTemperature(request.getTemperature());
|
||||
openAiRequest.setTopP(request.getTopP());
|
||||
openAiRequest.setMaxTokens(request.getMaxTokens());
|
||||
openAiRequest.setSeed(request.getSeed());
|
||||
openAiRequest.setTools(toOpenAiTools(request.getTools()));
|
||||
openAiRequest.setToolChoice(request.getToolChoice());
|
||||
openAiRequest.setResponseFormat(toOpenAiResponseFormat(request.getResponseFormat()));
|
||||
return openAiRequest;
|
||||
}
|
||||
|
||||
public UnifiedChatResponse toUnifiedResponse(OpenAiChatCompletionResponse response) {
|
||||
UnifiedChatResponse unifiedResponse = new UnifiedChatResponse();
|
||||
unifiedResponse.setId(response.getId());
|
||||
unifiedResponse.setObject(response.getObject());
|
||||
unifiedResponse.setCreated(response.getCreated());
|
||||
unifiedResponse.setModel(response.getModel());
|
||||
unifiedResponse.setChoices(toUnifiedChoices(response.getChoices(), false));
|
||||
unifiedResponse.setUsage(toUnifiedUsage(response.getUsage()));
|
||||
return unifiedResponse;
|
||||
}
|
||||
|
||||
public UnifiedChatChunk toUnifiedChunk(OpenAiChatCompletionChunkResponse chunk) {
|
||||
UnifiedChatChunk unifiedChunk = new UnifiedChatChunk();
|
||||
unifiedChunk.setId(chunk.getId());
|
||||
unifiedChunk.setObject(chunk.getObject());
|
||||
unifiedChunk.setCreated(chunk.getCreated());
|
||||
unifiedChunk.setModel(chunk.getModel());
|
||||
unifiedChunk.setChoices(toUnifiedChunkChoices(chunk.getChoices()));
|
||||
unifiedChunk.setUsage(toUnifiedUsage(chunk.getUsage()));
|
||||
return unifiedChunk;
|
||||
}
|
||||
|
||||
public OpenAiChatCompletionResponse toOpenAiResponse(UnifiedChatResponse response) {
|
||||
OpenAiChatCompletionResponse openAiResponse = new OpenAiChatCompletionResponse();
|
||||
openAiResponse.setId(response.getId());
|
||||
openAiResponse.setObject(response.getObject());
|
||||
openAiResponse.setCreated(response.getCreated());
|
||||
openAiResponse.setModel(response.getModel());
|
||||
openAiResponse.setChoices(toOpenAiChoices(response.getChoices(), false));
|
||||
openAiResponse.setUsage(toOpenAiUsage(response.getUsage()));
|
||||
return openAiResponse;
|
||||
}
|
||||
|
||||
public OpenAiChatCompletionChunkResponse toOpenAiChunk(UnifiedChatChunk chunk) {
|
||||
OpenAiChatCompletionChunkResponse openAiChunk = new OpenAiChatCompletionChunkResponse();
|
||||
openAiChunk.setId(chunk.getId());
|
||||
openAiChunk.setObject(chunk.getObject());
|
||||
openAiChunk.setCreated(chunk.getCreated());
|
||||
openAiChunk.setModel(chunk.getModel());
|
||||
openAiChunk.setChoices(toOpenAiChunkChoices(chunk.getChoices()));
|
||||
openAiChunk.setUsage(toOpenAiUsage(chunk.getUsage()));
|
||||
return openAiChunk;
|
||||
}
|
||||
|
||||
private void validateMessages(JsonNode messagesNode) {
|
||||
if (messagesNode == null || !messagesNode.isArray() || messagesNode.isEmpty()) {
|
||||
throw ModelInvokeException.badRequest("messages 必须为非空数组", "messages", "messages_required");
|
||||
}
|
||||
for (int i = 0; i < messagesNode.size(); i++) {
|
||||
JsonNode messageNode = messagesNode.get(i);
|
||||
if (!messageNode.isObject()) {
|
||||
throw ModelInvokeException.badRequest("messages[" + i + "] 必须为对象", "messages", "invalid_message");
|
||||
}
|
||||
validateAllowedFields(messageNode, MESSAGE_FIELDS, "messages[" + i + "]");
|
||||
String role = textValue(messageNode, "role");
|
||||
if (!Set.of("system", "developer", "user", "assistant", "tool").contains(role)) {
|
||||
throw ModelInvokeException.badRequest("messages[" + i + "].role 不受支持", "messages", "invalid_message_role");
|
||||
}
|
||||
JsonNode contentNode = messageNode.get("content");
|
||||
if (contentNode != null && !contentNode.isNull() && !contentNode.isTextual() && !contentNode.isArray()) {
|
||||
throw ModelInvokeException.badRequest("messages[" + i + "].content 仅支持字符串或数组", "messages", "invalid_message_content");
|
||||
}
|
||||
if (contentNode != null && contentNode.isArray()) {
|
||||
validateContentParts(contentNode, "messages[" + i + "].content");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateContentParts(JsonNode contentNode, String path) {
|
||||
for (int i = 0; i < contentNode.size(); i++) {
|
||||
JsonNode partNode = contentNode.get(i);
|
||||
if (!partNode.isObject()) {
|
||||
throw ModelInvokeException.badRequest(path + "[" + i + "] 必须为对象", "messages", "invalid_content_part");
|
||||
}
|
||||
validateAllowedFields(partNode, CONTENT_PART_FIELDS, path + "[" + i + "]");
|
||||
String type = textValue(partNode, "type");
|
||||
if (!Set.of("text", "image_url").contains(type)) {
|
||||
throw ModelInvokeException.badRequest(path + "[" + i + "].type 不受支持", "messages", "content_part_not_supported");
|
||||
}
|
||||
if ("text".equals(type) && StrUtil.isBlank(textValue(partNode, "text"))) {
|
||||
throw ModelInvokeException.badRequest(path + "[" + i + "].text 不能为空", "messages", "content_text_required");
|
||||
}
|
||||
if ("image_url".equals(type)) {
|
||||
JsonNode imageUrlNode = partNode.get("image_url");
|
||||
if (imageUrlNode == null || !imageUrlNode.isObject()) {
|
||||
throw ModelInvokeException.badRequest(path + "[" + i + "].image_url 必须为对象", "messages", "image_url_required");
|
||||
}
|
||||
validateAllowedFields(imageUrlNode, IMAGE_URL_FIELDS, path + "[" + i + "].image_url");
|
||||
if (StrUtil.isBlank(textValue(imageUrlNode, "url"))) {
|
||||
throw ModelInvokeException.badRequest(path + "[" + i + "].image_url.url 不能为空", "messages", "image_url_required");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateTools(JsonNode toolsNode) {
|
||||
if (isAbsentNode(toolsNode)) {
|
||||
return;
|
||||
}
|
||||
if (!toolsNode.isArray()) {
|
||||
throw ModelInvokeException.badRequest("tools 必须为数组", "tools", "invalid_tools");
|
||||
}
|
||||
for (int i = 0; i < toolsNode.size(); i++) {
|
||||
JsonNode toolNode = toolsNode.get(i);
|
||||
if (!toolNode.isObject()) {
|
||||
throw ModelInvokeException.badRequest("tools[" + i + "] 必须为对象", "tools", "invalid_tools");
|
||||
}
|
||||
validateAllowedFields(toolNode, TOOL_FIELDS, "tools[" + i + "]");
|
||||
JsonNode functionNode = toolNode.get("function");
|
||||
if (functionNode == null || !functionNode.isObject()) {
|
||||
throw ModelInvokeException.badRequest("tools[" + i + "].function 必须为对象", "tools", "invalid_tool_function");
|
||||
}
|
||||
validateAllowedFields(functionNode, TOOL_FUNCTION_FIELDS, "tools[" + i + "].function");
|
||||
if (StrUtil.isBlank(textValue(functionNode, "name"))) {
|
||||
throw ModelInvokeException.badRequest("tools[" + i + "].function.name 不能为空", "tools", "tool_name_required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateToolChoice(JsonNode toolChoiceNode) {
|
||||
if (isAbsentNode(toolChoiceNode)) {
|
||||
return;
|
||||
}
|
||||
if (toolChoiceNode.isTextual()) {
|
||||
return;
|
||||
}
|
||||
if (!toolChoiceNode.isObject()) {
|
||||
throw ModelInvokeException.badRequest("tool_choice 仅支持字符串或对象", "tool_choice", "invalid_tool_choice");
|
||||
}
|
||||
validateAllowedFields(toolChoiceNode, TOOL_CHOICE_FIELDS, "tool_choice");
|
||||
}
|
||||
|
||||
private void validateResponseFormat(JsonNode responseFormatNode) {
|
||||
if (isAbsentNode(responseFormatNode)) {
|
||||
return;
|
||||
}
|
||||
if (!responseFormatNode.isObject()) {
|
||||
throw ModelInvokeException.badRequest("response_format 必须为对象", "response_format", "invalid_response_format");
|
||||
}
|
||||
validateAllowedFields(responseFormatNode, RESPONSE_FORMAT_FIELDS, "response_format");
|
||||
if (StrUtil.isBlank(textValue(responseFormatNode, "type"))) {
|
||||
throw ModelInvokeException.badRequest("response_format.type 不能为空", "response_format", "response_format_type_required");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAbsentNode(JsonNode node) {
|
||||
return node == null || node.isNull() || node.isMissingNode();
|
||||
}
|
||||
|
||||
private void validateAllowedFields(JsonNode node, Set<String> allowedFields, String path) {
|
||||
Iterator<String> fieldNames = node.fieldNames();
|
||||
Set<String> unsupportedFields = new LinkedHashSet<>();
|
||||
while (fieldNames.hasNext()) {
|
||||
String fieldName = fieldNames.next();
|
||||
if (!allowedFields.contains(fieldName)) {
|
||||
unsupportedFields.add(path == null ? fieldName : path + "." + fieldName);
|
||||
}
|
||||
}
|
||||
if (!unsupportedFields.isEmpty()) {
|
||||
String field = unsupportedFields.iterator().next();
|
||||
throw ModelInvokeException.badRequest("存在未支持字段: " + field, field, "unsupported_field");
|
||||
}
|
||||
}
|
||||
|
||||
private String textValue(JsonNode node, String fieldName) {
|
||||
JsonNode fieldNode = node.get(fieldName);
|
||||
if (fieldNode == null || fieldNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
return fieldNode.asText();
|
||||
}
|
||||
|
||||
private List<UnifiedMessage> toUnifiedMessages(List<OpenAiChatCompletionRequest.Message> messages) {
|
||||
if (messages == null) {
|
||||
return null;
|
||||
}
|
||||
List<UnifiedMessage> result = new ArrayList<>();
|
||||
for (OpenAiChatCompletionRequest.Message message : messages) {
|
||||
UnifiedMessage unifiedMessage = new UnifiedMessage();
|
||||
unifiedMessage.setRole(message.getRole());
|
||||
unifiedMessage.setContent(message.getContent());
|
||||
unifiedMessage.setContentParts(toUnifiedContentParts(message.getContentParts()));
|
||||
unifiedMessage.setName(message.getName());
|
||||
unifiedMessage.setToolCallId(message.getToolCallId());
|
||||
unifiedMessage.setToolCalls(toUnifiedToolCalls(message.getToolCalls()));
|
||||
result.add(unifiedMessage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<OpenAiChatCompletionRequest.Message> toOpenAiMessages(List<UnifiedMessage> messages) {
|
||||
if (messages == null) {
|
||||
return null;
|
||||
}
|
||||
List<OpenAiChatCompletionRequest.Message> result = new ArrayList<>();
|
||||
for (UnifiedMessage message : messages) {
|
||||
OpenAiChatCompletionRequest.Message openAiMessage = new OpenAiChatCompletionRequest.Message();
|
||||
openAiMessage.setRole(message.getRole());
|
||||
if (message.getContentParts() != null && !message.getContentParts().isEmpty()) {
|
||||
ObjectNode contentNode = objectMapper.createObjectNode();
|
||||
contentNode.set("content", objectMapper.valueToTree(toOpenAiContentParts(message.getContentParts())));
|
||||
openAiMessage.setContentNode(contentNode.get("content"));
|
||||
} else {
|
||||
ObjectNode contentNode = objectMapper.createObjectNode();
|
||||
if (message.getContent() == null) {
|
||||
contentNode.putNull("content");
|
||||
} else {
|
||||
contentNode.put("content", message.getContent());
|
||||
}
|
||||
openAiMessage.setContentNode(contentNode.get("content"));
|
||||
}
|
||||
openAiMessage.setName(message.getName());
|
||||
openAiMessage.setToolCallId(message.getToolCallId());
|
||||
openAiMessage.setToolCalls(toOpenAiToolCalls(message.getToolCalls()));
|
||||
result.add(openAiMessage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<UnifiedContentPart> toUnifiedContentParts(List<OpenAiChatCompletionRequest.ContentPart> contentParts) {
|
||||
if (contentParts == null) {
|
||||
return null;
|
||||
}
|
||||
List<UnifiedContentPart> result = new ArrayList<>();
|
||||
for (OpenAiChatCompletionRequest.ContentPart contentPart : contentParts) {
|
||||
UnifiedContentPart unifiedContentPart = new UnifiedContentPart();
|
||||
unifiedContentPart.setType(contentPart.getType());
|
||||
unifiedContentPart.setText(contentPart.getText());
|
||||
if (contentPart.getImageUrl() != null) {
|
||||
UnifiedImageUrl imageUrl = new UnifiedImageUrl();
|
||||
imageUrl.setUrl(contentPart.getImageUrl().getUrl());
|
||||
imageUrl.setDetail(contentPart.getImageUrl().getDetail());
|
||||
unifiedContentPart.setImageUrl(imageUrl);
|
||||
}
|
||||
result.add(unifiedContentPart);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<OpenAiChatCompletionRequest.ContentPart> toOpenAiContentParts(List<UnifiedContentPart> contentParts) {
|
||||
if (contentParts == null) {
|
||||
return null;
|
||||
}
|
||||
List<OpenAiChatCompletionRequest.ContentPart> result = new ArrayList<>();
|
||||
for (UnifiedContentPart contentPart : contentParts) {
|
||||
OpenAiChatCompletionRequest.ContentPart openAiPart = new OpenAiChatCompletionRequest.ContentPart();
|
||||
openAiPart.setType(contentPart.getType());
|
||||
openAiPart.setText(contentPart.getText());
|
||||
if (contentPart.getImageUrl() != null) {
|
||||
OpenAiChatCompletionRequest.ImageUrl imageUrl = new OpenAiChatCompletionRequest.ImageUrl();
|
||||
imageUrl.setUrl(contentPart.getImageUrl().getUrl());
|
||||
imageUrl.setDetail(contentPart.getImageUrl().getDetail());
|
||||
openAiPart.setImageUrl(imageUrl);
|
||||
}
|
||||
result.add(openAiPart);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<UnifiedTool> toUnifiedTools(List<OpenAiChatCompletionRequest.Tool> tools) {
|
||||
if (tools == null) {
|
||||
return null;
|
||||
}
|
||||
List<UnifiedTool> result = new ArrayList<>();
|
||||
for (OpenAiChatCompletionRequest.Tool tool : tools) {
|
||||
UnifiedTool unifiedTool = new UnifiedTool();
|
||||
unifiedTool.setType(tool.getType());
|
||||
if (tool.getFunction() != null) {
|
||||
UnifiedToolFunction function = new UnifiedToolFunction();
|
||||
function.setName(tool.getFunction().getName());
|
||||
function.setDescription(tool.getFunction().getDescription());
|
||||
function.setParameters(tool.getFunction().getParameters());
|
||||
unifiedTool.setFunction(function);
|
||||
}
|
||||
result.add(unifiedTool);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<OpenAiChatCompletionRequest.Tool> toOpenAiTools(List<UnifiedTool> tools) {
|
||||
if (tools == null) {
|
||||
return null;
|
||||
}
|
||||
List<OpenAiChatCompletionRequest.Tool> result = new ArrayList<>();
|
||||
for (UnifiedTool tool : tools) {
|
||||
OpenAiChatCompletionRequest.Tool openAiTool = new OpenAiChatCompletionRequest.Tool();
|
||||
openAiTool.setType(tool.getType());
|
||||
if (tool.getFunction() != null) {
|
||||
OpenAiChatCompletionRequest.ToolFunction function = new OpenAiChatCompletionRequest.ToolFunction();
|
||||
function.setName(tool.getFunction().getName());
|
||||
function.setDescription(tool.getFunction().getDescription());
|
||||
function.setParameters(tool.getFunction().getParameters());
|
||||
openAiTool.setFunction(function);
|
||||
}
|
||||
result.add(openAiTool);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private UnifiedResponseFormat toUnifiedResponseFormat(OpenAiChatCompletionRequest.ResponseFormat responseFormat) {
|
||||
if (responseFormat == null) {
|
||||
return null;
|
||||
}
|
||||
UnifiedResponseFormat unifiedResponseFormat = new UnifiedResponseFormat();
|
||||
unifiedResponseFormat.setType(responseFormat.getType());
|
||||
unifiedResponseFormat.setJsonSchema(responseFormat.getJsonSchema());
|
||||
return unifiedResponseFormat;
|
||||
}
|
||||
|
||||
private OpenAiChatCompletionRequest.ResponseFormat toOpenAiResponseFormat(UnifiedResponseFormat responseFormat) {
|
||||
if (responseFormat == null) {
|
||||
return null;
|
||||
}
|
||||
OpenAiChatCompletionRequest.ResponseFormat openAiResponseFormat = new OpenAiChatCompletionRequest.ResponseFormat();
|
||||
openAiResponseFormat.setType(responseFormat.getType());
|
||||
openAiResponseFormat.setJsonSchema(responseFormat.getJsonSchema());
|
||||
return openAiResponseFormat;
|
||||
}
|
||||
|
||||
private List<UnifiedChoice> toUnifiedChoices(List<OpenAiChatCompletionResponse.Choice> choices, boolean delta) {
|
||||
if (choices == null) {
|
||||
return null;
|
||||
}
|
||||
List<UnifiedChoice> result = new ArrayList<>();
|
||||
for (OpenAiChatCompletionResponse.Choice choice : choices) {
|
||||
UnifiedChoice unifiedChoice = new UnifiedChoice();
|
||||
unifiedChoice.setIndex(choice.getIndex());
|
||||
UnifiedMessage message = new UnifiedMessage();
|
||||
if (choice.getMessage() != null) {
|
||||
message.setRole(choice.getMessage().getRole());
|
||||
message.setContent(choice.getMessage().getContent());
|
||||
message.setToolCallId(choice.getMessage().getToolCallId());
|
||||
message.setToolCalls(toUnifiedToolCalls(choice.getMessage().getToolCalls()));
|
||||
}
|
||||
if (delta) {
|
||||
unifiedChoice.setDelta(message);
|
||||
} else {
|
||||
unifiedChoice.setMessage(message);
|
||||
}
|
||||
unifiedChoice.setFinishReason(choice.getFinishReason());
|
||||
result.add(unifiedChoice);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<UnifiedChoice> toUnifiedChunkChoices(List<OpenAiChatCompletionChunkResponse.Choice> choices) {
|
||||
if (choices == null) {
|
||||
return null;
|
||||
}
|
||||
List<UnifiedChoice> result = new ArrayList<>();
|
||||
for (OpenAiChatCompletionChunkResponse.Choice choice : choices) {
|
||||
UnifiedChoice unifiedChoice = new UnifiedChoice();
|
||||
unifiedChoice.setIndex(choice.getIndex());
|
||||
UnifiedMessage delta = new UnifiedMessage();
|
||||
if (choice.getDelta() != null) {
|
||||
delta.setRole(choice.getDelta().getRole());
|
||||
delta.setContent(choice.getDelta().getContent());
|
||||
delta.setToolCalls(toUnifiedToolCalls(choice.getDelta().getToolCalls()));
|
||||
}
|
||||
unifiedChoice.setDelta(delta);
|
||||
unifiedChoice.setFinishReason(choice.getFinishReason());
|
||||
result.add(unifiedChoice);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<OpenAiChatCompletionResponse.Choice> toOpenAiChoices(List<UnifiedChoice> choices, boolean delta) {
|
||||
if (choices == null) {
|
||||
return null;
|
||||
}
|
||||
List<OpenAiChatCompletionResponse.Choice> result = new ArrayList<>();
|
||||
for (UnifiedChoice choice : choices) {
|
||||
OpenAiChatCompletionResponse.Choice openAiChoice = new OpenAiChatCompletionResponse.Choice();
|
||||
openAiChoice.setIndex(choice.getIndex());
|
||||
UnifiedMessage source = delta ? choice.getDelta() : choice.getMessage();
|
||||
if (source != null) {
|
||||
OpenAiChatCompletionResponse.Message message = new OpenAiChatCompletionResponse.Message();
|
||||
message.setRole(source.getRole());
|
||||
message.setContent(source.getContent());
|
||||
message.setToolCallId(source.getToolCallId());
|
||||
message.setToolCalls(toOpenAiToolCalls(source.getToolCalls()));
|
||||
openAiChoice.setMessage(message);
|
||||
}
|
||||
openAiChoice.setFinishReason(choice.getFinishReason());
|
||||
result.add(openAiChoice);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<OpenAiChatCompletionChunkResponse.Choice> toOpenAiChunkChoices(List<UnifiedChoice> choices) {
|
||||
if (choices == null) {
|
||||
return null;
|
||||
}
|
||||
List<OpenAiChatCompletionChunkResponse.Choice> result = new ArrayList<>();
|
||||
for (UnifiedChoice choice : choices) {
|
||||
OpenAiChatCompletionChunkResponse.Choice openAiChoice = new OpenAiChatCompletionChunkResponse.Choice();
|
||||
openAiChoice.setIndex(choice.getIndex());
|
||||
if (choice.getDelta() != null) {
|
||||
OpenAiChatCompletionChunkResponse.Delta delta = new OpenAiChatCompletionChunkResponse.Delta();
|
||||
delta.setRole(choice.getDelta().getRole());
|
||||
delta.setContent(choice.getDelta().getContent());
|
||||
delta.setToolCalls(toOpenAiToolCalls(choice.getDelta().getToolCalls()));
|
||||
openAiChoice.setDelta(delta);
|
||||
}
|
||||
openAiChoice.setFinishReason(choice.getFinishReason());
|
||||
result.add(openAiChoice);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<UnifiedToolCall> toUnifiedToolCalls(List<OpenAiChatCompletionResponse.ToolCall> toolCalls) {
|
||||
if (toolCalls == null) {
|
||||
return null;
|
||||
}
|
||||
List<UnifiedToolCall> result = new ArrayList<>();
|
||||
for (OpenAiChatCompletionResponse.ToolCall toolCall : toolCalls) {
|
||||
UnifiedToolCall unifiedToolCall = new UnifiedToolCall();
|
||||
unifiedToolCall.setIndex(toolCall.getIndex());
|
||||
unifiedToolCall.setId(toolCall.getId());
|
||||
unifiedToolCall.setType(toolCall.getType());
|
||||
if (toolCall.getFunction() != null) {
|
||||
UnifiedToolCallFunction function = new UnifiedToolCallFunction();
|
||||
function.setName(toolCall.getFunction().getName());
|
||||
function.setArguments(toolCall.getFunction().getArguments());
|
||||
unifiedToolCall.setFunction(function);
|
||||
}
|
||||
result.add(unifiedToolCall);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<OpenAiChatCompletionResponse.ToolCall> toOpenAiToolCalls(List<UnifiedToolCall> toolCalls) {
|
||||
if (toolCalls == null) {
|
||||
return null;
|
||||
}
|
||||
List<OpenAiChatCompletionResponse.ToolCall> result = new ArrayList<>();
|
||||
for (UnifiedToolCall toolCall : toolCalls) {
|
||||
OpenAiChatCompletionResponse.ToolCall openAiToolCall = new OpenAiChatCompletionResponse.ToolCall();
|
||||
openAiToolCall.setIndex(toolCall.getIndex());
|
||||
openAiToolCall.setId(toolCall.getId());
|
||||
openAiToolCall.setType(toolCall.getType());
|
||||
if (toolCall.getFunction() != null) {
|
||||
OpenAiChatCompletionResponse.ToolCallFunction function = new OpenAiChatCompletionResponse.ToolCallFunction();
|
||||
function.setName(toolCall.getFunction().getName());
|
||||
function.setArguments(toolCall.getFunction().getArguments());
|
||||
openAiToolCall.setFunction(function);
|
||||
}
|
||||
result.add(openAiToolCall);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private UnifiedUsage toUnifiedUsage(OpenAiChatCompletionResponse.Usage usage) {
|
||||
if (usage == null) {
|
||||
return null;
|
||||
}
|
||||
UnifiedUsage unifiedUsage = new UnifiedUsage();
|
||||
unifiedUsage.setPromptTokens(usage.getPromptTokens());
|
||||
unifiedUsage.setCompletionTokens(usage.getCompletionTokens());
|
||||
unifiedUsage.setTotalTokens(usage.getTotalTokens());
|
||||
return unifiedUsage;
|
||||
}
|
||||
|
||||
private OpenAiChatCompletionResponse.Usage toOpenAiUsage(UnifiedUsage usage) {
|
||||
if (usage == null) {
|
||||
return null;
|
||||
}
|
||||
OpenAiChatCompletionResponse.Usage openAiUsage = new OpenAiChatCompletionResponse.Usage();
|
||||
openAiUsage.setPromptTokens(usage.getPromptTokens());
|
||||
openAiUsage.setCompletionTokens(usage.getCompletionTokens());
|
||||
openAiUsage.setTotalTokens(usage.getTotalTokens());
|
||||
return openAiUsage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class UnifiedChatChunk {
|
||||
|
||||
private String id;
|
||||
private String object;
|
||||
private Long created;
|
||||
private String model;
|
||||
private List<UnifiedChoice> choices;
|
||||
private UnifiedUsage usage;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getObject() {
|
||||
return object;
|
||||
}
|
||||
|
||||
public void setObject(String object) {
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
public Long getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Long created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public List<UnifiedChoice> getChoices() {
|
||||
return choices;
|
||||
}
|
||||
|
||||
public void setChoices(List<UnifiedChoice> choices) {
|
||||
this.choices = choices;
|
||||
}
|
||||
|
||||
public UnifiedUsage getUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
public void setUsage(UnifiedUsage usage) {
|
||||
this.usage = usage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class UnifiedChatRequest {
|
||||
|
||||
private String model;
|
||||
private List<UnifiedMessage> messages;
|
||||
private Boolean stream;
|
||||
private Double temperature;
|
||||
private Double topP;
|
||||
private Integer maxTokens;
|
||||
private Long seed;
|
||||
private List<UnifiedTool> tools;
|
||||
private JsonNode toolChoice;
|
||||
private UnifiedResponseFormat responseFormat;
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public List<UnifiedMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void setMessages(List<UnifiedMessage> messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public Boolean getStream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
public void setStream(Boolean stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
public Double getTemperature() {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
public void setTemperature(Double temperature) {
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
public Double getTopP() {
|
||||
return topP;
|
||||
}
|
||||
|
||||
public void setTopP(Double topP) {
|
||||
this.topP = topP;
|
||||
}
|
||||
|
||||
public Integer getMaxTokens() {
|
||||
return maxTokens;
|
||||
}
|
||||
|
||||
public void setMaxTokens(Integer maxTokens) {
|
||||
this.maxTokens = maxTokens;
|
||||
}
|
||||
|
||||
public Long getSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public void setSeed(Long seed) {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
public List<UnifiedTool> getTools() {
|
||||
return tools;
|
||||
}
|
||||
|
||||
public void setTools(List<UnifiedTool> tools) {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
public JsonNode getToolChoice() {
|
||||
return toolChoice;
|
||||
}
|
||||
|
||||
public void setToolChoice(JsonNode toolChoice) {
|
||||
this.toolChoice = toolChoice;
|
||||
}
|
||||
|
||||
public UnifiedResponseFormat getResponseFormat() {
|
||||
return responseFormat;
|
||||
}
|
||||
|
||||
public void setResponseFormat(UnifiedResponseFormat responseFormat) {
|
||||
this.responseFormat = responseFormat;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class UnifiedChatResponse {
|
||||
|
||||
private String id;
|
||||
private String object;
|
||||
private Long created;
|
||||
private String model;
|
||||
private List<UnifiedChoice> choices;
|
||||
private UnifiedUsage usage;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getObject() {
|
||||
return object;
|
||||
}
|
||||
|
||||
public void setObject(String object) {
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
public Long getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Long created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public List<UnifiedChoice> getChoices() {
|
||||
return choices;
|
||||
}
|
||||
|
||||
public void setChoices(List<UnifiedChoice> choices) {
|
||||
this.choices = choices;
|
||||
}
|
||||
|
||||
public UnifiedUsage getUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
public void setUsage(UnifiedUsage usage) {
|
||||
this.usage = usage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
public class UnifiedChoice {
|
||||
|
||||
private Integer index;
|
||||
private UnifiedMessage message;
|
||||
private UnifiedMessage delta;
|
||||
private String finishReason;
|
||||
|
||||
public Integer getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(Integer index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public UnifiedMessage getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(UnifiedMessage message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public UnifiedMessage getDelta() {
|
||||
return delta;
|
||||
}
|
||||
|
||||
public void setDelta(UnifiedMessage delta) {
|
||||
this.delta = delta;
|
||||
}
|
||||
|
||||
public String getFinishReason() {
|
||||
return finishReason;
|
||||
}
|
||||
|
||||
public void setFinishReason(String finishReason) {
|
||||
this.finishReason = finishReason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
public class UnifiedContentPart {
|
||||
|
||||
private String type;
|
||||
private String text;
|
||||
private UnifiedImageUrl imageUrl;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public UnifiedImageUrl getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(UnifiedImageUrl imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
public class UnifiedImageUrl {
|
||||
|
||||
private String url;
|
||||
private String detail;
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
|
||||
public void setDetail(String detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class UnifiedMessage {
|
||||
|
||||
private String role;
|
||||
private String content;
|
||||
private List<UnifiedContentPart> contentParts;
|
||||
private String name;
|
||||
private String toolCallId;
|
||||
private List<UnifiedToolCall> toolCalls;
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public List<UnifiedContentPart> getContentParts() {
|
||||
return contentParts;
|
||||
}
|
||||
|
||||
public void setContentParts(List<UnifiedContentPart> contentParts) {
|
||||
this.contentParts = contentParts;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getToolCallId() {
|
||||
return toolCallId;
|
||||
}
|
||||
|
||||
public void setToolCallId(String toolCallId) {
|
||||
this.toolCallId = toolCallId;
|
||||
}
|
||||
|
||||
public List<UnifiedToolCall> getToolCalls() {
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
public void setToolCalls(List<UnifiedToolCall> toolCalls) {
|
||||
this.toolCalls = toolCalls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
public class UnifiedResponseFormat {
|
||||
|
||||
private String type;
|
||||
private JsonNode jsonSchema;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public JsonNode getJsonSchema() {
|
||||
return jsonSchema;
|
||||
}
|
||||
|
||||
public void setJsonSchema(JsonNode jsonSchema) {
|
||||
this.jsonSchema = jsonSchema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
public class UnifiedTool {
|
||||
|
||||
private String type;
|
||||
private UnifiedToolFunction function;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public UnifiedToolFunction getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
public void setFunction(UnifiedToolFunction function) {
|
||||
this.function = function;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
public class UnifiedToolCall {
|
||||
|
||||
private Integer index;
|
||||
private String id;
|
||||
private String type;
|
||||
private UnifiedToolCallFunction function;
|
||||
|
||||
public Integer getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(Integer index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public UnifiedToolCallFunction getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
public void setFunction(UnifiedToolCallFunction function) {
|
||||
this.function = function;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
public class UnifiedToolCallFunction {
|
||||
|
||||
private String name;
|
||||
private String arguments;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public void setArguments(String arguments) {
|
||||
this.arguments = arguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
public class UnifiedToolFunction {
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private JsonNode parameters;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public JsonNode getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public void setParameters(JsonNode parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.ai.invoke.model;
|
||||
|
||||
public class UnifiedUsage {
|
||||
|
||||
private Integer promptTokens;
|
||||
private Integer completionTokens;
|
||||
private Integer totalTokens;
|
||||
|
||||
public Integer getPromptTokens() {
|
||||
return promptTokens;
|
||||
}
|
||||
|
||||
public void setPromptTokens(Integer promptTokens) {
|
||||
this.promptTokens = promptTokens;
|
||||
}
|
||||
|
||||
public Integer getCompletionTokens() {
|
||||
return completionTokens;
|
||||
}
|
||||
|
||||
public void setCompletionTokens(Integer completionTokens) {
|
||||
this.completionTokens = completionTokens;
|
||||
}
|
||||
|
||||
public Integer getTotalTokens() {
|
||||
return totalTokens;
|
||||
}
|
||||
|
||||
public void setTotalTokens(Integer totalTokens) {
|
||||
this.totalTokens = totalTokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package tech.easyflow.ai.invoke.protocol.openai;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class OpenAiChatCompletionChunkResponse {
|
||||
|
||||
private String id;
|
||||
private String object;
|
||||
private Long created;
|
||||
private String model;
|
||||
private List<Choice> choices;
|
||||
private OpenAiChatCompletionResponse.Usage usage;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getObject() {
|
||||
return object;
|
||||
}
|
||||
|
||||
public void setObject(String object) {
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
public Long getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Long created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public List<Choice> getChoices() {
|
||||
return choices;
|
||||
}
|
||||
|
||||
public void setChoices(List<Choice> choices) {
|
||||
this.choices = choices;
|
||||
}
|
||||
|
||||
public OpenAiChatCompletionResponse.Usage getUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
public void setUsage(OpenAiChatCompletionResponse.Usage usage) {
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Choice {
|
||||
|
||||
private Integer index;
|
||||
private Delta delta;
|
||||
@JSONField(name = "finish_reason")
|
||||
@JsonProperty("finish_reason")
|
||||
private String finishReason;
|
||||
|
||||
public Integer getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(Integer index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public Delta getDelta() {
|
||||
return delta;
|
||||
}
|
||||
|
||||
public void setDelta(Delta delta) {
|
||||
this.delta = delta;
|
||||
}
|
||||
|
||||
public String getFinishReason() {
|
||||
return finishReason;
|
||||
}
|
||||
|
||||
public void setFinishReason(String finishReason) {
|
||||
this.finishReason = finishReason;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Delta {
|
||||
|
||||
private String role;
|
||||
private String content;
|
||||
@JSONField(name = "tool_calls")
|
||||
@JsonProperty("tool_calls")
|
||||
private List<OpenAiChatCompletionResponse.ToolCall> toolCalls;
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public List<OpenAiChatCompletionResponse.ToolCall> getToolCalls() {
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
public void setToolCalls(List<OpenAiChatCompletionResponse.ToolCall> toolCalls) {
|
||||
this.toolCalls = toolCalls;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
package tech.easyflow.ai.invoke.protocol.openai;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class OpenAiChatCompletionRequest {
|
||||
|
||||
private String model;
|
||||
private List<Message> messages;
|
||||
private Boolean stream;
|
||||
private Double temperature;
|
||||
@JSONField(name = "top_p")
|
||||
@JsonProperty("top_p")
|
||||
private Double topP;
|
||||
@JSONField(name = "max_tokens")
|
||||
@JsonProperty("max_tokens")
|
||||
private Integer maxTokens;
|
||||
private Long seed;
|
||||
private List<Tool> tools;
|
||||
@JSONField(name = "tool_choice")
|
||||
@JsonProperty("tool_choice")
|
||||
private JsonNode toolChoice;
|
||||
@JSONField(name = "response_format")
|
||||
@JsonProperty("response_format")
|
||||
private ResponseFormat responseFormat;
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public List<Message> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void setMessages(List<Message> messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public Boolean getStream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
public void setStream(Boolean stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
public Double getTemperature() {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
public void setTemperature(Double temperature) {
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
public Double getTopP() {
|
||||
return topP;
|
||||
}
|
||||
|
||||
public void setTopP(Double topP) {
|
||||
this.topP = topP;
|
||||
}
|
||||
|
||||
public Integer getMaxTokens() {
|
||||
return maxTokens;
|
||||
}
|
||||
|
||||
public void setMaxTokens(Integer maxTokens) {
|
||||
this.maxTokens = maxTokens;
|
||||
}
|
||||
|
||||
public Long getSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public void setSeed(Long seed) {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
public List<Tool> getTools() {
|
||||
return tools;
|
||||
}
|
||||
|
||||
public void setTools(List<Tool> tools) {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
public JsonNode getToolChoice() {
|
||||
return toolChoice;
|
||||
}
|
||||
|
||||
public void setToolChoice(JsonNode toolChoice) {
|
||||
this.toolChoice = toolChoice;
|
||||
}
|
||||
|
||||
public ResponseFormat getResponseFormat() {
|
||||
return responseFormat;
|
||||
}
|
||||
|
||||
public void setResponseFormat(ResponseFormat responseFormat) {
|
||||
this.responseFormat = responseFormat;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Message {
|
||||
|
||||
private String role;
|
||||
private String content;
|
||||
private List<ContentPart> contentParts;
|
||||
private String name;
|
||||
@JSONField(name = "tool_call_id")
|
||||
@JsonProperty("tool_call_id")
|
||||
private String toolCallId;
|
||||
@JSONField(name = "tool_calls")
|
||||
@JsonProperty("tool_calls")
|
||||
private List<OpenAiChatCompletionResponse.ToolCall> toolCalls;
|
||||
|
||||
@JsonProperty("content")
|
||||
public void setContentNode(JsonNode contentNode) {
|
||||
if (contentNode == null || contentNode.isNull()) {
|
||||
this.content = null;
|
||||
this.contentParts = null;
|
||||
return;
|
||||
}
|
||||
if (contentNode.isTextual()) {
|
||||
this.content = contentNode.asText();
|
||||
this.contentParts = null;
|
||||
return;
|
||||
}
|
||||
if (contentNode.isArray()) {
|
||||
this.content = null;
|
||||
this.contentParts = OpenAiJsonSupport.convertContentParts(contentNode);
|
||||
return;
|
||||
}
|
||||
throw new IllegalArgumentException("message.content 仅支持字符串或数组");
|
||||
}
|
||||
|
||||
@JSONField(name = "content")
|
||||
@JsonProperty("content")
|
||||
public Object getContentNode() {
|
||||
if (contentParts != null) {
|
||||
return contentParts;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public List<ContentPart> getContentParts() {
|
||||
return contentParts;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getToolCallId() {
|
||||
return toolCallId;
|
||||
}
|
||||
|
||||
public void setToolCallId(String toolCallId) {
|
||||
this.toolCallId = toolCallId;
|
||||
}
|
||||
|
||||
public List<OpenAiChatCompletionResponse.ToolCall> getToolCalls() {
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
public void setToolCalls(List<OpenAiChatCompletionResponse.ToolCall> toolCalls) {
|
||||
this.toolCalls = toolCalls;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ContentPart {
|
||||
|
||||
private String type;
|
||||
private String text;
|
||||
@JSONField(name = "image_url")
|
||||
@JsonProperty("image_url")
|
||||
private ImageUrl imageUrl;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public ImageUrl getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(ImageUrl imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ImageUrl {
|
||||
|
||||
private String url;
|
||||
private String detail;
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
|
||||
public void setDetail(String detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Tool {
|
||||
|
||||
private String type;
|
||||
private ToolFunction function;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ToolFunction getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
public void setFunction(ToolFunction function) {
|
||||
this.function = function;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ToolFunction {
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private JsonNode parameters;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public JsonNode getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public void setParameters(JsonNode parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ResponseFormat {
|
||||
|
||||
private String type;
|
||||
@JSONField(name = "json_schema")
|
||||
@JsonProperty("json_schema")
|
||||
private JsonNode jsonSchema;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public JsonNode getJsonSchema() {
|
||||
return jsonSchema;
|
||||
}
|
||||
|
||||
public void setJsonSchema(JsonNode jsonSchema) {
|
||||
this.jsonSchema = jsonSchema;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package tech.easyflow.ai.invoke.protocol.openai;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class OpenAiChatCompletionResponse {
|
||||
|
||||
private String id;
|
||||
private String object;
|
||||
private Long created;
|
||||
private String model;
|
||||
private List<Choice> choices;
|
||||
private Usage usage;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getObject() {
|
||||
return object;
|
||||
}
|
||||
|
||||
public void setObject(String object) {
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
public Long getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Long created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public List<Choice> getChoices() {
|
||||
return choices;
|
||||
}
|
||||
|
||||
public void setChoices(List<Choice> choices) {
|
||||
this.choices = choices;
|
||||
}
|
||||
|
||||
public Usage getUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
public void setUsage(Usage usage) {
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Choice {
|
||||
|
||||
private Integer index;
|
||||
private Message message;
|
||||
@JSONField(name = "finish_reason")
|
||||
@JsonProperty("finish_reason")
|
||||
private String finishReason;
|
||||
|
||||
public Integer getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(Integer index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public Message getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(Message message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getFinishReason() {
|
||||
return finishReason;
|
||||
}
|
||||
|
||||
public void setFinishReason(String finishReason) {
|
||||
this.finishReason = finishReason;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Message {
|
||||
|
||||
private String role;
|
||||
private String content;
|
||||
@JSONField(name = "tool_call_id")
|
||||
@JsonProperty("tool_call_id")
|
||||
private String toolCallId;
|
||||
@JSONField(name = "tool_calls")
|
||||
@JsonProperty("tool_calls")
|
||||
private List<ToolCall> toolCalls;
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getToolCallId() {
|
||||
return toolCallId;
|
||||
}
|
||||
|
||||
public void setToolCallId(String toolCallId) {
|
||||
this.toolCallId = toolCallId;
|
||||
}
|
||||
|
||||
public List<ToolCall> getToolCalls() {
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
public void setToolCalls(List<ToolCall> toolCalls) {
|
||||
this.toolCalls = toolCalls;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ToolCall {
|
||||
|
||||
private Integer index;
|
||||
private String id;
|
||||
private String type;
|
||||
private ToolCallFunction function;
|
||||
|
||||
public Integer getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(Integer index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ToolCallFunction getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
public void setFunction(ToolCallFunction function) {
|
||||
this.function = function;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ToolCallFunction {
|
||||
|
||||
private String name;
|
||||
private String arguments;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public void setArguments(String arguments) {
|
||||
this.arguments = arguments;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Usage {
|
||||
|
||||
@JSONField(name = "prompt_tokens")
|
||||
@JsonProperty("prompt_tokens")
|
||||
private Integer promptTokens;
|
||||
@JSONField(name = "completion_tokens")
|
||||
@JsonProperty("completion_tokens")
|
||||
private Integer completionTokens;
|
||||
@JSONField(name = "total_tokens")
|
||||
@JsonProperty("total_tokens")
|
||||
private Integer totalTokens;
|
||||
|
||||
public Integer getPromptTokens() {
|
||||
return promptTokens;
|
||||
}
|
||||
|
||||
public void setPromptTokens(Integer promptTokens) {
|
||||
this.promptTokens = promptTokens;
|
||||
}
|
||||
|
||||
public Integer getCompletionTokens() {
|
||||
return completionTokens;
|
||||
}
|
||||
|
||||
public void setCompletionTokens(Integer completionTokens) {
|
||||
this.completionTokens = completionTokens;
|
||||
}
|
||||
|
||||
public Integer getTotalTokens() {
|
||||
return totalTokens;
|
||||
}
|
||||
|
||||
public void setTotalTokens(Integer totalTokens) {
|
||||
this.totalTokens = totalTokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package tech.easyflow.ai.invoke.protocol.openai;
|
||||
|
||||
public class OpenAiErrorResponse {
|
||||
|
||||
private Error error;
|
||||
|
||||
public Error getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(Error error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public static class Error {
|
||||
|
||||
private String message;
|
||||
private String type;
|
||||
private String param;
|
||||
private String code;
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getParam() {
|
||||
return param;
|
||||
}
|
||||
|
||||
public void setParam(String param) {
|
||||
this.param = param;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.ai.invoke.protocol.openai;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class OpenAiJsonSupport {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private OpenAiJsonSupport() {
|
||||
}
|
||||
|
||||
static List<OpenAiChatCompletionRequest.ContentPart> convertContentParts(JsonNode node) {
|
||||
return OBJECT_MAPPER.convertValue(node, new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.ai.invoke.provider;
|
||||
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
|
||||
|
||||
public interface ModelProviderGateway {
|
||||
|
||||
boolean supports(String providerType);
|
||||
|
||||
UnifiedChatResponse chat(Model model, UnifiedChatRequest request);
|
||||
|
||||
void chatStream(Model model, UnifiedChatRequest request, UnifiedChatChunkObserver observer);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package tech.easyflow.ai.invoke.provider;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class ModelProviderGatewayRegistry {
|
||||
|
||||
private final List<ModelProviderGateway> gateways;
|
||||
|
||||
public ModelProviderGatewayRegistry(List<ModelProviderGateway> gateways) {
|
||||
this.gateways = gateways;
|
||||
}
|
||||
|
||||
public ModelProviderGateway getGateway(String providerType) {
|
||||
return gateways.stream()
|
||||
.filter(gateway -> gateway.supports(providerType))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> ModelInvokeException.badRequest(
|
||||
"当前 providerType 暂不支持统一模型调用: " + providerType,
|
||||
"model",
|
||||
"provider_not_supported"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.ai.invoke.provider;
|
||||
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
|
||||
|
||||
public interface UnifiedChatChunkObserver {
|
||||
|
||||
void onChunk(UnifiedChatChunk chunk);
|
||||
|
||||
default void onComplete() {
|
||||
}
|
||||
|
||||
default void onError(Throwable throwable) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package tech.easyflow.ai.invoke.provider.base;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.BufferedSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
|
||||
import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionChunkResponse;
|
||||
import tech.easyflow.common.util.OkHttpClientUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class AbstractOpenAiCompatibleGateway {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AbstractOpenAiCompatibleGateway.class);
|
||||
private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");
|
||||
|
||||
protected final ObjectMapper objectMapper;
|
||||
protected final OpenAiProtocolMapper openAiProtocolMapper;
|
||||
protected final ThreadPoolTaskExecutor sseThreadPool;
|
||||
|
||||
protected AbstractOpenAiCompatibleGateway(ObjectMapper objectMapper,
|
||||
OpenAiProtocolMapper openAiProtocolMapper,
|
||||
ThreadPoolTaskExecutor sseThreadPool) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.openAiProtocolMapper = openAiProtocolMapper;
|
||||
this.sseThreadPool = sseThreadPool;
|
||||
}
|
||||
|
||||
protected Response executePost(Model model, Object requestBody) {
|
||||
try {
|
||||
String body = objectMapper.writeValueAsString(requestBody);
|
||||
Request request = new Request.Builder()
|
||||
.url(buildUrl(model))
|
||||
.addHeader("Authorization", "Bearer " + model.checkAndGetApiKey())
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(RequestBody.create(body, JSON_TYPE))
|
||||
.build();
|
||||
return OkHttpClientUtil.buildDefaultClient().newCall(request).execute();
|
||||
} catch (ModelInvokeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected String buildUrl(Model model) {
|
||||
return StrUtil.removeSuffix(model.checkAndGetEndpoint(), "/")
|
||||
+ "/"
|
||||
+ StrUtil.removePrefix(model.checkAndGetRequestPath(), "/");
|
||||
}
|
||||
|
||||
protected void validateResponse(Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
return;
|
||||
}
|
||||
String message = extractUpstreamErrorMessage(response);
|
||||
int code = response.code();
|
||||
if (code >= 500 || code == 429) {
|
||||
throw ModelInvokeException.serviceUnavailable(message);
|
||||
}
|
||||
throw ModelInvokeException.badGateway(message);
|
||||
}
|
||||
|
||||
protected String extractUpstreamErrorMessage(Response response) throws IOException {
|
||||
ResponseBody body = response.body();
|
||||
String bodyString = body == null ? "" : body.string();
|
||||
if (StrUtil.isBlank(bodyString)) {
|
||||
return "上游模型调用失败,HTTP " + response.code();
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(bodyString);
|
||||
String errorMessage = root.path("error").path("message").asText();
|
||||
if (StrUtil.isNotBlank(errorMessage)) {
|
||||
return "上游模型调用失败,HTTP " + response.code() + ": " + errorMessage;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "上游模型调用失败,HTTP " + response.code() + ": " + bodyString;
|
||||
}
|
||||
|
||||
protected void streamResponse(Response response,
|
||||
tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver observer) {
|
||||
sseThreadPool.execute(() -> {
|
||||
try (Response closeableResponse = response; ResponseBody body = closeableResponse.body()) {
|
||||
if (body == null) {
|
||||
throw new IOException("上游流式响应体为空");
|
||||
}
|
||||
BufferedSource source = body.source();
|
||||
while (!source.exhausted()) {
|
||||
String line = source.readUtf8Line();
|
||||
if (line == null) {
|
||||
break;
|
||||
}
|
||||
if (line.isBlank() || !line.startsWith("data:")) {
|
||||
continue;
|
||||
}
|
||||
String payload = line.substring(5).trim();
|
||||
if (Objects.equals(payload, "[DONE]")) {
|
||||
observer.onComplete();
|
||||
return;
|
||||
}
|
||||
OpenAiChatCompletionChunkResponse chunkResponse = objectMapper.readValue(
|
||||
payload,
|
||||
OpenAiChatCompletionChunkResponse.class
|
||||
);
|
||||
UnifiedChatChunk chunk = openAiProtocolMapper.toUnifiedChunk(chunkResponse);
|
||||
observer.onChunk(chunk);
|
||||
}
|
||||
observer.onComplete();
|
||||
} catch (Exception e) {
|
||||
log.error("streamResponse error: {}", e.getMessage(), e);
|
||||
observer.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package tech.easyflow.ai.invoke.provider.openai;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import okhttp3.Response;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse;
|
||||
import tech.easyflow.ai.invoke.provider.ModelProviderGateway;
|
||||
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
|
||||
import tech.easyflow.ai.invoke.provider.base.AbstractOpenAiCompatibleGateway;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class OpenAiGateway extends AbstractOpenAiCompatibleGateway implements ModelProviderGateway {
|
||||
|
||||
private static final Set<String> OPENAI_COMPATIBLE_PROVIDER_TYPES = Set.of(
|
||||
"openai",
|
||||
"deepseek",
|
||||
"aliyun",
|
||||
"zhipu",
|
||||
"minimax",
|
||||
"kimi",
|
||||
"siliconlow",
|
||||
"ollama",
|
||||
"self-hosted"
|
||||
);
|
||||
|
||||
public OpenAiGateway(ObjectMapper objectMapper,
|
||||
OpenAiProtocolMapper openAiProtocolMapper,
|
||||
ThreadPoolTaskExecutor sseThreadPool) {
|
||||
super(objectMapper, openAiProtocolMapper, sseThreadPool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String providerType) {
|
||||
return providerType != null && OPENAI_COMPATIBLE_PROVIDER_TYPES.contains(providerType.toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnifiedChatResponse chat(Model model, UnifiedChatRequest request) {
|
||||
OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.toOpenAiRequest(request);
|
||||
try (Response response = executePost(model, openAiRequest)) {
|
||||
validateResponse(response);
|
||||
if (response.body() == null) {
|
||||
throw tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("上游模型响应为空");
|
||||
}
|
||||
OpenAiChatCompletionResponse responseBody = objectMapper.readValue(
|
||||
response.body().string(),
|
||||
OpenAiChatCompletionResponse.class
|
||||
);
|
||||
return openAiProtocolMapper.toUnifiedResponse(responseBody);
|
||||
} catch (tech.easyflow.ai.invoke.exception.ModelInvokeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(Model model, UnifiedChatRequest request, UnifiedChatChunkObserver observer) {
|
||||
OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.toOpenAiRequest(request);
|
||||
Response response = executePost(model, openAiRequest);
|
||||
try {
|
||||
validateResponse(response);
|
||||
streamResponse(response, observer);
|
||||
} catch (RuntimeException | java.io.IOException e) {
|
||||
response.close();
|
||||
throw e instanceof RuntimeException ? (RuntimeException) e
|
||||
: tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package tech.easyflow.ai.invoke.service;
|
||||
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
|
||||
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
|
||||
|
||||
public interface UnifiedModelInvokeService {
|
||||
|
||||
UnifiedChatResponse chat(UnifiedChatRequest request);
|
||||
|
||||
void chatStream(UnifiedChatRequest request, UnifiedChatChunkObserver observer);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package tech.easyflow.ai.invoke.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedContentPart;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedMessage;
|
||||
import tech.easyflow.ai.invoke.provider.ModelProviderGateway;
|
||||
import tech.easyflow.ai.invoke.provider.ModelProviderGatewayRegistry;
|
||||
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
|
||||
import tech.easyflow.ai.invoke.service.UnifiedModelInvokeService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class UnifiedModelInvokeServiceImpl implements UnifiedModelInvokeService {
|
||||
|
||||
private final ModelService modelService;
|
||||
private final ModelProviderGatewayRegistry gatewayRegistry;
|
||||
|
||||
public UnifiedModelInvokeServiceImpl(ModelService modelService,
|
||||
ModelProviderGatewayRegistry gatewayRegistry) {
|
||||
this.modelService = modelService;
|
||||
this.gatewayRegistry = gatewayRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnifiedChatResponse chat(UnifiedChatRequest request) {
|
||||
Model model = resolveModel(request);
|
||||
validateRequestAgainstModel(model, request);
|
||||
ModelProviderGateway gateway = gatewayRegistry.getGateway(model.getModelProvider().getProviderType());
|
||||
return gateway.chat(model, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(UnifiedChatRequest request, UnifiedChatChunkObserver observer) {
|
||||
Model model = resolveModel(request);
|
||||
validateRequestAgainstModel(model, request);
|
||||
ModelProviderGateway gateway = gatewayRegistry.getGateway(model.getModelProvider().getProviderType());
|
||||
gateway.chatStream(model, request, observer);
|
||||
}
|
||||
|
||||
private Model resolveModel(UnifiedChatRequest request) {
|
||||
if (request == null || StrUtil.isBlank(request.getModel())) {
|
||||
throw ModelInvokeException.badRequest("model 不能为空", "model", "model_required");
|
||||
}
|
||||
Model model = modelService.getModelInstanceByInvokeCode(request.getModel());
|
||||
if (model == null) {
|
||||
throw ModelInvokeException.notFound("未找到可调用模型: " + request.getModel());
|
||||
}
|
||||
if (!Boolean.TRUE.equals(model.getPublishEnabled())) {
|
||||
throw ModelInvokeException.badRequest("当前模型未开启 API 调用发布", "model", "model_not_published");
|
||||
}
|
||||
if (!Model.MODEL_TYPES[0].equals(model.getModelType())) {
|
||||
throw ModelInvokeException.badRequest("当前模型不是 chatModel,无法通过 chat/completions 调用", "model", "model_type_mismatch");
|
||||
}
|
||||
if (model.getModelProvider() == null || StrUtil.isBlank(model.getModelProvider().getProviderType())) {
|
||||
throw ModelInvokeException.badRequest("当前模型缺少 providerType 配置", "model", "provider_type_missing");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private void validateRequestAgainstModel(Model model, UnifiedChatRequest request) {
|
||||
List<UnifiedMessage> messages = request.getMessages();
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
throw ModelInvokeException.badRequest("messages 不能为空", "messages", "messages_required");
|
||||
}
|
||||
if (hasImageInput(messages)) {
|
||||
if (!Boolean.TRUE.equals(model.getSupportImage())) {
|
||||
throw ModelInvokeException.badRequest("当前模型不支持图片输入", "messages", "image_not_supported");
|
||||
}
|
||||
if (Boolean.TRUE.equals(model.getSupportImageB64Only()) && hasNonBase64Image(messages)) {
|
||||
throw ModelInvokeException.badRequest("当前模型仅支持 base64 图片输入", "messages", "image_base64_only");
|
||||
}
|
||||
}
|
||||
if (request.getTools() != null && !request.getTools().isEmpty() && !Boolean.TRUE.equals(model.getSupportTool())) {
|
||||
throw ModelInvokeException.badRequest("当前模型不支持 tools 参数", "tools", "tool_not_supported");
|
||||
}
|
||||
if (hasToolMessage(messages) && !Boolean.TRUE.equals(model.getSupportToolMessage())) {
|
||||
throw ModelInvokeException.badRequest("当前模型不支持 tool 消息透传", "messages", "tool_message_not_supported");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasImageInput(List<UnifiedMessage> messages) {
|
||||
return messages.stream()
|
||||
.map(UnifiedMessage::getContentParts)
|
||||
.filter(parts -> parts != null && !parts.isEmpty())
|
||||
.flatMap(List::stream)
|
||||
.anyMatch(part -> "image_url".equals(part.getType()));
|
||||
}
|
||||
|
||||
private boolean hasNonBase64Image(List<UnifiedMessage> messages) {
|
||||
return messages.stream()
|
||||
.map(UnifiedMessage::getContentParts)
|
||||
.filter(parts -> parts != null && !parts.isEmpty())
|
||||
.flatMap(List::stream)
|
||||
.filter(part -> "image_url".equals(part.getType()))
|
||||
.map(UnifiedContentPart::getImageUrl)
|
||||
.filter(imageUrl -> imageUrl != null && StrUtil.isNotBlank(imageUrl.getUrl()))
|
||||
.anyMatch(imageUrl -> !StrUtil.startWithIgnoreCase(imageUrl.getUrl(), "data:"));
|
||||
}
|
||||
|
||||
private boolean hasToolMessage(List<UnifiedMessage> messages) {
|
||||
return messages.stream().anyMatch(message ->
|
||||
StrUtil.equals(message.getRole(), "tool")
|
||||
|| (message.getToolCalls() != null && !message.getToolCalls().isEmpty())
|
||||
|| StrUtil.isNotBlank(message.getToolCallId())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class DocumentChunkIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private DocumentChunkService documentChunkService;
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.DOCUMENT_CHUNK_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("文档分段不存在");
|
||||
}
|
||||
DocumentChunk documentChunk = documentChunkService.getById(String.valueOf(identifier));
|
||||
if (documentChunk == null) {
|
||||
throw new BusinessException("文档分段不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(documentChunk.getDocumentCollectionId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class DocumentIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private DocumentService documentService;
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.DOCUMENT_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
Document document = documentService.getById(String.valueOf(identifier));
|
||||
if (document == null) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(document.getCollectionId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqCategory;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.FaqCategoryService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class FaqCategoryIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private FaqCategoryService faqCategoryService;
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.FAQ_CATEGORY_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("FAQ分类不存在");
|
||||
}
|
||||
FaqCategory faqCategory = faqCategoryService.getById(String.valueOf(identifier));
|
||||
if (faqCategory == null) {
|
||||
throw new BusinessException("FAQ分类不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(faqCategory.getCollectionId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.FaqItemService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class FaqItemIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private FaqItemService faqItemService;
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.FAQ_ITEM_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("FAQ不存在");
|
||||
}
|
||||
FaqItem faqItem = faqItemService.getById(String.valueOf(identifier));
|
||||
if (faqItem == null) {
|
||||
throw new BusinessException("FAQ不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(faqItem.getCollectionId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class KnowledgeIdOrSlugResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.KNOWLEDGE_ID_OR_SLUG == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier));
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class KnowledgeIdResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.KNOWLEDGE_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(String.valueOf(identifier));
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class KnowledgeReadAccessSnapshot {
|
||||
|
||||
private final RoleCategoryAccessSnapshot categoryAccess;
|
||||
private final Set<BigInteger> readableDeptIds;
|
||||
|
||||
public KnowledgeReadAccessSnapshot(RoleCategoryAccessSnapshot categoryAccess, Set<BigInteger> readableDeptIds) {
|
||||
this.categoryAccess = categoryAccess;
|
||||
this.readableDeptIds = readableDeptIds == null
|
||||
? Collections.emptySet()
|
||||
: Collections.unmodifiableSet(new LinkedHashSet<>(readableDeptIds));
|
||||
}
|
||||
|
||||
public BigInteger getAccountId() {
|
||||
return categoryAccess == null ? null : categoryAccess.getAccountId();
|
||||
}
|
||||
|
||||
public boolean isSuperAdmin() {
|
||||
return categoryAccess != null && categoryAccess.isSuperAdmin();
|
||||
}
|
||||
|
||||
public boolean isRestricted() {
|
||||
return categoryAccess == null || categoryAccess.isRestricted();
|
||||
}
|
||||
|
||||
public Set<BigInteger> getCategoryIds() {
|
||||
return categoryAccess == null ? Collections.emptySet() : categoryAccess.getCategoryIds();
|
||||
}
|
||||
|
||||
public Set<BigInteger> getReadableDeptIds() {
|
||||
return readableDeptIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.DocumentCollectionTableDef.DOCUMENT_COLLECTION;
|
||||
|
||||
@Component
|
||||
public class KnowledgeVisibilityQueryHelper {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Resource
|
||||
private SysDeptService sysDeptService;
|
||||
|
||||
public KnowledgeReadAccessSnapshot getCurrentReadSnapshot() {
|
||||
RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getCurrentAccess(CategoryResourceType.KNOWLEDGE.getCode());
|
||||
if (categoryAccess.isSuperAdmin()) {
|
||||
return new KnowledgeReadAccessSnapshot(categoryAccess, Collections.emptySet());
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
Set<BigInteger> deptIds = loginAccount == null
|
||||
? Collections.emptySet()
|
||||
: sysDeptService.getSelfAndAncestorDeptIds(loginAccount.getDeptId());
|
||||
return new KnowledgeReadAccessSnapshot(categoryAccess, deptIds);
|
||||
}
|
||||
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper) {
|
||||
applyReadableAccess(queryWrapper, getCurrentReadSnapshot());
|
||||
}
|
||||
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper, KnowledgeReadAccessSnapshot snapshot) {
|
||||
if (snapshot.isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
BigInteger accountId = snapshot.getAccountId();
|
||||
if (accountId == null) {
|
||||
queryWrapper.eq("id", BigInteger.valueOf(-1));
|
||||
return;
|
||||
}
|
||||
QueryCondition ownerCondition = DOCUMENT_COLLECTION.CREATED_BY.eq(accountId);
|
||||
if (snapshot.isRestricted() && snapshot.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.and(ownerCondition);
|
||||
return;
|
||||
}
|
||||
QueryCondition visibilityCondition = DOCUMENT_COLLECTION.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name());
|
||||
if (!snapshot.getReadableDeptIds().isEmpty()) {
|
||||
visibilityCondition = visibilityCondition.or(
|
||||
DOCUMENT_COLLECTION.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name())
|
||||
.and(DOCUMENT_COLLECTION.DEPT_ID.in(snapshot.getReadableDeptIds()))
|
||||
);
|
||||
}
|
||||
QueryCondition readableCondition = visibilityCondition;
|
||||
if (snapshot.isRestricted()) {
|
||||
readableCondition = DOCUMENT_COLLECTION.CATEGORY_ID.in(snapshot.getCategoryIds()).and(visibilityCondition);
|
||||
}
|
||||
queryWrapper.and(ownerCondition.or(readableCondition));
|
||||
}
|
||||
|
||||
public boolean canRead(DocumentCollection collection) {
|
||||
return canRead(collection, getCurrentReadSnapshot());
|
||||
}
|
||||
|
||||
public boolean canRead(DocumentCollection collection, KnowledgeReadAccessSnapshot snapshot) {
|
||||
if (collection == null) {
|
||||
return false;
|
||||
}
|
||||
if (snapshot.isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
BigInteger accountId = snapshot.getAccountId();
|
||||
if (accountId != null && accountId.equals(collection.getCreatedBy())) {
|
||||
return true;
|
||||
}
|
||||
if (snapshot.isRestricted() && (collection.getCategoryId() == null || !snapshot.getCategoryIds().contains(collection.getCategoryId()))) {
|
||||
return false;
|
||||
}
|
||||
VisibilityScope scope = VisibilityScope.fromOrDefault(collection.getVisibilityScope(), VisibilityScope.PRIVATE);
|
||||
if (VisibilityScope.PUBLIC == scope) {
|
||||
return true;
|
||||
}
|
||||
return VisibilityScope.DEPT == scope
|
||||
&& collection.getDeptId() != null
|
||||
&& snapshot.getReadableDeptIds().contains(collection.getDeptId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class WorkflowExecKeyResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private WorkflowExecResultService workflowExecResultService;
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.WORKFLOW == resourceType && ResourceLookup.EXEC_KEY == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("工作流执行记录不存在");
|
||||
}
|
||||
WorkflowExecResult execResult = workflowExecResultService.getByExecKey(String.valueOf(identifier));
|
||||
if (execResult == null) {
|
||||
throw new BusinessException("工作流执行记录不存在");
|
||||
}
|
||||
Workflow workflow = workflowService.getDetail(String.valueOf(execResult.getWorkflowId()));
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(workflow, execResult.getCreatedBy());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class WorkflowIdResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.WORKFLOW == resourceType && ResourceLookup.WORKFLOW_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
Workflow workflow = workflowService.getDetail(String.valueOf(identifier));
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(workflow, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class WorkflowReadAccessSnapshot {
|
||||
|
||||
private final RoleCategoryAccessSnapshot categoryAccess;
|
||||
private final Set<BigInteger> readableDeptIds;
|
||||
|
||||
public WorkflowReadAccessSnapshot(RoleCategoryAccessSnapshot categoryAccess, Set<BigInteger> readableDeptIds) {
|
||||
this.categoryAccess = categoryAccess;
|
||||
this.readableDeptIds = readableDeptIds == null
|
||||
? Collections.emptySet()
|
||||
: Collections.unmodifiableSet(new LinkedHashSet<>(readableDeptIds));
|
||||
}
|
||||
|
||||
public BigInteger getAccountId() {
|
||||
return categoryAccess == null ? null : categoryAccess.getAccountId();
|
||||
}
|
||||
|
||||
public boolean isSuperAdmin() {
|
||||
return categoryAccess != null && categoryAccess.isSuperAdmin();
|
||||
}
|
||||
|
||||
public boolean isRestricted() {
|
||||
return categoryAccess == null || categoryAccess.isRestricted();
|
||||
}
|
||||
|
||||
public Set<BigInteger> getCategoryIds() {
|
||||
return categoryAccess == null ? Collections.emptySet() : categoryAccess.getCategoryIds();
|
||||
}
|
||||
|
||||
public Set<BigInteger> getReadableDeptIds() {
|
||||
return readableDeptIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.WorkflowTableDef.WORKFLOW;
|
||||
|
||||
@Component
|
||||
public class WorkflowVisibilityQueryHelper {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Resource
|
||||
private SysDeptService sysDeptService;
|
||||
|
||||
public WorkflowReadAccessSnapshot getCurrentReadSnapshot() {
|
||||
RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getCurrentAccess(CategoryResourceType.WORKFLOW.getCode());
|
||||
if (categoryAccess.isSuperAdmin()) {
|
||||
return new WorkflowReadAccessSnapshot(categoryAccess, Collections.emptySet());
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
Set<BigInteger> deptIds = loginAccount == null
|
||||
? Collections.emptySet()
|
||||
: sysDeptService.getSelfAndAncestorDeptIds(loginAccount.getDeptId());
|
||||
return new WorkflowReadAccessSnapshot(categoryAccess, deptIds);
|
||||
}
|
||||
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper) {
|
||||
applyReadableAccess(queryWrapper, getCurrentReadSnapshot());
|
||||
}
|
||||
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper, WorkflowReadAccessSnapshot snapshot) {
|
||||
if (snapshot.isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
BigInteger accountId = snapshot.getAccountId();
|
||||
if (accountId == null) {
|
||||
queryWrapper.eq("id", BigInteger.valueOf(-1));
|
||||
return;
|
||||
}
|
||||
QueryCondition ownerCondition = WORKFLOW.CREATED_BY.eq(accountId);
|
||||
if (snapshot.isRestricted() && snapshot.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.and(ownerCondition);
|
||||
return;
|
||||
}
|
||||
QueryCondition visibilityCondition = WORKFLOW.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name());
|
||||
if (!snapshot.getReadableDeptIds().isEmpty()) {
|
||||
visibilityCondition = visibilityCondition.or(
|
||||
WORKFLOW.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name())
|
||||
.and(WORKFLOW.DEPT_ID.in(snapshot.getReadableDeptIds()))
|
||||
);
|
||||
}
|
||||
QueryCondition readableCondition = visibilityCondition;
|
||||
if (snapshot.isRestricted()) {
|
||||
readableCondition = WORKFLOW.CATEGORY_ID.in(snapshot.getCategoryIds()).and(visibilityCondition);
|
||||
}
|
||||
queryWrapper.and(ownerCondition.or(readableCondition));
|
||||
}
|
||||
|
||||
public boolean canRead(Workflow workflow) {
|
||||
return canRead(workflow, getCurrentReadSnapshot());
|
||||
}
|
||||
|
||||
public boolean canRead(Workflow workflow, WorkflowReadAccessSnapshot snapshot) {
|
||||
if (workflow == null) {
|
||||
return false;
|
||||
}
|
||||
if (snapshot.isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
BigInteger accountId = snapshot.getAccountId();
|
||||
if (accountId != null && accountId.equals(workflow.getCreatedBy())) {
|
||||
return true;
|
||||
}
|
||||
if (snapshot.isRestricted() && (workflow.getCategoryId() == null || !snapshot.getCategoryIds().contains(workflow.getCategoryId()))) {
|
||||
return false;
|
||||
}
|
||||
VisibilityScope scope = VisibilityScope.fromOrDefault(workflow.getVisibilityScope(), VisibilityScope.PRIVATE);
|
||||
if (VisibilityScope.PUBLIC == scope) {
|
||||
return true;
|
||||
}
|
||||
return VisibilityScope.DEPT == scope
|
||||
&& workflow.getDeptId() != null
|
||||
&& snapshot.getReadableDeptIds().contains(workflow.getDeptId());
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package tech.easyflow.ai.service;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
@@ -25,4 +26,10 @@ public interface DocumentService extends IService<Document> {
|
||||
Result textSplit(DocumentCollectionSplitParams documentCollectionSplitParams);
|
||||
|
||||
Result saveTextResult(List<DocumentChunk> documentChunks, Document document);
|
||||
|
||||
Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(DocumentImportDtos.AnalyzeRequest request);
|
||||
|
||||
Result<DocumentImportDtos.PreviewResponse> previewImport(DocumentImportDtos.PreviewRequest request);
|
||||
|
||||
Result<DocumentImportDtos.CommitResponse> commitImport(DocumentImportDtos.CommitRequest request);
|
||||
}
|
||||
|
||||
@@ -24,4 +24,16 @@ public interface ModelService extends IService<Model> {
|
||||
void removeByEntity(Model entity);
|
||||
|
||||
Model getModelInstance(BigInteger modelId);
|
||||
|
||||
Model getModelInstanceByInvokeCode(String invokeCode);
|
||||
|
||||
void validateForSaveOrUpdate(Model entity, boolean isSave);
|
||||
|
||||
List<Model> listInvokeModels();
|
||||
|
||||
List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType);
|
||||
|
||||
Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled);
|
||||
|
||||
List<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, Boolean publishEnabled);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import tech.easyflow.ai.entity.PluginCategory;
|
||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -16,7 +15,7 @@ import java.util.List;
|
||||
*/
|
||||
public interface PluginCategoryMappingService extends IService<PluginCategoryMapping> {
|
||||
|
||||
boolean updateRelation(BigInteger pluginId, ArrayList<BigInteger> categoryIds);
|
||||
boolean updateRelation(BigInteger pluginId, List<BigInteger> categoryIds);
|
||||
|
||||
List<PluginCategory> getPluginCategories(BigInteger pluginId);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.List;
|
||||
*/
|
||||
public interface PluginService extends IService<Plugin> {
|
||||
|
||||
boolean savePlugin(Plugin plugin);
|
||||
Plugin savePlugin(Plugin plugin);
|
||||
|
||||
boolean removePlugin(String id);
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
public interface PluginVisibilityService {
|
||||
|
||||
Set<BigInteger> getCurrentVisiblePluginIds();
|
||||
|
||||
boolean canAccessPlugin(Long createdBy, BigInteger pluginId);
|
||||
|
||||
void assertPluginVisible(Long createdBy, BigInteger pluginId, String message);
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import tech.easyflow.common.util.UrlEncoderUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
@@ -107,6 +108,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
private McpService mcpService;
|
||||
@Resource(name = "default")
|
||||
FileStorageService storageService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Override
|
||||
public Bot getDetail(String id) {
|
||||
@@ -144,6 +147,13 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
if (aiBot == null) {
|
||||
return ChatSseUtil.sendSystemError(conversationId, "聊天助手不存在");
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
try {
|
||||
categoryPermissionService.assertCategoryResourceVisible("BOT", aiBot.getCreatedBy(), aiBot.getCategoryId(), "无权限访问聊天助手");
|
||||
} catch (BusinessException e) {
|
||||
return ChatSseUtil.sendSystemError(conversationId, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (aiBot.getModelId() == null) {
|
||||
return ChatSseUtil.sendSystemError(conversationId, "请配置大模型!");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ import com.easyagents.core.model.embedding.EmbeddingOptions;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import com.easyagents.rag.core.RagChunk;
|
||||
import com.easyagents.rag.core.RagDefaults;
|
||||
import com.easyagents.rag.core.RagStrategyCodes;
|
||||
import com.easyagents.rag.ingestion.RagIngestionService;
|
||||
import com.easyagents.rag.ingestion.model.AnalysisResult;
|
||||
import com.easyagents.rag.ingestion.model.StrategyConfig;
|
||||
import com.easyagents.search.engine.service.DocumentSearcher;
|
||||
import com.mybatisflex.core.keygen.impl.FlexIDKeyGenerator;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
@@ -24,6 +30,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.config.SearcherFactory;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportKeys;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportPreviewService;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
||||
import tech.easyflow.ai.mapper.DocumentMapper;
|
||||
@@ -42,6 +51,7 @@ import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -81,6 +91,12 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
@Autowired
|
||||
private SearcherFactory searcherFactory;
|
||||
|
||||
@Autowired
|
||||
private RagIngestionService ragIngestionService;
|
||||
|
||||
@Autowired
|
||||
private DocumentImportPreviewService documentImportPreviewService;
|
||||
|
||||
@Override
|
||||
public Page<Document> getDocumentList(String knowledgeId, int pageSize, int pageNum, String fileName) {
|
||||
QueryWrapper queryWrapper=QueryWrapper.create()
|
||||
@@ -250,23 +266,397 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
return Result.fail(1, "切割结果无有效文本,无法进行向量化");
|
||||
}
|
||||
|
||||
Boolean result = storeDocument(document, validChunks);
|
||||
if (result) {
|
||||
this.getMapper().insert(document);
|
||||
AtomicInteger sort = new AtomicInteger(1);
|
||||
validChunks.forEach(item -> {
|
||||
item.setDocumentCollectionId(document.getCollectionId());
|
||||
item.setSorting(sort.get());
|
||||
item.setDocumentId(document.getId());
|
||||
sort.getAndIncrement();
|
||||
documentChunkService.save(item);
|
||||
});
|
||||
StoreExecutionContext storeContext = prepareStoreContext(document);
|
||||
storeDocumentChunks(storeContext, validChunks);
|
||||
try {
|
||||
persistDocumentWithChunks(document, validChunks);
|
||||
updateKnowledgeAfterStore(storeContext);
|
||||
return Result.ok();
|
||||
} catch (Exception e) {
|
||||
cleanupPersistedDocument(document);
|
||||
rollbackStoredChunks(storeContext, validChunks);
|
||||
Log.error("保存文档失败: documentId={}, title={}", document.getId(), document.getTitle(), e);
|
||||
throw new BusinessException("保存失败:" + e.getMessage());
|
||||
}
|
||||
return Result.fail(1, "保存失败");
|
||||
}
|
||||
|
||||
protected Boolean storeDocument(Document entity, List<DocumentChunk> documentChunks) {
|
||||
StoreExecutionContext storeContext = prepareStoreContext(entity);
|
||||
storeDocumentChunks(storeContext, documentChunks);
|
||||
updateKnowledgeAfterStore(storeContext);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(DocumentImportDtos.AnalyzeRequest request) {
|
||||
DocumentCollection knowledge = assertDocumentCollection(request.getKnowledgeId());
|
||||
if (request.getFiles() == null || request.getFiles().isEmpty()) {
|
||||
throw new BusinessException("请先上传文件");
|
||||
}
|
||||
|
||||
List<DocumentImportDtos.AnalyzeItem> items = new ArrayList<>();
|
||||
for (DocumentImportDtos.FileItem file : request.getFiles()) {
|
||||
AnalysisResult analysis = analyzeSingleFile(file.getFilePath(), file.getFileName());
|
||||
StrategyConfig strategyConfig = resolveStrategyConfig(
|
||||
knowledge,
|
||||
null,
|
||||
analysis
|
||||
);
|
||||
|
||||
DocumentImportDtos.AnalyzeItem item = new DocumentImportDtos.AnalyzeItem();
|
||||
item.setFilePath(file.getFilePath());
|
||||
item.setFileName(file.getFileName());
|
||||
item.setAnalysis(analysis);
|
||||
item.setStrategyConfig(strategyConfig);
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
DocumentImportDtos.AnalyzeResponse response = new DocumentImportDtos.AnalyzeResponse();
|
||||
response.setItems(items);
|
||||
response.setTotal(items.size());
|
||||
return Result.ok(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.PreviewResponse> previewImport(DocumentImportDtos.PreviewRequest request) {
|
||||
DocumentCollection knowledge = assertDocumentCollection(request.getKnowledgeId());
|
||||
if (request.getFiles() == null || request.getFiles().isEmpty()) {
|
||||
throw new BusinessException("请先上传文件");
|
||||
}
|
||||
|
||||
List<DocumentImportDtos.PreviewFileResult> items = new ArrayList<>();
|
||||
int totalChunks = 0;
|
||||
for (DocumentImportDtos.PreviewFileRequest file : request.getFiles()) {
|
||||
DocumentImportDtos.PreviewSession session = buildPreviewSession(knowledge, file);
|
||||
String sessionId = documentImportPreviewService.put(session);
|
||||
|
||||
DocumentImportDtos.PreviewFileResult item = new DocumentImportDtos.PreviewFileResult();
|
||||
item.setPreviewSessionId(sessionId);
|
||||
item.setFilePath(file.getFilePath());
|
||||
item.setFileName(file.getFileName());
|
||||
item.setStrategyCode(session.getStrategyConfig().getStrategyCode());
|
||||
item.setStrategyLabel(ragIngestionService.toStrategyLabel(session.getStrategyConfig().getStrategyCode()));
|
||||
item.setAnalysis(session.getAnalysis());
|
||||
item.setChunks(session.getPreviewChunks());
|
||||
item.setTotalChunks(session.getPreviewChunks().size());
|
||||
item.setTotalWarnings(countWarnings(session.getPreviewChunks()));
|
||||
items.add(item);
|
||||
totalChunks += session.getPreviewChunks().size();
|
||||
}
|
||||
|
||||
DocumentImportDtos.PreviewResponse response = new DocumentImportDtos.PreviewResponse();
|
||||
response.setItems(items);
|
||||
response.setTotalFiles(items.size());
|
||||
response.setTotalChunks(totalChunks);
|
||||
return Result.ok(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.CommitResponse> commitImport(DocumentImportDtos.CommitRequest request) {
|
||||
DocumentCollection knowledge = assertDocumentCollection(request.getKnowledgeId());
|
||||
if (request.getPreviewSessionIds() == null || request.getPreviewSessionIds().isEmpty()) {
|
||||
throw new BusinessException("请选择需要提交的预览结果");
|
||||
}
|
||||
|
||||
List<DocumentImportDtos.CommitFileResult> results = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int errorCount = 0;
|
||||
for (String previewSessionId : request.getPreviewSessionIds()) {
|
||||
DocumentImportDtos.CommitFileResult result = new DocumentImportDtos.CommitFileResult();
|
||||
result.setPreviewSessionId(previewSessionId);
|
||||
try {
|
||||
DocumentImportDtos.PreviewSession session = documentImportPreviewService.getRequired(previewSessionId);
|
||||
if (!Objects.equals(session.getKnowledgeId(), knowledge.getId())) {
|
||||
throw new BusinessException("预览会话与当前知识库不匹配");
|
||||
}
|
||||
commitSingleSession(session);
|
||||
result.setSuccess(true);
|
||||
result.setFileName(session.getFileName());
|
||||
result.setDocumentId(session.getDocument().getId());
|
||||
result.setChunkCount(session.getDocumentChunks().size());
|
||||
documentImportPreviewService.remove(previewSessionId);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
result.setSuccess(false);
|
||||
result.setReason(e.getMessage());
|
||||
errorCount++;
|
||||
}
|
||||
results.add(result);
|
||||
}
|
||||
|
||||
DocumentImportDtos.CommitResponse response = new DocumentImportDtos.CommitResponse();
|
||||
response.setTotalFiles(results.size());
|
||||
response.setSuccessCount(successCount);
|
||||
response.setErrorCount(errorCount);
|
||||
response.setResults(results);
|
||||
return Result.ok(response);
|
||||
}
|
||||
|
||||
private void commitSingleSession(DocumentImportDtos.PreviewSession session) {
|
||||
Document document = session.getDocument();
|
||||
document.setCreated(new Date());
|
||||
document.setModified(new Date());
|
||||
document.setCreatedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
||||
document.setModifiedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
||||
for (DocumentChunk chunk : session.getDocumentChunks()) {
|
||||
chunk.setDocumentId(document.getId());
|
||||
chunk.setDocumentCollectionId(document.getCollectionId());
|
||||
}
|
||||
|
||||
StoreExecutionContext storeContext = prepareStoreContext(document);
|
||||
storeDocumentChunks(storeContext, session.getDocumentChunks());
|
||||
try {
|
||||
persistDocumentWithChunks(document, session.getDocumentChunks());
|
||||
updateKnowledgeAfterStore(storeContext);
|
||||
} catch (Exception e) {
|
||||
cleanupPersistedDocument(document);
|
||||
rollbackStoredChunks(storeContext, session.getDocumentChunks());
|
||||
throw new BusinessException("提交导入失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentImportDtos.PreviewSession buildPreviewSession(DocumentCollection knowledge,
|
||||
DocumentImportDtos.PreviewFileRequest fileRequest) {
|
||||
AnalysisResult analysis = analyzeSingleFile(fileRequest.getFilePath(), fileRequest.getFileName());
|
||||
StrategyConfig strategyConfig = resolveStrategyConfig(knowledge, fileRequest.getStrategyConfig(), analysis);
|
||||
List<RagChunk> previewChunks = ragIngestionService.split(analysis, strategyConfig);
|
||||
if (previewChunks.isEmpty()) {
|
||||
throw new BusinessException("未生成有效分块,请调整策略后重试");
|
||||
}
|
||||
|
||||
FlexIDKeyGenerator flexIDKeyGenerator = new FlexIDKeyGenerator();
|
||||
Document document = buildPreviewDocument(flexIDKeyGenerator, knowledge, fileRequest, analysis, strategyConfig);
|
||||
List<DocumentChunk> documentChunks = buildDocumentChunks(flexIDKeyGenerator, document, previewChunks);
|
||||
|
||||
DocumentImportDtos.PreviewSession session = new DocumentImportDtos.PreviewSession();
|
||||
session.setKnowledgeId(knowledge.getId());
|
||||
session.setFilePath(fileRequest.getFilePath());
|
||||
session.setFileName(fileRequest.getFileName());
|
||||
session.setSourceFormat(analysis.getSourceFormat());
|
||||
session.setStrategyConfig(strategyConfig);
|
||||
session.setAnalysis(analysis);
|
||||
session.setDocument(document);
|
||||
session.setDocumentChunks(documentChunks);
|
||||
session.setPreviewChunks(previewChunks);
|
||||
session.setCreatedAt(new Date());
|
||||
return session;
|
||||
}
|
||||
|
||||
private Document buildPreviewDocument(FlexIDKeyGenerator flexIDKeyGenerator,
|
||||
DocumentCollection knowledge,
|
||||
DocumentImportDtos.PreviewFileRequest fileRequest,
|
||||
AnalysisResult analysis,
|
||||
StrategyConfig strategyConfig) {
|
||||
Document document = new Document();
|
||||
document.setId(new BigInteger(String.valueOf(flexIDKeyGenerator.generate(document, null))));
|
||||
document.setCollectionId(knowledge.getId());
|
||||
document.setDocumentType(analysis.getSourceFormat());
|
||||
document.setDocumentPath(fileRequest.getFilePath());
|
||||
document.setTitle(fileRequest.getFileName());
|
||||
document.setContent(analysis.getNormalizedContent());
|
||||
document.setCreated(new Date());
|
||||
document.setModified(new Date());
|
||||
document.setModifiedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
||||
|
||||
Map<String, Object> options = new LinkedHashMap<>();
|
||||
options.put(DocumentImportKeys.KEY_DOCUMENT_STRATEGY_CODE, strategyConfig.getStrategyCode());
|
||||
options.put(DocumentImportKeys.KEY_DOCUMENT_STRATEGY_LABEL, ragIngestionService.toStrategyLabel(strategyConfig.getStrategyCode()));
|
||||
options.put(DocumentImportKeys.KEY_DOCUMENT_STRATEGY_SNAPSHOT, strategyConfigToMap(strategyConfig));
|
||||
options.put(DocumentImportKeys.KEY_DOCUMENT_ANALYSIS_SUMMARY, analysis.getFeatures());
|
||||
options.put(DocumentImportKeys.KEY_DOCUMENT_SOURCE_FILE_EXT, analysis.getSourceFormat());
|
||||
options.put(DocumentImportKeys.KEY_DOCUMENT_PREVIEW_VERSION, "v1");
|
||||
document.setOptions(options);
|
||||
return document;
|
||||
}
|
||||
|
||||
private List<DocumentChunk> buildDocumentChunks(FlexIDKeyGenerator flexIDKeyGenerator,
|
||||
Document document,
|
||||
List<RagChunk> previewChunks) {
|
||||
List<DocumentChunk> chunks = new ArrayList<>();
|
||||
for (int i = 0; i < previewChunks.size(); i++) {
|
||||
RagChunk previewChunk = previewChunks.get(i);
|
||||
DocumentChunk chunk = new DocumentChunk();
|
||||
chunk.setId(new BigInteger(String.valueOf(flexIDKeyGenerator.generate(chunk, null))));
|
||||
chunk.setDocumentId(document.getId());
|
||||
chunk.setDocumentCollectionId(document.getCollectionId());
|
||||
chunk.setContent(previewChunk.getContent());
|
||||
chunk.setSorting(i + 1);
|
||||
|
||||
Map<String, Object> options = new LinkedHashMap<>(previewChunk.getOptions());
|
||||
options.put("chunkType", previewChunk.getChunkType());
|
||||
options.put("sourceLabel", previewChunk.getSourceLabel());
|
||||
options.put("headingPath", previewChunk.getHeadingPath());
|
||||
options.put("charCount", previewChunk.getCharCount());
|
||||
options.put("tokenEstimate", previewChunk.getTokenEstimate());
|
||||
options.put("qaQuestion", previewChunk.getQuestion());
|
||||
options.put("qaAnswer", previewChunk.getAnswer());
|
||||
options.put("partNo", previewChunk.getPartNo());
|
||||
options.put("partTotal", previewChunk.getPartTotal());
|
||||
options.put("warnings", previewChunk.getWarnings());
|
||||
chunk.setOptions(options);
|
||||
chunks.add(chunk);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private AnalysisResult analyzeSingleFile(String filePath, String fileName) {
|
||||
String fileExt = normalizeFileExtension(fileName, filePath);
|
||||
assertSupportedImportFile(fileExt);
|
||||
String content = readFileContent(filePath, fileName);
|
||||
return ragIngestionService.analyze(content, fileExt);
|
||||
}
|
||||
|
||||
private String readFileContent(String filePath, String fileName) {
|
||||
try (InputStream inputStream = storageService.readStream(filePath)) {
|
||||
return File2TextUtil.readFromStream(inputStream, fileName, null);
|
||||
} catch (IOException e) {
|
||||
Log.error("读取导入文件失败: filePath={}, fileName={}", filePath, fileName, e);
|
||||
throw new BusinessException("文件解析失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertSupportedImportFile(String fileExt) {
|
||||
if (!Arrays.asList("pdf", "docx", "txt", "md").contains(fileExt)) {
|
||||
throw new BusinessException("当前仅支持 pdf/docx/txt/md 文档导入");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeFileExtension(String fileName, String filePath) {
|
||||
String target = StringUtil.hasText(fileName) ? fileName : filePath;
|
||||
String ext = FileUtil.getFileTypeByExtension(target);
|
||||
return ext == null ? "" : ext.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private DocumentCollection assertDocumentCollection(BigInteger knowledgeId) {
|
||||
DocumentCollection knowledge = knowledgeService.getById(knowledgeId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
if (knowledge.isFaqCollection()) {
|
||||
throw new BusinessException("FAQ知识库不支持文档上传");
|
||||
}
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
private StrategyConfig resolveStrategyConfig(DocumentCollection knowledge,
|
||||
StrategyConfig requestConfig,
|
||||
AnalysisResult analysisResult) {
|
||||
Map<String, Object> options = knowledge.getOptions() == null
|
||||
? Collections.emptyMap()
|
||||
: knowledge.getOptions();
|
||||
String recommended = analysisResult.getRecommendedStrategyCode();
|
||||
String defaultStrategyCode = asString(options.get(DocumentImportKeys.KEY_SPLITTER_DEFAULT_STRATEGY));
|
||||
String fallbackStrategyCode = asString(options.get(DocumentImportKeys.KEY_SPLITTER_FALLBACK_STRATEGY));
|
||||
Boolean autoRecommendEnabled = asBoolean(options.get(DocumentImportKeys.KEY_SPLITTER_AUTO_RECOMMEND_ENABLED), true);
|
||||
|
||||
StrategyConfig config = readProfileConfig(options, defaultStrategyCode);
|
||||
if (config == null) {
|
||||
config = StrategyConfig.defaults();
|
||||
}
|
||||
String requestedStrategyCode = requestConfig == null ? null : requestConfig.getStrategyCode();
|
||||
|
||||
String strategyCode = StringUtil.hasText(requestedStrategyCode)
|
||||
? requestedStrategyCode
|
||||
: config.getStrategyCode();
|
||||
if (!StringUtil.hasText(strategyCode) || RagStrategyCodes.AUTO.equals(strategyCode)) {
|
||||
strategyCode = Boolean.TRUE.equals(autoRecommendEnabled)
|
||||
? recommended
|
||||
: (StringUtil.hasText(defaultStrategyCode) ? defaultStrategyCode : recommended);
|
||||
}
|
||||
if (!StringUtil.hasText(strategyCode)) {
|
||||
strategyCode = StringUtil.hasText(fallbackStrategyCode)
|
||||
? fallbackStrategyCode
|
||||
: RagStrategyCodes.PARAGRAPH_LENGTH;
|
||||
}
|
||||
|
||||
StrategyConfig profileConfig = readProfileConfig(options, strategyCode);
|
||||
if (profileConfig != null) {
|
||||
mergeStrategyConfig(config, profileConfig);
|
||||
}
|
||||
if (requestConfig != null) {
|
||||
mergeStrategyConfig(config, requestConfig);
|
||||
}
|
||||
config.setStrategyCode(strategyCode);
|
||||
if (config.getChunkSize() == null || config.getChunkSize() <= 0) {
|
||||
config.setChunkSize(RagDefaults.CHUNK_SIZE);
|
||||
}
|
||||
if (config.getOverlapSize() == null || config.getOverlapSize() < 0) {
|
||||
config.setOverlapSize(RagDefaults.OVERLAP_SIZE);
|
||||
}
|
||||
if (config.getMdSplitterLevel() == null || config.getMdSplitterLevel() <= 0) {
|
||||
config.setMdSplitterLevel(RagDefaults.MD_SPLITTER_LEVEL);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private StrategyConfig readProfileConfig(Map<String, Object> options, String strategyCode) {
|
||||
if (!StringUtil.hasText(strategyCode)) {
|
||||
return null;
|
||||
}
|
||||
Object profileObject = options.get(DocumentImportKeys.KEY_SPLITTER_STRATEGY_PROFILES);
|
||||
if (!(profileObject instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
Object strategyObject = ((Map<String, Object>) profileObject).get(strategyCode);
|
||||
if (!(strategyObject instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> rawProfile = (Map<String, Object>) strategyObject;
|
||||
StrategyConfig config = StrategyConfig.defaults();
|
||||
config.setStrategyCode(strategyCode);
|
||||
config.setChunkSize(asInteger(rawProfile.get("chunkSize"), config.getChunkSize()));
|
||||
config.setOverlapSize(asInteger(rawProfile.get("overlapSize"), config.getOverlapSize()));
|
||||
config.setRegex(asString(rawProfile.get("regex")));
|
||||
config.setRowsPerChunk(asInteger(rawProfile.get("rowsPerChunk"), config.getRowsPerChunk()));
|
||||
config.setMdSplitterLevel(asInteger(rawProfile.get("mdSplitterLevel"), config.getMdSplitterLevel()));
|
||||
return config;
|
||||
}
|
||||
|
||||
private void mergeStrategyConfig(StrategyConfig target, StrategyConfig source) {
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
if (StringUtil.hasText(source.getStrategyCode())) {
|
||||
target.setStrategyCode(source.getStrategyCode());
|
||||
}
|
||||
if (source.getChunkSize() != null) {
|
||||
target.setChunkSize(source.getChunkSize());
|
||||
}
|
||||
if (source.getOverlapSize() != null) {
|
||||
target.setOverlapSize(source.getOverlapSize());
|
||||
}
|
||||
if (StringUtil.hasText(source.getRegex())) {
|
||||
target.setRegex(source.getRegex());
|
||||
}
|
||||
if (source.getRowsPerChunk() != null) {
|
||||
target.setRowsPerChunk(source.getRowsPerChunk());
|
||||
}
|
||||
if (source.getMdSplitterLevel() != null) {
|
||||
target.setMdSplitterLevel(source.getMdSplitterLevel());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> strategyConfigToMap(StrategyConfig strategyConfig) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("strategyCode", strategyConfig.getStrategyCode());
|
||||
map.put("chunkSize", strategyConfig.getChunkSize());
|
||||
map.put("overlapSize", strategyConfig.getOverlapSize());
|
||||
map.put("regex", strategyConfig.getRegex());
|
||||
map.put("rowsPerChunk", strategyConfig.getRowsPerChunk());
|
||||
map.put("mdSplitterLevel", strategyConfig.getMdSplitterLevel());
|
||||
return map;
|
||||
}
|
||||
|
||||
private int countWarnings(List<RagChunk> chunks) {
|
||||
int total = 0;
|
||||
for (RagChunk chunk : chunks) {
|
||||
total += chunk.getWarnings() == null ? 0 : chunk.getWarnings().size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private StoreExecutionContext prepareStoreContext(Document entity) {
|
||||
DocumentCollection knowledge = knowledgeService.getById(entity.getCollectionId());
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
@@ -274,23 +664,22 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
if (knowledge.isFaqCollection()) {
|
||||
throw new BusinessException("FAQ知识库不支持文档上传");
|
||||
}
|
||||
DocumentStore documentStore = null;
|
||||
|
||||
DocumentStore documentStore;
|
||||
try {
|
||||
documentStore = knowledge.toDocumentStore();
|
||||
} catch (Exception e) {
|
||||
Log.error(e.getMessage());
|
||||
Log.error("向量库配置错误: knowledgeId={}", knowledge.getId(), e);
|
||||
throw new BusinessException("向量数据库配置错误");
|
||||
}
|
||||
|
||||
if (documentStore == null) {
|
||||
throw new BusinessException("向量数据库配置错误");
|
||||
}
|
||||
// 设置向量模型
|
||||
|
||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
throw new BusinessException("该知识库未配置大模型");
|
||||
}
|
||||
// 设置向量模型
|
||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||
documentStore.setEmbeddingModel(embeddingModel);
|
||||
|
||||
@@ -300,46 +689,152 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
embeddingOptions.setDimensions(knowledge.getDimensionOfVectorModel());
|
||||
options.setEmbeddingOptions(embeddingOptions);
|
||||
options.setIndexName(options.getCollectionName());
|
||||
|
||||
DocumentSearcher searcher = null;
|
||||
if (knowledge.isSearchEngineEnabled()) {
|
||||
searcher = searcherFactory.getSearcher((String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE));
|
||||
}
|
||||
return new StoreExecutionContext(knowledge, model, embeddingModel, documentStore, options, searcher);
|
||||
}
|
||||
|
||||
private void storeDocumentChunks(StoreExecutionContext storeContext, List<DocumentChunk> documentChunks) {
|
||||
List<com.easyagents.core.document.Document> documents = new ArrayList<>();
|
||||
documentChunks.forEach(item -> {
|
||||
for (DocumentChunk item : documentChunks) {
|
||||
com.easyagents.core.document.Document document = new com.easyagents.core.document.Document();
|
||||
document.setId(item.getId());
|
||||
document.setContent(item.getContent());
|
||||
documents.add(document);
|
||||
}
|
||||
);
|
||||
StoreResult result = null;
|
||||
|
||||
StoreResult result;
|
||||
try {
|
||||
result = documentStore.store(documents, options);
|
||||
result = storeContext.documentStore.store(documents, storeContext.options);
|
||||
} catch (Exception e) {
|
||||
Log.error("Vector store failed: knowledgeId={}, collection={}, chunkCount={}",
|
||||
knowledge.getId(), options.getCollectionName(), documents.size(), e);
|
||||
storeContext.knowledge.getId(),
|
||||
storeContext.options.getCollectionName(),
|
||||
documents.size(),
|
||||
e);
|
||||
throw new BusinessException("向量过程中发生错误,错误信息为:" + e.getMessage());
|
||||
}
|
||||
if (result == null || !result.isSuccess()) {
|
||||
Log.error("DocumentStore.store failed: " + result);
|
||||
Log.error("DocumentStore.store failed: {}", result);
|
||||
throw new BusinessException("DocumentStore.store failed");
|
||||
}
|
||||
|
||||
if (knowledge.isSearchEngineEnabled()) {
|
||||
// 获取搜索引擎
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher((String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE));
|
||||
// 添加到搜索引擎
|
||||
documents.forEach(searcher::addDocument);
|
||||
if (storeContext.searcher != null) {
|
||||
for (com.easyagents.core.document.Document document : documents) {
|
||||
storeContext.searcher.addDocument(document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rollbackStoredChunks(StoreExecutionContext storeContext, List<DocumentChunk> documentChunks) {
|
||||
try {
|
||||
List<BigInteger> chunkIds = new ArrayList<>();
|
||||
for (DocumentChunk chunk : documentChunks) {
|
||||
chunkIds.add(chunk.getId());
|
||||
}
|
||||
storeContext.documentStore.delete(chunkIds, storeContext.options);
|
||||
if (storeContext.searcher != null) {
|
||||
for (BigInteger chunkId : chunkIds) {
|
||||
storeContext.searcher.deleteDocument(chunkId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.error("回滚向量文档失败: knowledgeId={}", storeContext.knowledge.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateKnowledgeAfterStore(StoreExecutionContext storeContext) {
|
||||
DocumentCollection documentCollection = new DocumentCollection();
|
||||
documentCollection.setId(entity.getCollectionId());
|
||||
Map<String, Object> knowledgeOptions = knowledge.getOptions();
|
||||
documentCollection.setId(storeContext.knowledge.getId());
|
||||
Map<String, Object> knowledgeOptions = storeContext.knowledge.getOptions() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(storeContext.knowledge.getOptions());
|
||||
knowledgeOptions.put(KEY_CAN_UPDATE_EMBEDDING_MODEL, false);
|
||||
documentCollection.setOptions(knowledgeOptions);
|
||||
knowledgeService.updateById(documentCollection);
|
||||
if (knowledge.getDimensionOfVectorModel() == null) {
|
||||
int dimension = Model.getEmbeddingDimension(embeddingModel);
|
||||
knowledge.setDimensionOfVectorModel(dimension);
|
||||
knowledgeService.updateById(knowledge);
|
||||
|
||||
if (storeContext.knowledge.getDimensionOfVectorModel() == null) {
|
||||
int dimension = Model.getEmbeddingDimension(storeContext.embeddingModel);
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(storeContext.knowledge.getId());
|
||||
update.setDimensionOfVectorModel(dimension);
|
||||
knowledgeService.updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistDocumentWithChunks(Document document, List<DocumentChunk> chunks) {
|
||||
this.getMapper().insert(document);
|
||||
AtomicInteger sort = new AtomicInteger(1);
|
||||
for (DocumentChunk item : chunks) {
|
||||
item.setDocumentCollectionId(document.getCollectionId());
|
||||
item.setDocumentId(document.getId());
|
||||
item.setSorting(sort.getAndIncrement());
|
||||
documentChunkService.save(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupPersistedDocument(Document document) {
|
||||
if (document == null || document.getId() == null) {
|
||||
return;
|
||||
}
|
||||
documentChunkMapper.deleteByQuery(QueryWrapper.create().eq(DocumentChunk::getDocumentId, document.getId()));
|
||||
this.getMapper().deleteById(document.getId());
|
||||
}
|
||||
|
||||
private String asString(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Integer asInteger(Object value, Integer defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
if (value instanceof String && StringUtil.hasText((String) value)) {
|
||||
return Integer.parseInt((String) value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private Boolean asBoolean(Object value, boolean defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue() != 0;
|
||||
}
|
||||
return Boolean.parseBoolean(String.valueOf(value));
|
||||
}
|
||||
|
||||
private static class StoreExecutionContext {
|
||||
private final DocumentCollection knowledge;
|
||||
private final Model model;
|
||||
private final EmbeddingModel embeddingModel;
|
||||
private final DocumentStore documentStore;
|
||||
private final StoreOptions options;
|
||||
private final DocumentSearcher searcher;
|
||||
|
||||
private StoreExecutionContext(DocumentCollection knowledge,
|
||||
Model model,
|
||||
EmbeddingModel embeddingModel,
|
||||
DocumentStore documentStore,
|
||||
StoreOptions options,
|
||||
DocumentSearcher searcher) {
|
||||
this.knowledge = knowledge;
|
||||
this.model = model;
|
||||
this.embeddingModel = embeddingModel;
|
||||
this.documentStore = documentStore;
|
||||
this.options = options;
|
||||
this.searcher = searcher;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public DocumentSplitter getDocumentSplitter(DocumentCollectionSplitParams params) {
|
||||
|
||||
@@ -10,17 +10,22 @@ import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||
import com.easyagents.core.model.rerank.RerankModel;
|
||||
import com.easyagents.core.store.VectorData;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.util.StringUtil;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
import tech.easyflow.ai.mapper.ModelMapper;
|
||||
import tech.easyflow.ai.service.ModelProviderService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
import tech.easyflow.common.util.SqlOperatorsUtil;
|
||||
import tech.easyflow.common.util.SqlUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -182,6 +187,152 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
||||
throw new BusinessException("模型ID不能为空");
|
||||
}
|
||||
Model model = modelMapper.selectOneWithRelationsById(modelId);
|
||||
return fillProviderDefaults(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model getModelInstanceByInvokeCode(String invokeCode) {
|
||||
if (StrUtil.isBlank(invokeCode)) {
|
||||
throw new BusinessException("invokeCode不能为空");
|
||||
}
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getInvokeCode, invokeCode.trim());
|
||||
Model model = modelMapper.selectOneWithRelationsByQuery(queryWrapper);
|
||||
return fillProviderDefaults(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateForSaveOrUpdate(Model entity, boolean isSave) {
|
||||
if (entity == null) {
|
||||
throw new BusinessException("模型配置不能为空");
|
||||
}
|
||||
if (entity.getPublishEnabled() == null) {
|
||||
entity.setPublishEnabled(Boolean.FALSE);
|
||||
}
|
||||
|
||||
String originalInvokeCode = StrUtil.trim(entity.getInvokeCode());
|
||||
String invokeCode = originalInvokeCode;
|
||||
boolean autoGeneratedInvokeCode = StrUtil.isBlank(invokeCode);
|
||||
if (autoGeneratedInvokeCode) {
|
||||
invokeCode = buildDefaultInvokeCode(entity.getModelName());
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(entity.getPublishEnabled())) {
|
||||
if (StrUtil.isBlank(invokeCode)) {
|
||||
throw new BusinessException("开启 API 调用前,请先配置 invokeCode");
|
||||
}
|
||||
if (!Model.MODEL_TYPES[0].equals(entity.getModelType())) {
|
||||
throw new BusinessException("只有聊天模型支持开启 API 调用");
|
||||
}
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(invokeCode)) {
|
||||
entity.setInvokeCode(null);
|
||||
return;
|
||||
}
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getInvokeCode, invokeCode);
|
||||
if (!isSave && entity.getId() != null) {
|
||||
queryWrapper.ne(Model::getId, entity.getId());
|
||||
}
|
||||
boolean duplicated = modelMapper.selectCountByQuery(queryWrapper) > 0;
|
||||
if (duplicated && autoGeneratedInvokeCode && !Boolean.TRUE.equals(entity.getPublishEnabled())) {
|
||||
entity.setInvokeCode(null);
|
||||
return;
|
||||
}
|
||||
if (duplicated) {
|
||||
throw new BusinessException("invokeCode 已存在,请更换后重试");
|
||||
}
|
||||
entity.setInvokeCode(invokeCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Model> listInvokeModels() {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getModelType, Model.MODEL_TYPES[0]);
|
||||
return modelMapper.selectListWithRelationsByQuery(queryWrapper).stream()
|
||||
.map(this::fillProviderDefaults)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(
|
||||
entity,
|
||||
entity == null ? com.mybatisflex.core.query.SqlOperators.empty() : SqlOperatorsUtil.build(entity.getClass())
|
||||
);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType));
|
||||
List<Model> list = Tree.tryToTree(modelMapper.selectListWithRelationsByQuery(queryWrapper), asTree);
|
||||
list.forEach(this::decorateModelTitle);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) {
|
||||
Model existing = getModelInstance(id);
|
||||
if (existing == null) {
|
||||
throw new BusinessException("模型不存在");
|
||||
}
|
||||
if (!Model.MODEL_TYPES[0].equals(existing.getModelType())) {
|
||||
throw new BusinessException("只有聊天模型支持统一网关配置");
|
||||
}
|
||||
|
||||
existing.setInvokeCode(invokeCode);
|
||||
existing.setPublishEnabled(Boolean.TRUE.equals(publishEnabled));
|
||||
validateForSaveOrUpdate(existing, false);
|
||||
modelMapper.update(existing);
|
||||
return getModelInstance(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, Boolean publishEnabled) {
|
||||
if (CollectionUtils.isEmpty(ids)) {
|
||||
throw new BusinessException("请选择要操作的模型");
|
||||
}
|
||||
|
||||
List<BigInteger> uniqueIds = new ArrayList<>(new LinkedHashSet<>(ids));
|
||||
List<Model> updatedModels = new ArrayList<>(uniqueIds.size());
|
||||
for (BigInteger id : uniqueIds) {
|
||||
updatedModels.add(updateInvokeConfig(id, null, publishEnabled));
|
||||
}
|
||||
return updatedModels;
|
||||
}
|
||||
|
||||
private void decorateModelTitle(Model model) {
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
String providerName = Optional.ofNullable(model.getModelProvider())
|
||||
.map(ModelProvider::getProviderName)
|
||||
.orElse("-");
|
||||
model.setTitle(providerName + "/" + model.getTitle());
|
||||
}
|
||||
|
||||
private String buildOrderBy(String sortKey, String sortType) {
|
||||
sortKey = StringUtil.camelToUnderline(sortKey);
|
||||
return SqlUtil.buildOrderBy(sortKey, sortType, "id desc");
|
||||
}
|
||||
|
||||
private String buildDefaultInvokeCode(String modelName) {
|
||||
if (StrUtil.isBlank(modelName)) {
|
||||
return null;
|
||||
}
|
||||
String normalized = modelName.trim()
|
||||
.replaceAll("[^A-Za-z0-9._:-]+", "-")
|
||||
.replaceAll("-{2,}", "-")
|
||||
.replaceFirst("^[^A-Za-z0-9]+", "");
|
||||
if (StrUtil.isBlank(normalized)) {
|
||||
return null;
|
||||
}
|
||||
if (normalized.length() > 128) {
|
||||
normalized = normalized.substring(0, 128);
|
||||
}
|
||||
if (normalized.length() == 1) {
|
||||
normalized = normalized + "-model";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private Model fillProviderDefaults(Model model) {
|
||||
if (model == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,23 @@ package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.entity.PluginCategory;
|
||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||
import tech.easyflow.ai.mapper.PluginCategoryMapper;
|
||||
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
|
||||
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 服务层实现。
|
||||
@@ -31,45 +36,46 @@ public class PluginCategoryMappingServiceImpl extends ServiceImpl<PluginCategory
|
||||
private PluginCategoryMapper pluginCategoryMapper;
|
||||
|
||||
@Override
|
||||
public boolean updateRelation(BigInteger pluginId, ArrayList<BigInteger> categoryIds) {
|
||||
if (categoryIds == null){
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().select("*")
|
||||
@Transactional
|
||||
public boolean updateRelation(BigInteger pluginId, List<BigInteger> categoryIds) {
|
||||
if (pluginId == null) {
|
||||
throw new BusinessException("插件ID不能为空");
|
||||
}
|
||||
|
||||
List<BigInteger> targetCategoryIds = categoryIds == null
|
||||
? Collections.emptyList()
|
||||
: categoryIds.stream()
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
QueryWrapper currentRelationQuery = QueryWrapper.create().select("category_id")
|
||||
.from("tb_plugin_category_mapping")
|
||||
.where("plugin_id = ?", pluginId);
|
||||
int delete = relationMapper.deleteByQuery(queryWrapper);
|
||||
if (delete <= 0){
|
||||
throw new BusinessException("删除失败");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (BigInteger categoryId : categoryIds) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().select("*")
|
||||
List<BigInteger> currentCategoryIds = relationMapper.selectListByQueryAs(currentRelationQuery, BigInteger.class);
|
||||
Set<BigInteger> currentCategoryIdSet = new LinkedHashSet<>(currentCategoryIds);
|
||||
Set<BigInteger> targetCategoryIdSet = new LinkedHashSet<>(targetCategoryIds);
|
||||
|
||||
Set<BigInteger> categoryIdsToDelete = new LinkedHashSet<>(currentCategoryIdSet);
|
||||
categoryIdsToDelete.removeAll(targetCategoryIdSet);
|
||||
if (!categoryIdsToDelete.isEmpty()) {
|
||||
QueryWrapper deleteQuery = QueryWrapper.create()
|
||||
.from("tb_plugin_category_mapping")
|
||||
.where("plugin_id = ?", pluginId)
|
||||
.where("category_id = ?", categoryId);
|
||||
PluginCategoryMapping selectedOneByQuery = relationMapper.selectOneByQuery(queryWrapper);
|
||||
.in("category_id", new ArrayList<>(categoryIdsToDelete));
|
||||
relationMapper.deleteByQuery(deleteQuery);
|
||||
}
|
||||
|
||||
Set<BigInteger> categoryIdsToInsert = new LinkedHashSet<>(targetCategoryIdSet);
|
||||
categoryIdsToInsert.removeAll(currentCategoryIdSet);
|
||||
for (BigInteger categoryId : categoryIdsToInsert) {
|
||||
PluginCategoryMapping pluginCategoryMapping = new PluginCategoryMapping();
|
||||
pluginCategoryMapping.setCategoryId(categoryId);
|
||||
pluginCategoryMapping.setPluginId(pluginId);
|
||||
if (selectedOneByQuery == null) {
|
||||
int insert = relationMapper.insert(pluginCategoryMapping);
|
||||
if (insert <= 0) {
|
||||
throw new BusinessException("新增失败");
|
||||
throw new BusinessException("新增分类关联失败");
|
||||
}
|
||||
} else {
|
||||
QueryWrapper queryWrapperUpdate = QueryWrapper.create().select("*")
|
||||
.from("tb_plugin_category_mapping")
|
||||
.where("plugin_id = ?", pluginId);
|
||||
PluginCategoryMapping selectedOne = relationMapper.selectOneByQuery(queryWrapper);
|
||||
if (selectedOne != null){
|
||||
continue;
|
||||
}
|
||||
int update = relationMapper.updateByQuery(pluginCategoryMapping, queryWrapperUpdate);
|
||||
if (update <= 0){
|
||||
throw new BusinessException("更新失败");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
|
||||
import tech.easyflow.ai.mapper.PluginMapper;
|
||||
import tech.easyflow.ai.service.BotPluginService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.PluginTableDef.PLUGIN;
|
||||
|
||||
/**
|
||||
* 服务层实现。
|
||||
*
|
||||
@@ -46,15 +56,19 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
||||
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private PluginVisibilityService pluginVisibilityService;
|
||||
|
||||
@Override
|
||||
public boolean savePlugin(Plugin plugin) {
|
||||
public Plugin savePlugin(Plugin plugin) {
|
||||
plugin.setCreated(new Date());
|
||||
int insert = pluginMapper.insert(plugin);
|
||||
if (insert <= 0) {
|
||||
throw new BusinessException("保存失败");
|
||||
}
|
||||
return true;
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,22 +118,39 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
||||
|
||||
@Override
|
||||
public Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category) {
|
||||
// 通过分类查询插件
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().select(PluginCategoryMapping::getPluginId)
|
||||
.eq(PluginCategoryMapping::getCategoryId, category);
|
||||
// 分页查询该分类中的插件
|
||||
Page<BigInteger> pagePluginIds = pluginCategoryMappingMapper.paginateAs(new Page<>(pageNumber, pageSize), queryWrapper, BigInteger.class);
|
||||
Page<PluginCategoryMapping> paginateCategories = pluginCategoryMappingMapper.paginate(pageNumber, pageSize, queryWrapper);
|
||||
List<Plugin> plugins = Collections.emptyList();
|
||||
if (paginateCategories.getRecords().isEmpty()) {
|
||||
return Result.ok(new Page<>(plugins, pageNumber, pageSize, paginateCategories.getTotalRow()));
|
||||
List<BigInteger> allCategoryPluginIds = pluginCategoryMappingMapper.selectListByQueryAs(queryWrapper, BigInteger.class)
|
||||
.stream()
|
||||
.filter(item -> item != null)
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
if (CollectionUtil.isEmpty(allCategoryPluginIds)) {
|
||||
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, 0L));
|
||||
}
|
||||
List<BigInteger> pluginIds = pagePluginIds.getRecords();
|
||||
// 查询对应的插件信息
|
||||
QueryWrapper queryPluginWrapper = QueryWrapper.create().select()
|
||||
.in(Plugin::getId, pluginIds);
|
||||
plugins = pluginMapper.selectListByQuery(queryPluginWrapper);
|
||||
Page<Plugin> aiPluginPage = new Page<>(plugins, pageNumber, pageSize, paginateCategories.getTotalRow());
|
||||
|
||||
List<BigInteger> orderedCategoryPluginIds = new ArrayList<>(new LinkedHashSet<>(allCategoryPluginIds));
|
||||
List<BigInteger> visiblePluginIds = orderedCategoryPluginIds;
|
||||
if (access.isRestricted()) {
|
||||
Set<BigInteger> visiblePluginIdSet = new LinkedHashSet<>(pluginVisibilityService.getCurrentVisiblePluginIds());
|
||||
List<BigInteger> creatorPluginIds = queryCreatorPluginIds(orderedCategoryPluginIds, access.getAccountIdAsLong());
|
||||
LinkedHashSet<BigInteger> mergedVisibleIds = orderedCategoryPluginIds.stream()
|
||||
.filter(pluginId -> visiblePluginIdSet.contains(pluginId) || creatorPluginIds.contains(pluginId))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
visiblePluginIds = new ArrayList<>(mergedVisibleIds);
|
||||
}
|
||||
if (visiblePluginIds.isEmpty()) {
|
||||
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, 0L));
|
||||
}
|
||||
|
||||
int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize));
|
||||
if (fromIndex >= visiblePluginIds.size()) {
|
||||
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, visiblePluginIds.size()));
|
||||
}
|
||||
int toIndex = Math.min(visiblePluginIds.size(), Math.toIntExact(fromIndex + pageSize));
|
||||
List<BigInteger> currentPagePluginIds = new ArrayList<>(visiblePluginIds.subList(fromIndex, toIndex));
|
||||
List<Plugin> plugins = queryPluginsByIds(currentPagePluginIds);
|
||||
Page<Plugin> aiPluginPage = new Page<>(plugins, pageNumber, pageSize, visiblePluginIds.size());
|
||||
return Result.ok(aiPluginPage);
|
||||
}
|
||||
|
||||
@@ -129,5 +160,37 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<BigInteger> queryCreatorPluginIds(List<BigInteger> pluginIds, Long creatorId) {
|
||||
if (CollectionUtil.isEmpty(pluginIds) || creatorId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
QueryWrapper creatorPluginWrapper = QueryWrapper.create().select(Plugin::getId)
|
||||
.in(Plugin::getId, pluginIds)
|
||||
.eq(Plugin::getCreatedBy, creatorId);
|
||||
return pluginMapper.selectListByQueryAs(creatorPluginWrapper, BigInteger.class);
|
||||
}
|
||||
|
||||
private List<Plugin> queryPluginsByIds(List<BigInteger> pluginIds) {
|
||||
if (CollectionUtil.isEmpty(pluginIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds);
|
||||
List<Plugin> plugins = pluginMapper.selectListByQuery(queryPluginWrapper);
|
||||
Map<BigInteger, Plugin> pluginMap = plugins.stream().collect(Collectors.toMap(
|
||||
Plugin::getId,
|
||||
item -> item,
|
||||
(left, right) -> left,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
List<Plugin> orderedPlugins = new ArrayList<>();
|
||||
for (BigInteger pluginId : pluginIds) {
|
||||
Plugin plugin = pluginMap.get(pluginId);
|
||||
if (plugin != null) {
|
||||
orderedPlugins.add(plugin);
|
||||
}
|
||||
}
|
||||
return orderedPlugins;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class PluginVisibilityServiceImpl implements PluginVisibilityService {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Resource
|
||||
private PluginCategoryMappingMapper pluginCategoryMappingMapper;
|
||||
|
||||
@Override
|
||||
public Set<BigInteger> getCurrentVisiblePluginIds() {
|
||||
RoleCategoryAccessSnapshot snapshot = categoryPermissionService.getCurrentAccess("PLUGIN");
|
||||
if (!snapshot.isRestricted() || CollectionUtil.isEmpty(snapshot.getCategoryIds())) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
QueryWrapper mappingWrapper = QueryWrapper.create()
|
||||
.select(PluginCategoryMapping::getPluginId)
|
||||
.in(PluginCategoryMapping::getCategoryId, snapshot.getCategoryIds());
|
||||
List<BigInteger> pluginIds = pluginCategoryMappingMapper.selectListByQueryAs(mappingWrapper, BigInteger.class);
|
||||
return new LinkedHashSet<>(pluginIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canAccessPlugin(Long createdBy, BigInteger pluginId) {
|
||||
RoleCategoryAccessSnapshot snapshot = categoryPermissionService.getCurrentAccess("PLUGIN");
|
||||
if (!snapshot.isRestricted()) {
|
||||
return true;
|
||||
}
|
||||
if (createdBy != null && snapshot.getAccountId() != null
|
||||
&& snapshot.getAccountId().equals(BigInteger.valueOf(createdBy))) {
|
||||
return true;
|
||||
}
|
||||
if (CollectionUtil.isEmpty(snapshot.getCategoryIds())) {
|
||||
return false;
|
||||
}
|
||||
QueryWrapper mappingWrapper = QueryWrapper.create()
|
||||
.select(PluginCategoryMapping::getPluginId)
|
||||
.eq(PluginCategoryMapping::getPluginId, pluginId)
|
||||
.in(PluginCategoryMapping::getCategoryId, snapshot.getCategoryIds());
|
||||
List<BigInteger> pluginIds = pluginCategoryMappingMapper.selectListByQueryAs(mappingWrapper, BigInteger.class);
|
||||
return CollectionUtil.isNotEmpty(pluginIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertPluginVisible(Long createdBy, BigInteger pluginId, String message) {
|
||||
if (!canAccessPlugin(createdBy, pluginId)) {
|
||||
throw new BusinessException(message == null ? "无权限访问该资源" : message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package tech.easyflow.ai.invoke.mapper;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedChoice;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedContentPart;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedImageUrl;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedMessage;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedResponseFormat;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedTool;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedToolCall;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedToolCallFunction;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedToolFunction;
|
||||
import tech.easyflow.ai.invoke.model.UnifiedUsage;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
|
||||
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class OpenAiProtocolMapperTest {
|
||||
|
||||
private final OpenAiProtocolMapper mapper = new OpenAiProtocolMapper(new ObjectMapper());
|
||||
|
||||
@Test
|
||||
public void shouldParseTextAndImageRequest() {
|
||||
String rawBody = """
|
||||
{
|
||||
"model": "gpt-4-1-prod",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "帮我看下这张图"
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "data:image/png;base64,AAAA",
|
||||
"detail": "high"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stream": false,
|
||||
"temperature": 0.2,
|
||||
"top_p": 0.8,
|
||||
"max_tokens": 512,
|
||||
"seed": 7,
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "query_weather",
|
||||
"description": "query weather",
|
||||
"parameters": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "weather_schema"
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
OpenAiChatCompletionRequest request = mapper.readRequest(rawBody);
|
||||
UnifiedChatRequest unifiedRequest = mapper.toUnifiedRequest(request);
|
||||
|
||||
Assert.assertEquals("gpt-4-1-prod", unifiedRequest.getModel());
|
||||
Assert.assertEquals(Long.valueOf(7), unifiedRequest.getSeed());
|
||||
Assert.assertEquals(Integer.valueOf(512), unifiedRequest.getMaxTokens());
|
||||
Assert.assertNotNull(unifiedRequest.getTools());
|
||||
Assert.assertEquals(1, unifiedRequest.getTools().size());
|
||||
Assert.assertEquals("query_weather", unifiedRequest.getTools().get(0).getFunction().getName());
|
||||
Assert.assertEquals("json_schema", unifiedRequest.getResponseFormat().getType());
|
||||
Assert.assertEquals("weather_schema", unifiedRequest.getResponseFormat().getJsonSchema().get("name").asText());
|
||||
|
||||
UnifiedMessage message = unifiedRequest.getMessages().get(0);
|
||||
Assert.assertEquals("user", message.getRole());
|
||||
Assert.assertNotNull(message.getContentParts());
|
||||
Assert.assertEquals(2, message.getContentParts().size());
|
||||
UnifiedContentPart imagePart = message.getContentParts().get(1);
|
||||
Assert.assertEquals("image_url", imagePart.getType());
|
||||
Assert.assertEquals("data:image/png;base64,AAAA", imagePart.getImageUrl().getUrl());
|
||||
Assert.assertEquals("high", imagePart.getImageUrl().getDetail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRejectUnsupportedRootField() {
|
||||
String rawBody = """
|
||||
{
|
||||
"model": "gpt-4-1-prod",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "hello"
|
||||
}
|
||||
],
|
||||
"n": 2
|
||||
}
|
||||
""";
|
||||
|
||||
ModelInvokeException exception = Assert.assertThrows(
|
||||
ModelInvokeException.class,
|
||||
() -> mapper.readRequest(rawBody)
|
||||
);
|
||||
|
||||
Assert.assertEquals(400, exception.getStatus());
|
||||
Assert.assertEquals("unsupported_field", exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowMissingOptionalFields() {
|
||||
String rawBody = """
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,介绍一下你自己。"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
OpenAiChatCompletionRequest request = mapper.readRequest(rawBody);
|
||||
UnifiedChatRequest unifiedRequest = mapper.toUnifiedRequest(request);
|
||||
|
||||
Assert.assertEquals("deepseek-chat", unifiedRequest.getModel());
|
||||
Assert.assertNotNull(unifiedRequest.getMessages());
|
||||
Assert.assertEquals(1, unifiedRequest.getMessages().size());
|
||||
Assert.assertNull(unifiedRequest.getTools());
|
||||
Assert.assertNull(unifiedRequest.getToolChoice());
|
||||
Assert.assertNull(unifiedRequest.getResponseFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMapToolCallsAndUsageInResponse() {
|
||||
UnifiedChatResponse response = new UnifiedChatResponse();
|
||||
response.setId("chatcmpl-1");
|
||||
response.setObject("chat.completion");
|
||||
response.setCreated(123L);
|
||||
response.setModel("gpt-4-1-prod");
|
||||
|
||||
UnifiedToolCallFunction toolCallFunction = new UnifiedToolCallFunction();
|
||||
toolCallFunction.setName("query_weather");
|
||||
toolCallFunction.setArguments("{\"city\":\"shanghai\"}");
|
||||
|
||||
UnifiedToolCall toolCall = new UnifiedToolCall();
|
||||
toolCall.setId("call_1");
|
||||
toolCall.setType("function");
|
||||
toolCall.setFunction(toolCallFunction);
|
||||
|
||||
UnifiedMessage message = new UnifiedMessage();
|
||||
message.setRole("assistant");
|
||||
message.setContent(null);
|
||||
message.setToolCalls(Collections.singletonList(toolCall));
|
||||
|
||||
UnifiedChoice choice = new UnifiedChoice();
|
||||
choice.setIndex(0);
|
||||
choice.setMessage(message);
|
||||
choice.setFinishReason("tool_calls");
|
||||
|
||||
UnifiedUsage usage = new UnifiedUsage();
|
||||
usage.setPromptTokens(12);
|
||||
usage.setCompletionTokens(34);
|
||||
usage.setTotalTokens(46);
|
||||
|
||||
response.setChoices(List.of(choice));
|
||||
response.setUsage(usage);
|
||||
|
||||
OpenAiChatCompletionResponse openAiResponse = mapper.toOpenAiResponse(response);
|
||||
|
||||
Assert.assertEquals("chatcmpl-1", openAiResponse.getId());
|
||||
Assert.assertEquals("tool_calls", openAiResponse.getChoices().get(0).getFinishReason());
|
||||
Assert.assertEquals("query_weather", openAiResponse.getChoices().get(0).getMessage().getToolCalls().get(0).getFunction().getName());
|
||||
Assert.assertEquals(Integer.valueOf(46), openAiResponse.getUsage().getTotalTokens());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user