194 Commits

Author SHA1 Message Date
evo
13da60e151 Merge pull request #189 from MuSan-Li/feature_20250904_fix_sql
feat: 添加session表会话ID
2025-09-04 17:02:06 +08:00
l90215
1f0c0ba0a9 feat: 添加session表会话ID 2025-09-04 17:01:13 +08:00
ageerle
ef3541fe77 Merge pull request #188 from xiaonieli7/feature_20250813_fix_codeOptimization
补充统一计费代理类BillingChatServiceProxy
2025-09-04 16:44:52 +08:00
Administrator
2b5fd810a4 fix(billing): 统一计费代理类BillingChatServiceProxy 2025-09-04 16:41:14 +08:00
Administrator
4a8d21a742 fix(billing): 1. 新增统一计费代理 BillingChatServiceProxy位置:ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/proxy/BillingChatServiceProxy.java 作用:为所有ChatService实现类提供透明的计费代理包装
核心功能:
  AI回复前余额预检查,避免无效消耗
  自动收集AI回复内容
  统一处理AI回复的保存和计费
   适配多种AI服务的数据格式
  2. 重构工厂类
  ChatServiceFactory
  改进:自动为所有ChatService包装计费代理
 新增方法:getOriginalService() 用于获取未包装的原始服务优势:调用方无需关心计费逻辑,完全透明
 3. 增强计费服务 IChatCostService 接口
   新增方法:checkBalanceSufficient() - 余额预检查
   分离关注点:saveMessage() - 仅保存消息
    publishBillingEvent() - 仅发布计费事件
    deductToken() - 仅执行计费扣费
2025-09-04 16:35:55 +08:00
ageerle
c62530176f Merge pull request #187 from xiaonieli7/feature_20250813_fix_codeOptimization
修改了计费逻辑
2025-09-04 15:43:48 +08:00
Administrator
c7554d7e35 fix(billing): 1. 新增统一计费代理 BillingChatServiceProxy位置:ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/proxy/BillingChatServiceProxy.java 作用:为所有ChatService实现类提供透明的计费代理包装
核心功能:
  AI回复前余额预检查,避免无效消耗
  自动收集AI回复内容
  统一处理AI回复的保存和计费
   适配多种AI服务的数据格式
  2. 重构工厂类
  ChatServiceFactory
  改进:自动为所有ChatService包装计费代理
 新增方法:getOriginalService() 用于获取未包装的原始服务优势:调用方无需关心计费逻辑,完全透明
 3. 增强计费服务 IChatCostService 接口
   新增方法:checkBalanceSufficient() - 余额预检查
   分离关注点:saveMessage() - 仅保存消息
    publishBillingEvent() - 仅发布计费事件
    deductToken() - 仅执行计费扣费
2025-09-04 15:37:52 +08:00
Administrator
1e4af3d01b fix(billing): 修复Token计费逻辑和消息更新机制
* 修复Token计费算法:按批次计费而非Token数量计费
* 添加ChatRequest.messageId字段支持消息关联更新
* 优化消息保存流程:分离基础信息保存和计费信息更新
* 修复预检查逻辑:统一预检查和实际扣费计算方式
* 调整Token阈值:100 → 1000,减少扣费频次
* 完善事件传递:ChatMessageCreatedEvent增加messageId

Fixes: 余额预检查误判、消息计费信息缺失、Token计费不准确
2025-08-29 15:19:37 +08:00
Administrator
1e3b49c9b8 用户发送消息 → 预检查余额 → 保存用户消息 → 发布计费事件 → 异步扣费 → 保存账单记录
添加了billingType计费类型字段消息保存的时候写入进去
2025-08-27 16:48:48 +08:00
Administrator
9f7f00e50c 用户发送消息 → 预检查余额 → 保存用户消息 → 发布计费事件 → 异步扣费 → 保存账单记录
添加了billingType计费类型字段消息保存的时候写入进去
2025-08-27 15:30:59 +08:00
Administrator
1c721981db Merge remote-tracking branch 'origin/main'
# Conflicts:
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java
#	ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java
2025-08-27 10:49:37 +08:00
AmAzing129
6e6ba84fd2 docs: add contributors bubble 2025-08-27 10:47:34 +08:00
likunlong
ef69778bb7 feat: 下掉模型能力逻辑代码; 2025-08-27 10:47:34 +08:00
l90215
7a374d877b feat: 调整知识库问答接入提示词模板 2025-08-27 10:47:34 +08:00
likunlong
43426054ec feat: 兼容不选自动模型时的原先逻辑;封装通用方法,简化创建有监控的SSE,简化流式错误输出并通知重试; 2025-08-27 10:47:31 +08:00
likunlong
ccdbb20935 feat: 不选择模型自动选择时走原始默认逻辑; 2025-08-27 10:47:29 +08:00
likunlong
4b37cfe97d feat: 失败回调器中使用emitter对象的唯一hash作为key,不再使用session,不与业务进行绑定,同时也保证跨线程调用的正确性; 2025-08-27 10:47:26 +08:00
likunlong
c43d4784de feat: 处理在非Web线程中获取Request中token失败的问题; 2025-08-27 10:47:25 +08:00
likunlong
359cee28d5 feat: 修改目前实现类使用统一重试降级逻辑; 2025-08-27 10:47:22 +08:00
likunlong
aa11c1f233 feat: 问答时添加统一重试和降级逻辑; 2025-08-27 10:47:20 +08:00
likunlong
a0d029c142 feat: 自动设置请求参数中的模型名称; 2025-08-27 10:47:20 +08:00
likunlong
6ce52befe2 feat: 根据是否有附件和是否自动,自动选择模型并且获取服务; 2025-08-27 10:47:19 +08:00
likunlong
330bdc3761 feat: 数据库chat_model添加优先级字段; 2025-08-27 10:47:19 +08:00
likunlong
4f7ad59e46 feat: 添加自动获取高优先级模型和服务的逻辑; 2025-08-27 10:47:19 +08:00
l90215
b696fde881 feat: 合并代码 删除不需要的文件 2025-08-27 10:47:19 +08:00
fy53888
00f9a1a55b 修改字典下拉带查找功能 2025-08-27 10:47:19 +08:00
fy53888
a1c7b86e72 备分一下2 2025-08-27 10:47:19 +08:00
fy53888
62676a54fb 备分一下2 2025-08-27 10:47:19 +08:00
fy53888
e51425a951 备分一下 2025-08-27 10:47:17 +08:00
fy53888
268be2d9ec 更新后端生成類型 Integer出錯的問 2025-08-27 10:47:09 +08:00
fy53888
f448a18e44 更新后端生成類型 Integer出錯的問題 2025-08-27 10:47:07 +08:00
lixiang
22e59fe5a1 向量库sql查询去除匹配分值字段 2025-08-27 10:47:01 +08:00
violateer
0780e3b8c9 fix: 修改krole_group_ids字段名 2025-08-27 10:47:01 +08:00
violateer
0cdba56a07 feature: 添加开启知识库角色,用户可见个人知识库及角色分配知识库 2025-08-27 10:47:01 +08:00
l90215
ebc13c06af feat: 更新只是库角色默认不开启 2025-08-27 10:47:00 +08:00
l90215
416f011c73 feat: fix代码生成类型问题 2025-08-27 10:47:00 +08:00
likunlong
bfeb389171 feat: 获取模型接口支持返回模型能力;模型表增加模型能力字段; 2025-08-27 10:47:00 +08:00
violateer
caf7f14781 feature: 新增生成前端文件模板接口 2025-08-27 10:46:51 +08:00
l90215
a6eb98daab feat: fix代码生成类型问题 2025-08-27 10:46:37 +08:00
l90215
4834b615a6 feat: fix代码生成类型问题 2025-08-27 10:46:37 +08:00
l90215
42aabeed96 feat: 添加空格 2025-08-27 10:46:37 +08:00
ageerle
bd9ffb10a9 Merge pull request #178 from AmAzing129/main
docs: add contributors bubble
2025-08-22 14:33:27 +08:00
AmAzing129
bb9c85ac3c docs: add contributors bubble 2025-08-22 14:29:03 +08:00
evo
70ca78d935 Merge pull request #176 from LM20230311/feat-model-priority
feat: 下掉模型能力逻辑代码;
2025-08-20 17:55:15 +08:00
evo
1af8c4ee50 Merge pull request #177 from MuSan-Li/feature_20250820_fix_prompt_temp
feat: 调整知识库问答接入提示词模板
2025-08-20 17:55:05 +08:00
l90215
b9276c5dcc feat: 调整知识库问答接入提示词模板 2025-08-20 17:52:51 +08:00
likunlong
d1e98a2001 feat: 下掉模型能力逻辑代码; 2025-08-20 16:09:53 +08:00
evo
baf065a294 Merge pull request #175 from LM20230311/feat-model-priority
Feat model priority:支持自动选择模型;支持模型的重试;
2025-08-20 14:04:17 +08:00
likunlong
842a39d6d2 feat: 兼容不选自动模型时的原先逻辑;封装通用方法,简化创建有监控的SSE,简化流式错误输出并通知重试; 2025-08-19 20:28:53 +08:00
likunlong
9fba91c35f feat: 不选择模型自动选择时走原始默认逻辑; 2025-08-19 18:00:20 +08:00
likunlong
498135b7fd feat: 失败回调器中使用emitter对象的唯一hash作为key,不再使用session,不与业务进行绑定,同时也保证跨线程调用的正确性; 2025-08-19 17:53:27 +08:00
likunlong
c3ab13ae67 feat: 处理在非Web线程中获取Request中token失败的问题; 2025-08-19 17:39:20 +08:00
likunlong
1638b9dd75 feat: 修改目前实现类使用统一重试降级逻辑; 2025-08-19 16:51:51 +08:00
likunlong
4434d8346c feat: 问答时添加统一重试和降级逻辑; 2025-08-19 16:46:25 +08:00
likunlong
119483df86 feat: 自动设置请求参数中的模型名称; 2025-08-19 15:12:24 +08:00
evo
98f7e3ada2 Merge pull request #174 from MuSan-Li/feature_202500819_fix_merge
feat: 合并代码 删除不需要的文件
2025-08-19 12:44:59 +08:00
l90215
50d9e0e843 feat: 合并代码 删除不需要的文件 2025-08-19 12:43:34 +08:00
evo
2871cf7630 Merge pull request #171 from fy53888/main
修改字典功能 和模板生成id 太长19位 改为1,2,3
2025-08-19 12:33:40 +08:00
likunlong
07cb351807 feat: 根据是否有附件和是否自动,自动选择模型并且获取服务; 2025-08-19 10:32:17 +08:00
fy53888
951ee6bd8a 修改字典下拉带查找功能 2025-08-19 09:48:35 +08:00
fy53888
5b6605c345 备分一下2 2025-08-18 22:04:15 +08:00
fy53888
5db116ec88 备分一下2 2025-08-18 22:03:51 +08:00
fy53888
645c754dd0 备分一下 2025-08-18 19:56:26 +08:00
likunlong
8751bb5104 feat: 数据库chat_model添加优先级字段; 2025-08-18 14:49:56 +08:00
likunlong
8d0c557bdb feat: 添加自动获取高优先级模型和服务的逻辑; 2025-08-18 14:30:08 +08:00
evo
5ac785c570 Merge pull request #173 from lixiang-cell/lixiang8.18
向量库sql查询去除匹配分值字段
2025-08-18 12:30:26 +08:00
evo
60145f9291 Merge pull request #170 from violateer/feature_fix_k_role_20280815
feature: 添加开启知识库角色,用户可见个人知识库及角色分配知识库
2025-08-18 12:30:09 +08:00
lixiang
d7b89cd1b3 向量库sql查询去除匹配分值字段 2025-08-18 11:12:06 +08:00
fy53888
5264b47c2f 修改字典功能 和模板生成id 太长19位 改为1,2,3 2025-08-17 09:14:19 +08:00
violateer
e9cd9e84d4 fix: 修改krole_group_ids字段名 2025-08-15 20:43:14 +08:00
violateer
b52f7a7112 feature: 添加开启知识库角色,用户可见个人知识库及角色分配知识库 2025-08-15 20:41:59 +08:00
evo
2abdf762c1 Merge pull request #168 from MuSan-Li/feature_20250815_update_k_role_config
feat: 更新只是库角色默认不开启
2025-08-15 09:58:32 +08:00
l90215
099c94e3cb feat: 更新只是库角色默认不开启 2025-08-15 09:56:47 +08:00
Administrator
affdc5e3a6 问题概述
1.保存消息和计费逻辑存在耦合
2.修改计费逻辑:
按次计费被阈值限制:旧逻辑把 TIMES 分支放在 totalTokens ≥ 100 的大分支里,导致没到100 token时不扣费,违背“每次调用就扣费”的语义。
token累计不当:TIMES 分支只扣费不处理累计,同时在 totalTokens < 100 时不会进入任何TIMES逻辑,累计会无意义增长。
粒度不稳定:TOKEN 计费一旦达阈值就把 total 全扣完并清零,不利于对账与用户体验。
打印方式:使用 System.out.println,不利于生产追踪。

