mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-06-19 04:17:04 +00:00
Compare commits
1 Commits
main
...
260503-fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
410cb0b6f2 |
63
.monkeycode/MEMORY.md
Normal file
63
.monkeycode/MEMORY.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 用户指令记忆
|
||||
|
||||
本文件记录了用户的指令、偏好和教导,用于在未来的交互中提供参考。
|
||||
|
||||
## 格式
|
||||
|
||||
### 用户指令条目
|
||||
用户指令条目应遵循以下格式:
|
||||
|
||||
[用户指令摘要]
|
||||
- Date: [YYYY-MM-DD]
|
||||
- Context: [提及的场景或时间]
|
||||
- Instructions:
|
||||
- [用户教导或指示的内容,逐行描述]
|
||||
|
||||
### 项目知识条目
|
||||
Agent 在任务执行过程中发现的条目应遵循以下格式:
|
||||
|
||||
[项目知识摘要]
|
||||
- Date: [YYYY-MM-DD]
|
||||
- Context: Agent 在执行 [具体任务描述] 时发现
|
||||
- Category: [代码结构|代码模式|代码生成|构建方法|测试方法|依赖关系|环境配置]
|
||||
- Instructions:
|
||||
- [具体的知识点,逐行描述]
|
||||
|
||||
## 去重策略
|
||||
- 添加新条目前,检查是否存在相似或相同的指令
|
||||
- 若发现重复,跳过新条目或与已有条目合并
|
||||
- 合并时,更新上下文或日期信息
|
||||
- 这有助于避免冗余条目,保持记忆文件整洁
|
||||
|
||||
## 条目
|
||||
|
||||
[项目技术栈偏好:LangChain4j]
|
||||
- Date: 2026-05-03
|
||||
- Context: 用户在讨论“集成 AI 编程能力”方案时说明
|
||||
- Category: 依赖关系
|
||||
- Instructions:
|
||||
- 项目基于 LangChain4j 设计仓库级理解与自动化动作能力
|
||||
|
||||
[需求澄清:构建通用 Coding Agent]
|
||||
- Date: 2026-05-03
|
||||
- Context: 用户纠正方案范围时说明
|
||||
- Category: 代码结构
|
||||
- Instructions:
|
||||
- 目标是基于 LangChain4j 构建通用 coding agent,而非仅项目内问答助手
|
||||
- Agent 需要可操作文件、调用工具、跨前后端完成任务(如新建前端页面并对接现有后端)
|
||||
|
||||
[内置工具自动注册机制]
|
||||
- Date: 2026-05-03
|
||||
- Context: Agent 在执行 coding agent 工具扩展时发现
|
||||
- Category: 代码结构
|
||||
- Instructions:
|
||||
- ruoyi-chat 模块通过 BuiltinToolProvider + @Component 自动发现并注册内置工具
|
||||
- 新增工具无需手工改注册表,BuiltinToolRegistry 会在启动时扫描并创建可供 Agent 调用的实例
|
||||
|
||||
[新增能力方向:AI 报表 Agent]
|
||||
- Date: 2026-05-03
|
||||
- Context: 用户提出新的产品化需求
|
||||
- Category: 代码模式
|
||||
- Instructions:
|
||||
- 用户希望通过自然语言生成报表,包含数据库查询和 HTML 报表生成
|
||||
- 用户希望在报表页面内通过提示词继续动态编辑页面
|
||||
70
README.md
70
README.md
@@ -21,8 +21,8 @@
|
||||
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
|
||||
|
||||
**[English](README_EN.md)** | **[📖 使用文档](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 在线体验](https://web.ruoyiai.chat/)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
**[English](README_EN.md)** | **[📖 使用文档](https://doc.pandarobot.chat)** |
|
||||
**[🚀 在线体验](https://web.pandarobot.chat)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型
|
||||
|
||||
## 🚀 快速体验
|
||||
|
||||
### 在线演示
|
||||
|
||||
| 平台 | 地址 | 账号 |
|
||||
|:------:|---|---|
|
||||
| 用户端 | [web.pandarobot.chat](https://web.pandarobot.chat) | admin / admin123 |
|
||||
| 管理后台 | [admin.pandarobot.chat](https://admin.pandarobot.chat) | admin / admin123 |
|
||||
|
||||
### 项目源码
|
||||
|
||||
@@ -58,10 +66,35 @@
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||
- **安全认证**:Sa-Token + JWT 双重保障
|
||||
|
||||
|
||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
|
||||
## 使用web coding急速部署
|
||||
#### 在线体验: https://monkeycode-ai.com/?ic=019d9e9f-edc3-7a4b-8987-11b028751a1e
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<img src="docs/image/01.png" alt="web code" width="660" height="400"><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<img src="docs/image/02.png" alt="web code" width="660" height="400"><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<img src="docs/image/03.png" alt="web code" width="660" height="400"><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### 等待10分钟左右即可完成
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
@@ -177,7 +210,7 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
|
||||
想要深入了解安装部署、功能配置和二次开发?
|
||||
|
||||
**👉 [完整使用文档](https://doc.ruoyiai.chat/)**
|
||||
**👉 [完整使用文档](https://doc.pandarobot.chat)**
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
@@ -200,29 +233,17 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
## 🙏 特别鸣谢
|
||||
|
||||
感谢以下优秀的开源项目为本项目提供支持:
|
||||
- [Spring AI Alibaba Copilot](https://github.com/spring-ai-alibaba/copilot) - 基于spring-ai-alibaba
|
||||
的智能编码助手
|
||||
- [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 后台管理模板
|
||||
|
||||
## 🌐 生态伙伴
|
||||
|
||||
## 💎 赞助商
|
||||
|
||||
**感谢以下赞助商对本项目的支持:**
|
||||
|
||||
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[访问Atlas Cloud官网](https://www.atlascloud.ai?ref=89F97E) · [编程计划优惠](https://www.atlascloud.ai/console/coding-plan)
|
||||
全模态 AI 推理平台,为开发者提供统一的 AI API,支持视频生成、图像生成和大语言模型。一次接入,即可访问 **300+ 精选模型**。
|
||||
|
||||
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="火山引擎 CodingPlan" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[火山引擎 CodingPlan 开发者计划](https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
||||
火山引擎是字节跳动旗下的云与 AI 服务平台,火山方舟提供豆包大模型、DeepSeek 等多种模型的 API 接入,为开发者提供一站式 AI 开发与推理服务。
|
||||
|
||||
- [PPIO 派欧云](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - 提供高性价比的 GPU
|
||||
算力和模型 API 服务
|
||||
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - 万卡RTX40系GPU+海内外主流模型API服务,秒级响应,按量计费,新客免费用。
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
@@ -236,11 +257,6 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
<em>邀请进群学习</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx06.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>微信技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ群二维码" width="200" height="200"><br>
|
||||
<strong>QQ技术交流群</strong><br>
|
||||
<em>技术讨论</em>
|
||||
@@ -254,7 +270,7 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
---
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.ruoyiai.chat/)**
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.pandarobot.chat)**
|
||||
|
||||
*用 ❤️ 打造,由 RuoYi AI 开源社区维护*
|
||||
|
||||
|
||||
55
README_EN.md
55
README_EN.md
@@ -22,8 +22,8 @@
|
||||
|
||||
*An out-of-the-box full-stack AI platform supporting multi-agent collaboration, Supervisor mode orchestration, and multiple decision models, with advanced RAG technology and visual workflow orchestration capabilities*
|
||||
|
||||
**[中文](README.md)** | **[📖 Documentation](https://doc.ruoyiai.chat/)** |
|
||||
**[🚀 Live Demo](https://web.ruoyiai.chat/)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
**[中文](README.md)** | **[📖 Documentation](https://doc.pandarobot.chat)** |
|
||||
**[🚀 Live Demo](https://web.pandarobot.chat)** | **[🐛 Report Issues](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 Feature Requests](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,15 @@
|
||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Live Demo
|
||||
|
||||
| Platform | URL | Account |
|
||||
|:------:|---|---|
|
||||
| User Frontend | [web.pandarobot.chat](https://web.pandarobot.chat) | admin / admin123 |
|
||||
| Admin Panel | [admin.pandarobot.chat](https://admin.pandarobot.chat) | admin / admin123 |
|
||||
|
||||
### Project Repositories
|
||||
|
||||
| Module | GitHub Repository | Gitee Repository | GitCode Repository |
|
||||
@@ -60,6 +69,8 @@
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||
- **Security**: Sa-Token + JWT dual-layer security
|
||||
|
||||
|
||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
||||
@@ -179,7 +190,7 @@ docker-compose -f docker-compose-all.yaml restart [service-name]
|
||||
|
||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||
|
||||
**👉 [Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
**👉 [Complete Documentation](https://doc.pandarobot.chat)**
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -202,47 +213,23 @@ This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) f
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
Thanks to the following excellent open-source projects for their support:
|
||||
- [Spring AI Alibaba Copilot](https://github.com/spring-ai-alibaba/copilot) - Intelligent coding assistant based on spring-ai-alibaba
|
||||
- [Langchain4j](https://github.com/langchain4j/langchain4j) - Powerful Java LLM development framework
|
||||
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - Mature enterprise-level rapid development framework
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) - Modern Vue admin template
|
||||
|
||||
## 💎 Sponsors
|
||||
## 🌐 Ecosystem Partners
|
||||
|
||||
**Thanks to the following sponsors for supporting this project:**
|
||||
- [PPIO Cloud](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - Provides cost-effective GPU computing and model API services
|
||||
- [Youyun Intelligent Computing](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - Thousands of RTX40 series GPUs + mainstream models API services, second-level response, pay-per-use, free for new customers.
|
||||
|
||||
<a href="https://www.atlascloud.ai?ref=89F97E">
|
||||
<img src="docs/image/sponsor/atlascloud_banner.png" alt="Atlas Cloud" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[Visit Atlas Cloud](https://www.atlascloud.ai?ref=89F97E) · [Coding Plan Promotion](https://www.atlascloud.ai/console/coding-plan)
|
||||
A full-modal AI inference platform that gives developers a unified AI API, supporting video generation, image generation, and LLMs. Connect once to access **300+ curated models**.
|
||||
|
||||
<a href="https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai">
|
||||
<img src="docs/image/sponsor/huoshan.png" alt="Volcengine CodingPlan" width="160" height="80">
|
||||
</a>
|
||||
|
||||
[Volcengine CodingPlan Developer Program](https://www.volcengine.com/activity/codingplan?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ageerle-ruoyi-ai)
|
||||
Volcengine is ByteDance's cloud and AI service platform. Volcengine Ark provides API access to Doubao LLM, DeepSeek, and more — a one-stop AI development and inference platform for developers.
|
||||
|
||||
## 💬 Community Chat
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/wx.png" alt="WeChat QR Code" width="200" height="200"><br>
|
||||
<strong>Scan to add author on WeChat</strong><br>
|
||||
<em>Join group for learning</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/qq.png" alt="QQ Group QR Code" width="200" height="200"><br>
|
||||
<strong>QQ Tech Exchange Group</strong><br>
|
||||
<em>Technical discussion</em>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
**[📱 Join Telegram Group](
|
||||
https://t.me/+LqooQAc5HxRmYmE1)**
|
||||
|
||||
</div>
|
||||
|
||||
@@ -250,7 +237,7 @@ Volcengine is ByteDance's cloud and AI service platform. Volcengine Ark provides
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README.md)** • **[📖 Complete Documentation](https://doc.ruoyiai.chat/)**
|
||||
**[⭐ Star to Support](https://github.com/ageerle/ruoyi-ai)** • **[Fork to Contribute](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 中文](README.md)** • **[📖 Complete Documentation](https://doc.pandarobot.chat)**
|
||||
|
||||
*Built with ❤️, maintained by the RuoYi AI open-source community*
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 127 KiB |
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
Navicat MySQL Dump SQL
|
||||
|
||||
Source Server : localhost_3306
|
||||
Source Server : 本地
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 80045 (8.0.45)
|
||||
Source Host : localhost:3306
|
||||
@@ -11,11 +11,13 @@
|
||||
Target Server Version : 80045 (8.0.45)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 21/05/2026 11:03:49
|
||||
Date: 15/03/2026 23:56:19
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
-- 忽略所有错误,继续执行
|
||||
SET sql_mode = '';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for chat_message
|
||||
@@ -70,9 +72,9 @@ CREATE TABLE `chat_model` (
|
||||
-- ----------------------------
|
||||
-- Records of chat_model
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) VALUES (2000585866022060033, 'chat', 'zai-org/glm-5.1', 'atlas', 'zai-org/glm-5.1', NULL, 'Y', 'https://api.atlascloud.ai/v1', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', '对话模型', 0);
|
||||
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) VALUES (2007528268536287233, 'vector', 'embedding-3', 'zhipu', 'embedding-3', 2048, 'N', 'https://open.bigmodel.cn', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', '向量模型', 0);
|
||||
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) VALUES (2045071617578237953, 'rerank', 'rerank', 'zhipu', 'rerank', NULL, 'N', 'https://open.bigmodel.cn', 'sk_xx', 103, 1, '2026-04-17 17:27:24', 1, '2026-04-20 15:21:48', '重排序模型', 0);
|
||||
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'zai-org/glm-5', 'ppio', 'zai-org/glm-5', NULL, 'Y', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse Attention(DSA)稀疏注意力机制,在显著降低计算开销的同时优化长上下文性能;通过可扩展强化学习框架,整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro;同时,模型依托大型智能体任务合成管线,具备更强的工具调用与多步骤决策能力,并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型,可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言,并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中,BGE-M3展现出显著优势,其性能在MIRACL、MKQA等国际基准测试中位居前列。此外,针对长文档检索场景,该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2045735140488847361, 'chat', 'deepseek-chat', 'custom_api', 'deepseek-chat', NULL, NULL, 'https://api.deepseek.com', 'sk_xx', 103, 1, '2026-04-19 13:24:00', 1, '2026-04-19 13:24:00', 'deepseek对话模型', 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for chat_provider
|
||||
@@ -105,11 +107,12 @@ CREATE TABLE `chat_provider` (
|
||||
-- ----------------------------
|
||||
-- Records of chat_provider
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (11, '深度求索', 'deepseek', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/5ba8c30f153246898a4d7dc7b846de8d.png', 'DeepSeek官方API', 'https://api.deepseek.com', '0', 0, 103, '2026-04-19 12:52:34', '1', '1', '2026-04-19 13:13:25', 'DeepSeek官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/da071783c9284fdd9ed1ce1b57b3c75c.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 4, 103, '2025-12-14 21:48:11', '1', '1', '2026-04-19 13:14:00', '智谱AI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/v1', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/anthropic/v1/messages', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (15, 'Atlas Cloud', 'atlas', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/atlascloud.png', '全模态AI推理平台', 'https://api.atlascloud.ai/v1', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'Atlas Cloud AI平台', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (15, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务,支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0);
|
||||
@@ -1088,7 +1091,6 @@ CREATE TABLE `knowledge_attach` (
|
||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
|
||||
`status` tinyint NULL DEFAULT 0 COMMENT '解析状态: 0待解析, 1解析中, 2已解析, 3解析失败',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `idx_kname`(`knowledge_id` ASC, `name` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2033199209203183619 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '知识库附件' ROW_FORMAT = DYNAMIC;
|
||||
@@ -1113,9 +1115,7 @@ CREATE TABLE `knowledge_fragment` (
|
||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
|
||||
`knowledge_id` bigint NULL DEFAULT NULL COMMENT '知识库ID',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
FULLTEXT INDEX `ft_content`(`content`) WITH PARSER `ngram`
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2033199209131880451 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '知识片段' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
@@ -1135,7 +1135,6 @@ CREATE TABLE `knowledge_info` (
|
||||
`separator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '知识分隔符',
|
||||
`overlap_char` int NULL DEFAULT NULL COMMENT '重叠字符数',
|
||||
`retrieve_limit` int NULL DEFAULT NULL COMMENT '知识库中检索的条数',
|
||||
`similarity_threshold` double NULL DEFAULT 0.5 COMMENT '相似度阈值',
|
||||
`text_block_size` int NULL DEFAULT NULL COMMENT '文本块大小',
|
||||
`vector_model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '向量库',
|
||||
`embedding_model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '向量模型',
|
||||
@@ -1146,13 +1145,6 @@ CREATE TABLE `knowledge_info` (
|
||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
|
||||
`enable_rerank` tinyint NULL DEFAULT 0 COMMENT '是否启用重排序(0否 1是)',
|
||||
`rerank_score_threshold` double NULL DEFAULT NULL COMMENT '重排序相关性分数阈值',
|
||||
`rerank_top_n` int NULL DEFAULT NULL COMMENT '重排序后返回的文档数量',
|
||||
`rerank_model` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '重排序模型名称',
|
||||
`enable_hybrid` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用混合检索',
|
||||
`hybrid_alpha` double NULL DEFAULT 0.5 COMMENT '混合检索权重比例 (0.0=纯向量, 1.0=纯关键词)',
|
||||
`system_prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '系统提示词',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2033198818050781187 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '知识库' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
@@ -1182,7 +1174,7 @@ CREATE TABLE `mcp_market_info` (
|
||||
INDEX `idx_name`(`name` ASC) USING BTREE,
|
||||
INDEX `idx_status`(`status` ASC) USING BTREE,
|
||||
INDEX `idx_tenant_id`(`tenant_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP市场表' ROW_FORMAT = DYNAMIC;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP市场表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of mcp_market_info
|
||||
@@ -1206,7 +1198,7 @@ CREATE TABLE `mcp_market_tool` (
|
||||
INDEX `idx_market_id`(`market_id` ASC) USING BTREE,
|
||||
INDEX `idx_tool_name`(`tool_name` ASC) USING BTREE,
|
||||
INDEX `idx_is_loaded`(`is_loaded` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP市场工具关联表' ROW_FORMAT = DYNAMIC;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP市场工具关联表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of mcp_market_tool
|
||||
@@ -1235,7 +1227,7 @@ CREATE TABLE `mcp_tool_info` (
|
||||
INDEX `idx_type`(`type` ASC) USING BTREE,
|
||||
INDEX `idx_status`(`status` ASC) USING BTREE,
|
||||
INDEX `idx_tenant_id`(`tenant_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP工具表' ROW_FORMAT = DYNAMIC;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MCP工具表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of mcp_tool_info
|
||||
@@ -2093,7 +2085,6 @@ INSERT INTO `sys_dict_data` VALUES (2026642525081116674, '000000', 1, '图像',
|
||||
INSERT INTO `sys_dict_data` VALUES (2026643983713247233, '000000', 1, '次数计费', '1', 'sys_model_billing', NULL, 'green', 'N', 103, 1, '2026-02-25 21:02:34', 1, '2026-02-25 21:02:56', NULL);
|
||||
INSERT INTO `sys_dict_data` VALUES (2026644058522853378, '000000', 2, 'token计费', '2', 'sys_model_billing', NULL, 'primary', 'N', 103, 1, '2026-02-25 21:02:51', 1, '2026-02-25 21:02:51', NULL);
|
||||
INSERT INTO `sys_dict_data` VALUES (2027261114955931650, '000000', 2, '向量', 'vector', 'chat_model_category', NULL, 'default', 'N', 103, 1, '2026-02-27 13:54:49', 1, '2026-02-27 13:54:54', NULL);
|
||||
INSERT INTO `sys_dict_data` VALUES (2045070879435259905, '000000', 4, '重排序', 'rerank', 'chat_model_category', NULL, '#000000', 'N', 103, 1, '2026-04-17 17:24:28', 1, '2026-04-19 01:02:20', '重排序模型');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_dict_type
|
||||
@@ -2165,6 +2156,243 @@ CREATE TABLE `sys_logininfor` (
|
||||
-- ----------------------------
|
||||
-- Records of sys_logininfor
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_logininfor` VALUES (2018437160497668098, '000000', 'admin', 'web', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 05:31:34');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018489328494317570, '000000', 'admin', 'web', 'pc', '58.56.198.114', '中国|山东省|泰安市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 08:58:52');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018490223370047490, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 09:02:26');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018494821124149250, '000000', 'admin', 'web', 'pc', '180.165.21.147', '中国|上海|上海市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 09:20:42');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018495323429801985, '000000', 'admin', 'web', 'pc', '218.1.209.117', '中国|上海|上海市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 09:22:42');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018496463106084866, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 09:27:13');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018506873276338177, '000000', 'admin', 'web', 'pc', '14.155.110.230', '中国|广东省|深圳市|电信', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-03 10:08:35');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018507142684872705, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-03 10:09:40');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018507255759114241, '000000', 'admin', 'web', 'pc', '27.156.68.14', '中国|福建省|福州市|电信', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-03 10:10:06');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018507799294775297, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 10:12:16');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018507878344822786, '000000', 'admin', 'web', 'pc', '39.78.246.253', '中国|山东省|济南市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 10:12:35');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018508087959359489, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 10:13:25');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018513467045187586, '000000', 'admin', 'web', 'pc', '111.175.56.210', '中国|湖北省|武汉市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 10:34:47');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018515864068952066, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 10:44:19');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018516837059399682, '000000', 'admin', 'web', 'pc', '115.236.45.35', '中国|浙江省|杭州市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 10:48:11');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018517630747545602, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 10:51:20');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018530038144700418, '000000', 'admin', 'web', 'pc', '116.128.248.194', '中国|湖南省|长沙市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 11:40:38');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018531663223590914, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 11:47:06');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018534417249734658, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 11:58:02');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018537768200835074, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 12:11:21');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018537900388519937, '000000', 'admin', 'web', 'pc', '183.54.238.100', '中国|广东省|广州市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 12:11:53');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018549455184334849, '000000', 'admin', 'web', 'pc', '13.212.58.4', '美国|康涅狄格|亚马逊', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-03 12:57:48');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018554371911061506, '000000', 'admin', 'web', 'pc', '61.182.224.157', '中国|河北省|石家庄市|联通', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 13:17:20');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018560767549378562, '000000', 'admin', 'web', 'pc', '106.39.125.218', '中国|北京|北京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 13:42:45');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018567861803552769, '000000', 'admin', 'web', 'pc', '58.16.14.89', '中国|贵州省|贵阳市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 14:10:56');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018568178360258561, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 14:12:12');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018571219020943362, '000000', 'admin', 'web', 'pc', '116.169.71.139', '中国|辽宁省|威瑞森', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-03 14:24:17');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018574610824564737, '000000', 'admin', 'web', 'pc', '113.13.108.124', '中国|广西|柳州市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 14:37:45');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018574668861149186, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 14:37:59');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018577941697531906, '000000', 'admin', 'web', 'pc', '36.110.12.226', '中国|北京|北京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 14:50:59');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018578431793565697, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-03 14:52:56');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018582006717775874, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 15:07:08');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018582568484605953, '000000', 'admin', 'web', 'pc', '223.104.43.1', '中国|北京|北京市|移动', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 15:09:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018585018155274242, '000000', 'admin', 'web', 'pc', '116.169.127.68', '中国|辽宁省|威瑞森', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 15:19:06');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018586528248791041, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 15:25:07');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018586925091393537, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 15:26:41');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018587111276548097, '000000', 'admin', 'web', 'pc', '117.159.171.116', '中国|河南省|焦作市|移动', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 15:27:26');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018588210859479041, '000000', 'admin', 'web', 'pc', '61.169.93.182', '中国|上海|上海市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 15:31:48');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018588453218947074, '000000', 'admin', 'web', 'pc', '61.133.210.59', '中国|宁夏|银川市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '1', 'Password input error 1 times', '2026-02-03 15:32:45');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018588518431985666, '000000', 'admin', 'web', 'pc', '61.133.210.59', '中国|宁夏|银川市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 15:33:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018588841208844289, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 15:34:18');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018589986920730625, '000000', 'admin', 'web', 'pc', '115.236.69.226', '中国|浙江省|杭州市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 15:38:51');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018590616590618625, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 15:41:21');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018593068400381953, '000000', 'admin', 'web', 'pc', '180.123.251.84', '中国|江苏省|徐州市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 15:51:06');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018594019760803842, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 15:54:53');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018595653618372609, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 16:01:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018596016505360386, '000000', 'admin', 'web', 'pc', '114.242.16.161', '中国|北京|北京市|联通', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 16:02:49');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018596170675392513, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 16:03:25');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018599719354372098, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 16:17:32');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018599739910656001, '000000', 'admin', 'web', 'pc', '42.235.239.253', '中国|河南省|新乡市|联通', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 16:17:36');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018600606151872513, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 16:21:03');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018600933580214274, '000000', 'admin', 'web', 'pc', '219.142.141.194', '中国|北京|北京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 16:22:21');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018602589315272705, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-03 16:28:56');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018605159794479105, '000000', 'admin', 'web', 'pc', '222.168.89.242', '中国|吉林省|长春市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 16:39:09');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018609628598898690, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-03 16:56:54');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018609656977559553, '000000', 'admin', 'web', 'pc', '121.35.46.195', '中国|广东省|深圳市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 16:57:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018610104228777985, '000000', 'admin', 'web', 'pc', '115.236.69.226', '中国|浙江省|杭州市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 16:58:47');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018610284827119618, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 16:59:31');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018611513854660610, '000000', 'admin ', 'web', 'pc', '222.90.12.243', '中国|陕西省|西安市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '1', 'Password input error 1 times', '2026-02-03 17:04:24');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018611519181426690, '000000', 'admin ', 'web', 'pc', '222.90.12.243', '中国|陕西省|西安市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '1', 'Password input error 2 times', '2026-02-03 17:04:25');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018611644431732738, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 17:04:55');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018611724014456833, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 17:05:14');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018612993819021314, '000000', 'admin', 'web', 'pc', '223.160.130.33', '中国|北京|北京市|广电', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-03 17:10:16');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018614987845668866, '000000', 'admin ', 'web', 'pc', '222.90.12.243', '中国|陕西省|西安市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '1', 'Password input error 1 times', '2026-02-03 17:18:12');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018615005365276674, '000000', 'admin', 'web', 'pc', '222.90.12.243', '中国|陕西省|西安市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 17:18:16');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018615245262688258, '000000', 'admin', 'web', 'pc', '61.184.94.104', '中国|湖北省|十堰市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 17:19:13');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018615451685359617, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 17:20:02');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018615588662939650, '000000', 'admin', 'web', 'pc', '183.241.255.169', '中国|北京|北京市|移动', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 17:20:35');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018615733957824514, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 17:21:10');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018618361873829889, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 17:31:36');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018619606214774785, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 17:36:33');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018621042193469441, '000000', 'admin', 'web', 'pc', '27.227.167.177', '中国|甘肃省|兰州市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 17:42:15');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018623610328059905, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 17:52:28');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018623677315289089, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-03 17:52:44');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018623870995664897, '000000', 'admin', 'web', 'pc', '59.57.134.162', '中国|福建省|厦门市|电信', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-03 17:53:30');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018624044400775169, '000000', 'admin', 'web', 'pc', '39.162.49.29', '中国|河南省|郑州市|移动', 'Chrome', 'Windows 10 or Windows Server 2016', '1', 'Password input error 1 times', '2026-02-03 17:54:11');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018624054987198465, '000000', 'admin', 'web', 'pc', '39.162.49.29', '中国|河南省|郑州市|移动', 'Chrome', 'Windows 10 or Windows Server 2016', '1', 'Password input error 2 times', '2026-02-03 17:54:14');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018624077900681218, '000000', 'admin', 'web', 'pc', '39.162.49.29', '中国|河南省|郑州市|移动', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 17:54:19');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018625637493903362, '000000', 'admin', 'web', 'pc', '114.253.10.114', '中国|北京|北京市|联通', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-03 18:00:31');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018625743718846465, '000000', 'admin', 'web', 'pc', '111.198.137.189', '中国|北京|北京市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 18:00:56');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018625749372768258, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-03 18:00:58');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018625771480944641, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 18:01:03');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018625852410040321, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '退出成功', '2026-02-03 18:01:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018625877240320002, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-03 18:01:28');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018627882260238337, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 18:09:26');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018628150670528513, '000000', 'admin', 'web', 'pc', '114.253.10.114', '中国|北京|北京市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 18:10:30');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018672955786137601, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 21:08:32');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018689142062452738, '000000', 'admin ', 'web', 'pc', '221.205.106.23', '中国|山西省|太原市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '1', 'Password input error 1 times', '2026-02-03 22:12:52');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018689178624200706, '000000', 'admin', 'web', 'pc', '221.205.106.23', '中国|山西省|太原市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 22:13:00');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018689429430996993, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 22:14:00');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018693323410247681, '000000', 'admin', 'web', 'pc', '140.206.143.69', '中国|上海|上海市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 22:29:28');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018694798878314498, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'OSX', '0', '登录成功', '2026-02-03 22:35:20');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018710386166075394, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 23:37:17');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018715692476534785, '000000', 'admin', 'web', 'pc', '113.248.49.12', '中国|重庆|重庆市|电信', 'Firefox', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-03 23:58:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018715768049504257, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Firefox', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-03 23:58:40');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018745408302485505, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 01:56:26');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018745476409593857, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-02-04 01:56:43');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018852568944480258, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-04 09:02:16');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018852760607395842, '000000', 'admin', 'web', 'pc', '39.170.0.61', '中国|浙江省|杭州市|移动', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-04 09:03:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018855559080579073, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:14:08');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018856680327090177, '000000', 'admin', 'web', 'pc', '180.201.162.34', '中国|山东省|青岛市|教育网', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 09:18:36');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018856825580032001, '000000', 'admin', 'web', 'pc', '60.10.20.65', '中国|河北省|廊坊市|联通', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 09:19:10');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018857125414047745, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:20:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018857183823925249, '000000', 'admin', 'web', 'pc', '114.253.10.114', '中国|北京|北京市|联通', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-04 09:20:36');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018857493854294017, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-04 09:21:50');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018860201030062082, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:32:35');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018860399353532418, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:33:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018860419058372609, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:33:27');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018862938719391745, '000000', 'admin', 'web', 'pc', '113.65.15.168', '中国|广东省|广州市|电信', 'Firefox', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 09:43:28');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018863017278705665, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Firefox', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:43:47');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018863424306548737, '000000', 'admin', 'web', 'pc', '114.253.10.114', '中国|北京|北京市|联通', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-04 09:45:24');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018864538947031042, '000000', 'admin', 'web', 'pc', '114.224.36.132', '中国|江苏省|无锡市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 09:49:49');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018864646790975490, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:50:15');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018864913326411777, '000000', 'admin', 'web', 'pc', '14.220.151.192', '中国|广东省|东莞市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 09:51:19');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018865224053035009, '000000', 'admin', 'web', 'pc', '8.220.210.62', '中国|阿里巴巴', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 09:52:33');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018865354428780546, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:53:04');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018865432400891905, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:53:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018865469902163970, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-02-04 09:53:31');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018865485031018498, '154726', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:53:35');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018865600516984833, '154726', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-02-04 09:54:03');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018865826023739394, '000000', 'admin', 'web', 'pc', '113.104.237.0', '中国|广东省|深圳市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 09:54:56');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018865939207032834, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 09:55:23');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018868158258089986, '000000', 'admin', 'web', 'pc', '39.170.71.231', '中国|浙江省|杭州市|移动', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 10:04:12');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018868610617970690, '000000', 'admin', 'web', 'pc', '219.152.39.216', '中国|重庆|重庆市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 10:06:00');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018870568124813313, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 10:13:47');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018882173784952834, '000000', 'admin', 'web', 'pc', '220.196.184.73', '中国|上海|上海市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '1', 'Password input error 1 times', '2026-02-04 10:59:54');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018882179346599938, '000000', 'admin', 'web', 'pc', '220.196.184.73', '中国|上海|上海市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 10:59:55');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018882457076633602, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 11:01:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018882886007132162, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 11:02:44');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018883626641526786, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Firefox', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 11:05:40');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018885851870793730, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 11:14:31');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018887170559971330, '000000', 'admin', 'web', 'pc', '114.222.24.76', '中国|江苏省|南京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 11:19:45');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018887913358626817, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 11:22:42');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018889490362404865, '000000', 'admin', 'web', 'pc', '124.165.224.106', '中国|山西省|吕梁市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 11:28:58');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018891240427360257, '000000', 'admin', 'web', 'pc', '163.142.243.239', '中国|广东省|佛山市|联通', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 11:35:56');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018895246847512578, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 11:51:51');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018897167293485058, '000000', 'admin', 'web', 'pc', '113.57.48.151', '中国|湖北省|武汉市|联通', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 11:59:29');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018898894247825410, '000000', 'admin', 'web', 'pc', '114.254.1.2', '中国|北京|北京市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 12:06:20');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018899094601338882, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 12:07:08');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018916480444403714, '000000', 'admin', 'web', 'pc', '219.143.206.106', '中国|北京|北京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 13:16:13');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018916794622939137, '000000', 'admin', 'web', 'pc', '116.252.72.152', '中国|广西|南宁市|电信', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-04 13:17:28');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018917938204119041, '000000', 'admin', 'web', 'pc', '219.143.206.106', '中国|北京|北京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 13:22:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018918018160136193, '000000', 'admin', 'web', 'pc', '218.240.181.5', '中国|北京|北京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 13:22:20');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018918326378565634, '000000', 'admin', 'web', 'pc', '218.240.181.5', '中国|北京|北京市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 13:23:33');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018920804255928322, '000000', 'admin', 'web', 'pc', '219.143.206.106', '中国|北京|北京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 13:33:24');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018921548166074369, '000000', 'admin', 'web', 'pc', '219.143.206.106', '中国|北京|北京市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 13:36:21');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018922995012210690, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Quark', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 13:42:06');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018926354117038082, '000000', 'admin', 'web', 'pc', '8.220.210.62', '中国|阿里巴巴', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 13:55:27');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018927011129593858, '000000', 'admin', 'web', 'pc', '60.190.252.115', '中国|浙江省|杭州市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 13:58:04');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018927468619108353, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 13:59:53');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018927515784056834, '000000', 'admin', 'web', 'pc', '14.111.243.245', '中国|重庆|重庆市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '1', 'Password input error 1 times', '2026-02-04 14:00:04');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018927562521186305, '000000', 'admin', 'web', 'pc', '14.111.243.245', '中国|重庆|重庆市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 14:00:15');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018929485043339265, '000000', 'admin', 'web', 'pc', '124.114.150.254', '中国|陕西省|西安市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 14:07:54');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018931277877612545, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 14:15:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018933698284621826, '000000', 'admin', 'web', 'pc', '222.35.11.50', '中国|北京|北京市|铁通', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 14:24:38');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018933723182010369, '000000', 'admin', 'web', 'pc', '183.162.217.80', '中国|安徽省|蚌埠市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 14:24:44');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018933918011625473, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 14:25:31');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018935595993272322, '000000', 'admin', 'web', 'pc', '122.225.239.202', '中国|浙江省|嘉兴市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 14:32:11');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018937821423865857, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 14:41:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018938936521527298, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 14:45:27');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018946088032145409, '000000', 'admin', 'web', 'pc', '223.104.202.88', '中国|陕西省|咸阳市|移动', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 15:13:52');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018946463766286337, '000000', 'admin', 'web', 'pc', '114.139.48.241', '中国|贵州省|遵义市|电信', 'Chrome', 'OSX', '0', 'Login successful', '2026-02-04 15:15:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018948323336130562, '000000', 'admin', 'web', 'pc', '113.207.43.98', '中国|重庆|重庆市|联通', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 15:22:45');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018948839856279554, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 15:24:48');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018950286861799426, '000000', 'admin', 'web', 'pc', '112.224.162.237', '中国|山东省|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 15:30:33');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018950904724721665, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 15:33:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018950963323342849, '000000', 'admin', 'web', 'pc', '182.132.201.232', '中国|四川省|眉山市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 15:33:15');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018950986727559170, '000000', 'admin', 'web', 'pc', '111.8.48.211', '中国|湖南省|长沙市|移动', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 15:33:20');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018951848111771650, '000000', 'admin', 'web', 'pc', '219.138.228.254', '中国|湖北省|鄂州市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 15:36:46');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018952415047454722, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 15:39:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018954681871634433, '000000', 'admin', 'web', 'pc', '123.235.153.115', '中国|山东省|青岛市|联通', 'MicroMessenger', 'OSX', '0', 'Login successful', '2026-02-04 15:48:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018959635684397057, '000000', 'admin', 'web', 'pc', '58.56.198.114', '中国|山东省|泰安市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:07:42');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018961274495438849, '000000', 'admin', 'web', 'pc', '120.197.21.117', '中国|广东省|广州市|移动', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:14:13');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018961365956431873, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:14:35');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018961449058177025, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:14:55');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018961841162686465, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:16:28');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018961976873586689, '000000', 'admin', 'web', 'pc', '36.112.184.220', '中国|北京|北京市|电信', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:17:00');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018962175272554497, '000000', 'admin', 'web', 'pc', '39.144.212.209', '中国|移动', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:17:48');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018962369447858177, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:18:34');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018963149303189505, '000000', 'admin', 'web', 'pc', '112.97.202.126', '中国|广东省|东莞市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:21:40');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018963440199143425, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:22:49');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018966187199827970, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-02-04 16:33:44');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018966195156422657, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:33:46');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018967272132055041, '000000', 'admin', 'web', 'pc', '121.8.154.218', '中国|广东省|广州市|电信', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:38:03');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018967970655637505, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:40:49');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018968030697099265, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:41:04');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018970160656945154, '000000', 'admin', 'web', 'pc', '183.209.146.73', '中国|江苏省|南京市|移动', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:49:32');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018970873374052354, '000000', 'admin', 'web', 'pc', '60.1.71.196', '中国|河北省|石家庄市|联通', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:52:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018970947504181249, '000000', 'admin', 'web', 'pc', '223.88.78.216', '中国|河南省|郑州市|移动', 'Chrome', 'Windows 10 or Windows Server 2016', '0', 'Login successful', '2026-02-04 16:52:39');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018971098872418305, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 16:53:15');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018978580902580226, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 17:22:59');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018978998839808002, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 17:24:39');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018984098123616257, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 17:44:55');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018988627267293186, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 18:02:54');
|
||||
INSERT INTO `sys_logininfor` VALUES (2018990733382520834, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 18:11:17');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019029939576246274, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 20:47:04');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019040366678904834, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 21:28:30');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019057692774109186, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 22:37:21');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019057715637260289, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-04 22:37:26');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019113073072279553, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'OSX', '0', '登录成功', '2026-02-05 02:17:25');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019203832710565889, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 08:18:03');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019210865128116226, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-05 08:46:00');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019213517912150018, '154726', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 08:56:33');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019217038615121921, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 09:10:32');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019219134710157314, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Linux', '0', '登录成功', '2026-02-05 09:18:52');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019222079201157121, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 09:30:34');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019223047400198146, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'OSX', '0', '登录成功', '2026-02-05 09:34:25');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019224288503140354, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 09:39:20');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019224998575153153, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-02-05 09:42:10');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019225059417726977, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 09:42:24');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019240817392693249, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-05 10:45:01');
|
||||
INSERT INTO `sys_logininfor` VALUES (2019447979716972545, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-06 00:28:13');
|
||||
INSERT INTO `sys_logininfor` VALUES (2026536636865163265, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 13:56:00');
|
||||
INSERT INTO `sys_logininfor` VALUES (2026556949535502337, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 15:16:43');
|
||||
INSERT INTO `sys_logininfor` VALUES (2026578433112911874, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 16:42:05');
|
||||
INSERT INTO `sys_logininfor` VALUES (2026638437400518657, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 20:40:31');
|
||||
INSERT INTO `sys_logininfor` VALUES (2026647463072952321, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 21:16:23');
|
||||
INSERT INTO `sys_logininfor` VALUES (2026653919016968194, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-02-25 21:42:02');
|
||||
INSERT INTO `sys_logininfor` VALUES (2026654082020204546, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 21:42:41');
|
||||
INSERT INTO `sys_logininfor` VALUES (2026654455514587138, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-25 21:44:10');
|
||||
INSERT INTO `sys_logininfor` VALUES (2027260957187186689, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-02-27 13:54:12');
|
||||
INSERT INTO `sys_logininfor` VALUES (2030617171346399233, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-08 20:10:35');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033083137191841794, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 15:29:27');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033102367094214657, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 16:45:52');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033116897354551298, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 17:43:36');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033133175565836289, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 18:48:17');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033167392953675778, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 21:04:16');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033168637663719425, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 21:09:12');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033170263812157441, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-03-15 21:15:40');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033170654197002242, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 21:17:13');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033170805703651330, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '退出成功', '2026-03-15 21:17:49');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033170821767835650, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 21:17:53');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033170964009267201, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 21:18:27');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033197762549985282, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 23:04:56');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033200372921217025, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '1', '密码输入错误1次', '2026-03-15 23:15:19');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033200386498179073, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 23:15:22');
|
||||
INSERT INTO `sys_logininfor` VALUES (2033210457315696642, '000000', 'admin', 'pc', 'pc', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2026-03-15 23:55:23');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_menu
|
||||
@@ -2948,7 +3176,7 @@ CREATE TABLE `sys_user` (
|
||||
-- ----------------------------
|
||||
-- Records of sys_user
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_user` VALUES (1, '000000', 103, 'admin', 'admin', 'sys_user', 'ageerle@163.com', '15888888888', '1', NULL, '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', '2026-05-19 23:16:57', 103, 1, '2026-02-05 09:22:12', -1, '2026-05-19 23:16:57', '管理员', NULL, 0.00);
|
||||
INSERT INTO `sys_user` VALUES (1, '000000', 103, 'admin', 'admin', 'sys_user', 'ageerle@163.com', '15888888888', '1', NULL, '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', '2026-03-15 23:55:23', 103, 1, '2026-02-05 09:22:12', -1, '2026-03-15 23:55:23', '管理员', NULL, 0.00);
|
||||
INSERT INTO `sys_user` VALUES (3, '000000', 108, 'test', '本部门及以下 密码666666', 'sys_user', '', '', '0', NULL, '$2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne', '0', '0', '127.0.0.1', '2026-02-05 09:22:12', 103, 1, '2026-02-05 09:22:12', 3, '2026-02-05 09:22:12', NULL, NULL, 0.00);
|
||||
INSERT INTO `sys_user` VALUES (4, '000000', 102, 'test1', '仅本人 密码666666', 'sys_user', '', '', '0', NULL, '$2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne', '0', '0', '127.0.0.1', '2026-02-05 09:22:12', 103, 1, '2026-02-05 09:22:12', 4, '2026-02-05 09:22:12', NULL, NULL, 0.00);
|
||||
|
||||
|
||||
320
ruoyi-admin/src/main/resources/static/report/index.html
Normal file
320
ruoyi-admin/src/main/resources/static/report/index.html
Normal file
@@ -0,0 +1,320 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Report Agent</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--primary:#6366f1;--primary-hover:#4f46e5;--bg:#f8f9fa;--card:#fff;--border:#e2e8f0;--text:#1e293b;--text-muted:#64748b;--success:#22c55e;--warning:#f59e0b;--error:#ef4444;--shadow:0 1px 3px rgba(0,0,0,0.1);--radius:8px}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;min-height:100vh}
|
||||
.app{max-width:1200px;margin:0 auto;padding:20px}
|
||||
.header{text-align:center;margin-bottom:32px}
|
||||
.header h1{font-size:28px;background:linear-gradient(135deg,var(--primary),#a855f7);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.header p{color:var(--text-muted);margin-top:4px}
|
||||
.steps{display:flex;gap:8px;margin-bottom:24px;align-items:center}
|
||||
.step{flex:1;padding:12px 16px;background:var(--card);border:2px solid var(--border);border-radius:var(--radius);text-align:center;transition:all 0.3s}
|
||||
.step.active{border-color:var(--primary);background:#eef2ff}
|
||||
.step.done{border-color:var(--success);background:#f0fdf4}
|
||||
.step h3{font-size:14px;color:var(--text-muted);margin-bottom:2px}
|
||||
.step .num{font-size:24px;font-weight:700}
|
||||
.card{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);padding:24px;margin-bottom:20px}
|
||||
.card h2{font-size:18px;margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||
textarea{width:100%;padding:12px;border:1px solid var(--border);border-radius:var(--radius);resize:vertical;font-size:14px;line-height:1.5;font-family:inherit}
|
||||
textarea:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(99,102,241,0.1)}
|
||||
.model-select{display:flex;align-items:center;gap:12px;margin-bottom:16px}
|
||||
.model-select label{font-weight:500}
|
||||
.model-select select{padding:8px 12px;border:1px solid var(--border);border-radius:var(--radius);font-size:14px}
|
||||
.model-select input[type=number]{width:80px}
|
||||
.btn{padding:10px 24px;border:none;border-radius:var(--radius);font-size:14px;font-weight:600;cursor:pointer;transition:all 0.2s;display:inline-flex;align-items:center;gap:8px}
|
||||
.btn-primary{background:var(--primary);color:#fff}
|
||||
.btn-primary:hover{background:var(--primary-hover)}
|
||||
.btn-success{background:var(--success);color:#fff}
|
||||
.btn-success:hover{background:#16a34a}
|
||||
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
|
||||
.btn-outline:hover{border-color:var(--primary);color:var(--primary)}
|
||||
.btn:disabled{opacity:0.6;cursor:not-allowed}
|
||||
.btn-group{display:flex;gap:8px;margin-top:16px}
|
||||
.sql-preview{background:#1e293b;color:#e2e8f0;padding:16px;border-radius:var(--radius);font-family:monospace;white-space:pre-wrap;overflow:auto;max-height:300px;line-height:1.5}
|
||||
.sql-block{background:#1e293b;color:#e2e8f0;padding:12px 16px;border-radius:var(--radius);margin:12px 0;font-family:monospace;overflow-x:auto}
|
||||
.sql-block .label{color:#94a3b8;font-size:12px;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px}
|
||||
.status-msg{padding:12px 16px;border-radius:var(--radius);margin:12px 0;font-size:14px;display:flex;align-items:center;gap:8px}
|
||||
.status-info{background:#e0f2fe;color:#0369a1}
|
||||
.status-success{background:#dcfce7;color:#15803d}
|
||||
.status-error{background:#fee2e2;color:#dc2626}
|
||||
.loader{display:inline-block;width:16px;height:16px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spin 0.6s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
#report-container{display:none}
|
||||
#report-container iframe{width:100%;height:70vh;border:1px solid var(--border);border-radius:var(--radius)}
|
||||
.section{display:none}
|
||||
.section.active{display:block}
|
||||
.edit-bar{display:flex;gap:8px;margin-top:16px;align-items:center}
|
||||
.edit-bar input{flex:1;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius);font-size:14px}
|
||||
.edit-bar input:focus{outline:none;border-color:var(--primary)}
|
||||
.fade-in{animation:fadeIn 0.3s ease-in}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<div class="header">
|
||||
<h1>AI Report Agent</h1>
|
||||
<p>自然语言描述需求,自动生成数据报表</p>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<div class="step active" id="step-1"><h3>Step 1</h3><div class="num" data-step="1">描述需求</div></div>
|
||||
<div class="step" id="step-2"><h3>Step 2</h3><div class="num" data-step="2">确认SQL</div></div>
|
||||
<div class="step" id="step-3"><h3>Step 3</h3><div class="num" data-step="3">查看报表</div></div>
|
||||
</div>
|
||||
|
||||
<div class="section active fade-in" id="section-prompt">
|
||||
<div class="card">
|
||||
<h2>输入报表需求</h2>
|
||||
<div class="model-select">
|
||||
<label>模型:</label>
|
||||
<select id="model-name" required>
|
||||
<option value="">-- 选择模型 --</option>
|
||||
</select>
|
||||
<label>最大行数:</label>
|
||||
<input type="number" id="max-rows" value="100" min="1" max="1000">
|
||||
</div>
|
||||
<textarea id="user-prompt" rows="4" placeholder="例如:查询最近30天注册的用户数量,按日期分组统计"></textarea>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="btn-generate"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section fade-in" id="section-preview">
|
||||
<div class="card">
|
||||
<h2>SQL 预览与确认</h2>
|
||||
<div id="preview-info"></div>
|
||||
<div class="sql-preview" id="sql-code"></div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success" id="btn-execute" disabled></button>
|
||||
<button class="btn btn-outline" id="btn-modify" disabled></button>
|
||||
<button class="btn btn-outline" id="btn-cancel"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section fade-in" id="section-report">
|
||||
<div id="report-container">
|
||||
<iframe id="report-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
||||
</div>
|
||||
<div class="edit-bar">
|
||||
<input type="text" id="edit-prompt" placeholder="输入修改要求,例如:把表格改成柱状图展示">
|
||||
<button class="btn btn-primary" id="btn-refine"></button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline" id="btn-back"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-area"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '';
|
||||
let currentReport = {
|
||||
model: '',
|
||||
title: '',
|
||||
summary: '',
|
||||
sql: '',
|
||||
html: '',
|
||||
queryResult: ''
|
||||
};
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/chat/model/list');
|
||||
const data = await res.json();
|
||||
const select = document.getElementById('model-name');
|
||||
if (data.code === 200 && data.rows) {
|
||||
data.rows.forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.modelName || m.name;
|
||||
opt.textContent = m.modelName || m.name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('无法加载模型列表,请手动填写', e);
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(msg, type) {
|
||||
const area = document.getElementById('status-area');
|
||||
area.innerHTML = '<div class="status-msg status-' + (type || 'info') + '">' + msg + '</div>';
|
||||
setTimeout(() => area.innerHTML = '', 5000);
|
||||
}
|
||||
|
||||
function setLoading(btn, loading) {
|
||||
if (loading) {
|
||||
btn.disabled = true;
|
||||
btn._orig = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="loader"></span> 处理中...';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = btn._orig || '';
|
||||
}
|
||||
}
|
||||
|
||||
function setStep(s) {
|
||||
document.querySelectorAll('.step').forEach(el => {
|
||||
const n = parseInt(el.dataset.step || el.querySelector('.num').dataset.step);
|
||||
el.classList.remove('active', 'done');
|
||||
if (n === s) el.classList.add('active');
|
||||
else if (n < s) el.classList.add('done');
|
||||
});
|
||||
document.querySelectorAll('.section').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById(['section-prompt','section-preview','section-report'][s - 1]).classList.add('active');
|
||||
}
|
||||
|
||||
// Step 1: Generate SQL
|
||||
document.getElementById('btn-generate').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-generate');
|
||||
const model = document.getElementById('model-name').value;
|
||||
const prompt = document.getElementById('user-prompt').value.trim();
|
||||
const maxRows = parseInt(document.getElementById('max-rows').value) || 100;
|
||||
|
||||
if (!model) { showStatus('请选择或填写模型名称', 'error'); return; }
|
||||
if (!prompt) { showStatus('请输入报表需求', 'error'); return; }
|
||||
|
||||
setLoading(btn, true);
|
||||
showStatus('正在生成 SQL 预览...', 'info');
|
||||
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/chat/report/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model, prompt, maxRows })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code !== 200 || !data.data) throw new Error(data.msg || '生成失败');
|
||||
|
||||
currentReport.model = model;
|
||||
currentReport.title = data.data.title;
|
||||
currentReport.summary = data.data.summary;
|
||||
currentReport.sql = data.data.sql;
|
||||
currentReport.html = data.data.html || '';
|
||||
currentReport.queryResult = data.data.queryResult || '';
|
||||
|
||||
document.getElementById('preview-info').innerHTML =
|
||||
'<p><strong>报表标题:</strong> ' + escapeHtml(currentReport.title) + '</p>' +
|
||||
'<p><strong>摘要:</strong> ' + escapeHtml(currentReport.summary) + '</p>';
|
||||
document.getElementById('sql-code').textContent = currentReport.sql;
|
||||
document.getElementById('btn-execute').disabled = false;
|
||||
document.getElementById('btn-modify').disabled = false;
|
||||
setStep(2);
|
||||
showStatus('SQL 生成成功,请确认后执行', 'success');
|
||||
} catch (e) {
|
||||
showStatus('生成失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: Execute
|
||||
document.getElementById('btn-execute').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-execute');
|
||||
setLoading(btn, true);
|
||||
showStatus('正在执行查询并生成报表...', 'info');
|
||||
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/chat/report/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: currentReport.model,
|
||||
title: currentReport.title,
|
||||
summary: currentReport.summary,
|
||||
sql: currentReport.sql
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code !== 200 || !data.data) throw new Error(data.msg || '执行失败');
|
||||
|
||||
currentReport.html = data.data.html;
|
||||
currentReport.queryResult = data.data.queryResult;
|
||||
|
||||
const container = document.getElementById('report-container');
|
||||
const frame = document.getElementById('report-frame');
|
||||
container.style.display = 'block';
|
||||
frame.contentWindow.document.open();
|
||||
frame.contentWindow.document.write(currentReport.html);
|
||||
frame.contentWindow.document.close();
|
||||
|
||||
setStep(3);
|
||||
showStatus('报表生成成功', 'success');
|
||||
} catch (e) {
|
||||
showStatus('执行失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3: Refine
|
||||
document.getElementById('btn-refine').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-refine');
|
||||
const prompt = document.getElementById('edit-prompt').value.trim();
|
||||
if (!prompt) { showStatus('请输入修改要求', 'error'); return; }
|
||||
|
||||
setLoading(btn, true);
|
||||
showStatus('正在优化报表...', 'info');
|
||||
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/chat/report/refine', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: currentReport.model,
|
||||
prompt,
|
||||
html: currentReport.html,
|
||||
dataContext: currentReport.queryResult
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code !== 200 || !data.data) throw new Error(data.msg || '修改失败');
|
||||
|
||||
currentReport.html = data.data.html;
|
||||
currentReport.summary = data.data.summary;
|
||||
|
||||
const frame = document.getElementById('report-frame');
|
||||
frame.contentWindow.document.open();
|
||||
frame.contentWindow.document.write(currentReport.html);
|
||||
frame.contentWindow.document.close();
|
||||
|
||||
document.getElementById('edit-prompt').value = '';
|
||||
showStatus('报表已更新', 'success');
|
||||
} catch (e) {
|
||||
showStatus('修改失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Back button
|
||||
document.getElementById('btn-cancel').addEventListener('click', () => { setStep(1); showStatus('已取消', 'info'); });
|
||||
document.getElementById('btn-back').addEventListener('click', () => { setStep(2); });
|
||||
document.getElementById('btn-modify').addEventListener('click', () => { setStep(1); document.getElementById('user-prompt').value = document.getElementById('edit-prompt').value || ''; });
|
||||
|
||||
function escapeHtml(s) {
|
||||
if (!s) return '';
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// Init
|
||||
loadModels();
|
||||
document.getElementById('btn-generate').textContent = '生成 SQL 预览';
|
||||
document.getElementById('btn-execute').textContent = '确认执行并生成报表';
|
||||
document.getElementById('btn-cancel').textContent = '返回修改';
|
||||
document.getElementById('btn-modify').textContent = '重新编辑需求';
|
||||
document.getElementById('btn-refine').textContent = '执行修改';
|
||||
document.getElementById('btn-back').textContent = '返回查看 SQL';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.ruoyi.agent;
|
||||
|
||||
import dev.langchain4j.agentic.Agent;
|
||||
import dev.langchain4j.service.SystemMessage;
|
||||
import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
|
||||
/**
|
||||
* 通用 Coding Agent
|
||||
* 使用任务规划、文件操作与受控命令执行能力完成开发任务
|
||||
*/
|
||||
public interface CodingAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
你是一个通用 coding agent,负责把用户的软件开发请求落地为可执行改动。
|
||||
|
||||
必须遵循:
|
||||
1. 先调用 task_planner 输出结构化计划与 approval token
|
||||
2. 获得用户确认后再执行 create_file、edit_file、run_command
|
||||
3. run_command 必须使用 task_planner 返回的 approval token 和 approval scope
|
||||
4. 优先最小改动,保持可验证、可回滚
|
||||
5. 输出最终变更摘要、验证结果与下一步建议
|
||||
""")
|
||||
@UserMessage("{{query}}")
|
||||
@Agent("通用 Coding Agent,支持任务规划、文件操作与受控命令执行")
|
||||
String execute(@V("query") String query);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.ruoyi.controller.chat;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.domain.dto.request.AiReportExecuteRequest;
|
||||
import org.ruoyi.domain.dto.request.AiReportGenerateRequest;
|
||||
import org.ruoyi.domain.dto.request.AiReportRefineRequest;
|
||||
import org.ruoyi.domain.dto.response.AiReportResponse;
|
||||
import org.ruoyi.service.report.IAiReportService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/chat/report")
|
||||
public class AiReportController {
|
||||
|
||||
private final IAiReportService aiReportService;
|
||||
|
||||
@PostMapping("/generate")
|
||||
public R<AiReportResponse> generate(@RequestBody @Valid AiReportGenerateRequest request) {
|
||||
return R.ok(aiReportService.generate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/execute")
|
||||
public R<AiReportResponse> execute(@RequestBody @Valid AiReportExecuteRequest request) {
|
||||
return R.ok(aiReportService.execute(request));
|
||||
}
|
||||
|
||||
@PostMapping("/refine")
|
||||
public R<AiReportResponse> refine(@RequestBody @Valid AiReportRefineRequest request) {
|
||||
return R.ok(aiReportService.refine(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AiReportExecuteRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "报表标题不能为空")
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "报表摘要不能为空")
|
||||
private String summary;
|
||||
|
||||
@NotBlank(message = "SQL 不能为空")
|
||||
private String sql;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AiReportGenerateRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "报表需求不能为空")
|
||||
private String prompt;
|
||||
|
||||
private Integer maxRows;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AiReportRefineRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "编辑提示词不能为空")
|
||||
private String prompt;
|
||||
|
||||
@NotBlank(message = "当前报表 HTML 不能为空")
|
||||
private String html;
|
||||
|
||||
private String dataContext;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.ruoyi.domain.dto.response;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class AiReportResponse {
|
||||
|
||||
private String sql;
|
||||
|
||||
private String title;
|
||||
|
||||
private String summary;
|
||||
|
||||
private String queryResult;
|
||||
|
||||
private String html;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public enum ChatModeType {
|
||||
DEEP_SEEK("deepseek", "深度求索"),
|
||||
QIAN_WEN("qianwen", "通义千问"),
|
||||
OPEN_AI("openai", "openai"),
|
||||
ATLAS("atlas", "Atlas Cloud"),
|
||||
PPIO("ppio", "ppio"),
|
||||
CUSTOM_API("custom_api", "自定义API"),
|
||||
MINIMAX("minimax", "MiniMax"),
|
||||
XIAOMI("xiaomi", "小米MiMo");
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.ruoyi.mcp.tools;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 执行审批令牌存储
|
||||
* 使用内存存储短期有效令牌,用于“先计划后执行”闭环
|
||||
*/
|
||||
public final class ApprovalTokenStore {
|
||||
|
||||
private static final Map<String, TokenRecord> TOKENS = new ConcurrentHashMap<>();
|
||||
|
||||
private ApprovalTokenStore() {
|
||||
}
|
||||
|
||||
public static String issue(String goal, long ttlSeconds) {
|
||||
cleanupExpired();
|
||||
String token = UUID.randomUUID().toString();
|
||||
long expireAt = Instant.now().getEpochSecond() + Math.max(60, ttlSeconds);
|
||||
TOKENS.put(token, new TokenRecord(goal == null ? "" : goal, expireAt));
|
||||
return token;
|
||||
}
|
||||
|
||||
public static boolean consume(String token, String scope) {
|
||||
if (token == null || token.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
TokenRecord record = TOKENS.remove(token.trim());
|
||||
if (record == null) {
|
||||
return false;
|
||||
}
|
||||
if (record.expireAtEpochSeconds() < Instant.now().getEpochSecond()) {
|
||||
return false;
|
||||
}
|
||||
String normalizedScope = scope == null ? "" : scope.trim();
|
||||
return record.goal().equals(normalizedScope);
|
||||
}
|
||||
|
||||
private static void cleanupExpired() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
TOKENS.entrySet().removeIf(entry -> entry.getValue().expireAtEpochSeconds() < now);
|
||||
}
|
||||
|
||||
private record TokenRecord(String goal, long expireAtEpochSeconds) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.ruoyi.mcp.tools;
|
||||
|
||||
import dev.langchain4j.agent.tool.Tool;
|
||||
import org.ruoyi.mcp.service.core.BuiltinToolProvider;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 创建文件工具
|
||||
* 在工作区内创建新文件,可选择是否覆盖已有文件
|
||||
*/
|
||||
@Component
|
||||
public class CreateFileTool implements BuiltinToolProvider {
|
||||
|
||||
public static final String DESCRIPTION = "Creates a new file with provided content. " +
|
||||
"Supports creating parent directories automatically. " +
|
||||
"Use absolute paths within the workspace directory. " +
|
||||
"Set overwrite to true to replace existing file content.";
|
||||
|
||||
private final String rootDirectory;
|
||||
private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
|
||||
|
||||
public CreateFileTool() {
|
||||
this.rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||
}
|
||||
|
||||
@Tool(DESCRIPTION)
|
||||
public String createFile(String filePath, String content, Boolean overwrite) {
|
||||
try {
|
||||
if (filePath == null || filePath.trim().isEmpty()) {
|
||||
return "Error: File path cannot be empty";
|
||||
}
|
||||
|
||||
Path path = Paths.get(filePath);
|
||||
if (!path.isAbsolute()) {
|
||||
return "Error: File path must be absolute: " + filePath;
|
||||
}
|
||||
|
||||
if (!isWithinWorkspace(path)) {
|
||||
return "Error: File path must be within the workspace directory (" + rootDirectory + "): " + filePath;
|
||||
}
|
||||
|
||||
if (Files.exists(path) && Files.isDirectory(path)) {
|
||||
return "Error: Path is a directory, not a file: " + filePath;
|
||||
}
|
||||
|
||||
boolean allowOverwrite = overwrite != null && overwrite;
|
||||
if (Files.exists(path) && !allowOverwrite) {
|
||||
return "Error: File already exists. Set overwrite=true to replace content: " + filePath;
|
||||
}
|
||||
|
||||
Path parent = path.getParent();
|
||||
if (parent != null && !Files.exists(parent)) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
|
||||
String safeContent = content == null ? "" : content;
|
||||
if (allowOverwrite) {
|
||||
Files.writeString(path, safeContent, StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
|
||||
} else {
|
||||
Files.writeString(path, safeContent, StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||
}
|
||||
|
||||
return "Successfully created file: " + getRelativePath(path);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error creating file: {}", filePath, e);
|
||||
return "Error: " + e.getMessage();
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error creating file: {}", filePath, e);
|
||||
return "Error: Unexpected error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolName() {
|
||||
return "create_file";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "创建文件";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return DESCRIPTION;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,10 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 编辑文件工具
|
||||
@@ -75,12 +78,9 @@ public class EditFileTool implements BuiltinToolProvider {
|
||||
|
||||
// 读取原始内容
|
||||
String originalContent = Files.readString(path, StandardCharsets.UTF_8);
|
||||
List<String> originalLines = Arrays.asList(originalContent.split("\n"));
|
||||
|
||||
// 应用diff
|
||||
try {
|
||||
// 这里简化处理,直接用新内容替换
|
||||
// 在实际应用中,可能需要更复杂的diff解析
|
||||
String newContent = applyDiff(originalContent, diff);
|
||||
|
||||
// 写入文件
|
||||
@@ -104,14 +104,100 @@ public class EditFileTool implements BuiltinToolProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的diff应用逻辑
|
||||
* 实际应用中可能需要使用更复杂的diff解析器
|
||||
* 仅支持 unified diff(包含 @@ hunk 头)
|
||||
*/
|
||||
private String applyDiff(String originalContent, String diff) {
|
||||
// 这里简化处理,实际应用中需要解析diff格式
|
||||
// 目前将diff作为新内容直接替换
|
||||
// 可以考虑使用jgit等库来解析 unified diff 格式
|
||||
return diff;
|
||||
List<String> originalLines = new ArrayList<>(Arrays.asList(originalContent.split("\n", -1)));
|
||||
List<String> diffLines = Arrays.asList(diff.split("\n", -1));
|
||||
|
||||
int i = 0;
|
||||
while (i < diffLines.size()) {
|
||||
String line = diffLines.get(i);
|
||||
|
||||
if (line.startsWith("---") || line.startsWith("+++")) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.startsWith("@@")) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
HunkHeader header = parseHunkHeader(line);
|
||||
int targetIndex = Math.max(0, header.oldStart - 1);
|
||||
i++;
|
||||
|
||||
while (i < diffLines.size()) {
|
||||
String hunkLine = diffLines.get(i);
|
||||
if (hunkLine.startsWith("@@") || hunkLine.startsWith("---") || hunkLine.startsWith("+++")) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (hunkLine.startsWith("\\ No newline at end of file")) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hunkLine.isEmpty()) {
|
||||
// unified diff 中空内容上下文行会表现为空字符串,视为上下文行
|
||||
ensureExpectedLine(originalLines, targetIndex, "");
|
||||
targetIndex++;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
char op = hunkLine.charAt(0);
|
||||
String content = hunkLine.length() > 1 ? hunkLine.substring(1) : "";
|
||||
switch (op) {
|
||||
case ' ':
|
||||
ensureExpectedLine(originalLines, targetIndex, content);
|
||||
targetIndex++;
|
||||
break;
|
||||
case '-':
|
||||
ensureExpectedLine(originalLines, targetIndex, content);
|
||||
originalLines.remove(targetIndex);
|
||||
break;
|
||||
case '+':
|
||||
originalLines.add(targetIndex, content);
|
||||
targetIndex++;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported diff line: " + hunkLine);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return String.join("\n", originalLines);
|
||||
}
|
||||
|
||||
private void ensureExpectedLine(List<String> lines, int index, String expected) {
|
||||
if (index < 0 || index >= lines.size()) {
|
||||
throw new IllegalArgumentException("Diff out of range at line index: " + index);
|
||||
}
|
||||
String actual = lines.get(index);
|
||||
if (!actual.equals(expected)) {
|
||||
throw new IllegalArgumentException("Diff context mismatch at line " + (index + 1)
|
||||
+ ", expected: [" + expected + "], actual: [" + actual + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private HunkHeader parseHunkHeader(String headerLine) {
|
||||
Pattern p = Pattern.compile("@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@.*");
|
||||
Matcher m = p.matcher(headerLine);
|
||||
if (!m.matches()) {
|
||||
throw new IllegalArgumentException("Invalid unified diff hunk header: " + headerLine);
|
||||
}
|
||||
|
||||
int oldStart = Integer.parseInt(m.group(1));
|
||||
int oldCount = m.group(2) == null ? 1 : Integer.parseInt(m.group(2));
|
||||
int newStart = Integer.parseInt(m.group(3));
|
||||
int newCount = m.group(4) == null ? 1 : Integer.parseInt(m.group(4));
|
||||
return new HunkHeader(oldStart, oldCount, newStart, newCount);
|
||||
}
|
||||
|
||||
private record HunkHeader(int oldStart, int oldCount, int newStart, int newCount) {
|
||||
}
|
||||
|
||||
private boolean isWithinWorkspace(Path filePath) {
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package org.ruoyi.mcp.tools;
|
||||
|
||||
import dev.langchain4j.agent.tool.Tool;
|
||||
import org.ruoyi.mcp.service.core.BuiltinToolProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 受控命令执行工具
|
||||
* 仅允许执行白名单命令,禁止高风险和破坏性命令
|
||||
*/
|
||||
@Component
|
||||
public class RunCommandTool implements BuiltinToolProvider {
|
||||
|
||||
public static final String DESCRIPTION = "Runs a safe whitelisted command in workspace. " +
|
||||
"Only supports non-interactive commands for build/test/git status workflows. " +
|
||||
"Blocks destructive and shell-chaining commands. " +
|
||||
"Use absolute working directory inside workspace. " +
|
||||
"Requires an approval token from task_planner before execution.";
|
||||
|
||||
private static final int DEFAULT_TIMEOUT_SECONDS = 60;
|
||||
private static final int MAX_TIMEOUT_SECONDS = 120;
|
||||
private static final int MAX_OUTPUT_CHARS = 20_000;
|
||||
|
||||
private static final Set<String> ALLOWED_COMMANDS = Set.of(
|
||||
"mvn", "./mvnw", "npm", "pnpm", "yarn", "go", "gradle", "./gradlew", "git"
|
||||
);
|
||||
|
||||
private static final Map<String, Set<String>> ALLOWED_SUBCOMMANDS = Map.of(
|
||||
"git", Set.of("status", "diff", "log", "branch", "checkout", "switch", "add", "commit", "restore"),
|
||||
"mvn", Set.of("compile", "test", "package", "verify"),
|
||||
"./mvnw", Set.of("compile", "test", "package", "verify"),
|
||||
"npm", Set.of("run", "test", "install", "ci"),
|
||||
"pnpm", Set.of("run", "test", "install"),
|
||||
"yarn", Set.of("run", "test", "install"),
|
||||
"go", Set.of("test", "build"),
|
||||
"gradle", Set.of("test", "build"),
|
||||
"./gradlew", Set.of("test", "build")
|
||||
);
|
||||
|
||||
private static final Set<String> BLOCKED_EXACT_TOKENS = Set.of(
|
||||
"rm", "rmdir", "unlink", "shutdown", "reboot", "poweroff", "sudo", "su", "mkfs", "fdisk",
|
||||
"mount", "umount", "iptables", "nft", "useradd", "userdel"
|
||||
);
|
||||
|
||||
private static final List<String> BLOCKED_PHRASES = List.of(
|
||||
"git reset --hard", "git clean -f", "git clean -fd", "git clean -xdf"
|
||||
);
|
||||
|
||||
private final String rootDirectory;
|
||||
private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
|
||||
|
||||
public RunCommandTool() {
|
||||
this.rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||
}
|
||||
|
||||
@Tool(DESCRIPTION)
|
||||
public String runCommand(String command, String workingDirectory, Integer timeoutSeconds, String approvalToken,
|
||||
String approvalScope) {
|
||||
try {
|
||||
if (!ApprovalTokenStore.consume(approvalToken, approvalScope)) {
|
||||
return "Error: Invalid or expired approval token. Please generate a new plan token via task_planner and confirm before execution.";
|
||||
}
|
||||
|
||||
if (command == null || command.trim().isEmpty()) {
|
||||
return "Error: Command cannot be empty";
|
||||
}
|
||||
String normalized = command.trim();
|
||||
|
||||
if (containsShellChaining(normalized)) {
|
||||
return "Error: Shell chaining operators are not allowed";
|
||||
}
|
||||
|
||||
String lower = normalized.toLowerCase();
|
||||
for (String blocked : BLOCKED_PHRASES) {
|
||||
if (lower.contains(blocked)) {
|
||||
return "Error: Command contains blocked token: " + blocked;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> parts = List.of(normalized.split("\\s+"));
|
||||
List<String> lowerParts = parts.stream().map(String::toLowerCase).collect(Collectors.toList());
|
||||
for (String token : lowerParts) {
|
||||
if (BLOCKED_EXACT_TOKENS.contains(token)) {
|
||||
return "Error: Command contains blocked token: " + token;
|
||||
}
|
||||
}
|
||||
|
||||
String binary = parts.get(0);
|
||||
if (!ALLOWED_COMMANDS.contains(binary)) {
|
||||
return "Error: Command is not in allowed list: " + binary;
|
||||
}
|
||||
String subcommandError = validateSubcommand(binary, parts);
|
||||
if (subcommandError != null) {
|
||||
return "Error: " + subcommandError;
|
||||
}
|
||||
|
||||
Path workdir = resolveWorkdir(workingDirectory);
|
||||
if (!Files.exists(workdir) || !Files.isDirectory(workdir)) {
|
||||
return "Error: Working directory not found: " + workdir;
|
||||
}
|
||||
|
||||
int timeout = timeoutSeconds == null ? DEFAULT_TIMEOUT_SECONDS : timeoutSeconds;
|
||||
if (timeout < 1 || timeout > MAX_TIMEOUT_SECONDS) {
|
||||
return "Error: timeoutSeconds must be between 1 and " + MAX_TIMEOUT_SECONDS;
|
||||
}
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(parts);
|
||||
pb.directory(workdir.toFile());
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
Process process = pb.start();
|
||||
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<String> outputFuture = executor.submit(() -> readProcessOutput(process, MAX_OUTPUT_CHARS));
|
||||
|
||||
boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
|
||||
if (!finished) {
|
||||
process.destroyForcibly();
|
||||
process.waitFor(5, TimeUnit.SECONDS);
|
||||
executor.shutdownNow();
|
||||
return "Error: Command timeout after " + timeout + " seconds";
|
||||
}
|
||||
|
||||
String output = getOutputSafely(outputFuture, executor);
|
||||
|
||||
int code = process.exitValue();
|
||||
String relativeWd = getRelativePath(workdir);
|
||||
String summary = "Command: " + normalized + "\nWorkingDirectory: " + relativeWd + "\nExitCode: " + code + "\n\n";
|
||||
return summary + output;
|
||||
} catch (IOException e) {
|
||||
logger.error("Error running command: {}", command, e);
|
||||
return "Error: " + e.getMessage();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return "Error: Command execution interrupted";
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error running command: {}", command, e);
|
||||
return "Error: Unexpected error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveWorkdir(String workingDirectory) throws IOException {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
if (workingDirectory == null || workingDirectory.isBlank()) {
|
||||
return workspaceRoot;
|
||||
}
|
||||
|
||||
Path path = Paths.get(workingDirectory);
|
||||
Path resolved;
|
||||
if (!path.isAbsolute()) {
|
||||
resolved = workspaceRoot.resolve(path).normalize();
|
||||
} else {
|
||||
resolved = path.normalize();
|
||||
}
|
||||
|
||||
if (!resolved.startsWith(workspaceRoot)) {
|
||||
throw new IOException("Working directory must be within workspace: " + workingDirectory);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private boolean containsShellChaining(String command) {
|
||||
return command.contains("&&") || command.contains("||") || command.contains(";") || command.contains("|");
|
||||
}
|
||||
|
||||
private String validateSubcommand(String binary, List<String> parts) {
|
||||
Set<String> allowedSubs = ALLOWED_SUBCOMMANDS.get(binary);
|
||||
if (allowedSubs == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parts.size() < 2) {
|
||||
return "Missing subcommand for " + binary;
|
||||
}
|
||||
|
||||
String sub = parts.get(1);
|
||||
if (sub.startsWith("-")) {
|
||||
return "Flag-only command is not allowed for " + binary;
|
||||
}
|
||||
|
||||
if (!allowedSubs.contains(sub)) {
|
||||
return "Subcommand is not allowed for " + binary + ": " + sub;
|
||||
}
|
||||
|
||||
if (("npm".equals(binary) || "pnpm".equals(binary) || "yarn".equals(binary))
|
||||
&& "install".equals(sub) && !parts.contains("-g") && !parts.contains("--global")) {
|
||||
return "Package install must be global (-g/--global)";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String readProcessOutput(Process process, int maxChars) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (sb.length() + line.length() + 1 <= maxChars) {
|
||||
sb.append(line).append('\n');
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private String getOutputSafely(Future<String> outputFuture, ExecutorService executor)
|
||||
throws InterruptedException, ExecutionException {
|
||||
try {
|
||||
return outputFuture.get(5, TimeUnit.SECONDS);
|
||||
} catch (java.util.concurrent.TimeoutException e) {
|
||||
return "";
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(Duration.ofSeconds(2).toSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private String getRelativePath(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
return workspaceRoot.relativize(filePath.toRealPath()).toString();
|
||||
} catch (Exception e) {
|
||||
return filePath.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolName() {
|
||||
return "run_command";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "执行命令";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return DESCRIPTION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.ruoyi.mcp.tools;
|
||||
|
||||
import dev.langchain4j.agent.tool.Tool;
|
||||
import org.ruoyi.mcp.service.core.BuiltinToolProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 任务规划工具
|
||||
* 将自然语言开发任务转换为可执行的结构化计划
|
||||
*/
|
||||
@Component
|
||||
public class TaskPlannerTool implements BuiltinToolProvider {
|
||||
|
||||
private static final long DEFAULT_APPROVAL_TTL_SECONDS = 30 * 60;
|
||||
|
||||
public static final String DESCRIPTION = "Creates a structured coding task plan from a natural language request. " +
|
||||
"Returns objective, constraints, steps with acceptance criteria, and risk level. " +
|
||||
"Use this tool before executing file or command operations.";
|
||||
|
||||
@Tool(DESCRIPTION)
|
||||
public String planTask(String goal, String constraints, String executionMode) {
|
||||
if (goal == null || goal.trim().isEmpty()) {
|
||||
return "Error: goal cannot be empty";
|
||||
}
|
||||
|
||||
String normalizedGoal = goal.trim();
|
||||
String normalizedConstraints = normalize(constraints);
|
||||
String mode = normalizeMode(executionMode);
|
||||
RiskLevel risk = detectRisk(normalizedGoal, normalizedConstraints);
|
||||
|
||||
List<String> steps = buildSteps(normalizedGoal, mode, risk);
|
||||
List<String> acceptance = buildAcceptance(mode, risk);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# Task Plan\n");
|
||||
sb.append("Objective: ").append(normalizedGoal).append("\n");
|
||||
sb.append("ExecutionMode: ").append(mode).append("\n");
|
||||
sb.append("RiskLevel: ").append(risk.name()).append("\n");
|
||||
if (!normalizedConstraints.isEmpty()) {
|
||||
sb.append("Constraints: ").append(normalizedConstraints).append("\n");
|
||||
}
|
||||
|
||||
sb.append("\nSteps:\n");
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
sb.append(i + 1).append(". ").append(steps.get(i)).append("\n");
|
||||
}
|
||||
|
||||
sb.append("\nAcceptanceCriteria:\n");
|
||||
for (int i = 0; i < acceptance.size(); i++) {
|
||||
sb.append(i + 1).append(". ").append(acceptance.get(i)).append("\n");
|
||||
}
|
||||
|
||||
String approvalToken = ApprovalTokenStore.issue(normalizedGoal, DEFAULT_APPROVAL_TTL_SECONDS);
|
||||
|
||||
sb.append("\nExecutionApproval:\n");
|
||||
sb.append("1. ApprovalToken: ").append(approvalToken).append("\n");
|
||||
sb.append("2. ApprovalScope: ").append(normalizedGoal).append("\n");
|
||||
sb.append("3. TokenTTLSeconds: ").append(DEFAULT_APPROVAL_TTL_SECONDS).append("\n");
|
||||
sb.append("4. Run command tools only after explicit user confirmation\n");
|
||||
|
||||
sb.append("\nSafetyGates:\n");
|
||||
sb.append("1. Only workspace-scoped file operations are allowed\n");
|
||||
sb.append("2. Only whitelisted commands are allowed\n");
|
||||
sb.append("3. Risky actions require explicit user confirmation\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private String normalizeMode(String executionMode) {
|
||||
String mode = normalize(executionMode).toUpperCase();
|
||||
if (!"PLAN_ONLY".equals(mode) && !"PLAN_AND_EXECUTE".equals(mode)) {
|
||||
return "PLAN_ONLY";
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
private RiskLevel detectRisk(String goal, String constraints) {
|
||||
String text = (goal + " " + constraints).toLowerCase();
|
||||
if (containsAny(text, "delete", "drop", "remove", "reset --hard", "force", "rewrite history")) {
|
||||
return RiskLevel.HIGH;
|
||||
}
|
||||
if (containsAny(text, "refactor", "multi-module", "database", "migration", "deploy", "production")) {
|
||||
return RiskLevel.MEDIUM;
|
||||
}
|
||||
return RiskLevel.LOW;
|
||||
}
|
||||
|
||||
private boolean containsAny(String text, String... words) {
|
||||
for (String word : words) {
|
||||
if (text.contains(word)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<String> buildSteps(String goal, String mode, RiskLevel risk) {
|
||||
List<String> steps = new ArrayList<>();
|
||||
steps.add("Clarify target scope and impacted modules for: " + goal);
|
||||
steps.add("Discover relevant files and APIs using list/read/search tools");
|
||||
steps.add("Design minimal change set and draft patch plan");
|
||||
steps.add("Apply file changes incrementally and keep each step reversible");
|
||||
steps.add("Run whitelisted validation commands (build/test/lint) for impacted modules only");
|
||||
if ("PLAN_AND_EXECUTE".equals(mode)) {
|
||||
steps.add("Prepare summary of changes and execution logs for user review");
|
||||
} else {
|
||||
steps.add("Return executable checklist and wait for execution approval");
|
||||
}
|
||||
if (risk != RiskLevel.LOW) {
|
||||
steps.add("Request explicit confirmation before any medium/high-risk operation");
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
private List<String> buildAcceptance(String mode, RiskLevel risk) {
|
||||
List<String> criteria = new ArrayList<>();
|
||||
criteria.add("Planned steps are concrete, ordered, and testable");
|
||||
criteria.add("Every file/command action is bounded within workspace and policy constraints");
|
||||
criteria.add("Validation commands and expected outputs are specified");
|
||||
if ("PLAN_AND_EXECUTE".equals(mode)) {
|
||||
criteria.add("Executed changes produce a verifiable diff and command results");
|
||||
} else {
|
||||
criteria.add("Plan is ready for immediate execution after user approval");
|
||||
}
|
||||
if (risk == RiskLevel.HIGH) {
|
||||
criteria.add("High-risk actions are isolated and require explicit user confirmation");
|
||||
}
|
||||
return criteria;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolName() {
|
||||
return "task_planner";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "任务规划";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return DESCRIPTION;
|
||||
}
|
||||
|
||||
private enum RiskLevel {
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public interface AbstractChatService {
|
||||
|
||||
/**
|
||||
* 创建同步聊天模型(供 Agent/SupervisorAgent 使用)
|
||||
* 默认实现使用 OpenAI 兼容协议,适用于 OpenAI、DeepSeek、Atlas Cloud 等兼容接口的 provider。
|
||||
* 默认实现使用 OpenAI 兼容协议,适用于 OpenAI、DeepSeek、PPIO 等兼容接口的 provider。
|
||||
* ZhiPu、QianWen、Ollama 等需覆盖此方法使用各自 SDK。
|
||||
*
|
||||
* @param chatModelVo 模型配置
|
||||
|
||||
@@ -29,6 +29,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.agent.ChartGenerationAgent;
|
||||
import org.ruoyi.agent.CodingAgent;
|
||||
import org.ruoyi.agent.EchartsAgent;
|
||||
import org.ruoyi.agent.SkillsAgent;
|
||||
import org.ruoyi.agent.SqlAgent;
|
||||
@@ -54,6 +55,12 @@ import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||
import org.ruoyi.factory.ChatServiceFactory;
|
||||
import org.ruoyi.mcp.service.core.ToolProviderFactory;
|
||||
import org.ruoyi.mcp.tools.CreateFileTool;
|
||||
import org.ruoyi.mcp.tools.EditFileTool;
|
||||
import org.ruoyi.mcp.tools.ListDirectoryTool;
|
||||
import org.ruoyi.mcp.tools.ReadFileTool;
|
||||
import org.ruoyi.mcp.tools.RunCommandTool;
|
||||
import org.ruoyi.mcp.tools.TaskPlannerTool;
|
||||
import org.ruoyi.observability.*;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
@@ -316,11 +323,25 @@ public class ChatServiceFacade implements IChatService {
|
||||
.listener(new MyAgentListener())
|
||||
.build();
|
||||
|
||||
// 构建子 Agent 6: CodingAgent - 负责通用开发任务落地
|
||||
CodingAgent codingAgent = AgenticServices.agentBuilder(CodingAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.tools(
|
||||
new TaskPlannerTool(),
|
||||
new ListDirectoryTool(),
|
||||
new ReadFileTool(),
|
||||
new CreateFileTool(),
|
||||
new EditFileTool(),
|
||||
new RunCommandTool()
|
||||
)
|
||||
.listener(new MyAgentListener())
|
||||
.build();
|
||||
|
||||
// 构建监督者 Agent - 管理多个子 Agent
|
||||
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
|
||||
.chatModel(plannerModel)
|
||||
//.listener(new SupervisorStreamListener(null))
|
||||
.subAgents(skillsAgent,searchAgent, sqlAgent, chartGenerationAgent, echartsAgent)
|
||||
.subAgents(skillsAgent,searchAgent, sqlAgent, chartGenerationAgent, echartsAgent, codingAgent)
|
||||
// 加入历史上下文 - 使用 ChatMemoryProvider 提供持久化的聊天内存
|
||||
//.chatMemoryProvider(memoryId -> createChatMemory(chatRequest.getSessionId()))
|
||||
.responseStrategy(SupervisorResponseStrategy.LAST)
|
||||
@@ -618,4 +639,3 @@ public class ChatServiceFacade implements IChatService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Atlas Cloud服务调用
|
||||
* PPIO服务调用
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
@@ -23,7 +23,7 @@ import java.util.List;
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AtlaServiceImpl implements AbstractChatService {
|
||||
public class PPIOServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
@@ -38,7 +38,7 @@ public class AtlaServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.ATLAS.getCode();
|
||||
return ChatModeType.PPIO.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.ruoyi.service.embed.impl;
|
||||
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @Author: Robust_H
|
||||
* @Date: 2025-09-30-下午3:59
|
||||
* @Description: 硅基流动(兼容 OpenAi)
|
||||
*/
|
||||
@Component("ppio")
|
||||
public class PPIOEmbeddingProvider extends OpenAiEmbeddingProvider {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.ruoyi.service.report;
|
||||
|
||||
import org.ruoyi.domain.dto.request.AiReportGenerateRequest;
|
||||
import org.ruoyi.domain.dto.request.AiReportExecuteRequest;
|
||||
import org.ruoyi.domain.dto.request.AiReportRefineRequest;
|
||||
import org.ruoyi.domain.dto.response.AiReportResponse;
|
||||
|
||||
public interface IAiReportService {
|
||||
|
||||
AiReportResponse generate(AiReportGenerateRequest request);
|
||||
|
||||
AiReportResponse execute(AiReportExecuteRequest request);
|
||||
|
||||
AiReportResponse refine(AiReportRefineRequest request);
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package org.ruoyi.service.report.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.domain.dto.request.AiReportExecuteRequest;
|
||||
import org.ruoyi.domain.dto.request.AiReportGenerateRequest;
|
||||
import org.ruoyi.domain.dto.request.AiReportRefineRequest;
|
||||
import org.ruoyi.domain.dto.response.AiReportResponse;
|
||||
import org.ruoyi.service.report.IAiReportService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AiReportServiceImpl implements IAiReportService {
|
||||
|
||||
private static final Pattern MYSQL_JDBC_PATTERN =
|
||||
Pattern.compile("^jdbc:mysql://([^:/?]+)(?::(\\d+))?/([^?]+).*$");
|
||||
|
||||
private static final int DEFAULT_MAX_ROWS = 100;
|
||||
private static final int ABSOLUTE_MAX_ROWS = 1000;
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${spring.datasource.dynamic.datasource.master.url:}")
|
||||
private String jdbcUrl;
|
||||
|
||||
@Value("${spring.datasource.dynamic.datasource.master.username:}")
|
||||
private String dbUsername;
|
||||
|
||||
@Value("${spring.datasource.dynamic.datasource.master.password:}")
|
||||
private String dbPassword;
|
||||
|
||||
@Override
|
||||
public AiReportResponse generate(AiReportGenerateRequest request) {
|
||||
ChatModel model = buildModel(request.getModel());
|
||||
int maxRows = resolveMaxRows(request.getMaxRows());
|
||||
|
||||
String tableInfo = executeShellSql("SHOW TABLES", 10);
|
||||
|
||||
String sqlPlanPrompt = """
|
||||
你是数据分析师。基于用户需求和表信息,只生成一个安全的 SELECT SQL。
|
||||
返回 JSON,不要返回解释:
|
||||
{"title":"","summary":"","sql":""}
|
||||
|
||||
约束:
|
||||
1) SQL 必须是 SELECT 开头
|
||||
2) 严禁写入/删除/更新语句
|
||||
3) 尽量不要 SELECT *
|
||||
4) 若用户未指定行数,默认 LIMIT %d
|
||||
|
||||
表信息(来自 shell 查询):
|
||||
%s
|
||||
|
||||
用户需求:
|
||||
%s
|
||||
""".formatted(maxRows, tableInfo, request.getPrompt());
|
||||
|
||||
String planRaw = model.chat(sqlPlanPrompt);
|
||||
JsonNode plan = parseJson(planRaw);
|
||||
|
||||
String title = plan.path("title").asText("AI 报表");
|
||||
String summary = plan.path("summary").asText("根据自然语言需求自动生成");
|
||||
String sql = normalizeSql(plan.path("sql").asText(""));
|
||||
validateSelectSql(sql);
|
||||
|
||||
return AiReportResponse.builder()
|
||||
.title(title)
|
||||
.summary(summary)
|
||||
.sql(sql)
|
||||
.queryResult("")
|
||||
.html("")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiReportResponse execute(AiReportExecuteRequest request) {
|
||||
ChatModel model = buildModel(request.getModel());
|
||||
String sql = normalizeSql(request.getSql());
|
||||
validateSelectSql(sql);
|
||||
|
||||
String queryResult = executeShellSql(sql, 30);
|
||||
|
||||
String htmlPrompt = """
|
||||
你是前端报表工程师。请生成一个完整的 HTML 页面(只输出 HTML,不要 markdown 代码块)。
|
||||
页面要求:
|
||||
1) 展示标题、摘要、SQL、查询结果(保留原始文本)
|
||||
2) 风格专业,适合企业报表
|
||||
3) 页面包含一个“继续编辑”输入框和按钮
|
||||
4) 点击按钮后调用 POST /chat/report/refine
|
||||
JSON: {"model":"%s","prompt":"用户输入","html":"当前页面 outerHTML","dataContext":"%s"}
|
||||
5) 收到返回后,用返回的 html 替换当前页面
|
||||
|
||||
标题:%s
|
||||
摘要:%s
|
||||
SQL:%s
|
||||
查询结果:
|
||||
%s
|
||||
""".formatted(request.getModel(), escapeForJson(queryResult), request.getTitle(), request.getSummary(), sql, queryResult);
|
||||
|
||||
String html = stripCodeFence(model.chat(htmlPrompt));
|
||||
|
||||
return AiReportResponse.builder()
|
||||
.title(request.getTitle())
|
||||
.summary(request.getSummary())
|
||||
.sql(sql)
|
||||
.queryResult(queryResult)
|
||||
.html(html)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiReportResponse refine(AiReportRefineRequest request) {
|
||||
ChatModel model = buildModel(request.getModel());
|
||||
String dataContext = request.getDataContext() == null ? "" : request.getDataContext();
|
||||
|
||||
String refinePrompt = """
|
||||
你是前端报表工程师。请基于当前 HTML 和用户要求进行修改。
|
||||
仅输出完整 HTML,不要解释。
|
||||
|
||||
修改要求:
|
||||
%s
|
||||
|
||||
数据上下文:
|
||||
%s
|
||||
|
||||
当前 HTML:
|
||||
%s
|
||||
""".formatted(request.getPrompt(), dataContext, request.getHtml());
|
||||
|
||||
String updatedHtml = stripCodeFence(model.chat(refinePrompt));
|
||||
|
||||
return AiReportResponse.builder()
|
||||
.title("AI 报表(已编辑)")
|
||||
.summary(request.getPrompt())
|
||||
.sql("")
|
||||
.queryResult(dataContext)
|
||||
.html(updatedHtml)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String executeShellSql(String sql, int timeoutSeconds) {
|
||||
MysqlConnectionInfo info = parseMysqlConnection(jdbcUrl);
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("mysql");
|
||||
command.add("--batch");
|
||||
command.add("--raw");
|
||||
command.add("--default-character-set=utf8mb4");
|
||||
command.add("-h");
|
||||
command.add(info.host());
|
||||
command.add("-P");
|
||||
command.add(String.valueOf(info.port()));
|
||||
command.add("-u");
|
||||
command.add(dbUsername);
|
||||
command.add("-D");
|
||||
command.add(info.database());
|
||||
command.add("-e");
|
||||
command.add(sql);
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.redirectErrorStream(true);
|
||||
pb.environment().put("MYSQL_PWD", dbPassword == null ? "" : dbPassword);
|
||||
|
||||
try {
|
||||
Process process = pb.start();
|
||||
String output;
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append('\n');
|
||||
}
|
||||
output = sb.toString().trim();
|
||||
}
|
||||
|
||||
boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
|
||||
if (!finished) {
|
||||
process.destroyForcibly();
|
||||
throw new IllegalArgumentException("shell 查询超时");
|
||||
}
|
||||
|
||||
int code = process.exitValue();
|
||||
if (code != 0) {
|
||||
throw new IllegalArgumentException("shell 查询失败: " + output);
|
||||
}
|
||||
return output;
|
||||
} catch (Exception e) {
|
||||
log.error("shell 查询执行失败, sql={}", sql, e);
|
||||
throw new IllegalArgumentException("shell 查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private MysqlConnectionInfo parseMysqlConnection(String url) {
|
||||
if (url == null || url.isBlank()) {
|
||||
throw new IllegalArgumentException("未配置 MySQL JDBC URL");
|
||||
}
|
||||
|
||||
Matcher matcher = MYSQL_JDBC_PATTERN.matcher(url.trim());
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException("无法解析 JDBC URL: " + url);
|
||||
}
|
||||
|
||||
String host = matcher.group(1);
|
||||
int port = matcher.group(2) == null ? 3306 : Integer.parseInt(matcher.group(2));
|
||||
String database = matcher.group(3);
|
||||
if (database.contains("/")) {
|
||||
database = database.substring(0, database.indexOf('/'));
|
||||
}
|
||||
return new MysqlConnectionInfo(host, port, database);
|
||||
}
|
||||
|
||||
private ChatModel buildModel(String modelName) {
|
||||
ChatModelVo modelVo = chatModelService.selectModelByName(modelName);
|
||||
if (modelVo == null) {
|
||||
throw new IllegalArgumentException("模型不存在: " + modelName);
|
||||
}
|
||||
|
||||
return OpenAiChatModel.builder()
|
||||
.baseUrl(modelVo.getApiHost())
|
||||
.apiKey(modelVo.getApiKey())
|
||||
.modelName(modelVo.getModelName())
|
||||
.build();
|
||||
}
|
||||
|
||||
private JsonNode parseJson(String raw) {
|
||||
try {
|
||||
String candidate = raw.trim();
|
||||
if (candidate.startsWith("```") && candidate.contains("{")) {
|
||||
int start = candidate.indexOf('{');
|
||||
int end = candidate.lastIndexOf('}');
|
||||
candidate = candidate.substring(start, end + 1);
|
||||
}
|
||||
return objectMapper.readTree(candidate);
|
||||
} catch (Exception e) {
|
||||
log.error("解析 SQL 计划 JSON 失败: {}", raw, e);
|
||||
throw new IllegalArgumentException("模型返回格式错误,未能解析 SQL 计划");
|
||||
}
|
||||
}
|
||||
|
||||
private String stripCodeFence(String content) {
|
||||
String value = content == null ? "" : content.trim();
|
||||
if (value.startsWith("```")) {
|
||||
int firstLineBreak = value.indexOf('\n');
|
||||
int lastFence = value.lastIndexOf("```");
|
||||
if (firstLineBreak > -1 && lastFence > firstLineBreak) {
|
||||
return value.substring(firstLineBreak + 1, lastFence).trim();
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private int resolveMaxRows(Integer maxRows) {
|
||||
if (maxRows == null || maxRows < 1) {
|
||||
return DEFAULT_MAX_ROWS;
|
||||
}
|
||||
return Math.min(maxRows, ABSOLUTE_MAX_ROWS);
|
||||
}
|
||||
|
||||
private String normalizeSql(String sql) {
|
||||
String value = sql == null ? "" : sql.trim();
|
||||
if (value.endsWith(";")) {
|
||||
value = value.substring(0, value.length() - 1).trim();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void validateSelectSql(String sql) {
|
||||
if (sql.isBlank()) {
|
||||
throw new IllegalArgumentException("SQL 不能为空");
|
||||
}
|
||||
String upperSql = sql.toUpperCase();
|
||||
if (!upperSql.startsWith("SELECT")) {
|
||||
throw new IllegalArgumentException("仅允许 SELECT SQL");
|
||||
}
|
||||
if (upperSql.contains(";") || upperSql.contains("UPDATE ") || upperSql.contains("DELETE ")
|
||||
|| upperSql.contains("INSERT ") || upperSql.contains("DROP ") || upperSql.contains("ALTER ")) {
|
||||
throw new IllegalArgumentException("SQL 含有不允许的语句");
|
||||
}
|
||||
}
|
||||
|
||||
private String escapeForJson(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
|
||||
}
|
||||
|
||||
private record MysqlConnectionInfo(String host, int port, String database) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package org.ruoyi.agent;
|
||||
|
||||
import dev.langchain4j.agentic.AgenticServices;
|
||||
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
|
||||
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
|
||||
import dev.langchain4j.model.chat.listener.ChatModelListener;
|
||||
import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
|
||||
import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
|
||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import dev.langchain4j.service.SystemMessage;
|
||||
import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ruoyi.observability.OutputChannel;
|
||||
import org.ruoyi.observability.StreamingOutputWrapper;
|
||||
import org.ruoyi.observability.SupervisorStreamListener;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 子 Agent 流式输出集成测试
|
||||
*
|
||||
* 测试内容:
|
||||
* 1. 观察单个 Agent 的流式输出
|
||||
* 2. 观察 Supervisor 调用子 Agent 的流式输出
|
||||
* 3. 验证 AgentListener 事件回调
|
||||
* 4. 验证 StreamingOutputWrapper 的 token 拦截
|
||||
*
|
||||
* 注意:运行测试前需要配置正确的 API Key
|
||||
* 可以通过环境变量或直接修改配置区域
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/04/10
|
||||
*/
|
||||
@Disabled("需要配置 API Key 后手动启用")
|
||||
public class StreamingAgentIntegrationTest {
|
||||
|
||||
// ==================== 配置区域 ====================
|
||||
private static final String BASE_URL = "https://api.ppio.com/openai";
|
||||
private static final String API_KEY = System.getenv("PPIO_API_KEY") != null
|
||||
? System.getenv("PPIO_API_KEY")
|
||||
: "xx"; // 默认 Key
|
||||
private static final String MODEL_NAME = "deepseek/deepseek-v3.2";
|
||||
|
||||
private StreamingChatModel streamingModel;
|
||||
private OpenAiChatModel syncModel;
|
||||
|
||||
// ==================== Agent 接口定义 ====================
|
||||
|
||||
public interface MathAgent {
|
||||
@SystemMessage("你是一个数学计算助手,帮助用户解决数学问题。直接给出计算结果和简要解释。")
|
||||
@UserMessage("计算:{{query}}")
|
||||
@dev.langchain4j.agentic.Agent("数学计算助手")
|
||||
String calculate(@V("query") String query);
|
||||
}
|
||||
|
||||
public interface TextAgent {
|
||||
@SystemMessage("你是一个文本分析助手,帮助用户分析文本内容。给出简洁的分析结果。")
|
||||
@UserMessage("分析以下文本:{{text}}")
|
||||
@dev.langchain4j.agentic.Agent("文本分析助手")
|
||||
String analyze(@V("text") String text);
|
||||
}
|
||||
|
||||
public interface WeatherAgent {
|
||||
@SystemMessage("你是一个天气助手。根据用户提供的信息给出天气相关的回答。")
|
||||
@UserMessage("回答问题:{{query}}")
|
||||
@dev.langchain4j.agentic.Agent("天气助手")
|
||||
String answer(@V("query") String query);
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// streamingModel = OpenAiStreamingChatModel.builder()
|
||||
// .baseUrl(BASE_URL)
|
||||
// .apiKey(API_KEY)
|
||||
// .modelName(MODEL_NAME)
|
||||
// .build();
|
||||
|
||||
streamingModel = OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.apiKey(API_KEY)
|
||||
.listeners(List.of(new ChatModelListener() {
|
||||
@Override
|
||||
public void onRequest(ChatModelRequestContext ctx) {
|
||||
// 请求发送前
|
||||
}
|
||||
@Override
|
||||
public void onResponse(ChatModelResponseContext ctx) {
|
||||
// 响应完成后
|
||||
}
|
||||
@Override
|
||||
public void onError(ChatModelErrorContext ctx) {
|
||||
// 错误时
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
|
||||
|
||||
syncModel = OpenAiChatModel.builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.apiKey(API_KEY)
|
||||
.modelName(MODEL_NAME)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ==================== 测试方法 ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("测试1: 基础流式输出 - 单个 Agent")
|
||||
void testBasicStreamingAgent() throws Exception {
|
||||
System.out.println("\n=== 测试1: 基础流式输出 - 单个 Agent ===\n");
|
||||
|
||||
// 创建事件总线
|
||||
String requestId = UUID.randomUUID().toString();
|
||||
OutputChannel channel = OutputChannel.create(requestId);
|
||||
CountDownLatch completed = new CountDownLatch(1);
|
||||
|
||||
// 包装模型以捕获流式输出
|
||||
ChatModel wrappedModel = new StreamingOutputWrapper(streamingModel, channel);
|
||||
|
||||
// 构建 Agent
|
||||
MathAgent mathAgent = AgenticServices.agentBuilder(MathAgent.class)
|
||||
.chatModel(wrappedModel)
|
||||
.build();
|
||||
|
||||
// 异步执行
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
System.out.println(">>> 调用 MathAgent.calculate()...");
|
||||
String result = mathAgent.calculate("计算 123 * 456 + 789 的值");
|
||||
System.out.println("\n>>> 最终结果: " + result);
|
||||
} catch (Exception e) {
|
||||
System.err.println(">>> 异常: " + e.getMessage());
|
||||
channel.completeWithError(e);
|
||||
} finally {
|
||||
channel.complete();
|
||||
completed.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// drain 推送
|
||||
channel.drain(text -> {
|
||||
System.out.print(text);
|
||||
System.out.flush();
|
||||
});
|
||||
|
||||
completed.await(30, TimeUnit.SECONDS);
|
||||
OutputChannel.remove(requestId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试2: Supervisor 模式 - 单个子 Agent 流式输出")
|
||||
void testSupervisorWithSingleSubAgent() throws Exception {
|
||||
System.out.println("\n=== 测试2: Supervisor 模式 - 单个子 Agent ===\n");
|
||||
|
||||
String requestId = UUID.randomUUID().toString();
|
||||
OutputChannel channel = OutputChannel.create(requestId);
|
||||
CountDownLatch completed = new CountDownLatch(1);
|
||||
|
||||
// 包装模型
|
||||
|
||||
// 子 Agent
|
||||
MathAgent mathAgent = AgenticServices.agentBuilder(MathAgent.class)
|
||||
.streamingChatModel(streamingModel)
|
||||
.build();
|
||||
|
||||
|
||||
// Supervisor(注册监听器)
|
||||
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
|
||||
.chatModel(syncModel)
|
||||
//.listener(new SupervisorStreamListener(channel))
|
||||
.subAgents(mathAgent)
|
||||
.responseStrategy(SupervisorResponseStrategy.LAST)
|
||||
.build();
|
||||
|
||||
// 异步执行
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
System.out.println(">>> Supervisor.invoke() 开始...");
|
||||
String result = supervisor.invoke("帮我计算 999 除以 3 等于多少");
|
||||
System.out.println("\n>>> Supervisor 结果: " + result);
|
||||
} catch (Exception e) {
|
||||
System.err.println(">>> 异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
// channel.completeWithError(e);
|
||||
} finally {
|
||||
// channel.complete();
|
||||
completed.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// drain 推送
|
||||
channel.drain(text -> {
|
||||
System.out.print(text);
|
||||
System.out.flush();
|
||||
});
|
||||
|
||||
completed.await(60, TimeUnit.SECONDS);
|
||||
OutputChannel.remove(requestId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试3: Supervisor 模式 - 多个子 Agent 流式输出")
|
||||
void testSupervisorWithMultipleSubAgents() throws Exception {
|
||||
System.out.println("\n=== 测试3: Supervisor 模式 - 多个子 Agent ===\n");
|
||||
|
||||
String requestId = UUID.randomUUID().toString();
|
||||
OutputChannel channel = OutputChannel.create(requestId);
|
||||
CountDownLatch completed = new CountDownLatch(1);
|
||||
|
||||
// 包装模型
|
||||
ChatModel wrappedModel = new StreamingOutputWrapper(streamingModel, channel);
|
||||
|
||||
|
||||
// 子 Agent
|
||||
MathAgent mathAgent = AgenticServices.agentBuilder(MathAgent.class)
|
||||
.chatModel(wrappedModel)
|
||||
.build();
|
||||
|
||||
TextAgent textAgent = AgenticServices.agentBuilder(TextAgent.class)
|
||||
.chatModel(wrappedModel)
|
||||
.build();
|
||||
|
||||
WeatherAgent weatherAgent = AgenticServices.agentBuilder(WeatherAgent.class)
|
||||
.chatModel(wrappedModel)
|
||||
.build();
|
||||
|
||||
// Supervisor
|
||||
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
|
||||
.chatModel(syncModel)
|
||||
.listener(new SupervisorStreamListener(channel))
|
||||
.subAgents(mathAgent, textAgent, weatherAgent)
|
||||
.responseStrategy(SupervisorResponseStrategy.LAST)
|
||||
.build();
|
||||
|
||||
// 异步执行 - 提一个会触发多个 Agent 的问题
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
System.out.println(">>> Supervisor.invoke() 开始...");
|
||||
String result = supervisor.invoke(
|
||||
"请帮我做两件事:1. 计算 50 * 20 的结果;2. 分析 '人工智能正在改变世界' 这句话的含义"
|
||||
);
|
||||
System.out.println("\n>>> Supervisor 结果: " + result);
|
||||
} catch (Exception e) {
|
||||
System.err.println(">>> 异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
channel.completeWithError(e);
|
||||
} finally {
|
||||
channel.complete();
|
||||
completed.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// drain 推送 - 实时观察流式输出
|
||||
channel.drain(text -> {
|
||||
System.out.print("观察流式输出:"+text);
|
||||
System.out.flush();
|
||||
});
|
||||
|
||||
completed.await(90, TimeUnit.SECONDS);
|
||||
OutputChannel.remove(requestId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试4: 直接观察 StreamingChatModel 的流式响应")
|
||||
void testDirectStreamingChatModel() throws Exception {
|
||||
System.out.println("\n=== 测试4: 直接观察 StreamingChatModel ===\n");
|
||||
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
CountDownLatch completed = new CountDownLatch(1);
|
||||
|
||||
streamingModel.chat("你好,请自我介绍", new dev.langchain4j.model.chat.response.StreamingChatResponseHandler() {
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
buffer.append(partialResponse);
|
||||
System.out.print(partialResponse);
|
||||
System.out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
System.out.println("\n\n[完成] 总Token数: " +
|
||||
(completeResponse.metadata() != null && completeResponse.metadata().tokenUsage() != null
|
||||
? completeResponse.metadata().tokenUsage().totalTokenCount()
|
||||
: "无"));
|
||||
completed.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
System.err.println("[错误] " + error.getMessage());
|
||||
completed.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
completed.await(30, TimeUnit.SECONDS);
|
||||
System.out.println("完整响应内容: " + buffer.toString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user