mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-10 10:17:31 +00:00
Compare commits
46 Commits
v2.1.0
...
f1f7cb1084
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1f7cb1084 | ||
|
|
acb1f27c37 | ||
|
|
e46245d97d | ||
|
|
0eff37fa51 | ||
|
|
c1a178c0be | ||
|
|
94d4446321 | ||
|
|
d7930ad713 | ||
|
|
48270add01 | ||
|
|
3786644a25 | ||
|
|
53532681d3 | ||
|
|
daa7b7315b | ||
|
|
df3b687be4 | ||
|
|
ecb5ef32fc | ||
|
|
e5116472ed | ||
|
|
e58aeb5361 | ||
|
|
b4306289f0 | ||
|
|
d9c47bd983 | ||
|
|
8ddbb43dde | ||
|
|
dfe8c7dc85 | ||
|
|
fd94a1772f | ||
|
|
c105d47d99 | ||
|
|
4e2ec2dc82 | ||
|
|
614280d8ea | ||
|
|
2fae8d0ad0 | ||
|
|
d7c2d1bcf3 | ||
|
|
122f63dfbd | ||
|
|
719e968192 | ||
|
|
bf790ceb51 | ||
|
|
de5488bd8c | ||
|
|
c77a245a4d | ||
|
|
6dcd8823cd | ||
|
|
8be480e06c | ||
|
|
11286de676 | ||
|
|
5aaf0a672c | ||
|
|
0089706336 | ||
|
|
cc129801b9 | ||
|
|
e1dc22348c | ||
|
|
f37e4da669 | ||
|
|
3e097d9a68 | ||
|
|
97ae5a46cd | ||
|
|
baa664ac4f | ||
|
|
353fbf26b8 | ||
|
|
f79b4ec012 | ||
|
|
0a73cb4e17 | ||
|
|
d635e30b4a | ||
|
|
ca50d1ddfb |
434
README.md
434
README.md
@@ -1,10 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
# RuoYi AI
|
# RuoYi AI
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
<!-- PROJECT SHIELDS -->
|
|
||||||
|
|
||||||
[![Contributors][contributors-shield]][contributors-url]
|
[![Contributors][contributors-shield]][contributors-url]
|
||||||
[![Forks][forks-shield]][forks-url]
|
[![Forks][forks-shield]][forks-url]
|
||||||
@@ -12,295 +8,140 @@
|
|||||||
[![Issues][issues-shield]][issues-url]
|
[![Issues][issues-shield]][issues-url]
|
||||||
[![MIT License][license-shield]][license-url]
|
[![MIT License][license-shield]][license-url]
|
||||||
|
|
||||||
|
<img src="image/00.png" alt="RuoYi AI Logo" width="120" height="120">
|
||||||
|
|
||||||
<!-- PROJECT LOGO -->
|
### 企业级AI助手平台
|
||||||
<br />
|
|
||||||
|
|
||||||
|
*开箱即用的智能AI平台,深度集成 FastGPT、扣子(Coze)、DIFY 等主流AI平台,提供先进的RAG技术和多模型支持*
|
||||||
|
|
||||||
<img style="text-align: center;" src="image/00.png" alt="Logo" width="150" height="150">
|
**[🇺🇸 English](README_CN.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>
|
</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) |
|
||||||
|
|
||||||
|
## 🛠️ 技术架构
|
||||||
|
|
||||||
|
### 🏗️ 核心框架
|
||||||
|
- **后端架构**: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,我们会自动同步到其他代码托管平台
|
||||||
|
|
||||||
|
## 📄 开源协议
|
||||||
|
|
||||||
|
本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。
|
||||||
|
|
||||||
|
## 🙏 特别鸣谢
|
||||||
|
|
||||||
|
感谢以下优秀的开源项目为本项目提供支持:
|
||||||
|
|
||||||
|
- [Spring AI Alibaba Copilot](https://github.com/springaialibaba/spring-ai-alibaba-copilot) - 基于Spring AI的智能编码助手,集成MCP工具协议,支持项目分析和代码生成
|
||||||
|
- [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 服务
|
||||||
|
|
||||||
|
## 💬 社区交流
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
#### 管理端
|
---
|
||||||
<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);"/>
|
<div align="center">
|
||||||
<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);"/>
|
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[🍴 Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_CN.md)** • **[📖 查看完整文档](https://doc.pandarobot.chat)**
|
||||||
<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);"/>
|
|
||||||
|
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Badge Links -->
|
||||||
### 开发环境
|
|
||||||
|
|
||||||
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模板
|
|
||||||
|
|
||||||
Q:vben5 的模板默认是没有的吗?
|
|
||||||
|
|
||||||
A:vben模板是收费的 请联系vben-vue-plus作者获取。
|
|
||||||
|
|
||||||
### 版本控制
|
|
||||||
|
|
||||||
该项目使用Git进行版本管理。您可以在repository参看当前可用版本。
|
|
||||||
|
|
||||||
|
|
||||||
### 版权说明
|
|
||||||
|
|
||||||
该项目使用了MIT授权许可,详情请参阅 [LICENSE.txt](https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE)
|
|
||||||
|
|
||||||
### 项目现状
|
|
||||||
|
|
||||||
目前,项目还处于早期阶段,距离成熟还有很长的路要走。由于个人精力有限,项目的发展速度受到了一定的限制。为了加快项目的进度,我真诚地希望更多人能够参与到项目中来。无论是经验丰富的开发者,还是刚刚入门的小白,我都热烈欢迎你们提交Pull Request(PR)👏👏👏。即使代码修改得很少,或者存在一些错误,都没有关系。我会认真审核每一位贡献者的代码,并和大家一起完善项目⛽️⛽️⛽️。
|
|
||||||
|
|
||||||
### 开发计划
|
|
||||||
|
|
||||||
| 主题 | 方向 | 时间节点 |
|
|
||||||
| --- |-----------------------------------|--------|
|
|
||||||
| 前端简化版 | 与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
|
|
||||||
[contributors-shield]: https://img.shields.io/github/contributors/ageerle/ruoyi-ai.svg?style=flat-square
|
[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
|
[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-shield]: https://img.shields.io/github/forks/ageerle/ruoyi-ai.svg?style=flat-square
|
||||||
@@ -308,29 +149,6 @@
|
|||||||
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
[stars-shield]: https://img.shields.io/github/stars/ageerle/ruoyi-ai.svg?style=flat-square
|
||||||
[stars-url]: https://github.com/ageerle/ruoyi-ai/stargazers
|
[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-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-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
|
[license-url]: https://github.com/ageerle/ruoyi-ai/blob/main/LICENSE
|
||||||
[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>
|
|
||||||
|
|
||||||
|
|
||||||
#### 进群学习
|
|
||||||
🏠 小助手wx:ruoyi-ai(加人备注:ruoyi-ai)
|
|
||||||
🏠 小助手qq:1603234088 (加人备注: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>
|
|
||||||
|
|
||||||
|
|||||||
161
README_CN.md
Normal file
161
README_CN.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
|
||||||
|
# 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) |
|
||||||
|
|
||||||
|
## 🛠️ 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 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
image/code01.png
Normal file
BIN
image/code01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 830 KiB |
BIN
image/code02.png
Normal file
BIN
image/code02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 727 KiB |
BIN
image/code03.png
Normal file
BIN
image/code03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 528 KiB |
BIN
image/code04.png
Normal file
BIN
image/code04.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
BIN
image/qq.png
BIN
image/qq.png
Binary file not shown.
|
Before Width: | Height: | Size: 392 KiB After Width: | Height: | Size: 267 KiB |
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.ibatis.reflection.MetaObject;
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
import org.ruoyi.common.core.domain.model.LoginUser;
|
import org.ruoyi.common.core.domain.model.LoginUser;
|
||||||
import org.ruoyi.common.core.exception.ServiceException;
|
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.core.utils.ObjectUtils;
|
||||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||||
import org.ruoyi.core.domain.BaseEntity;
|
import org.ruoyi.core.domain.BaseEntity;
|
||||||
@@ -91,7 +92,8 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
|
|||||||
private LoginUser getLoginUser() {
|
private LoginUser getLoginUser() {
|
||||||
LoginUser loginUser;
|
LoginUser loginUser;
|
||||||
try {
|
try {
|
||||||
loginUser = LoginHelper.getLoginUser();
|
String token = BaseContext.getCurrentToken();
|
||||||
|
loginUser = LoginHelper.getLoginUser(token);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("自动注入警告 => 用户未登录");
|
log.warn("自动注入警告 => 用户未登录");
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import cn.hutool.core.convert.Convert;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.core.constant.TenantConstants;
|
import org.ruoyi.common.core.constant.TenantConstants;
|
||||||
import org.ruoyi.common.core.constant.UserConstants;
|
import org.ruoyi.common.core.constant.UserConstants;
|
||||||
import org.ruoyi.common.core.domain.model.LoginUser;
|
import org.ruoyi.common.core.domain.model.LoginUser;
|
||||||
@@ -29,6 +30,7 @@ import java.util.Set;
|
|||||||
*
|
*
|
||||||
* @author Lion Li
|
* @author Lion Li
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class LoginHelper {
|
public class LoginHelper {
|
||||||
|
|
||||||
@@ -82,6 +84,15 @@ public class LoginHelper {
|
|||||||
return loginUser;
|
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
|
* 获取用户id
|
||||||
*/
|
*/
|
||||||
|
|||||||
164
ruoyi-extend/ruoyi-ai-copilot/pom.xml
Normal file
164
ruoyi-extend/ruoyi-ai-copilot/pom.xml
Normal 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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 // 完全自动模式,所有操作都不需要确认
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 "未找到路径参数";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("🎉 ========================================");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.service.LogStreamService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE日志流控制器
|
||||||
|
* 提供SSE连接端点
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/logs")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class LogStreamController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LogStreamController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LogStreamService logStreamService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建立SSE连接
|
||||||
|
* 前端通过此端点建立实时日志推送连接
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/stream/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
public SseEmitter streamLogs(@PathVariable("taskId") String taskId) {
|
||||||
|
logger.info("🔗 收到SSE连接请求: taskId={}", taskId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
SseEmitter emitter = logStreamService.createConnection(taskId);
|
||||||
|
logger.info("✅ SSE连接建立成功: taskId={}", taskId);
|
||||||
|
return emitter;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("❌ SSE连接建立失败: taskId={}, error={}", taskId, e.getMessage());
|
||||||
|
throw new RuntimeException("Failed to create SSE connection: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭SSE连接
|
||||||
|
*/
|
||||||
|
@PostMapping("/close/{taskId}")
|
||||||
|
public void closeConnection(@PathVariable("taskId") String taskId) {
|
||||||
|
logger.info("🔚 收到关闭SSE连接请求: taskId={}", taskId);
|
||||||
|
logStreamService.closeConnection(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接状态
|
||||||
|
*/
|
||||||
|
@GetMapping("/status")
|
||||||
|
public ConnectionStatus getConnectionStatus() {
|
||||||
|
int activeConnections = logStreamService.getActiveConnectionCount();
|
||||||
|
logger.debug("📊 当前活跃SSE连接数: {}", activeConnections);
|
||||||
|
|
||||||
|
ConnectionStatus status = new ConnectionStatus();
|
||||||
|
status.setActiveConnections(activeConnections);
|
||||||
|
status.setStatus("OK");
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接状态DTO
|
||||||
|
*/
|
||||||
|
public static class ConnectionStatus {
|
||||||
|
private int activeConnections;
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public int getActiveConnections() {
|
||||||
|
return activeConnections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveConnections(int activeConnections) {
|
||||||
|
this.activeConnections = activeConnections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.model.TaskStatus;
|
||||||
|
import com.example.demo.service.ContinuousConversationService;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/task")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class TaskStatusController {
|
||||||
|
|
||||||
|
private final ContinuousConversationService conversationService;
|
||||||
|
|
||||||
|
public TaskStatusController(ContinuousConversationService conversationService) {
|
||||||
|
this.conversationService = conversationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务状态
|
||||||
|
*/
|
||||||
|
@GetMapping("/status/{taskId}")
|
||||||
|
public Mono<TaskStatusDto> getTaskStatus(@PathVariable("taskId") String taskId) {
|
||||||
|
return Mono.fromCallable(() -> {
|
||||||
|
TaskStatus status = conversationService.getTaskStatus(taskId);
|
||||||
|
if (status == null) {
|
||||||
|
throw new RuntimeException("Task not found: " + taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskStatusDto dto = new TaskStatusDto();
|
||||||
|
dto.setTaskId(status.getTaskId());
|
||||||
|
dto.setStatus(status.getStatus());
|
||||||
|
dto.setCurrentAction(status.getCurrentAction());
|
||||||
|
dto.setSummary(status.getSummary());
|
||||||
|
dto.setCurrentTurn(status.getCurrentTurn());
|
||||||
|
dto.setTotalEstimatedTurns(status.getTotalEstimatedTurns());
|
||||||
|
dto.setProgressPercentage(status.getProgressPercentage());
|
||||||
|
dto.setElapsedTime(status.getElapsedTime());
|
||||||
|
dto.setErrorMessage(status.getErrorMessage());
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/result/{taskId}")
|
||||||
|
public Mono<ConversationResultDto> getConversationResult(@PathVariable("taskId") String taskId) {
|
||||||
|
return Mono.fromCallable(() -> {
|
||||||
|
ContinuousConversationService.ConversationResult result = conversationService.getConversationResult(taskId);
|
||||||
|
if (result == null) {
|
||||||
|
throw new RuntimeException("Conversation result not found: " + taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConversationResultDto dto = new ConversationResultDto();
|
||||||
|
dto.setTaskId(taskId);
|
||||||
|
dto.setFullResponse(result.getFullResponse());
|
||||||
|
dto.setTurnResponses(result.getTurnResponses());
|
||||||
|
dto.setTotalTurns(result.getTotalTurns());
|
||||||
|
dto.setReachedMaxTurns(result.isReachedMaxTurns());
|
||||||
|
dto.setStopReason(result.getStopReason());
|
||||||
|
dto.setTotalDurationMs(result.getTotalDurationMs());
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO类
|
||||||
|
public static class TaskStatusDto {
|
||||||
|
private String taskId;
|
||||||
|
private String status;
|
||||||
|
private String currentAction;
|
||||||
|
private String summary;
|
||||||
|
private int currentTurn;
|
||||||
|
private int totalEstimatedTurns;
|
||||||
|
private double progressPercentage;
|
||||||
|
private long elapsedTime;
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getTaskId() { return taskId; }
|
||||||
|
public void setTaskId(String taskId) { this.taskId = taskId; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getCurrentAction() { return currentAction; }
|
||||||
|
public void setCurrentAction(String currentAction) { this.currentAction = currentAction; }
|
||||||
|
|
||||||
|
public String getSummary() { return summary; }
|
||||||
|
public void setSummary(String summary) { this.summary = summary; }
|
||||||
|
|
||||||
|
public int getCurrentTurn() { return currentTurn; }
|
||||||
|
public void setCurrentTurn(int currentTurn) { this.currentTurn = currentTurn; }
|
||||||
|
|
||||||
|
public int getTotalEstimatedTurns() { return totalEstimatedTurns; }
|
||||||
|
public void setTotalEstimatedTurns(int totalEstimatedTurns) { this.totalEstimatedTurns = totalEstimatedTurns; }
|
||||||
|
|
||||||
|
public double getProgressPercentage() { return progressPercentage; }
|
||||||
|
public void setProgressPercentage(double progressPercentage) { this.progressPercentage = progressPercentage; }
|
||||||
|
|
||||||
|
public long getElapsedTime() { return elapsedTime; }
|
||||||
|
public void setElapsedTime(long elapsedTime) { this.elapsedTime = elapsedTime; }
|
||||||
|
|
||||||
|
public String getErrorMessage() { return errorMessage; }
|
||||||
|
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话结果DTO类
|
||||||
|
public static class ConversationResultDto {
|
||||||
|
private String taskId;
|
||||||
|
private String fullResponse;
|
||||||
|
private java.util.List<String> turnResponses;
|
||||||
|
private int totalTurns;
|
||||||
|
private boolean reachedMaxTurns;
|
||||||
|
private String stopReason;
|
||||||
|
private long totalDurationMs;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getTaskId() { return taskId; }
|
||||||
|
public void setTaskId(String taskId) { this.taskId = taskId; }
|
||||||
|
|
||||||
|
public String getFullResponse() { return fullResponse; }
|
||||||
|
public void setFullResponse(String fullResponse) { this.fullResponse = fullResponse; }
|
||||||
|
|
||||||
|
public java.util.List<String> getTurnResponses() { return turnResponses; }
|
||||||
|
public void setTurnResponses(java.util.List<String> turnResponses) { this.turnResponses = turnResponses; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web页面控制器
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class WebController {
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
public String index() {
|
||||||
|
return "index";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理favicon.ico请求,避免404错误
|
||||||
|
*/
|
||||||
|
@GetMapping("/favicon.ico")
|
||||||
|
public ResponseEntity<Void> favicon() {
|
||||||
|
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.example.demo.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天请求数据传输对象
|
||||||
|
*/
|
||||||
|
public class ChatRequestDto {
|
||||||
|
private String message;
|
||||||
|
private String sessionId; // 可选:用于会话管理
|
||||||
|
|
||||||
|
public ChatRequestDto() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatRequestDto(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatRequestDto(String message, String sessionId) {
|
||||||
|
this.message = message;
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ChatRequestDto{" +
|
||||||
|
"message='" + message + '\'' +
|
||||||
|
", sessionId='" + sessionId + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project context information
|
||||||
|
* Contains complete project analysis results for AI understanding
|
||||||
|
*/
|
||||||
|
public class ProjectContext {
|
||||||
|
private Path projectRoot;
|
||||||
|
private ProjectType projectType;
|
||||||
|
private ProjectStructure projectStructure;
|
||||||
|
private List<DependencyInfo> dependencies;
|
||||||
|
private List<ConfigFile> configFiles;
|
||||||
|
private CodeStatistics codeStatistics;
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
private String contextSummary;
|
||||||
|
|
||||||
|
public ProjectContext() {
|
||||||
|
this.dependencies = new ArrayList<>();
|
||||||
|
this.configFiles = new ArrayList<>();
|
||||||
|
this.metadata = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectContext(Path projectRoot) {
|
||||||
|
this();
|
||||||
|
this.projectRoot = projectRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependency information class
|
||||||
|
*/
|
||||||
|
public static class DependencyInfo {
|
||||||
|
private String name;
|
||||||
|
private String version;
|
||||||
|
private String type; // "compile", "test", "runtime", etc.
|
||||||
|
private String scope;
|
||||||
|
private boolean isDirectDependency;
|
||||||
|
|
||||||
|
public DependencyInfo(String name, String version, String type) {
|
||||||
|
this.name = name;
|
||||||
|
this.version = version;
|
||||||
|
this.type = type;
|
||||||
|
this.isDirectDependency = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getVersion() { return version; }
|
||||||
|
public void setVersion(String version) { this.version = version; }
|
||||||
|
|
||||||
|
public String getType() { return type; }
|
||||||
|
public void setType(String type) { this.type = type; }
|
||||||
|
|
||||||
|
public String getScope() { return scope; }
|
||||||
|
public void setScope(String scope) { this.scope = scope; }
|
||||||
|
|
||||||
|
public boolean isDirectDependency() { return isDirectDependency; }
|
||||||
|
public void setDirectDependency(boolean directDependency) { isDirectDependency = directDependency; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s:%s (%s)", name, version, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration file information class
|
||||||
|
*/
|
||||||
|
public static class ConfigFile {
|
||||||
|
private String fileName;
|
||||||
|
private String relativePath;
|
||||||
|
private String fileType; // "properties", "yaml", "json", "xml", etc.
|
||||||
|
private Map<String, Object> keySettings;
|
||||||
|
private boolean isMainConfig;
|
||||||
|
|
||||||
|
public ConfigFile(String fileName, String relativePath, String fileType) {
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.relativePath = relativePath;
|
||||||
|
this.fileType = fileType;
|
||||||
|
this.keySettings = new HashMap<>();
|
||||||
|
this.isMainConfig = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getFileName() { return fileName; }
|
||||||
|
public void setFileName(String fileName) { this.fileName = fileName; }
|
||||||
|
|
||||||
|
public String getRelativePath() { return relativePath; }
|
||||||
|
public void setRelativePath(String relativePath) { this.relativePath = relativePath; }
|
||||||
|
|
||||||
|
public String getFileType() { return fileType; }
|
||||||
|
public void setFileType(String fileType) { this.fileType = fileType; }
|
||||||
|
|
||||||
|
public Map<String, Object> getKeySettings() { return keySettings; }
|
||||||
|
public void setKeySettings(Map<String, Object> keySettings) { this.keySettings = keySettings; }
|
||||||
|
|
||||||
|
public boolean isMainConfig() { return isMainConfig; }
|
||||||
|
public void setMainConfig(boolean mainConfig) { isMainConfig = mainConfig; }
|
||||||
|
|
||||||
|
public void addSetting(String key, Object value) {
|
||||||
|
this.keySettings.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code statistics information class
|
||||||
|
*/
|
||||||
|
public static class CodeStatistics {
|
||||||
|
private int totalLines;
|
||||||
|
private int codeLines;
|
||||||
|
private int commentLines;
|
||||||
|
private int blankLines;
|
||||||
|
private Map<String, Integer> languageLines;
|
||||||
|
private int totalClasses;
|
||||||
|
private int totalMethods;
|
||||||
|
private int totalFunctions;
|
||||||
|
|
||||||
|
public CodeStatistics() {
|
||||||
|
this.languageLines = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public int getTotalLines() { return totalLines; }
|
||||||
|
public void setTotalLines(int totalLines) { this.totalLines = totalLines; }
|
||||||
|
|
||||||
|
public int getCodeLines() { return codeLines; }
|
||||||
|
public void setCodeLines(int codeLines) { this.codeLines = codeLines; }
|
||||||
|
|
||||||
|
public int getCommentLines() { return commentLines; }
|
||||||
|
public void setCommentLines(int commentLines) { this.commentLines = commentLines; }
|
||||||
|
|
||||||
|
public int getBlankLines() { return blankLines; }
|
||||||
|
public void setBlankLines(int blankLines) { this.blankLines = blankLines; }
|
||||||
|
|
||||||
|
public Map<String, Integer> getLanguageLines() { return languageLines; }
|
||||||
|
public void setLanguageLines(Map<String, Integer> languageLines) { this.languageLines = languageLines; }
|
||||||
|
|
||||||
|
public int getTotalClasses() { return totalClasses; }
|
||||||
|
public void setTotalClasses(int totalClasses) { this.totalClasses = totalClasses; }
|
||||||
|
|
||||||
|
public int getTotalMethods() { return totalMethods; }
|
||||||
|
public void setTotalMethods(int totalMethods) { this.totalMethods = totalMethods; }
|
||||||
|
|
||||||
|
public int getTotalFunctions() { return totalFunctions; }
|
||||||
|
public void setTotalFunctions(int totalFunctions) { this.totalFunctions = totalFunctions; }
|
||||||
|
|
||||||
|
public void addLanguageLines(String language, int lines) {
|
||||||
|
this.languageLines.put(language, this.languageLines.getOrDefault(language, 0) + lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate project context summary
|
||||||
|
*/
|
||||||
|
public String generateContextSummary() {
|
||||||
|
StringBuilder summary = new StringBuilder();
|
||||||
|
|
||||||
|
// Basic information
|
||||||
|
summary.append("=== PROJECT CONTEXT ===\n");
|
||||||
|
summary.append("Project: ").append(projectRoot != null ? projectRoot.getFileName() : "Unknown").append("\n");
|
||||||
|
summary.append("Type: ").append(projectType != null ? projectType.getDisplayName() : "Unknown").append("\n");
|
||||||
|
summary.append("Language: ").append(projectType != null ? projectType.getPrimaryLanguage() : "Unknown").append("\n");
|
||||||
|
summary.append("Package Manager: ").append(projectType != null ? projectType.getPackageManager() : "Unknown").append("\n\n");
|
||||||
|
|
||||||
|
// Project structure
|
||||||
|
if (projectStructure != null) {
|
||||||
|
summary.append("=== PROJECT STRUCTURE ===\n");
|
||||||
|
summary.append(projectStructure.getStructureSummary()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
if (!dependencies.isEmpty()) {
|
||||||
|
summary.append("=== DEPENDENCIES ===\n");
|
||||||
|
dependencies.stream()
|
||||||
|
.filter(DependencyInfo::isDirectDependency)
|
||||||
|
.limit(10)
|
||||||
|
.forEach(dep -> summary.append("- ").append(dep.toString()).append("\n"));
|
||||||
|
if (dependencies.size() > 10) {
|
||||||
|
summary.append("... and ").append(dependencies.size() - 10).append(" more dependencies\n");
|
||||||
|
}
|
||||||
|
summary.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration files
|
||||||
|
if (!configFiles.isEmpty()) {
|
||||||
|
summary.append("=== CONFIGURATION FILES ===\n");
|
||||||
|
configFiles.stream()
|
||||||
|
.filter(ConfigFile::isMainConfig)
|
||||||
|
.forEach(config -> summary.append("- ").append(config.getFileName())
|
||||||
|
.append(" (").append(config.getFileType()).append(")\n"));
|
||||||
|
summary.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code statistics
|
||||||
|
if (codeStatistics != null) {
|
||||||
|
summary.append("=== CODE STATISTICS ===\n");
|
||||||
|
summary.append("Total Lines: ").append(codeStatistics.getTotalLines()).append("\n");
|
||||||
|
summary.append("Code Lines: ").append(codeStatistics.getCodeLines()).append("\n");
|
||||||
|
if (codeStatistics.getTotalClasses() > 0) {
|
||||||
|
summary.append("Classes: ").append(codeStatistics.getTotalClasses()).append("\n");
|
||||||
|
}
|
||||||
|
if (codeStatistics.getTotalMethods() > 0) {
|
||||||
|
summary.append("Methods: ").append(codeStatistics.getTotalMethods()).append("\n");
|
||||||
|
}
|
||||||
|
summary.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contextSummary = summary.toString();
|
||||||
|
return this.contextSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dependency summary
|
||||||
|
*/
|
||||||
|
public String getDependencySummary() {
|
||||||
|
if (dependencies.isEmpty()) {
|
||||||
|
return "No dependencies found";
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies.stream()
|
||||||
|
.filter(DependencyInfo::isDirectDependency)
|
||||||
|
.limit(5)
|
||||||
|
.map(DependencyInfo::getName)
|
||||||
|
.reduce((a, b) -> a + ", " + b)
|
||||||
|
.orElse("No direct dependencies");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Path getProjectRoot() { return projectRoot; }
|
||||||
|
public void setProjectRoot(Path projectRoot) { this.projectRoot = projectRoot; }
|
||||||
|
|
||||||
|
public ProjectType getProjectType() { return projectType; }
|
||||||
|
public void setProjectType(ProjectType projectType) { this.projectType = projectType; }
|
||||||
|
|
||||||
|
public ProjectStructure getProjectStructure() { return projectStructure; }
|
||||||
|
public void setProjectStructure(ProjectStructure projectStructure) { this.projectStructure = projectStructure; }
|
||||||
|
|
||||||
|
public List<DependencyInfo> getDependencies() { return dependencies; }
|
||||||
|
public void setDependencies(List<DependencyInfo> dependencies) { this.dependencies = dependencies; }
|
||||||
|
|
||||||
|
public List<ConfigFile> getConfigFiles() { return configFiles; }
|
||||||
|
public void setConfigFiles(List<ConfigFile> configFiles) { this.configFiles = configFiles; }
|
||||||
|
|
||||||
|
public CodeStatistics getCodeStatistics() { return codeStatistics; }
|
||||||
|
public void setCodeStatistics(CodeStatistics codeStatistics) { this.codeStatistics = codeStatistics; }
|
||||||
|
|
||||||
|
public Map<String, Object> getMetadata() { return metadata; }
|
||||||
|
public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; }
|
||||||
|
|
||||||
|
public String getContextSummary() { return contextSummary; }
|
||||||
|
public void setContextSummary(String contextSummary) { this.contextSummary = contextSummary; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project structure information model
|
||||||
|
* Contains project directory structure, file statistics and other information
|
||||||
|
*/
|
||||||
|
public class ProjectStructure {
|
||||||
|
private Path projectRoot;
|
||||||
|
private ProjectType projectType;
|
||||||
|
private List<DirectoryInfo> directories;
|
||||||
|
private Map<String, Integer> fileTypeCount;
|
||||||
|
private List<String> keyFiles;
|
||||||
|
private int totalFiles;
|
||||||
|
private int totalDirectories;
|
||||||
|
private long totalSize;
|
||||||
|
|
||||||
|
public ProjectStructure() {
|
||||||
|
this.directories = new ArrayList<>();
|
||||||
|
this.fileTypeCount = new HashMap<>();
|
||||||
|
this.keyFiles = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectStructure(Path projectRoot, ProjectType projectType) {
|
||||||
|
this();
|
||||||
|
this.projectRoot = projectRoot;
|
||||||
|
this.projectType = projectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory information inner class
|
||||||
|
*/
|
||||||
|
public static class DirectoryInfo {
|
||||||
|
private String name;
|
||||||
|
private String relativePath;
|
||||||
|
private int fileCount;
|
||||||
|
private List<String> files;
|
||||||
|
private boolean isImportant; // Whether it's an important directory (like src, test, etc.)
|
||||||
|
|
||||||
|
public DirectoryInfo(String name, String relativePath) {
|
||||||
|
this.name = name;
|
||||||
|
this.relativePath = relativePath;
|
||||||
|
this.files = new ArrayList<>();
|
||||||
|
this.isImportant = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getRelativePath() { return relativePath; }
|
||||||
|
public void setRelativePath(String relativePath) { this.relativePath = relativePath; }
|
||||||
|
|
||||||
|
public int getFileCount() { return fileCount; }
|
||||||
|
public void setFileCount(int fileCount) { this.fileCount = fileCount; }
|
||||||
|
|
||||||
|
public List<String> getFiles() { return files; }
|
||||||
|
public void setFiles(List<String> files) { this.files = files; }
|
||||||
|
|
||||||
|
public boolean isImportant() { return isImportant; }
|
||||||
|
public void setImportant(boolean important) { isImportant = important; }
|
||||||
|
|
||||||
|
public void addFile(String fileName) {
|
||||||
|
this.files.add(fileName);
|
||||||
|
this.fileCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add directory information
|
||||||
|
*/
|
||||||
|
public void addDirectory(DirectoryInfo directoryInfo) {
|
||||||
|
this.directories.add(directoryInfo);
|
||||||
|
this.totalDirectories++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add file type statistics
|
||||||
|
*/
|
||||||
|
public void addFileType(String extension, int count) {
|
||||||
|
this.fileTypeCount.put(extension, this.fileTypeCount.getOrDefault(extension, 0) + count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add key file
|
||||||
|
*/
|
||||||
|
public void addKeyFile(String fileName) {
|
||||||
|
if (!this.keyFiles.contains(fileName)) {
|
||||||
|
this.keyFiles.add(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get project structure summary
|
||||||
|
*/
|
||||||
|
public String getStructureSummary() {
|
||||||
|
StringBuilder summary = new StringBuilder();
|
||||||
|
summary.append("Project: ").append(projectRoot != null ? projectRoot.getFileName() : "Unknown").append("\n");
|
||||||
|
summary.append("Type: ").append(projectType != null ? projectType.getDisplayName() : "Unknown").append("\n");
|
||||||
|
summary.append("Directories: ").append(totalDirectories).append("\n");
|
||||||
|
summary.append("Files: ").append(totalFiles).append("\n");
|
||||||
|
|
||||||
|
if (!keyFiles.isEmpty()) {
|
||||||
|
summary.append("Key Files: ").append(String.join(", ", keyFiles)).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileTypeCount.isEmpty()) {
|
||||||
|
summary.append("File Types: ");
|
||||||
|
fileTypeCount.entrySet().stream()
|
||||||
|
.sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
|
||||||
|
.limit(5)
|
||||||
|
.forEach(entry -> summary.append(entry.getKey()).append("(").append(entry.getValue()).append(") "));
|
||||||
|
summary.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get important directories list
|
||||||
|
*/
|
||||||
|
public List<DirectoryInfo> getImportantDirectories() {
|
||||||
|
return directories.stream()
|
||||||
|
.filter(DirectoryInfo::isImportant)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark important directories based on project type
|
||||||
|
*/
|
||||||
|
public void markImportantDirectories() {
|
||||||
|
if (projectType == null) return;
|
||||||
|
|
||||||
|
for (DirectoryInfo dir : directories) {
|
||||||
|
String dirName = dir.getName().toLowerCase();
|
||||||
|
|
||||||
|
// Common important directories
|
||||||
|
if (dirName.equals("src") || dirName.equals("source") ||
|
||||||
|
dirName.equals("test") || dirName.equals("tests") ||
|
||||||
|
dirName.equals("config") || dirName.equals("conf") ||
|
||||||
|
dirName.equals("docs") || dirName.equals("doc")) {
|
||||||
|
dir.setImportant(true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project type specific important directories
|
||||||
|
switch (projectType) {
|
||||||
|
case JAVA_MAVEN:
|
||||||
|
case JAVA_GRADLE:
|
||||||
|
case SPRING_BOOT:
|
||||||
|
if (dirName.equals("main") || dirName.equals("resources") ||
|
||||||
|
dirName.equals("webapp") || dirName.equals("target") ||
|
||||||
|
dirName.equals("build")) {
|
||||||
|
dir.setImportant(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NODE_JS:
|
||||||
|
case REACT:
|
||||||
|
case VUE:
|
||||||
|
case ANGULAR:
|
||||||
|
case NEXT_JS:
|
||||||
|
if (dirName.equals("node_modules") || dirName.equals("public") ||
|
||||||
|
dirName.equals("dist") || dirName.equals("build") ||
|
||||||
|
dirName.equals("components") || dirName.equals("pages")) {
|
||||||
|
dir.setImportant(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PYTHON:
|
||||||
|
case DJANGO:
|
||||||
|
case FLASK:
|
||||||
|
case FASTAPI:
|
||||||
|
if (dirName.equals("venv") || dirName.equals("env") ||
|
||||||
|
dirName.equals("__pycache__") || dirName.equals("migrations") ||
|
||||||
|
dirName.equals("static") || dirName.equals("templates")) {
|
||||||
|
dir.setImportant(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Path getProjectRoot() { return projectRoot; }
|
||||||
|
public void setProjectRoot(Path projectRoot) { this.projectRoot = projectRoot; }
|
||||||
|
|
||||||
|
public ProjectType getProjectType() { return projectType; }
|
||||||
|
public void setProjectType(ProjectType projectType) { this.projectType = projectType; }
|
||||||
|
|
||||||
|
public List<DirectoryInfo> getDirectories() { return directories; }
|
||||||
|
public void setDirectories(List<DirectoryInfo> directories) { this.directories = directories; }
|
||||||
|
|
||||||
|
public Map<String, Integer> getFileTypeCount() { return fileTypeCount; }
|
||||||
|
public void setFileTypeCount(Map<String, Integer> fileTypeCount) { this.fileTypeCount = fileTypeCount; }
|
||||||
|
|
||||||
|
public List<String> getKeyFiles() { return keyFiles; }
|
||||||
|
public void setKeyFiles(List<String> keyFiles) { this.keyFiles = keyFiles; }
|
||||||
|
|
||||||
|
public int getTotalFiles() { return totalFiles; }
|
||||||
|
public void setTotalFiles(int totalFiles) { this.totalFiles = totalFiles; }
|
||||||
|
|
||||||
|
public int getTotalDirectories() { return totalDirectories; }
|
||||||
|
public void setTotalDirectories(int totalDirectories) { this.totalDirectories = totalDirectories; }
|
||||||
|
|
||||||
|
public long getTotalSize() { return totalSize; }
|
||||||
|
public void setTotalSize(long totalSize) { this.totalSize = totalSize; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project type enumeration
|
||||||
|
* Supports mainstream project type detection
|
||||||
|
*/
|
||||||
|
public enum ProjectType {
|
||||||
|
// Java projects
|
||||||
|
JAVA_MAVEN("Java Maven", "pom.xml", "Maven-based Java project"),
|
||||||
|
JAVA_GRADLE("Java Gradle", "build.gradle", "Gradle-based Java project"),
|
||||||
|
SPRING_BOOT("Spring Boot", "pom.xml", "Spring Boot application"),
|
||||||
|
|
||||||
|
// JavaScript/Node.js projects
|
||||||
|
NODE_JS("Node.js", "package.json", "Node.js project"),
|
||||||
|
REACT("React", "package.json", "React application"),
|
||||||
|
VUE("Vue.js", "package.json", "Vue.js application"),
|
||||||
|
ANGULAR("Angular", "package.json", "Angular application"),
|
||||||
|
NEXT_JS("Next.js", "package.json", "Next.js application"),
|
||||||
|
|
||||||
|
// Python projects
|
||||||
|
PYTHON("Python", "requirements.txt", "Python project"),
|
||||||
|
DJANGO("Django", "manage.py", "Django web application"),
|
||||||
|
FLASK("Flask", "app.py", "Flask web application"),
|
||||||
|
FASTAPI("FastAPI", "main.py", "FastAPI application"),
|
||||||
|
|
||||||
|
// Other project types
|
||||||
|
DOTNET("ASP.NET", "*.csproj", ".NET project"),
|
||||||
|
GO("Go", "go.mod", "Go project"),
|
||||||
|
RUST("Rust", "Cargo.toml", "Rust project"),
|
||||||
|
PHP("PHP", "composer.json", "PHP project"),
|
||||||
|
|
||||||
|
// Web frontend
|
||||||
|
HTML_STATIC("Static HTML", "index.html", "Static HTML website"),
|
||||||
|
|
||||||
|
// Unknown type
|
||||||
|
UNKNOWN("Unknown", "", "Unknown project type");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final String keyFile;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
ProjectType(String displayName, String keyFile, String description) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.keyFile = keyFile;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKeyFile() {
|
||||||
|
return keyFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if it's a Java project
|
||||||
|
*/
|
||||||
|
public boolean isJavaProject() {
|
||||||
|
return this == JAVA_MAVEN || this == JAVA_GRADLE || this == SPRING_BOOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if it's a JavaScript project
|
||||||
|
*/
|
||||||
|
public boolean isJavaScriptProject() {
|
||||||
|
return this == NODE_JS || this == REACT || this == VUE ||
|
||||||
|
this == ANGULAR || this == NEXT_JS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if it's a Python project
|
||||||
|
*/
|
||||||
|
public boolean isPythonProject() {
|
||||||
|
return this == PYTHON || this == DJANGO || this == FLASK || this == FASTAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if it's a Web project
|
||||||
|
*/
|
||||||
|
public boolean isWebProject() {
|
||||||
|
return isJavaScriptProject() || this == HTML_STATIC ||
|
||||||
|
this == DJANGO || this == FLASK || this == FASTAPI || this == SPRING_BOOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the primary programming language of the project
|
||||||
|
*/
|
||||||
|
public String getPrimaryLanguage() {
|
||||||
|
if (isJavaProject()) return "Java";
|
||||||
|
if (isJavaScriptProject()) return "JavaScript";
|
||||||
|
if (isPythonProject()) return "Python";
|
||||||
|
|
||||||
|
switch (this) {
|
||||||
|
case DOTNET: return "C#";
|
||||||
|
case GO: return "Go";
|
||||||
|
case RUST: return "Rust";
|
||||||
|
case PHP: return "PHP";
|
||||||
|
case HTML_STATIC: return "HTML";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the recommended package manager
|
||||||
|
*/
|
||||||
|
public String getPackageManager() {
|
||||||
|
switch (this) {
|
||||||
|
case JAVA_MAVEN:
|
||||||
|
case SPRING_BOOT:
|
||||||
|
return "Maven";
|
||||||
|
case JAVA_GRADLE:
|
||||||
|
return "Gradle";
|
||||||
|
case NODE_JS:
|
||||||
|
case REACT:
|
||||||
|
case VUE:
|
||||||
|
case ANGULAR:
|
||||||
|
case NEXT_JS:
|
||||||
|
return "npm/yarn";
|
||||||
|
case PYTHON:
|
||||||
|
case DJANGO:
|
||||||
|
case FLASK:
|
||||||
|
case FASTAPI:
|
||||||
|
return "pip";
|
||||||
|
case DOTNET:
|
||||||
|
return "NuGet";
|
||||||
|
case GO:
|
||||||
|
return "go mod";
|
||||||
|
case RUST:
|
||||||
|
return "Cargo";
|
||||||
|
case PHP:
|
||||||
|
return "Composer";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class TaskStatus {
|
||||||
|
private String taskId;
|
||||||
|
private String status; // RUNNING, COMPLETED, ERROR
|
||||||
|
private String currentAction;
|
||||||
|
private String summary;
|
||||||
|
private int currentTurn;
|
||||||
|
private int totalEstimatedTurns;
|
||||||
|
private long startTime;
|
||||||
|
private long lastUpdateTime;
|
||||||
|
private List<String> actionHistory;
|
||||||
|
private String errorMessage;
|
||||||
|
private double progressPercentage;
|
||||||
|
|
||||||
|
public TaskStatus(String taskId) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
this.status = "RUNNING";
|
||||||
|
this.startTime = System.currentTimeMillis();
|
||||||
|
this.lastUpdateTime = this.startTime;
|
||||||
|
this.actionHistory = new ArrayList<>();
|
||||||
|
this.progressPercentage = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getTaskId() { return taskId; }
|
||||||
|
public void setTaskId(String taskId) { this.taskId = taskId; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
this.lastUpdateTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentAction() { return currentAction; }
|
||||||
|
public void setCurrentAction(String currentAction) {
|
||||||
|
this.currentAction = currentAction;
|
||||||
|
this.lastUpdateTime = System.currentTimeMillis();
|
||||||
|
if (currentAction != null && !currentAction.trim().isEmpty()) {
|
||||||
|
this.actionHistory.add(currentAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSummary() { return summary; }
|
||||||
|
public void setSummary(String summary) { this.summary = summary; }
|
||||||
|
|
||||||
|
public int getCurrentTurn() { return currentTurn; }
|
||||||
|
public void setCurrentTurn(int currentTurn) {
|
||||||
|
this.currentTurn = currentTurn;
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalEstimatedTurns() { return totalEstimatedTurns; }
|
||||||
|
public void setTotalEstimatedTurns(int totalEstimatedTurns) {
|
||||||
|
this.totalEstimatedTurns = totalEstimatedTurns;
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStartTime() { return startTime; }
|
||||||
|
public long getLastUpdateTime() { return lastUpdateTime; }
|
||||||
|
|
||||||
|
public List<String> getActionHistory() { return actionHistory; }
|
||||||
|
|
||||||
|
public String getErrorMessage() { return errorMessage; }
|
||||||
|
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||||
|
|
||||||
|
public double getProgressPercentage() { return progressPercentage; }
|
||||||
|
|
||||||
|
private void updateProgress() {
|
||||||
|
if (totalEstimatedTurns > 0) {
|
||||||
|
this.progressPercentage = Math.min(100.0, (double) currentTurn / totalEstimatedTurns * 100.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getElapsedTime() {
|
||||||
|
return System.currentTimeMillis() - startTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package com.example.demo.schema;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Schema definition class
|
||||||
|
* Used to define tool parameter structure and validation rules
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class JsonSchema {
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
private String description;
|
||||||
|
private String pattern;
|
||||||
|
private Number minimum;
|
||||||
|
private Number maximum;
|
||||||
|
private List<Object> enumValues;
|
||||||
|
|
||||||
|
@JsonProperty("properties")
|
||||||
|
private Map<String, JsonSchema> properties;
|
||||||
|
|
||||||
|
@JsonProperty("required")
|
||||||
|
private List<String> requiredFields;
|
||||||
|
|
||||||
|
@JsonProperty("items")
|
||||||
|
private JsonSchema items;
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
public JsonSchema() {}
|
||||||
|
|
||||||
|
// Static factory methods
|
||||||
|
public static JsonSchema object() {
|
||||||
|
JsonSchema schema = new JsonSchema();
|
||||||
|
schema.type = "object";
|
||||||
|
schema.properties = new HashMap<>();
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonSchema string(String description) {
|
||||||
|
JsonSchema schema = new JsonSchema();
|
||||||
|
schema.type = "string";
|
||||||
|
schema.description = description;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonSchema number(String description) {
|
||||||
|
JsonSchema schema = new JsonSchema();
|
||||||
|
schema.type = "number";
|
||||||
|
schema.description = description;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonSchema integer(String description) {
|
||||||
|
JsonSchema schema = new JsonSchema();
|
||||||
|
schema.type = "integer";
|
||||||
|
schema.description = description;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonSchema bool(String description) {
|
||||||
|
JsonSchema schema = new JsonSchema();
|
||||||
|
schema.type = "boolean";
|
||||||
|
schema.description = description;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonSchema array(JsonSchema items) {
|
||||||
|
JsonSchema schema = new JsonSchema();
|
||||||
|
schema.type = "array";
|
||||||
|
schema.items = items;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonSchema array(String description, JsonSchema items) {
|
||||||
|
JsonSchema schema = new JsonSchema();
|
||||||
|
schema.type = "array";
|
||||||
|
schema.description = description;
|
||||||
|
schema.items = items;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fluent methods
|
||||||
|
public JsonSchema addProperty(String name, JsonSchema property) {
|
||||||
|
if (this.properties == null) {
|
||||||
|
this.properties = new HashMap<>();
|
||||||
|
}
|
||||||
|
this.properties.put(name, property);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonSchema required(String... fields) {
|
||||||
|
this.requiredFields = Arrays.asList(fields);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonSchema pattern(String pattern) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonSchema minimum(Number minimum) {
|
||||||
|
this.minimum = minimum;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonSchema maximum(Number maximum) {
|
||||||
|
this.maximum = maximum;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonSchema enumValues(Object... values) {
|
||||||
|
this.enumValues = Arrays.asList(values);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getType() { return type; }
|
||||||
|
public void setType(String type) { this.type = type; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getPattern() { return pattern; }
|
||||||
|
public void setPattern(String pattern) { this.pattern = pattern; }
|
||||||
|
|
||||||
|
public Number getMinimum() { return minimum; }
|
||||||
|
public void setMinimum(Number minimum) { this.minimum = minimum; }
|
||||||
|
|
||||||
|
public Number getMaximum() { return maximum; }
|
||||||
|
public void setMaximum(Number maximum) { this.maximum = maximum; }
|
||||||
|
|
||||||
|
public List<Object> getEnumValues() { return enumValues; }
|
||||||
|
public void setEnumValues(List<Object> enumValues) { this.enumValues = enumValues; }
|
||||||
|
|
||||||
|
public Map<String, JsonSchema> getProperties() { return properties; }
|
||||||
|
public void setProperties(Map<String, JsonSchema> properties) { this.properties = properties; }
|
||||||
|
|
||||||
|
public List<String> getRequiredFields() { return requiredFields; }
|
||||||
|
public void setRequiredFields(List<String> requiredFields) { this.requiredFields = requiredFields; }
|
||||||
|
|
||||||
|
public JsonSchema getItems() { return items; }
|
||||||
|
public void setItems(JsonSchema items) { this.items = items; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package com.example.demo.schema;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.networknt.schema.JsonSchemaFactory;
|
||||||
|
import com.networknt.schema.SpecVersion;
|
||||||
|
import com.networknt.schema.ValidationMessage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Schema validator
|
||||||
|
* Used to validate tool parameters against defined schema
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SchemaValidator {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SchemaValidator.class);
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final JsonSchemaFactory schemaFactory;
|
||||||
|
|
||||||
|
public SchemaValidator() {
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate data against schema
|
||||||
|
*
|
||||||
|
* @param schema JSON Schema definition
|
||||||
|
* @param data Data to validate
|
||||||
|
* @return Validation error message, null means validation passed
|
||||||
|
*/
|
||||||
|
public String validate(JsonSchema schema, Object data) {
|
||||||
|
try {
|
||||||
|
// Convert custom JsonSchema to standard JSON Schema string
|
||||||
|
String schemaJson = objectMapper.writeValueAsString(schema);
|
||||||
|
logger.debug("Schema JSON: {}", schemaJson);
|
||||||
|
|
||||||
|
// Create JSON Schema validator
|
||||||
|
com.networknt.schema.JsonSchema jsonSchema = schemaFactory.getSchema(schemaJson);
|
||||||
|
|
||||||
|
// Convert data to JsonNode
|
||||||
|
String dataJson = objectMapper.writeValueAsString(data);
|
||||||
|
JsonNode dataNode = objectMapper.readTree(dataJson);
|
||||||
|
logger.debug("Data JSON: {}", dataJson);
|
||||||
|
|
||||||
|
// Execute validation
|
||||||
|
Set<ValidationMessage> errors = jsonSchema.validate(dataNode);
|
||||||
|
|
||||||
|
if (errors.isEmpty()) {
|
||||||
|
logger.debug("Schema validation passed");
|
||||||
|
return null; // Validation passed
|
||||||
|
} else {
|
||||||
|
String errorMessage = errors.stream()
|
||||||
|
.map(ValidationMessage::getMessage)
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
logger.warn("Schema validation failed: {}", errorMessage);
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
String errorMessage = "Schema validation error: " + e.getMessage();
|
||||||
|
logger.error(errorMessage, e);
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple type validation (fallback solution)
|
||||||
|
* Used when JSON Schema validation fails
|
||||||
|
*/
|
||||||
|
public String validateSimple(JsonSchema schema, Object data) {
|
||||||
|
if (schema == null || data == null) {
|
||||||
|
return "Schema or data is null";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic type checking
|
||||||
|
String expectedType = schema.getType();
|
||||||
|
if (expectedType != null) {
|
||||||
|
String actualType = getDataType(data);
|
||||||
|
if (!isTypeCompatible(expectedType, actualType)) {
|
||||||
|
return String.format("Type mismatch: expected %s, got %s", expectedType, actualType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required field checking (only for object type)
|
||||||
|
if ("object".equals(expectedType) && schema.getRequiredFields() != null) {
|
||||||
|
if (!(data instanceof java.util.Map)) {
|
||||||
|
return "Expected object type for required field validation";
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.Map<String, Object> dataMap = (java.util.Map<String, Object>) data;
|
||||||
|
|
||||||
|
for (String requiredField : schema.getRequiredFields()) {
|
||||||
|
if (!dataMap.containsKey(requiredField) || dataMap.get(requiredField) == null) {
|
||||||
|
return "Missing required field: " + requiredField;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Validation passed
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDataType(Object data) {
|
||||||
|
if (data == null) return "null";
|
||||||
|
if (data instanceof String) return "string";
|
||||||
|
if (data instanceof Integer || data instanceof Long) return "integer";
|
||||||
|
if (data instanceof Number) return "number";
|
||||||
|
if (data instanceof Boolean) return "boolean";
|
||||||
|
if (data instanceof java.util.List) return "array";
|
||||||
|
if (data instanceof java.util.Map) return "object";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTypeCompatible(String expectedType, String actualType) {
|
||||||
|
if (expectedType.equals(actualType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number type compatibility
|
||||||
|
if ("number".equals(expectedType) && "integer".equals(actualType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志事件基类
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class LogEvent {
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
private String taskId;
|
||||||
|
private String message;
|
||||||
|
private String timestamp;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
public LogEvent() {}
|
||||||
|
|
||||||
|
public LogEvent(String type, String taskId, String message, String timestamp) {
|
||||||
|
this.type = type;
|
||||||
|
this.taskId = taskId;
|
||||||
|
this.message = message;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static factory methods
|
||||||
|
public static LogEvent createConnectionEvent(String taskId) {
|
||||||
|
LogEvent event = new LogEvent();
|
||||||
|
event.setType("CONNECTION_ESTABLISHED");
|
||||||
|
event.setTaskId(taskId);
|
||||||
|
event.setMessage("SSE连接已建立");
|
||||||
|
event.setTimestamp(java.time.LocalDateTime.now().format(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 String getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(String timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "LogEvent{" +
|
||||||
|
"type='" + type + '\'' +
|
||||||
|
", taskId='" + taskId + '\'' +
|
||||||
|
", message='" + message + '\'' +
|
||||||
|
", timestamp='" + timestamp + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE日志推送服务
|
||||||
|
* 负责将AOP日志实时推送到前端
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class LogStreamService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LogStreamService.class);
|
||||||
|
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
// 活跃的SSE连接 taskId -> SseEmitter
|
||||||
|
private final Map<String, SseEmitter> activeConnections = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// JSON序列化器
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建立SSE连接
|
||||||
|
*/
|
||||||
|
public SseEmitter createConnection(String taskId) {
|
||||||
|
logger.info("🔗 建立SSE连接: taskId={}", taskId);
|
||||||
|
|
||||||
|
SseEmitter emitter = new SseEmitter(0L); // 无超时
|
||||||
|
|
||||||
|
// 设置连接事件处理
|
||||||
|
emitter.onCompletion(() -> {
|
||||||
|
logger.info("✅ SSE连接完成: taskId={}", taskId);
|
||||||
|
activeConnections.remove(taskId);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.onTimeout(() -> {
|
||||||
|
logger.warn("⏰ SSE连接超时: taskId={}", taskId);
|
||||||
|
activeConnections.remove(taskId);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.onError((ex) -> {
|
||||||
|
logger.error("❌ SSE连接错误: taskId={}, error={}", taskId, ex.getMessage());
|
||||||
|
activeConnections.remove(taskId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存连接
|
||||||
|
activeConnections.put(taskId, emitter);
|
||||||
|
|
||||||
|
// 发送连接成功消息
|
||||||
|
sendLogEvent(taskId, LogEvent.createConnectionEvent(taskId));
|
||||||
|
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭SSE连接
|
||||||
|
*/
|
||||||
|
public void closeConnection(String taskId) {
|
||||||
|
SseEmitter emitter = activeConnections.remove(taskId);
|
||||||
|
if (emitter != null) {
|
||||||
|
try {
|
||||||
|
emitter.complete();
|
||||||
|
logger.info("🔚 关闭SSE连接: taskId={}", taskId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("关闭SSE连接失败: taskId={}, error={}", taskId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送工具开始执行事件
|
||||||
|
*/
|
||||||
|
public void pushToolStart(String taskId, String toolName, String filePath, String message) {
|
||||||
|
ToolLogEvent event = new ToolLogEvent();
|
||||||
|
event.setType("TOOL_START");
|
||||||
|
event.setTaskId(taskId);
|
||||||
|
event.setToolName(toolName);
|
||||||
|
event.setFilePath(filePath);
|
||||||
|
event.setMessage(message);
|
||||||
|
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||||
|
event.setIcon(getToolIcon(toolName));
|
||||||
|
event.setStatus("RUNNING");
|
||||||
|
|
||||||
|
sendLogEvent(taskId, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送工具执行成功事件
|
||||||
|
*/
|
||||||
|
public void pushToolSuccess(String taskId, String toolName, String filePath, String message, long executionTime) {
|
||||||
|
ToolLogEvent event = new ToolLogEvent();
|
||||||
|
event.setType("TOOL_SUCCESS");
|
||||||
|
event.setTaskId(taskId);
|
||||||
|
event.setToolName(toolName);
|
||||||
|
event.setFilePath(filePath);
|
||||||
|
event.setMessage(message);
|
||||||
|
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||||
|
event.setIcon(getToolIcon(toolName));
|
||||||
|
event.setStatus("SUCCESS");
|
||||||
|
event.setExecutionTime(executionTime);
|
||||||
|
|
||||||
|
sendLogEvent(taskId, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送工具执行失败事件
|
||||||
|
*/
|
||||||
|
public void pushToolError(String taskId, String toolName, String filePath, String message, long executionTime) {
|
||||||
|
ToolLogEvent event = new ToolLogEvent();
|
||||||
|
event.setType("TOOL_ERROR");
|
||||||
|
event.setTaskId(taskId);
|
||||||
|
event.setToolName(toolName);
|
||||||
|
event.setFilePath(filePath);
|
||||||
|
event.setMessage(message);
|
||||||
|
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||||
|
event.setIcon("❌");
|
||||||
|
event.setStatus("ERROR");
|
||||||
|
event.setExecutionTime(executionTime);
|
||||||
|
|
||||||
|
sendLogEvent(taskId, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送任务完成事件
|
||||||
|
*/
|
||||||
|
public void pushTaskComplete(String taskId) {
|
||||||
|
LogEvent event = new LogEvent();
|
||||||
|
event.setType("TASK_COMPLETE");
|
||||||
|
event.setTaskId(taskId);
|
||||||
|
event.setMessage("任务执行完成");
|
||||||
|
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||||
|
|
||||||
|
sendLogEvent(taskId, event);
|
||||||
|
|
||||||
|
// 延迟关闭连接
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000); // 等待2秒让前端处理完成事件
|
||||||
|
closeConnection(taskId);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送日志事件到前端
|
||||||
|
*/
|
||||||
|
private void sendLogEvent(String taskId, Object event) {
|
||||||
|
SseEmitter emitter = activeConnections.get(taskId);
|
||||||
|
if (emitter != null) {
|
||||||
|
try {
|
||||||
|
String jsonData = objectMapper.writeValueAsString(event);
|
||||||
|
logger.info("📤 准备推送日志事件: taskId={}, type={}, data={}", taskId,
|
||||||
|
event instanceof LogEvent ? ((LogEvent) event).getType() : "unknown", jsonData);
|
||||||
|
|
||||||
|
emitter.send(SseEmitter.event()
|
||||||
|
.name("log")
|
||||||
|
.data(jsonData));
|
||||||
|
|
||||||
|
logger.info("✅ 日志事件推送成功: taskId={}", taskId);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("推送日志事件失败: taskId={}, error={}", taskId, e.getMessage());
|
||||||
|
activeConnections.remove(taskId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("⚠️ 未找到SSE连接: taskId={}, 无法推送事件", taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具图标
|
||||||
|
*/
|
||||||
|
private String getToolIcon(String toolName) {
|
||||||
|
switch (toolName) {
|
||||||
|
case "readFile": return "📖";
|
||||||
|
case "writeFile": return "✏️";
|
||||||
|
case "editFile": return "📝";
|
||||||
|
case "listDirectory": return "📁";
|
||||||
|
case "analyzeProject": return "🔍";
|
||||||
|
case "scaffoldProject": return "🏗️";
|
||||||
|
case "smartEdit": return "🧠";
|
||||||
|
default: return "⚙️";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取活跃连接数
|
||||||
|
*/
|
||||||
|
public int getActiveConnectionCount() {
|
||||||
|
return activeConnections.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,339 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.example.demo.model.ProjectContext;
|
||||||
|
import com.example.demo.model.ProjectStructure;
|
||||||
|
import com.example.demo.model.ProjectType;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目上下文分析器
|
||||||
|
* 提供完整的项目分析功能,生成AI可理解的项目上下文
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ProjectContextAnalyzer {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ProjectContextAnalyzer.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ProjectTypeDetector projectTypeDetector;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ProjectDiscoveryService projectDiscoveryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析项目并生成完整上下文
|
||||||
|
* @param projectRoot 项目根目录
|
||||||
|
* @return 项目上下文信息
|
||||||
|
*/
|
||||||
|
public ProjectContext analyzeProject(Path projectRoot) {
|
||||||
|
logger.info("Starting comprehensive project analysis for: {}", projectRoot);
|
||||||
|
|
||||||
|
ProjectContext context = new ProjectContext(projectRoot);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 检测项目类型
|
||||||
|
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||||
|
context.setProjectType(projectType);
|
||||||
|
logger.debug("Detected project type: {}", projectType);
|
||||||
|
|
||||||
|
// 2. 分析项目结构
|
||||||
|
ProjectStructure structure = projectDiscoveryService.analyzeProjectStructure(projectRoot);
|
||||||
|
context.setProjectStructure(structure);
|
||||||
|
logger.debug("Analyzed project structure with {} directories",
|
||||||
|
structure.getDirectories().size());
|
||||||
|
|
||||||
|
// 3. 分析依赖关系
|
||||||
|
List<ProjectContext.DependencyInfo> dependencies =
|
||||||
|
projectDiscoveryService.analyzeDependencies(projectRoot);
|
||||||
|
context.setDependencies(dependencies);
|
||||||
|
logger.debug("Found {} dependencies", dependencies.size());
|
||||||
|
|
||||||
|
// 4. 查找配置文件
|
||||||
|
List<ProjectContext.ConfigFile> configFiles =
|
||||||
|
projectDiscoveryService.findConfigurationFiles(projectRoot);
|
||||||
|
context.setConfigFiles(configFiles);
|
||||||
|
logger.debug("Found {} configuration files", configFiles.size());
|
||||||
|
|
||||||
|
// 5. 分析代码统计
|
||||||
|
ProjectContext.CodeStatistics codeStats = analyzeCodeStatistics(projectRoot, projectType);
|
||||||
|
context.setCodeStatistics(codeStats);
|
||||||
|
logger.debug("Code statistics: {} total lines", codeStats.getTotalLines());
|
||||||
|
|
||||||
|
// 6. 收集项目元数据
|
||||||
|
Map<String, Object> metadata = collectProjectMetadata(projectRoot, projectType);
|
||||||
|
context.setMetadata(metadata);
|
||||||
|
|
||||||
|
// 7. 生成上下文摘要
|
||||||
|
String summary = context.generateContextSummary();
|
||||||
|
logger.debug("Generated context summary with {} characters", summary.length());
|
||||||
|
|
||||||
|
logger.info("Project analysis completed successfully for: {}", projectRoot);
|
||||||
|
return context;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error during project analysis for: " + projectRoot, e);
|
||||||
|
// 返回部分分析结果
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析代码统计信息
|
||||||
|
*/
|
||||||
|
private ProjectContext.CodeStatistics analyzeCodeStatistics(Path projectRoot, ProjectType projectType) {
|
||||||
|
logger.debug("Analyzing code statistics for: {}", projectRoot);
|
||||||
|
|
||||||
|
ProjectContext.CodeStatistics stats = new ProjectContext.CodeStatistics();
|
||||||
|
|
||||||
|
try {
|
||||||
|
analyzeCodeInDirectory(projectRoot, stats, projectType, 0, 3);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Error analyzing code statistics", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归分析目录中的代码
|
||||||
|
*/
|
||||||
|
private void analyzeCodeInDirectory(Path directory, ProjectContext.CodeStatistics stats,
|
||||||
|
ProjectType projectType, int currentDepth, int maxDepth) {
|
||||||
|
if (currentDepth > maxDepth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Stream<Path> paths = Files.list(directory)) {
|
||||||
|
paths.forEach(path -> {
|
||||||
|
try {
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
String dirName = path.getFileName().toString();
|
||||||
|
// 跳过不需要分析的目录
|
||||||
|
if (!shouldSkipDirectory(dirName)) {
|
||||||
|
analyzeCodeInDirectory(path, stats, projectType, currentDepth + 1, maxDepth);
|
||||||
|
}
|
||||||
|
} else if (Files.isRegularFile(path)) {
|
||||||
|
analyzeCodeFile(path, stats, projectType);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Error processing path during code analysis: " + path, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error listing directory: " + directory, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析单个代码文件
|
||||||
|
*/
|
||||||
|
private void analyzeCodeFile(Path filePath, ProjectContext.CodeStatistics stats, ProjectType projectType) {
|
||||||
|
String fileName = filePath.getFileName().toString();
|
||||||
|
String extension = getFileExtension(fileName).toLowerCase();
|
||||||
|
|
||||||
|
// 只分析代码文件
|
||||||
|
if (!isCodeFile(extension, projectType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<String> lines = Files.readAllLines(filePath);
|
||||||
|
int totalLines = lines.size();
|
||||||
|
int codeLines = 0;
|
||||||
|
int commentLines = 0;
|
||||||
|
int blankLines = 0;
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
String trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.isEmpty()) {
|
||||||
|
blankLines++;
|
||||||
|
} else if (isCommentLine(trimmedLine, extension)) {
|
||||||
|
commentLines++;
|
||||||
|
} else {
|
||||||
|
codeLines++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
stats.setTotalLines(stats.getTotalLines() + totalLines);
|
||||||
|
stats.setCodeLines(stats.getCodeLines() + codeLines);
|
||||||
|
stats.setCommentLines(stats.getCommentLines() + commentLines);
|
||||||
|
stats.setBlankLines(stats.getBlankLines() + blankLines);
|
||||||
|
|
||||||
|
// 按语言统计
|
||||||
|
String language = getLanguageByExtension(extension);
|
||||||
|
stats.addLanguageLines(language, totalLines);
|
||||||
|
|
||||||
|
// 分析类和方法(简单实现)
|
||||||
|
if (extension.equals(".java")) {
|
||||||
|
analyzeJavaFile(lines, stats);
|
||||||
|
} else if (extension.equals(".js") || extension.equals(".ts")) {
|
||||||
|
analyzeJavaScriptFile(lines, stats);
|
||||||
|
} else if (extension.equals(".py")) {
|
||||||
|
analyzePythonFile(lines, stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error reading file for code analysis: " + filePath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析Java文件
|
||||||
|
*/
|
||||||
|
private void analyzeJavaFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||||
|
for (String line : lines) {
|
||||||
|
String trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.matches(".*\\bclass\\s+\\w+.*")) {
|
||||||
|
stats.setTotalClasses(stats.getTotalClasses() + 1);
|
||||||
|
}
|
||||||
|
if (trimmedLine.matches(".*\\b(public|private|protected)\\s+.*\\s+\\w+\\s*\\(.*\\).*")) {
|
||||||
|
stats.setTotalMethods(stats.getTotalMethods() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析JavaScript文件
|
||||||
|
*/
|
||||||
|
private void analyzeJavaScriptFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||||
|
for (String line : lines) {
|
||||||
|
String trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.matches(".*\\bfunction\\s+\\w+.*") ||
|
||||||
|
trimmedLine.matches(".*\\w+\\s*:\\s*function.*") ||
|
||||||
|
trimmedLine.matches(".*\\w+\\s*=\\s*\\(.*\\)\\s*=>.*")) {
|
||||||
|
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析Python文件
|
||||||
|
*/
|
||||||
|
private void analyzePythonFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||||
|
for (String line : lines) {
|
||||||
|
String trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.matches("^class\\s+\\w+.*:")) {
|
||||||
|
stats.setTotalClasses(stats.getTotalClasses() + 1);
|
||||||
|
}
|
||||||
|
if (trimmedLine.matches("^def\\s+\\w+.*:")) {
|
||||||
|
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集项目元数据
|
||||||
|
*/
|
||||||
|
private Map<String, Object> collectProjectMetadata(Path projectRoot, ProjectType projectType) {
|
||||||
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
|
|
||||||
|
metadata.put("projectName", projectRoot.getFileName().toString());
|
||||||
|
metadata.put("projectType", projectType.name());
|
||||||
|
metadata.put("primaryLanguage", projectType.getPrimaryLanguage());
|
||||||
|
metadata.put("packageManager", projectType.getPackageManager());
|
||||||
|
metadata.put("analysisTimestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 检查版本控制
|
||||||
|
if (Files.exists(projectRoot.resolve(".git"))) {
|
||||||
|
metadata.put("versionControl", "Git");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查CI/CD配置
|
||||||
|
if (Files.exists(projectRoot.resolve(".github"))) {
|
||||||
|
metadata.put("cicd", "GitHub Actions");
|
||||||
|
} else if (Files.exists(projectRoot.resolve(".gitlab-ci.yml"))) {
|
||||||
|
metadata.put("cicd", "GitLab CI");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查Docker支持
|
||||||
|
if (Files.exists(projectRoot.resolve("Dockerfile"))) {
|
||||||
|
metadata.put("containerization", "Docker");
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成编辑上下文
|
||||||
|
*/
|
||||||
|
public String buildEditContext(Path projectRoot, String editDescription) {
|
||||||
|
logger.debug("Building edit context for: {}", projectRoot);
|
||||||
|
|
||||||
|
ProjectContext context = analyzeProject(projectRoot);
|
||||||
|
|
||||||
|
StringBuilder contextBuilder = new StringBuilder();
|
||||||
|
contextBuilder.append("=== EDIT CONTEXT ===\n");
|
||||||
|
contextBuilder.append("Edit Request: ").append(editDescription).append("\n\n");
|
||||||
|
contextBuilder.append(context.generateContextSummary());
|
||||||
|
|
||||||
|
return contextBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法
|
||||||
|
private boolean shouldSkipDirectory(String dirName) {
|
||||||
|
return dirName.equals(".git") || dirName.equals("node_modules") ||
|
||||||
|
dirName.equals("target") || dirName.equals("build") ||
|
||||||
|
dirName.equals("dist") || dirName.equals("__pycache__") ||
|
||||||
|
dirName.startsWith(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileExtension(String fileName) {
|
||||||
|
int lastDot = fileName.lastIndexOf('.');
|
||||||
|
return lastDot > 0 ? fileName.substring(lastDot) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCodeFile(String extension, ProjectType projectType) {
|
||||||
|
return extension.equals(".java") || extension.equals(".js") || extension.equals(".ts") ||
|
||||||
|
extension.equals(".py") || extension.equals(".html") || extension.equals(".css") ||
|
||||||
|
extension.equals(".jsx") || extension.equals(".tsx") || extension.equals(".vue") ||
|
||||||
|
extension.equals(".go") || extension.equals(".rs") || extension.equals(".php") ||
|
||||||
|
extension.equals(".cs") || extension.equals(".cpp") || extension.equals(".c");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCommentLine(String line, String extension) {
|
||||||
|
switch (extension) {
|
||||||
|
case ".java":
|
||||||
|
case ".js":
|
||||||
|
case ".ts":
|
||||||
|
case ".jsx":
|
||||||
|
case ".tsx":
|
||||||
|
case ".css":
|
||||||
|
return line.startsWith("//") || line.startsWith("/*") || line.startsWith("*");
|
||||||
|
case ".py":
|
||||||
|
return line.startsWith("#");
|
||||||
|
case ".html":
|
||||||
|
return line.startsWith("<!--");
|
||||||
|
default:
|
||||||
|
return line.startsWith("#") || line.startsWith("//");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLanguageByExtension(String extension) {
|
||||||
|
switch (extension) {
|
||||||
|
case ".java": return "Java";
|
||||||
|
case ".js": case ".jsx": return "JavaScript";
|
||||||
|
case ".ts": case ".tsx": return "TypeScript";
|
||||||
|
case ".py": return "Python";
|
||||||
|
case ".html": return "HTML";
|
||||||
|
case ".css": return "CSS";
|
||||||
|
case ".vue": return "Vue";
|
||||||
|
case ".go": return "Go";
|
||||||
|
case ".rs": return "Rust";
|
||||||
|
case ".php": return "PHP";
|
||||||
|
case ".cs": return "C#";
|
||||||
|
default: return "Other";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.example.demo.model.ProjectContext;
|
||||||
|
import com.example.demo.model.ProjectStructure;
|
||||||
|
import com.example.demo.model.ProjectType;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目发现和分析服务
|
||||||
|
* 负责分析项目结构、依赖关系和配置信息
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ProjectDiscoveryService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ProjectDiscoveryService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProjectTypeDetector projectTypeDetector;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析项目结构
|
||||||
|
* @param projectRoot 项目根目录
|
||||||
|
* @return 项目结构信息
|
||||||
|
*/
|
||||||
|
public ProjectStructure analyzeProjectStructure(Path projectRoot) {
|
||||||
|
logger.debug("Analyzing project structure for: {}", projectRoot);
|
||||||
|
|
||||||
|
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||||
|
ProjectStructure structure = new ProjectStructure(projectRoot, projectType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
analyzeDirectoryStructure(projectRoot, structure, 0, 3); // 最大深度3层
|
||||||
|
structure.markImportantDirectories();
|
||||||
|
|
||||||
|
logger.info("Project structure analysis completed for: {}", projectRoot);
|
||||||
|
return structure;
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error analyzing project structure for: " + projectRoot, e);
|
||||||
|
return structure; // 返回部分分析结果
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归分析目录结构
|
||||||
|
*/
|
||||||
|
private void analyzeDirectoryStructure(Path currentPath, ProjectStructure structure,
|
||||||
|
int currentDepth, int maxDepth) throws IOException {
|
||||||
|
if (currentDepth > maxDepth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Stream<Path> paths = Files.list(currentPath)) {
|
||||||
|
paths.forEach(path -> {
|
||||||
|
try {
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
String dirName = path.getFileName().toString();
|
||||||
|
String relativePath = structure.getProjectRoot().relativize(path).toString();
|
||||||
|
|
||||||
|
// 跳过常见的忽略目录
|
||||||
|
if (shouldIgnoreDirectory(dirName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectStructure.DirectoryInfo dirInfo =
|
||||||
|
new ProjectStructure.DirectoryInfo(dirName, relativePath);
|
||||||
|
|
||||||
|
// 分析目录中的文件
|
||||||
|
analyzeDirectoryFiles(path, dirInfo);
|
||||||
|
structure.addDirectory(dirInfo);
|
||||||
|
|
||||||
|
// 递归分析子目录
|
||||||
|
if (currentDepth < maxDepth) {
|
||||||
|
analyzeDirectoryStructure(path, structure, currentDepth + 1, maxDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (Files.isRegularFile(path)) {
|
||||||
|
// 处理根目录下的文件
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
String extension = getFileExtension(fileName);
|
||||||
|
|
||||||
|
structure.addFileType(extension, 1);
|
||||||
|
structure.setTotalFiles(structure.getTotalFiles() + 1);
|
||||||
|
|
||||||
|
// 检查是否为关键文件
|
||||||
|
if (isKeyFile(fileName, structure.getProjectType())) {
|
||||||
|
structure.addKeyFile(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 累计文件大小
|
||||||
|
try {
|
||||||
|
structure.setTotalSize(structure.getTotalSize() + Files.size(path));
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Could not get size for file: {}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Error processing path: " + path, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析目录中的文件
|
||||||
|
*/
|
||||||
|
private void analyzeDirectoryFiles(Path directory, ProjectStructure.DirectoryInfo dirInfo) {
|
||||||
|
try (Stream<Path> files = Files.list(directory)) {
|
||||||
|
files.filter(Files::isRegularFile)
|
||||||
|
.forEach(file -> {
|
||||||
|
String fileName = file.getFileName().toString();
|
||||||
|
dirInfo.addFile(fileName);
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error analyzing files in directory: {}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析项目依赖
|
||||||
|
*/
|
||||||
|
public List<ProjectContext.DependencyInfo> analyzeDependencies(Path projectRoot) {
|
||||||
|
logger.debug("Analyzing dependencies for: {}", projectRoot);
|
||||||
|
|
||||||
|
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||||
|
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (projectType) {
|
||||||
|
case JAVA_MAVEN:
|
||||||
|
case SPRING_BOOT:
|
||||||
|
dependencies.addAll(analyzeMavenDependencies(projectRoot));
|
||||||
|
break;
|
||||||
|
case NODE_JS:
|
||||||
|
case REACT:
|
||||||
|
case VUE:
|
||||||
|
case ANGULAR:
|
||||||
|
case NEXT_JS:
|
||||||
|
dependencies.addAll(analyzeNpmDependencies(projectRoot));
|
||||||
|
break;
|
||||||
|
case PYTHON:
|
||||||
|
case DJANGO:
|
||||||
|
case FLASK:
|
||||||
|
case FASTAPI:
|
||||||
|
dependencies.addAll(analyzePythonDependencies(projectRoot));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.info("Dependency analysis not supported for project type: {}", projectType);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error analyzing dependencies for: " + projectRoot, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Found {} dependencies for project: {}", dependencies.size(), projectRoot);
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析Maven依赖
|
||||||
|
*/
|
||||||
|
private List<ProjectContext.DependencyInfo> analyzeMavenDependencies(Path projectRoot) {
|
||||||
|
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||||
|
Path pomFile = projectRoot.resolve("pom.xml");
|
||||||
|
|
||||||
|
if (!Files.exists(pomFile)) {
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String pomContent = Files.readString(pomFile);
|
||||||
|
// 简单的XML解析 - 在实际项目中应该使用专门的XML解析器
|
||||||
|
if (pomContent.contains("spring-boot-starter-web")) {
|
||||||
|
dependencies.add(new ProjectContext.DependencyInfo(
|
||||||
|
"spring-boot-starter-web", "auto", "compile"));
|
||||||
|
}
|
||||||
|
if (pomContent.contains("spring-boot-starter-data-jpa")) {
|
||||||
|
dependencies.add(new ProjectContext.DependencyInfo(
|
||||||
|
"spring-boot-starter-data-jpa", "auto", "compile"));
|
||||||
|
}
|
||||||
|
if (pomContent.contains("spring-boot-starter-test")) {
|
||||||
|
dependencies.add(new ProjectContext.DependencyInfo(
|
||||||
|
"spring-boot-starter-test", "auto", "test"));
|
||||||
|
}
|
||||||
|
// 可以添加更多依赖检测逻辑
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error reading pom.xml", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析NPM依赖
|
||||||
|
*/
|
||||||
|
private List<ProjectContext.DependencyInfo> analyzeNpmDependencies(Path projectRoot) {
|
||||||
|
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||||
|
Path packageJsonPath = projectRoot.resolve("package.json");
|
||||||
|
|
||||||
|
if (!Files.exists(packageJsonPath)) {
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String content = Files.readString(packageJsonPath);
|
||||||
|
JsonNode packageJson = objectMapper.readTree(content);
|
||||||
|
|
||||||
|
// 分析生产依赖
|
||||||
|
JsonNode deps = packageJson.get("dependencies");
|
||||||
|
if (deps != null) {
|
||||||
|
deps.fields().forEachRemaining(entry -> {
|
||||||
|
dependencies.add(new ProjectContext.DependencyInfo(
|
||||||
|
entry.getKey(), entry.getValue().asText(), "production"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析开发依赖
|
||||||
|
JsonNode devDeps = packageJson.get("devDependencies");
|
||||||
|
if (devDeps != null) {
|
||||||
|
devDeps.fields().forEachRemaining(entry -> {
|
||||||
|
ProjectContext.DependencyInfo depInfo = new ProjectContext.DependencyInfo(
|
||||||
|
entry.getKey(), entry.getValue().asText(), "development");
|
||||||
|
depInfo.setDirectDependency(true);
|
||||||
|
dependencies.add(depInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error reading package.json", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析Python依赖
|
||||||
|
*/
|
||||||
|
private List<ProjectContext.DependencyInfo> analyzePythonDependencies(Path projectRoot) {
|
||||||
|
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||||
|
Path requirementsFile = projectRoot.resolve("requirements.txt");
|
||||||
|
|
||||||
|
if (Files.exists(requirementsFile)) {
|
||||||
|
try {
|
||||||
|
List<String> lines = Files.readAllLines(requirementsFile);
|
||||||
|
for (String line : lines) {
|
||||||
|
line = line.trim();
|
||||||
|
if (!line.isEmpty() && !line.startsWith("#")) {
|
||||||
|
String[] parts = line.split("==|>=|<=|>|<");
|
||||||
|
String name = parts[0].trim();
|
||||||
|
String version = parts.length > 1 ? parts[1].trim() : "latest";
|
||||||
|
dependencies.add(new ProjectContext.DependencyInfo(name, version, "runtime"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error reading requirements.txt", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找配置文件
|
||||||
|
*/
|
||||||
|
public List<ProjectContext.ConfigFile> findConfigurationFiles(Path projectRoot) {
|
||||||
|
logger.debug("Finding configuration files for: {}", projectRoot);
|
||||||
|
|
||||||
|
List<ProjectContext.ConfigFile> configFiles = new ArrayList<>();
|
||||||
|
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 通用配置文件
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "application.properties", "properties");
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "application.yml", "yaml");
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "application.yaml", "yaml");
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "config.json", "json");
|
||||||
|
|
||||||
|
// 项目类型特定的配置文件
|
||||||
|
switch (projectType) {
|
||||||
|
case JAVA_MAVEN:
|
||||||
|
case SPRING_BOOT:
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "pom.xml", "xml");
|
||||||
|
break;
|
||||||
|
case NODE_JS:
|
||||||
|
case REACT:
|
||||||
|
case VUE:
|
||||||
|
case ANGULAR:
|
||||||
|
case NEXT_JS:
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "package.json", "json");
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "webpack.config.js", "javascript");
|
||||||
|
break;
|
||||||
|
case PYTHON:
|
||||||
|
case DJANGO:
|
||||||
|
case FLASK:
|
||||||
|
case FASTAPI:
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "requirements.txt", "text");
|
||||||
|
addConfigFileIfExists(configFiles, projectRoot, "setup.py", "python");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error finding configuration files for: " + projectRoot, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Found {} configuration files for project: {}", configFiles.size(), projectRoot);
|
||||||
|
return configFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加配置文件(如果存在)
|
||||||
|
*/
|
||||||
|
private void addConfigFileIfExists(List<ProjectContext.ConfigFile> configFiles,
|
||||||
|
Path projectRoot, String fileName, String fileType) {
|
||||||
|
Path configPath = projectRoot.resolve(fileName);
|
||||||
|
if (Files.exists(configPath)) {
|
||||||
|
String relativePath = projectRoot.relativize(configPath).toString();
|
||||||
|
ProjectContext.ConfigFile configFile =
|
||||||
|
new ProjectContext.ConfigFile(fileName, relativePath, fileType);
|
||||||
|
|
||||||
|
// 标记主要配置文件
|
||||||
|
if (fileName.equals("pom.xml") || fileName.equals("package.json") ||
|
||||||
|
fileName.startsWith("application.")) {
|
||||||
|
configFile.setMainConfig(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
configFiles.add(configFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否应该忽略目录
|
||||||
|
*/
|
||||||
|
private boolean shouldIgnoreDirectory(String dirName) {
|
||||||
|
return dirName.equals(".git") || dirName.equals(".svn") ||
|
||||||
|
dirName.equals("node_modules") || dirName.equals("target") ||
|
||||||
|
dirName.equals("build") || dirName.equals("dist") ||
|
||||||
|
dirName.equals("__pycache__") || dirName.equals(".idea") ||
|
||||||
|
dirName.equals(".vscode") || dirName.startsWith(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名
|
||||||
|
*/
|
||||||
|
private String getFileExtension(String fileName) {
|
||||||
|
int lastDot = fileName.lastIndexOf('.');
|
||||||
|
return lastDot > 0 ? fileName.substring(lastDot) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为关键文件
|
||||||
|
*/
|
||||||
|
private boolean isKeyFile(String fileName, ProjectType projectType) {
|
||||||
|
// 通用关键文件
|
||||||
|
if (fileName.equals("README.md") || fileName.equals("LICENSE") ||
|
||||||
|
fileName.equals("Dockerfile") || fileName.equals(".gitignore")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 项目类型特定的关键文件
|
||||||
|
if (projectType != null) {
|
||||||
|
String keyFile = projectType.getKeyFile();
|
||||||
|
if (keyFile != null && !keyFile.isEmpty()) {
|
||||||
|
return fileName.equals(keyFile) || fileName.matches(keyFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.example.demo.model.ProjectType;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目模板扩展服务
|
||||||
|
* 生成README、gitignore等通用文件模板
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ProjectTemplateExtensions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成README.md内容
|
||||||
|
*/
|
||||||
|
public String generateReadmeContent(Map<String, String> variables) {
|
||||||
|
return String.format("""
|
||||||
|
# %s
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Java 17 or higher (for Java projects)
|
||||||
|
- Node.js 16+ (for JavaScript projects)
|
||||||
|
- Python 3.8+ (for Python projects)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd %s
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
# For Java Maven projects
|
||||||
|
mvn clean install
|
||||||
|
|
||||||
|
# For Node.js projects
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# For Python projects
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the application:
|
||||||
|
```bash
|
||||||
|
# For Java Maven projects
|
||||||
|
mvn spring-boot:run
|
||||||
|
|
||||||
|
# For Node.js projects
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# For Python projects
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
%s/
|
||||||
|
├── src/ # Source code
|
||||||
|
├── test/ # Test files
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── README.md # This file
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Java projects
|
||||||
|
mvn test
|
||||||
|
|
||||||
|
# For Node.js projects
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# For Python projects
|
||||||
|
python -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Java projects
|
||||||
|
mvn clean package
|
||||||
|
|
||||||
|
# For Node.js projects
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# For Python projects
|
||||||
|
python setup.py build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Features
|
||||||
|
|
||||||
|
- Feature 1: Description
|
||||||
|
- Feature 2: Description
|
||||||
|
- Feature 3: Description
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the project
|
||||||
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 👥 Authors
|
||||||
|
|
||||||
|
- **%s** - *Initial work* - [%s](mailto:%s)
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- Hat tip to anyone whose code was used
|
||||||
|
- Inspiration
|
||||||
|
- etc
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Created with ❤️ by %s
|
||||||
|
""",
|
||||||
|
variables.get("PROJECT_NAME_PASCAL"),
|
||||||
|
variables.get("DESCRIPTION"),
|
||||||
|
variables.get("PROJECT_NAME"),
|
||||||
|
variables.get("PROJECT_NAME"),
|
||||||
|
variables.get("AUTHOR"),
|
||||||
|
variables.get("AUTHOR"),
|
||||||
|
variables.get("EMAIL"),
|
||||||
|
variables.get("AUTHOR")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成.gitignore内容
|
||||||
|
*/
|
||||||
|
public String generateGitignoreContent(ProjectType projectType) {
|
||||||
|
StringBuilder gitignore = new StringBuilder();
|
||||||
|
|
||||||
|
// 通用忽略规则
|
||||||
|
gitignore.append("""
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
""");
|
||||||
|
|
||||||
|
// 项目类型特定的忽略规则
|
||||||
|
switch (projectType) {
|
||||||
|
case JAVA_MAVEN:
|
||||||
|
case JAVA_GRADLE:
|
||||||
|
case SPRING_BOOT:
|
||||||
|
gitignore.append("""
|
||||||
|
# Java
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
*.nar
|
||||||
|
hs_err_pid*
|
||||||
|
|
||||||
|
# Maven
|
||||||
|
target/
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.next
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
# Spring Boot
|
||||||
|
spring-boot-*.log
|
||||||
|
|
||||||
|
""");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NODE_JS:
|
||||||
|
case REACT:
|
||||||
|
case VUE:
|
||||||
|
case ANGULAR:
|
||||||
|
case NEXT_JS:
|
||||||
|
gitignore.append("""
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# parcel-bundler cache
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
public
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
""");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PYTHON:
|
||||||
|
case DJANGO:
|
||||||
|
case FLASK:
|
||||||
|
case FASTAPI:
|
||||||
|
gitignore.append("""
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
""");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 基本忽略规则已经添加
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gitignore.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.example.demo.model.ProjectType;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目类型检测器
|
||||||
|
* 基于文件特征自动识别项目类型
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ProjectTypeDetector {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ProjectTypeDetector.class);
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测项目类型
|
||||||
|
* @param projectRoot 项目根目录
|
||||||
|
* @return 检测到的项目类型
|
||||||
|
*/
|
||||||
|
public ProjectType detectProjectType(Path projectRoot) {
|
||||||
|
if (!Files.exists(projectRoot) || !Files.isDirectory(projectRoot)) {
|
||||||
|
logger.warn("Project root does not exist or is not a directory: {}", projectRoot);
|
||||||
|
return ProjectType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("Detecting project type for: {}", projectRoot);
|
||||||
|
|
||||||
|
// 按优先级检测项目类型
|
||||||
|
ProjectType detectedType = detectByKeyFiles(projectRoot);
|
||||||
|
if (detectedType != ProjectType.UNKNOWN) {
|
||||||
|
logger.info("Detected project type: {} for {}", detectedType, projectRoot);
|
||||||
|
return detectedType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果关键文件检测失败,尝试基于目录结构检测
|
||||||
|
detectedType = detectByDirectoryStructure(projectRoot);
|
||||||
|
if (detectedType != ProjectType.UNKNOWN) {
|
||||||
|
logger.info("Detected project type by structure: {} for {}", detectedType, projectRoot);
|
||||||
|
return detectedType;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Could not determine project type for: {}", projectRoot);
|
||||||
|
return ProjectType.UNKNOWN;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error detecting project type for: " + projectRoot, e);
|
||||||
|
return ProjectType.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于关键文件检测项目类型
|
||||||
|
*/
|
||||||
|
private ProjectType detectByKeyFiles(Path projectRoot) throws IOException {
|
||||||
|
// Java Maven项目
|
||||||
|
if (Files.exists(projectRoot.resolve("pom.xml"))) {
|
||||||
|
// 检查是否为Spring Boot项目
|
||||||
|
if (isSpringBootProject(projectRoot)) {
|
||||||
|
return ProjectType.SPRING_BOOT;
|
||||||
|
}
|
||||||
|
return ProjectType.JAVA_MAVEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Java Gradle项目
|
||||||
|
if (Files.exists(projectRoot.resolve("build.gradle")) ||
|
||||||
|
Files.exists(projectRoot.resolve("build.gradle.kts"))) {
|
||||||
|
return ProjectType.JAVA_GRADLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node.js项目
|
||||||
|
if (Files.exists(projectRoot.resolve("package.json"))) {
|
||||||
|
return analyzeNodeJsProject(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Python项目
|
||||||
|
if (Files.exists(projectRoot.resolve("requirements.txt")) ||
|
||||||
|
Files.exists(projectRoot.resolve("setup.py")) ||
|
||||||
|
Files.exists(projectRoot.resolve("pyproject.toml"))) {
|
||||||
|
return analyzePythonProject(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// .NET项目
|
||||||
|
try (Stream<Path> files = Files.list(projectRoot)) {
|
||||||
|
if (files.anyMatch(path -> path.toString().endsWith(".csproj") ||
|
||||||
|
path.toString().endsWith(".sln"))) {
|
||||||
|
return ProjectType.DOTNET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go项目
|
||||||
|
if (Files.exists(projectRoot.resolve("go.mod"))) {
|
||||||
|
return ProjectType.GO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rust项目
|
||||||
|
if (Files.exists(projectRoot.resolve("Cargo.toml"))) {
|
||||||
|
return ProjectType.RUST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP项目
|
||||||
|
if (Files.exists(projectRoot.resolve("composer.json"))) {
|
||||||
|
return ProjectType.PHP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态HTML项目
|
||||||
|
if (Files.exists(projectRoot.resolve("index.html"))) {
|
||||||
|
return ProjectType.HTML_STATIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为Spring Boot项目
|
||||||
|
*/
|
||||||
|
private boolean isSpringBootProject(Path projectRoot) {
|
||||||
|
try {
|
||||||
|
Path pomFile = projectRoot.resolve("pom.xml");
|
||||||
|
if (!Files.exists(pomFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String pomContent = Files.readString(pomFile);
|
||||||
|
return pomContent.contains("spring-boot-starter") ||
|
||||||
|
pomContent.contains("org.springframework.boot");
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error reading pom.xml for Spring Boot detection", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析Node.js项目类型
|
||||||
|
*/
|
||||||
|
private ProjectType analyzeNodeJsProject(Path projectRoot) {
|
||||||
|
try {
|
||||||
|
Path packageJsonPath = projectRoot.resolve("package.json");
|
||||||
|
String content = Files.readString(packageJsonPath);
|
||||||
|
JsonNode packageJson = objectMapper.readTree(content);
|
||||||
|
|
||||||
|
// 检查依赖来确定具体的框架类型
|
||||||
|
JsonNode dependencies = packageJson.get("dependencies");
|
||||||
|
JsonNode devDependencies = packageJson.get("devDependencies");
|
||||||
|
|
||||||
|
if (hasDependency(dependencies, "react") || hasDependency(devDependencies, "react")) {
|
||||||
|
return ProjectType.REACT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDependency(dependencies, "vue") || hasDependency(devDependencies, "vue")) {
|
||||||
|
return ProjectType.VUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDependency(dependencies, "@angular/core") ||
|
||||||
|
hasDependency(devDependencies, "@angular/cli")) {
|
||||||
|
return ProjectType.ANGULAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDependency(dependencies, "next") || hasDependency(devDependencies, "next")) {
|
||||||
|
return ProjectType.NEXT_JS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectType.NODE_JS;
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error analyzing package.json", e);
|
||||||
|
return ProjectType.NODE_JS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析Python项目类型
|
||||||
|
*/
|
||||||
|
private ProjectType analyzePythonProject(Path projectRoot) {
|
||||||
|
// 检查Django项目
|
||||||
|
if (Files.exists(projectRoot.resolve("manage.py"))) {
|
||||||
|
return ProjectType.DJANGO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查Flask项目
|
||||||
|
if (Files.exists(projectRoot.resolve("app.py")) ||
|
||||||
|
Files.exists(projectRoot.resolve("application.py"))) {
|
||||||
|
return ProjectType.FLASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查FastAPI项目
|
||||||
|
if (Files.exists(projectRoot.resolve("main.py"))) {
|
||||||
|
try {
|
||||||
|
String content = Files.readString(projectRoot.resolve("main.py"));
|
||||||
|
if (content.contains("from fastapi import") || content.contains("import fastapi")) {
|
||||||
|
return ProjectType.FASTAPI;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error reading main.py for FastAPI detection", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectType.PYTHON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于目录结构检测项目类型
|
||||||
|
*/
|
||||||
|
private ProjectType detectByDirectoryStructure(Path projectRoot) {
|
||||||
|
try {
|
||||||
|
List<String> directories = Files.list(projectRoot)
|
||||||
|
.filter(Files::isDirectory)
|
||||||
|
.map(path -> path.getFileName().toString().toLowerCase())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Java项目特征目录
|
||||||
|
if (directories.contains("src") &&
|
||||||
|
(directories.contains("target") || directories.contains("build"))) {
|
||||||
|
return ProjectType.JAVA_MAVEN; // 默认为Maven
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node.js项目特征目录
|
||||||
|
if (directories.contains("node_modules") ||
|
||||||
|
directories.contains("public") ||
|
||||||
|
directories.contains("dist")) {
|
||||||
|
return ProjectType.NODE_JS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Python项目特征目录
|
||||||
|
if (directories.contains("venv") ||
|
||||||
|
directories.contains("env") ||
|
||||||
|
directories.contains("__pycache__")) {
|
||||||
|
return ProjectType.PYTHON;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error analyzing directory structure", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否存在特定依赖
|
||||||
|
*/
|
||||||
|
private boolean hasDependency(JsonNode dependencies, String dependencyName) {
|
||||||
|
return dependencies != null && dependencies.has(dependencyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目类型的详细信息
|
||||||
|
*/
|
||||||
|
public String getProjectTypeDetails(Path projectRoot, ProjectType projectType) {
|
||||||
|
StringBuilder details = new StringBuilder();
|
||||||
|
details.append("Project Type: ").append(projectType.getDisplayName()).append("\n");
|
||||||
|
details.append("Primary Language: ").append(projectType.getPrimaryLanguage()).append("\n");
|
||||||
|
details.append("Package Manager: ").append(projectType.getPackageManager()).append("\n");
|
||||||
|
|
||||||
|
// 添加特定项目类型的详细信息
|
||||||
|
switch (projectType) {
|
||||||
|
case SPRING_BOOT:
|
||||||
|
details.append("Framework: Spring Boot\n");
|
||||||
|
details.append("Build Tool: Maven\n");
|
||||||
|
break;
|
||||||
|
case REACT:
|
||||||
|
details.append("Framework: React\n");
|
||||||
|
details.append("Runtime: Node.js\n");
|
||||||
|
break;
|
||||||
|
case DJANGO:
|
||||||
|
details.append("Framework: Django\n");
|
||||||
|
details.append("Language: Python\n");
|
||||||
|
break;
|
||||||
|
// 可以添加更多项目类型的详细信息
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TaskSummaryService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TaskSummaryService.class);
|
||||||
|
|
||||||
|
private static final Pattern[] ACTION_PATTERNS = {
|
||||||
|
Pattern.compile("(?i)creating?\\s+(?:a\\s+)?(?:new\\s+)?(.{1,50}?)(?:\\s+file|\\s+directory|\\s+project)?", Pattern.CASE_INSENSITIVE),
|
||||||
|
Pattern.compile("(?i)writing?\\s+(?:to\\s+)?(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||||
|
Pattern.compile("(?i)reading?\\s+(?:from\\s+)?(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||||
|
Pattern.compile("(?i)editing?\\s+(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||||
|
Pattern.compile("(?i)listing?\\s+(?:the\\s+)?(.{1,50}?)(?:\\s+directory)?", Pattern.CASE_INSENSITIVE),
|
||||||
|
Pattern.compile("(?i)analyzing?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE),
|
||||||
|
Pattern.compile("(?i)generating?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE),
|
||||||
|
Pattern.compile("(?i)building?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String[] ACTION_VERBS = {
|
||||||
|
"创建", "写入", "读取", "编辑", "列出", "分析", "生成", "构建",
|
||||||
|
"creating", "writing", "reading", "editing", "listing", "analyzing", "generating", "building"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从AI响应中提取任务摘要
|
||||||
|
*/
|
||||||
|
public String extractTaskSummary(String aiResponse) {
|
||||||
|
if (aiResponse == null || aiResponse.trim().isEmpty()) {
|
||||||
|
return "处理中...";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理响应文本
|
||||||
|
String cleanResponse = aiResponse.replaceAll("```[\\s\\S]*?```", "").trim();
|
||||||
|
|
||||||
|
// 尝试匹配具体操作
|
||||||
|
for (Pattern pattern : ACTION_PATTERNS) {
|
||||||
|
Matcher matcher = pattern.matcher(cleanResponse);
|
||||||
|
if (matcher.find()) {
|
||||||
|
String action = matcher.group(0).trim();
|
||||||
|
if (action.length() > 50) {
|
||||||
|
action = action.substring(0, 47) + "...";
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找动作词汇
|
||||||
|
String lowerResponse = cleanResponse.toLowerCase();
|
||||||
|
for (String verb : ACTION_VERBS) {
|
||||||
|
if (lowerResponse.contains(verb.toLowerCase())) {
|
||||||
|
// 提取包含动作词的句子
|
||||||
|
String[] sentences = cleanResponse.split("[.!?\\n]");
|
||||||
|
for (String sentence : sentences) {
|
||||||
|
if (sentence.toLowerCase().contains(verb.toLowerCase())) {
|
||||||
|
String summary = sentence.trim();
|
||||||
|
if (summary.length() > 60) {
|
||||||
|
summary = summary.substring(0, 57) + "...";
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到具体操作,返回通用描述
|
||||||
|
if (cleanResponse.length() > 60) {
|
||||||
|
return cleanResponse.substring(0, 57) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanResponse.isEmpty() ? "处理中..." : cleanResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 估算任务复杂度和预期轮数
|
||||||
|
*/
|
||||||
|
public int estimateTaskComplexity(String initialMessage) {
|
||||||
|
if (initialMessage == null) return 1;
|
||||||
|
|
||||||
|
String lowerMessage = initialMessage.toLowerCase();
|
||||||
|
int complexity = 1;
|
||||||
|
|
||||||
|
// 基于关键词估算复杂度
|
||||||
|
if (lowerMessage.contains("project") || lowerMessage.contains("项目")) complexity += 3;
|
||||||
|
if (lowerMessage.contains("complete") || lowerMessage.contains("完整")) complexity += 2;
|
||||||
|
if (lowerMessage.contains("multiple") || lowerMessage.contains("多个")) complexity += 2;
|
||||||
|
if (lowerMessage.contains("full-stack") || lowerMessage.contains("全栈")) complexity += 4;
|
||||||
|
if (lowerMessage.contains("website") || lowerMessage.contains("网站")) complexity += 2;
|
||||||
|
if (lowerMessage.contains("api") || lowerMessage.contains("接口")) complexity += 2;
|
||||||
|
|
||||||
|
// 基于文件操作数量估算
|
||||||
|
long fileOperations = lowerMessage.chars()
|
||||||
|
.mapToObj(c -> String.valueOf((char) c))
|
||||||
|
.filter(s -> s.matches(".*(?:create|write|edit|file|directory).*"))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
complexity += (int) Math.min(fileOperations / 2, 5);
|
||||||
|
|
||||||
|
return Math.min(complexity, 15); // 最大15轮
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成当前状态的用户友好描述
|
||||||
|
*/
|
||||||
|
public String generateStatusDescription(String status, String currentAction, int currentTurn, int totalTurns) {
|
||||||
|
StringBuilder desc = new StringBuilder();
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "RUNNING":
|
||||||
|
if (currentAction != null && !currentAction.trim().isEmpty()) {
|
||||||
|
desc.append("🔄 ").append(currentAction);
|
||||||
|
} else {
|
||||||
|
desc.append("🤔 AI正在思考...");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalTurns > 1) {
|
||||||
|
desc.append(String.format(" (第%d/%d轮)", currentTurn, totalTurns));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "COMPLETED":
|
||||||
|
desc.append("✅ 任务完成");
|
||||||
|
if (totalTurns > 1) {
|
||||||
|
desc.append(String.format(" (共%d轮)", currentTurn));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ERROR":
|
||||||
|
desc.append("❌ 执行出错");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
desc.append("⏳ 处理中...");
|
||||||
|
}
|
||||||
|
|
||||||
|
return desc.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具执行日志记录服务
|
||||||
|
* 记录所有工具调用的详细信息,使用中文日志
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ToolExecutionLogger {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ToolExecutionLogger.class);
|
||||||
|
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
// 工具调用计数器
|
||||||
|
private final AtomicLong callCounter = new AtomicLong(0);
|
||||||
|
|
||||||
|
// 工具执行统计
|
||||||
|
private final Map<String, ToolStats> toolStats = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录工具调用开始
|
||||||
|
*/
|
||||||
|
public long logToolStart(String toolName, String description, Object parameters) {
|
||||||
|
long callId = callCounter.incrementAndGet();
|
||||||
|
String timestamp = LocalDateTime.now().format(formatter);
|
||||||
|
|
||||||
|
logger.info("🚀 [工具调用-{}] 开始执行工具: {}", callId, toolName);
|
||||||
|
logger.info("📝 [工具调用-{}] 工具描述: {}", callId, description);
|
||||||
|
logger.info("⚙️ [工具调用-{}] 调用参数: {}", callId, formatParameters(parameters));
|
||||||
|
logger.info("🕐 [工具调用-{}] 开始时间: {}", callId, timestamp);
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
toolStats.computeIfAbsent(toolName, k -> new ToolStats()).incrementCalls();
|
||||||
|
|
||||||
|
return callId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录工具调用成功
|
||||||
|
*/
|
||||||
|
public void logToolSuccess(long callId, String toolName, String result, long executionTimeMs) {
|
||||||
|
String timestamp = LocalDateTime.now().format(formatter);
|
||||||
|
|
||||||
|
logger.info("✅ [工具调用-{}] 工具执行成功: {}", callId, toolName);
|
||||||
|
logger.info("📊 [工具调用-{}] 执行结果: {}", callId, truncateResult(result));
|
||||||
|
logger.info("⏱️ [工具调用-{}] 执行耗时: {}ms", callId, executionTimeMs);
|
||||||
|
logger.info("🕐 [工具调用-{}] 完成时间: {}", callId, timestamp);
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
ToolStats stats = toolStats.get(toolName);
|
||||||
|
if (stats != null) {
|
||||||
|
stats.incrementSuccess();
|
||||||
|
stats.addExecutionTime(executionTimeMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录工具调用失败
|
||||||
|
*/
|
||||||
|
public void logToolError(long callId, String toolName, String error, long executionTimeMs) {
|
||||||
|
String timestamp = LocalDateTime.now().format(formatter);
|
||||||
|
|
||||||
|
logger.error("❌ [工具调用-{}] 工具执行失败: {}", callId, toolName);
|
||||||
|
logger.error("🚨 [工具调用-{}] 错误信息: {}", callId, error);
|
||||||
|
logger.error("⏱️ [工具调用-{}] 执行耗时: {}ms", callId, executionTimeMs);
|
||||||
|
logger.error("🕐 [工具调用-{}] 失败时间: {}", callId, timestamp);
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
ToolStats stats = toolStats.get(toolName);
|
||||||
|
if (stats != null) {
|
||||||
|
stats.incrementError();
|
||||||
|
stats.addExecutionTime(executionTimeMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录工具调用的详细步骤
|
||||||
|
*/
|
||||||
|
public void logToolStep(long callId, String toolName, String step, String details) {
|
||||||
|
logger.debug("🔄 [工具调用-{}] [{}] 执行步骤: {} - {}", callId, toolName, step, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录文件操作
|
||||||
|
*/
|
||||||
|
public void logFileOperation(long callId, String operation, String filePath, String details) {
|
||||||
|
logger.info("📁 [工具调用-{}] 文件操作: {} - 文件: {} - 详情: {}", callId, operation, filePath, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录项目分析
|
||||||
|
*/
|
||||||
|
public void logProjectAnalysis(long callId, String projectPath, String projectType, String details) {
|
||||||
|
logger.info("🔍 [工具调用-{}] 项目分析: 路径={}, 类型={}, 详情={}", callId, projectPath, projectType, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录项目创建
|
||||||
|
*/
|
||||||
|
public void logProjectCreation(long callId, String projectName, String projectType, String projectPath) {
|
||||||
|
logger.info("🏗️ [工具调用-{}] 项目创建: 名称={}, 类型={}, 路径={}", callId, projectName, projectType, projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具执行统计
|
||||||
|
*/
|
||||||
|
public void logToolStatistics() {
|
||||||
|
logger.info("📈 ========== 工具执行统计 ==========");
|
||||||
|
toolStats.forEach((toolName, stats) -> {
|
||||||
|
logger.info("🔧 工具: {} | 调用次数: {} | 成功: {} | 失败: {} | 平均耗时: {}ms",
|
||||||
|
toolName, stats.getTotalCalls(), stats.getSuccessCount(),
|
||||||
|
stats.getErrorCount(), stats.getAverageExecutionTime());
|
||||||
|
});
|
||||||
|
logger.info("📈 ================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化参数显示
|
||||||
|
*/
|
||||||
|
private String formatParameters(Object parameters) {
|
||||||
|
if (parameters == null) {
|
||||||
|
return "无参数";
|
||||||
|
}
|
||||||
|
String paramStr = parameters.toString();
|
||||||
|
return paramStr.length() > 200 ? paramStr.substring(0, 200) + "..." : paramStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截断结果显示
|
||||||
|
*/
|
||||||
|
private String truncateResult(String result) {
|
||||||
|
if (result == null) {
|
||||||
|
return "无结果";
|
||||||
|
}
|
||||||
|
return result.length() > 300 ? result.substring(0, 300) + "..." : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具统计信息内部类
|
||||||
|
*/
|
||||||
|
private static class ToolStats {
|
||||||
|
private long totalCalls = 0;
|
||||||
|
private long successCount = 0;
|
||||||
|
private long errorCount = 0;
|
||||||
|
private long totalExecutionTime = 0;
|
||||||
|
|
||||||
|
public void incrementCalls() { totalCalls++; }
|
||||||
|
public void incrementSuccess() { successCount++; }
|
||||||
|
public void incrementError() { errorCount++; }
|
||||||
|
public void addExecutionTime(long time) { totalExecutionTime += time; }
|
||||||
|
|
||||||
|
public long getTotalCalls() { return totalCalls; }
|
||||||
|
public long getSuccessCount() { return successCount; }
|
||||||
|
public long getErrorCount() { return errorCount; }
|
||||||
|
public long getAverageExecutionTime() {
|
||||||
|
return totalCalls > 0 ? totalExecutionTime / totalCalls : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具日志事件类
|
||||||
|
* 继承自LogEvent,添加工具相关的字段
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ToolLogEvent extends LogEvent {
|
||||||
|
|
||||||
|
private String toolName;
|
||||||
|
private String filePath;
|
||||||
|
private String icon;
|
||||||
|
private String status; // RUNNING, SUCCESS, ERROR
|
||||||
|
private Long executionTime; // 执行时间(毫秒)
|
||||||
|
private String summary; // 操作摘要
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
public ToolLogEvent() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ToolLogEvent(String type, String taskId, String toolName, String filePath,
|
||||||
|
String message, String timestamp, String icon, String status) {
|
||||||
|
super(type, taskId, message, timestamp);
|
||||||
|
this.toolName = toolName;
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.icon = icon;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getToolName() {
|
||||||
|
return toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToolName(String toolName) {
|
||||||
|
this.toolName = toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFilePath() {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilePath(String filePath) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getExecutionTime() {
|
||||||
|
return executionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExecutionTime(Long executionTime) {
|
||||||
|
this.executionTime = executionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSummary() {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSummary(String summary) {
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ToolLogEvent{" +
|
||||||
|
"toolName='" + toolName + '\'' +
|
||||||
|
", filePath='" + filePath + '\'' +
|
||||||
|
", icon='" + icon + '\'' +
|
||||||
|
", status='" + status + '\'' +
|
||||||
|
", executionTime=" + executionTime +
|
||||||
|
", summary='" + summary + '\'' +
|
||||||
|
"} " + super.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
package com.example.demo.tools;
|
||||||
|
|
||||||
|
import com.example.demo.config.AppProperties;
|
||||||
|
import com.example.demo.model.ProjectContext;
|
||||||
|
import com.example.demo.model.ProjectStructure;
|
||||||
|
import com.example.demo.schema.JsonSchema;
|
||||||
|
import com.example.demo.service.ProjectContextAnalyzer;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目分析工具
|
||||||
|
* 分析现有项目的结构、类型、依赖等信息
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AnalyzeProjectTool extends BaseTool<AnalyzeProjectTool.AnalyzeProjectParams> {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AnalyzeProjectTool.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProjectContextAnalyzer projectContextAnalyzer;
|
||||||
|
|
||||||
|
private final String rootDirectory;
|
||||||
|
private final AppProperties appProperties;
|
||||||
|
|
||||||
|
public AnalyzeProjectTool(AppProperties appProperties) {
|
||||||
|
super(
|
||||||
|
"analyze_project",
|
||||||
|
"AnalyzeProject",
|
||||||
|
"Analyze an existing project to understand its structure, type, dependencies, and configuration. " +
|
||||||
|
"Provides comprehensive project information that can be used for intelligent editing and refactoring.",
|
||||||
|
createSchema()
|
||||||
|
);
|
||||||
|
this.appProperties = appProperties;
|
||||||
|
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonSchema createSchema() {
|
||||||
|
return JsonSchema.object()
|
||||||
|
.addProperty("project_path", JsonSchema.string(
|
||||||
|
"Absolute path to the project root directory to analyze. " +
|
||||||
|
"Must be within the workspace directory."
|
||||||
|
))
|
||||||
|
.addProperty("analysis_depth", JsonSchema.string(
|
||||||
|
"Analysis depth: 'basic', 'detailed', or 'comprehensive'. " +
|
||||||
|
"Default: 'detailed'. " +
|
||||||
|
"- basic: Project type and structure only\n" +
|
||||||
|
"- detailed: Includes dependencies and configuration\n" +
|
||||||
|
"- comprehensive: Full analysis including code statistics"
|
||||||
|
))
|
||||||
|
.addProperty("include_code_stats", JsonSchema.bool(
|
||||||
|
"Whether to include detailed code statistics (lines of code, classes, methods, etc.). " +
|
||||||
|
"Default: true for detailed/comprehensive analysis"
|
||||||
|
))
|
||||||
|
.addProperty("output_format", JsonSchema.string(
|
||||||
|
"Output format: 'summary', 'detailed', or 'json'. Default: 'detailed'"
|
||||||
|
))
|
||||||
|
.required("project_path");
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AnalysisDepth {
|
||||||
|
BASIC("basic", "Basic project type and structure analysis"),
|
||||||
|
DETAILED("detailed", "Detailed analysis including dependencies and configuration"),
|
||||||
|
COMPREHENSIVE("comprehensive", "Comprehensive analysis with full code statistics");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
AnalysisDepth(String value, String description) {
|
||||||
|
this.value = value;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AnalysisDepth fromString(String value) {
|
||||||
|
for (AnalysisDepth depth : values()) {
|
||||||
|
if (depth.value.equals(value)) {
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DETAILED; // default
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() { return value; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OutputFormat {
|
||||||
|
SUMMARY("summary", "Brief summary of key project information"),
|
||||||
|
DETAILED("detailed", "Detailed human-readable analysis report"),
|
||||||
|
JSON("json", "Structured JSON output for programmatic use");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
OutputFormat(String value, String description) {
|
||||||
|
this.value = value;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OutputFormat fromString(String value) {
|
||||||
|
for (OutputFormat format : values()) {
|
||||||
|
if (format.value.equals(value)) {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DETAILED; // default
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() { return value; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String validateToolParams(AnalyzeProjectParams params) {
|
||||||
|
String baseValidation = super.validateToolParams(params);
|
||||||
|
if (baseValidation != null) {
|
||||||
|
return baseValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.projectPath == null || params.projectPath.trim().isEmpty()) {
|
||||||
|
return "Project path cannot be empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
Path projectPath = Paths.get(params.projectPath);
|
||||||
|
if (!projectPath.isAbsolute()) {
|
||||||
|
return "Project path must be absolute: " + params.projectPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.exists(projectPath)) {
|
||||||
|
return "Project path does not exist: " + params.projectPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isDirectory(projectPath)) {
|
||||||
|
return "Project path must be a directory: " + params.projectPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isWithinWorkspace(projectPath)) {
|
||||||
|
return "Project path must be within the workspace directory: " + params.projectPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze project tool method for Spring AI integration
|
||||||
|
*/
|
||||||
|
@Tool(name = "analyze_project", description = "Analyzes project structure, type, dependencies and other information")
|
||||||
|
public String analyzeProject(String projectPath, String analysisDepth, String outputFormat, Boolean includeCodeStats) {
|
||||||
|
try {
|
||||||
|
AnalyzeProjectParams params = new AnalyzeProjectParams();
|
||||||
|
params.setProjectPath(projectPath);
|
||||||
|
params.setAnalysisDepth(analysisDepth != null ? analysisDepth : "basic");
|
||||||
|
params.setOutputFormat(outputFormat != null ? outputFormat : "detailed");
|
||||||
|
params.setIncludeCodeStats(includeCodeStats != null ? includeCodeStats : false);
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
String validation = validateToolParams(params);
|
||||||
|
if (validation != null) {
|
||||||
|
return "Error: " + validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the tool
|
||||||
|
ToolResult result = execute(params).join();
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
return result.getLlmContent();
|
||||||
|
} else {
|
||||||
|
return "Error: " + result.getErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error in analyze project tool", e);
|
||||||
|
return "Error: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<ToolResult> execute(AnalyzeProjectParams params) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
logger.info("Starting project analysis for: {}", params.projectPath);
|
||||||
|
|
||||||
|
Path projectPath = Paths.get(params.projectPath);
|
||||||
|
AnalysisDepth depth = AnalysisDepth.fromString(params.analysisDepth);
|
||||||
|
OutputFormat format = OutputFormat.fromString(params.outputFormat);
|
||||||
|
|
||||||
|
// 执行项目分析
|
||||||
|
ProjectContext context = analyzeProject(projectPath, depth, params);
|
||||||
|
|
||||||
|
// 生成输出
|
||||||
|
String output = generateOutput(context, format, depth);
|
||||||
|
String summary = generateSummary(context);
|
||||||
|
|
||||||
|
logger.info("Project analysis completed for: {}", params.projectPath);
|
||||||
|
return ToolResult.success(summary, output);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error during project analysis", e);
|
||||||
|
return ToolResult.error("Project analysis failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行项目分析
|
||||||
|
*/
|
||||||
|
private ProjectContext analyzeProject(Path projectPath, AnalysisDepth depth, AnalyzeProjectParams params) {
|
||||||
|
logger.debug("Analyzing project with depth: {}", depth);
|
||||||
|
|
||||||
|
switch (depth) {
|
||||||
|
case BASIC:
|
||||||
|
return analyzeBasic(projectPath);
|
||||||
|
case DETAILED:
|
||||||
|
return analyzeDetailed(projectPath, params);
|
||||||
|
case COMPREHENSIVE:
|
||||||
|
return analyzeComprehensive(projectPath, params);
|
||||||
|
default:
|
||||||
|
return projectContextAnalyzer.analyzeProject(projectPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础分析
|
||||||
|
*/
|
||||||
|
private ProjectContext analyzeBasic(Path projectPath) {
|
||||||
|
// 只分析项目类型和基本结构
|
||||||
|
ProjectContext context = new ProjectContext(projectPath);
|
||||||
|
context.setProjectType(projectContextAnalyzer.projectTypeDetector.detectProjectType(projectPath));
|
||||||
|
context.setProjectStructure(projectContextAnalyzer.projectDiscoveryService.analyzeProjectStructure(projectPath));
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详细分析
|
||||||
|
*/
|
||||||
|
private ProjectContext analyzeDetailed(Path projectPath, AnalyzeProjectParams params) {
|
||||||
|
ProjectContext context = analyzeBasic(projectPath);
|
||||||
|
|
||||||
|
// 添加依赖和配置文件分析
|
||||||
|
context.setDependencies(projectContextAnalyzer.projectDiscoveryService.analyzeDependencies(projectPath));
|
||||||
|
context.setConfigFiles(projectContextAnalyzer.projectDiscoveryService.findConfigurationFiles(projectPath));
|
||||||
|
|
||||||
|
// 如果需要代码统计
|
||||||
|
if (params.includeCodeStats == null || params.includeCodeStats) {
|
||||||
|
// 简化的代码统计,避免性能问题
|
||||||
|
ProjectContext.CodeStatistics stats = new ProjectContext.CodeStatistics();
|
||||||
|
// 这里可以添加基本的代码统计逻辑
|
||||||
|
context.setCodeStatistics(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全面分析
|
||||||
|
*/
|
||||||
|
private ProjectContext analyzeComprehensive(Path projectPath, AnalyzeProjectParams params) {
|
||||||
|
// 使用完整的项目分析
|
||||||
|
return projectContextAnalyzer.analyzeProject(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成输出
|
||||||
|
*/
|
||||||
|
private String generateOutput(ProjectContext context, OutputFormat format, AnalysisDepth depth) {
|
||||||
|
switch (format) {
|
||||||
|
case SUMMARY:
|
||||||
|
return generateSummaryOutput(context);
|
||||||
|
case DETAILED:
|
||||||
|
return generateDetailedOutput(context, depth);
|
||||||
|
case JSON:
|
||||||
|
return generateJsonOutput(context);
|
||||||
|
default:
|
||||||
|
return generateDetailedOutput(context, depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成摘要输出
|
||||||
|
*/
|
||||||
|
private String generateSummaryOutput(ProjectContext context) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
|
||||||
|
output.append("📊 PROJECT ANALYSIS SUMMARY\n");
|
||||||
|
output.append("=" .repeat(50)).append("\n\n");
|
||||||
|
|
||||||
|
// 基本信息
|
||||||
|
output.append("🏗️ Project: ").append(context.getProjectRoot().getFileName()).append("\n");
|
||||||
|
output.append("🔧 Type: ").append(context.getProjectType().getDisplayName()).append("\n");
|
||||||
|
output.append("💻 Language: ").append(context.getProjectType().getPrimaryLanguage()).append("\n");
|
||||||
|
output.append("📦 Package Manager: ").append(context.getProjectType().getPackageManager()).append("\n\n");
|
||||||
|
|
||||||
|
// 结构信息
|
||||||
|
if (context.getProjectStructure() != null) {
|
||||||
|
ProjectStructure structure = context.getProjectStructure();
|
||||||
|
output.append("📁 Structure:\n");
|
||||||
|
output.append(" - Directories: ").append(structure.getTotalDirectories()).append("\n");
|
||||||
|
output.append(" - Files: ").append(structure.getTotalFiles()).append("\n");
|
||||||
|
output.append(" - Size: ").append(formatFileSize(structure.getTotalSize())).append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 依赖信息
|
||||||
|
if (context.getDependencies() != null && !context.getDependencies().isEmpty()) {
|
||||||
|
output.append("📚 Dependencies: ").append(context.getDependencies().size()).append(" found\n");
|
||||||
|
output.append(" - Key dependencies: ").append(context.getDependencySummary()).append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置文件
|
||||||
|
if (context.getConfigFiles() != null && !context.getConfigFiles().isEmpty()) {
|
||||||
|
output.append("⚙️ Configuration Files: ").append(context.getConfigFiles().size()).append(" found\n");
|
||||||
|
context.getConfigFiles().stream()
|
||||||
|
.filter(ProjectContext.ConfigFile::isMainConfig)
|
||||||
|
.forEach(config -> output.append(" - ").append(config.getFileName()).append("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成详细输出
|
||||||
|
*/
|
||||||
|
private String generateDetailedOutput(ProjectContext context, AnalysisDepth depth) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
|
||||||
|
output.append("📊 COMPREHENSIVE PROJECT ANALYSIS\n");
|
||||||
|
output.append("=" .repeat(60)).append("\n\n");
|
||||||
|
|
||||||
|
// 使用项目上下文的摘要生成功能
|
||||||
|
output.append(context.generateContextSummary());
|
||||||
|
|
||||||
|
// 添加分析深度特定的信息
|
||||||
|
if (depth == AnalysisDepth.COMPREHENSIVE) {
|
||||||
|
output.append("\n=== DETAILED INSIGHTS ===\n");
|
||||||
|
output.append(generateProjectInsights(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成JSON输出
|
||||||
|
*/
|
||||||
|
private String generateJsonOutput(ProjectContext context) {
|
||||||
|
// 简化的JSON输出实现
|
||||||
|
// 在实际项目中应该使用Jackson等JSON库
|
||||||
|
StringBuilder json = new StringBuilder();
|
||||||
|
json.append("{\n");
|
||||||
|
json.append(" \"projectName\": \"").append(context.getProjectRoot().getFileName()).append("\",\n");
|
||||||
|
json.append(" \"projectType\": \"").append(context.getProjectType().name()).append("\",\n");
|
||||||
|
json.append(" \"primaryLanguage\": \"").append(context.getProjectType().getPrimaryLanguage()).append("\",\n");
|
||||||
|
|
||||||
|
if (context.getProjectStructure() != null) {
|
||||||
|
ProjectStructure structure = context.getProjectStructure();
|
||||||
|
json.append(" \"structure\": {\n");
|
||||||
|
json.append(" \"directories\": ").append(structure.getTotalDirectories()).append(",\n");
|
||||||
|
json.append(" \"files\": ").append(structure.getTotalFiles()).append(",\n");
|
||||||
|
json.append(" \"totalSize\": ").append(structure.getTotalSize()).append("\n");
|
||||||
|
json.append(" },\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
json.append(" \"dependencyCount\": ").append(
|
||||||
|
context.getDependencies() != null ? context.getDependencies().size() : 0).append(",\n");
|
||||||
|
json.append(" \"configFileCount\": ").append(
|
||||||
|
context.getConfigFiles() != null ? context.getConfigFiles().size() : 0).append("\n");
|
||||||
|
|
||||||
|
json.append("}");
|
||||||
|
return json.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成项目洞察
|
||||||
|
*/
|
||||||
|
private String generateProjectInsights(ProjectContext context) {
|
||||||
|
StringBuilder insights = new StringBuilder();
|
||||||
|
|
||||||
|
// 项目健康度评估
|
||||||
|
insights.append("Project Health Assessment:\n");
|
||||||
|
|
||||||
|
// 检查是否有版本控制
|
||||||
|
if (context.getMetadata().containsKey("versionControl")) {
|
||||||
|
insights.append("✅ Version control detected: ").append(context.getMetadata().get("versionControl")).append("\n");
|
||||||
|
} else {
|
||||||
|
insights.append("⚠️ No version control detected\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有CI/CD
|
||||||
|
if (context.getMetadata().containsKey("cicd")) {
|
||||||
|
insights.append("✅ CI/CD configured: ").append(context.getMetadata().get("cicd")).append("\n");
|
||||||
|
} else {
|
||||||
|
insights.append("💡 Consider setting up CI/CD\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有容器化
|
||||||
|
if (context.getMetadata().containsKey("containerization")) {
|
||||||
|
insights.append("✅ Containerization: ").append(context.getMetadata().get("containerization")).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代码质量建议
|
||||||
|
insights.append("\nRecommendations:\n");
|
||||||
|
if (context.getProjectType().isJavaProject()) {
|
||||||
|
insights.append("- Consider using static analysis tools like SpotBugs or PMD\n");
|
||||||
|
insights.append("- Ensure proper test coverage with JUnit\n");
|
||||||
|
} else if (context.getProjectType().isJavaScriptProject()) {
|
||||||
|
insights.append("- Consider using ESLint for code quality\n");
|
||||||
|
insights.append("- Add TypeScript for better type safety\n");
|
||||||
|
} else if (context.getProjectType().isPythonProject()) {
|
||||||
|
insights.append("- Consider using pylint or flake8 for code quality\n");
|
||||||
|
insights.append("- Add type hints for better code documentation\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return insights.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成摘要
|
||||||
|
*/
|
||||||
|
private String generateSummary(ProjectContext context) {
|
||||||
|
return String.format("Analyzed %s project: %s (%s) with %d dependencies and %d config files",
|
||||||
|
context.getProjectType().getDisplayName(),
|
||||||
|
context.getProjectRoot().getFileName(),
|
||||||
|
context.getProjectType().getPrimaryLanguage(),
|
||||||
|
context.getDependencies() != null ? context.getDependencies().size() : 0,
|
||||||
|
context.getConfigFiles() != null ? context.getConfigFiles().size() : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小
|
||||||
|
*/
|
||||||
|
private String formatFileSize(long bytes) {
|
||||||
|
if (bytes < 1024) return bytes + " B";
|
||||||
|
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
|
||||||
|
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
|
||||||
|
return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径是否在工作空间内
|
||||||
|
*/
|
||||||
|
private boolean isWithinWorkspace(Path filePath) {
|
||||||
|
try {
|
||||||
|
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||||
|
Path normalizedPath = filePath.toRealPath();
|
||||||
|
return normalizedPath.startsWith(workspaceRoot);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Could not resolve workspace path", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析项目参数
|
||||||
|
*/
|
||||||
|
public static class AnalyzeProjectParams {
|
||||||
|
@JsonProperty("project_path")
|
||||||
|
private String projectPath;
|
||||||
|
|
||||||
|
@JsonProperty("analysis_depth")
|
||||||
|
private String analysisDepth = "detailed";
|
||||||
|
|
||||||
|
@JsonProperty("include_code_stats")
|
||||||
|
private Boolean includeCodeStats;
|
||||||
|
|
||||||
|
@JsonProperty("output_format")
|
||||||
|
private String outputFormat = "detailed";
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getProjectPath() { return projectPath; }
|
||||||
|
public void setProjectPath(String projectPath) { this.projectPath = projectPath; }
|
||||||
|
|
||||||
|
public String getAnalysisDepth() { return analysisDepth; }
|
||||||
|
public void setAnalysisDepth(String analysisDepth) { this.analysisDepth = analysisDepth; }
|
||||||
|
|
||||||
|
public Boolean getIncludeCodeStats() { return includeCodeStats; }
|
||||||
|
public void setIncludeCodeStats(Boolean includeCodeStats) { this.includeCodeStats = includeCodeStats; }
|
||||||
|
|
||||||
|
public String getOutputFormat() { return outputFormat; }
|
||||||
|
public void setOutputFormat(String outputFormat) { this.outputFormat = outputFormat; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("AnalyzeProjectParams{path='%s', depth='%s', format='%s'}",
|
||||||
|
projectPath, analysisDepth, outputFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.example.demo.tools;
|
||||||
|
|
||||||
|
import com.example.demo.schema.JsonSchema;
|
||||||
|
import com.example.demo.schema.SchemaValidator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.ai.chat.model.ToolContext;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base abstract class for tools
|
||||||
|
* All tools should inherit from this class
|
||||||
|
*/
|
||||||
|
public abstract class BaseTool<P> {
|
||||||
|
|
||||||
|
protected final Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
|
protected final String name;
|
||||||
|
protected final String displayName;
|
||||||
|
protected final String description;
|
||||||
|
protected final JsonSchema parameterSchema;
|
||||||
|
protected final boolean isOutputMarkdown;
|
||||||
|
protected final boolean canUpdateOutput;
|
||||||
|
|
||||||
|
protected SchemaValidator schemaValidator;
|
||||||
|
|
||||||
|
public BaseTool(String name, String displayName, String description, JsonSchema parameterSchema) {
|
||||||
|
this(name, displayName, description, parameterSchema, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseTool(String name, String displayName, String description, JsonSchema parameterSchema,
|
||||||
|
boolean isOutputMarkdown, boolean canUpdateOutput) {
|
||||||
|
this.name = name;
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.description = description;
|
||||||
|
this.parameterSchema = parameterSchema;
|
||||||
|
this.isOutputMarkdown = isOutputMarkdown;
|
||||||
|
this.canUpdateOutput = canUpdateOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Schema validator (through dependency injection)
|
||||||
|
*/
|
||||||
|
public void setSchemaValidator(SchemaValidator schemaValidator) {
|
||||||
|
this.schemaValidator = schemaValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate tool parameters
|
||||||
|
*
|
||||||
|
* @param params Parameter object
|
||||||
|
* @return Validation error message, null means validation passed
|
||||||
|
*/
|
||||||
|
public String validateToolParams(P params) {
|
||||||
|
if (schemaValidator == null || parameterSchema == null) {
|
||||||
|
logger.warn("Schema validator or parameter schema is null, skipping validation");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return schemaValidator.validate(parameterSchema, params);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Parameter validation failed", e);
|
||||||
|
return "Parameter validation error: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm whether user approval is needed for execution
|
||||||
|
*
|
||||||
|
* @param params Parameter object
|
||||||
|
* @return Confirmation details, null means no confirmation needed
|
||||||
|
*/
|
||||||
|
public CompletableFuture<ToolConfirmationDetails> shouldConfirmExecute(P params) {
|
||||||
|
return CompletableFuture.completedFuture(null); // Default no confirmation needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute tool
|
||||||
|
*
|
||||||
|
* @param params Parameter object
|
||||||
|
* @return Execution result
|
||||||
|
*/
|
||||||
|
public abstract CompletableFuture<ToolResult> execute(P params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tool description (for AI understanding)
|
||||||
|
*
|
||||||
|
* @param params Parameter object
|
||||||
|
* @return Description information
|
||||||
|
*/
|
||||||
|
public String getDescription(P params) {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public String getName() { return name; }
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public JsonSchema getParameterSchema() { return parameterSchema; }
|
||||||
|
public boolean isOutputMarkdown() { return isOutputMarkdown; }
|
||||||
|
public boolean canUpdateOutput() { return canUpdateOutput; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("Tool{name='%s', displayName='%s'}", name, displayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
package com.example.demo.tools;
|
||||||
|
|
||||||
|
import com.example.demo.config.AppProperties;
|
||||||
|
import com.example.demo.schema.JsonSchema;
|
||||||
|
import com.example.demo.service.ToolExecutionLogger;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.github.difflib.DiffUtils;
|
||||||
|
import com.github.difflib.UnifiedDiffUtils;
|
||||||
|
import com.github.difflib.patch.Patch;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File editing tool
|
||||||
|
* Supports file editing based on string replacement, automatically shows differences
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
|
||||||
|
|
||||||
|
private final String rootDirectory;
|
||||||
|
private final AppProperties appProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ToolExecutionLogger executionLogger;
|
||||||
|
|
||||||
|
public EditFileTool(AppProperties appProperties) {
|
||||||
|
super(
|
||||||
|
"edit_file",
|
||||||
|
"EditFile",
|
||||||
|
"Edits a file by replacing specified text with new text. " +
|
||||||
|
"Shows a diff of the changes before applying them. " +
|
||||||
|
"Supports both exact string matching and line-based editing. " +
|
||||||
|
"Use absolute paths within the workspace directory.",
|
||||||
|
createSchema()
|
||||||
|
);
|
||||||
|
this.appProperties = appProperties;
|
||||||
|
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getWorkspaceBasePath() {
|
||||||
|
return Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getPathExample(String subPath) {
|
||||||
|
return "Example: \"" + Paths.get(getWorkspaceBasePath(), subPath).toString() + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonSchema createSchema() {
|
||||||
|
return JsonSchema.object()
|
||||||
|
.addProperty("file_path", JsonSchema.string(
|
||||||
|
"MUST be an absolute path to the file to edit. Path must be within the workspace directory (" +
|
||||||
|
getWorkspaceBasePath() + "). " +
|
||||||
|
getPathExample("project/src/main.java") + ". " +
|
||||||
|
"Relative paths are NOT allowed."
|
||||||
|
))
|
||||||
|
.addProperty("old_str", JsonSchema.string(
|
||||||
|
"The exact string to find and replace. Must match exactly including whitespace and newlines."
|
||||||
|
))
|
||||||
|
.addProperty("new_str", JsonSchema.string(
|
||||||
|
"The new string to replace the old string with. Can be empty to delete the old string."
|
||||||
|
))
|
||||||
|
.addProperty("start_line", JsonSchema.integer(
|
||||||
|
"Optional: 1-based line number where the old_str starts. Helps with disambiguation."
|
||||||
|
).minimum(1))
|
||||||
|
.addProperty("end_line", JsonSchema.integer(
|
||||||
|
"Optional: 1-based line number where the old_str ends. Must be >= start_line."
|
||||||
|
).minimum(1))
|
||||||
|
.required("file_path", "old_str", "new_str");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String validateToolParams(EditFileParams params) {
|
||||||
|
String baseValidation = super.validateToolParams(params);
|
||||||
|
if (baseValidation != null) {
|
||||||
|
return baseValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证路径
|
||||||
|
if (params.filePath == null || params.filePath.trim().isEmpty()) {
|
||||||
|
return "File path cannot be empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.oldStr == null) {
|
||||||
|
return "Old string cannot be null";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.newStr == null) {
|
||||||
|
return "New string cannot be null";
|
||||||
|
}
|
||||||
|
|
||||||
|
Path filePath = Paths.get(params.filePath);
|
||||||
|
|
||||||
|
// Validate if it's an absolute path
|
||||||
|
if (!filePath.isAbsolute()) {
|
||||||
|
return "File path must be absolute: " + params.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证是否在工作目录内
|
||||||
|
if (!isWithinWorkspace(filePath)) {
|
||||||
|
return "File path must be within the workspace directory (" + rootDirectory + "): " + params.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证行号
|
||||||
|
if (params.startLine != null && params.endLine != null) {
|
||||||
|
if (params.endLine < params.startLine) {
|
||||||
|
return "End line must be >= start line";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<ToolConfirmationDetails> shouldConfirmExecute(EditFileParams params) {
|
||||||
|
// Decide whether confirmation is needed based on configuration
|
||||||
|
if (appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.AUTO_EDIT ||
|
||||||
|
appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.YOLO) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
Path filePath = Paths.get(params.filePath);
|
||||||
|
|
||||||
|
if (!Files.exists(filePath)) {
|
||||||
|
return null; // 文件不存在,无法预览差异
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentContent = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||||
|
String newContent = performEdit(currentContent, params);
|
||||||
|
|
||||||
|
if (newContent == null) {
|
||||||
|
return null; // Edit failed, cannot preview differences
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成差异显示
|
||||||
|
String diff = generateDiff(filePath.getFileName().toString(), currentContent, newContent);
|
||||||
|
String title = "Confirm Edit: " + getRelativePath(filePath);
|
||||||
|
|
||||||
|
return ToolConfirmationDetails.edit(title, filePath.getFileName().toString(), diff);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Could not read file for edit preview: " + params.filePath, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit file tool method for Spring AI integration
|
||||||
|
*/
|
||||||
|
@Tool(name = "edit_file", description = "Edits a file by replacing specified text with new text")
|
||||||
|
public String editFile(String filePath, String oldStr, String newStr, Integer startLine, Integer endLine) {
|
||||||
|
long callId = executionLogger.logToolStart("edit_file", "编辑文件内容",
|
||||||
|
String.format("文件=%s, 替换文本长度=%d->%d, 行号范围=%s-%s",
|
||||||
|
filePath, oldStr != null ? oldStr.length() : 0,
|
||||||
|
newStr != null ? newStr.length() : 0, startLine, endLine));
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
EditFileParams params = new EditFileParams();
|
||||||
|
params.setFilePath(filePath);
|
||||||
|
params.setOldStr(oldStr);
|
||||||
|
params.setNewStr(newStr);
|
||||||
|
params.setStartLine(startLine);
|
||||||
|
params.setEndLine(endLine);
|
||||||
|
|
||||||
|
executionLogger.logToolStep(callId, "edit_file", "参数验证", "验证文件路径和替换内容");
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
String validation = validateToolParams(params);
|
||||||
|
if (validation != null) {
|
||||||
|
long executionTime = System.currentTimeMillis() - startTime;
|
||||||
|
executionLogger.logToolError(callId, "edit_file", "参数验证失败: " + validation, executionTime);
|
||||||
|
return "Error: " + validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
String editDetails = startLine != null && endLine != null ?
|
||||||
|
String.format("行号范围编辑: %d-%d行", startLine, endLine) : "字符串替换编辑";
|
||||||
|
executionLogger.logFileOperation(callId, "编辑文件", filePath, editDetails);
|
||||||
|
|
||||||
|
// Execute the tool
|
||||||
|
ToolResult result = execute(params).join();
|
||||||
|
|
||||||
|
long executionTime = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
executionLogger.logToolSuccess(callId, "edit_file", "文件编辑成功", executionTime);
|
||||||
|
return result.getLlmContent();
|
||||||
|
} else {
|
||||||
|
executionLogger.logToolError(callId, "edit_file", result.getErrorMessage(), executionTime);
|
||||||
|
return "Error: " + result.getErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
long executionTime = System.currentTimeMillis() - startTime;
|
||||||
|
executionLogger.logToolError(callId, "edit_file", "工具执行异常: " + e.getMessage(), executionTime);
|
||||||
|
logger.error("Error in edit file tool", e);
|
||||||
|
return "Error: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<ToolResult> execute(EditFileParams params) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
Path filePath = Paths.get(params.filePath);
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!Files.exists(filePath)) {
|
||||||
|
return ToolResult.error("File not found: " + params.filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a file
|
||||||
|
if (!Files.isRegularFile(filePath)) {
|
||||||
|
return ToolResult.error("Path is not a regular file: " + params.filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取原始内容
|
||||||
|
String originalContent = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// 执行编辑
|
||||||
|
String newContent = performEdit(originalContent, params);
|
||||||
|
if (newContent == null) {
|
||||||
|
return ToolResult.error("Could not find the specified text to replace in file: " + params.filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建备份
|
||||||
|
if (shouldCreateBackup()) {
|
||||||
|
createBackup(filePath, originalContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write new content
|
||||||
|
Files.writeString(filePath, newContent, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Generate differences and results
|
||||||
|
String diff = generateDiff(filePath.getFileName().toString(), originalContent, newContent);
|
||||||
|
String relativePath = getRelativePath(filePath);
|
||||||
|
String successMessage = String.format("Successfully edited file: %s", params.filePath);
|
||||||
|
|
||||||
|
return ToolResult.success(successMessage, new FileDiff(diff, filePath.getFileName().toString()));
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error editing file: " + params.filePath, e);
|
||||||
|
return ToolResult.error("Error editing file: " + e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error editing file: " + params.filePath, e);
|
||||||
|
return ToolResult.error("Unexpected error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String performEdit(String content, EditFileParams params) {
|
||||||
|
// If line numbers are specified, use line numbers to assist in finding
|
||||||
|
if (params.startLine != null && params.endLine != null) {
|
||||||
|
return performEditWithLineNumbers(content, params);
|
||||||
|
} else {
|
||||||
|
return performSimpleEdit(content, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String performSimpleEdit(String content, EditFileParams params) {
|
||||||
|
// Simple string replacement
|
||||||
|
if (!content.contains(params.oldStr)) {
|
||||||
|
return null; // Cannot find string to replace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only replace the first match to avoid unexpected multiple replacements
|
||||||
|
int index = content.indexOf(params.oldStr);
|
||||||
|
if (index == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.substring(0, index) + params.newStr + content.substring(index + params.oldStr.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String performEditWithLineNumbers(String content, EditFileParams params) {
|
||||||
|
String[] lines = content.split("\n", -1); // -1 preserve trailing empty lines
|
||||||
|
|
||||||
|
// Validate line number range
|
||||||
|
if (params.startLine > lines.length || params.endLine > lines.length) {
|
||||||
|
return null; // Line number out of range
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract content from specified line range
|
||||||
|
StringBuilder targetContent = new StringBuilder();
|
||||||
|
for (int i = params.startLine - 1; i < params.endLine; i++) {
|
||||||
|
if (i > params.startLine - 1) {
|
||||||
|
targetContent.append("\n");
|
||||||
|
}
|
||||||
|
targetContent.append(lines[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否匹配
|
||||||
|
if (!targetContent.toString().equals(params.oldStr)) {
|
||||||
|
return null; // 指定行范围的内容与old_str不匹配
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行替换
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
|
||||||
|
// 添加前面的行
|
||||||
|
for (int i = 0; i < params.startLine - 1; i++) {
|
||||||
|
if (i > 0) result.append("\n");
|
||||||
|
result.append(lines[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新内容
|
||||||
|
if (params.startLine > 1) result.append("\n");
|
||||||
|
result.append(params.newStr);
|
||||||
|
|
||||||
|
// 添加后面的行
|
||||||
|
for (int i = params.endLine; i < lines.length; i++) {
|
||||||
|
result.append("\n");
|
||||||
|
result.append(lines[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateDiff(String fileName, String oldContent, String newContent) {
|
||||||
|
try {
|
||||||
|
List<String> oldLines = Arrays.asList(oldContent.split("\n"));
|
||||||
|
List<String> newLines = Arrays.asList(newContent.split("\n"));
|
||||||
|
|
||||||
|
Patch<String> patch = DiffUtils.diff(oldLines, newLines);
|
||||||
|
List<String> unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(
|
||||||
|
fileName + " (Original)",
|
||||||
|
fileName + " (Edited)",
|
||||||
|
oldLines,
|
||||||
|
patch,
|
||||||
|
3 // context lines
|
||||||
|
);
|
||||||
|
|
||||||
|
return String.join("\n", unifiedDiff);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Could not generate diff", e);
|
||||||
|
return "Diff generation failed: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createBackup(Path filePath, String content) throws IOException {
|
||||||
|
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||||||
|
String backupFileName = filePath.getFileName().toString() + ".backup." + timestamp;
|
||||||
|
Path backupPath = filePath.getParent().resolve(backupFileName);
|
||||||
|
|
||||||
|
Files.writeString(backupPath, content, StandardCharsets.UTF_8);
|
||||||
|
logger.info("Created backup: {}", backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldCreateBackup() {
|
||||||
|
return true; // 总是创建备份
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWithinWorkspace(Path filePath) {
|
||||||
|
try {
|
||||||
|
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||||
|
Path normalizedPath = filePath.normalize();
|
||||||
|
return normalizedPath.startsWith(workspaceRoot.normalize());
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Could not resolve workspace path", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRelativePath(Path filePath) {
|
||||||
|
try {
|
||||||
|
Path workspaceRoot = Paths.get(rootDirectory);
|
||||||
|
return workspaceRoot.relativize(filePath).toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return filePath.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑文件参数
|
||||||
|
*/
|
||||||
|
public static class EditFileParams {
|
||||||
|
@JsonProperty("file_path")
|
||||||
|
private String filePath;
|
||||||
|
|
||||||
|
@JsonProperty("old_str")
|
||||||
|
private String oldStr;
|
||||||
|
|
||||||
|
@JsonProperty("new_str")
|
||||||
|
private String newStr;
|
||||||
|
|
||||||
|
@JsonProperty("start_line")
|
||||||
|
private Integer startLine;
|
||||||
|
|
||||||
|
@JsonProperty("end_line")
|
||||||
|
private Integer endLine;
|
||||||
|
|
||||||
|
// 构造器
|
||||||
|
public EditFileParams() {}
|
||||||
|
|
||||||
|
public EditFileParams(String filePath, String oldStr, String newStr) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.oldStr = oldStr;
|
||||||
|
this.newStr = newStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getFilePath() { return filePath; }
|
||||||
|
public void setFilePath(String filePath) { this.filePath = filePath; }
|
||||||
|
|
||||||
|
public String getOldStr() { return oldStr; }
|
||||||
|
public void setOldStr(String oldStr) { this.oldStr = oldStr; }
|
||||||
|
|
||||||
|
public String getNewStr() { return newStr; }
|
||||||
|
public void setNewStr(String newStr) { this.newStr = newStr; }
|
||||||
|
|
||||||
|
public Integer getStartLine() { return startLine; }
|
||||||
|
public void setStartLine(Integer startLine) { this.startLine = startLine; }
|
||||||
|
|
||||||
|
public Integer getEndLine() { return endLine; }
|
||||||
|
public void setEndLine(Integer endLine) { this.endLine = endLine; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("EditFileParams{path='%s', oldStrLength=%d, newStrLength=%d, lines=%s-%s}",
|
||||||
|
filePath,
|
||||||
|
oldStr != null ? oldStr.length() : 0,
|
||||||
|
newStr != null ? newStr.length() : 0,
|
||||||
|
startLine, endLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
package com.example.demo.tools;
|
||||||
|
|
||||||
|
import com.example.demo.config.AppProperties;
|
||||||
|
import com.example.demo.utils.PathUtils;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件操作工具类 - 使用Spring AI 1.0.0 @Tool注解
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class FileOperationTools {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FileOperationTools.class);
|
||||||
|
|
||||||
|
private final String rootDirectory;
|
||||||
|
private final AppProperties appProperties;
|
||||||
|
|
||||||
|
// 在构造函数中
|
||||||
|
public FileOperationTools(AppProperties appProperties) {
|
||||||
|
this.appProperties = appProperties;
|
||||||
|
// 使用规范化的路径
|
||||||
|
this.rootDirectory = PathUtils.normalizePath(appProperties.getWorkspace().getRootDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tool(description = "Read the content of a file from the local filesystem. Supports pagination for large files.")
|
||||||
|
public String readFile(
|
||||||
|
@ToolParam(description = "The absolute path to the file to read. Must be within the workspace directory.")
|
||||||
|
String absolutePath,
|
||||||
|
@ToolParam(description = "Optional: For text files, the 0-based line number to start reading from.", required = false)
|
||||||
|
Integer offset,
|
||||||
|
@ToolParam(description = "Optional: For text files, the number of lines to read from the offset.", required = false)
|
||||||
|
Integer limit) {
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
logger.debug("Starting readFile operation for: {}", absolutePath);
|
||||||
|
// 验证路径
|
||||||
|
String validationError = validatePath(absolutePath);
|
||||||
|
if (validationError != null) {
|
||||||
|
return "Error: " + validationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path filePath = Paths.get(absolutePath);
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!Files.exists(filePath)) {
|
||||||
|
return "Error: File not found: " + absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为文件
|
||||||
|
if (!Files.isRegularFile(filePath)) {
|
||||||
|
return "Error: Path is not a regular file: " + absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小
|
||||||
|
long fileSize = Files.size(filePath);
|
||||||
|
if (fileSize > appProperties.getWorkspace().getMaxFileSize()) {
|
||||||
|
return "Error: File too large: " + fileSize + " bytes. Maximum allowed: " +
|
||||||
|
appProperties.getWorkspace().getMaxFileSize() + " bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件扩展名
|
||||||
|
String fileName = filePath.getFileName().toString();
|
||||||
|
if (!isAllowedFileType(fileName)) {
|
||||||
|
return "Error: File type not allowed: " + fileName +
|
||||||
|
". Allowed extensions: " + appProperties.getWorkspace().getAllowedExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件
|
||||||
|
if (offset != null && limit != null) {
|
||||||
|
return readFileWithPagination(filePath, offset, limit);
|
||||||
|
} else {
|
||||||
|
return readFullFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
logger.error("Error reading file: {} (duration: {}ms)", absolutePath, duration, e);
|
||||||
|
return String.format("❌ Error reading file: %s\n⏱️ Duration: %dms\n🔍 Details: %s",
|
||||||
|
absolutePath, duration, e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
logger.error("Unexpected error reading file: {} (duration: {}ms)", absolutePath, duration, e);
|
||||||
|
return String.format("❌ Unexpected error reading file: %s\n⏱️ Duration: %dms\n🔍 Details: %s",
|
||||||
|
absolutePath, duration, e.getMessage());
|
||||||
|
} finally {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
logger.debug("Completed readFile operation for: {} (duration: {}ms)", absolutePath, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tool(description = "Write content to a file. Creates new file or overwrites existing file.")
|
||||||
|
public String writeFile(
|
||||||
|
@ToolParam(description = "The absolute path to the file to write. Must be within the workspace directory.")
|
||||||
|
String filePath,
|
||||||
|
@ToolParam(description = "The content to write to the file")
|
||||||
|
String content) {
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
logger.debug("Starting writeFile operation for: {}", filePath);
|
||||||
|
// 验证路径
|
||||||
|
String validationError = validatePath(filePath);
|
||||||
|
if (validationError != null) {
|
||||||
|
return "Error: " + validationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证内容大小
|
||||||
|
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (contentBytes.length > appProperties.getWorkspace().getMaxFileSize()) {
|
||||||
|
return "Error: Content too large: " + contentBytes.length + " bytes. Maximum allowed: " +
|
||||||
|
appProperties.getWorkspace().getMaxFileSize() + " bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = Paths.get(filePath);
|
||||||
|
boolean isNewFile = !Files.exists(path);
|
||||||
|
|
||||||
|
// 确保父目录存在
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
Files.writeString(path, content, StandardCharsets.UTF_8,
|
||||||
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
|
|
||||||
|
long lineCount = content.lines().count();
|
||||||
|
String absolutePath = path.toAbsolutePath().toString();
|
||||||
|
String relativePath = getRelativePath(path);
|
||||||
|
|
||||||
|
if (isNewFile) {
|
||||||
|
return String.format("Successfully created file:\n📁 Full path: %s\n📂 Relative path: %s\n📊 Stats: %d lines, %d bytes",
|
||||||
|
absolutePath, relativePath, lineCount, contentBytes.length);
|
||||||
|
} else {
|
||||||
|
return String.format("Successfully wrote to file:\n📁 Full path: %s\n📂 Relative path: %s\n📊 Stats: %d lines, %d bytes",
|
||||||
|
absolutePath, relativePath, lineCount, contentBytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
logger.error("Error writing file: {} (duration: {}ms)", filePath, duration, e);
|
||||||
|
return String.format("❌ Error writing file: %s\n⏱️ Duration: %dms\n🔍 Details: %s",
|
||||||
|
filePath, duration, e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
logger.error("Unexpected error writing file: {} (duration: {}ms)", filePath, duration, e);
|
||||||
|
return String.format("❌ Unexpected error writing file: %s\n⏱️ Duration: %dms\n🔍 Details: %s",
|
||||||
|
filePath, duration, e.getMessage());
|
||||||
|
} finally {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
logger.debug("Completed writeFile operation for: {} (duration: {}ms)", filePath, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tool(description = "Edit a file by replacing specific text content.")
|
||||||
|
public String editFile(
|
||||||
|
@ToolParam(description = "The absolute path to the file to edit. Must be within the workspace directory.")
|
||||||
|
String filePath,
|
||||||
|
@ToolParam(description = "The text to find and replace in the file")
|
||||||
|
String oldText,
|
||||||
|
@ToolParam(description = "The new text to replace the old text with")
|
||||||
|
String newText) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证路径
|
||||||
|
String validationError = validatePath(filePath);
|
||||||
|
if (validationError != null) {
|
||||||
|
return "Error: " + validationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = Paths.get(filePath);
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
return "Error: File not found: " + filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为文件
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
return "Error: Path is not a regular file: " + filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取原始内容
|
||||||
|
String originalContent = Files.readString(path, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// 执行替换
|
||||||
|
if (!originalContent.contains(oldText)) {
|
||||||
|
return "Error: Could not find the specified text to replace in file: " + filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String newContent = originalContent.replace(oldText, newText);
|
||||||
|
|
||||||
|
// 写入新内容
|
||||||
|
Files.writeString(path, newContent, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
String absolutePath = path.toAbsolutePath().toString();
|
||||||
|
String relativePath = getRelativePath(path);
|
||||||
|
return String.format("Successfully edited file:\n📁 Full path: %s\n📂 Relative path: %s\n✏️ Replaced text successfully",
|
||||||
|
absolutePath, relativePath);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error editing file: " + filePath, e);
|
||||||
|
return "Error editing file: " + e.getMessage();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error editing file: " + filePath, e);
|
||||||
|
return "Unexpected error: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tool(description = "List the contents of a directory.")
|
||||||
|
public String listDirectory(
|
||||||
|
@ToolParam(description = "The absolute path to the directory to list. Must be within the workspace directory.")
|
||||||
|
String directoryPath,
|
||||||
|
@ToolParam(description = "Whether to list contents recursively", required = false)
|
||||||
|
Boolean recursive) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证路径
|
||||||
|
String validationError = validatePath(directoryPath);
|
||||||
|
if (validationError != null) {
|
||||||
|
return "Error: " + validationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = Paths.get(directoryPath);
|
||||||
|
|
||||||
|
// 检查目录是否存在
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
return "Error: Directory not found: " + directoryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为目录
|
||||||
|
if (!Files.isDirectory(path)) {
|
||||||
|
return "Error: Path is not a directory: " + directoryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isRecursive = recursive != null && recursive;
|
||||||
|
String absolutePath = path.toAbsolutePath().toString();
|
||||||
|
String relativePath = getRelativePath(path);
|
||||||
|
|
||||||
|
if (isRecursive) {
|
||||||
|
return listDirectoryRecursive(path, absolutePath, relativePath);
|
||||||
|
} else {
|
||||||
|
return listDirectorySimple(path, absolutePath, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error listing directory: " + directoryPath, e);
|
||||||
|
return "Error listing directory: " + e.getMessage();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error listing directory: " + directoryPath, e);
|
||||||
|
return "Unexpected error: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法
|
||||||
|
private String validatePath(String path) {
|
||||||
|
if (path == null || path.trim().isEmpty()) {
|
||||||
|
return "Path cannot be empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
Path filePath = Paths.get(path);
|
||||||
|
|
||||||
|
// 验证是否为绝对路径
|
||||||
|
if (!filePath.isAbsolute()) {
|
||||||
|
return "Path must be absolute: " + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证是否在工作目录内
|
||||||
|
if (!isWithinWorkspace(filePath)) {
|
||||||
|
return "Path must be within the workspace directory (" + rootDirectory + "): " + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWithinWorkspace(Path path) {
|
||||||
|
try {
|
||||||
|
Path workspacePath = Paths.get(rootDirectory).toRealPath();
|
||||||
|
Path targetPath = path.toRealPath();
|
||||||
|
return targetPath.startsWith(workspacePath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// 如果路径不存在,检查其父目录
|
||||||
|
try {
|
||||||
|
Path workspacePath = Paths.get(rootDirectory).toRealPath();
|
||||||
|
Path normalizedPath = path.normalize();
|
||||||
|
return normalizedPath.startsWith(workspacePath.normalize());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAllowedFileType(String fileName) {
|
||||||
|
List<String> allowedExtensions = appProperties.getWorkspace().getAllowedExtensions();
|
||||||
|
return allowedExtensions.stream()
|
||||||
|
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRelativePath(Path path) {
|
||||||
|
try {
|
||||||
|
Path workspacePath = Paths.get(rootDirectory);
|
||||||
|
return workspacePath.relativize(path).toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return path.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readFullFile(Path filePath) throws IOException {
|
||||||
|
String content = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||||
|
String absolutePath = filePath.toAbsolutePath().toString();
|
||||||
|
String relativePath = getRelativePath(filePath);
|
||||||
|
|
||||||
|
long lineCount = content.lines().count();
|
||||||
|
return String.format("📁 Full path: %s\n📂 Relative path: %s\n📊 Stats: %d lines, %d bytes\n\n📄 Content:\n%s",
|
||||||
|
absolutePath, relativePath, lineCount, content.getBytes(StandardCharsets.UTF_8).length, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readFileWithPagination(Path filePath, int offset, int limit) throws IOException {
|
||||||
|
List<String> allLines = Files.readAllLines(filePath, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if (offset >= allLines.size()) {
|
||||||
|
return "Error: Offset " + offset + " is beyond file length (" + allLines.size() + " lines)";
|
||||||
|
}
|
||||||
|
|
||||||
|
int endIndex = Math.min(offset + limit, allLines.size());
|
||||||
|
List<String> selectedLines = allLines.subList(offset, endIndex);
|
||||||
|
String content = String.join("\n", selectedLines);
|
||||||
|
|
||||||
|
String absolutePath = filePath.toAbsolutePath().toString();
|
||||||
|
String relativePath = getRelativePath(filePath);
|
||||||
|
return String.format("📁 Full path: %s\n📂 Relative path: %s\n📊 Showing lines %d-%d of %d total\n\n📄 Content:\n%s",
|
||||||
|
absolutePath, relativePath, offset + 1, endIndex, allLines.size(), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String listDirectorySimple(Path path, String absolutePath, String relativePath) throws IOException {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
result.append("📁 Full path: ").append(absolutePath).append("\n");
|
||||||
|
result.append("📂 Relative path: ").append(relativePath).append("\n\n");
|
||||||
|
result.append("📋 Directory contents:\n");
|
||||||
|
|
||||||
|
try (Stream<Path> entries = Files.list(path)) {
|
||||||
|
List<Path> sortedEntries = entries.sorted().collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (Path entry : sortedEntries) {
|
||||||
|
String name = entry.getFileName().toString();
|
||||||
|
String entryAbsolutePath = entry.toAbsolutePath().toString();
|
||||||
|
if (Files.isDirectory(entry)) {
|
||||||
|
result.append("📁 [DIR] ").append(name).append("/\n");
|
||||||
|
result.append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||||
|
} else {
|
||||||
|
long size = Files.size(entry);
|
||||||
|
result.append("📄 [FILE] ").append(name).append(" (").append(size).append(" bytes)\n");
|
||||||
|
result.append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String listDirectoryRecursive(Path path, String absolutePath, String relativePath) throws IOException {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
result.append("📁 Full path: ").append(absolutePath).append("\n");
|
||||||
|
result.append("📂 Relative path: ").append(relativePath).append("\n\n");
|
||||||
|
result.append("🌳 Directory tree (recursive):\n");
|
||||||
|
|
||||||
|
try (Stream<Path> entries = Files.walk(path)) {
|
||||||
|
entries.sorted()
|
||||||
|
.forEach(entry -> {
|
||||||
|
if (!entry.equals(path)) {
|
||||||
|
String entryAbsolutePath = entry.toAbsolutePath().toString();
|
||||||
|
String entryRelativePath = getRelativePath(entry);
|
||||||
|
|
||||||
|
// 计算缩进级别
|
||||||
|
int depth = entry.getNameCount() - path.getNameCount();
|
||||||
|
String indent = " ".repeat(depth);
|
||||||
|
|
||||||
|
if (Files.isDirectory(entry)) {
|
||||||
|
result.append(indent).append("📁 ").append(entryRelativePath).append("/\n");
|
||||||
|
result.append(indent).append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
long size = Files.size(entry);
|
||||||
|
result.append(indent).append("📄 ").append(entryRelativePath).append(" (").append(size).append(" bytes)\n");
|
||||||
|
result.append(indent).append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||||
|
} catch (IOException e) {
|
||||||
|
result.append(indent).append("📄 ").append(entryRelativePath).append(" (size unknown)\n");
|
||||||
|
result.append(indent).append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user