改动要点
1.新增独立方法
saveMessage(ChatRequest): 只落库。
publishBillingEvent(ChatRequest): 只发布异步计费事件。
保留组合方法 saveMessageAndPublishEvent(ChatRequest) 以便需要一行调用时使用。
调用处已改为“先保存,再发布事件”
SseServiceImpl: 先 saveMessage,再 publishBillingEvent。
SSEEventSourceListener: 同上。
DifyServiceImpl: 同上。

2.计费模式分流:
TIMES:每次调用直接扣费,不累计。
TOKEN:按阈值(100)批量扣费,保留余数,账单颗粒稳定。
保留余数:total = prev + delta;billable = floor(total/threshold)threshold;remainder = total % threshold。
日志替换:统一使用 log.debug。
结构更清晰、可维护。
所有金额计算统一用 BigDecimal,保留两位小数,RoundingMode.HALF_UP
按次计费:每次直接扣费(BigDecimal),边界转 Double
按 token 计费:按阈值批量结算,保留余数;费用=单价(BigDecimal)×可结算token数
1. 消息分类存储
用户消息:role="user", deductCost=null, totalTokens=本次token数, remark="用户消息"
系统账单:role="system", deductCost=实际扣费, totalTokens=计费token数, remark="TIMES_BILLING/TOKEN_BILLING"
2. 数据流程
用户发送消息 → 预检查余额 → 保存用户消息 → 发布计费事件 → 异步扣费 → 保存账单记录
2025-08-14 14:00:48 +08:00
evo
047044eb06 Merge pull request #166 from MuSan-Li/feature_202500807_fix_code_generate
feat: fix代码生成类型问题
2025-08-13 12:39:24 +08:00
l90215
7108727395 feat: fix代码生成类型问题 2025-08-13 12:37:10 +08:00
evo
d3732a155d Merge pull request #164 from MuSan-Li/feature_20250811_fix_code_generator
Feature 20250811 fix code generator
2025-08-11 22:00:20 +08:00
l90215
a0db91ebe6 Merge branch 'main' of https://github.com/MuSan-Li/ruoyi-ai into feature_20250811_fix_code_generator 2025-08-11 21:59:20 +08:00
l90215
e83d70e9c3 feat: fix代码生成类型问题 2025-08-11 21:59:02 +08:00
evo
5eb166839a Merge pull request #163 from LM20230311/feat-model-switching-association
feat: 获取模型接口支持返回模型能力;模型表增加模型能力字段;
2025-08-11 21:54:08 +08:00
evo
e27a6cb738 Merge pull request #162 from violateer/feature_gen_frontend_model_files
feature: 新增生成前端文件模板接口
2025-08-11 21:53:56 +08:00
likunlong
d964e86b23 feat: 获取模型接口支持返回模型能力;模型表增加模型能力字段; 2025-08-11 09:33:35 +08:00
violateer
86d7eab5b5 解决冲突 2025-08-10 17:22:48 +08:00
violateer
fa5ad8caf6 Merge remote-tracking branch 'origin/feature_gen_frontend_model_files' into feature_gen_frontend_model_files 2025-08-10 17:20:14 +08:00
violateer
e5011e0dd9 feature: 新增生成前端文件模板接口 2025-08-10 17:19:59 +08:00
violateer
5fe8bd7706 feature: 新增生成前端文件模板接口 2025-08-10 17:06:32 +08:00
evo
0f28e1f3f6 Merge pull request #161 from MuSan-Li/feature_202500807_fix_xx
fix 代码生成逻辑bug
2025-08-10 00:27:56 +08:00
l90215
503f86644e feat: fix代码生成类型问题 2025-08-10 00:25:49 +08:00
fy53888
579beb6833 修复后端生成类型 Integer 出错的问题 2025-08-09 22:03:31 +08:00
fy53888
22c0c733f6 更新获取Java类型后端生成類型 Integer出錯的問 2025-08-09 22:01:29 +08:00
fy53888
9f4a2256b4 更新后端生成類型 Integer出錯的問 2025-08-09 21:56:16 +08:00
fy53888
5a4d76ac09 更新后端生成類型 Integer出錯的問題 2025-08-09 21:55:23 +08:00
Administrator
5a2e08f87d 问题概述
1.保存消息和计费逻辑存在耦合
2.修改计费逻辑:
按次计费被阈值限制:旧逻辑把 TIMES 分支放在 totalTokens ≥ 100 的大分支里,导致没到100 token时不扣费,违背“每次调用就扣费”的语义。
token累计不当:TIMES 分支只扣费不处理累计,同时在 totalTokens < 100 时不会进入任何TIMES逻辑,累计会无意义增长。
粒度不稳定:TOKEN 计费一旦达阈值就把 total 全扣完并清零,不利于对账与用户体验。
打印方式:使用 System.out.println,不利于生产追踪。
3.建议数据库不要存扣除金额和累计消耗token,消息表里不需要存“累计到目前为止多少”,否则每条消息都变成快照,既冗余又易不一致

改动要点
1.新增独立方法
saveMessage(ChatRequest): 只落库。
publishBillingEvent(ChatRequest): 只发布异步计费事件。
保留组合方法 saveMessageAndPublishEvent(ChatRequest) 以便需要一行调用时使用。
调用处已改为“先保存,再发布事件”
SseServiceImpl: 先 saveMessage,再 publishBillingEvent。
SSEEventSourceListener: 同上。
DifyServiceImpl: 同上。

