feat: 增加多模型配置界面,AI服务独立剥离

This commit is contained in:
qiufeng
2026-07-13 23:17:48 +08:00
parent 35252b56af
commit 6c46f026ac
101 changed files with 2272 additions and 1080 deletions
+1
View File
@@ -71,3 +71,4 @@
/mf-common/mf-common-prometheus/target/
/mf-common/mf-common-demo/target/
/mf-api/mf-demo-api/target/
/mf-api/mf-ai-api/target/
+1
View File
@@ -209,6 +209,7 @@
| `mf_nocode.sql` | 低代码中心数据库 |
| `mf_workflow.sql` | 工作流数据库 |
| `mf_demo.sql` | 样例中心数据库(非必须) |
| `mf_ai.sql` | AI中心数据库 |
| `mfish_nocode.sql` | 单实例数据库<br />`单实例启动只需要执行这个脚本` |
```
+1
View File
@@ -12,6 +12,7 @@ postgresql数据库已完全适配,有需要请联系作者...
| `mf_nocode.sql` | 低代码中心数据库 |
| `mf_workflow.sql` | 工作流数据库 |
| `mf_demo.sql` | 样例中心数据库(非必须) |
| `mf_ai.sql` | AI中心数据库 |
| `mfish_nocode.sql` | 单实例数据库<br />`单实例启动只需要执行这个脚本` |
```
+36
View File
@@ -0,0 +1,36 @@
-- ----------------------------
-- AI中心数据库 (摸鱼小助手 AI相关配置库)
-- ----------------------------
DROP DATABASE IF EXISTS `mf_ai`;
CREATE DATABASE `mf_ai` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
USE `mf_ai`;
-- ----------------------------
-- Table structure for ai_model_config
-- ----------------------------
DROP TABLE IF EXISTS `ai_model_config`;
CREATE TABLE `ai_model_config` (
`id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`tenant_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '租户ID',
`provider` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '提供者: openai/ollama/deepseek/zhipuai/anthropic',
`model_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模型名称: gpt-4o, qwen3:8b, deepseek-v3 等',
`api_key` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'API密钥(加密存储)',
`base_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'API基础地址',
`max_tokens` int NULL DEFAULT 4096 COMMENT '最大token数',
`temperature` double NULL DEFAULT 0.7 COMMENT '温度参数',
`top_p` double NULL DEFAULT NULL COMMENT 'top_p参数',
`completions_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '补全项路径',
`enabled` tinyint NULL DEFAULT 1 COMMENT '是否启用 1启用 0禁用',
`sort_order` int NULL DEFAULT 0 COMMENT '排序(决定fallback优先级)',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI模型配置信息' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
+13 -13
View File
@@ -31,33 +31,33 @@ CREATE TABLE `config_info` (
`encrypted_data_key` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin NOT NULL DEFAULT '' COMMENT '密钥',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_configinfo_datagrouptenant`(`data_id`, `group_id`, `tenant_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_bin COMMENT = 'config_info' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_bin COMMENT = 'config_info' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of config_info
-- ----------------------------
INSERT INTO `config_info` VALUES (1, 'application-dev.yml', 'DEFAULT_GROUP', 'spring:\n autoconfigure:\n exclude:\n - org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration\n data:\n redis:\n host: localhost\n port: 6379\n password: \"1qaz@WSX\"\n database: 5\n timeout: 10s\n lettuce:\n pool:\n # 连接池中的最小空闲连接\n min-idle: 0\n # 连接池中的最大空闲连接\n max-idle: 8\n # 连接池的最大数据库连接数\n max-active: 8\n # #连接池最大阻塞等待时间(使用负值表示没有限制)\n max-wait: -1ms\n servlet:\n multipart:\n # 单个文件大小\n max-file-size: 1200MB\n # 设置总上传的文件大小\n max-request-size: 1200MB\n ai:\n openai:\n api-key: sk-or-v1-b4**************************************a99b135dae3b\n base-url: https://openrouter.ai/api\n chat:\n options:\n model: meta-llama/llama-3.3-70b-instruct:free\n max-tokens: 1000\n temperature: 0.7\n ollama:\n base-url: http://localhost:11434\n chat:\n model: qwen3:8b\n# feign 配置\nfeign:\n sentinel:\n enabled: true\n okhttp: \n enabled: true\n httpclient:\n enabled: false\n client:\n config:\n default:\n connectTimeout: 10000\n readTimeout: 10000\n compression:\n request:\n enabled: false\n response:\n enabled: false\n\n# 暴露监控端点\nmanagement:\n health:\n sentinel:\n # 关闭健康检查,避免日志刷屏\n enabled: false\n endpoints:\n web:\n exposure:\n include: \n - info\n - health\n - metrics\n - prometheus\n endpoint:\n health:\n show-details: always\nlogging:\n config: classpath:logback-logstash.xml\nlog:\n level: info\n maxSize: 30MB\n# 临时缓存存储时间(单位:天)\ncache:\n temp:\n time: 7\nDBConnect:\n password:\n privateKey: 54f0e36b98cf63c3bc6185b61c6e4f2a6e1df3bdeb293a50d7b0bc66881c2419\nswagger:\n title: \'\'\n enabled: true\n description: 摸鱼框架接口文档\n license: Powered By mfish\n licenseUrl: http://www.mfish.com.cn\n version: 版本号:V2.1.1\n termsOfServiceUrl: http://www.mfish.com.cn\n ignoreServers: mf-gateway,mf-monitor\n contact:\n name: mfish\n email: qiufeng9862@qq.com\n url: http://www.mfish.com.cn\nspringdoc:\n swagger-ui:\n enabled: true\n path: /swagger-ui.html\n tags-sorter: alpha\n operations-sorter: alpha\n default-models-expand-depth: -1\n docExpansion: none\n api-docs:\n enabled: true\n path: /v3/api-docs\n use-management-port: false\n default-flat-param-object: true', '832d798b554120710e6615813bf9d699', '2026-02-08 10:36:25', '2026-02-08 10:36:47', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (2, 'application-dev.yml', 'DEFAULT_GROUP', 'spring:\n autoconfigure:\n exclude:\n - org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration\n data:\n redis:\n host: localhost\n port: 6379\n password: \"1qaz@WSX\"\n database: 5\n timeout: 10s\n lettuce:\n pool:\n # 连接池中的最小空闲连接\n min-idle: 0\n # 连接池中的最大空闲连接\n max-idle: 8\n # 连接池的最大数据库连接数\n max-active: 8\n # #连接池最大阻塞等待时间(使用负值表示没有限制)\n max-wait: -1ms\n servlet:\n multipart:\n # 单个文件大小\n max-file-size: 1200MB\n # 设置总上传的文件大小\n max-request-size: 1200MB\n ai:\n openai:\n api-key: sk-or-v1-b4**************************************a99b135dae3b\n base-url: https://openrouter.ai/api\n chat:\n options:\n model: meta-llama/llama-3.3-70b-instruct:free\n max-tokens: 1000\n temperature: 0.7\n ollama:\n base-url: http://localhost:11434\n chat:\n model: qwen3:8b\n# feign 配置\nfeign:\n sentinel:\n enabled: true\n okhttp: \n enabled: true\n httpclient:\n enabled: false\n client:\n config:\n default:\n connectTimeout: 10000\n readTimeout: 10000\n compression:\n request:\n enabled: false\n response:\n enabled: false\n\n# 暴露监控端点\nmanagement:\n health:\n sentinel:\n # 关闭健康检查,避免日志刷屏\n enabled: false\n endpoints:\n web:\n exposure:\n include: \n - info\n - health\n - metrics\n - prometheus\n endpoint:\n health:\n show-details: always\nlogging:\n config: classpath:logback-logstash.xml\nlog:\n level: info\n maxSize: 30MB\n# 临时缓存存储时间(单位:天)\ncache:\n temp:\n time: 7\nDBConnect:\n password:\n privateKey: 54f0e36b98cf63c3bc6185b61c6e4f2a6e1df3bdeb293a50d7b0bc66881c2419\nswagger:\n title: \'\'\n enabled: true\n description: 摸鱼框架接口文档\n license: Powered By mfish\n licenseUrl: http://www.mfish.com.cn\n version: 版本号:V2.1.1\n termsOfServiceUrl: http://www.mfish.com.cn\n ignoreServers: mf-gateway,mf-monitor\n contact:\n name: mfish\n email: qiufeng9862@qq.com\n url: http://www.mfish.com.cn\nspringdoc:\n swagger-ui:\n enabled: true\n path: /swagger-ui.html\n tags-sorter: alpha\n operations-sorter: alpha\n default-models-expand-depth: -1\n docExpansion: none\n api-docs:\n enabled: true\n path: /v3/api-docs\n use-management-port: false\n default-flat-param-object: true', '832d798b554120710e6615813bf9d699', '2026-02-08 10:36:25', '2026-02-08 10:36:47', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (3, 'mf-oauth-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'\'\r\n prefix: /oauth2\r\n# mybatis配置\r\nmybatis-plus:\r\n # 搜索指定包别名\r\n type-aliases-package: cn.com.mfish.oauth.mapper\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/**/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n thymeleaf:\r\n cache: false\r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_oauth?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\noauth2:\r\n expire:\r\n code: 180\r\n token: 21600\r\n refreshToken: 604800\r\n login:\r\n mutex: false\r\n user:\r\n autoCreate: false\r\n token:\r\n sm4key: 143be1ae6ee10b048f7e441cec2a9803\r\nwechat:\r\n miniapp:\r\n appId: wx*************e34a3\r\n secret: a65e*************fa222\r\ngitee:\r\n clientId: a83ea66522fef5f382c6aaffa875c7e87b21d3933277c44a326cd3407fc0b651\r\n clientSecret: 3ebdce2afcb4a7df9b16355f60d898d452f4d9cf8ad1ab8279bb3bb465e614db\r\n redirectUri: http://localhost:5186/giteeOauth2\r\n bindUri: http://localhost:5186/tenant/info/1?callback=gitee\r\ngithub:\r\n clientId: Ov23li7oH0Djw3ZEJCoJ\r\n clientSecret: ada9a510b3bde542e7510142f07d32a5f6bed131\r\n redirectUri: http://localhost:5186/githubOauth2\r\n bindUri: http://localhost:5186/tenant/info/1?callback=github', '34d1ab06f05a4c251d9cb4ce7547badc', '2026-02-08 10:37:25', '2026-02-08 10:37:25', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (4, 'mf-oauth-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'\'\r\n prefix: /oauth2\r\n# mybatis配置\r\nmybatis-plus:\r\n # 搜索指定包别名\r\n type-aliases-package: cn.com.mfish.oauth.mapper\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/**/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n thymeleaf:\r\n cache: false\r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_oauth?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\noauth2:\r\n expire:\r\n code: 180\r\n token: 21600\r\n refreshToken: 604800\r\n login:\r\n mutex: false\r\n user:\r\n autoCreate: false\r\n token:\r\n sm4key: 143be1ae6ee10b048f7e441cec2a9803\r\nwechat:\r\n miniapp:\r\n appId: wx*************e34a3\r\n secret: a65e*************fa222\r\ngitee:\r\n clientId: a83ea66522fef5f382c6aaffa875c7e87b21d3933277c44a326cd3407fc0b651\r\n clientSecret: 3ebdce2afcb4a7df9b16355f60d898d452f4d9cf8ad1ab8279bb3bb465e614db\r\n redirectUri: http://localhost:5186/giteeOauth2\r\n bindUri: http://localhost:5186/tenant/info/1?callback=gitee\r\ngithub:\r\n clientId: Ov23li7oH0Djw3ZEJCoJ\r\n clientSecret: ada9a510b3bde542e7510142f07d32a5f6bed131\r\n redirectUri: http://localhost:5186/githubOauth2\r\n bindUri: http://localhost:5186/tenant/info/1?callback=github', '34d1ab06f05a4c251d9cb4ce7547badc', '2026-02-08 10:37:25', '2026-02-08 10:37:25', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (1, 'application-dev.yml', 'DEFAULT_GROUP', 'spring:\n autoconfigure:\n exclude:\n - org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration\n - org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration\n data:\n redis:\n host: localhost\n port: 6379\n password: \"1qaz@WSX\"\n database: 5\n timeout: 10s\n lettuce:\n pool:\n # 连接池中的最小空闲连接\n min-idle: 0\n # 连接池中的最大空闲连接\n max-idle: 8\n # 连接池的最大数据库连接数\n max-active: 8\n # #连接池最大阻塞等待时间(使用负值表示没有限制)\n max-wait: -1ms\n servlet:\n multipart:\n # 单个文件大小\n max-file-size: 1200MB\n # 设置总上传的文件大小\n max-request-size: 1200MB\n# feign 配置\nfeign:\n sentinel:\n enabled: true\n okhttp: \n enabled: true\n httpclient:\n enabled: false\n client:\n config:\n default:\n connectTimeout: 10000\n readTimeout: 10000\n compression:\n request:\n enabled: false\n response:\n enabled: false\n\n# 暴露监控端点\nmanagement:\n metrics:\n enable:\n all: false\n health:\n sentinel:\n # 关闭健康检查,避免日志刷屏\n enabled: false\n endpoints:\n web:\n exposure:\n include: \n - info\n - health\n - metrics\n - prometheus\n endpoint:\n health:\n show-details: always\nlogging:\n config: classpath:logback-logstash.xml\nlog:\n level: info\n maxSize: 30MB\n# 临时缓存存储时间(单位:天)\ncache:\n temp:\n time: 7\nDBConnect:\n password:\n privateKey: 54f0e36b98cf63c3bc6185b61c6e4f2a6e1df3bdeb293a50d7b0bc66881c2419\nswagger:\n title: \'\'\n enabled: true\n description: 摸鱼框架接口文档\n license: Powered By mfish\n licenseUrl: http://www.mfish.com.cn\n version: 版本号:V2.1.1\n termsOfServiceUrl: http://www.mfish.com.cn\n ignoreServers: mf-gateway,mf-monitor\n contact:\n name: mfish\n email: qiufeng9862@qq.com\n url: http://www.mfish.com.cn\nspringdoc:\n swagger-ui:\n enabled: true\n path: /swagger-ui.html\n tags-sorter: alpha\n operations-sorter: alpha\n default-models-expand-depth: -1\n docExpansion: none\n api-docs:\n enabled: true\n path: /v3/api-docs\n use-management-port: false\n default-flat-param-object: true', '4027bdc69421ab032637384f95ae1f2c', '2026-02-08 10:36:25', '2026-07-09 23:21:21', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (2, 'application-dev.yml', 'DEFAULT_GROUP', 'spring:\n autoconfigure:\n exclude:\n - org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration\n - org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration\n data:\n redis:\n host: localhost\n port: 6379\n password: \"1qaz@WSX\"\n database: 5\n timeout: 10s\n lettuce:\n pool:\n # 连接池中的最小空闲连接\n min-idle: 0\n # 连接池中的最大空闲连接\n max-idle: 8\n # 连接池的最大数据库连接数\n max-active: 8\n # #连接池最大阻塞等待时间(使用负值表示没有限制)\n max-wait: -1ms\n servlet:\n multipart:\n # 单个文件大小\n max-file-size: 1200MB\n # 设置总上传的文件大小\n max-request-size: 1200MB\n# feign 配置\nfeign:\n sentinel:\n enabled: true\n okhttp: \n enabled: true\n httpclient:\n enabled: false\n client:\n config:\n default:\n connectTimeout: 10000\n readTimeout: 10000\n compression:\n request:\n enabled: false\n response:\n enabled: false\n\n# 暴露监控端点\nmanagement:\n metrics:\n enable:\n all: false\n health:\n sentinel:\n # 关闭健康检查,避免日志刷屏\n enabled: false\n endpoints:\n web:\n exposure:\n include: \n - info\n - health\n - metrics\n - prometheus\n endpoint:\n health:\n show-details: always\nlogging:\n config: classpath:logback-logstash.xml\nlog:\n level: info\n maxSize: 30MB\n# 临时缓存存储时间(单位:天)\ncache:\n temp:\n time: 7\nDBConnect:\n password:\n privateKey: 54f0e36b98cf63c3bc6185b61c6e4f2a6e1df3bdeb293a50d7b0bc66881c2419\nswagger:\n title: \'\'\n enabled: true\n description: 摸鱼框架接口文档\n license: Powered By mfish\n licenseUrl: http://www.mfish.com.cn\n version: 版本号:V2.1.1\n termsOfServiceUrl: http://www.mfish.com.cn\n ignoreServers: mf-gateway,mf-monitor\n contact:\n name: mfish\n email: qiufeng9862@qq.com\n url: http://www.mfish.com.cn\nspringdoc:\n swagger-ui:\n enabled: true\n path: /swagger-ui.html\n tags-sorter: alpha\n operations-sorter: alpha\n default-models-expand-depth: -1\n docExpansion: none\n api-docs:\n enabled: true\n path: /v3/api-docs\n use-management-port: false\n default-flat-param-object: true', '4027bdc69421ab032637384f95ae1f2c', '2026-02-08 10:36:25', '2026-07-09 23:21:21', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (3, 'mf-oauth-dev.yml', 'DEFAULT_GROUP', 'swagger:\n title: \'\'\n prefix: /oauth2\n# mybatis配置\nmybatis-plus:\n # 搜索指定包别名\n type-aliases-package: cn.com.mfish.oauth.mapper\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapper-locations: classpath*:cn/com/mfish/**/mapper/**/*Mapper.xml\n global-config:\n # 关闭MP3.0自带的banner\n banner: false\n db-config:\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\n #id-type: 0\n # 默认数据库表下划线命名\n table-underline: true\n #逻辑删除 删除前\n logic-not-delete-value: 0\n #逻辑删除 删除后\n logic-delete-value: 1\n configuration:\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n # 返回类型为Map,显示null对应的字段\n call-setters-on-nulls: true\n map-underscore-to-camel-case: true\n auto-mapping-behavior: full\n jdbc-type-for-null:\nspring: \n thymeleaf:\n cache: false\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://localhost:3306/mf_oauth?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: \n # url: \n # driver-class-name: \n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\noauth2:\n expire:\n code: 180\n token: 21600\n refreshToken: 604800\n login:\n mutex: false\n user:\n autoCreate: false\n token:\n sm4key: 143be1ae6ee10b048f7e441cec2a9803\nwechat:\n miniapp:\n appId: wxf7****************ee34a3\n secret: 7908**************************4a5f7d4e1\ngitee:\n clientId: a83ea665************************************6cd3407fc0b651\n clientSecret: 3ebdce2***************************************79bb3bb465e614db\n redirectUri: http://localhost:5186/giteeOauth2\n bindUri: http://localhost:5186/tenant/info/1?callback=gitee\ngithub:\n clientId: Ov2****************JCoJ\n clientSecret: ada9a***********************6bed131\n redirectUri: http://localhost:5186/githubOauth2\n bindUri: http://localhost:5186/tenant/info/1?callback=github', '748cf0dc25228959f8d09df8e56a85c1', '2026-02-08 10:37:25', '2026-07-10 09:07:14', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (4, 'mf-oauth-dev.yml', 'DEFAULT_GROUP', 'swagger:\n title: \'\'\n prefix: /oauth2\n# mybatis配置\nmybatis-plus:\n # 搜索指定包别名\n type-aliases-package: cn.com.mfish.oauth.mapper\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapper-locations: classpath*:cn/com/mfish/**/mapper/**/*Mapper.xml\n global-config:\n # 关闭MP3.0自带的banner\n banner: false\n db-config:\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\n #id-type: 0\n # 默认数据库表下划线命名\n table-underline: true\n #逻辑删除 删除前\n logic-not-delete-value: 0\n #逻辑删除 删除后\n logic-delete-value: 1\n configuration:\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n # 返回类型为Map,显示null对应的字段\n call-setters-on-nulls: true\n map-underscore-to-camel-case: true\n auto-mapping-behavior: full\n jdbc-type-for-null:\nspring: \n thymeleaf:\n cache: false\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://localhost:3306/mf_oauth?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: \n # url: \n # driver-class-name: \n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\noauth2:\n expire:\n code: 180\n token: 21600\n refreshToken: 604800\n login:\n mutex: false\n user:\n autoCreate: false\n token:\n sm4key: 143be1ae6ee10b048f7e441cec2a9803\nwechat:\n miniapp:\n appId: wxf7****************ee34a3\n secret: 7908**************************4a5f7d4e1\ngitee:\n clientId: a83ea665************************************6cd3407fc0b651\n clientSecret: 3ebdce2***************************************79bb3bb465e614db\n redirectUri: http://localhost:5186/giteeOauth2\n bindUri: http://localhost:5186/tenant/info/1?callback=gitee\ngithub:\n clientId: Ov2****************JCoJ\n clientSecret: ada9a***********************6bed131\n redirectUri: http://localhost:5186/githubOauth2\n bindUri: http://localhost:5186/tenant/info/1?callback=github', '748cf0dc25228959f8d09df8e56a85c1', '2026-02-08 10:37:25', '2026-07-10 09:07:14', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (5, 'mf-sys-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'\'\r\n prefix: /sys\r\n# mybatis配置\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/sys/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_system?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\ncode:\r\n template:\r\n keys:\r\n - src/main/java/${packageName}/controller/${entityName}Controller.java.ftl\r\n - src/main/java/${packageName}/entity/${entityName}.java.ftl\r\n - src/main/java/${packageName}/mapper/${entityName}Mapper.java.ftl\r\n - src/main/java/${packageName}/mapper/xml/${entityName}Mapper.xml.ftl\r\n - src/main/java/${packageName}/req/Req${entityName}.java.ftl\r\n - src/main/java/${packageName}/service/${entityName}Service.java.ftl\r\n - src/main/java/${packageName}/service/impl/${entityName}ServiceImpl.java.ftl\r\n - mfish-nocode-view/src/views/${apiPrefix}/${entityName#kebab-case}/${entityName#uncap_first}.data.ts.ftl\r\n - mfish-nocode-view/src/views/${apiPrefix}/${entityName#kebab-case}/${entityName}Modal.vue.ftl\r\n - mfish-nocode-view/src/views/${apiPrefix}/${entityName#kebab-case}/${entityName}ViewModal.vue.ftl\r\n - mfish-nocode-view/src/views/${apiPrefix}/${entityName#kebab-case}/index.vue.ftl\r\n - mfish-nocode-view/src/api/${apiPrefix}/model/${entityName}Model.ts.ftl\r\n - mfish-nocode-view/src/api/${apiPrefix}/${entityName}.ts.ftl\r\n path: template\r\n savePath:\r\n #前端地址\r\n front: E:/webcode\r\n #后端地址(需要指定到具体模块路径)\r\n back: E:/javacode/mfish-nocode-pro/mf-business/mf-sys', '03eedd7cd6a21c36288215e8c4ee9c97', '2026-02-08 10:37:54', '2026-02-08 10:37:54', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (6, 'mf-sys-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'\'\r\n prefix: /sys\r\n# mybatis配置\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/sys/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_system?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\ncode:\r\n template:\r\n keys:\r\n - src/main/java/${packageName}/controller/${entityName}Controller.java.ftl\r\n - src/main/java/${packageName}/entity/${entityName}.java.ftl\r\n - src/main/java/${packageName}/mapper/${entityName}Mapper.java.ftl\r\n - src/main/java/${packageName}/mapper/xml/${entityName}Mapper.xml.ftl\r\n - src/main/java/${packageName}/req/Req${entityName}.java.ftl\r\n - src/main/java/${packageName}/service/${entityName}Service.java.ftl\r\n - src/main/java/${packageName}/service/impl/${entityName}ServiceImpl.java.ftl\r\n - mfish-nocode-view/src/views/${apiPrefix}/${entityName#kebab-case}/${entityName#uncap_first}.data.ts.ftl\r\n - mfish-nocode-view/src/views/${apiPrefix}/${entityName#kebab-case}/${entityName}Modal.vue.ftl\r\n - mfish-nocode-view/src/views/${apiPrefix}/${entityName#kebab-case}/${entityName}ViewModal.vue.ftl\r\n - mfish-nocode-view/src/views/${apiPrefix}/${entityName#kebab-case}/index.vue.ftl\r\n - mfish-nocode-view/src/api/${apiPrefix}/model/${entityName}Model.ts.ftl\r\n - mfish-nocode-view/src/api/${apiPrefix}/${entityName}.ts.ftl\r\n path: template\r\n savePath:\r\n #前端地址\r\n front: E:/webcode\r\n #后端地址(需要指定到具体模块路径)\r\n back: E:/javacode/mfish-nocode-pro/mf-business/mf-sys', '03eedd7cd6a21c36288215e8c4ee9c97', '2026-02-08 10:37:54', '2026-02-08 10:37:54', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (7, 'mf-storage-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'\'\r\n prefix: /storage\r\n# mybatis配置\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/storage/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_system?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\nstorage:\r\n active: local\r\n address: http://localhost:8888/storage/file/\r\n local:\r\n storagePath: E:/storage\r\n aliyun:\r\n endpoint: oss-cn-hangzhou.aliyuncs.com\r\n accessKeyId: LTAI5***********FsgT\r\n accessKeySecret: ZtZGF***************JADjS2f\r\n bucketName: landwise\r\n qiniu:\r\n domain: http://cdn.mfish.com.cn\r\n accessKey: cHlX4*******************cdsexgvP\r\n secretKey: QPACH*******************1vEa1t4S\r\n bucket: mfish-nocode', '5d34bf317473dbb57abe28db773a7834', '2026-02-08 10:38:15', '2026-02-08 10:38:15', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (8, 'mf-storage-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'\'\r\n prefix: /storage\r\n# mybatis配置\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/storage/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_system?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\nstorage:\r\n active: local\r\n address: http://localhost:8888/storage/file/\r\n local:\r\n storagePath: E:/storage\r\n aliyun:\r\n endpoint: oss-cn-hangzhou.aliyuncs.com\r\n accessKeyId: LTAI5***********FsgT\r\n accessKeySecret: ZtZGF***************JADjS2f\r\n bucketName: landwise\r\n qiniu:\r\n domain: http://cdn.mfish.com.cn\r\n accessKey: cHlX4*******************cdsexgvP\r\n secretKey: QPACH*******************1vEa1t4S\r\n bucket: mfish-nocode', '5d34bf317473dbb57abe28db773a7834', '2026-02-08 10:38:15', '2026-02-08 10:38:15', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (9, 'mf-gateway-dev.yml', 'DEFAULT_GROUP', 'spring:\n cloud:\n gateway:\n server:\n webflux:\n globalcors:\n cors-configurations:\n \'[/**]\':\n max-age: 3600\n allowed-origin-patterns: \"*\"\n allowed-headers: \"*\"\n allow-credentials: true\n allowed-methods:\n - GET\n - POST\n - DELETE\n - PUT\n - OPTION \n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: oauth2\n uri: lb://mf-oauth\n predicates:\n - Path=/oauth2/**\n filters:\n - CacheFilter\n - StripPrefix=1\n # 测试中心\n - id: test\n uri: lb://mf-test\n predicates:\n - Path=/test/**\n filters:\n - StripPrefix=1\n - id: sys\n uri: lb://mf-sys\n predicates:\n - Path=/sys/**\n filters:\n - StripPrefix=1\n - id: demo\n uri: lb://mf-demo\n predicates:\n - Path=/demo/**\n filters:\n - StripPrefix=1\n - id: storage\n uri: lb://mf-storage\n predicates:\n - Path=/storage/**\n filters:\n - StripPrefix=1\n - id: scheduler\n uri: lb://mf-scheduler\n predicates:\n - Path=/scheduler/**\n filters:\n - StripPrefix=1\n - id: nocode\n uri: lb://mf-nocode\n predicates:\n - Path=/nocode/**\n filters:\n - StripPrefix=1\n - id: workflow\n uri: lb://mf-workflow\n predicates:\n - Path=/workflow/**\n filters:\n - StripPrefix=1\n - id: ai\n uri: lb://mf-ai\n predicates:\n - Path=/ai/**\n filters:\n - StripPrefix=1\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n # 不校验白名单\n ignore:\n whites:\n - /oauth2/user/revoke\n - /oauth2/accessToken\n - /oauth2/authorize\n - /oauth2/sendMsg\n - /oauth2/qrCodeLogin/*\n - /oauth2/wx/bind/**\n - /oauth2/static/**\n - /oauth2/gitee/token/**\n - /oauth2/gitee/url\n - /oauth2/github/token/**\n - /oauth2/github/url\n - /storage/file/*.*\n - /code/**\n - /test/**\n - /*/v3/api-docs\n# swagger configuration\nspringdoc: \n webjars: \n # 设置为空,不要前缀,不配置这个就需要多加一层路径`/webjars`->`http:host:网关ip/webjars/swagger-ui/index.html`\n prefix:', 'be9f7df0750e8e41f064fe2896e8d13a', '2026-02-08 10:38:39', '2026-05-15 23:50:51', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (10, 'mf-gateway-dev.yml', 'DEFAULT_GROUP', 'spring:\n cloud:\n gateway:\n server:\n webflux:\n globalcors:\n cors-configurations:\n \'[/**]\':\n max-age: 3600\n allowed-origin-patterns: \"*\"\n allowed-headers: \"*\"\n allow-credentials: true\n allowed-methods:\n - GET\n - POST\n - DELETE\n - PUT\n - OPTION \n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: oauth2\n uri: lb://mf-oauth\n predicates:\n - Path=/oauth2/**\n filters:\n - CacheFilter\n - StripPrefix=1\n # 测试中心\n - id: test\n uri: lb://mf-test\n predicates:\n - Path=/test/**\n filters:\n - StripPrefix=1\n - id: sys\n uri: lb://mf-sys\n predicates:\n - Path=/sys/**\n filters:\n - StripPrefix=1\n - id: demo\n uri: lb://mf-demo\n predicates:\n - Path=/demo/**\n filters:\n - StripPrefix=1\n - id: storage\n uri: lb://mf-storage\n predicates:\n - Path=/storage/**\n filters:\n - StripPrefix=1\n - id: scheduler\n uri: lb://mf-scheduler\n predicates:\n - Path=/scheduler/**\n filters:\n - StripPrefix=1\n - id: nocode\n uri: lb://mf-nocode\n predicates:\n - Path=/nocode/**\n filters:\n - StripPrefix=1\n - id: workflow\n uri: lb://mf-workflow\n predicates:\n - Path=/workflow/**\n filters:\n - StripPrefix=1\n - id: ai\n uri: lb://mf-ai\n predicates:\n - Path=/ai/**\n filters:\n - StripPrefix=1\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n # 不校验白名单\n ignore:\n whites:\n - /oauth2/user/revoke\n - /oauth2/accessToken\n - /oauth2/authorize\n - /oauth2/sendMsg\n - /oauth2/qrCodeLogin/*\n - /oauth2/wx/bind/**\n - /oauth2/static/**\n - /oauth2/gitee/token/**\n - /oauth2/gitee/url\n - /oauth2/github/token/**\n - /oauth2/github/url\n - /storage/file/*.*\n - /code/**\n - /test/**\n - /*/v3/api-docs\n# swagger configuration\nspringdoc: \n webjars: \n # 设置为空,不要前缀,不配置这个就需要多加一层路径`/webjars`->`http:host:网关ip/webjars/swagger-ui/index.html`\n prefix:', 'be9f7df0750e8e41f064fe2896e8d13a', '2026-02-08 10:38:39', '2026-05-15 23:50:51', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (7, 'mf-storage-dev.yml', 'DEFAULT_GROUP', 'swagger:\n title: \'\'\n prefix: /storage\n# mybatis配置\nmybatis-plus:\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapper-locations: classpath*:cn/com/mfish/storage/mapper/**/*Mapper.xml\n global-config:\n # 关闭MP3.0自带的banner\n banner: false\n db-config:\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\n #id-type: 0\n # 默认数据库表下划线命名\n table-underline: true\n #逻辑删除 删除前\n logic-not-delete-value: 0\n #逻辑删除 删除后\n logic-delete-value: 1\n configuration:\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n # 返回类型为Map,显示null对应的字段\n call-setters-on-nulls: true\n map-underscore-to-camel-case: true\n auto-mapping-behavior: full\n jdbc-type-for-null:\nspring: \n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://localhost:3306/mf_system?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: \n # url: \n # driver-class-name: \n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\nstorage:\n active: local\n address: http://localhost:8888/storage/file/\n local:\n storagePath: E:/storage\n aliyun:\n endpoint: oss-cn-hangzhou.aliyuncs.com\n accessKeyId: LTAI5***********FsgT\n accessKeySecret: ZtZGF***************JADjS2f\n bucketName: mfish-nocode\n qiniu:\n domain: http://cdn.mfish.com.cn\n accessKey: cHlX4*******************cdsexgvP\n secretKey: QPACH*******************1vEa1t4S\n bucket: mfish-nocode', '99b5e4f41980a831496b58ce36650b78', '2026-02-08 10:38:15', '2026-07-10 09:12:49', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (8, 'mf-storage-dev.yml', 'DEFAULT_GROUP', 'swagger:\n title: \'\'\n prefix: /storage\n# mybatis配置\nmybatis-plus:\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapper-locations: classpath*:cn/com/mfish/storage/mapper/**/*Mapper.xml\n global-config:\n # 关闭MP3.0自带的banner\n banner: false\n db-config:\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\n #id-type: 0\n # 默认数据库表下划线命名\n table-underline: true\n #逻辑删除 删除前\n logic-not-delete-value: 0\n #逻辑删除 删除后\n logic-delete-value: 1\n configuration:\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n # 返回类型为Map,显示null对应的字段\n call-setters-on-nulls: true\n map-underscore-to-camel-case: true\n auto-mapping-behavior: full\n jdbc-type-for-null:\nspring: \n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://localhost:3306/mf_system?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: \n # url: \n # driver-class-name: \n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\nstorage:\n active: local\n address: http://localhost:8888/storage/file/\n local:\n storagePath: E:/storage\n aliyun:\n endpoint: oss-cn-hangzhou.aliyuncs.com\n accessKeyId: LTAI5***********FsgT\n accessKeySecret: ZtZGF***************JADjS2f\n bucketName: mfish-nocode\n qiniu:\n domain: http://cdn.mfish.com.cn\n accessKey: cHlX4*******************cdsexgvP\n secretKey: QPACH*******************1vEa1t4S\n bucket: mfish-nocode', '99b5e4f41980a831496b58ce36650b78', '2026-02-08 10:38:15', '2026-07-10 09:12:49', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (9, 'mf-gateway-dev.yml', 'DEFAULT_GROUP', 'spring:\n cloud:\n gateway:\n server:\n webflux:\n globalcors:\n cors-configurations:\n \'[/**]\':\n max-age: 3600\n allowed-origin-patterns: \"*\"\n allowed-headers: \"*\"\n allow-credentials: true\n allowed-methods:\n - GET\n - POST\n - DELETE\n - PUT\n - OPTION \n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: oauth2\n uri: lb://mf-oauth\n predicates:\n - Path=/oauth2/**\n filters:\n - CacheFilter\n - StripPrefix=1\n # 测试中心\n - id: test\n uri: lb://mf-test\n predicates:\n - Path=/test/**\n filters:\n - StripPrefix=1\n - id: sys\n uri: lb://mf-sys\n predicates:\n - Path=/sys/**\n filters:\n - StripPrefix=1\n - id: demo\n uri: lb://mf-demo\n predicates:\n - Path=/demo/**\n filters:\n - StripPrefix=1\n - id: storage\n uri: lb://mf-storage\n predicates:\n - Path=/storage/**\n filters:\n - StripPrefix=1\n - id: scheduler\n uri: lb://mf-scheduler\n predicates:\n - Path=/scheduler/**\n filters:\n - StripPrefix=1\n - id: nocode\n uri: lb://mf-nocode\n predicates:\n - Path=/nocode/**\n filters:\n - StripPrefix=1\n - id: workflow\n uri: lb://mf-workflow\n predicates:\n - Path=/workflow/**\n filters:\n - StripPrefix=1\n - id: ai\n uri: lb://mf-ai\n predicates:\n - Path=/ai/**\n filters:\n - StripPrefix=1\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n # 不校验白名单\n ignore:\n whites:\n - /oauth2/user/revoke\n - /oauth2/accessToken\n - /oauth2/authorize\n - /oauth2/sendMsg\n - /oauth2/qrCodeLogin/*\n - /oauth2/wx/bind/**\n - /oauth2/static/**\n - /oauth2/gitee/token/**\n - /oauth2/gitee/url\n - /oauth2/github/token/**\n - /oauth2/github/url\n - /storage/file/*.*\n - /code/**\n - /test/**\n - /*/v3/api-docs\n# swagger configuration\nspringdoc: \n webjars: \n # 设置为空,不要前缀,不配置这个就需要多加一层路径`/webjars`->`http:host:网关ip/webjars/swagger-ui/index.html`\n prefix:', 'be9f7df0750e8e41f064fe2896e8d13a', '2026-02-08 10:38:39', '2026-06-30 16:55:07', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (10, 'mf-gateway-dev.yml', 'DEFAULT_GROUP', 'spring:\n cloud:\n gateway:\n server:\n webflux:\n globalcors:\n cors-configurations:\n \'[/**]\':\n max-age: 3600\n allowed-origin-patterns: \"*\"\n allowed-headers: \"*\"\n allow-credentials: true\n allowed-methods:\n - GET\n - POST\n - DELETE\n - PUT\n - OPTION \n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: oauth2\n uri: lb://mf-oauth\n predicates:\n - Path=/oauth2/**\n filters:\n - CacheFilter\n - StripPrefix=1\n # 测试中心\n - id: test\n uri: lb://mf-test\n predicates:\n - Path=/test/**\n filters:\n - StripPrefix=1\n - id: sys\n uri: lb://mf-sys\n predicates:\n - Path=/sys/**\n filters:\n - StripPrefix=1\n - id: demo\n uri: lb://mf-demo\n predicates:\n - Path=/demo/**\n filters:\n - StripPrefix=1\n - id: storage\n uri: lb://mf-storage\n predicates:\n - Path=/storage/**\n filters:\n - StripPrefix=1\n - id: scheduler\n uri: lb://mf-scheduler\n predicates:\n - Path=/scheduler/**\n filters:\n - StripPrefix=1\n - id: nocode\n uri: lb://mf-nocode\n predicates:\n - Path=/nocode/**\n filters:\n - StripPrefix=1\n - id: workflow\n uri: lb://mf-workflow\n predicates:\n - Path=/workflow/**\n filters:\n - StripPrefix=1\n - id: ai\n uri: lb://mf-ai\n predicates:\n - Path=/ai/**\n filters:\n - StripPrefix=1\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n # 不校验白名单\n ignore:\n whites:\n - /oauth2/user/revoke\n - /oauth2/accessToken\n - /oauth2/authorize\n - /oauth2/sendMsg\n - /oauth2/qrCodeLogin/*\n - /oauth2/wx/bind/**\n - /oauth2/static/**\n - /oauth2/gitee/token/**\n - /oauth2/gitee/url\n - /oauth2/github/token/**\n - /oauth2/github/url\n - /storage/file/*.*\n - /code/**\n - /test/**\n - /*/v3/api-docs\n# swagger configuration\nspringdoc: \n webjars: \n # 设置为空,不要前缀,不配置这个就需要多加一层路径`/webjars`->`http:host:网关ip/webjars/swagger-ui/index.html`\n prefix:', 'be9f7df0750e8e41f064fe2896e8d13a', '2026-02-08 10:38:39', '2026-06-30 16:55:07', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (11, 'mf-scheduler-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼调度中心\'\r\n prefix: /scheduler\r\n# mybatis配置\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/scheduler/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_scheduler?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\norg:\r\n quartz:\r\n scheduler:\r\n instanceName: MfishClusteredScheduler\r\n instanceId: AUTO\r\n threadPool:\r\n poolClass: org.quartz.simpl.SimpleThreadPool\r\n threadCount: 50\r\n threadPriority: 5\r\n jobStore:\r\n jdbcClass: org.quartz.impl.jdbcjobstore.JobStoreTX\r\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\r\n useProperties: true\r\n isClustered: true\r\n tablePrefix: QRTZ_\r\n dataSource: mf_scheduler\r\n clusterCheckinInterval: 20000\r\n misfireThreshold: 60000\r\n#rocketmq配置信息\r\nrocketmq:\r\n #nameservice服务器地址(多个以英文逗号隔开)\r\n name-server: 127.0.0.1:9876\r\n #生产者配置\r\n producer:\r\n #组名\r\n group: mfish-scheduler-group\r\n sendMessageTimeout: 300000\r\n #目的地(topic:tag\r\n #topic\r\n topic: mfish-scheduler-topic', '804951d75ca731c1bd02f89f2868abe1', '2026-02-08 10:39:07', '2026-02-08 10:39:07', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (12, 'mf-scheduler-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼调度中心\'\r\n prefix: /scheduler\r\n# mybatis配置\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/scheduler/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_scheduler?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\norg:\r\n quartz:\r\n scheduler:\r\n instanceName: MfishClusteredScheduler\r\n instanceId: AUTO\r\n threadPool:\r\n poolClass: org.quartz.simpl.SimpleThreadPool\r\n threadCount: 50\r\n threadPriority: 5\r\n jobStore:\r\n jdbcClass: org.quartz.impl.jdbcjobstore.JobStoreTX\r\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\r\n useProperties: true\r\n isClustered: true\r\n tablePrefix: QRTZ_\r\n dataSource: mf_scheduler\r\n clusterCheckinInterval: 20000\r\n misfireThreshold: 60000\r\n#rocketmq配置信息\r\nrocketmq:\r\n #nameservice服务器地址(多个以英文逗号隔开)\r\n name-server: 127.0.0.1:9876\r\n #生产者配置\r\n producer:\r\n #组名\r\n group: mfish-scheduler-group\r\n sendMessageTimeout: 300000\r\n #目的地(topic:tag\r\n #topic\r\n topic: mfish-scheduler-topic', '804951d75ca731c1bd02f89f2868abe1', '2026-02-08 10:39:07', '2026-02-08 10:39:07', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (13, 'mf-workflow-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼工作流服务\'\r\n prefix: /workflow\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/workflow/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n dynamic:\r\n hikari:\r\n # ========== 连接池核心参数 ==========\r\n minimum-idle: 5 # 连接池最小空闲连接数\r\n maximum-pool-size: 20 # 连接池最大连接数\r\n idle-timeout: 300000 # 5分钟,空闲连接存活最大时间\r\n max-lifetime: 1800000 # 30分钟 连接最大存活时间\r\n connection-timeout: 30000 # 连接超时时间\r\n validation-timeout: 5000\r\n # ========== 连接检测 ==========\r\n connection-test-query: SELECT 1\r\n # ========== PreparedStatement 缓存 ==========\r\n data-source-properties:\r\n cachePrepStmts: true\r\n prepStmtCacheSize: 250\r\n prepStmtCacheSqlLimit: 2048\r\n useServerPrepStmts: true\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_workflow?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\n# Flowable 会自动建表\r\nflowable:\r\n # 自动更新数据库结构\r\n database-schema-update: true\r\n # 启用异步执行器\r\n async-executor-activate: true\r\n eventregistry:\r\n enabled: false\r\n # 检查流程定义\r\n# check-process-definitions: false', '09248143eabcad1efb0939f571a1005b', '2026-02-08 10:40:12', '2026-02-08 10:40:12', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (14, 'mf-workflow-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼工作流服务\'\r\n prefix: /workflow\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/workflow/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n dynamic:\r\n hikari:\r\n # ========== 连接池核心参数 ==========\r\n minimum-idle: 5 # 连接池最小空闲连接数\r\n maximum-pool-size: 20 # 连接池最大连接数\r\n idle-timeout: 300000 # 5分钟,空闲连接存活最大时间\r\n max-lifetime: 1800000 # 30分钟 连接最大存活时间\r\n connection-timeout: 30000 # 连接超时时间\r\n validation-timeout: 5000\r\n # ========== 连接检测 ==========\r\n connection-test-query: SELECT 1\r\n # ========== PreparedStatement 缓存 ==========\r\n data-source-properties:\r\n cachePrepStmts: true\r\n prepStmtCacheSize: 250\r\n prepStmtCacheSqlLimit: 2048\r\n useServerPrepStmts: true\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_workflow?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\n# Flowable 会自动建表\r\nflowable:\r\n # 自动更新数据库结构\r\n database-schema-update: true\r\n # 启用异步执行器\r\n async-executor-activate: true\r\n eventregistry:\r\n enabled: false\r\n # 检查流程定义\r\n# check-process-definitions: false', '09248143eabcad1efb0939f571a1005b', '2026-02-08 10:40:12', '2026-02-08 10:40:12', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (15, 'mf-monitor-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼监控中心\'\r\nspring: \r\n security:\r\n user:\r\n name: admin\r\n password: 123456\r\n boot:\r\n admin:\r\n ui:\r\n title: 摸鱼监控中心', 'cf16dec113c8fcf0728533a30e679e0a', '2026-02-08 10:40:35', '2026-02-08 10:40:35', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (16, 'mf-monitor-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼监控中心\'\r\nspring: \r\n security:\r\n user:\r\n name: admin\r\n password: 123456\r\n boot:\r\n admin:\r\n ui:\r\n title: 摸鱼监控中心', 'cf16dec113c8fcf0728533a30e679e0a', '2026-02-08 10:40:35', '2026-02-08 10:40:35', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (17, 'mf-nocode-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼无代码中心\'\r\n prefix: /nocode\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/nocode/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_nocode?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\nstorage:\r\n active: local\r\n address: http://localhost:8888/storage/file/\r\n local:\r\n storagePath: E:/storage\r\n aliyun:\r\n endpoint: oss-cn-hangzhou.aliyuncs.com\r\n accessKeyId: LTAI5***********FsgT\r\n accessKeySecret: ZtZGF***************JADjS2f\r\n bucketName: landwise\r\n qiniu:\r\n domain: http://cdn.mfish.com.cn\r\n accessKey: cHlX4*******************cdsexgvP\r\n secretKey: QPACH*******************1vEa1t4S\r\n bucket: mfish-nocode', '9da1faf051318a3b52a6c58f8d1b3773', '2026-02-08 10:40:58', '2026-02-08 10:40:58', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'text', NULL, '');
INSERT INTO `config_info` VALUES (18, 'mf-nocode-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼无代码中心\'\r\n prefix: /nocode\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/nocode/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_nocode?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\nstorage:\r\n active: local\r\n address: http://localhost:8888/storage/file/\r\n local:\r\n storagePath: E:/storage\r\n aliyun:\r\n endpoint: oss-cn-hangzhou.aliyuncs.com\r\n accessKeyId: LTAI5***********FsgT\r\n accessKeySecret: ZtZGF***************JADjS2f\r\n bucketName: landwise\r\n qiniu:\r\n domain: http://cdn.mfish.com.cn\r\n accessKey: cHlX4*******************cdsexgvP\r\n secretKey: QPACH*******************1vEa1t4S\r\n bucket: mfish-nocode', '9da1faf051318a3b52a6c58f8d1b3773', '2026-02-08 10:40:58', '2026-02-08 10:40:58', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'text', NULL, '');
INSERT INTO `config_info` VALUES (17, 'mf-nocode-dev.yml', 'DEFAULT_GROUP', 'swagger:\n title: \'摸鱼无代码中心\'\n prefix: /nocode\nmybatis-plus:\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapper-locations: classpath*:cn/com/mfish/nocode/mapper/**/*Mapper.xml\n global-config:\n # 关闭MP3.0自带的banner\n banner: false\n db-config:\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\n #id-type: 0\n # 默认数据库表下划线命名\n table-underline: true\n #逻辑删除 删除前\n logic-not-delete-value: 0\n #逻辑删除 删除后\n logic-delete-value: 1\n configuration:\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n # 返回类型为Map,显示null对应的字段\n call-setters-on-nulls: true\n map-underscore-to-camel-case: true\n auto-mapping-behavior: full\n jdbc-type-for-null:\nspring: \n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://localhost:3306/mf_nocode?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: \n # url: \n # driver-class-name: \n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\nstorage:\n active: local\n address: http://localhost:8888/storage/file/\n local:\n storagePath: E:/storage\n aliyun:\n endpoint: oss-cn-hangzhou.aliyuncs.com\n accessKeyId: LTAI5***********FsgT\n accessKeySecret: ZtZGF***************JADjS2f\n bucketName: mfish-nocode\n qiniu:\n domain: http://cdn.mfish.com.cn\n accessKey: cHlX4*******************cdsexgvP\n secretKey: QPACH*******************1vEa1t4S\n bucket: mfish-nocode', '8c1b715ea47953c10864fd80ac2be655', '2026-02-08 10:40:58', '2026-07-10 09:12:33', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (18, 'mf-nocode-dev.yml', 'DEFAULT_GROUP', 'swagger:\n title: \'摸鱼无代码中心\'\n prefix: /nocode\nmybatis-plus:\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapper-locations: classpath*:cn/com/mfish/nocode/mapper/**/*Mapper.xml\n global-config:\n # 关闭MP3.0自带的banner\n banner: false\n db-config:\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\n #id-type: 0\n # 默认数据库表下划线命名\n table-underline: true\n #逻辑删除 删除前\n logic-not-delete-value: 0\n #逻辑删除 删除后\n logic-delete-value: 1\n configuration:\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n # 返回类型为Map,显示null对应的字段\n call-setters-on-nulls: true\n map-underscore-to-camel-case: true\n auto-mapping-behavior: full\n jdbc-type-for-null:\nspring: \n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://localhost:3306/mf_nocode?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: \n # url: \n # driver-class-name: \n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\nstorage:\n active: local\n address: http://localhost:8888/storage/file/\n local:\n storagePath: E:/storage\n aliyun:\n endpoint: oss-cn-hangzhou.aliyuncs.com\n accessKeyId: LTAI5***********FsgT\n accessKeySecret: ZtZGF***************JADjS2f\n bucketName: mfish-nocode\n qiniu:\n domain: http://cdn.mfish.com.cn\n accessKey: cHlX4*******************cdsexgvP\n secretKey: QPACH*******************1vEa1t4S\n bucket: mfish-nocode', '8c1b715ea47953c10864fd80ac2be655', '2026-02-08 10:40:58', '2026-07-10 09:12:33', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (19, 'mf-demo-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼样例服务\'\r\n prefix: /demo\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/demo/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭', 'df5971c7c04f34636ac6b9c790b3073d', '2026-02-08 10:41:21', '2026-02-08 10:41:21', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (20, 'mf-demo-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼样例服务\'\r\n prefix: /demo\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/demo/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭', 'df5971c7c04f34636ac6b9c790b3073d', '2026-02-08 10:41:21', '2026-02-08 10:41:21', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (21, 'mf-ai-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼AI中心\'\r\n prefix: /ai\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/ai/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\n ai:\r\n openai:\r\n api-key: sk-or-v1-b4**************************************a99b135dae3b\r\n base-url: https://openrouter.ai/api\r\n chat:\r\n options:\r\n model: meta-llama/llama-3.3-70b-instruct:free\r\n max-tokens: 1000\r\n temperature: 0.7\r\n ollama:\r\n base-url: http://localhost:11434\r\n chat:\r\n model: qwen3:8b', 'c36840b577c0940c00d629f2497cc706', '2026-02-08 10:41:48', '2026-02-08 10:41:48', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (22, 'mf-ai-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼AI中心\'\r\n prefix: /ai\r\nmybatis-plus:\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapper-locations: classpath*:cn/com/mfish/ai/mapper/**/*Mapper.xml\r\n global-config:\r\n # 关闭MP3.0自带的banner\r\n banner: false\r\n db-config:\r\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\r\n #id-type: 0\r\n # 默认数据库表下划线命名\r\n table-underline: true\r\n #逻辑删除 删除前\r\n logic-not-delete-value: 0\r\n #逻辑删除 删除后\r\n logic-delete-value: 1\r\n configuration:\r\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\r\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\r\n # 返回类型为Map,显示null对应的字段\r\n call-setters-on-nulls: true\r\n map-underscore-to-camel-case: true\r\n auto-mapping-behavior: full\r\n jdbc-type-for-null:\r\nspring: \r\n datasource:\r\n druid:\r\n stat-view-servlet:\r\n enabled: true\r\n loginUsername: admin\r\n loginPassword: 123456\r\n dynamic:\r\n druid:\r\n initial-size: 5\r\n min-idle: 5\r\n maxActive: 20\r\n maxWait: 60000\r\n timeBetweenEvictionRunsMillis: 60000\r\n minEvictableIdleTimeMillis: 300000\r\n validationQuery: SELECT 1 FROM DUAL\r\n testWhileIdle: true\r\n testOnBorrow: false\r\n testOnReturn: false\r\n poolPreparedStatements: true\r\n maxPoolPreparedStatementPerConnectionSize: 20\r\n filters: stat,slf4j\r\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\r\n datasource:\r\n # 主库数据源\r\n master:\r\n driver-class-name: com.mysql.cj.jdbc.Driver\r\n url: jdbc:mysql://localhost:3306/mf_demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\r\n username: root\r\n password: 123456\r\n # 从库数据源\r\n # slave:\r\n # username: \r\n # password: \r\n # url: \r\n # driver-class-name: \r\n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\r\n ai:\r\n openai:\r\n api-key: sk-or-v1-b4**************************************a99b135dae3b\r\n base-url: https://openrouter.ai/api\r\n chat:\r\n options:\r\n model: meta-llama/llama-3.3-70b-instruct:free\r\n max-tokens: 1000\r\n temperature: 0.7\r\n ollama:\r\n base-url: http://localhost:11434\r\n chat:\r\n model: qwen3:8b', 'c36840b577c0940c00d629f2497cc706', '2026-02-08 10:41:48', '2026-02-08 10:41:48', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (21, 'mf-ai-dev.yml', 'DEFAULT_GROUP', 'swagger:\n title: \'摸鱼AI中心\'\n prefix: /ai\nmybatis-plus:\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapper-locations: classpath*:cn/com/mfish/ai/mapper/**/*Mapper.xml\n global-config:\n # 关闭MP3.0自带的banner\n banner: false\n db-config:\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\n #id-type: 0\n # 默认数据库表下划线命名\n table-underline: true\n #逻辑删除 删除前\n logic-not-delete-value: 0\n #逻辑删除 删除后\n logic-delete-value: 1\n configuration:\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n # 返回类型为Map,显示null对应的字段\n call-setters-on-nulls: true\n map-underscore-to-camel-case: true\n auto-mapping-behavior: full\n jdbc-type-for-null:\nspring: \n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://localhost:3306/mf_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: \n # url: \n # driver-class-name: \n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\n ai:\n openai:\n api-key: sk-or-v1-b4**************************************a99b135dae3b\n base-url: https://openrouter.ai/api\n chat:\n model: meta-llama/llama-3.3-70b-instruct:free\n max-tokens: 1000\n temperature: 0.7\n ollama:\n base-url: http://localhost:11434\n chat:\n model: qwen3:8b\nai:\n api-key:\n hex-key: 8ca90bd312db42aac5a8313ce9a0eac3', 'be444f1caa0f727eb32de9134ccce9aa', '2026-02-08 10:41:48', '2026-07-10 09:10:53', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (22, 'mf-ai-dev.yml', 'DEFAULT_GROUP', 'swagger:\n title: \'摸鱼AI中心\'\n prefix: /ai\nmybatis-plus:\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapper-locations: classpath*:cn/com/mfish/ai/mapper/**/*Mapper.xml\n global-config:\n # 关闭MP3.0自带的banner\n banner: false\n db-config:\n #主键类型 0:\"数据库ID自增\",1:\"该类型为未设置主键类型\", 2:\"用户输入ID\",3:\"全局唯一ID (数字类型唯一ID)\", 4:\"全局唯一ID UUID\",5:\"字符串全局唯一ID (idWorker 的字符串表示)\";\n #id-type: 0\n # 默认数据库表下划线命名\n table-underline: true\n #逻辑删除 删除前\n logic-not-delete-value: 0\n #逻辑删除 删除后\n logic-delete-value: 1\n configuration:\n # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用\n #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n # 返回类型为Map,显示null对应的字段\n call-setters-on-nulls: true\n map-underscore-to-camel-case: true\n auto-mapping-behavior: full\n jdbc-type-for-null:\nspring: \n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://localhost:3306/mf_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: \n # url: \n # driver-class-name: \n # seata: true # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭\n ai:\n openai:\n api-key: sk-or-v1-b4**************************************a99b135dae3b\n base-url: https://openrouter.ai/api\n chat:\n model: meta-llama/llama-3.3-70b-instruct:free\n max-tokens: 1000\n temperature: 0.7\n ollama:\n base-url: http://localhost:11434\n chat:\n model: qwen3:8b\nai:\n api-key:\n hex-key: 8ca90bd312db42aac5a8313ce9a0eac3', 'be444f1caa0f727eb32de9134ccce9aa', '2026-02-08 10:41:48', '2026-07-10 09:10:53', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (23, 'mf-consume-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼调度中心-MQ消费端测试\'\r\n#rocketmq配置信息\r\nrocketmq:\r\n #生产者配置\r\n consumer:\r\n nameServer: 127.0.0.1:9876\r\n #topic\r\n topic: mfish-scheduler-topic\r\n #组名\r\n group: mfish-scheduler-group', '3b38a5c258a1bdd32b06844e87a4e1f1', '2026-02-08 10:42:04', '2026-02-08 10:42:04', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (24, 'mf-consume-dev.yml', 'DEFAULT_GROUP', 'swagger:\r\n title: \'摸鱼调度中心-MQ消费端测试\'\r\n#rocketmq配置信息\r\nrocketmq:\r\n #生产者配置\r\n consumer:\r\n nameServer: 127.0.0.1:9876\r\n #topic\r\n topic: mfish-scheduler-topic\r\n #组名\r\n group: mfish-scheduler-group', '3b38a5c258a1bdd32b06844e87a4e1f1', '2026-02-08 10:42:04', '2026-02-08 10:42:04', 'nacos', '0:0:0:0:0:0:0:1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (25, 'sentinel-mf-gateway', 'DEFAULT_GROUP', '[\r\n {\r\n \"resource\": \"mf-oauth\",\r\n \"count\": 500,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"mf-sys\",\r\n \"count\": 500,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"mf-storage\",\r\n \"count\": 100,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"mf-scheduler\",\r\n \"count\": 100,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n }\r\n]', '2f2496295023706014fa181fe2b4ffa3', '2026-02-08 10:42:36', '2026-02-08 10:42:36', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', '', NULL, NULL, NULL, 'json', NULL, '');
+4 -4
View File
@@ -1,7 +1,7 @@
/**
DEMO样例表(采用微服务启动,如果需要使用演示样例需要创建该库)
根据个人使用场景选择是否初始化
*/
-- ----------------------------------------
-- DEMO样例表(采用微服务启动,如果需要使用演示样例需要创建该库)
-- 根据个人使用场景选择是否初始化
-- ----------------------------------------
DROP DATABASE IF EXISTS `mf_demo`;
CREATE DATABASE `mf_demo` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
+3 -3
View File
@@ -1,6 +1,6 @@
/**
自助大屏、自助API相关表(使用自助大屏相关功能需要创建该表)
*/
-- ----------------------------------------
-- 自助大屏、自助API相关表(使用自助大屏相关功能需要创建该表)
-- ----------------------------------------
DROP DATABASE IF EXISTS `mf_nocode`;
CREATE DATABASE `mf_nocode` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
+8 -3
View File
@@ -1,6 +1,6 @@
/**
微服务认证、用户、权限、租户相关表(采用微服务启动,需要创建该库)
*/
-- ----------------------------------------
-- 微服务认证、用户、权限、租户相关表(采用微服务启动,需要创建该库)
-- ----------------------------------------
DROP DATABASE IF EXISTS `mf_oauth`;
CREATE DATABASE `mf_oauth` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
@@ -229,6 +229,11 @@ INSERT INTO `sso_menu` VALUES ('ad5b361ff34235e8ec85cd613a59bf6e', '8ae3ea763294
INSERT INTO `sso_menu` VALUES ('c5309ca3ba545da9950278637c90e674', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300002', 3, '新增', '#', 2, 2, '', NULL, 'workflow:flowManage:query,workflow:flowManage:insert', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('e92728a6d996d4b7cb3acb0fc031d183', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300004', 3, '删除', '#', 4, 2, '', NULL, 'workflow:flowManage:query,workflow:flowManage:delete', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('c0adefdb601f6e41f82a06ff6513613e', '53e8eaceee36c1d54e43319fdd60811b', '0001300008', 2, '工作流样例', 'ant-design:fork-outlined', 11, 1, '/demo-leave-apply', '/demo/demo-leave-apply/index.vue', NULL, 0, 1, NULL, 1, '', 'admin', '2026-04-20 17:05:36', 'admin', '2026-04-20 20:05:50');
INSERT INTO `sso_menu` VALUES ('3993e903efe9406aca08697bf80a4140', 'f65cc0cfee8cced9bf0613def2d04b42', '000020001500004', 3, '删除', '#', 4, 2, '', NULL, 'ai:aiModelConfig:query,ai:aiModelConfig:delete', 0, 1, NULL, NULL, '', 'admin', '2026-07-06 21:35:04', '', NULL);
INSERT INTO `sso_menu` VALUES ('417074a416446e7b6382691e0750bdad', 'f65cc0cfee8cced9bf0613def2d04b42', '000020001500001', 3, '查询', '#', 1, 2, '', NULL, 'ai:aiModelConfig:query', 0, 1, NULL, NULL, '', 'admin', '2026-07-06 21:35:04', '', NULL);
INSERT INTO `sso_menu` VALUES ('4ee0d5036b299c5efb7674c9ccf1d647', 'f65cc0cfee8cced9bf0613def2d04b42', '000020001500002', 3, '新增', '#', 2, 2, '', NULL, 'ai:aiModelConfig:query,ai:aiModelConfig:insert', 0, 1, NULL, NULL, '', 'admin', '2026-07-06 21:35:04', '', NULL);
INSERT INTO `sso_menu` VALUES ('567c06fabc1f6c429aec3e6b1c7fa511', 'f65cc0cfee8cced9bf0613def2d04b42', '000020001500003', 3, '修改', '#', 3, 2, '', NULL, 'ai:aiModelConfig:query,ai:aiModelConfig:update', 0, 1, NULL, NULL, '', 'admin', '2026-07-06 21:35:04', '', NULL);
INSERT INTO `sso_menu` VALUES ('f65cc0cfee8cced9bf0613def2d04b42', '2a4e024fdc76063da32926c63ca9ead2', '0000200015', 2, '模型配置', 'ant-design:open-a-i-outlined', 13, 1, '/ai-model-config', '/ai/ai-model-config/index.vue', NULL, 0, 1, NULL, 1, '', 'admin', '2026-07-06 21:35:04', 'admin', '2026-07-06 21:40:34');
-- ----------------------------
-- Table structure for sso_org
+3 -3
View File
@@ -1,6 +1,6 @@
/**
调度策略相关表,使用系统提供调度服务需要用到相关表(采用微服务启动,需要创建该库)
*/
-- ----------------------------------------
-- 调度策略相关表,使用系统提供调度服务需要用到相关表(采用微服务启动,需要创建该库)
-- ----------------------------------------
DROP DATABASE IF EXISTS `mf_scheduler`;
CREATE DATABASE `mf_scheduler` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
+8 -3
View File
@@ -1,6 +1,6 @@
/**
系统相关表(采用微服务启动,需要创建该库)
*/
-- ----------------------------------------
-- 系统相关表(采用微服务启动,需要创建该库)
-- ----------------------------------------
DROP DATABASE IF EXISTS `mf_system`;
CREATE DATABASE `mf_system` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
@@ -123,6 +123,7 @@ INSERT INTO `sys_dict` VALUES ('b0f0a03f9e879c2ce3b7722a3cd01061', 'HTTP请求
INSERT INTO `sys_dict` VALUES ('4a9914bd42939163ceb9c5b4002cfa66', 'HTTP请求方式', 'http_request_method', 0, NULL, 'admin', '2025-07-22 14:30:25', 'admin', '2025-07-22 14:30:25');
INSERT INTO `sys_dict` VALUES ('220bbd3b1dd32fd37d0abbd279a14774', '工作流任务状态', 'workflow_task_status', 0, '工作流任务状态字典', 'admin', '2025-10-10 16:27:01', 'admin', '2025-10-10 16:27:01');
INSERT INTO `sys_dict` VALUES ('eeb27772c310addeae7c12d296521399', '工作流审批类型', 'workflow_process_key', 0, '工作流流程定义key,例如大屏发布审批是screen_release', 'admin', '2025-10-15 16:34:32', 'admin', '2025-10-15 16:34:32');
INSERT INTO `sys_dict` VALUES ('cd338555eccf73e26e9243837d6c711e', '模型接入协议', 'ai_model_protocol', 0, 'AI模型接入的协议,比如openai协议', 'admin', '2026-07-10 16:56:59', '', NULL);
-- ----------------------------
-- Table structure for sys_dict_item
@@ -272,6 +273,10 @@ INSERT INTO `sys_dict_item` VALUES ('261f489024bb90163e49ee85498df47e', '220bbd3
INSERT INTO `sys_dict_item` VALUES ('07582f8cb8443cd3de3f5ab150386690', '220bbd3b1dd32fd37d0abbd279a14774', 'workflow_task_status', '已取消', 'terminated', 0, 3, '', 'red', 0, NULL, 'admin', '2025-10-10 16:29:10', 'admin', '2025-10-10 16:29:14');
INSERT INTO `sys_dict_item` VALUES ('4e14582f0b59762f2ca1cfda04539202', 'eeb27772c310addeae7c12d296521399', 'workflow_process_key', '大屏发布', 'screen_release', 0, 1, 'ant-design:fund-projection-screen-outlined', '', 0, NULL, 'admin', '2025-10-15 16:35:15', 'admin', '2025-10-16 11:04:43');
INSERT INTO `sys_dict_item` VALUES ('581a7e287feec3be8771175bdc5dab9b', 'eeb27772c310addeae7c12d296521399', 'workflow_process_key', '工作流样例审批', 'demo_leave_apply_release', 0, 2, 'ant-design:fork-outlined', 'blue', 0, '工作流试用样例', 'admin', '2026-04-08 20:08:42', 'admin', '2026-04-20 21:38:45');
INSERT INTO `sys_dict_item` VALUES ('e100aa3c678f2bf6a4e1e24a9feee89f', 'cd338555eccf73e26e9243837d6c711e', 'ai_model_protocol', 'openai', 'openai', 0, 1, 'ant-design:open-a-i-outlined', 'green', 0, NULL, 'admin', '2026-07-10 16:57:27', 'admin', '2026-07-10 16:57:37');
INSERT INTO `sys_dict_item` VALUES ('cac0983d1430c0a7dce98bd8c01cc19c', 'cd338555eccf73e26e9243837d6c711e', 'ai_model_protocol', 'ollama', 'ollama', 0, 2, '', 'blue', 0, NULL, 'admin', '2026-07-10 16:58:34', '', NULL);
INSERT INTO `sys_dict_item` VALUES ('64a2906317754ac19c779e04ddc155de', 'cd338555eccf73e26e9243837d6c711e', 'ai_model_protocol', 'deepseek', 'deepseek', 0, 3, NULL, 'cyan', 0, NULL, 'admin', '2026-07-10 16:58:49', '', NULL);
INSERT INTO `sys_dict_item` VALUES ('76faf36cc4e459171c273fa833e79ff4', 'cd338555eccf73e26e9243837d6c711e', 'ai_model_protocol', 'anthropic', 'anthropic', 0, 4, NULL, 'pink', 0, NULL, 'admin', '2026-07-10 16:59:22', '', NULL);
-- ----------------------------
-- Table structure for sys_log
+3 -3
View File
@@ -1,6 +1,6 @@
/**
*/
-- ----------------------------------------
-- 工作流数据库(采用微服务启动时,需要创建工作流数据库,数据表引擎会自动创建)
-- ----------------------------------------
DROP DATABASE IF EXISTS `mf_workflow`;
CREATE DATABASE `mf_workflow` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
+13 -3
View File
@@ -1,6 +1,6 @@
/**
*/
-- ----------------------------------------
-- 单实例启动需要初始化的表(如果采用单实例启动,只需要创建这个数据库,其他微服务相关数据库无需创建)
-- ----------------------------------------
DROP DATABASE IF EXISTS `mfish_nocode`;
CREATE DATABASE `mfish_nocode` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
@@ -454,6 +454,11 @@ INSERT INTO `sso_menu` VALUES ('ad5b361ff34235e8ec85cd613a59bf6e', '8ae3ea763294
INSERT INTO `sso_menu` VALUES ('c5309ca3ba545da9950278637c90e674', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300002', 3, '新增', '#', 2, 2, '', NULL, 'workflow:flowManage:query,workflow:flowManage:insert', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('e92728a6d996d4b7cb3acb0fc031d183', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300004', 3, '删除', '#', 4, 2, '', NULL, 'workflow:flowManage:query,workflow:flowManage:delete', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('c0adefdb601f6e41f82a06ff6513613e', '53e8eaceee36c1d54e43319fdd60811b', '0001300008', 2, '工作流样例', 'ant-design:fork-outlined', 11, 1, '/demo-leave-apply', '/demo/demo-leave-apply/index.vue', NULL, 0, 1, NULL, 1, '', 'admin', '2026-04-20 17:05:36', 'admin', '2026-04-20 20:05:50');
INSERT INTO `sso_menu` VALUES ('3993e903efe9406aca08697bf80a4140', 'f65cc0cfee8cced9bf0613def2d04b42', '000020001500004', 3, '删除', '#', 4, 2, '', NULL, 'ai:aiModelConfig:query,ai:aiModelConfig:delete', 0, 1, NULL, NULL, '', 'admin', '2026-07-06 21:35:04', '', NULL);
INSERT INTO `sso_menu` VALUES ('417074a416446e7b6382691e0750bdad', 'f65cc0cfee8cced9bf0613def2d04b42', '000020001500001', 3, '查询', '#', 1, 2, '', NULL, 'ai:aiModelConfig:query', 0, 1, NULL, NULL, '', 'admin', '2026-07-06 21:35:04', '', NULL);
INSERT INTO `sso_menu` VALUES ('4ee0d5036b299c5efb7674c9ccf1d647', 'f65cc0cfee8cced9bf0613def2d04b42', '000020001500002', 3, '新增', '#', 2, 2, '', NULL, 'ai:aiModelConfig:query,ai:aiModelConfig:insert', 0, 1, NULL, NULL, '', 'admin', '2026-07-06 21:35:04', '', NULL);
INSERT INTO `sso_menu` VALUES ('567c06fabc1f6c429aec3e6b1c7fa511', 'f65cc0cfee8cced9bf0613def2d04b42', '000020001500003', 3, '修改', '#', 3, 2, '', NULL, 'ai:aiModelConfig:query,ai:aiModelConfig:update', 0, 1, NULL, NULL, '', 'admin', '2026-07-06 21:35:04', '', NULL);
INSERT INTO `sso_menu` VALUES ('f65cc0cfee8cced9bf0613def2d04b42', '2a4e024fdc76063da32926c63ca9ead2', '0000200015', 2, '模型配置', 'ant-design:open-a-i-outlined', 13, 1, '/ai-model-config', '/ai/ai-model-config/index.vue', NULL, 0, 1, NULL, 1, '', 'admin', '2026-07-06 21:35:04', 'admin', '2026-07-06 21:40:34');
-- ----------------------------
-- Table structure for sso_org
@@ -991,6 +996,7 @@ INSERT INTO `sys_dict` VALUES ('b0f0a03f9e879c2ce3b7722a3cd01061', 'HTTP请求
INSERT INTO `sys_dict` VALUES ('4a9914bd42939163ceb9c5b4002cfa66', 'HTTP请求方式', 'http_request_method', 0, NULL, 'admin', '2025-07-22 14:30:25', 'admin', '2025-07-22 14:30:25');
INSERT INTO `sys_dict` VALUES ('220bbd3b1dd32fd37d0abbd279a14774', '工作流任务状态', 'workflow_task_status', 0, '工作流任务状态字典', 'admin', '2025-10-10 16:27:01', 'admin', '2025-10-10 16:27:01');
INSERT INTO `sys_dict` VALUES ('eeb27772c310addeae7c12d296521399', '工作流审批类型', 'workflow_process_key', 0, '工作流流程定义key,例如大屏发布审批是screen_release', 'admin', '2025-10-15 16:34:32', 'admin', '2025-10-15 16:34:32');
INSERT INTO `sys_dict` VALUES ('cd338555eccf73e26e9243837d6c711e', '模型接入协议', 'ai_model_protocol', 0, 'AI模型接入的协议,比如openai协议', 'admin', '2026-07-10 16:56:59', '', NULL);
-- ----------------------------
-- Table structure for sys_dict_item
@@ -1140,6 +1146,10 @@ INSERT INTO `sys_dict_item` VALUES ('261f489024bb90163e49ee85498df47e', '220bbd3
INSERT INTO `sys_dict_item` VALUES ('07582f8cb8443cd3de3f5ab150386690', '220bbd3b1dd32fd37d0abbd279a14774', 'workflow_task_status', '已取消', 'terminated', 0, 3, '', 'red', 0, NULL, 'admin', '2025-10-10 16:29:10', 'admin', '2025-10-10 16:29:14');
INSERT INTO `sys_dict_item` VALUES ('4e14582f0b59762f2ca1cfda04539202', 'eeb27772c310addeae7c12d296521399', 'workflow_process_key', '大屏发布', 'screen_release', 0, 1, 'ant-design:fund-projection-screen-outlined', '', 0, NULL, 'admin', '2025-10-15 16:35:15', 'admin', '2025-10-16 11:04:43');
INSERT INTO `sys_dict_item` VALUES ('581a7e287feec3be8771175bdc5dab9b', 'eeb27772c310addeae7c12d296521399', 'workflow_process_key', '工作流样例审批', 'demo_leave_apply_release', 0, 2, 'ant-design:fork-outlined', 'blue', 0, '工作流试用样例', 'admin', '2026-04-08 20:08:42', 'admin', '2026-04-20 21:38:45');
INSERT INTO `sys_dict_item` VALUES ('e100aa3c678f2bf6a4e1e24a9feee89f', 'cd338555eccf73e26e9243837d6c711e', 'ai_model_protocol', 'openai', 'openai', 0, 1, 'ant-design:open-a-i-outlined', 'green', 0, NULL, 'admin', '2026-07-10 16:57:27', 'admin', '2026-07-10 16:57:37');
INSERT INTO `sys_dict_item` VALUES ('cac0983d1430c0a7dce98bd8c01cc19c', 'cd338555eccf73e26e9243837d6c711e', 'ai_model_protocol', 'ollama', 'ollama', 0, 2, '', 'blue', 0, NULL, 'admin', '2026-07-10 16:58:34', '', NULL);
INSERT INTO `sys_dict_item` VALUES ('64a2906317754ac19c779e04ddc155de', 'cd338555eccf73e26e9243837d6c711e', 'ai_model_protocol', 'deepseek', 'deepseek', 0, 3, NULL, 'cyan', 0, NULL, 'admin', '2026-07-10 16:58:49', '', NULL);
INSERT INTO `sys_dict_item` VALUES ('76faf36cc4e459171c273fa833e79ff4', 'cd338555eccf73e26e9243837d6c711e', 'ai_model_protocol', 'anthropic', 'anthropic', 0, 4, NULL, 'pink', 0, NULL, 'admin', '2026-07-10 16:59:22', '', NULL);
-- ----------------------------
-- Table structure for sys_log
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-api</artifactId>
<version>mf-2.4.0</version>
</parent>
<artifactId>mf-ai-api</artifactId>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-core</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,69 @@
package cn.com.mfish.ai.api.entity;
import cn.com.mfish.common.core.entity.BaseEntity;
import cn.idev.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @description: AI模型配置信息
* @author: mfish
* @date: 2026-07-03
* @version: V2.4.0
*/
@Data
@TableName("ai_model_config")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "ai_model_config对象 AI模型配置信息")
public class AiModelConfig extends BaseEntity<String> {
@ExcelProperty("唯一ID")
@Schema(description = "唯一ID")
@TableId(type = IdType.ASSIGN_UUID)
@Accessors(chain = true)
private String id;
@ExcelProperty("租户ID")
@Schema(description = "租户ID")
private String tenantId;
@ExcelProperty("提供者: openai/ollama/deepseek/zhipuai/anthropic等")
@Schema(description = "提供者: openai/ollama/deepseek/zhipuai/anthropic等")
private String provider;
@ExcelProperty("模型名称: gpt-4o, qwen3:8b, deepseek-v3 等")
@Schema(description = "模型名称: gpt-4o, qwen3:8b, deepseek-v3 等")
private String modelName;
@ExcelProperty("接入协议: openai/ollama/deepseek/anthropic")
@Schema(description = "接入协议: openai/ollama/deepseek/anthropic")
private String protocol;
@ExcelProperty("API密钥(加密存储)")
@Schema(description = "API密钥(加密存储)")
private String apiKey;
@ExcelProperty("API基础地址")
@Schema(description = "API基础地址")
private String baseUrl;
@ExcelProperty("最大token数")
@Schema(description = "最大token数")
private Integer maxTokens;
@ExcelProperty("温度参数")
@Schema(description = "温度参数")
private Double temperature;
@ExcelProperty("top_p参数")
@Schema(description = "top_p参数")
private Double topP;
@ExcelProperty("补全项路径")
@Schema(description = "补全项路径")
private String completionsPath;
@ExcelProperty("是否启用 1启用 0禁用")
@Schema(description = "是否启用 1启用 0禁用")
private Short enabled;
@ExcelProperty("排序(决定fallback优先级)")
@Schema(description = "排序(决定fallback优先级)")
private Integer sortOrder;
@ExcelProperty("备注")
@Schema(description = "备注")
private String remark;
}
@@ -0,0 +1,45 @@
package cn.com.mfish.ai.api.fallback;
import cn.com.mfish.ai.api.entity.AiModelConfig;
import cn.com.mfish.ai.api.remote.RemoteAiModelConfigService;
import cn.com.mfish.ai.api.req.ReqAiModelConfig;
import cn.com.mfish.ai.api.vo.AiModelConfigVo;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @description: ai模型配置服务降级处理
* @author: mfish
* @date: 2026/07/03
*/
@Component
@Slf4j
public class RemoteAiModelConfigFallBack implements FallbackFactory<RemoteAiModelConfigService> {
/**
* 创建ai模型配置服务降级实例
*
* @param cause 导致降级的异常原因
* @return 降级后的请假申请审批服务实例
*/
@Override
public RemoteAiModelConfigService create(Throwable cause) {
log.error("错误: demo请假申请审批回调接口调用异常", cause);
return new RemoteAiModelConfigService() {
@Override
public Result<PageResult<AiModelConfigVo>> queryPageList(String origin, ReqAiModelConfig reqAiModelConfig, ReqPage reqPage) {
return Result.fail("错误:查询分页列表失败" + cause.getMessage());
}
@Override
public Result<List<AiModelConfig>> queryList(String origin, ReqAiModelConfig reqAiModelConfig) {
return Result.fail("错误:查询列表失败" + cause.getMessage());
}
};
}
}
@@ -0,0 +1,31 @@
package cn.com.mfish.ai.api.remote;
import cn.com.mfish.ai.api.entity.AiModelConfig;
import cn.com.mfish.ai.api.fallback.RemoteAiModelConfigFallBack;
import cn.com.mfish.ai.api.req.ReqAiModelConfig;
import cn.com.mfish.ai.api.vo.AiModelConfigVo;
import cn.com.mfish.common.core.constants.RPCConstants;
import cn.com.mfish.common.core.constants.ServiceConstants;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import java.util.List;
/**
* @description: ai模型配置服务
* @author: mfish
* @date: 2026/07/03
*/
@FeignClient(contextId = "remoteAiModelConfigService", value = ServiceConstants.AI_SERVICE, fallbackFactory = RemoteAiModelConfigFallBack.class)
public interface RemoteAiModelConfigService {
@GetMapping("/aiModelConfig")
Result<PageResult<AiModelConfigVo>> queryPageList(@RequestHeader(RPCConstants.REQ_ORIGIN) String origin, @SpringQueryMap ReqAiModelConfig reqAiModelConfig, @SpringQueryMap ReqPage reqPage);
@GetMapping("/aiModelConfig/list")
Result<List<AiModelConfig>> queryList(@RequestHeader(RPCConstants.REQ_ORIGIN) String origin, @SpringQueryMap ReqAiModelConfig reqAiModelConfig);
}
@@ -0,0 +1,23 @@
package cn.com.mfish.ai.api.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @description: AI模型配置信息
* @author: mfish
* @date: 2026-07-03
* @version: V2.4.0
*/
@Data
@Accessors(chain = true)
@Schema(description = "AI模型配置信息请求参数")
public class ReqAiModelConfig {
@Schema(description = "模型名称: gpt-4o, qwen3:8b, deepseek-v3 等")
private String modelName;
@Schema(description = "是否启用 1启用 0禁用")
private Integer enabled;
@Schema(description = "接入协议: openai/ollama/deepseek/anthropic")
private String protocol;
}
@@ -0,0 +1,21 @@
package cn.com.mfish.ai.api.vo;
import cn.com.mfish.ai.api.entity.AiModelConfig;
/**
* @description: AI模型配置信息VO
* @author: mfish
* @date: 2026/7/8
*/
public class AiModelConfigVo extends AiModelConfig {
public String getApiKey() {
String apiKey = super.getApiKey();
if (apiKey == null) {
return null;
}
if (apiKey.length() <= 6) {
return apiKey.charAt(0) + "****";
}
return apiKey.substring(0, 3) + "****" + apiKey.substring(apiKey.length() - 3);
}
}
@@ -0,0 +1 @@
cn.com.mfish.ai.api.fallback.RemoteAiModelConfigFallBack
@@ -3,10 +3,13 @@ package cn.com.mfish.common.oauth.api.fallback;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.common.oauth.api.remote.RemoteMenuService;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @description: 菜单远程调用失败处理
* @author: mfish
@@ -25,6 +28,11 @@ public class RemoteMenuFallback implements FallbackFactory<RemoteMenuService> {
public RemoteMenuService create(Throwable cause) {
log.error("菜单服务调用失败:{}", cause.getMessage());
return new RemoteMenuService() {
@Override
public Result<List<SsoMenu>> queryMenuTree(String origin, ReqSsoMenu reqSsoMenu) {
return Result.fail("错误:查询菜单树失败" + cause.getMessage());
}
@Override
public Result<SsoMenu> add(String origin, SsoMenu ssoMenu) {
return Result.fail("错误:保存菜单失败" + cause.getMessage());
@@ -5,9 +5,13 @@ import cn.com.mfish.common.core.constants.ServiceConstants;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.common.oauth.api.fallback.RemoteMenuFallback;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @description: 菜单远程接口
* @author: mfish
@@ -15,6 +19,16 @@ import org.springframework.web.bind.annotation.*;
*/
@FeignClient(contextId = "remoteMenuService", value = ServiceConstants.OAUTH_SERVICE, fallbackFactory = RemoteMenuFallback.class)
public interface RemoteMenuService {
/**
* 查询菜单树
* @param origin 来源
* @param reqSsoMenu 查询参数
* @return 菜单树
*/
@GetMapping("/menu/tree")
Result<List<SsoMenu>> queryMenuTree(@RequestHeader(RPCConstants.REQ_ORIGIN) String origin, @SpringQueryMap ReqSsoMenu reqSsoMenu);
/**
* 新增菜单
*
@@ -1,4 +1,4 @@
package cn.com.mfish.common.oauth.req;
package cn.com.mfish.common.oauth.api.req;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -1,4 +1,4 @@
package cn.com.mfish.sys.entity;
package cn.com.mfish.sys.api.entity;
import cn.com.mfish.common.core.entity.BaseEntity;
import cn.idev.excel.annotation.ExcelProperty;
@@ -1,11 +1,16 @@
package cn.com.mfish.sys.api.fallback;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.sys.api.entity.Dict;
import cn.com.mfish.sys.api.entity.DictItem;
import cn.com.mfish.sys.api.remote.RemoteDictService;
import cn.com.mfish.sys.api.req.ReqDict;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @description: 远程字典失败处理
* @author: mfish
@@ -24,6 +29,16 @@ public class RemoteDictFallback implements FallbackFactory<RemoteDictService> {
@Override
public RemoteDictService create(Throwable cause) {
log.error("错误:字典调用异常", cause);
return (origin, dictCode) -> Result.fail("错误:查询字典数据出错");
return new RemoteDictService() {
@Override
public Result<List<DictItem>> queryByCode(String origin, String dictCode) {
return Result.fail("查询字典项失败");
}
@Override
public Result<List<Dict>> queryList(String origin, ReqDict reqDict) {
return Result.fail("查询字典列表失败");
}
};
}
}
@@ -3,10 +3,13 @@ package cn.com.mfish.sys.api.remote;
import cn.com.mfish.common.core.constants.RPCConstants;
import cn.com.mfish.common.core.constants.ServiceConstants;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.sys.api.entity.Dict;
import cn.com.mfish.sys.api.entity.DictItem;
import cn.com.mfish.sys.api.fallback.RemoteDictFallback;
import cn.com.mfish.sys.api.req.ReqDict;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
@@ -29,4 +32,14 @@ public interface RemoteDictService {
*/
@GetMapping("/dictItem/{dictCode}")
Result<List<DictItem>> queryByCode(@RequestHeader(RPCConstants.REQ_ORIGIN) String origin, @Parameter(name = "dictCode", description = "字典编码") @PathVariable String dictCode);
/**
* 查询字典列表
*
* @param origin 来源
* @param reqDict 查询参数
* @return 字典列表
*/
@GetMapping("/dict/list")
Result<List<Dict>> queryList(@RequestHeader(RPCConstants.REQ_ORIGIN) String origin, @SpringQueryMap ReqDict reqDict);
}
@@ -1,4 +1,4 @@
package cn.com.mfish.sys.req;
package cn.com.mfish.sys.api.req;
import io.swagger.v3.oas.annotations.media.Schema;
+1
View File
@@ -19,6 +19,7 @@
<module>mf-workflow-api</module>
<module>mf-nocode-api</module>
<module>mf-demo-api</module>
<module>mf-ai-api</module>
</modules>
<properties>
+16 -21
View File
@@ -23,40 +23,35 @@
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- 核心工具(StringUtils、ServiceConstants、Jackson等) -->
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-core</artifactId>
</dependency>
<!-- AI公共模块(AiConfig、ChatResponseVo、AiRouteService等) -->
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-ai</artifactId>
</dependency>
<!-- 数据源(MyBatis-Plus等,AI模型配置表需要) -->
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-log</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-oauth</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-ds</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<!-- WebFlux替代ServletLLM长连接不占用线程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- WebFlux版Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>${swagger.doc.version}</version>
</dependency>
<!-- Jackson序列化(WebFlux不自动引入,需显式声明) -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-sys-api</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-prometheus</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,73 @@
package cn.com.mfish.ai.agent;
import cn.com.mfish.common.ai.client.IClientAssistant;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.ChatResponseVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* AI助手注册中心
* 自动发现所有IClientAssistant实现按getPath()注册到路由映射表
* 用于单实例模式下替代Servlet forward直接调用助手实现真正的SSE流式传输
*
* @author: mfish
* @date: 2026/07/02
*/
@Slf4j
@Component
public class AssistantRegistry {
private static final String DEFAULT_PATH = "/ai/agent/chat";
private final Map<String, IClientAssistant> assistantMap;
private final IClientAssistant defaultAssistant;
public AssistantRegistry(@Autowired(required = false) List<IClientAssistant> assistants) {
if (assistants != null && !assistants.isEmpty()) {
this.assistantMap = assistants.stream()
.collect(Collectors.toMap(IClientAssistant::getPath, a -> a));
this.defaultAssistant = assistantMap.get(DEFAULT_PATH);
} else {
this.assistantMap = Map.of();
this.defaultAssistant = null;
}
if (!assistantMap.isEmpty()) {
log.info("[助手注册] 已注册AI助手: {}", assistantMap.keySet());
}
}
/**
* 根据路由路径获取助手
*
* @param path 网关路由路径/ai/agent/chat
* @return 对应的助手实例未找到时回退到默认助手
*/
public IClientAssistant getAssistant(String path) {
IClientAssistant assistant = assistantMap.get(path);
if (assistant != null) {
return assistant;
}
if (defaultAssistant != null) {
log.warn("[助手注册] 未找到路径[{}]对应的助手,回退到默认助手", path);
return defaultAssistant;
}
throw new IllegalStateException("无可用的AI助手,请检查IClientAssistant实现是否已注册");
}
/**
* 根据路由路径调用助手聊天
*
* @param path 网关路由路径
* @param aiRequest 聊天请求
* @return 流式聊天响应
*/
public Flux<ChatResponseVo> chat(String path, AiRequest aiRequest) {
return getAssistant(path).chat(aiRequest);
}
}
@@ -0,0 +1,72 @@
package cn.com.mfish.ai.agent;
import cn.com.mfish.ai.service.LlmModelRouter;
import cn.com.mfish.common.ai.client.IClientAssistant;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.ChatResponseVo;
import cn.com.mfish.common.core.utils.StringUtils;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import reactor.core.publisher.Flux;
import java.util.Objects;
/**
* @description: 系统中心助手
* <p>
* 按租户路由模型每次请求实时解析租户 LlmModelRouter 取该租户的ChatModel
* ChatModel按配置签名缓存共享不随租户增长并实时构建ChatClient
* 不缓存ChatClient避免为每个租户常驻一份系统提示词+advisor带来的内存膨胀
* <p>
* 线程安全ChatClient每次请求独立构建用完即弃无共享可变状态
* 模型在请求线程解析AuthInfoUtils不支持异步构建后的引用可安全用于reactive链
*
* @author: mfish
* @date: 2025/8/22
*/
public abstract class BaseAssistant implements IClientAssistant {
private final ChatMemory chatMemory;
private final LlmModelRouter llmModelRouter;
public BaseAssistant(ChatMemory chatMemory, LlmModelRouter llmModelRouter) {
this.llmModelRouter = llmModelRouter;
this.chatMemory = chatMemory;
}
/**
* 子类提供系统提示词
*/
protected abstract String getSystemPrompt();
/**
* 实时构建当前请求租户对应的ChatClient从路由取该租户ChatModel包装系统提示词与advisor
* 不缓存ChatClient构建本身轻量仅包装ChatModel与提示词字符串不重建HTTP客户端
* 实时构建可避免为每个租户常驻一份提示词内存必须在请求线程调用AuthInfoUtils不支持异步
*/
protected ChatClient getChatClient() {
String tenantId = llmModelRouter.currentTenantId();
return ChatClient.builder(llmModelRouter.getChatModel(tenantId))
.defaultSystem(getSystemPrompt())
.defaultAdvisors(new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
/**
* 聊天返回id
*
* @return 聊天信息
*/
@Override
public Flux<ChatResponseVo> chat(AiRequest aiRequest) {
return chat(aiRequest.getSessionId(), aiRequest.getMessage().getContent())
.filter(resp -> "STOP".equals(Objects.requireNonNull(resp.getResult()).getMetadata().getFinishReason())
|| StringUtils.isNotEmpty(resp.getResult().getOutput().getText()))
.map(resp -> new ChatResponseVo().setId(aiRequest.getId())
.setContent(Objects.requireNonNull(resp.getResult()).getOutput().getText())
.setFinishReason(resp.getResult().getMetadata().getFinishReason())
);
}
}
@@ -0,0 +1,108 @@
package cn.com.mfish.ai.agent;
import cn.com.mfish.ai.service.LlmModelRouter;
import cn.com.mfish.common.ai.entity.AiRouterVo;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.core.web.Result;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ResponseEntity;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.Objects;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
/**
* @description: 摸鱼小助手配置
* <p>
* 按租户路由模型每次请求实时解析租户 LlmModelRouter 取该租户的ChatModel
* ChatModel按配置签名缓存共享不随租户增长并实时构建ChatClient
* 不缓存ChatClient避免为每个租户常驻一份系统提示词+advisor带来的内存膨胀
*
* @author: mfish
* @date: 2025/8/21
*/
@Component
public class GatewayAssistant {
private static final String DEFAULT_PROMPT = "你好,简单介绍下摸鱼低代码";
private static final String SYSTEM_PROMPT = """
你是"摸鱼低代码"的路由小助手你会根据用户的问题判断用户需要访问的路由路径!
当有人问"摸鱼低代码"相关信息时实际是在问我们整个平台的信息
你只能返回路由路径不能返回其他内容
结果以json格式返回
返回格式{"path":"/ai/agent/chat","name":"摸鱼小助手","serviceId":"mf-ai"}
# 路由路径
1. /ai/agent/chat : 摸鱼小助手 默认路由 对应微服务mf-ai
2. /ai/sys/assist : 系统信息 对应微服务mf-ai 系统信息主要包括字典信息分类目录信息日志信息代码生成功能在线用户信息数据库连接信息数据源信息
3. /ai/oauth2/assist : 认证信息 对应微服务mf-ai 认证信息主要包括菜单信息组织信息角色信息帐号信息租户信息
4. /ai/nocode/assist : 低代码信息 对应微服务mf-ai 低代码信息主要包括自助大屏自助API组件管理公式信息
```
如果用户需要查询字典相关信息分类目录相关信息日志相关信息等系统信息返回系统中心助手的路由路径
如果用户需要查询菜单相关信息组织相关信息角色相关信息帐号相关信息租户相关信息等认证中心信息返回认证中心助手的路由路径
如果用户需要查询自助大屏自助API数据集可视化等低代码中心信息返回低代码中心助手的路由路径
```
注意
返回的路由路径必须上失眠列出来的路由路径
如果当前问题与路由路径无关分析下之前的问题是否是之前问题的补充如果是则返回上一次的路由路径
如果不知道用户需要访问的路由路径默认返回{"path":"/ai/agent/chat","name":"摸鱼小助手","serviceId":"mf-ai"}
路由路径不能包含空格
请使用中文回答
""";
private final ChatMemory chatMemory;
private final LlmModelRouter llmModelRouter;
public GatewayAssistant(ChatMemory chatMemory, LlmModelRouter llmModelRouter) {
this.chatMemory = chatMemory;
this.llmModelRouter = llmModelRouter;
}
/**
* 实时构建当前请求租户对应的ChatClient不缓存避免租户级提示词内存膨胀
* 必须在请求线程调用AuthInfoUtils不支持异步
*/
private ChatClient getChatClient() {
String tenantId = llmModelRouter.currentTenantId();
return ChatClient.builder(llmModelRouter.getChatModel(tenantId))
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
public Mono<Result<AiRouterVo>> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
String finalPrompt = prompt;
// 在请求线程解析租户并构建ChatClient避免在reactive异步线程中获取租户
ChatClient chatClient = getChatClient();
return Mono.fromCallable(() -> {
ResponseEntity<ChatResponse, AiRouterVo> responseEntity = chatClient.prompt()
.user(finalPrompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.call()
.responseEntity(AiRouterVo.class);
AiRouterVo vo = Objects.requireNonNullElseGet(responseEntity.entity(), AiRouterVo::new);
return Result.ok(vo);
})
.subscribeOn(Schedulers.boundedElastic());
}
}
@@ -0,0 +1,62 @@
package cn.com.mfish.ai.agent;
import cn.com.mfish.ai.service.LlmModelRouter;
import cn.com.mfish.common.core.utils.StringUtils;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
/**
* @description: 摸鱼小助手配置
* @author: mfish
* @date: 2025/8/21
*/
@Component
public class MfishAssistant extends BaseAssistant {
private static final String DEFAULT_PROMPT = "你好,简单介绍下摸鱼低代码";
public MfishAssistant(ChatMemory chatMemory, LlmModelRouter llmModelRouter) {
super(chatMemory, llmModelRouter);
}
@Override
protected String getSystemPrompt() {
return """
你是摸鱼低代码的小助手是一个可爱的傻白甜萝莉你会用可爱的语言和我聊天解决问题!
当有人问摸鱼低代码相关信息时实际是在问我们整个平台的信息
摸鱼低代码平台是一款致力于让开发像摸鱼一样轻松的低代码/无代码平台
我们希望打破技术门槛让程序员和非程序员都能快速构建业务系统提升效率释放创造力
这不仅是程序员偷闲时的效率神器更是职场小白的建站利器甚至是领导画原型的秘密武器
我可以扮演以下角色
1. 低代码开发人员
2. 非技术人员如产品经理业务分析师等
3. 技术支持人员
4. 客户服务
""";
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
@Override
public Flux<ChatResponse> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
return this.getChatClient().prompt().user(prompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.stream().chatResponse();
}
@Override
public String getPath() {
return "/ai/agent/chat";
}
}
@@ -0,0 +1,76 @@
package cn.com.mfish.ai.agent;
import cn.com.mfish.ai.service.LlmModelRouter;
import cn.com.mfish.common.core.utils.StringUtils;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
/**
* @description: 低代码中心助手
* @author: mfish
* @date: 2025/8/22
*/
@Component
public class NoCodeAssistant extends BaseAssistant {
/**
* 默认提示语
*/
private static final String DEFAULT_PROMPT = "你好,简单介绍下低代码中心助手";
/**
* 构造低代码助手初始化聊天客户端及系统提示词
*
* @param chatMemory 聊天记忆
*/
public NoCodeAssistant(ChatMemory chatMemory, LlmModelRouter llmModelRouter) {
super(chatMemory, llmModelRouter);
}
@Override
protected String getSystemPrompt() {
return """
你是低代码中心助手是一个可爱的傻白甜萝莉你会用可爱的语言和我聊天解决问题!
你主要辅助用户完成低代码中心的一些基础操作
你能够通过调用工具来完成低代码中心基础操作包括查询新增修改删除导入导出等
你主要辅助用户完成低代码中心相关模块的信息检索执行操作
低代码中心主要包含 自助大屏自助API组件管理公式信息
注意
1. 我需要你调用工具来帮我回答问题不要自己编造答案
2. 不要随意修改查询结果只返回工具返回的内容
3. 如果未找到相应工具告诉用户你执行不了这个操作
4. 如果工具参数缺失提示用户输入缺失参数引导用户输入有用信息
5. 请使用中文回答
""";
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
@Override
public Flux<ChatResponse> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
return this.getChatClient().prompt()
.system("你必须先调用工具,再基于工具结果回答问题")
.user(prompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.stream()
.chatResponse();
}
@Override
public String getPath() {
return "/ai/nocode/assist";
}
}
@@ -0,0 +1,95 @@
package cn.com.mfish.ai.agent;
import cn.com.mfish.ai.service.LlmModelRouter;
import cn.com.mfish.ai.tools.MenuTools;
import cn.com.mfish.ai.tools.UserTools;
import cn.com.mfish.common.core.utils.AuthInfoUtils;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.oauth.api.remote.RemoteUserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
/**
* @description: 认证中心助手
* @author: mfish
* @date: 2025/8/22
*/
@Slf4j
@Component
public class OauthAssistant extends BaseAssistant {
private static final String DEFAULT_PROMPT = "你好,简单介绍下认证中心助手";
@Resource
MenuTools menuTools;
@Resource
RemoteUserService remoteUserService;
public OauthAssistant(ChatMemory chatMemory, LlmModelRouter llmModelRouter) {
super(chatMemory, llmModelRouter);
}
@Override
protected String getSystemPrompt() {
return """
你是摸鱼低代码的认证中心助手是一个可爱的傻白甜萝莉你会用可爱的语言和我聊天解决问题!
当有人问摸鱼低代码相关信息时实际是在问我们整个平台的信息
你主要辅助用户完成认证中心的一些基础操作
你能够通过调用工具来完成认证中心基础操作包括查询新增修改删除导入导出等
你主要辅助用户完成认证中心相关模块的信息检索执行操作
认证中心主要包含 菜单信息组织信息角色信息帐号信息租户信息
注意
1. 我需要你调用工具来帮我回答问题不要自己编造答案
2. 不要随意修改查询结果只返回工具返回的内容
3. 如果未找到相应工具告诉用户你执行不了这个操作
4. 如果工具参数缺失提示用户输入缺失参数引导用户输入有用信息
5. 请使用中文回答
""";
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
@Override
public Flux<ChatResponse> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
String userId = AuthInfoUtils.getCurrentUserId();
String tenantId = AuthInfoUtils.getCurrentTenantId();
String finalPrompt = prompt;
ChatClient.ChatClientRequestSpec requestSpec = this.getChatClient().prompt()
.system("你必须先调用工具,再基于工具结果回答问题")
.user(finalPrompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.tools(menuTools, new UserTools(userId, tenantId, remoteUserService));
return requestSpec
.stream()
.chatResponse()
.onErrorResume(e -> {
log.warn("流式调用失败,降级为非流式调用: {}", e.getMessage());
return Mono.fromCallable(() -> requestSpec.call().chatResponse())
.subscribeOn(Schedulers.boundedElastic())
.flux();
});
}
@Override
public String getPath() {
return "/ai/oauth2/assist";
}
}
@@ -0,0 +1,87 @@
package cn.com.mfish.ai.agent;
import cn.com.mfish.ai.service.LlmModelRouter;
import cn.com.mfish.ai.tools.DictTools;
import cn.com.mfish.common.core.utils.StringUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
/**
* @description: 系统中心助手
* @author: mfish
* @date: 2025/8/22
*/
@Slf4j
@Component
public class SysAssistant extends BaseAssistant {
private static final String DEFAULT_PROMPT = "你好,简单介绍下系统中心助手";
@Resource
private DictTools dictTools;
public SysAssistant(ChatMemory chatMemory, LlmModelRouter llmModelRouter) {
super(chatMemory, llmModelRouter);
}
@Override
protected String getSystemPrompt() {
return """
你是摸鱼低代码的系统中心助手是一个可爱的傻白甜萝莉你会用可爱的语言和我聊天解决问题!
当有人问摸鱼低代码相关信息时实际是在问我们整个平台的信息
你主要辅助用户完成系统中心的一些基础操作
你能够通过调用工具来完成系统中心基础操作包括查询新增修改删除等
你主要辅助用户完成系统中心相关模块的信息检索执行操作
系统中心主要包含 字典信息 分类目录信息 日志信息 代码生成功能 在线用户信息 数据库连接信息 数据源信息
注意
1. 我需要你调用工具来帮我回答问题不要自己编造答案
2. 不要随意修改查询结果只返回工具返回的内容
3. 如果未找到相应工具告诉用户你执行不了这个操作
4. 如果工具参数缺失提示用户输入缺失参数引导用户输入有用信息
5. 请使用中文回答
""";
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
@Override
public Flux<ChatResponse> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
String finalPrompt = prompt;
ChatClient.ChatClientRequestSpec requestSpec = this.getChatClient().prompt()
.system("你必须先调用工具,再基于工具结果回答问题")
.user(finalPrompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.tools(dictTools);
return
requestSpec.stream().chatResponse()
.onErrorResume(e -> {
log.warn("流式调用失败,降级为非流式调用: {}", e.getMessage());
return Mono.fromCallable(() -> requestSpec.call().chatResponse())
.subscribeOn(Schedulers.boundedElastic())
.flux();
});
}
@Override
public String getPath() {
return "/ai/sys/assist";
}
}
@@ -0,0 +1,54 @@
package cn.com.mfish.ai.config;
import cn.com.mfish.common.core.utils.ServletUtils;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.scheduler.Schedulers;
/**
* @description: Reactor上下文传播配置
* 解决WebFlux响应式编程中subscribeOn/publishOn切换线程导致ThreadLocal丢失的问题
* <p>
* 支持两种运行模式
* 1. WebFlux模式传播 ServerWebExchange ThreadLocal
* 2. Servlet单实例模式传播 RequestContextHolder ThreadLocal
* </p>
* @author: mfish
* @date: 2026/7/6
*/
@Configuration
public class ReactorContextPropagationConfig {
private static final String SCHEDULE_HOOK_KEY = "mfish.context.propagation";
@PostConstruct
public void init() {
Schedulers.onScheduleHook(SCHEDULE_HOOK_KEY, runnable -> {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServerWebExchange exchange = ServletUtils.getExchange();
if (requestAttributes == null && exchange == null) {
return runnable;
}
return () -> {
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes);
}
if (exchange != null) {
ServletUtils.setExchange(exchange);
}
try {
runnable.run();
} finally {
if (requestAttributes != null) {
RequestContextHolder.resetRequestAttributes();
}
if (exchange != null) {
ServletUtils.removeExchange();
}
}
};
});
}
}
@@ -0,0 +1,36 @@
package cn.com.mfish.ai.controller;
import cn.com.mfish.ai.agent.MfishAssistant;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.ChatResponseVo;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
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 reactor.core.publisher.Flux;
/**
* @description: 摸鱼小助手
* @author: mfish
* @date: 2026/7/2
*/
@Tag(name = "摸鱼小助手")
@RestController
@RequestMapping("/agent")
@RequiredArgsConstructor
public class AiController {
private final MfishAssistant mfishAssistant;
/**
* 与摸鱼小助手聊天
*
* @param aiRequest AI请求参数包含会话ID和提示词
* @return 聊天响应流
*/
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatResponseVo> chat(@RequestBody AiRequest aiRequest) {
return mfishAssistant.chat(aiRequest);
}
}
@@ -0,0 +1,165 @@
package cn.com.mfish.ai.controller;
import cn.com.mfish.ai.api.entity.AiModelConfig;
import cn.com.mfish.ai.api.req.ReqAiModelConfig;
import cn.com.mfish.ai.api.vo.AiModelConfigVo;
import cn.com.mfish.common.ai.service.AiModelConfigService;
import cn.com.mfish.common.core.annotation.InnerUser;
import cn.com.mfish.common.core.enums.OperateType;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.log.annotation.Log;
import cn.com.mfish.common.oauth.annotation.DataScope;
import cn.com.mfish.common.oauth.annotation.RequiresPermissions;
import cn.com.mfish.common.oauth.common.DataScopeType;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
/**
* @description: AI模型配置信息
* @author: mfish
* @date: 2026-07-03
* @version: V2.4.0
*/
@Slf4j
@Tag(name = "AI模型配置信息")
@RestController
@RequestMapping("/aiModelConfig")
public class AiModelConfigController {
@Resource
private AiModelConfigService aiModelConfigService;
/**
* 分页列表查询脱敏处理
*
* @param reqAiModelConfig AI模型配置信息请求参数
* @param reqPage 分页参数
* @return 返回AI模型配置信息-分页列表
*/
@Operation(summary = "AI模型配置信息-分页列表查询", description = "AI模型配置信息-分页列表查询")
@GetMapping
@RequiresPermissions("ai:aiModelConfig:query")
@DataScope(table = "ai_model_config", type = DataScopeType.Tenant, excludes = {"tenant_id = '1'"}, superIgnore = true)
public Result<PageResult<AiModelConfigVo>> queryPageList(ReqAiModelConfig reqAiModelConfig, ReqPage reqPage) {
return aiModelConfigService.queryPageList(reqAiModelConfig, reqPage);
}
/**
* 内部列表查询不脱敏
*
* @param reqAiModelConfig AI模型配置信息请求参数
* @return 返回AI模型配置信息-列表
*/
@Operation(summary = "AI模型配置信息-列表查询", description = "AI模型配置信息-列表查询")
@GetMapping("/list")
@InnerUser
public Result<List<AiModelConfig>> queryList(ReqAiModelConfig reqAiModelConfig) {
return Result.ok(aiModelConfigService.queryList(reqAiModelConfig));
}
/**
* 添加
*
* @param aiModelConfig AI模型配置信息对象
* @return 返回AI模型配置信息-添加结果
*/
@Log(title = "AI模型配置信息-添加", operateType = OperateType.INSERT)
@Operation(summary = "AI模型配置信息-添加")
@PostMapping
@RequiresPermissions("ai:aiModelConfig:insert")
public Result<AiModelConfig> add(@RequestBody AiModelConfig aiModelConfig) {
return aiModelConfigService.add(aiModelConfig);
}
/**
* 编辑
*
* @param aiModelConfig AI模型配置信息对象
* @return 返回AI模型配置信息-编辑结果
*/
@Log(title = "AI模型配置信息-编辑", operateType = OperateType.UPDATE)
@Operation(summary = "AI模型配置信息-编辑")
@PutMapping
@RequiresPermissions("ai:aiModelConfig:update")
public Result<AiModelConfig> edit(@RequestBody AiModelConfig aiModelConfig) {
return aiModelConfigService.edit(aiModelConfig);
}
/**
* 通过id删除
*
* @param id 唯一ID
* @return 返回AI模型配置信息-删除结果
*/
@Log(title = "AI模型配置信息-通过id删除", operateType = OperateType.DELETE)
@Operation(summary = "AI模型配置信息-通过id删除")
@DeleteMapping("/{id}")
@RequiresPermissions("ai:aiModelConfig:delete")
public Result<Boolean> delete(@Parameter(name = "id", description = "唯一性ID") @PathVariable String id) {
return aiModelConfigService.delete(id);
}
/**
* 批量删除
*
* @param ids 批量ID
* @return 返回AI模型配置信息-删除结果
*/
@Log(title = "AI模型配置信息-批量删除", operateType = OperateType.DELETE)
@Operation(summary = "AI模型配置信息-批量删除")
@DeleteMapping("/batch/{ids}")
@RequiresPermissions("ai:aiModelConfig:delete")
public Result<Boolean> deleteBatch(@Parameter(name = "ids", description = "唯一性ID") @PathVariable String ids) {
return aiModelConfigService.deleteBatch(ids);
}
/**
* 通过id查询不脱敏
*
* @param id 唯一ID
* @return 返回AI模型配置信息对象
*/
@Operation(summary = "AI模型配置信息-通过id查询")
@GetMapping("/{id}")
@InnerUser
public Result<AiModelConfig> queryById(@Parameter(name = "id", description = "唯一性ID") @PathVariable String id) {
return aiModelConfigService.queryById(id);
}
/**
* 通过id查询脱敏处理
*
* @param id 唯一ID
* @return 返回AI模型配置信息对象
*/
@Operation(summary = "AI模型配置信息-通过id查询")
@GetMapping("/mask/{id}")
@RequiresPermissions("ai:aiModelConfig:query")
@DataScope(table = "ai_model_config", type = DataScopeType.Tenant, excludes = {"tenant_id = '1'"}, superIgnore = true)
public Result<AiModelConfigVo> queryByIdVo(@Parameter(name = "id", description = "唯一性ID") @PathVariable String id) {
return aiModelConfigService.queryByIdVo(id);
}
/**
* 导出
*
* @param reqAiModelConfig AI模型配置信息请求参数
* @param reqPage 分页参数
* @throws IOException IO异常
*/
@Operation(summary = "导出AI模型配置信息", description = "导出AI模型配置信息")
@GetMapping("/export")
@RequiresPermissions("ai:aiModelConfig:export")
public void export(ReqAiModelConfig reqAiModelConfig, ReqPage reqPage) throws IOException {
aiModelConfigService.export(reqAiModelConfig, reqPage);
}
}
@@ -8,8 +8,8 @@ import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.PostMapping;
@@ -31,13 +31,6 @@ import java.util.UUID;
/**
* LLM统一代理控制器WebFlux版
* 对外提供OpenAI兼容的 /v1/chat/completions 接口
* <p>
* 改进
* 1. 多模型路由 + fallback链通过LlmModelRouter
* 2. 使用Spring AI Message列表保持对话结构
* 3. 使用Jackson ObjectMapper序列化替代手工拼接JSON
* 4. 支持流式SSE和非流式JSON双模式
* 5. WebFlux非Servlet架构LLM长连接不占用Servlet线程
*
* @author: mfish
* @date: 2026/07/01
@@ -116,8 +109,7 @@ public class LlmProxyController {
.map(chatResponse -> buildResponse(completionId, model, chatResponse))
.onErrorResume(ex -> {
log.error("[LLM代理] 同步调用失败", ex);
return Mono.just(buildErrorResponse(completionId, model,
"AI服务暂时不可用,请稍后再试。"));
return Mono.just(buildResponse(completionId, model, "AI服务暂时不可用,请稍后再试。"));
})
.map(resp -> response.bufferFactory()
.wrap(writeJson(resp).getBytes(StandardCharsets.UTF_8)))
@@ -147,7 +139,7 @@ public class LlmProxyController {
}
private Prompt buildPrompt(List<Message> messages, ChatCompletionDto.Request request) {
OpenAiChatOptions options = OpenAiChatOptions.builder()
ChatOptions options = ChatOptions.builder()
.model(request.getModel() != null ? request.getModel() : "default")
.maxTokens(request.getMaxTokens() != null ? request.getMaxTokens() : 1000)
.temperature(request.getTemperature() != null ? request.getTemperature() : 0.7)
@@ -173,36 +165,22 @@ public class LlmProxyController {
}
private ChatCompletionDto.Response buildResponse(String id, String model, ChatResponse chatResponse) {
ChatCompletionDto.Response response = new ChatCompletionDto.Response();
response.setId(id);
response.setCreated(Instant.now().getEpochSecond());
response.setModel(model != null ? model : "default");
ChatCompletionDto.ResponseChoice choice = new ChatCompletionDto.ResponseChoice();
choice.setIndex(0);
ChatCompletionDto.ResponseMessage message = new ChatCompletionDto.ResponseMessage();
message.setContent(extractContent(chatResponse));
choice.setMessage(message);
choice.setFinishReason("stop");
response.setChoices(List.of(choice));
ChatCompletionDto.Response response = buildResponse(id, model, extractContent(chatResponse));
// 尝试提取usage信息
try {
if (chatResponse.getMetadata() != null && chatResponse.getMetadata().getUsage() != null) {
var usageData = chatResponse.getMetadata().getUsage();
ChatCompletionDto.Usage usage = new ChatCompletionDto.Usage();
usage.setPromptTokens(usageData.getPromptTokens().intValue());
usage.setCompletionTokens(usageData.getCompletionTokens().intValue());
usage.setTotalTokens(usageData.getTotalTokens().intValue());
response.setUsage(usage);
}
var usageData = chatResponse.getMetadata().getUsage();
ChatCompletionDto.Usage usage = new ChatCompletionDto.Usage();
usage.setPromptTokens(usageData.getPromptTokens());
usage.setCompletionTokens(usageData.getCompletionTokens());
usage.setTotalTokens(usageData.getTotalTokens());
response.setUsage(usage);
} catch (Exception e) {
log.debug("[LLM代理] 获取usage信息失败,跳过", e);
}
return response;
}
private ChatCompletionDto.Response buildErrorResponse(String id, String model, String errorMessage) {
private ChatCompletionDto.Response buildResponse(String id, String model, String message) {
ChatCompletionDto.Response response = new ChatCompletionDto.Response();
response.setId(id);
response.setCreated(Instant.now().getEpochSecond());
@@ -210,9 +188,9 @@ public class LlmProxyController {
ChatCompletionDto.ResponseChoice choice = new ChatCompletionDto.ResponseChoice();
choice.setIndex(0);
ChatCompletionDto.ResponseMessage message = new ChatCompletionDto.ResponseMessage();
message.setContent(errorMessage);
choice.setMessage(message);
ChatCompletionDto.ResponseMessage responseMessage = new ChatCompletionDto.ResponseMessage();
responseMessage.setContent(message);
choice.setMessage(responseMessage);
choice.setFinishReason("stop");
response.setChoices(List.of(choice));
return response;
@@ -0,0 +1,38 @@
package cn.com.mfish.ai.controller;
import cn.com.mfish.ai.agent.NoCodeAssistant;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.ChatResponseVo;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
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 reactor.core.publisher.Flux;
/**
* @description: 低代码中心助手
* @author: mfish
* @date: 2025/8/22
*/
@Tag(name = "低代码中心助手")
@RestController
@RequestMapping("/nocode")
@RequiredArgsConstructor
public class NoCodeAiController {
private final NoCodeAssistant noCodeAssistant;
/**
* 低代码AI助手对话接口流式响应
*
* @param aiRequest AI请求参数
* @return 返回流式对话响应
*/
@PostMapping(value = "/assist", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatResponseVo> noCodeAssistant(@RequestBody AiRequest aiRequest) {
return noCodeAssistant.chat(aiRequest);
}
}
@@ -1,8 +1,8 @@
package cn.com.mfish.oauth.controller;
package cn.com.mfish.ai.controller;
import cn.com.mfish.ai.agent.OauthAssistant;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.ChatResponseVo;
import cn.com.mfish.oauth.agent.OauthAssistant;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
@@ -19,7 +19,7 @@ import reactor.core.publisher.Flux;
*/
@Tag(name = "认证小助手")
@RestController
@RequestMapping("/ai")
@RequestMapping("/oauth2")
@RequiredArgsConstructor
public class OauthAiController {
@@ -0,0 +1,65 @@
package cn.com.mfish.ai.controller;
import cn.com.mfish.ai.agent.AssistantRegistry;
import cn.com.mfish.ai.service.AiRouteService;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.AiRouterVo;
import cn.com.mfish.common.ai.entity.ChatResponseVo;
import cn.com.mfish.common.prom.annotation.MetricsMonitor;
import cn.com.mfish.common.prom.annotation.MetricsMonitors;
import cn.com.mfish.common.prom.enums.MetricEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
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 reactor.core.publisher.Flux;
/**
* @description: 路由控制器
* @author: mfish
* @date: 2026/7/6
*/
@Tag(name = "路由控制器")
@RestController
@RequestMapping("/router")
@RequiredArgsConstructor
@Slf4j
public class RouterController {
@Resource
AiRouteService aiRouteService;
@Resource
AssistantRegistry assistantRegistry;
/**
* AI智能路由单实例模式
* 同步阻塞等待AI路由决策完成后通过AssistantRegistry直接调用目标助手
* 返回Flux<ChatResponseVo>由Spring MVC原生处理SSE流式响应
* 避免Servlet forward缓冲整个响应导致流式失效的问题
*/
@Operation(summary = "Ai路由", description = "Ai路由")
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@MetricsMonitors({
@MetricsMonitor(metricEnum = MetricEnum.MFISH_REQUEST_COUNT, tagValues = {"POST", "/ai/router"}),
@MetricsMonitor(metricEnum = MetricEnum.MFISH_REQUEST_DURATION, tagValues = {"POST", "/ai/router"})
})
public Flux<ChatResponseVo> aiRouter(@RequestBody AiRequest aiRequest) {
String sessionId = aiRequest.getSessionId();
String prompt = aiRequest.getMessage() != null ? aiRequest.getMessage().getContent() : null;
log.info("[AI路由-单实例] 接收到智能路由请求, sessionId={}, prompt={}", sessionId, prompt);
return aiRouteService.resolve(sessionId, prompt)
.defaultIfEmpty(new AiRouterVo())
.flatMapMany(vo -> {
String targetPath = vo.getPath();
log.info("[AI路由-单实例] 路由到, path={}", targetPath);
return assistantRegistry.chat(targetPath, aiRequest);
});
}
}
@@ -1,9 +1,8 @@
package cn.com.mfish.sys.controller;
package cn.com.mfish.ai.controller;
import cn.com.mfish.ai.agent.SysAssistant;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.ChatResponseVo;
import cn.com.mfish.sys.agent.MfishAssistant;
import cn.com.mfish.sys.agent.SysAssistant;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
@@ -20,25 +19,12 @@ import reactor.core.publisher.Flux;
*/
@Tag(name = "系统小助手")
@RestController
@RequestMapping("/ai")
@RequestMapping("/sys")
@RequiredArgsConstructor
public class SysAiController {
private final MfishAssistant mfishAssistant;
private final SysAssistant sysAssistant;
/**
* 与摸鱼小助手聊天
*
* @param aiRequest AI请求参数包含会话ID和提示词
* @return 聊天响应流
*/
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatResponseVo> chat(@RequestBody AiRequest aiRequest) {
return mfishAssistant.chat(aiRequest);
}
/**
* 与系统中心助手聊天
*
@@ -0,0 +1,15 @@
package cn.com.mfish.ai.event;
import org.springframework.context.ApplicationEvent;
/**
* 数据库中的AI模型配置发生变更新增/编辑/删除时发布此事件
* 用于触发 {@code LlmModelRouter} 重新加载模型
*
* @author mfish
*/
public class AiModelChangedEvent extends ApplicationEvent {
public AiModelChangedEvent(Object source) {
super(source);
}
}
@@ -0,0 +1,40 @@
package cn.com.mfish.ai.filter;
import cn.com.mfish.common.core.utils.ServletUtils;
import org.jspecify.annotations.NonNull;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* @description: 请求上下文过滤器WebFlux模式
* 负责将ServerWebExchange写入ThreadLocal
* 线程切换时的传播由ReactorContextPropagationConfig统一处理
* @author: mfish
* @date: 2026/7/3
*/
@Component
public class ReactiveRequestContextFilter implements WebFilter, Ordered {
@Override
public @NonNull Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (isExcludedPath(path)) {
return chain.filter(exchange);
}
ServletUtils.setExchange(exchange);
return chain.filter(exchange)
.doFinally(signal -> ServletUtils.removeExchange());
}
private boolean isExcludedPath(String path) {
return path.startsWith("/actuator") || path.startsWith("/health");
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
@@ -0,0 +1,14 @@
package cn.com.mfish.ai.mapper;
import cn.com.mfish.ai.api.entity.AiModelConfig;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @description: AI模型配置信息
* @author: mfish
* @date: 2026-07-03
* @version: V2.4.0
*/
public interface AiModelConfigMapper extends BaseMapper<AiModelConfig> {
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.com.mfish.ai.mapper.AiModelConfigMapper">
</mapper>
@@ -1,10 +1,9 @@
package cn.com.mfish.common.ai.service;
package cn.com.mfish.ai.service;
import cn.com.mfish.common.ai.agent.GatewayAssistant;
import cn.com.mfish.ai.agent.GatewayAssistant;
import cn.com.mfish.common.ai.entity.AiRouterVo;
import cn.com.mfish.common.core.constants.ServiceConstants;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.core.web.Result;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
@@ -59,7 +58,7 @@ public class AiRouteService {
AiRouterVo cached = routeCache.getIfPresent(prompt);
if (cached != null) {
// 缓存结果也需校验防止历史缓存中存在无效serviceId
cached = validateAndFix(cached);
validateAndFix(cached);
log.info("[AI路由] 命中缓存, serviceId={}, path={}", cached.getServiceId(), cached.getPath());
return Mono.just(cached);
}
@@ -74,7 +73,7 @@ public class AiRouteService {
log.warn("[AI路由] LLM返回空结果,使用默认路由");
vo = new AiRouterVo();
}
vo = validateAndFix(vo);
validateAndFix(vo);
log.info("[AI路由] LLM决策结果, serviceId={}, path={}, name={}", vo.getServiceId(), vo.getPath(), vo.getName());
routeCache.put(finalPrompt, vo);
return vo;
@@ -89,12 +88,11 @@ public class AiRouteService {
* 校验LLM返回的路由结果修正无效的serviceId
* LLM可能幻觉出不存在的服务名如mf-ai需要校验并回退到默认值
*/
private AiRouterVo validateAndFix(AiRouterVo vo) {
private void validateAndFix(AiRouterVo vo) {
if (!ServiceConstants.MfService.isValid(vo.getServiceId())) {
log.warn("[AI路由] LLM返回无效serviceId='{}',不在有效枚举{}中,回退到默认值{}",
vo.getServiceId(), ServiceConstants.MfService.allServiceIds(), ServiceConstants.MfService.SYS.getValue());
vo.setServiceId(ServiceConstants.MfService.SYS.getValue());
}
return vo;
}
}
@@ -1,31 +1,36 @@
package cn.com.mfish.ai.service;
import cn.com.mfish.ai.api.entity.AiModelConfig;
import cn.com.mfish.ai.api.req.ReqAiModelConfig;
import cn.com.mfish.ai.event.AiModelChangedEvent;
import cn.com.mfish.common.ai.service.AiModelConfigService;
import cn.com.mfish.common.ai.service.LlmModelFactory;
import cn.com.mfish.common.core.exception.MyRuntimeException;
import cn.com.mfish.common.core.utils.AuthInfoUtils;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* LLM多模型路由服务
* <p>
* 通过ApplicationContext自动发现所有ChatModel BeanBean名称自动转提供者名称
* - "openAiChatModel" "openai"
* - "ollamaChatModel" "ollama"
* - "zhiPuAiChatModel" "zhipuai"
* 按租户维度路由数据库中 tenantId 为空的配置属于全局模型作为未配置专属模型租户的兜底
* 每个租户拥有独立的ChatModel列表按sortOrder决定fallback优先级
* <p>
* 模型名提供者的映射支持两种来源
* 1. 内置前缀规则默认兜底
* 2. 数据库配置动态刷新优先级更高
* 创建性能优化通过配置签名缓存已构建的ChatModel重建时仅构建新增/变更的配置
* 未变更的ChatModel直接复用避免重复创建HTTP客户端等昂贵对象
* <p>
* fallback顺序优先使用模型名对应的提供者失败后按注册顺序依次降级
* 线程安全租户模型表为 volatile 不可变快照读操作无锁重建整体替换引用
*
* @author: mfish
* @date: 2026/07/01
@@ -33,190 +38,189 @@ import java.util.concurrent.ConcurrentHashMap;
@Service
@Slf4j
public class LlmModelRouter {
private final ChatModel openaiChatModel;
private final AiModelConfigService aiModelConfigService;
/**
* 租户 该租户可用的ChatModel列表不可变快照
* 通过 volatile 发布读操作无锁且线程安全写操作整体替换引用
*/
private volatile Map<String, List<ChatModel>> tenantProviders = Map.of();
/**
* 配置签名 已构建ChatModel 的缓存仅在 rebuildLock 内访问
* 重建时优先复用未变更配置对应的ChatModel避免重复构建HTTP客户端等昂贵对象
* 被发布到快照的ChatModel对象即使后续从缓存移除也不影响正在使用它的请求它们持有对象引用
*/
private final Map<String, ChatModel> modelCache = new HashMap<>();
/**
* 串行化重建动作读操作不获取此锁因此重建期间读取线程仍可使用旧快照不受阻塞
*/
private final ReentrantLock rebuildLock = new ReentrantLock();
/** Bean名称后缀 → 提供者名称的转换规则 */
private static final String BEAN_SUFFIX = "ChatModel";
public LlmModelRouter(ChatModel openAiChatModel, AiModelConfigService aiModelConfigService) {
this.openaiChatModel = openAiChatModel;
this.aiModelConfigService = aiModelConfigService;
}
/** 提供者名称 → ChatModel实例 */
private final Map<String, ChatModel> providers = new LinkedHashMap<>();
/** fallback顺序(按Bean注册顺序) */
private final List<String> fallbackOrder;
/** 模型名前缀 → 提供者名称的映射(内置默认规则) */
private final Map<String, String> defaultPrefixMap = new LinkedHashMap<>();
/** 模型名前缀 → 提供者名称的映射(数据库动态配置,优先级高于默认) */
private final Map<String, String> dynamicPrefixMap = new ConcurrentHashMap<>();
public LlmModelRouter(ApplicationContext applicationContext) {
// 自动发现所有ChatModel Bean
Map<String, ChatModel> beans = applicationContext.getBeansOfType(ChatModel.class);
for (Map.Entry<String, ChatModel> entry : beans.entrySet()) {
String providerName = beanNameToProvider(entry.getKey());
this.providers.put(providerName, entry.getValue());
log.info("[LLM路由] 注册模型提供者: {} ({})", providerName, entry.getKey());
}
this.fallbackOrder = List.copyOf(this.providers.keySet());
// 内置默认前缀规则
initDefaultPrefixMap();
log.info("[LLM路由] 已注册提供者: {}, fallback顺序: {}", providers.keySet(), fallbackOrder);
@PostConstruct
public void initProviders() {
doInitProviders();
}
/**
* Bean名称转提供者名称
* "openAiChatModel" "openai"
* "ollamaChatModel" "ollama"
* "zhiPuAiChatModel" "zhipuai"
* 监听数据库模型配置变更事件重新加载模型
* 助手不再缓存ChatClient每请求实时构建因此重载后无需通知助手刷新
* 下次请求自动取到新的ChatModel
*/
private String beanNameToProvider(String beanName) {
if (beanName.endsWith(BEAN_SUFFIX)) {
beanName = beanName.substring(0, beanName.length() - BEAN_SUFFIX.length());
@EventListener
public void onModelChanged(AiModelChangedEvent event) {
log.info("[LLM路由] 收到模型变更事件,开始重新初始化");
try {
doInitProviders();
} catch (Exception e) {
log.error("[LLM路由] 重新初始化失败,保持现有模型配置", e);
return;
}
// 驼峰转小写首字母小写
if (beanName.isEmpty()) {
return "default";
}
// 将驼峰转为下划线再连成小写openAi open_ai openai
StringBuilder sb = new StringBuilder();
for (int i = 0; i < beanName.length(); i++) {
char c = beanName.charAt(i);
if (Character.isUpperCase(c)) {
if (i > 0) {
sb.append('_');
log.info("[LLM路由] 模型重新初始化完成");
}
private void doInitProviders() {
rebuildLock.lock();
try {
List<AiModelConfig> configs = aiModelConfigService.queryList(new ReqAiModelConfig().setEnabled(1));
Map<String, List<ChatModel>> newMap = new HashMap<>();
Set<String> usedSignatures = new HashSet<>();
if (configs != null) {
for (AiModelConfig config : configs) {
if (1 != config.getEnabled()) {
continue;
}
String signature = signatureOf(config);
if (!usedSignatures.add(signature)) {
continue; // 同签名去重
}
try {
// 签名命中缓存则复用已构建的ChatModel避免重复创建昂贵的HTTP客户端
ChatModel chatModel = modelCache.computeIfAbsent(signature, k -> buildModel(config));
newMap.computeIfAbsent(config.getTenantId(), k -> new ArrayList<>()).add(chatModel);
log.info("[LLM路由] 注册: tenant={}, protocol={}, provider={}, model={}",
config.getTenantId(), config.getProtocol(), config.getProvider(), config.getModelName());
} catch (Exception e) {
log.error("[LLM路由] 注册失败: tenant={}, protocol={}, provider={}, model={}",
config.getTenantId(), config.getProtocol(), config.getProvider(), config.getModelName(), e);
}
}
sb.append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString().replace("_", "");
}
/**
* 初始化内置默认前缀规则
* 这些规则在数据库没有配置时作为兜底使用
*/
private void initDefaultPrefixMap() {
// Ollama常见模型
defaultPrefixMap.put("llama", "ollama");
defaultPrefixMap.put("mistral", "ollama");
defaultPrefixMap.put("phi", "ollama");
defaultPrefixMap.put("gemma", "ollama");
// OpenAI兼容API常见模型
defaultPrefixMap.put("gpt", "openai");
defaultPrefixMap.put("qwen", "openai");
defaultPrefixMap.put("deepseek", "openai");
defaultPrefixMap.put("o1", "openai");
defaultPrefixMap.put("o3", "openai");
defaultPrefixMap.put("chatglm", "zhipuai");
}
/**
* 刷新数据库动态映射配置
* 由外部定时任务或配置变更事件调用
*
* @param prefixMap 模型名前缀 提供者名称的映射
*/
public void refreshDynamicPrefixMap(Map<String, String> prefixMap) {
dynamicPrefixMap.clear();
if (prefixMap != null) {
dynamicPrefixMap.putAll(prefixMap);
}
log.info("[LLM路由] 刷新动态映射配置: {}", dynamicPrefixMap);
}
/**
* 根据模型名解析提供者名称
* 优先级动态配置 > 内置默认规则 > 首个注册的提供者
*/
public String resolveProvider(String model) {
if (model == null || model.isEmpty()) {
return fallbackOrder.getFirst();
}
String lower = model.toLowerCase();
// 1. 优先查动态配置数据库
for (Map.Entry<String, String> entry : dynamicPrefixMap.entrySet()) {
if (lower.startsWith(entry.getKey()) && providers.containsKey(entry.getValue())) {
return entry.getValue();
// 构建不可变快照
Map<String, List<ChatModel>> snapshot = new HashMap<>();
for (Map.Entry<String, List<ChatModel>> e : newMap.entrySet()) {
snapshot.put(e.getKey(), List.copyOf(e.getValue()));
}
}
// 2. 兜底查内置默认规则
for (Map.Entry<String, String> entry : defaultPrefixMap.entrySet()) {
if (lower.startsWith(entry.getKey()) && providers.containsKey(entry.getValue())) {
return entry.getValue();
// 清理不再使用的缓存项释放已删除配置占用的资源
modelCache.keySet().retainAll(usedSignatures);
tenantProviders = Map.copyOf(snapshot);
if (snapshot.isEmpty()) {
log.warn("[LLM路由] 数据库无启用的模型配置");
}
} finally {
rebuildLock.unlock();
}
// 3. 直接匹配提供者名称
if (providers.containsKey(lower)) {
return lower;
}
// 4. 默认返回首个提供者
return fallbackOrder.getFirst();
}
private ChatModel buildModel(AiModelConfig config) {
return LlmModelFactory.create(
config.getProtocol(), config.getApiKey(),
config.getBaseUrl(), config.getModelName(),
config.getMaxTokens(), config.getTemperature());
}
/**
* 获取所有已注册的提供者名称
* 配置签名由影响ChatModel构建的字段组成签名不变则复用已构建的ChatModel
* 使用加密后的apiKey与解密后一一对应避免明文密钥出现在签名串中
*/
public Set<String> getProviderNames() {
return Collections.unmodifiableSet(providers.keySet());
private String signatureOf(AiModelConfig config) {
return config.getProtocol() + "|" + config.getModelName() + "|" + config.getBaseUrl()
+ "|" + config.getApiKey() + "|" + config.getMaxTokens() + "|" + config.getTemperature();
}
/**
* 流式调用支持fallback链
* 安全获取当前请求租户ID无请求上下文如启动期构造时返回 null回退到全局模型
* 注意必须在请求线程调用不能在reactive异步线程中调用
*/
public String currentTenantId() {
try {
return AuthInfoUtils.getCurrentTenantId();
} catch (Exception e) {
return AuthInfoUtils.SUPER_TENANT_ID;
}
}
/**
* 获取指定租户的ChatModel租户专属模型 全局模型 默认模型
*/
public ChatModel getChatModel(String tenantId) {
List<ChatModel> list = resolveProviders(tenantId);
return list.isEmpty() ? openaiChatModel : list.getFirst();
}
/**
* 解析租户可用的provider链优先租户专属为空则回退全局
*/
private List<ChatModel> resolveProviders(String tenantId) {
Map<String, List<ChatModel>> snapshot = tenantProviders;
List<ChatModel> list = snapshot.get(tenantId);
if (list == null || list.isEmpty()) {
list = snapshot.get(AuthInfoUtils.SUPER_TENANT_ID);
}
return list == null ? List.of() : list;
}
/**
* 流式调用支持fallback链租户在请求线程解析后传入链路避免在reactive异步线程中获取租户
*/
public Flux<ChatResponse> streamWithFallback(Prompt prompt, String model) {
String primaryProvider = resolveProvider(model);
List<String> chain = buildFallbackChain(primaryProvider);
return tryStreamChain(prompt, chain, 0, model);
List<ChatModel> providers = resolveProviders(currentTenantId());
return tryStreamChain(prompt, 0, model, providers);
}
/**
* 同步调用支持fallback链
* 同步调用支持fallback链租户在请求线程解析后传入链路
*/
public Mono<ChatResponse> callWithFallback(Prompt prompt, String model) {
String primaryProvider = resolveProvider(model);
List<String> chain = buildFallbackChain(primaryProvider);
return tryCallChain(prompt, chain, 0);
List<ChatModel> providers = resolveProviders(currentTenantId());
return tryCallChain(prompt, 0, model, providers);
}
private List<String> buildFallbackChain(String primaryProvider) {
List<String> chain = new ArrayList<>();
chain.add(primaryProvider);
for (String provider : fallbackOrder) {
if (!chain.contains(provider)) {
chain.add(provider);
}
}
return chain;
}
private Flux<ChatResponse> tryStreamChain(Prompt prompt, List<String> chain, int index, String model) {
if (index >= chain.size()) {
return Flux.error(new RuntimeException("所有模型均不可用"));
}
String provider = chain.get(index);
log.info("[LLM路由] 流式调用, provider={}, model={}", provider, model);
return providers.get(provider).stream(prompt)
private Flux<ChatResponse> tryStreamChain(Prompt prompt, int index, String model, List<ChatModel> providers) {
ChatModel provider = getProvider(index, model, providers);
return provider.stream(prompt)
.onErrorResume(ex -> {
log.warn("[LLM路由] 提供者{}流式调用失败,尝试fallback", provider, ex);
return tryStreamChain(prompt, chain, index + 1, model);
return tryStreamChain(prompt, index + 1, model, providers);
});
}
private Mono<ChatResponse> tryCallChain(Prompt prompt, List<String> chain, int index) {
if (index >= chain.size()) {
return Mono.error(new RuntimeException("所有模型均不可用"));
}
String provider = chain.get(index);
log.info("[LLM路由] 同步调用, provider={}", provider);
return Mono.fromCallable(() -> providers.get(provider).call(prompt))
private Mono<ChatResponse> tryCallChain(Prompt prompt, int index, String model, List<ChatModel> providers) {
ChatModel provider = getProvider(index, model, providers);
return Mono.fromCallable(() -> provider.call(prompt))
.subscribeOn(Schedulers.boundedElastic())
.onErrorResume(ex -> {
log.warn("[LLM路由] 提供者{}同步调用失败,尝试fallback", provider, ex);
return tryCallChain(prompt, chain, index + 1);
return tryCallChain(prompt, index + 1, model, providers);
});
}
private ChatModel getProvider(int index, String model, List<ChatModel> providers) {
if (index >= providers.size()) {
throw new MyRuntimeException("所有模型均不可用");
}
ChatModel provider;
if (model == null || model.isEmpty()) {
provider = providers.get(index);
} else {
provider = providers.stream().filter(p -> Objects.equals(p.getOptions().getModel(), model)).findFirst().orElseGet(() -> providers.get(index));
}
log.info("[LLM路由] 调用, provider={}, model={}", provider, provider.getOptions().getModel());
return provider;
}
}
@@ -0,0 +1,216 @@
package cn.com.mfish.ai.service.impl;
import cn.com.mfish.ai.api.entity.AiModelConfig;
import cn.com.mfish.ai.api.req.ReqAiModelConfig;
import cn.com.mfish.ai.api.vo.AiModelConfigVo;
import cn.com.mfish.ai.event.AiModelChangedEvent;
import cn.com.mfish.ai.mapper.AiModelConfigMapper;
import cn.com.mfish.common.ai.service.AiModelConfigService;
import cn.com.mfish.common.core.secret.SM4Utils;
import cn.com.mfish.common.core.utils.AuthInfoUtils;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.core.utils.excel.ExcelUtils;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* @description: AI模型配置信息
* @author: mfish
* @date: 2026-07-03
* @version: V2.4.0
*/
@Service
public class AiModelConfigServiceImpl extends ServiceImpl<AiModelConfigMapper, AiModelConfig> implements AiModelConfigService {
@Value("${ai.api-key.hex-key:}")
private String hexApiKey;
private final ApplicationEventPublisher eventPublisher;
public AiModelConfigServiceImpl(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
/**
* 模型配置发生变更后发布事件触发 LlmModelRouter 重新加载模型并通知助手重建 chatClient
*/
private void publishModelChanged() {
eventPublisher.publishEvent(new AiModelChangedEvent(this));
}
/**
* 分页列表查询
*
* @param reqAiModelConfig AI模型配置信息请求参数
* @param reqPage 分页参数
* @return 返回AI模型配置信息-分页列表
*/
@Override
public Result<PageResult<AiModelConfigVo>> queryPageList(ReqAiModelConfig reqAiModelConfig, ReqPage reqPage) {
return Result.ok(new PageResult<>(queryList(reqAiModelConfig, reqPage)), "AI模型配置信息-查询成功!");
}
/**
* 获取列表
*
* @param reqAiModelConfig AI模型配置信息请求参数
* @param reqPage 分页参数
* @return 返回AI模型配置信息-分页列表
*/
private List<AiModelConfigVo> queryList(ReqAiModelConfig reqAiModelConfig, ReqPage reqPage) {
PageHelper.startPage(reqPage.getPageNum(), reqPage.getPageSize());
List<AiModelConfig> list = queryList(reqAiModelConfig);
List<AiModelConfigVo> voList = new ArrayList<>();
for (AiModelConfig entity : list) {
AiModelConfigVo vo = new AiModelConfigVo();
BeanUtils.copyProperties(entity, vo);
voList.add(vo);
}
return voList;
}
@Override
public List<AiModelConfig> queryList(ReqAiModelConfig reqAiModelConfig) {
LambdaQueryWrapper<AiModelConfig> lambdaQueryWrapper = new LambdaQueryWrapper<AiModelConfig>()
.eq(!StringUtils.isEmpty(reqAiModelConfig.getModelName()), AiModelConfig::getModelName, reqAiModelConfig.getModelName())
.eq(null != reqAiModelConfig.getEnabled(), AiModelConfig::getEnabled, reqAiModelConfig.getEnabled())
.eq(!StringUtils.isEmpty(reqAiModelConfig.getProtocol()), AiModelConfig::getProtocol, reqAiModelConfig.getProtocol())
.orderByAsc(AiModelConfig::getSortOrder);
List<AiModelConfig> list = list(lambdaQueryWrapper);
for (int i = 0; i < list.size(); i++) {
AiModelConfig entity = list.get(i);
if (StringUtils.isEmpty(entity.getApiKey())) {
continue;
}
try {
entity.setApiKey(SM4Utils.decryptEcb(hexApiKey, entity.getApiKey()));
} catch (Exception e) {
log.error("解密异常" + e.getMessage(), e);
list.remove(i--);
}
}
return list;
}
/**
* 添加
*
* @param aiModelConfig AI模型配置信息对象
* @return 返回AI模型配置信息-添加结果
*/
@Override
public Result<AiModelConfig> add(AiModelConfig aiModelConfig) {
aiModelConfig.setTenantId(AuthInfoUtils.getCurrentTenantId());
if (StringUtils.isNotEmpty(aiModelConfig.getApiKey())) {
aiModelConfig.setApiKey(SM4Utils.encryptEcb(hexApiKey, aiModelConfig.getApiKey()));
}
if (save(aiModelConfig)) {
publishModelChanged();
return Result.ok(aiModelConfig, "AI模型配置信息-添加成功!");
}
return Result.fail(aiModelConfig, "错误:AI模型配置信息-添加失败!");
}
/**
* 编辑
*
* @param aiModelConfig AI模型配置信息对象
* @return 返回AI模型配置信息-编辑结果
*/
@Override
public Result<AiModelConfig> edit(AiModelConfig aiModelConfig) {
if (StringUtils.isNotEmpty(aiModelConfig.getApiKey())) {
//判断是否包含****
if (aiModelConfig.getApiKey().contains("****")) {
aiModelConfig.setApiKey(null);
} else {
aiModelConfig.setApiKey(SM4Utils.encryptEcb(hexApiKey, aiModelConfig.getApiKey()));
}
}
if (updateById(aiModelConfig)) {
publishModelChanged();
return Result.ok(aiModelConfig, "AI模型配置信息-编辑成功!");
}
return Result.fail(aiModelConfig, "错误:AI模型配置信息-编辑失败!");
}
/**
* 通过id删除
*
* @param id 唯一ID
* @return 返回AI模型配置信息-删除结果
*/
@Override
public Result<Boolean> delete(String id) {
if (removeById(id)) {
publishModelChanged();
return Result.ok(true, "AI模型配置信息-删除成功!");
}
return Result.fail(false, "错误:AI模型配置信息-删除失败!");
}
/**
* 批量删除
*
* @param ids 批量ID
* @return 返回AI模型配置信息-删除结果
*/
@Override
public Result<Boolean> deleteBatch(String ids) {
if (removeByIds(Arrays.asList(ids.split(",")))) {
publishModelChanged();
return Result.ok(true, "AI模型配置信息-批量删除成功!");
}
return Result.fail(false, "错误:AI模型配置信息-批量删除失败!");
}
/**
* 通过id查询
*
* @param id 唯一ID
* @return 返回AI模型配置信息对象
*/
@Override
public Result<AiModelConfig> queryById(String id) {
AiModelConfig aiModelConfig = getById(id);
return Result.ok(aiModelConfig, "AI模型配置信息-查询成功!");
}
@Override
public Result<AiModelConfigVo> queryByIdVo(String id) {
AiModelConfig entity = getById(id);
if (entity == null) {
return Result.fail(null, "错误:AI模型配置信息-未找到对应数据!");
}
AiModelConfigVo vo = new AiModelConfigVo();
BeanUtils.copyProperties(entity, vo);
return Result.ok(vo, "AI模型配置信息-查询成功!");
}
/**
* 导出
*
* @param reqAiModelConfig AI模型配置信息请求参数
* @param reqPage 分页参数
* @throws IOException IO异常
*/
@Override
public void export(ReqAiModelConfig reqAiModelConfig, ReqPage reqPage) throws IOException {
//swagger调用会用问题使用postman测试
ExcelUtils.write("AI模型配置信息_" + new SimpleDateFormat("yyyy-MM-dd").format(new Date()), queryList(reqAiModelConfig, reqPage));
}
}
@@ -1,12 +1,11 @@
package cn.com.mfish.sys.tools;
package cn.com.mfish.ai.tools;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.constants.RPCConstants;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.sys.service.DictItemService;
import cn.com.mfish.sys.api.entity.Dict;
import cn.com.mfish.sys.api.entity.DictItem;
import cn.com.mfish.sys.entity.Dict;
import cn.com.mfish.sys.req.ReqDict;
import cn.com.mfish.sys.service.DictService;
import cn.com.mfish.sys.api.remote.RemoteDictService;
import cn.com.mfish.sys.api.req.ReqDict;
import jakarta.annotation.Resource;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
@@ -25,9 +24,7 @@ import java.util.List;
@Component
public class DictTools {
@Resource
private DictService dictService;
@Resource
private DictItemService dictItemService;
private RemoteDictService remoteDictService;
/**
* 根据字典名称查询字典编码
@@ -37,7 +34,7 @@ public class DictTools {
*/
@Tool(description = "查询字典编码", returnDirect = true)
public List<Dict> queryCodeByName(@ToolParam(description = "字典名称") String dictName) {
return dictService.queryList(new ReqDict().setDictName(dictName), new ReqPage(1, 100));
return remoteDictService.queryList(RPCConstants.INNER, new ReqDict().setDictName(dictName)).getData();
}
/**
@@ -48,7 +45,7 @@ public class DictTools {
*/
@Tool(description = "查询字典项", returnDirect = true)
public List<DictItem> queryByCode(@ToolParam(description = "字典编码") String dictCode) {
Result<List<DictItem>> result = dictItemService.queryByCode(dictCode);
Result<List<DictItem>> result = remoteDictService.queryByCode(RPCConstants.INNER, dictCode);
if (result.isSuccess()) {
return result.getData();
}
@@ -1,9 +1,10 @@
package cn.com.mfish.oauth.tools;
package cn.com.mfish.ai.tools;
import cn.com.mfish.common.core.constants.RPCConstants;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.common.oauth.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.service.SsoMenuService;
import cn.com.mfish.common.oauth.api.remote.RemoteMenuService;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import jakarta.annotation.Resource;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
@@ -21,15 +22,15 @@ import java.util.List;
@Component
public class MenuTools {
@Resource
private SsoMenuService ssoMenuService;
private RemoteMenuService remoteMenuService;
@Tool(description = "查询菜单树结构")
public Result<List<SsoMenu>> queryList() {
return ssoMenuService.queryMenuTree(new ReqSsoMenu().setMenuType(1), null);
return remoteMenuService.queryMenuTree(RPCConstants.INNER, new ReqSsoMenu().setMenuType(1));
}
@Tool(description = "查询子菜单信息")
public Result<List<SsoMenu>> queryChild(@ToolParam(description = "菜单名称") String menuName) {
return ssoMenuService.queryMenuTree(new ReqSsoMenu().setMenuName(menuName), null);
return remoteMenuService.queryMenuTree(RPCConstants.INNER, new ReqSsoMenu().setMenuName(menuName));
}
}
@@ -1,9 +1,9 @@
package cn.com.mfish.oauth.tools;
package cn.com.mfish.ai.tools;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.core.constants.RPCConstants;
import cn.com.mfish.common.oauth.api.entity.SsoOrg;
import cn.com.mfish.common.oauth.api.entity.UserRole;
import cn.com.mfish.common.oauth.service.SsoUserService;
import cn.com.mfish.common.oauth.api.remote.RemoteUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.annotation.Description;
@@ -21,21 +21,21 @@ import java.util.Set;
public class UserTools {
private final String userId;
private final String tenantId;
private final SsoUserService ssoUserService;
private final RemoteUserService remoteUserService;
@Tool(description = "获取当前用户角色信息")
public List<UserRole> getUserRoles() {
return ssoUserService.getUserRoles(userId, tenantId);
return remoteUserService.getRoles(RPCConstants.INNER, userId, tenantId).getData();
}
@Tool(description = "获取当前用户组织树")
public Result<List<SsoOrg>> getOrgs() {
return ssoUserService.getOrgs(userId, "all");
public List<SsoOrg> getOrgs() {
return remoteUserService.getOrgs(RPCConstants.INNER, userId, "all").getData();
}
@Tool(description = "获取当前用户权限")
public Result<Set<String>> getPermissions() {
return Result.ok(ssoUserService.getUserPermissions(userId, tenantId));
public Set<String> getPermissions() {
return remoteUserService.getPermissions(RPCConstants.INNER, userId, tenantId).getData();
}
}
-4
View File
@@ -32,9 +32,5 @@
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-sys</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-ai</artifactId>
</dependency>
</dependencies>
</project>
@@ -1,61 +0,0 @@
package cn.com.mfish.sys.agent;
import cn.com.mfish.common.ai.agent.BaseAssistant;
import cn.com.mfish.common.core.utils.StringUtils;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
/**
* @description: 摸鱼小助手配置
* @author: mfish
* @date: 2025/8/21
*/
@Component
public class MfishAssistant extends BaseAssistant {
private static final String DEFAULT_PROMPT = "你好,简单介绍下摸鱼低代码";
private final ChatClient chatClient;
public MfishAssistant(ChatModel openAiChatModel, ChatMemory chatMemory) {
this.chatClient = ChatClient.builder(openAiChatModel)
.defaultSystem("""
你是摸鱼低代码的小助手是一个可爱的傻白甜萝莉你会用可爱的语言和我聊天解决问题!
当有人问摸鱼低代码相关信息时实际是在问我们整个平台的信息
摸鱼低代码平台是一款致力于让开发像摸鱼一样轻松的低代码/无代码平台
我们希望打破技术门槛让程序员和非程序员都能快速构建业务系统提升效率释放创造力
这不仅是程序员偷闲时的效率神器更是职场小白的建站利器甚至是领导画原型的秘密武器
我可以扮演以下角色
1. 低代码开发人员
2. 非技术人员如产品经理业务分析师等
3. 技术支持人员
4. 客户服务
""")
.defaultAdvisors(new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
@Override
public Flux<ChatResponse> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
return this.chatClient.prompt().user(prompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.stream().chatResponse();
}
}
@@ -1,84 +0,0 @@
package cn.com.mfish.sys.agent;
import cn.com.mfish.common.ai.agent.BaseAssistant;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.sys.tools.DictTools;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
/**
* @description: 系统中心助手
* @author: mfish
* @date: 2025/8/22
*/
@Slf4j
@Component
public class SysAssistant extends BaseAssistant {
private static final String DEFAULT_PROMPT = "你好,简单介绍下系统中心助手";
private final ChatClient chatClient;
@Resource
private DictTools dictTools;
public SysAssistant(ChatModel openAiChatModel, ChatMemory chatMemory) {
this.chatClient = ChatClient.builder(openAiChatModel)
.defaultSystem("""
你是摸鱼低代码的系统中心助手是一个可爱的傻白甜萝莉你会用可爱的语言和我聊天解决问题!
当有人问摸鱼低代码相关信息时实际是在问我们整个平台的信息
你主要辅助用户完成系统中心的一些基础操作
你能够通过调用工具来完成系统中心基础操作包括查询新增修改删除等
你主要辅助用户完成系统中心相关模块的信息检索执行操作
系统中心主要包含 字典信息 分类目录信息 日志信息 代码生成功能 在线用户信息 数据库连接信息 数据源信息
注意
1. 我需要你调用工具来帮我回答问题不要自己编造答案
2. 不要随意修改查询结果只返回工具返回的内容
3. 如果未找到相应工具告诉用户你执行不了这个操作
4. 如果工具参数缺失提示用户输入缺失参数引导用户输入有用信息
5. 请使用中文回答
""")
.defaultAdvisors(new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
@Override
public Flux<ChatResponse> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
String finalPrompt = prompt;
ChatClient.ChatClientRequestSpec requestSpec = this.chatClient.prompt()
.system("你必须先调用工具,再基于工具结果回答问题")
.user(finalPrompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.tools(dictTools);
return
requestSpec.stream().chatResponse()
.onErrorResume(e -> {
log.warn("流式调用失败,降级为非流式调用: {}", e.getMessage());
return Mono.fromCallable(() -> requestSpec.call().chatResponse())
.subscribeOn(Schedulers.boundedElastic())
.flux();
});
}
}
@@ -8,9 +8,9 @@ import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.log.annotation.Log;
import cn.com.mfish.common.oauth.annotation.RequiresPermissions;
import cn.com.mfish.sys.entity.Dict;
import cn.com.mfish.sys.req.ReqDict;
import cn.com.mfish.sys.service.DictService;
import cn.com.mfish.sys.api.entity.Dict;
import cn.com.mfish.sys.api.req.ReqDict;
import cn.com.mfish.common.sys.service.DictService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
/**
* @Description: 字典
@@ -48,6 +49,19 @@ public class DictController {
}
/**
* 字典列表查询
*
* @param reqDict 请求参数
* @return 返回字典列表
*/
@Operation(summary = "字典-列表查询", description = "字典-列表查询")
@GetMapping("/list")
@RequiresPermissions("sys:dict:query")
public Result<List<Dict>> queryList(ReqDict reqDict) {
return Result.ok(dictService.queryList(reqDict, new ReqPage(1, 10000)), "字典列表-查询成功!");
}
/**
* 添加
*
@@ -11,7 +11,7 @@ import cn.com.mfish.common.sys.req.ReqDictItem;
import cn.com.mfish.common.sys.service.DictItemService;
import cn.com.mfish.sys.api.entity.DictItem;
import cn.com.mfish.sys.cache.DictCache;
import cn.com.mfish.sys.entity.Dict;
import cn.com.mfish.sys.api.entity.Dict;
import cn.com.mfish.sys.mapper.DictMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.pagehelper.PageHelper;
@@ -1,6 +1,6 @@
package cn.com.mfish.sys.mapper;
import cn.com.mfish.sys.entity.Dict;
import cn.com.mfish.sys.api.entity.Dict;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
@@ -7,10 +7,10 @@ import cn.com.mfish.common.sys.req.ReqDictItem;
import cn.com.mfish.common.sys.service.DictItemService;
import cn.com.mfish.sys.api.entity.DictItem;
import cn.com.mfish.sys.cache.DictCache;
import cn.com.mfish.sys.entity.Dict;
import cn.com.mfish.sys.api.entity.Dict;
import cn.com.mfish.sys.mapper.DictMapper;
import cn.com.mfish.sys.req.ReqDict;
import cn.com.mfish.sys.service.DictService;
import cn.com.mfish.sys.api.req.ReqDict;
import cn.com.mfish.common.sys.service.DictService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
+26 -2
View File
@@ -25,13 +25,37 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-model</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-deepseek</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-anthropic</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-ai-api</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring</artifactId>
</dependency>
</dependencies>
</project>
@@ -1,33 +0,0 @@
package cn.com.mfish.common.ai.agent;
import cn.com.mfish.common.ai.client.IClientAssistant;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.ChatResponseVo;
import cn.com.mfish.common.core.utils.StringUtils;
import reactor.core.publisher.Flux;
import java.util.Objects;
/**
* @description: 系统中心助手
* @author: mfish
* @date: 2025/8/22
*/
public abstract class BaseAssistant implements IClientAssistant {
/**
* 聊天返回id
*
* @return 聊天信息
*/
@Override
public Flux<ChatResponseVo> chat(AiRequest aiRequest) {
return chat(aiRequest.getSessionId(), aiRequest.getMessage().getContent())
.filter(resp -> "STOP".equals(Objects.requireNonNull(resp.getResult()).getMetadata().getFinishReason())
|| StringUtils.isNotEmpty(resp.getResult().getOutput().getText()))
.map(resp -> new ChatResponseVo().setId(aiRequest.getId())
.setContent(Objects.requireNonNull(resp.getResult()).getOutput().getText())
.setFinishReason(resp.getResult().getMetadata().getFinishReason())
);
}
}
@@ -1,88 +0,0 @@
package cn.com.mfish.common.ai.agent;
import cn.com.mfish.common.ai.entity.AiRouterVo;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.core.web.Result;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ResponseEntity;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
import java.util.Objects;
/**
* @description: 摸鱼小助手配置
* @author: mfish
* @date: 2025/8/21
*/
@Component
public class GatewayAssistant {
private static final String DEFAULT_PROMPT = "你好,简单介绍下摸鱼低代码";
private final ChatClient chatClient;
public GatewayAssistant(ChatModel openAiChatModel, ChatMemory chatMemory) {
this.chatClient = ChatClient.builder(openAiChatModel)
.defaultSystem("""
你是"摸鱼低代码"的路由小助手你会根据用户的问题判断用户需要访问的路由路径!
当有人问"摸鱼低代码"相关信息时实际是在问我们整个平台的信息
你只能返回路由路径不能返回其他内容
结果以json格式返回
返回格式{"path":"/sys/ai/chat","name":"摸鱼小助手","serviceId":"mf-sys"}
# 路由路径
1. /sys/ai/chat : 摸鱼小助手 默认路由 对应微服务mf-sys
2. /sys/ai/assist : 系统中心助手 对应微服务mf-sys 系统中心主要包括字典信息分类目录信息日志信息代码生成功能在线用户信息数据库连接信息数据源信息
3. /oauth2/ai/assist : 认证中心助手 对应微服务mf-oauth 认证中心主要包括菜单信息组织信息角色信息帐号信息租户信息
4. /nocode/ai/assist : 低代码中心助手 对应微服务mf-nocode 低代码中心主要包括自助大屏自助API组件管理公式信息
```
如果用户需要查询字典相关信息分类目录相关信息日志相关信息等系统信息返回系统中心助手的路由路径
如果用户需要查询菜单相关信息组织相关信息角色相关信息帐号相关信息租户相关信息等认证中心信息返回认证中心助手的路由路径
如果用户需要查询自助大屏自助API数据集可视化等低代码中心信息返回低代码中心助手的路由路径
```
注意
返回的路由路径必须上失眠列出来的路由路径
如果当前问题与路由路径无关分析下之前的问题是否是之前问题的补充如果是则返回上一次的路由路径
如果不知道用户需要访问的路由路径默认返回{"path":"/sys/ai/chat","name":"摸鱼小助手","serviceId":"mf-sys"}
路由路径不能包含空格
请使用中文回答
""")
.defaultAdvisors(new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
public Mono<Result<AiRouterVo>> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
String finalPrompt = prompt;
return Mono.fromCallable(() -> {
ResponseEntity<ChatResponse, AiRouterVo> responseEntity = this.chatClient.prompt()
.user(finalPrompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.call()
.responseEntity(AiRouterVo.class);
AiRouterVo vo = Objects.requireNonNullElseGet(responseEntity.entity(), AiRouterVo::new);
return Result.ok(vo);
})
.subscribeOn(Schedulers.boundedElastic());
}
}
@@ -25,4 +25,12 @@ public interface IClientAssistant {
* @return 聊天结果
*/
Flux<ChatResponseVo> chat(AiRequest aiRequest);
/**
* 获取助手对应的网关路由路径
* 用于AssistantRegistry按路径查找助手替代Servlet forward实现真正的SSE流式传输
*
* @return 路由路径/ai/agent/chat/ai/sys/assist/ai/oauth2/assist
*/
String getPath();
}
@@ -2,6 +2,7 @@ package cn.com.mfish.common.ai.config;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -13,6 +14,10 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
public class AiConfig {
@Bean
public ChatMemoryRepository chatMemoryRepository() {
return new InMemoryChatMemoryRepository();
}
@Bean
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory.builder()
@@ -15,7 +15,7 @@ import lombok.experimental.Accessors;
@Accessors(chain = true)
public class AiRouterVo {
@Schema(description = "路由路径")
private String path = "/sys/ai/chat";
private String path = "/ai/agent/chat";
@Schema(description = "路由名称")
private String name = "摸鱼小助手";
@Schema(description = "目标微服务ID,如mf-sys、mf-oauth,用于Gateway动态路由")
@@ -0,0 +1,105 @@
package cn.com.mfish.common.ai.enums;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.ai.ollama.api.OllamaModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import java.util.Arrays;
/**
* LLM接入协议
* @author mfish
* @date 2026/07/10
*/
public enum LlmProtocol {
OPENAI("openai") {
@Override
public ChatModel create(String apiKey, String baseUrl, String model, Integer maxTokens, Double temperature) {
return OpenAiChatModel.builder()
.options(OpenAiChatOptions.builder()
.apiKey(apiKey)
.model(model)
.temperature(temperature)
.maxTokens(maxTokens)
.baseUrl(baseUrl)
.build())
.build();
}
},
OLLAMA("ollama") {
@Override
public ChatModel create(String apiKey, String baseUrl, String model, Integer maxTokens, Double temperature) {
var api = OllamaApi.builder()
.baseUrl(baseUrl)
.build();
var options = OllamaChatOptions.builder()
.model(OllamaModel.valueOf(model))
.numPredict(maxTokens)
.temperature(temperature)
.build();
return OllamaChatModel.builder().ollamaApi(api).options(options).build();
}
},
DEEPSEEK("deepseek") {
@Override
public ChatModel create(String apiKey, String baseUrl, String model, Integer maxTokens, Double temperature) {
DeepSeekApi deepSeekApi = DeepSeekApi.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.build();
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model(model)
.temperature(temperature)
.maxTokens(maxTokens)
.build();
return DeepSeekChatModel.builder()
.deepSeekApi(deepSeekApi)
.options(options)
.build();
}
},
ANTHROPIC("anthropic") {
@Override
public ChatModel create(String apiKey, String baseUrl, String model, Integer maxTokens, Double temperature) {
return AnthropicChatModel.builder()
.options(AnthropicChatOptions.builder()
.model(model)
.maxTokens(maxTokens)
.temperature(temperature)
.apiKey(apiKey)
.baseUrl(baseUrl)
.build())
.build();
}
};
private final String value;
LlmProtocol(String value) {
this.value = value;
}
public abstract ChatModel create(String apiKey, String baseUrl, String model, Integer maxTokens, Double temperature);
public static LlmProtocol fromValue(String value) {
return Arrays.stream(values())
.filter(p -> p.value.equalsIgnoreCase(value))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("不支持的接入协议: " + value));
}
@Override
public String toString() {
return value;
}
}
@@ -0,0 +1,93 @@
package cn.com.mfish.common.ai.service;
import cn.com.mfish.ai.api.entity.AiModelConfig;
import cn.com.mfish.ai.api.req.ReqAiModelConfig;
import cn.com.mfish.ai.api.vo.AiModelConfigVo;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import com.baomidou.mybatisplus.extension.service.IService;
import java.io.IOException;
import java.util.List;
/**
* @description: AI模型配置信息
* @author: mfish
* @date: 2026-07-03
* @version: V2.4.0
*/
public interface AiModelConfigService extends IService<AiModelConfig> {
/**
* 分页列表查询
*
* @param reqAiModelConfig AI模型配置信息请求参数
* @param reqPage 分页参数
* @return 返回AI模型配置信息-分页列表
*/
Result<PageResult<AiModelConfigVo>> queryPageList(ReqAiModelConfig reqAiModelConfig, ReqPage reqPage);
/**
* 列表查询
*
* @return 返回AI模型配置信息-列表
*/
List<AiModelConfig> queryList(ReqAiModelConfig reqAiModelConfig);
/**
* 添加
*
* @param aiModelConfig AI模型配置信息对象
* @return 返回AI模型配置信息-添加结果
*/
Result<AiModelConfig> add(AiModelConfig aiModelConfig);
/**
* 编辑
*
* @param aiModelConfig AI模型配置信息对象
* @return 返回AI模型配置信息-编辑结果
*/
Result<AiModelConfig> edit(AiModelConfig aiModelConfig);
/**
* 通过id删除
*
* @param id 唯一ID
* @return 返回AI模型配置信息-删除结果
*/
Result<Boolean> delete(String id);
/**
* 批量删除
*
* @param ids 批量ID
* @return 返回AI模型配置信息-删除结果
*/
Result<Boolean> deleteBatch(String ids);
/**
* 通过id查询
*
* @param id 唯一ID
* @return 返回AI模型配置信息对象
*/
Result<AiModelConfig> queryById(String id);
/**
* 通过id查询VO
*
* @param id 唯一ID
* @return 返回AI模型配置信息VO对象
*/
Result<AiModelConfigVo> queryByIdVo(String id);
/**
* 导出
*
* @param reqAiModelConfig AI模型配置信息请求参数
* @param reqPage 分页参数
* @throws IOException IO异常
*/
void export(ReqAiModelConfig reqAiModelConfig, ReqPage reqPage) throws IOException;
}
@@ -0,0 +1,28 @@
package cn.com.mfish.common.ai.service;
import cn.com.mfish.common.ai.enums.LlmProtocol;
import org.springframework.ai.chat.model.ChatModel;
/**
* LLM模型工厂
* @author mfish
* @date 2026/07/10
*/
public class LlmModelFactory {
/**
* 创建LLM模型
* @param protocol 接入协议
* @param apiKey API密钥
* @param baseUrl 基础URL
* @param modelName 模型名称
* @param maxTokens 最大令牌数
* @param temperature 温度
* @return LLM模型
*/
public static ChatModel create(String protocol, String apiKey,
String baseUrl, String modelName,
Integer maxTokens, Double temperature) {
return LlmProtocol.fromValue(protocol).create(apiKey, baseUrl, modelName, maxTokens, temperature);
}
}
@@ -1,2 +0,0 @@
cn.com.mfish.common.ai.agent.GatewayAssistant
cn.com.mfish.common.ai.service.AiRouteService
+4
View File
@@ -69,5 +69,9 @@
<groupId>cn.com.mfish</groupId>
<artifactId>mf-demo-api</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-ai</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,37 @@
package cn.com.mfish.common.api;
import cn.com.mfish.ai.api.entity.AiModelConfig;
import cn.com.mfish.ai.api.remote.RemoteAiModelConfigService;
import cn.com.mfish.ai.api.req.ReqAiModelConfig;
import cn.com.mfish.ai.api.vo.AiModelConfigVo;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.ai.service.AiModelConfigService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @description: ai模型配置服务
* @author: mfish
* @date: 2026/07/03
*/
@Service("remoteAiModelConfigService")
public class BootAiModelConfigService implements RemoteAiModelConfigService {
@Resource
private AiModelConfigService aiModelConfigService;
@Override
public Result<PageResult<AiModelConfigVo>> queryPageList(String origin, ReqAiModelConfig reqAiModelConfig, ReqPage reqPage) {
return aiModelConfigService.queryPageList(reqAiModelConfig, reqPage);
}
@Override
public Result<List<AiModelConfig>> queryList(String origin, ReqAiModelConfig reqAiModelConfig) {
return Result.ok(aiModelConfigService.queryList(reqAiModelConfig), "查询模型配置列表成功");
}
}
@@ -1,10 +1,14 @@
package cn.com.mfish.common.api;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.sys.service.DictItemService;
import cn.com.mfish.sys.api.entity.Dict;
import cn.com.mfish.sys.api.entity.DictItem;
import cn.com.mfish.sys.api.remote.RemoteDictService;
import cn.com.mfish.sys.api.req.ReqDict;
import org.springframework.stereotype.Service;
import cn.com.mfish.common.sys.service.DictService;
import jakarta.annotation.Resource;
@@ -21,9 +25,16 @@ public class BootDictService implements RemoteDictService {
@Resource
DictItemService dictItemService;
@Resource
DictService dictService;
@Override
public Result<List<DictItem>> queryByCode(String origin, String dictCode) {
return dictItemService.queryByCode(dictCode);
}
@Override
public Result<List<Dict>> queryList(String origin, ReqDict reqDict) {
return Result.ok(dictService.queryList(reqDict, new ReqPage(1, 10000)), "查询字典列表成功");
}
}
@@ -3,10 +3,13 @@ package cn.com.mfish.common.api;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.common.oauth.api.remote.RemoteMenuService;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.service.SsoMenuService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @description: 菜单远程接口的本地实现
* @author: mfish
@@ -17,6 +20,11 @@ public class BootMenuService implements RemoteMenuService {
@Resource
SsoMenuService ssoMenuService;
@Override
public Result<List<SsoMenu>> queryMenuTree(String origin, ReqSsoMenu reqSsoMenu) {
return ssoMenuService.queryMenuTree(reqSsoMenu, null);
}
@Override
public Result<SsoMenu> add(String origin, SsoMenu ssoMenu) {
return ssoMenuService.insertMenu(ssoMenu);
@@ -1,5 +1,7 @@
package cn.com.mfish.common.core.constants;
import lombok.Getter;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
@@ -36,6 +38,7 @@ public class ServiceConstants {
* 微服务枚举引用ServiceConstants中的编译期常量保持单一定义源
* gatewayPrefix: 网关路由前缀/sys用于将网关路径转换为服务实际路径
*/
@Getter
public enum MfService {
OAUTH(OAUTH_SERVICE, "/oauth2"),
SYS(SYS_SERVICE, "/sys"),
@@ -54,14 +57,6 @@ public class ServiceConstants {
this.gatewayPrefix = gatewayPrefix;
}
public String getValue() {
return value;
}
public String getGatewayPrefix() {
return gatewayPrefix;
}
/**
* 获取所有微服务ID集合
*/
@@ -85,9 +80,9 @@ public class ServiceConstants {
/**
* 将网关路径转换为服务实际路径
* 例如/sys/ai/assist /ai/assist去掉/sys前缀
* 例如/ai/sys/assist /sys/assist去掉/sys前缀
*
* @param gatewayPath 网关路径/sys/ai/assist
* @param gatewayPath 网关路径/ai/sys/assist
* @return 服务路径/ai/assist如果路径不以该服务前缀开头则原样返回
*/
public String toServicePath(String gatewayPath) {
@@ -1,5 +1,6 @@
package cn.com.mfish.common.core.secret;
import cn.com.mfish.common.core.exception.MyRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
@@ -84,7 +85,6 @@ public class SM4Utils {
*/
public static String encryptEcb(String hexKey, String paramStr) {
try {
String cipherText = "";
// 16进制字符串-->byte[]
org.bouncycastle.util.Arrays.fill(new byte[hexKey.length() / 2], (byte) 0);
@@ -94,11 +94,10 @@ public class SM4Utils {
// 加密后的数组
byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
// byte[]-->hexString
cipherText = Hex.toHexString(cipherArray);
return cipherText;
return Hex.toHexString(cipherArray);
} catch (Exception e) {
log.error("ECB加密异常" + e.getMessage(), e);
return paramStr;
throw new MyRuntimeException("ECB加密异常", e);
}
}
@@ -127,10 +126,6 @@ public class SM4Utils {
* @return 解密后的字符串
*/
public static String decryptEcb(String hexKey, String cipherText) {
// 用于接收解密后的字符串
String decryptStr = "";
// hexString-->byte[]
byte[] keyData = Hex.decode(hexKey);
// hexString-->byte[]
byte[] cipherData = Hex.decode(cipherText);
@@ -139,11 +134,11 @@ public class SM4Utils {
try {
srcData = decrypt_Ecb_Padding(keyData, cipherData);
// byte[]-->String
decryptStr = new String(srcData, ENCODING);
return new String(srcData, ENCODING);
} catch (Exception e) {
log.error("ECB解密异常" + e.getMessage(), e);
throw new MyRuntimeException("ECB解密异常", e);
}
return decryptStr;
}
/**
@@ -3,9 +3,11 @@ package cn.com.mfish.common.core.utils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.server.ServerWebExchange;
import java.util.*;
@@ -16,6 +18,41 @@ import java.util.*;
*/
public class ServletUtils {
private static final ThreadLocal<ServerWebExchange> EXCHANGE_HOLDER = new ThreadLocal<>();
public static void setExchange(ServerWebExchange exchange) {
EXCHANGE_HOLDER.set(exchange);
}
public static void removeExchange() {
EXCHANGE_HOLDER.remove();
}
public static ServerWebExchange getExchange() {
return EXCHANGE_HOLDER.get();
}
public static ServerHttpRequest getServerHttpRequest() {
ServerWebExchange exchange = EXCHANGE_HOLDER.get();
return exchange != null ? exchange.getRequest() : null;
}
/**
* 获取String参数
*
* @return 返回与header名称相对应的header值如果请求对象为null或指定的header不存在则返回null
*/
public static String getHeader(String header) {
HttpServletRequest request = getRequest();
if (request != null) {
return request.getHeader(header);
}
ServerHttpRequest serverHttpRequest = getServerHttpRequest();
if (serverHttpRequest != null) {
return serverHttpRequest.getHeaders().getFirst(header);
}
return null;
}
/**
* 获取String参数
*/
@@ -33,38 +70,20 @@ public class ServletUtils {
return null;
}
}
/**
* 获取header信息
* 该方法用于从HTTP请求中提取指定的header信息
*
* @param header 指定的header名称不能为空
* @return 返回与header名称相对应的header值如果请求对象为null或指定的header不存在则返回null
*/
public static String getHeader(String header) {
HttpServletRequest request = getRequest();
if (request == null) {
return null;
}
return request.getHeader(header);
}
/**
* 获取请求属性
* <p>
* 本方法旨在从HTTP请求中获取特定的属性值通过提供属性名称它首先确保有一个有效的请求对象存在
* 如果请求对象不存在方法直接返回null表明在没有有效请求的情况下无法获取属性
* 当请求对象存在时方法则根据提供的属性名从请求中提取相应的属性值
*
* @param attr 请求属性的名称用于标识所需的属性
* @return 如果请求存在且指定的属性存在则返回该属性值否则返回null
*/
public static Object getAttribute(String attr) {
HttpServletRequest request = getRequest();
if (request == null) {
return null;
if (request != null) {
return request.getAttribute(attr);
}
return request.getAttribute(attr);
ServerWebExchange exchange = getExchange();
if (exchange != null) {
return exchange.getAttributes().get(attr);
}
return null;
}
/**
@@ -72,4 +72,10 @@ public @interface DataScope {
* @return 跳过条件
*/
String[] ignores() default {};
/**
* 超户是否忽略数据权限默认 超户也受数据权限限制如果超户不需要受数据权限限制将属性设置为true
* @return 是否忽略
*/
boolean superIgnore() default false;
}
@@ -1,6 +1,7 @@
package cn.com.mfish.common.oauth.scope;
import cn.com.mfish.common.core.exception.MyRuntimeException;
import cn.com.mfish.common.core.utils.AuthInfoUtils;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.oauth.annotation.DataScope;
import cn.com.mfish.common.oauth.common.DataScopeUtils;
@@ -74,6 +75,9 @@ public class SelectInterceptor implements Interceptor {
private Map<String, List<DataScopeValue>> buildParamsMap(List<DataScope> scopes) {
Map<String, List<DataScopeValue>> map = new HashMap<>();
for (DataScope scope : scopes) {
if (scope.superIgnore() && AuthInfoUtils.isSuper()) {
continue;
}
String tableName = scope.table();
List<DataScopeValue> list;
if (map.containsKey(tableName)) {
@@ -93,7 +97,8 @@ public class SelectInterceptor implements Interceptor {
private Map<String, List<String>> buildIgnoreMap(List<DataScope> scopes) {
Map<String, List<String>> map = new HashMap<>();
for (DataScope scope : scopes) {
if (scope.ignores() == null || scope.ignores().length == 0) {
if (scope.ignores() == null || scope.ignores().length == 0
|| (scope.superIgnore() && AuthInfoUtils.isSuper())) {
continue;
}
String tableName = scope.table();
@@ -2,7 +2,7 @@ package cn.com.mfish.common.oauth.service;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.common.oauth.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
@@ -1,9 +1,9 @@
package cn.com.mfish.sys.service;
package cn.com.mfish.common.sys.service;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.sys.entity.Dict;
import cn.com.mfish.sys.req.ReqDict;
import cn.com.mfish.sys.api.entity.Dict;
import cn.com.mfish.sys.api.req.ReqDict;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
-4
View File
@@ -81,10 +81,6 @@
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-captcha</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-ai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
@@ -26,17 +26,6 @@ public class RouteFunctionConfig {
@Resource
CheckCodeService checkCodeService;
/**
* AI智能路由入口仅用于让/aiRouter请求进入Gateway Filter链
* 实际路由目标由AiRouteFilter动态覆盖此处uri仅作占位
*/
@Bean
public RouteLocator aiRouterRoute(RouteLocatorBuilder builder) {
return builder.routes()
.route("aiRouter", r -> r.path("/aiRouter").uri("lb://mf-sys"))
.build();
}
/**
* LLM代理路由/v1开头的请求路由到mf-ai微服务
* LLM慢速长连接由mf-ai处理避免占用网关资源
@@ -1,186 +0,0 @@
package cn.com.mfish.gateway.filter;
import cn.com.mfish.common.ai.entity.AiRouterVo;
import cn.com.mfish.common.ai.service.AiRouteService;
import cn.com.mfish.common.core.constants.ServiceConstants;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
/**
* AI智能路由过滤器微服务网关模式
* 拦截 /aiRouter POST请求读取请求体
* 通过AiRouteService决策后动态路由到目标微服务
* 请求体原样透传到目标接口后续AiRequest增加字段无需修改此处
* <p>
* 过滤器顺序说明
* 必须在 RouteToRequestUrlFilter(10000) 之后ReactiveLoadBalancerClientFilter(10150) 之前执行
* 这样才能覆盖RouteToRequestUrlFilter设置的GATEWAY_REQUEST_URL_ATTR使路由指向AI决策的目标服务
*
* @author: mfish
* @date: 2025/8/22
*/
@Component
@Slf4j
public class AiRouteFilter implements GlobalFilter, Ordered {
private static final String AI_ROUTE_PATH = "/aiRouter";
private final AiRouteService aiRouteService;
public AiRouteFilter(AiRouteService aiRouteService) {
this.aiRouteService = aiRouteService;
}
@Override
public @NonNull Mono<Void> filter(ServerWebExchange exchange, @NonNull GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 只拦截AI路由POST请求
if (!AI_ROUTE_PATH.equals(path) || request.getMethod() != HttpMethod.POST) {
return chain.filter(exchange);
}
// 读取请求体缓存后用于解析参数和透传到下游
return DataBufferUtils.join(request.getBody())
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
})
.defaultIfEmpty(new byte[0])
.flatMap(bytes -> {
// 从请求体解析sessionId和prompt用于AI路由决策
String sessionId = null;
String prompt = null;
if (bytes.length > 0) {
String body = new String(bytes, StandardCharsets.UTF_8);
sessionId = extractJsonValue(body, "sessionId");
String content = extractJsonValue(body, "content");
if (content != null) {
prompt = content;
}
}
log.info("[AI路由] 接收到智能路由请求, sessionId={}, prompt={}", sessionId, prompt);
return aiRouteService.resolve(sessionId, prompt)
.flatMap(vo -> forwardTo(exchange, chain, vo, bytes));
});
}
/**
* 动态路由到目标微服务
* 1. 将网关路径/sys/ai/assist转换为服务实际路径/ai/assist
* 2. 通过 GATEWAY_REQUEST_URL_ATTR 设置目标URLlb:// 协议触发 LoadBalancer 解析
* 3. 同时修改请求URI路径确保下游服务接收到正确的路径而非原始/aiRouter
* 4. 请求体原样透传用装饰器包装缓存后的byte数组供下游重复读取
*/
private Mono<Void> forwardTo(ServerWebExchange exchange, GatewayFilterChain chain, AiRouterVo vo, byte[] bodyBytes) {
String serviceId = vo.getServiceId();
String gatewayPath = vo.getPath();
// 将网关路径转换为服务实际路径去掉网关前缀/sys/ai/assist /ai/assist
ServiceConstants.MfService mfService = ServiceConstants.MfService.fromValue(serviceId);
String servicePath = mfService != null ? mfService.toServicePath(gatewayPath) : gatewayPath;
// 构建目标URI使用服务实际路径
URI targetUri = URI.create("lb://" + serviceId + servicePath);
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, targetUri);
// 修改请求URI将原始路径/aiRouter替换为服务实际路径
URI originalUri = exchange.getRequest().getURI();
URI newUri = rebuildUri(originalUri, servicePath);
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public @NonNull URI getURI() {
return newUri;
}
@Override
public @NonNull Flux<DataBuffer> getBody() {
if (bodyBytes.length > 0) {
return Flux.just(dataBufferFactory.wrap(bodyBytes));
}
return Flux.empty();
}
};
log.info("[AI路由] 动态路由到, targetUri={}, 网关路径={}, 服务路径={}", targetUri, gatewayPath, servicePath);
return chain.filter(exchange.mutate().request(decorator).build());
}
/**
* 重建URI将原始路径替换为新路径保留原始的query和fragment
*/
private URI rebuildUri(URI originalUri, String newPath) {
return URI.create(originalUri.getScheme() + "://"
+ originalUri.getRawAuthority()
+ newPath
+ (originalUri.getRawQuery() != null ? "?" + originalUri.getRawQuery() : "")
+ (originalUri.getRawFragment() != null ? "#" + originalUri.getRawFragment() : ""));
}
/**
* 从JSON字符串中简单提取字段值避免引入额外JSON库依赖
*/
private String extractJsonValue(String json, String key) {
String pattern = "\"" + key + "\"";
int keyIndex = json.indexOf(pattern);
if (keyIndex < 0) {
return null;
}
int colonIndex = json.indexOf(':', keyIndex + pattern.length());
if (colonIndex < 0) {
return null;
}
// 找值的起始位置
int valueStart = colonIndex + 1;
while (valueStart < json.length() && json.charAt(valueStart) == ' ') {
valueStart++;
}
if (valueStart >= json.length()) {
return null;
}
if (json.charAt(valueStart) == '"') {
// 字符串值
int valueEnd = json.indexOf('"', valueStart + 1);
if (valueEnd < 0) {
return null;
}
return json.substring(valueStart + 1, valueEnd);
}
// 非字符串值数字布尔等取到逗号或}为止
int valueEnd = valueStart;
while (valueEnd < json.length() && json.charAt(valueEnd) != ',' && json.charAt(valueEnd) != '}') {
valueEnd++;
}
return json.substring(valueStart, valueEnd).trim();
}
@Override
public int getOrder() {
// 必须在 RouteToRequestUrlFilter(10000) 之后ReactiveLoadBalancerClientFilter(10150) 之前
return 10100;
}
}
-4
View File
@@ -44,9 +44,5 @@
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-captcha</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-ai</artifactId>
</dependency>
</dependencies>
</project>
@@ -1,92 +0,0 @@
package cn.com.mfish.oauth.agent;
import cn.com.mfish.common.ai.agent.BaseAssistant;
import cn.com.mfish.common.core.utils.AuthInfoUtils;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.oauth.service.SsoUserService;
import cn.com.mfish.oauth.tools.MenuTools;
import cn.com.mfish.oauth.tools.UserTools;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
/**
* @description: 认证中心助手
* @author: mfish
* @date: 2025/8/22
*/
@Slf4j
@Component
public class OauthAssistant extends BaseAssistant {
private static final String DEFAULT_PROMPT = "你好,简单介绍下认证中心助手";
private final ChatClient chatClient;
@Resource
MenuTools menuTools;
@Resource
SsoUserService ssoUserService;
public OauthAssistant(ChatModel openAiChatModel, ChatMemory chatMemory) {
this.chatClient = ChatClient.builder(openAiChatModel)
.defaultSystem("""
你是摸鱼低代码的认证中心助手是一个可爱的傻白甜萝莉你会用可爱的语言和我聊天解决问题!
当有人问摸鱼低代码相关信息时实际是在问我们整个平台的信息
你主要辅助用户完成认证中心的一些基础操作
你能够通过调用工具来完成认证中心基础操作包括查询新增修改删除导入导出等
你主要辅助用户完成认证中心相关模块的信息检索执行操作
认证中心主要包含 菜单信息组织信息角色信息帐号信息租户信息
注意
1. 我需要你调用工具来帮我回答问题不要自己编造答案
2. 不要随意修改查询结果只返回工具返回的内容
3. 如果未找到相应工具告诉用户你执行不了这个操作
4. 如果工具参数缺失提示用户输入缺失参数引导用户输入有用信息
5. 请使用中文回答
""")
.defaultAdvisors(new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
/**
* 聊天
*
* @param sessionId 会话id
* @param prompt 提示词
* @return 聊天信息
*/
@Override
public Flux<ChatResponse> chat(String sessionId, String prompt) {
if (StringUtils.isEmpty(prompt.trim())) {
prompt = DEFAULT_PROMPT;
}
String userId = AuthInfoUtils.getCurrentUserId();
String tenantId = AuthInfoUtils.getCurrentTenantId();
String finalPrompt = prompt;
ChatClient.ChatClientRequestSpec requestSpec = this.chatClient.prompt()
.system("你必须先调用工具,再基于工具结果回答问题")
.user(finalPrompt)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.tools(menuTools, new UserTools(userId, tenantId, ssoUserService));
return requestSpec
.stream()
.chatResponse()
.onErrorResume(e -> {
log.warn("流式调用失败,降级为非流式调用: {}", e.getMessage());
return Mono.fromCallable(() -> requestSpec.call().chatResponse())
.subscribeOn(Schedulers.boundedElastic())
.flux();
});
}
}
@@ -7,7 +7,7 @@ import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.log.annotation.Log;
import cn.com.mfish.common.oauth.annotation.RequiresPermissions;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.common.oauth.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.service.SsoMenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -22,7 +22,7 @@ import cn.com.mfish.common.oauth.common.OauthUtils;
import cn.com.mfish.common.oauth.entity.RedisAccessToken;
import cn.com.mfish.common.oauth.entity.SsoRole;
import cn.com.mfish.common.oauth.entity.WeChatToken;
import cn.com.mfish.common.oauth.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.req.ReqSsoOrg;
import cn.com.mfish.common.oauth.req.ReqSsoUser;
import cn.com.mfish.common.oauth.service.SsoMenuService;
@@ -1,7 +1,7 @@
package cn.com.mfish.oauth.mapper;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.common.oauth.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
@@ -10,7 +10,7 @@ import cn.com.mfish.common.oauth.common.OauthUtils;
import cn.com.mfish.oauth.cache.common.ClearCache;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.oauth.mapper.SsoMenuMapper;
import cn.com.mfish.common.oauth.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.api.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.service.SsoMenuService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+11
View File
@@ -39,6 +39,17 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- WebFlux替代ServletLLM长连接不占用线程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- WebFlux版Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>${swagger.doc.version}</version>
</dependency>
</dependencies>
<build>
<finalName>mf-ai</finalName>
Binary file not shown.
+10 -9
View File
@@ -49,6 +49,16 @@
<systemPath>${project.basedir}/lib/mf-nocode-${mfish.version}.jar</systemPath>
<version>${mfish.version}</version>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-workflow</artifactId>
<version>${mfish.version}</version>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-ai</artifactId>
<version>${mfish.version}</version>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-bi</artifactId>
@@ -62,15 +72,6 @@
<artifactId>mf-common-api</artifactId>
<version>${mfish.version}</version>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-prometheus</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-workflow</artifactId>
<version>${mfish.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -1,30 +1,14 @@
package cn.com.mfish.boot.controller;
import cn.com.mfish.common.ai.entity.AiRequest;
import cn.com.mfish.common.ai.entity.AiRouterVo;
import cn.com.mfish.common.ai.service.AiRouteService;
import cn.com.mfish.common.captcha.service.CheckCodeService;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.prom.annotation.MetricsMonitor;
import cn.com.mfish.common.prom.annotation.MetricsMonitors;
import cn.com.mfish.common.prom.enums.MetricEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import tools.jackson.databind.ObjectMapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Map;
/**
@@ -38,10 +22,6 @@ import java.util.Map;
public class BootController {
@Resource
CheckCodeService checkCodeService;
@Resource
AiRouteService aiRouteService;
@Resource
ObjectMapper objectMapper;
@Operation(summary = "获取验证码", description = "获取验证码")
@GetMapping("/captcha")
@@ -55,101 +35,4 @@ public class BootController {
return Result.fail("错误:未授权,请联系管理员");
}
/**
* AI智能路由单实例模式
* 同步阻塞等待AI路由决策完成后在原始请求线程上forward到目标Controller
* 避免异步处理上下文冲突BootController的Mono异步与目标Controller的Flux SSE异步冲突
* forward使用缓存body的CachingRequestWrapper确保目标@RequestBody能读取到完整请求体
*/
@Operation(summary = "Ai路由", description = "Ai路由")
@PostMapping("/aiRouter")
@MetricsMonitors({
@MetricsMonitor(metricEnum = MetricEnum.MFISH_REQUEST_COUNT, tagValues = {"POST", "/aiRouter"}),
@MetricsMonitor(metricEnum = MetricEnum.MFISH_REQUEST_DURATION, tagValues = {"POST", "/aiRouter"})
})
public void aiRouter(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// 读取原始请求体并缓存
byte[] bodyBytes = request.getInputStream().readAllBytes();
AiRequest aiRequest = bodyBytes.length > 0
? objectMapper.readValue(bodyBytes, AiRequest.class)
: new AiRequest();
String sessionId = aiRequest.getSessionId();
String prompt = aiRequest.getMessage() != null ? aiRequest.getMessage().getContent() : null;
log.info("[AI路由-单实例] 接收到智能路由请求, sessionId={}, prompt={}", sessionId, prompt);
// 同步阻塞等待路由决策完成
AiRouterVo vo = aiRouteService.resolve(sessionId, prompt).block();
if (vo == null) {
vo = new AiRouterVo();
}
String targetPath = vo.getPath();
log.info("[AI路由-单实例] 内部转发到, path={}", targetPath);
// 用缓存body的wrapper包装requestforward到目标Controller
CachingRequestWrapper cachedRequest = new CachingRequestWrapper(request, bodyBytes);
request.getRequestDispatcher(targetPath).forward(cachedRequest, response);
}
/**
* 缓存请求体的Request包装器
* 解决Servlet输入流只能读取一次的问题
*/
static class CachingRequestWrapper extends HttpServletRequestWrapper {
private final byte[] cachedBody;
CachingRequestWrapper(HttpServletRequest request, byte[] cachedBody) {
super(request);
this.cachedBody = cachedBody;
}
@Override
public ServletInputStream getInputStream() {
return new CachedServletInputStream(cachedBody);
}
@Override
public int getContentLength() {
return cachedBody.length;
}
@Override
public long getContentLengthLong() {
return cachedBody.length;
}
}
static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
CachedServletInputStream(byte[] data) {
this.inputStream = new ByteArrayInputStream(data);
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException("不支持异步读取");
}
@Override
public int read() {
return inputStream.read();
}
@Override
public int read(byte[] b, int off, int len) {
return inputStream.read(b, off, len);
}
}
}
}
@@ -4,10 +4,12 @@ spring:
api-key: sk-or-v1-b49*****************************************135dae3b
base-url: https://openrouter.ai/api
chat:
options:
max-tokens: 1000
temperature: 0.7
max-tokens: 1000
temperature: 0.7
ollama:
base-url: http://localhost:11434
chat:
model: qwen3:8b
model: qwen3:8b
ai:
api-key:
hex-key: 8ca90bd312db42aac5a8313ce9a0eac3
@@ -27,12 +27,12 @@ wechat:
appId: wx*************4a3
secret: a6***************efa227
gitee:
clientId: a83ea66522fef5f382c6aaffa875c7e87b21d3933277c44a326cd3407fc0b651
clientSecret: 3ebdce2afcb4a7df9b16355f60d898d452f4d9cf8ad1ab8279bb3bb465e614db
clientId: a83ea6***********************************44a326cd3407fc0b651
clientSecret: 3eb****************************************9bb3bb465e614db
redirectUri: http://localhost:5186/giteeOauth2
bindUri: http://localhost:5186/tenant/info/1?callback=gitee
github:
clientId: Ov23li7oH0Djw3ZEJCoJ
clientSecret: ada9a510b3bde542e7510142f07d32a5f6bed131
clientId: Ov2******************************3ZEJCoJ
clientSecret: ada9a************************32a5f6bed131
redirectUri: http://localhost:5186/githubOauth2
bindUri: http://localhost:5186/tenant/info/1?callback=github
Binary file not shown.
-4
View File
@@ -45,10 +45,6 @@
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-file</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-ai</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-workflow</artifactId>

Some files were not shown because too many files have changed in this diff Show More