2.计费模式分流:
TIMES:每次调用直接扣费,不累计。
TOKEN:按阈值(100)批量扣费,保留余数,账单颗粒稳定。
保留余数:total = prev + delta;billable = floor(total/threshold)threshold;remainder = total % threshold。
日志替换:统一使用 log.debug。
结构更清晰、可维护。
所有金额计算统一用 BigDecimal,保留两位小数,RoundingMode.HALF_UP
按次计费:每次直接扣费(BigDecimal),边界转 Double
按 token 计费:按阈值批量结算,保留余数;费用=单价(BigDecimal)×可结算token数
2025-08-08 13:39:37 +08:00
l90215
838a393abc feat: 添加空格 2025-08-07 21:13:02 +08:00
evo
210a9d9b14 Merge pull request #159 from MuSan-Li/feature_20250807_fix_code_generator
feat: 代码生成模板优化
2025-08-07 09:03:08 +08:00
l90215
66518925c1 feat: 代码生成模板优化 2025-08-07 09:02:16 +08:00
evo
19d3f018b8 Merge pull request #158 from MuSan-Li/feature_20250805_fix_data_model_del
feat: 优化删除数据模型也删除模型字段sql条件
2025-08-05 22:11:00 +08:00
l90215
450ec6db44 feat: 优化删除数据模型也删除模型字段sql条件 2025-08-05 22:06:05 +08:00
evo
0bb897de1f Merge pull request #157 from MuSan-Li/feature_20250805_fix_data_model_del
feat: 优化删除数据模型也删除模型字段&添加mapper文件注解
2025-08-05 19:51:28 +08:00
l90215
ab9ff52200 feat: 优化删除数据模型也删除模型字段&添加mapper文件注解 2025-08-05 19:50:29 +08:00
evo
d94bcf250e Merge pull request #156 from MuSan-Li/feature_20250804_fix_bug
feat: 优化空指针bug&格式化代码&初始化用户知识库角色配置
2025-08-04 20:54:27 +08:00
l90215
10708b7625 feat: 优化空指针bug&格式化代码&初始化用户知识库角色配置 2025-08-04 20:50:49 +08:00
ageerle
5e032a68d5 Merge pull request #154 from violateer/feature/knowledge-role
添加删除知识库角色组时关联删除数据
2025-08-03 09:59:50 +08:00
violateer
bd41ff0f28 添加删除知识库角色组时关联删除数据 2025-08-02 22:29:02 +08:00
evo
3ffba456f4 Merge pull request #153 from MuSan-Li/feature_20250802_fix_k_sql
feat: 调整知识库角色sql
2025-08-02 21:25:52 +08:00
l90215
993346534e feat: 调整知识库角色sql 2025-08-02 21:25:16 +08:00
evo
38169ba90d 修复查询字段名称
feat: 修复一些逻辑问题
2025-08-02 19:39:23 +08:00
l90215
b003e1b3bb feat: 修复查询字段名称 2025-08-02 19:38:50 +08:00
l90215
f36dda74a1 feat: 修复一些逻辑问题 2025-08-02 19:37:07 +08:00
evo
c609095c69 Merge pull request #151 from MuSan-Li/feature_20250801_fix_menu_sql
feat: 调整菜单结构sql
2025-08-02 18:37:47 +08:00
l90215
93e34b3f03 feat: 调整菜单结构sql 2025-08-02 18:36:50 +08:00
ageerle
cc4ca69640 Merge pull request #143 from violateer/feature/knowledge-role
添加知识库权限控制功能
2025-08-02 15:56:42 +08:00
ageerle
85930f4b12 Merge pull request #132 from HHANG52/main
feat(chat): 修复仅非管理员设置根据用户 ID查询聊天记录
2025-08-02 15:56:31 +08:00
ageerle
3c19a2b7a4 Merge pull request #142 from keke-cxn/main
修复:请求地址'/system/role/authUser/selectAll',发生未知异常:cn.dev33.satoken.exce…
2025-08-02 15:56:16 +08:00
keke
21c390c4d6 修复:1.在使用dify的时候,发送1+1=?模型返回2,但是第二次问他为什么等于2的时候,模型无法获取上下文信息,经过排查,发现缺少conversationId,session表需要添加conversationId字段
2.在使用dify的时候message表中并没有存储模型返回的消息,并且扣除费用、
2025-08-02 13:18:30 +08:00
ageerle
cdef9e1c89 feat: 增加演示模式 2025-07-31 14:58:29 +08:00
evo
3c60f43daa Merge pull request #147 from MuSan-Li/feature_20250721_generate_code
feat: 调整生成代码结构
2025-07-30 09:31:04 +08:00
l90215
a24fa7a2b4 feat: 调整生成代码结构 2025-07-30 09:24:31 +08:00
ageerle
21f8462ebb Merge pull request #146 from MuSan-Li/feature_20250721_generate_code
feat: 调整生成代码结构
2025-07-30 09:10:32 +08:00
l90215
897222c41c feat: 调整生成代码结构 2025-07-29 23:34:39 +08:00
ageerle
227670df9d Merge pull request #145 from MuSan-Li/feature_20250721_generate_code
feat: 提交替换代码生成sql
2025-07-29 09:43:29 +08:00
l90215
6b2cf1a3c3 feat: 提交替换代码生成sql 2025-07-28 17:47:12 +08:00
ageerle
37f832c31c Merge pull request #144 from MuSan-Li/feature_20250721_generate_code
替换代码生成
2025-07-28 09:32:19 +08:00
l90215
25cf1f9744 feat: 更新数据库密码... 2025-07-27 22:47:55 +08:00
l90215
a48178685c feat: 更新代码生成功能-优化逻辑 2025-07-27 22:37:26 +08:00
l90215
7e60cb357f feat: 更新代码生成功能-优化逻辑 2025-07-27 00:33:33 +08:00
l90215
915a393427 feat: 更新代码生成功能-优化取数逻辑 2025-07-24 12:36:26 +08:00
l90215
de323d8c45 feat: 更新代码生成功能-添加数据模型批量添加字段数据 2025-07-23 10:46:29 +08:00
l90215
ffe4867d40 feat: 更新代码生成功能-二阶段 2025-07-22 18:50:37 +08:00
l90215
8b3c0b4134 feat: 更新代码生成功能-一阶段 2025-07-21 16:03:08 +08:00
violateer
999282210e 添加知识库权限控制功能 2025-07-20 10:17:50 +08:00
violateer
a99344813f 添加知识库权限控制功能 2025-07-20 10:05:38 +08:00
ageerle
63ec00cd71 Merge pull request #136 from Administratos-User/main
修复token不扣费和扣费异常的问题
2025-07-16 15:53:56 +08:00
ageerle
7eebd87cc8 Merge pull request #141 from lixiang-cell/lixiang
存储向量库同时存储元数据的fid和docId,提供根据fid docId删除
2025-07-16 15:52:19 +08:00
keke
9a816bb0c7 修复:当前用户【如果没有绑定任何部门的情况下】在查询用户列表分页的时候 调用 SysUserMapper 的 selectPageUserList() 时候报错:sql错误 2025-07-15 00:02:44 +08:00
keke
bc6ed508b0 修复:1.用户原本绑定了岗位,若想要取消所有的岗位,没有进行逻辑实现
2.用户原本绑定了角色,若想要取消所有的角色,没有进行逻辑实现
2025-07-14 23:47:47 +08:00
keke
d64abaaed3 修复:请求地址'/system/role/authUser/selectAll',发生未知异常:cn.dev33.satoken.exception.NotWebContextException: 非Web上下文无法获取Request 2025-07-14 23:39:47 +08:00
lixiang
285aa2ae62 存储向量库同时存储元数据的fid和docId,提供根据fid docId删除 2025-07-14 17:15:29 +08:00
Administratos-User
99114d3301 修复token不扣费和扣费异常的问题
1.本次提交的token数+未付费token数 判断大于100token的时候就进行扣费,当然这里还可以改成更多,我觉得100合适。
2.不需要进行扣费的地方屏蔽了相关代码。
3.SessionId传递异常 建议前端传递uuid,也就是每次会话的id。
2025-07-11 11:59:20 +08:00
HHANG
e857f0345b Merge branch 'ageerle:main' into main 2025-07-11 10:32:12 +08:00
ageerle
138fa5f0e9 Merge pull request #133 from MuSan-Li/feature_20250516_add_dept_error
feat: 修复打开岗位管理爆错
2025-07-11 09:49:55 +08:00
l90215
241c6dc57a feat: 修复打开岗位管理爆粗 2025-07-10 23:53:17 +08:00
ageerle
6005339ec8 upadate md 2025-07-10 23:00:04 +08:00
ageerle
db892d35fb feat: 更新项目说明 2025-07-10 17:12:20 +08:00
HHANG
605b223985 feat(chat): 修复仅非管理员设置根据用户 ID查询聊天记录
- 在 ChatMessageServiceImpl 类中,仅当用户不是超级管理员时,才自动设置消息的用户 ID,确保了超级管理员可以查看对话聊天
2025-07-09 16:19:48 +08:00
ageerle
e117ec8e27 feat: 更新项目说明 2025-07-09 14:27:55 +08:00
ageerle
5e8db861ea feat: 更新项目说明 2025-07-08 15:30:25 +08:00
ageerle
fa2791e2e3 feat: 更新项目说明 2025-07-08 14:31:01 +08:00
ageerle
f1f7cb1084 feat: 更新项目说明 2025-07-08 12:26:24 +08:00
ageerle
acb1f27c37 feat: 更新项目说明 2025-07-08 12:10:03 +08:00
ageerle
e46245d97d feat: 更新项目说明 2025-07-07 13:44:55 +08:00
ageerle
0eff37fa51 ai编程助手 2025-07-07 12:36:53 +08:00
ageerle
c1a178c0be Merge remote-tracking branch 'origin/main' 2025-07-07 12:33:21 +08:00
ageerle
94d4446321 feat: 编程助手 2025-07-07 12:33:06 +08:00
lindaxia
d7930ad713 修复导包问题 2025-07-06 08:12:38 +08:00
lindaxia
48270add01 Merge branch 'main' of https://github.com/ageerle/ruoyi-ai 2025-07-06 07:52:25 +08:00
lindaxia
3786644a25 新增小程序接口 2025-07-06 07:51:38 +08:00
ageerle
53532681d3 Merge pull request #131 from Cyclones-Y/main
[fix][feat](chat): 修复“自动注入警告 => 用户未登录”; 添加会话中SaToken-token值传递和使用;
2025-07-05 21:29:24 +08:00
Yzm
daa7b7315b [fix][feat](chat): 添加会话中SaToken-token值传递和使用;修复“自动注入警告 => 用户未登录”
- 新增 BaseContext 类,用于保存和获取当前登录用户的 token
- 在 OpenAIServiceImpl 和 imageServiceImpl 中获取当前会话 token
- 将 token 作为参数传递给 SSEEventSourceListener
- 在 SSEEventSourceListener 中使用 token 进行用户身份验证和权限控制
- 修改 LoginHelper,增加根据 token 获取登录用户信息的方法
- 更新 InjectionMetaObjectHandler,使用 BaseContext 获取当前 token
- 修复对话时出先”自动注入警告 => 用户未登录“
2025-07-05 19:12:33 +08:00
ageerle
df3b687be4 Merge pull request #129 from MuSan-Li/feature_20250703_add_model_category_select
feat: 模型管理增加模型分类下拉框
2025-07-03 10:48:43 +08:00
l90215
ecb5ef32fc feat: 模型管理增加模型分类下拉框 2025-07-03 10:29:25 +08:00
evo
e5116472ed Merge branch 'ageerle:main' into main 2025-07-03 09:50:59 +08:00
lindaxia
e58aeb5361 更新README.md 2025-07-02 11:04:35 +08:00
lindaxia
b4306289f0 fix weaviate向量库根据数据类进行删除 2025-07-01 18:50:12 +08:00
lindaxia
d9c47bd983 修复删除知识库清空相关表 2025-07-01 18:06:49 +08:00
evo
8ddbb43dde Merge branch 'ageerle:main' into main 2025-07-01 10:02:32 +08:00
ageerle
dfe8c7dc85 Merge pull request #127 from Cyclones-Y/main
feat(chat): 集成 FastGPT 聊天模型
2025-06-30 23:21:31 +08:00
Yzm
fd94a1772f feat(chat): 集成 FastGPT 聊天模型- 在 ChatModeType 枚举中添加 FASTGPT 选项- 新增 FastGPT 相关的实体类和请求响应类
- 实现 FastGPT聊天服务接口
- 添加 FastGPT SSE 事件监听器
2025-06-30 22:03:31 +08:00
ageerle
c105d47d99 Merge pull request #125 from MuSan-Li/feature_20250626_fix_update_role
修复修改角色时候报错
2025-06-27 10:45:32 +08:00
l90215
4e2ec2dc82 feat: 修复修改角色时候报错&优化一些代码风格 2025-06-26 10:19:08 +08:00
evo
614280d8ea Merge branch 'ageerle:main' into main 2025-06-25 12:57:49 +08:00
ageerle
2fae8d0ad0 feat: 更新任务规划演示 2025-06-24 14:21:43 +08:00
ageerle
d7c2d1bcf3 feat: 更新任务规划演示 2025-06-24 14:16:40 +08:00
ageerle
122f63dfbd Merge remote-tracking branch 'origin/main' 2025-06-24 13:52:01 +08:00
ageerle
719e968192 feat: 更新任务规划演示 2025-06-24 13:51:51 +08:00
evo
bf790ceb51 Merge branch 'ageerle:main' into main 2025-06-24 11:43:08 +08:00
ageerle
de5488bd8c Merge pull request #123 from abin0515/one-step-script
one-step-script: 修改MacOS上运行快速启动脚本遇到的bug
2025-06-24 10:30:49 +08:00
GH Action - Upstream Sync
c77a245a4d Merge branch 'main' of https://github.com/ageerle/ruoyi-ai 2025-06-24 01:57:42 +00:00
Bin Xiao
6dcd8823cd one-step-script: 修改MacOS上运行快速启动脚本遇到的bug 2025-06-23 18:21:01 -04:00
ageerle
8be480e06c Update README.md 2025-06-23 16:49:13 +08:00
ageerle
11286de676 Merge pull request #118 from MuSan-Li/feature_20250610_add_prompt_template
Feature 20250610 add prompt template
2025-06-23 16:36:40 +08:00
MuSan-Li
5aaf0a672c feat: 删除fork文件 2025-06-12 17:55:19 +08:00
MuSan-Li
0089706336 添加提示词模板字典相关SQL 2025-06-12 17:37:04 +08:00
MuSan-Li
cc129801b9 添加提示词模板 2025-06-12 17:06:06 +08:00
GH Action - Upstream Sync
e1dc22348c Merge branch 'main' of https://github.com/ageerle/ruoyi-ai 2025-06-07 01:53:26 +00:00
ageerle
f37e4da669 feat: 更新二维码 2025-06-06 10:13:02 +08:00
GH Action - Upstream Sync
3e097d9a68 Merge branch 'main' of https://github.com/ageerle/ruoyi-ai 2025-06-06 01:54:02 +00:00
ageerle
97ae5a46cd feat: 调整sql脚本 2025-06-05 16:16:32 +08:00
ageerle
baa664ac4f feat: 图片识别功能优化 2025-06-05 16:00:06 +08:00
ageerle
353fbf26b8 Merge pull request #115 from Code-Mr-Jiu/main
上传图片支持使用后台image分类下通义千问模型
2025-06-05 13:53:20 +08:00
ageerle
f79b4ec012 Merge pull request #114 from Code-Mr-Jiu/jiuyi-dev
上传图片支持使用后台image分类模型
2025-06-05 13:53:09 +08:00
酒亦
0a73cb4e17 上传图片支持使用后台image分类下通义千问模型 2025-06-05 12:05:28 +08:00
酒亦
d635e30b4a 上传图片支持使用后台image分类模型 2025-06-02 08:11:22 +08:00
evo
ca50d1ddfb Create main.yml 2025-05-30 16:05:04 +08:00
266 changed files with 21620 additions and 4654 deletions

447
README.md
View File

@@ -1,10 +1,6 @@
# RuoYi AI
<!-- PROJECT SHIELDS -->
<div align="center">
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
@@ -12,295 +8,153 @@
[![Issues][issues-shield]][issues-url]
[![MIT License][license-shield]][license-url]
<img src="image/00.png" alt="RuoYi AI Logo" width="120" height="120">
<!-- PROJECT LOGO -->
<br />
### 企业级AI助手平台
*开箱即用的智能AI平台深度集成 FastGPT、扣子(Coze)、DIFY 等主流AI平台提供先进的RAG技术和多模型支持*
<img style="text-align: center;" src="image/00.png" alt="Logo" width="150" height="150">
**[🇺🇸 English](README_EN.md)** | **[📖 使用文档](https://doc.pandarobot.chat)** | **[🚀 在线体验](https://web.pandarobot.chat)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
<h3 style="text-align: center;">快速搭建属于自己的 AI 助手平台</h3>
<p style="text-align: center;">
全新升级,开箱即用,简单高效
<br />
<a href="https://doc.pandarobot.chat"><strong>探索本项目的文档 »</strong></a>
<br />
<br />
<a href="https://web.pandarobot.chat">项目预览</a>
·
<a href="https://github.com/ageerle/ruoyi-ai/issues">报告Bug</a>
·
<a href="https://github.com/ageerle/ruoyi-ai/issues">提出新特性</a>
</p>
## 快速启动
### 拉取镜像(最低配置2H2G):
```bash
script/deploy/deploy目录下执行: docker-compose up -d
```
### 通过脚本启动(最低配置4H4G):
1. 确认系统内已经安装好以下软件
- docker
- docker-compose
- git
- unzip
2. **克隆项目**
```bash
git clone https://github.com/ageerle/ruoyi-ai
cd ruoyi-ai/script/deploy/one-step-script
```
3. **启动部署脚本**
中文界面部署脚本(拉取gitee仓库)
```bash
./deploy-cn.sh
```
按照脚本提示一步步操作,如果是一台新服务器,选择默认配置,直接回车即可。
<img src="image/deploy-01.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
耐心等待安装完成...
英文界面部署脚本(拉取github仓库)
```bash
./deploy-en.sh
```
4. 如果在执行部署脚本过程中不需要在本地重新构建编译服务软件包以及重新封装容器镜像则需要在脚本交互提出以下问题时选择D按键进行直接部署否则就会执行全新的编译构建及容器封装之后再执行部署
```
已将模板文件复制到部署目录。
正在使用您的配置更新 .env 文件...
已使用您的配置更新 .env 文件。
正在使用您的配置更新 docker-compose.yaml 文件...
已使用您的配置更新 docker-compose.yaml 文件。
=== 构建或部署选项 ===
您想构建新镜像 (B) 还是直接使用现有镜像部署 (D)[B/d]:
```
5. **访问应用界面**
- 用户界面:`http://your-server-ip:8081`
- 管理员界面:`http://your-server-ip:8082`
## 目录
- [系统体验](#系统体验)
- [源码地址](#源码地址)
- [配套文档](#项目文档)
- [核心功能](#核心功能)
- [项目演示](#项目演示)
- [管理端](#管理端)
- [用户端](#用户端)
- [开发环境](#开发环境)
- [项目结构](#项目结构)
- [ruoyi-ai](#ruoyi-ai)
- [注意事项](#注意事项)
- [vben模板](#vben模板)
- [贡献者](#贡献者)
- [如何参与开源项目](#如何参与开源项目)
- [版本控制](#版本控制)
- [作者](#作者)
- [鸣谢](#鸣谢)
- [技术讨论群](#技术讨论群)
### 系统体验
- 用户端https://web.pandarobot.chat
- 演示账号: demo 密码demo123
- 管理端https://admin.pandarobot.chat
- 演示账号: admin 密码admin123
- 商业版体验商业版请联系下方小助手获取演示地址预计6月份上线
### 源码地址
[1]github
- 前端服务-用户端: https://github.com/ageerle/ruoyi-web
- 前端服务-管理端: https://github.com/ageerle/ruoyi-admin
- 后端服务https://github.com/ageerle/ruoyi-ai
[2]gitcode
- 前端服务-用户端https://gitcode.com/ageerle/ruoyi-web
- 前端服务-管理端: https://gitcode.com/ageerle/ruoyi-admin
- 后端服务https://gitcode.com/ageerle/ruoyi-ai
### 配套文档
- 配套文档: https://doc.pandarobot.chat
- 项目部署文档https://doc.pandarobot.chat/guide/introduction/
### 核心功能与技术亮点
#### 1. 全栈式开源系统
- 全套开源系统:提供完整的前端应用、后台管理,基于MIT协议开箱即用。
#### 2. 本地化 RAG 方案
- 基于 **Langchain4j** 框架,支持 Milvus/Weaviate/Qdrant 向量库,结合 BGE-large-zh-v1.5 本地向量化模型 实现高效文档检索与知识库构建。
- 支持 本地 LLM 接入,结合私有知识库实现安全可控的问答系统,避免依赖云端服务的隐私风险。
#### 3. 多模态 AI 引擎与工具集成
- 智能对话:支持 OpenAI GPT-4、Azure、ChatGLM 等主流模型,内置 SSE/WebSocket 协议实现低延迟交互,兼容 **扣子**、**DIFY** 等平台 API 调用。
- **Spring AI MCP** 支持:通过注解快速定义本地工具,支持调用 MCP 广场 的海量 MCP Server 服务,扩展模型能力边界。
#### 4. 企业级扩展与商业化支持
- 即时通讯集成:支持对接个人微信、企业微信及微信公众号,实现消息自动回复、用户管理与智能客服。
- 支付系统集成易支付、微信支付、Stripe 国际信用卡支付,满足商业化场景需求。
#### 5. 多媒体处理与创新功能
- AI 绘画:集成 DALL·E-3、MidJourney、Stable Diffusion支持文生图、图生图及风格化创作适用于营销素材生成与创意设计。
- PPT 制作:根据文本输入自动生成结构化幻灯片,支持自定义模板(需要使用三方平台 如:文多多)。
### 项目演示
#### mcp支持
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/mcp-01.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/mcp-02.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/mcp-03.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/mcp-04.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
</div>
## ✨ 核心亮点
### 🤖 智能AI引擎
- **多模型接入**:支持 OpenAI GPT-4、Azure、ChatGLM、通义千问、智谱AI 等主流模型
- **AI平台集成**:深度集成 **FastGPT**、**扣子(Coze)**、**DIFY** 等主流AI应用平台
- **Spring AI MCP 集成**基于模型上下文协议打造可扩展的AI工具生态系统
- **实时流式对话**:采用 SSE/WebSocket 技术,提供丝滑的对话体验
- **AI 编程助手**:内置智能代码分析和项目脚手架生成能力
### 🌟 AI平台生态集成
- **FastGPT 深度集成**:原生支持 FastGPT API包括知识库检索、工作流编排和上下文管理
- **扣子(Coze) 官方SDK**集成字节跳动扣子平台官方SDK支持Bot对话和流式响应
- **DIFY 完整兼容**:使用 DIFY Java Client支持应用编排、工作流和知识库管理
- **统一聊天接口**:提供统一的聊天服务接口,支持多平台无缝切换和负载均衡
### 🧠 本地化RAG方案
- **私有知识库**:基于 Langchain4j 框架 + BGE-large-zh-v1.5 中文向量模型
- **多种向量库**:支持 Milvus、Weaviate、Qdrant 等主流向量数据库
- **数据安全可控**:支持完全本地部署,保护企业数据隐私
- **灵活模型部署**:兼容 Ollama、vLLM 等本地推理框架
### 🎨 AI创作工具
- **AI 绘画创作**:深度集成 DALL·E-3、MidJourney、Stable Diffusion
- **智能PPT生成**:一键将文本内容转换为精美演示文稿
- **多模态理解**:支持文本、图片、文档等多种格式的智能处理
## 🚀 快速体验
### 在线演示
- **用户端体验**[web.pandarobot.chat](https://web.pandarobot.chat) (账号demo 密码demo123)
- **管理后台**[admin.pandarobot.chat](https://admin.pandarobot.chat) (账号admin 密码admin123)
### 项目源码
| 项目模块 | GitHub 仓库 | Gitee 仓库 | GitCode 仓库 |
|---------|------------|-----------|-------------|
| 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
### 合作项目
| 项目介绍 | GitHub 仓库 | Gitee 仓库 |
|:--------:|:----------:|:----------:|
| 前端简化版 | [ruoyi-element-ai](https://github.com/element-plus-x/ruoyi-element-ai) | [ruoyi-element-ai](https://gitee.com/he-jiayue/ruoyi-element-ai) |
## 🛠️ 技术架构
### 🏗️ 核心框架
- **后端架构**Spring Boot 3.4 + Spring AI + Langchain4j
- **数据存储**MySQL 8.0 + Redis + 向量数据库Milvus/Weaviate/Qdrant
- **前端技术**Vue 3 + Vben Admin + Naive UI
- **安全认证**Sa-Token + JWT 双重保障
### 🔧 系统组件
- **文档处理**PDF、Word、Excel 解析,图像智能分析
- **实时通信**WebSocket 实时通信SSE 流式响应
- **系统监控**:完善的日志体系、性能监控、服务健康检查
## 📚 使用文档
想要深入了解安装部署、功能配置和二次开发?
**👉 [完整使用文档](https://doc.pandarobot.chat)**
## 🤝 参与贡献
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪
### 贡献方式
1. **Fork** 项目到您的账户
2. **创建分支** (`git checkout -b feature/新功能名称`)
3. **提交代码** (`git commit -m '添加某某功能'`)
4. **推送分支** (`git push origin feature/新功能名称`)
5. **发起 Pull Request**
> 💡 **小贴士**:建议将 PR 提交到 GitHub我们会自动同步到其他代码托管平台
<a href="https://openomy.com/ageerle/ruoyi-ai" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.com/svg?repo=ageerle/ruoyi-ai&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
## 📄 开源协议
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
## 🙏 特别鸣谢
感谢以下优秀的开源项目为本项目提供支持:
- [Spring AI Alibaba Copilot](https://github.com/springaialibaba/spring-ai-alibaba-copilot) - 基于spring-ai-alibaba 的智能编码助手
- [Spring AI](https://spring.io/projects/spring-ai) - Spring 官方 AI 集成框架
- [Langchain4j](https://github.com/langchain4j/langchain4j) - 强大的 Java LLM 开发框架
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 成熟的企业级快速开发框架
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - 现代化的 Vue 后台管理模板
- [chatgpt-java](https://github.com/Grt1228/chatgpt-java) - 优秀的 ChatGPT Java SDK
## 🌐 生态伙伴
- [PPIO 派欧云](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - 提供高性价比的 GPU 算力和模型 API 服务
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - 万卡RTX40系GPU+海内外主流模型API服务秒级响应按量计费新客免费用。
## 💬 社区交流
<div align="center">
<table>
<tr>
<td align="center">
<img src="image/wx.png" alt="微信二维码" width="200" height="200"><br>
<strong>扫码添加作者微信</strong><br>
<em>邀请进群学习</em>
</td>
<td align="center">
<img src="image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
<strong>QQ技术交流群</strong><br>
<em>技术讨论</em>
</td>
</tr>
</table>
#### 用户端
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/08.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/09.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/10.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/11.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
</div>
#### 管理端
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/02.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/03.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/04.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/05.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
---
<div align="center">
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[🍴 Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.pandarobot.chat)**
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
</div>
### 开发环境
1. jdk 17
2. mysql 5.7、8.0
3. redis 版本必须 >= 5.X
4. maven 3.8+
5. nodejs 20+ & pnpm
- 附-部署配套视频https://www.bilibili.com/video/BV1jDXkYWEba
<div>
<img src="image/教程搭建.png" alt="drawing" width="600px" height="300px"/>
</div>
### 项目结构
- RuoYi-AI
```
├─ ruoyi-admin // 管理模块
│ └─ RuoYiApplication // 启动类
│ └─ RuoYiServletInitializer // 容器部署初始化类
│ └─ resources // 资源文件
│ └─ i18n/messages.properties // 国际化配置文件
│ └─ application.yml // 框架总配置文件
│ └─ application-dev.yml // 开发环境配置文件
│ └─ application-prod.yml // 生产环境配置文件
│ └─ banner.txt // 框架启动图标
│ └─ logback-plus.xml // 日志配置文件
│ └─ ip2region.xdb // IP区域地址库
├─ ruoyi-common // 通用模块
│ └─ ruoyi-common-bom // common依赖包管理
└─ ruoyi-common-chat // 聊天模块
│ └─ ruoyi-common-core // 核心模块
│ └─ ruoyi-common-doc // 系统接口模块
│ └─ ruoyi-common-encrypt // 数据加解密模块
│ └─ ruoyi-common-excel // excel模块
│ └─ ruoyi-common-idempotent // 幂等功能模块
│ └─ ruoyi-common-json // 序列化模块
│ └─ ruoyi-common-log // 日志模块
│ └─ ruoyi-common-mail // 邮件模块
│ └─ ruoyi-common-mybatis // 数据库模块
│ └─ ruoyi-common-oss // oss服务模块
│ └─ ruoyi-common-pay // 支付模块
│ └─ ruoyi-common-ratelimiter // 限流功能模块
│ └─ ruoyi-common-redis // 缓存服务模块
│ └─ ruoyi-common-satoken // satoken模块
│ └─ ruoyi-common-security // 安全模块
│ └─ ruoyi-common-sensitive // 脱敏模块
│ └─ ruoyi-common-sms // 短信模块
│ └─ ruoyi-common-tenant // 租户模块
│ └─ ruoyi-common-translation // 通用翻译模块
│ └─ ruoyi-common-web // web模块
├─ ruoyi-modules // 模块组
│ └─ ruoyi-demo // 演示模块
│ └─ ruoyi-system // 业务模块
├─ .run // 执行脚本文件
├─ .editorconfig // 编辑器编码格式配置
├─ LICENSE // 开源协议
├─ pom.xml // 公共依赖
├─ README.md // 框架说明文件
```
### 注意事项
- vben模板
Qvben5 的模板默认是没有的吗?
Avben模板是收费的 请联系vben-vue-plus作者获取。
### 版本控制
该项目使用Git进行版本管理。您可以在repository参看当前可用版本。
### 版权说明
该项目使用了MIT授权许可详情请参阅 [LICENSE.txt](https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE)
### 项目现状
目前项目还处于早期阶段距离成熟还有很长的路要走。由于个人精力有限项目的发展速度受到了一定的限制。为了加快项目的进度我真诚地希望更多人能够参与到项目中来。无论是经验丰富的开发者还是刚刚入门的小白我都热烈欢迎你们提交Pull RequestPR👏👏👏。即使代码修改得很少或者存在一些错误都没有关系。我会认真审核每一位贡献者的代码并和大家一起完善项目⛽
### 开发计划
| 主题 | 方向 | 时间节点 |
| --- |-----------------------------------|--------|
| 前端简化版 | 与element-plus-x框架合作推出基于该框架的前端简化版 | 2025.5 |
| agent2agent | Agent2Agent协议支持 | 2025.6 |
| 流程编排 | 通过可视化界面和灵活的配置方式快速构建AI应用 | 2025.7 |
- 感谢
最后我要感谢RuoYi-Vue-Plus、chatgpt-java、chatgpt-web-midjourney-proxy等优秀框架。正是因为这些项目的开源和共享我才能够在这个基础上进行开发使我们的项目能够取得今天的成果。再次感谢这些项目及其背后的开发者们
希望更多志同道合的朋友能够加入我们共同推动这个项目的发展。让我们一起努力将这个项目打造成一个真正成熟、实用的AI平台
#### 如何参与开源项目
贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献,我们都非常感谢!🙏
1. Fork 这个项目
2. 创建你的功能分支 (`git checkout -b feature/dev`)
3. 提交你的更改 (`git commit -m 'Add some dev'`)
4. 推送到分支 (`git push origin feature/dev`)
5. 打开拉取请求
6. pr请提交到GitHub上会定时同步到gitee
#### 项目文档
1. 项目文档基于vitepress构建
2. 按照[如何参与开源项目](#如何参与开源项目)拉取https://github.com/ageerle/ruoyi-doc
3. 安装依赖npm install
4. 启动项目npm run docs:dev
5. 主页路径docs/guide/introduction/index.md
### 鸣谢
- [chatgpt-java](https://github.com/Grt1228/chatgpt-java)
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus)
- [chatgpt-web-midjourney-proxy](https://github.com/Dooy/chatgpt-web-midjourney-proxy)
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin)
- [Naive UI](https://www.naiveui.com)
<!-- links -->
[your-project-path]:https://github.com/ageerle/ruoyi-ai
<!-- Badge Links -->
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
@@ -308,29 +162,6 @@
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
[issues-url]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
[license-shield]: https://img.shields.io/github/license/ageerle/ruoyi-ai.svg?style=flat-square
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/master/LICENSE.txt
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
## 🌿 第三方生态
- [PPIO 派欧云:一键调用高性价比的开源模型 API 和 GPU 容器](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai)
### 附:技术讨论群
#### 技术交流(如需进群请添加小助手)
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/wx.png" alt="drawing" style="width: 400px; height: 400px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
</div>
#### 进群学习
🏠 小助手wxruoyi-ai加人备注ruoyi-ai
🏠 小助手qq1603234088 加人备注ruoyi-ai
👏👏👏 ruoyi-ai官方交流群qq区
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/qq.png" alt="drawing" style="width: 400px; height: 400px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
</div>
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE

167
README_EN.md Normal file
View File

@@ -0,0 +1,167 @@
# RuoYi AI
<div align="center">
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![MIT License][license-shield]][license-url]
<img src="image/00.png" alt="RuoYi AI Logo" width="120" height="120">
### Enterprise-Grade AI Assistant Platform
*Production-ready AI platform with deep integration of FastGPT, Coze, DIFY and advanced RAG technology*
**[📖 中文文档](README.md)** | **[📚 Documentation](https://doc.pandarobot.chat)** | **[🚀 Live Demo](https://web.pandarobot.chat)** | **[🐛 Report Bug](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Request Feature](https://github.com/ageerle/ruoyi-ai/issues)**
</div>
## ✨ Key Features
### 🤖 Advanced AI Engine
- **Multi-Model Support**: OpenAI GPT-4, Azure, ChatGLM, Qwen, ZhipuAI
- **AI Platform Integration**: Deep integration with **FastGPT**, **Coze**, **DIFY** and other leading AI platforms
- **Spring AI MCP Integration**: Extensible tool ecosystem with Model Context Protocol
- **Streaming Chat**: Real-time SSE/WebSocket communication
- **AI Copilot**: Intelligent code analysis and project scaffolding
### 🌟 AI Platform Ecosystem
- **FastGPT Deep Integration**: Native FastGPT API support with knowledge base retrieval, workflow orchestration and context management
- **Coze Official SDK**: Integration with ByteDance Coze platform official SDK, supporting Bot conversations and streaming responses
- **DIFY Full Compatibility**: Using DIFY Java Client for app orchestration, workflows and knowledge base management
- **Unified Chat Interface**: Standardized chat service interface supporting seamless platform switching and load balancing
### 🧠 Enterprise RAG Solution
- **Local Knowledge Base**: Langchain4j + BGE-large-zh-v1.5 embeddings
- **Vector Database Support**: Milvus, Weaviate, Qdrant
- **Privacy-First**: On-premise deployment with local LLM support
- **Ollama & vLLM Compatible**: Flexible model deployment options
### 🎨 Creative AI Tools
- **AI Art Generation**: DALL·E-3, MidJourney, Stable Diffusion integration
- **PPT Creation**: Automated slide generation from text input
- **Multi-Modal Processing**: Text, image, and document understanding
## 🚀 Quick Start
### Live Demo
- **User Portal**: [web.pandarobot.chat](https://web.pandarobot.chat) (demo/demo123)
- **Admin Panel**: [admin.pandarobot.chat](https://admin.pandarobot.chat) (admin/admin123)
### Source Code
| Component | GitHub | Gitee | GitCode |
|-----------|--------|-------|---------|
| Backend API | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
| User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
| Admin Frontend | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
### Collaborative Projects
| Project Description | GitHub Repository | Gitee Repository |
|:-------------------:|:-----------------:|:----------------:|
| Simplified Frontend | [ruoyi-element-ai](https://github.com/element-plus-x/ruoyi-element-ai) | [ruoyi-element-ai](https://gitee.com/he-jiayue/ruoyi-element-ai) |
## 🛠️ Tech Stack
### Core Framework
- **Backend**: Spring Boot 3.4, Spring AI, Langchain4j
- **Database**: MySQL 8.0, Redis, Vector Databases (Milvus/Weaviate/Qdrant)
- **Frontend**: Vue 3, Vben Admin, Naive UI
- **Authentication**: Sa-Token, JWT
### System Components
- **File Processing**: PDF, Word, Excel parsing, intelligent image analysis
- **Real-time Communication**: WebSocket real-time communication, SSE streaming
- **System Monitoring**: Comprehensive logging, performance monitoring, health checks
## 📚 Documentation
For detailed setup, configuration, and development guides, visit our comprehensive documentation:
**[📖 Official Documentation](https://doc.pandarobot.chat)**
## 🤝 Contributing
We welcome contributions from developers of all skill levels! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated.
### How to Contribute
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
*Please submit PRs to GitHub - they will be synchronized to other platforms automatically.*
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
Special thanks to these amazing open source projects:
- [Spring AI Alibaba Copilot](https://github.com/springaialibaba/spring-ai-alibaba-copilot) - Intelligent coding assistant based on spring-ai-alibaba with MCP protocol integration for project analysis and code generation
- [Spring AI](https://spring.io/projects/spring-ai) - Spring's AI integration framework
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Java LLM framework
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Enterprise development framework
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Vue admin template
- [chatgpt-java](https://github.com/Grt1228/chatgpt-java) - ChatGPT Java SDK
## 🌐 Ecosystem Partners
- [PPIO Cloud](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - Cost-effective GPU containers and model APIs
## 💬 Community
<div align="center">
<table>
<tr>
<td align="center">
<img src="image/wx.png" alt="WeChat" width="200" height="200"><br>
<strong>Add Author WeChat</strong><br>
<em>Scan to join learning group</em>
</td>
<td align="center">
<img src="image/qq.png" alt="QQ Group" width="200" height="200"><br>
<strong>QQ Group</strong><br>
<em>Technical discussion</em>
</td>
</tr>
</table>
</div>
---
<div align="center">
**[⭐ Star this repo](https://github.com/ageerle/ruoyi-ai)** • **[🍴 Fork it](https://github.com/ageerle/ruoyi-ai/fork)** • **[📖 中文文档](README.md)** • **[📚 Documentation](https://doc.pandarobot.chat)**
*Built with ❤️ by the RuoYi AI community*
</div>
<!-- Badge Links -->
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
[contributors-url]: https://github.com/ageerle/ruoyi-ai/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
[forks-url]: https://github.com/ageerle/ruoyi-ai/network/members
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
[issues-shield]: https://img.shields.io/github/issues/ageerle/ruoyi-ai.svg?style=flat-square
[issues-url]: https://github.com/ageerle/ruoyi-ai/issues
[license-shield]: https://img.shields.io/github/license/ageerle/ruoyi-ai.svg?style=flat-square
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

View File

@@ -17,8 +17,8 @@ spring:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username: ruoyi-ai
password: ruoyi-ai
username: root
password: root
hikari:
# 最大连接池数量

View File

@@ -1,16 +1,13 @@
# 项目相关配置
ruoyi:
# 名称
name: "ruoyi"
name: "ruoyi-ai"
# 版本
version: ${revision}
# 版权年份
copyrightYear: 2025
# 实例演示开关
demoEnabled: true
# 获取ip地址开关
addressEnabled: false
demoEnabled: false
captcha:
enable: false
@@ -133,6 +130,9 @@ security:
- /chat/send
# 文件上传
- /chat/upload
# 代码生成调用
- /tool/gen/getByTableName
- /tool/gen/batchGenCode
# 静态资源
- /*.html
- /**/*.html
@@ -161,6 +161,9 @@ tenant:
- sys_user_post
- sys_user_role
knowledge-role:
enable: false
# MyBatisPlus配置
# https://baomidou.com/config/
mybatis-plus:
@@ -185,7 +188,7 @@ mybatis-plus:
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
logImpl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
# 是否打印 Logo banner
banner: true

View File

@@ -0,0 +1,15 @@
package org.ruoyi.common.chat.entity.chat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FastGPTAnswerResponse {
private String id;
private String object;
private long created;
private String model;
private List<FastGPTChatChoice> choices;
}

View File

@@ -0,0 +1,25 @@
package org.ruoyi.common.chat.entity.chat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FastGPTChatChoice implements Serializable {
private long index;
/**
* 请求参数stream为true返回是delta
*/
@JsonProperty("delta")
private Message delta;
/**
* 请求参数stream为false返回是message
*/
@JsonProperty("message")
private Message message;
@JsonProperty("finish_reason")
private String finishReason;
}

View File

@@ -0,0 +1,41 @@
package org.ruoyi.common.chat.entity.chat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.io.Serializable;
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class FastGPTChatCompletion extends ChatCompletion implements Serializable {
/**
* 是否使用FastGPT提供的上下文
*/
private String chatId;
/**
* 是否返回详细信息;stream模式下会通过event进行区分非stream模式结果保存在responseData中.
*/
private boolean detail;
/**
* 运行时变量
* 模块变量一个对象会替换模块中输入fastgpt框内容里的{{key}}
*/
private Variables variables;
/**
* responseChatItemId: string | undefined 。
* 如果传入,则会将该值作为本次对话的响应消息的 ID
* FastGPT 会自动将该 ID 存入数据库。请确保,
* 在当前chatId下responseChatItemId是唯一的。
*/
private String responseChatItemId;
}

View File

@@ -0,0 +1,20 @@
package org.ruoyi.common.chat.entity.chat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Variables implements Serializable {
private String uid;
private String name;
}

View File

@@ -61,4 +61,30 @@ public class ChatRequest {
*/
private String role;
/**
* 对话id(每个聊天窗口都不一样)
*/
private Long uuid;
/**
* 是否有附件
*/
private Boolean hasAttachment;
/**
* 是否自动切换模型
*/
private Boolean autoSelectModel;
/**
* 会话令牌为避免在非Web线程中获取Request入口处注入
*/
private String token;
/**
* 消息ID保存消息成功后设置用于后续扣费更新
*/
private Long messageId;
}

View File

@@ -117,6 +117,16 @@ public class LoginUser implements Serializable {
*/
private Long roleId;
/**
* 关联角色类型
*/
private String kroleGroupType;
/**
* 关联角色id
*/
private String kroleGroupIds;
/**
* 获取登录id
*/

View File

@@ -1,6 +1,7 @@
package org.ruoyi.common.core.manager;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.utils.Threads;
import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -0,0 +1,25 @@
package org.ruoyi.common.core.service;
/**
* @description: 基于ThreadLocal封装工具类用户保存和获取当前登录用户Sa-Token token值
* @author: yzm
**/
public class BaseContext {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
/**
* @description: 设置值
* @author: yzm
* @param: [token] 线程token
**/
public static void setCurrentToken(String token){
threadLocal.set(token);
}
/**
* @description: 获取值
* @author: yzm
**/
public static String getCurrentToken(){
return threadLocal.get();
}
}

View File

@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.ruoyi.common.core.domain.model.LoginUser;
import org.ruoyi.common.core.exception.ServiceException;
import org.ruoyi.common.core.service.BaseContext;
import org.ruoyi.common.core.utils.ObjectUtils;
import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.core.domain.BaseEntity;
@@ -91,7 +92,8 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
private LoginUser getLoginUser() {
LoginUser loginUser;
try {
loginUser = LoginHelper.getLoginUser();
String token = BaseContext.getCurrentToken();
loginUser = LoginHelper.getLoginUser(token);
} catch (Exception e) {
log.warn("自动注入警告 => 用户未登录");
return null;

View File

@@ -11,9 +11,12 @@ import org.ruoyi.enums.DataBaseType;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 数据库助手
@@ -78,4 +81,83 @@ public class DataBaseHelper {
public static List<String> getDataSourceNameList() {
return new ArrayList<>(DS.getDataSources().keySet());
}
/**
* 获取当前连接的所有表名称
*/
public static List<String> getCurrentDataSourceTableNameList() {
DataSource dataSource = DS.determineDataSource();
List<String> tableNames = new ArrayList<>();
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
String catalog = conn.getCatalog();
String schema = conn.getSchema();
// 获取所有表名
try (var resultSet = metaData.getTables(catalog, schema, "%", new String[]{"TABLE"})) {
while (resultSet.next()) {
String tableName = resultSet.getString("TABLE_NAME");
tableNames.add(tableName);
}
}
} catch (SQLException e) {
throw new ServiceException("获取表名称失败: " + e.getMessage());
}
return tableNames;
}
/**
* 获取指定表的字段信息
*
* @param tableName 表名
* @return 字段信息列表
*/
public static List<Map<String, Object>> getTableColumnInfo(String tableName) {
DataSource dataSource = DS.determineDataSource();
List<Map<String, Object>> columns = new ArrayList<>();
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
String catalog = conn.getCatalog();
String schema = conn.getSchema();
// 获取表字段信息
try (ResultSet resultSet = metaData.getColumns(catalog, schema, tableName, "%")) {
while (resultSet.next()) {
Map<String, Object> column = new HashMap<>();
column.put("columnName", resultSet.getString("COLUMN_NAME"));
column.put("columnComment", resultSet.getString("REMARKS"));
column.put("dataType", resultSet.getString("TYPE_NAME"));
column.put("columnSize", resultSet.getInt("COLUMN_SIZE"));
column.put("isNullable", "YES".equals(resultSet.getString("IS_NULLABLE")));
column.put("ordinalPosition", resultSet.getInt("ORDINAL_POSITION"));
// 设置默认值
String defaultValue = resultSet.getString("COLUMN_DEF");
column.put("columnDefault", defaultValue);
columns.add(column);
}
}
// 获取主键信息
try (ResultSet pkResultSet = metaData.getPrimaryKeys(catalog, schema, tableName)) {
List<String> primaryKeys = new ArrayList<>();
while (pkResultSet.next()) {
primaryKeys.add(pkResultSet.getString("COLUMN_NAME"));
}
// 标记主键字段
for (Map<String, Object> column : columns) {
String columnName = (String) column.get("columnName");
column.put("isPrimaryKey", primaryKeys.contains(columnName));
}
}
} catch (SQLException e) {
throw new ServiceException("获取表字段信息失败: " + e.getMessage());
}
return columns;
}
}

View File

@@ -9,6 +9,7 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.constant.TenantConstants;
import org.ruoyi.common.core.constant.UserConstants;
import org.ruoyi.common.core.domain.model.LoginUser;
@@ -29,6 +30,7 @@ import java.util.Set;
*
* @author Lion Li
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoginHelper {
@@ -82,6 +84,15 @@ public class LoginHelper {
return loginUser;
}
public static <T extends LoginUser> T getLoginUser(String token) {
SaSession session = StpUtil.getTokenSessionByToken(token);
if (ObjectUtil.isNull(session)) {
return null;
}
return (T) session.get(LOGIN_USER_KEY);
}
/**
* 获取用户id
*/

View File

@@ -2,6 +2,7 @@ package org.ruoyi.common.sms.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* SMS短信 配置属性
@@ -10,6 +11,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @version 4.2.0
*/
@Data
@Component
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {

View File

@@ -1,6 +1,9 @@
package org.ruoyi.common.web.config;
import org.ruoyi.common.web.interceptor.DemoModeInterceptor;
import org.ruoyi.common.web.interceptor.PlusWebInvokeTimeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
@@ -18,10 +21,35 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@AutoConfiguration
public class ResourcesConfig implements WebMvcConfigurer {
@Autowired(required = false)
private DemoModeInterceptor demoModeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 全局访问性能拦截
registry.addInterceptor(new PlusWebInvokeTimeInterceptor());
// 演示模式拦截器
if (demoModeInterceptor != null) {
registry.addInterceptor(demoModeInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns(
// 排除静态资源
"/css/**",
"/js/**",
"/images/**",
"/fonts/**",
"/favicon.ico",
// 排除错误页面
"/error",
// 排除API文档
"/*/api-docs/**",
"/swagger-ui/**",
"/webjars/**",
// 排除监控端点
"/actuator/**"
);
}
}
@Override

View File

@@ -4,6 +4,7 @@ import lombok.Data;
import org.ruoyi.common.web.enums.CaptchaCategory;
import org.ruoyi.common.web.enums.CaptchaType;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 验证码 配置属性
@@ -11,6 +12,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Lion Li
*/
@Data
@Component
@ConfigurationProperties(prefix = "captcha")
public class CaptchaProperties {

View File

@@ -0,0 +1,100 @@
package org.ruoyi.common.web.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.config.RuoYiConfig;
import org.ruoyi.common.core.exception.DemoModeException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 演示模式拦截器
* 全局拦截所有编辑操作POST、PUT、DELETE、PATCH
*
* @author ruoyi
*/
@Slf4j
@Component
public class DemoModeInterceptor implements HandlerInterceptor {
@Autowired
private RuoYiConfig ruoYiConfig;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果演示模式未开启,直接放行
if (!ruoYiConfig.isDemoEnabled()) {
return true;
}
String method = request.getMethod();
String requestURI = request.getRequestURI();
// 拦截所有编辑操作
if (isEditOperation(method)) {
// 排除一些特殊的只读操作
if (isExcludedPath(requestURI)) {
return true;
}
log.warn("演示模式拦截: {} {}", method, requestURI);
throw new DemoModeException();
}
return true;
}
/**
* 判断是否为编辑操作
*/
private boolean isEditOperation(String method) {
return "POST".equalsIgnoreCase(method)
|| "PUT".equalsIgnoreCase(method)
|| "DELETE".equalsIgnoreCase(method)
|| "PATCH".equalsIgnoreCase(method);
}
/**
* 判断是否为排除的路径这些路径即使是POST等方法也不拦截
*/
private boolean isExcludedPath(String requestURI) {
// 排除登录相关
if (requestURI.contains("/auth/login") || requestURI.contains("/auth/logout")) {
return true;
}
// 排除导出操作虽然是POST但是只读操作
if (requestURI.contains("/export")) {
return true;
}
// 排除查询操作一些复杂查询使用POST
if (requestURI.contains("/list") || requestURI.contains("/query")) {
return true;
}
// 排除聊天接口(核心功能)
if (requestURI.contains("/chat/send") || requestURI.contains("/chat/upload")) {
return true;
}
// 排除文件上传预览等只读操作
if (requestURI.contains("/upload") || requestURI.contains("/preview")) {
return true;
}
// 排除支付回调
if (requestURI.contains("/pay/returnUrl") || requestURI.contains("/pay/notifyUrl")) {
return true;
}
// 排除重置密码(用户自己的操作)
if (requestURI.contains("/auth/reset/password")) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,164 @@
<?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>
<groupId>com.example</groupId>
<artifactId>ruoyi-ai-copilot</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>copilot</name>
<description>SpringAI - Alibaba - Copilot</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.4.5</spring-boot.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- AspectJ Runtime -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JSON Schema Validation -->
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.0.87</version>
</dependency>
<!-- Diff Utils -->
<dependency>
<groupId>io.github.java-diff-utils</groupId>
<artifactId>java-diff-utils</artifactId>
<version>4.12</version>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 添加Maven编译器插件配置 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<parameters>true</parameters>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>

View File

@@ -0,0 +1,85 @@
package com.example.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import com.example.demo.config.AppProperties;
import com.example.demo.util.BrowserUtil;
/**
* 主要功能:
* 1. 文件读取、写入、编辑
* 2. 目录列表和结构查看
* 4. 连续性文件操作
*/
@SpringBootApplication
@EnableConfigurationProperties(AppProperties.class)
@EnableAspectJAutoProxy
public class CopilotApplication {
private static final Logger logger = LoggerFactory.getLogger(CopilotApplication.class);
@Autowired
private AppProperties appProperties;
@Autowired
private Environment environment;
public static void main(String[] args) {
SpringApplication.run(CopilotApplication.class, args);
}
/**
* 应用启动完成后的事件监听器
* 自动打开浏览器访问应用首页
*/
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
AppProperties.Browser browserConfig = appProperties.getBrowser();
if (!browserConfig.isAutoOpen()) {
logger.info("Browser auto-open is disabled");
return;
}
// 获取实际的服务器端口
String port = environment.getProperty("server.port", "8080");
String actualUrl = browserConfig.getUrl().replace("${server.port:8080}", port);
logger.info("Application started successfully!");
logger.info("Preparing to open browser in {} seconds...", browserConfig.getDelaySeconds());
// 在新线程中延迟打开浏览器,避免阻塞主线程
new Thread(() -> {
try {
Thread.sleep(browserConfig.getDelaySeconds() * 1000L);
if (BrowserUtil.isValidUrl(actualUrl)) {
boolean success = BrowserUtil.openBrowser(actualUrl);
if (success) {
logger.info("✅ Browser opened successfully: {}", actualUrl);
System.out.println("🌐 Web interface opened: " + actualUrl);
} else {
logger.warn("❌ Failed to open browser automatically");
System.out.println("⚠️ Please manually open: " + actualUrl);
}
} else {
logger.error("❌ Invalid URL: {}", actualUrl);
System.out.println("⚠️ Invalid URL configured: " + actualUrl);
}
} catch (InterruptedException e) {
logger.warn("Browser opening was interrupted", e);
Thread.currentThread().interrupt();
}
}).start();
}
}

View File

@@ -0,0 +1,140 @@
package com.example.demo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.nio.file.Paths;
import java.util.List;
/**
* 应用配置属性
*/
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private Workspace workspace = new Workspace();
private Security security = new Security();
private Tools tools = new Tools();
private Browser browser = new Browser();
// Getters and Setters
public Workspace getWorkspace() { return workspace; }
public void setWorkspace(Workspace workspace) { this.workspace = workspace; }
public Security getSecurity() { return security; }
public void setSecurity(Security security) { this.security = security; }
public Tools getTools() { return tools; }
public void setTools(Tools tools) { this.tools = tools; }
public Browser getBrowser() { return browser; }
public void setBrowser(Browser browser) { this.browser = browser; }
/**
* 工作空间配置
*/
public static class Workspace {
// 使用 Paths.get() 和 File.separator 实现跨平台兼容
private String rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString();
private long maxFileSize = 10485760L; // 10MB
private List<String> allowedExtensions = List.of(
".txt", ".md", ".java", ".js", ".ts", ".json", ".xml",
".yml", ".yaml", ".properties", ".html", ".css", ".sql"
);
// Getters and Setters
public String getRootDirectory() { return rootDirectory; }
public void setRootDirectory(String rootDirectory) {
// 确保设置的路径也是跨平台兼容的
this.rootDirectory = Paths.get(rootDirectory).toString();
}
public long getMaxFileSize() { return maxFileSize; }
public void setMaxFileSize(long maxFileSize) { this.maxFileSize = maxFileSize; }
public List<String> getAllowedExtensions() { return allowedExtensions; }
public void setAllowedExtensions(List<String> allowedExtensions) { this.allowedExtensions = allowedExtensions; }
}
/**
* 安全配置
*/
public static class Security {
private ApprovalMode approvalMode = ApprovalMode.DEFAULT;
private List<String> dangerousCommands = List.of("rm", "del", "format", "fdisk", "mkfs");
// Getters and Setters
public ApprovalMode getApprovalMode() { return approvalMode; }
public void setApprovalMode(ApprovalMode approvalMode) { this.approvalMode = approvalMode; }
public List<String> getDangerousCommands() { return dangerousCommands; }
public void setDangerousCommands(List<String> dangerousCommands) { this.dangerousCommands = dangerousCommands; }
}
/**
* 工具配置
*/
public static class Tools {
private ToolConfig readFile = new ToolConfig(true);
private ToolConfig writeFile = new ToolConfig(true);
private ToolConfig editFile = new ToolConfig(true);
private ToolConfig listDirectory = new ToolConfig(true);
private ToolConfig shell = new ToolConfig(true);
// Getters and Setters
public ToolConfig getReadFile() { return readFile; }
public void setReadFile(ToolConfig readFile) { this.readFile = readFile; }
public ToolConfig getWriteFile() { return writeFile; }
public void setWriteFile(ToolConfig writeFile) { this.writeFile = writeFile; }
public ToolConfig getEditFile() { return editFile; }
public void setEditFile(ToolConfig editFile) { this.editFile = editFile; }
public ToolConfig getListDirectory() { return listDirectory; }
public void setListDirectory(ToolConfig listDirectory) { this.listDirectory = listDirectory; }
public ToolConfig getShell() { return shell; }
public void setShell(ToolConfig shell) { this.shell = shell; }
}
/**
* 工具配置
*/
public static class ToolConfig {
private boolean enabled;
public ToolConfig() {}
public ToolConfig(boolean enabled) { this.enabled = enabled; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}
/**
* 浏览器配置
*/
public static class Browser {
private boolean autoOpen = true;
private String url = "http://localhost:8080";
private int delaySeconds = 2;
// Getters and Setters
public boolean isAutoOpen() { return autoOpen; }
public void setAutoOpen(boolean autoOpen) { this.autoOpen = autoOpen; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public int getDelaySeconds() { return delaySeconds; }
public void setDelaySeconds(int delaySeconds) { this.delaySeconds = delaySeconds; }
}
/**
* 审批模式
*/
public enum ApprovalMode {
DEFAULT, // 默认模式,危险操作需要确认
AUTO_EDIT, // 自动编辑模式,文件编辑不需要确认
YOLO // 完全自动模式,所有操作都不需要确认
}
}

View File

@@ -0,0 +1,104 @@
package com.example.demo.config;
import com.example.demo.service.ToolExecutionLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 自定义工具执行监听器
* 提供中文日志和详细的文件操作信息记录
*
* 注意Spring AI 1.0.0使用@Tool注解来定义工具不需要ToolCallbackProvider接口
* 这个类主要用于工具执行的日志记录和监控
*/
@Component
public class CustomToolExecutionMonitor {
private static final Logger logger = LoggerFactory.getLogger(CustomToolExecutionMonitor.class);
@Autowired
private ToolExecutionLogger executionLogger;
/**
* 记录工具执行开始
*/
public long logToolStart(String toolName, String description, String parameters) {
String fileInfo = extractFileInfo(toolName, parameters);
long callId = executionLogger.logToolStart(toolName, description,
String.format("参数: %s | 文件信息: %s", parameters, fileInfo));
logger.debug("🚀 [Spring AI] 开始执行工具: {} | 文件/目录: {}", toolName, fileInfo);
return callId;
}
/**
* 记录工具执行成功
*/
public void logToolSuccess(long callId, String toolName, String result, long executionTime, String parameters) {
String fileInfo = extractFileInfo(toolName, parameters);
logger.debug("✅ [Spring AI] 工具执行成功: {} | 耗时: {}ms | 文件/目录: {}",
toolName, executionTime, fileInfo);
executionLogger.logToolSuccess(callId, toolName, result, executionTime);
}
/**
* 记录工具执行失败
*/
public void logToolError(long callId, String toolName, String errorMessage, long executionTime, String parameters) {
String fileInfo = extractFileInfo(toolName, parameters);
logger.error("❌ [Spring AI] 工具执行失败: {} | 耗时: {}ms | 文件/目录: {} | 错误: {}",
toolName, executionTime, fileInfo, errorMessage);
executionLogger.logToolError(callId, toolName, errorMessage, executionTime);
}
/**
* 提取文件信息用于日志记录
*/
private String extractFileInfo(String toolName, String arguments) {
try {
switch (toolName) {
case "readFile":
case "read_file":
return extractPathFromArgs(arguments, "absolutePath", "filePath");
case "writeFile":
case "write_file":
return extractPathFromArgs(arguments, "filePath");
case "editFile":
case "edit_file":
return extractPathFromArgs(arguments, "filePath");
case "listDirectory":
return extractPathFromArgs(arguments, "directoryPath", "path");
case "analyzeProject":
case "analyze_project":
return extractPathFromArgs(arguments, "projectPath");
case "scaffoldProject":
case "scaffold_project":
return extractPathFromArgs(arguments, "projectPath");
case "smartEdit":
case "smart_edit":
return extractPathFromArgs(arguments, "projectPath");
default:
return "未知文件路径";
}
} catch (Exception e) {
return "解析文件路径失败: " + e.getMessage();
}
}
/**
* 从参数中提取路径
*/
private String extractPathFromArgs(String arguments, String... pathKeys) {
for (String key : pathKeys) {
String pattern = "\"" + key + "\"\\s*:\\s*\"([^\"]+)\"";
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher m = p.matcher(arguments);
if (m.find()) {
return m.group(1);
}
}
return "未找到路径参数";
}
}

View File

@@ -0,0 +1,110 @@
package com.example.demo.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import java.util.concurrent.TimeoutException;
/**
* 全局异常处理器
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理超时异常
*/
@ExceptionHandler({TimeoutException.class, AsyncRequestTimeoutException.class})
public ResponseEntity<ErrorResponse> handleTimeoutException(Exception e, WebRequest request) {
logger.error("Request timeout occurred", e);
ErrorResponse errorResponse = new ErrorResponse(
"TIMEOUT_ERROR",
"Request timed out. The operation took too long to complete.",
"Please try again with a simpler request or check your network connection."
);
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(errorResponse);
}
/**
* 处理AI相关异常
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e, WebRequest request) {
logger.error("Runtime exception occurred", e);
// 检查是否是AI调用相关的异常
String message = e.getMessage();
if (message != null && (message.contains("tool") || message.contains("function") || message.contains("AI"))) {
ErrorResponse errorResponse = new ErrorResponse(
"AI_TOOL_ERROR",
"An error occurred during AI tool execution: " + message,
"The AI encountered an issue while processing your request. Please try rephrasing your request or try again."
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
ErrorResponse errorResponse = new ErrorResponse(
"RUNTIME_ERROR",
"An unexpected error occurred: " + message,
"Please try again. If the problem persists, contact support."
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
/**
* 处理所有其他异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception e, WebRequest request) {
logger.error("Unexpected exception occurred", e);
ErrorResponse errorResponse = new ErrorResponse(
"INTERNAL_ERROR",
"An internal server error occurred",
"Something went wrong on our end. Please try again later."
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
/**
* 错误响应类
*/
public static class ErrorResponse {
private String errorCode;
private String message;
private String suggestion;
private long timestamp;
public ErrorResponse(String errorCode, String message, String suggestion) {
this.errorCode = errorCode;
this.message = message;
this.suggestion = suggestion;
this.timestamp = System.currentTimeMillis();
}
// Getters and setters
public String getErrorCode() { return errorCode; }
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getSuggestion() { return suggestion; }
public void setSuggestion(String suggestion) { this.suggestion = suggestion; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}
}

View File

@@ -0,0 +1,49 @@
package com.example.demo.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 日志配置类
* 确保日志目录存在并记录应用启动信息
*/
@Configuration
public class LoggingConfiguration {
private static final Logger logger = LoggerFactory.getLogger(LoggingConfiguration.class);
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
// 确保日志目录存在
File logsDir = new File("logs");
if (!logsDir.exists()) {
boolean created = logsDir.mkdirs();
if (created) {
logger.info("📁 创建日志目录: {}", logsDir.getAbsolutePath());
}
}
// 记录应用启动信息
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
logger.info("🎉 ========================================");
logger.info("🚀 (♥◠‿◠)ノ゙ AI Copilot启动成功 ლ(´ڡ`ლ)゙");
logger.info("🕐 启动时间: {}", startTime);
logger.info("📝 日志级别: DEBUG (工具调用详细日志已启用)");
logger.info("📁 日志文件: logs/copilot-file-ops.log");
logger.info("🔧 支持的工具:");
logger.info(" 📖 read_file - 读取文件内容");
logger.info(" ✏️ write_file - 写入文件内容");
logger.info(" 📝 edit_file - 编辑文件内容");
logger.info(" 🔍 analyze_project - 分析项目结构");
logger.info(" 🏗️ scaffold_project - 创建项目脚手架");
logger.info(" 🧠 smart_edit - 智能编辑项目");
logger.info("🎉 ========================================");
}
}

View File

@@ -0,0 +1,96 @@
package com.example.demo.config;
import com.example.demo.schema.SchemaValidator;
import com.example.demo.tools.*;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* Spring AI 配置 - 使用Spring AI 1.0.0规范
*/
@Configuration
public class SpringAIConfiguration {
@Bean
public ChatClient chatClient(ChatModel chatModel,
FileOperationTools fileOperationTools,
SmartEditTool smartEditTool,
AnalyzeProjectTool analyzeProjectTool,
ProjectScaffoldTool projectScaffoldTool,
AppProperties appProperties) {
// 动态获取工作目录路径
String workspaceDir = appProperties.getWorkspace().getRootDirectory();
return ChatClient.builder(chatModel)
.defaultSystem("""
You are an expert software development assistant with access to file system tools.
You excel at creating complete, well-structured projects through systematic execution of multiple related tasks.
# CORE BEHAVIOR:
- When given a complex task (like "create a web project"), break it down into ALL necessary steps
- Execute MULTIPLE tool calls in sequence to complete the entire task
- Don't stop after just one file - create the complete project structure
- Always verify your work by reading files after creating them
- Continue working until the ENTIRE task is complete
# TASK EXECUTION STRATEGY:
1. **Plan First**: Mentally outline all files and directories needed
2. **Execute Systematically**: Use tools in logical sequence to build the complete solution
3. **Verify Progress**: Read files after creation to ensure correctness
4. **Continue Until Complete**: Don't stop until the entire requested project/task is finished
5. **Signal Continuation**: Use phrases like "Next, I will...", "Now I'll...", "Let me..." to indicate ongoing work
# AVAILABLE TOOLS:
- readFile: Read file contents (supports pagination)
- writeFile: Create or overwrite files
- editFile: Edit files by replacing specific text
- listDirectory: List directory contents (supports recursive)
- analyzeProject: Analyze existing projects to understand structure and dependencies
- smartEdit: Intelligently edit projects based on natural language descriptions
- scaffoldProject: Create new projects with standard structure and templates
# CRITICAL RULES:
- ALWAYS use absolute paths starting with the workspace directory: """ + workspaceDir + """
- Use proper path separators for the current operating system
- For complex requests, execute 5-15 tool calls to create a complete solution
- Use continuation phrases to signal you have more work to do
- If creating a project, make it production-ready with proper structure
- Continue working until you've delivered a complete, functional result
- Only say "completed" or "finished" when the ENTIRE task is truly done
- The tools will show both full paths and relative paths - this helps users locate files
- Always mention the full path when describing what you've created
# PATH EXAMPLES:
- Correct absolute path format:+ workspaceDir + + file separator + filename
- Always ensure paths are within the workspace directory
- Use the system's native path separators
# CONTINUATION SIGNALS:
Use these phrases when you have more work to do:
- "Next, I will create..."
- "Now I'll add..."
- "Let me now..."
- "Moving on to..."
- "I'll proceed to..."
Remember: Your goal is to deliver COMPLETE solutions through continuous execution!
""")
.defaultTools(fileOperationTools, smartEditTool, analyzeProjectTool, projectScaffoldTool)
.build();
}
/**
* 为所有工具注入Schema验证器
*/
@Autowired
public void configureTools(List<BaseTool<?>> tools, SchemaValidator schemaValidator) {
tools.forEach(tool -> tool.setSchemaValidator(schemaValidator));
}
}

View File

@@ -0,0 +1,38 @@
package com.example.demo.config;
/**
* 任务上下文持有者
* 使用ThreadLocal存储当前任务ID供AOP切面使用
*/
public class TaskContextHolder {
private static final ThreadLocal<String> taskIdHolder = new ThreadLocal<>();
/**
* 设置当前任务ID
*/
public static void setCurrentTaskId(String taskId) {
taskIdHolder.set(taskId);
}
/**
* 获取当前任务ID
*/
public static String getCurrentTaskId() {
return taskIdHolder.get();
}
/**
* 清除当前任务ID
*/
public static void clearCurrentTaskId() {
taskIdHolder.remove();
}
/**
* 检查是否有当前任务ID
*/
public static boolean hasCurrentTaskId() {
return taskIdHolder.get() != null;
}
}

View File

@@ -0,0 +1,314 @@
package com.example.demo.config;
import com.example.demo.service.ToolExecutionLogger;
import com.example.demo.service.LogStreamService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 工具调用日志切面
* 拦截 Spring AI 的工具调用并提供中文日志
*/
@Aspect
@Component
public class ToolCallLoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(ToolCallLoggingAspect.class);
@Autowired
private ToolExecutionLogger executionLogger;
@Autowired
private LogStreamService logStreamService;
/**
* 拦截使用@Tool注解的方法执行
*/
@Around("@annotation(org.springframework.ai.tool.annotation.Tool)")
public Object interceptToolAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
// 详细的参数信息
String parametersInfo = formatMethodParameters(args);
String fileInfo = extractFileInfoFromMethodArgs(methodName, args);
logger.debug("🚀 [Spring AI @Tool] 执行工具: {}.{} | 参数: {} | 文件/目录: {}",
className, methodName, parametersInfo, fileInfo);
// 获取当前任务ID (从线程本地变量或其他方式)
String taskId = getCurrentTaskId();
// 推送工具开始执行事件
if (taskId != null) {
String startMessage = generateStartMessage(methodName, fileInfo);
logStreamService.pushToolStart(taskId, methodName, fileInfo, startMessage);
}
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
logger.debug("✅ [Spring AI @Tool] 工具执行成功: {}.{} | 耗时: {}ms | 文件/目录: {} | 参数: {}",
className, methodName, executionTime, fileInfo, parametersInfo);
// 推送工具执行成功事件
if (taskId != null) {
String successMessage = generateSuccessMessage(methodName, fileInfo, result, executionTime);
logStreamService.pushToolSuccess(taskId, methodName, fileInfo, successMessage, executionTime);
}
return result;
} catch (Throwable e) {
long executionTime = System.currentTimeMillis() - startTime;
logger.error("❌ [Spring AI @Tool] 工具执行失败: {}.{} | 耗时: {}ms | 文件/目录: {} | 参数: {} | 错误: {}",
className, methodName, executionTime, fileInfo, parametersInfo, e.getMessage());
// 推送工具执行失败事件
if (taskId != null) {
String errorMessage = generateErrorMessage(methodName, fileInfo, e.getMessage());
logStreamService.pushToolError(taskId, methodName, fileInfo, errorMessage, executionTime);
}
throw e;
}
}
/**
* 格式化方法参数为可读字符串
*/
private String formatMethodParameters(Object[] args) {
if (args == null || args.length == 0) {
return "无参数";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < args.length; i++) {
if (i > 0) sb.append(", ");
Object arg = args[i];
if (arg == null) {
sb.append("null");
} else if (arg instanceof String) {
String str = (String) arg;
// 如果字符串太长,截断显示
if (str.length() > 100) {
sb.append("\"").append(str.substring(0, 100)).append("...\"");
} else {
sb.append("\"").append(str).append("\"");
}
} else {
sb.append(arg.toString());
}
}
return sb.toString();
}
/**
* 从方法参数中直接提取文件信息
*/
private String extractFileInfoFromMethodArgs(String methodName, Object[] args) {
if (args == null || args.length == 0) {
return "无参数";
}
try {
switch (methodName) {
case "readFile":
// readFile(String absolutePath, Integer offset, Integer limit)
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
case "writeFile":
// writeFile(String filePath, String content)
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
case "editFile":
// editFile(String filePath, String oldText, String newText)
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
case "listDirectory":
// listDirectory(String directoryPath, Boolean recursive)
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
case "analyzeProject":
// analyzeProject(String projectPath, ...)
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
case "scaffoldProject":
// scaffoldProject(String projectName, String projectType, String projectPath, ...)
return args.length > 2 && args[2] != null ? args[2].toString() : "未指定路径";
case "smartEdit":
// smartEdit(String projectPath, ...)
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
default:
// 对于未知方法,尝试从第一个参数中提取路径
if (args.length > 0 && args[0] != null) {
String firstArg = args[0].toString();
if (firstArg.contains("/") || firstArg.contains("\\")) {
return firstArg;
}
}
return "未识别的工具类型";
}
} catch (Exception e) {
return "解析参数失败: " + e.getMessage();
}
}
/**
* 从参数字符串中提取文件信息(备用方法)
*/
private String extractFileInfoFromArgs(String toolName, String arguments) {
try {
switch (toolName) {
case "readFile":
case "read_file":
return extractPathFromString(arguments, "absolutePath", "filePath");
case "writeFile":
case "write_file":
case "editFile":
case "edit_file":
return extractPathFromString(arguments, "filePath");
case "listDirectory":
return extractPathFromString(arguments, "directoryPath", "path");
case "analyzeProject":
case "analyze_project":
case "scaffoldProject":
case "scaffold_project":
case "smartEdit":
case "smart_edit":
return extractPathFromString(arguments, "projectPath");
default:
return "未指定文件路径";
}
} catch (Exception e) {
return "解析文件路径失败";
}
}
/**
* 从字符串中提取路径
*/
private String extractPathFromString(String text, String... pathKeys) {
for (String key : pathKeys) {
// JSON 格式
Pattern jsonPattern = Pattern.compile("\"" + key + "\"\\s*:\\s*\"([^\"]+)\"");
Matcher jsonMatcher = jsonPattern.matcher(text);
if (jsonMatcher.find()) {
return jsonMatcher.group(1);
}
// 键值对格式
Pattern kvPattern = Pattern.compile(key + "=([^,\\s\\]]+)");
Matcher kvMatcher = kvPattern.matcher(text);
if (kvMatcher.find()) {
return kvMatcher.group(1);
}
}
return "未找到路径";
}
/**
* 获取当前任务ID
* 从线程本地变量或请求上下文中获取
*/
private String getCurrentTaskId() {
// 这里需要从某个地方获取当前任务ID
// 可以从ThreadLocal、RequestAttributes或其他方式获取
try {
// 临时实现:从线程名或其他方式获取
return TaskContextHolder.getCurrentTaskId();
} catch (Exception e) {
logger.debug("无法获取当前任务ID: {}", e.getMessage());
return null;
}
}
/**
* 生成工具开始执行消息
*/
private String generateStartMessage(String toolName, String fileInfo) {
switch (toolName) {
case "readFile":
return "正在读取文件: " + getFileName(fileInfo);
case "writeFile":
return "正在写入文件: " + getFileName(fileInfo);
case "editFile":
return "正在编辑文件: " + getFileName(fileInfo);
case "listDirectory":
return "正在列出目录: " + fileInfo;
case "analyzeProject":
return "正在分析项目: " + fileInfo;
case "scaffoldProject":
return "正在创建项目脚手架: " + fileInfo;
case "smartEdit":
return "正在智能编辑项目: " + fileInfo;
default:
return "正在执行工具: " + toolName;
}
}
/**
* 生成工具执行成功消息
*/
private String generateSuccessMessage(String toolName, String fileInfo, Object result, long executionTime) {
String fileName = getFileName(fileInfo);
switch (toolName) {
case "readFile":
return String.format("已读取文件 %s (耗时 %dms)", fileName, executionTime);
case "writeFile":
return String.format("已写入文件 %s (耗时 %dms)", fileName, executionTime);
case "editFile":
return String.format("已编辑文件 %s (耗时 %dms)", fileName, executionTime);
case "listDirectory":
return String.format("已列出目录 %s (耗时 %dms)", fileInfo, executionTime);
case "analyzeProject":
return String.format("已分析项目 %s (耗时 %dms)", fileInfo, executionTime);
case "scaffoldProject":
return String.format("已创建项目脚手架 %s (耗时 %dms)", fileInfo, executionTime);
case "smartEdit":
return String.format("已智能编辑项目 %s (耗时 %dms)", fileInfo, executionTime);
default:
return String.format("工具 %s 执行成功 (耗时 %dms)", toolName, executionTime);
}
}
/**
* 生成工具执行失败消息
*/
private String generateErrorMessage(String toolName, String fileInfo, String errorMsg) {
String fileName = getFileName(fileInfo);
return String.format("工具 %s 执行失败: %s (文件: %s)", toolName, errorMsg, fileName);
}
/**
* 从文件路径中提取文件名
*/
private String getFileName(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return "未知文件";
}
// 处理Windows和Unix路径
int lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
if (lastSlash >= 0 && lastSlash < filePath.length() - 1) {
return filePath.substring(lastSlash + 1);
}
return filePath;
}
}

View File

@@ -0,0 +1,258 @@
package com.example.demo.controller;
import com.example.demo.dto.ChatRequestDto;
import com.example.demo.service.ContinuousConversationService;
import com.example.demo.service.ToolExecutionLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* 聊天控制器
* 处理与AI的对话和工具调用
*/
@RestController
@RequestMapping("/api/chat")
@CrossOrigin(origins = "*")
public class ChatController {
private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
private final ChatClient chatClient;
private final ContinuousConversationService continuousConversationService;
private final ToolExecutionLogger executionLogger;
// 简单的会话存储生产环境应该使用数据库或Redis
private final List<Message> conversationHistory = new ArrayList<>();
public ChatController(ChatClient chatClient, ContinuousConversationService continuousConversationService, ToolExecutionLogger executionLogger) {
this.chatClient = chatClient;
this.continuousConversationService = continuousConversationService;
this.executionLogger = executionLogger;
}
/**
* 发送消息给AI - 支持连续工具调用
*/
// 在现有ChatController中修改sendMessage方法
@PostMapping("/message")
public Mono<ChatResponseDto> sendMessage(@RequestBody ChatRequestDto request) {
return Mono.fromCallable(() -> {
try {
logger.info("💬 ========== 新的聊天请求 ==========");
logger.info("📝 用户消息: {}", request.getMessage());
logger.info("🕐 请求时间: {}", java.time.LocalDateTime.now());
// 智能判断是否需要工具调用
boolean needsToolExecution = continuousConversationService.isLikelyToNeedTools(request.getMessage());
logger.info("🔍 工具需求分析: {}", needsToolExecution ? "可能需要工具" : "简单对话");
if (needsToolExecution) {
// 需要工具调用的复杂任务 - 使用异步模式
String taskId = continuousConversationService.startTask(request.getMessage());
logger.info("🆔 任务ID: {}", taskId);
// 记录任务开始
executionLogger.logToolStatistics(); // 显示当前统计
// 异步执行连续对话
CompletableFuture.runAsync(() -> {
try {
logger.info("🚀 开始异步执行连续对话任务: {}", taskId);
continuousConversationService.executeContinuousConversation(
taskId, request.getMessage(), conversationHistory
);
logger.info("✅ 连续对话任务完成: {}", taskId);
} catch (Exception e) {
logger.error("❌ 异步对话执行错误: {}", e.getMessage(), e);
}
});
// 返回异步任务响应
ChatResponseDto responseDto = new ChatResponseDto();
responseDto.setTaskId(taskId);
responseDto.setMessage("任务已启动,正在处理中...");
responseDto.setSuccess(true);
responseDto.setAsyncTask(true);
logger.info("📤 返回响应: taskId={}, 异步任务已启动", taskId);
return responseDto;
} else {
// 简单对话 - 使用流式模式
logger.info("🔄 执行流式对话处理");
// 返回流式响应标识,让前端建立流式连接
ChatResponseDto responseDto = new ChatResponseDto();
responseDto.setMessage("开始流式对话...");
responseDto.setSuccess(true);
responseDto.setAsyncTask(false); // 关键设置为false表示不是工具任务
responseDto.setStreamResponse(true); // 新增:标识为流式响应
responseDto.setTotalTurns(1);
logger.info("📤 返回流式响应标识");
return responseDto;
}
} catch (Exception e) {
logger.error("Error processing chat message", e);
ChatResponseDto errorResponse = new ChatResponseDto();
errorResponse.setMessage("Error: " + e.getMessage());
errorResponse.setSuccess(false);
return errorResponse;
}
});
}
/**
* 流式聊天 - 真正的流式实现
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamMessage(@RequestBody ChatRequestDto request) {
logger.info("🌊 开始流式对话: {}", request.getMessage());
return Flux.create(sink -> {
try {
UserMessage userMessage = new UserMessage(request.getMessage());
conversationHistory.add(userMessage);
// 使用Spring AI的流式API
Flux<String> contentStream = chatClient.prompt()
.messages(conversationHistory)
.stream()
.content();
// 订阅流式内容并转发给前端
contentStream
.doOnNext(content -> {
logger.debug("📨 流式内容片段: {}", content);
// 发送SSE格式的数据
sink.next("data: " + content + "\n\n");
})
.doOnComplete(() -> {
logger.info("✅ 流式对话完成");
sink.next("data: [DONE]\n\n");
sink.complete();
})
.doOnError(error -> {
logger.error("❌ 流式对话错误: {}", error.getMessage());
sink.error(error);
})
.subscribe();
} catch (Exception e) {
logger.error("❌ 流式对话启动失败: {}", e.getMessage());
sink.error(e);
}
});
}
/**
* 清除对话历史
*/
@PostMapping("/clear")
public Mono<Map<String, String>> clearHistory() {
conversationHistory.clear();
logger.info("Conversation history cleared");
return Mono.just(Map.of("status", "success", "message", "Conversation history cleared"));
}
/**
* 获取对话历史
*/
@GetMapping("/history")
public Mono<List<MessageDto>> getHistory() {
List<MessageDto> history = conversationHistory.stream()
.map(message -> {
MessageDto dto = new MessageDto();
dto.setContent(message.getText());
dto.setRole(message instanceof UserMessage ? "user" : "assistant");
return dto;
})
.toList();
return Mono.just(history);
}
// 注意Spring AI 1.0.0 使用不同的函数调用方式
// 函数需要在配置中注册,而不是在运行时动态创建
public static class ChatResponseDto {
private String taskId;
private String message;
private boolean success;
private boolean asyncTask;
private boolean streamResponse; // 新增:标识是否为流式响应
private int totalTurns;
private boolean reachedMaxTurns;
private String stopReason;
private long totalDurationMs;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public boolean isAsyncTask() {
return asyncTask;
}
public void setAsyncTask(boolean asyncTask) {
this.asyncTask = asyncTask;
}
public boolean isStreamResponse() {
return streamResponse;
}
public void setStreamResponse(boolean streamResponse) {
this.streamResponse = streamResponse;
}
public int getTotalTurns() { return totalTurns; }
public void setTotalTurns(int totalTurns) { this.totalTurns = totalTurns; }
public boolean isReachedMaxTurns() { return reachedMaxTurns; }
public void setReachedMaxTurns(boolean reachedMaxTurns) { this.reachedMaxTurns = reachedMaxTurns; }
public String getStopReason() { return stopReason; }
public void setStopReason(String stopReason) { this.stopReason = stopReason; }
public long getTotalDurationMs() { return totalDurationMs; }
public void setTotalDurationMs(long totalDurationMs) { this.totalDurationMs = totalDurationMs; }
}
public static class MessageDto {
private String content;
private String role;
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
}

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