mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-14 04:13:39 +00:00
Merge branch 'v3.0.0' of https://github.com/ageerle/ruoyi-ai into v3.0.0
This commit is contained in:
15
README.md
15
README.md
@@ -31,14 +31,13 @@
|
|||||||
|
|
||||||
## ✨ 核心亮点
|
## ✨ 核心亮点
|
||||||
|
|
||||||
| 模块 | 现有能力 | 扩展方向 |
|
| 模块 | 现有能力 | 扩展方向 |
|
||||||
|:---:|---|---|
|
|:----------:|---|------------------------|
|
||||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | 自动模式、容错机制 |
|
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | 自动模式、容错机制、计费管理 |
|
||||||
| **知识库** | 本地RAG + 向量库(Milvus/Weaviate) + 知识图谱 + 文档解析 +重排序 | 音频视频解析、知识出处 |
|
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate) + 文档解析 | 多模态、知识出处、知识图谱、重排序 |
|
||||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | 工具插件市场、toolAgent自动加载工具 |
|
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | 工具插件市场、 |
|
||||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | 更多节点类型 |
|
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | 更多节点类型 |
|
||||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型 | 智能体可配置 |
|
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型 | 智能体可配置 |
|
||||||
| **AI编程** | 智能代码分析、项目脚手架生成、Copilot助手 | 代码生成优化 |
|
|
||||||
|
|
||||||
## 🚀 快速体验
|
## 🚀 快速体验
|
||||||
|
|
||||||
|
|||||||
28
docs/docker/ minio/ docker-compose.yml
Normal file
28
docs/docker/ minio/ docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
minio:
|
||||||
|
image: minio/minio
|
||||||
|
container_name: minio
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9090:9090"
|
||||||
|
environment:
|
||||||
|
- MINIO_ACCESS_KEY=ruoyi
|
||||||
|
- MINIO_SECRET_KEY=ruoyi123
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
- minio_config:/root/.minio
|
||||||
|
command: server /data --console-address ":9090"
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- minio-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
minio-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minio_data:
|
||||||
|
minio_config:
|
||||||
65
docs/docker/ neo4j/docker-compose.yml
Normal file
65
docs/docker/ neo4j/docker-compose.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
neo4j:
|
||||||
|
image: neo4j:5.15.0
|
||||||
|
container_name: ruoyi-neo4j
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# HTTP端口
|
||||||
|
- "7474:7474"
|
||||||
|
# HTTPS端口
|
||||||
|
- "7473:7473"
|
||||||
|
# Bolt端口
|
||||||
|
- "7687:7687"
|
||||||
|
environment:
|
||||||
|
# 初始密码设置(首次启动后需要修改)
|
||||||
|
- NEO4J_AUTH=neo4j/your_password
|
||||||
|
# 接受许可协议
|
||||||
|
- NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
|
||||||
|
# 内存配置(根据服务器配置调整)
|
||||||
|
- NEO4J_dbms_memory_heap_initial__size=512m
|
||||||
|
- NEO4J_dbms_memory_heap_max__size=2g
|
||||||
|
- NEO4J_dbms_memory_pagecache_size=1g
|
||||||
|
# 事务日志配置
|
||||||
|
- NEO4J_dbms_tx__log_rotation_retention__policy=3 days
|
||||||
|
# 允许从任何主机连接
|
||||||
|
- NEO4J_dbms_default__listen__address=0.0.0.0
|
||||||
|
# 启用APOC插件
|
||||||
|
- NEO4J_dbms_security_procedures_unrestricted=apoc.*
|
||||||
|
- NEO4J_dbms_security_procedures_allowlist=apoc.*
|
||||||
|
# 日志级别
|
||||||
|
- NEO4J_dbms_logs_debug_level=INFO
|
||||||
|
volumes:
|
||||||
|
# 数据持久化
|
||||||
|
- neo4j_data:/data
|
||||||
|
# 日志持久化
|
||||||
|
- neo4j_logs:/logs
|
||||||
|
# 导入目录
|
||||||
|
- neo4j_import:/var/lib/neo4j/import
|
||||||
|
# 插件目录
|
||||||
|
- neo4j_plugins:/plugins
|
||||||
|
networks:
|
||||||
|
- ruoyi-network
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider localhost:7474 || exit 1" ]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
neo4j_data:
|
||||||
|
name: ruoyi-neo4j-data
|
||||||
|
neo4j_logs:
|
||||||
|
name: ruoyi-neo4j-logs
|
||||||
|
neo4j_import:
|
||||||
|
name: ruoyi-neo4j-import
|
||||||
|
neo4j_plugins:
|
||||||
|
name: ruoyi-neo4j-plugins
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ruoyi-network:
|
||||||
|
name: ruoyi-network
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
75
docs/docker/milvus/docker-compose.yml
Normal file
75
docs/docker/milvus/docker-compose.yml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
version: '3.5'
|
||||||
|
|
||||||
|
services:
|
||||||
|
etcd:
|
||||||
|
container_name: milvus-etcd
|
||||||
|
image: quay.io/coreos/etcd:v3.5.18
|
||||||
|
environment:
|
||||||
|
- ETCD_AUTO_COMPACTION_MODE=revision
|
||||||
|
- ETCD_AUTO_COMPACTION_RETENTION=1000
|
||||||
|
- ETCD_QUOTA_BACKEND_BYTES=4294967296
|
||||||
|
- ETCD_SNAPSHOT_COUNT=50000
|
||||||
|
volumes:
|
||||||
|
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
|
||||||
|
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "etcdctl", "endpoint", "health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
minio:
|
||||||
|
container_name: milvus-minio
|
||||||
|
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
|
||||||
|
environment:
|
||||||
|
MINIO_ACCESS_KEY: minioadmin
|
||||||
|
MINIO_SECRET_KEY: minioadmin
|
||||||
|
ports:
|
||||||
|
- "9001:9001"
|
||||||
|
- "9000:9000"
|
||||||
|
volumes:
|
||||||
|
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
|
||||||
|
command: minio server /minio_data --console-address ":9001"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
standalone:
|
||||||
|
container_name: milvus-standalone
|
||||||
|
image: milvusdb/milvus:v2.5.7
|
||||||
|
command: ["milvus", "run", "standalone"]
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined
|
||||||
|
environment:
|
||||||
|
ETCD_ENDPOINTS: etcd:2379
|
||||||
|
MINIO_ADDRESS: minio:9000
|
||||||
|
volumes:
|
||||||
|
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
|
||||||
|
interval: 30s
|
||||||
|
start_period: 90s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
ports:
|
||||||
|
- "19530:19530"
|
||||||
|
- "9091:9091"
|
||||||
|
depends_on:
|
||||||
|
- "etcd"
|
||||||
|
- "minio"
|
||||||
|
|
||||||
|
attu:
|
||||||
|
container_name: attu
|
||||||
|
image: zilliz/attu:v2.5.7
|
||||||
|
environment:
|
||||||
|
MILVUS_URL: milvus-standalone:19530
|
||||||
|
ports:
|
||||||
|
- "19500:3000" # 外部端口19500可以自定义
|
||||||
|
depends_on:
|
||||||
|
- "standalone"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: milvus
|
||||||
25
docs/docker/weaviate/docker-compose.yml
Normal file
25
docs/docker/weaviate/docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
weaviate:
|
||||||
|
command:
|
||||||
|
- --host
|
||||||
|
- 0.0.0.0
|
||||||
|
- --port
|
||||||
|
- '6038'
|
||||||
|
- --scheme
|
||||||
|
- http
|
||||||
|
image: semitechnologies/weaviate:1.19.7
|
||||||
|
ports:
|
||||||
|
- 6038:6038
|
||||||
|
- 50051:50051
|
||||||
|
volumes:
|
||||||
|
- weaviate_data:/var/lib/weaviate
|
||||||
|
environment:
|
||||||
|
QUERY_DEFAULTS_LIMIT: 25
|
||||||
|
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
|
||||||
|
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
|
||||||
|
DEFAULT_VECTORIZER_MODULE: 'none'
|
||||||
|
CLUSTER_HOSTNAME: 'node1'
|
||||||
|
volumes:
|
||||||
|
weaviate_data:
|
||||||
|
...
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
worker_processes 1;
|
|
||||||
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
|
||||||
# 可以根据业务并发量适当调高
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
# 高效传输文件
|
|
||||||
sendfile on;
|
|
||||||
# 长连接超时时间
|
|
||||||
keepalive_timeout 65;
|
|
||||||
# 单连接最大请求数,提高长连接复用率
|
|
||||||
keepalive_requests 100000;
|
|
||||||
# 限制body大小
|
|
||||||
client_max_body_size 100m;
|
|
||||||
client_header_buffer_size 32k;
|
|
||||||
client_body_buffer_size 512k;
|
|
||||||
# 开启静态资源压缩
|
|
||||||
gzip_static on;
|
|
||||||
# 连接数限制 (防御类配置) 10m 一般够用了,能存储上万 IP 的计数
|
|
||||||
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
|
||||||
limit_conn_zone $server_name zone=perserver:10m;
|
|
||||||
# 隐藏 nginx 版本号,防止暴露版本信息
|
|
||||||
server_tokens off;
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
upstream server {
|
|
||||||
ip_hash;
|
|
||||||
server 127.0.0.1:8080;
|
|
||||||
server 127.0.0.1:8081;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream monitor-admin {
|
|
||||||
server 127.0.0.1:9090;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream snailjob-server {
|
|
||||||
server 127.0.0.1:8800;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
# https配置参考 start
|
|
||||||
#listen 443 ssl;
|
|
||||||
|
|
||||||
# 证书直接存放 /docker/nginx/cert/ 目录下即可 更改证书名称即可 无需更改证书路径
|
|
||||||
#ssl on;
|
|
||||||
#ssl_certificate /etc/nginx/cert/xxx.local.crt; # /etc/nginx/cert/ 为docker映射路径 不允许更改
|
|
||||||
#ssl_certificate_key /etc/nginx/cert/xxx.local.key; # /etc/nginx/cert/ 为docker映射路径 不允许更改
|
|
||||||
#ssl_session_timeout 5m;
|
|
||||||
#ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
|
|
||||||
#ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1;
|
|
||||||
#ssl_prefer_server_ciphers on;
|
|
||||||
# https配置参考 end
|
|
||||||
|
|
||||||
# 演示环境配置 拦截除 GET POST 之外的所有请求
|
|
||||||
# if ($request_method !~* GET|POST) {
|
|
||||||
# rewrite ^/(.*)$ /403;
|
|
||||||
# }
|
|
||||||
|
|
||||||
# location = /403 {
|
|
||||||
# default_type application/json;
|
|
||||||
# return 200 '{"msg":"演示模式,不允许操作","code":500}';
|
|
||||||
# }
|
|
||||||
|
|
||||||
# 限制外网访问内网 actuator 相关路径
|
|
||||||
location ~ ^(/[^/]*)?/actuator.*(/.*)?$ {
|
|
||||||
return 403;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html; # docker映射路径 不允许更改
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
index index.html index.htm;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /prod-api/ {
|
|
||||||
# 设置客户端请求头中的 Host 信息(保持原始 Host)
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
# 获取客户端真实 IP
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# 自定义头 REMOTE-HOST,记录客户端 IP
|
|
||||||
proxy_set_header REMOTE-HOST $remote_addr;
|
|
||||||
# 获取完整的客户端 IP 链(经过多级代理时)
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# 设置后端响应超时时间(这里是 24 小时,适合长连接/SSE)
|
|
||||||
proxy_read_timeout 86400s;
|
|
||||||
# SSE (Server-Sent Events) 与 WebSocket 支持参数
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
# 禁用代理缓冲,数据直接传给客户端
|
|
||||||
proxy_buffering off;
|
|
||||||
# 禁用代理缓存
|
|
||||||
proxy_cache off;
|
|
||||||
# 按 IP 限制连接数(防 CC 攻击) 小型站:10~20 就够 中型站:50~100
|
|
||||||
limit_conn perip 20;
|
|
||||||
|
|
||||||
# 按 Server 限制总并发连接数 根据服务器的最大并发处理能力来定 太小会限制合法用户访问,太大会占满服务器资源
|
|
||||||
limit_conn perserver 500;
|
|
||||||
proxy_pass http://server/;
|
|
||||||
}
|
|
||||||
|
|
||||||
# https 会拦截内链所有的 http 请求 造成功能无法使用
|
|
||||||
# 解决方案1 将 admin 服务 也配置成 https
|
|
||||||
# 解决方案2 将菜单配置为外链访问 走独立页面 http 访问
|
|
||||||
location /admin/ {
|
|
||||||
# 设置客户端请求头中的 Host 信息(保持原始 Host)
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
# 获取客户端真实 IP
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# 自定义头 REMOTE-HOST,记录客户端 IP
|
|
||||||
proxy_set_header REMOTE-HOST $remote_addr;
|
|
||||||
# 获取完整的客户端 IP 链(经过多级代理时)
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# 禁用代理缓冲,数据直接传给客户端
|
|
||||||
proxy_buffering off;
|
|
||||||
# 禁用代理缓存
|
|
||||||
proxy_cache off;
|
|
||||||
proxy_pass http://monitor-admin/admin/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /snail-job/ {
|
|
||||||
# 设置客户端请求头中的 Host 信息(保持原始 Host)
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
# 获取客户端真实 IP
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# 自定义头 REMOTE-HOST,记录客户端 IP
|
|
||||||
proxy_set_header REMOTE-HOST $remote_addr;
|
|
||||||
# 获取完整的客户端 IP 链(经过多级代理时)
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# SSE (Server-Sent Events) 与 WebSocket 支持参数
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
# 禁用代理缓冲,直接传输给客户端
|
|
||||||
proxy_buffering off;
|
|
||||||
# 禁用代理缓存
|
|
||||||
proxy_cache off;
|
|
||||||
proxy_pass http://snailjob-server/snail-job/;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
root html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# redis 密码
|
|
||||||
requirepass ruoyi123
|
|
||||||
|
|
||||||
# key 监听器配置
|
|
||||||
# notify-keyspace-events Ex
|
|
||||||
|
|
||||||
# 配置持久化文件存储路径
|
|
||||||
dir /redis/data
|
|
||||||
# 配置rdb
|
|
||||||
# 15分钟内有至少1个key被更改则进行快照
|
|
||||||
save 900 1
|
|
||||||
# 5分钟内有至少10个key被更改则进行快照
|
|
||||||
save 300 10
|
|
||||||
# 1分钟内有至少10000个key被更改则进行快照
|
|
||||||
save 60 10000
|
|
||||||
# 开启压缩
|
|
||||||
rdbcompression yes
|
|
||||||
# rdb文件名 用默认的即可
|
|
||||||
dbfilename dump.rdb
|
|
||||||
|
|
||||||
# 开启aof
|
|
||||||
appendonly yes
|
|
||||||
# 文件名
|
|
||||||
appendfilename "appendonly.aof"
|
|
||||||
# 持久化策略,no:不同步,everysec:每秒一次,always:总是同步,速度比较慢
|
|
||||||
# appendfsync always
|
|
||||||
appendfsync everysec
|
|
||||||
# appendfsync no
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
数据目录 请执行 `chmod 777 /docker/redis/data` 赋予读写权限 否则将无法写入数据
|
|
||||||
File diff suppressed because it is too large
Load Diff
37
pom.xml
37
pom.xml
@@ -59,6 +59,8 @@
|
|||||||
<langgraph4j.version>1.5.3</langgraph4j.version>
|
<langgraph4j.version>1.5.3</langgraph4j.version>
|
||||||
<weaviate.version>1.19.6</weaviate.version>
|
<weaviate.version>1.19.6</weaviate.version>
|
||||||
<dify.version>1.0.7</dify.version>
|
<dify.version>1.0.7</dify.version>
|
||||||
|
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
|
||||||
|
<grpc.version>1.62.2</grpc.version>
|
||||||
<!-- Apache Commons Compress - 用于POI处理ZIP格式 -->
|
<!-- Apache Commons Compress - 用于POI处理ZIP格式 -->
|
||||||
<commons-compress.version>1.27.1</commons-compress.version>
|
<commons-compress.version>1.27.1</commons-compress.version>
|
||||||
|
|
||||||
@@ -129,6 +131,15 @@
|
|||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- gRPC BOM - 解决 Milvus SDK 依赖冲突,强制统一版本 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-bom</artifactId>
|
||||||
|
<version>${grpc.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- hutool 的依赖配置-->
|
<!-- hutool 的依赖配置-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
@@ -352,24 +363,12 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-job</artifactId>
|
|
||||||
<version>${revision}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-generator</artifactId>
|
<artifactId>ruoyi-generator</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-demo</artifactId>
|
|
||||||
<version>${revision}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-chat</artifactId>
|
<artifactId>ruoyi-chat</artifactId>
|
||||||
@@ -383,13 +382,6 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 微信模块 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-wechat</artifactId>
|
|
||||||
<version>${revision}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- AI流程编排模块 -->
|
<!-- AI流程编排模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
@@ -397,13 +389,6 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 企业微信SDK -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.binarywang</groupId>
|
|
||||||
<artifactId>weixin-java-cp</artifactId>
|
|
||||||
<version>${weixin-java-cp.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Jackson XML -->
|
<!-- Jackson XML -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
|||||||
@@ -70,23 +70,12 @@
|
|||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-job</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 代码生成-->
|
<!-- 代码生成-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-generator</artifactId>
|
<artifactId>ruoyi-generator</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- demo模块 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-demo</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-chat</artifactId>
|
<artifactId>ruoyi-chat</artifactId>
|
||||||
@@ -98,12 +87,6 @@
|
|||||||
<artifactId>ruoyi-workflow</artifactId>
|
<artifactId>ruoyi-workflow</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 微信模块 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ruoyi</groupId>
|
|
||||||
<artifactId>ruoyi-wechat</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- AI流程编排模块 -->
|
<!-- AI流程编排模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
|
|||||||
@@ -80,10 +80,16 @@ public class AuthController {
|
|||||||
// 授权类型和客户端id
|
// 授权类型和客户端id
|
||||||
String clientId = loginBody.getClientId();
|
String clientId = loginBody.getClientId();
|
||||||
String grantType = loginBody.getGrantType();
|
String grantType = loginBody.getGrantType();
|
||||||
|
log.info("登录请求 - clientId: {}, grantType: {}", clientId, grantType);
|
||||||
SysClientVo client = clientService.queryByClientId(clientId);
|
SysClientVo client = clientService.queryByClientId(clientId);
|
||||||
|
log.info("查询客户端结果 - client: {}, grantType: {}", client, client != null ? client.getGrantType() : "null");
|
||||||
// 查询不到 client 或 client 内不包含 grantType
|
// 查询不到 client 或 client 内不包含 grantType
|
||||||
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
|
if (ObjectUtil.isNull(client)) {
|
||||||
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
|
log.info("客户端id: {} 不存在!", clientId);
|
||||||
|
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
||||||
|
}
|
||||||
|
if (!StringUtils.contains(client.getGrantType(), grantType)) {
|
||||||
|
log.info("客户端id: {} 认证类型:{} 不匹配! 数据库grantType: {}", clientId, grantType, client.getGrantType());
|
||||||
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
||||||
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
|
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
|
||||||
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
|
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
|
||||||
|
|||||||
@@ -268,4 +268,4 @@ justauth:
|
|||||||
client-secret: 1f7d08**********5b7**********29e
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
||||||
|
|
||||||
AGENT_ALLOWED_TABLES: "abtest_rule,abtest_project,agent_ban_log,agent_ban_logs,agent_install_sub_task,agent_install_sum_task,agent_install_task"
|
AGENT_ALLOWED_TABLES: ""
|
||||||
|
|||||||
@@ -125,9 +125,6 @@ security:
|
|||||||
- /*/api-docs/**
|
- /*/api-docs/**
|
||||||
- /warm-flow-ui/config
|
- /warm-flow-ui/config
|
||||||
- /workflow/run
|
- /workflow/run
|
||||||
- /user/qrcode
|
|
||||||
- /user/login/qrcode
|
|
||||||
- /weixin/check
|
|
||||||
# 多租户配置
|
# 多租户配置
|
||||||
tenant:
|
tenant:
|
||||||
# 是否开启
|
# 是否开启
|
||||||
@@ -206,6 +203,7 @@ springdoc:
|
|||||||
name: ageerle
|
name: ageerle
|
||||||
email: ageerle@163.com
|
email: ageerle@163.com
|
||||||
url: https://gitee.com/ageerle/ruoyi-ai
|
url: https://gitee.com/ageerle/ruoyi-ai
|
||||||
|
|
||||||
#这里定义了两个分组,可定义多个,也可以不定义
|
#这里定义了两个分组,可定义多个,也可以不定义
|
||||||
group-configs:
|
group-configs:
|
||||||
- group: 1.演示模块
|
- group: 1.演示模块
|
||||||
@@ -247,12 +245,6 @@ management:
|
|||||||
show-details: ALWAYS
|
show-details: ALWAYS
|
||||||
logfile:
|
logfile:
|
||||||
external-file: ./logs/sys-console.log
|
external-file: ./logs/sys-console.log
|
||||||
health:
|
|
||||||
# ⚠️ 禁用 Neo4j 健康检查
|
|
||||||
# Spring Boot Actuator 会自动为 Neo4j 创建健康检查器
|
|
||||||
# 这会导致应用在启动时尝试连接到 Neo4j
|
|
||||||
neo4j:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
--- # 默认/推荐使用sse推送
|
--- # 默认/推荐使用sse推送
|
||||||
sse:
|
sse:
|
||||||
@@ -285,7 +277,7 @@ warm-flow:
|
|||||||
vector-store:
|
vector-store:
|
||||||
# 向量存储类型 可选(weaviate/milvus)
|
# 向量存储类型 可选(weaviate/milvus)
|
||||||
# 如需修改向量库类型,请修改此配置值!
|
# 如需修改向量库类型,请修改此配置值!
|
||||||
type: weaviate
|
type: milvus
|
||||||
# Weaviate配置
|
# Weaviate配置
|
||||||
weaviate:
|
weaviate:
|
||||||
protocol: http
|
protocol: http
|
||||||
@@ -295,81 +287,3 @@ vector-store:
|
|||||||
milvus:
|
milvus:
|
||||||
url: http://localhost:19530
|
url: http://localhost:19530
|
||||||
collectionname: LocalKnowledge
|
collectionname: LocalKnowledge
|
||||||
|
|
||||||
chat:
|
|
||||||
memory:
|
|
||||||
enabled: true
|
|
||||||
maxMessages: 20
|
|
||||||
persistenceEnabled: true
|
|
||||||
|
|
||||||
# 企业微信应用
|
|
||||||
wechat:
|
|
||||||
cp:
|
|
||||||
corpId:
|
|
||||||
appConfigs:
|
|
||||||
- agentId:
|
|
||||||
secret: ''
|
|
||||||
token: ''
|
|
||||||
aesKey: ''
|
|
||||||
|
|
||||||
--- # Neo4j 知识图谱配置
|
|
||||||
neo4j:
|
|
||||||
uri: bolt://117.72.192.162:7687
|
|
||||||
username: neo4j
|
|
||||||
password: MySecurePass123!
|
|
||||||
database: neo4j
|
|
||||||
max-connection-pool-size: 50
|
|
||||||
connection-timeout-seconds: 30
|
|
||||||
|
|
||||||
# 知识图谱配置
|
|
||||||
knowledge:
|
|
||||||
graph:
|
|
||||||
# 是否启用知识图谱功能
|
|
||||||
enabled: false
|
|
||||||
# 图数据库类型: neo4j 或 apache-age
|
|
||||||
database-type: neo4j
|
|
||||||
# 是否自动创建索引
|
|
||||||
auto-create-index: true
|
|
||||||
# 批量处理大小
|
|
||||||
batch-size: 1000
|
|
||||||
# 最大重试次数
|
|
||||||
max-retry-count: 3
|
|
||||||
|
|
||||||
# 实体抽取配置
|
|
||||||
extraction:
|
|
||||||
# 置信度阈值(低于此值的实体将被过滤)
|
|
||||||
confidence-threshold: 0.7
|
|
||||||
# 最大实体数量(每个文档)
|
|
||||||
max-entities-per-doc: 100
|
|
||||||
# 最大关系数量(每个文档)
|
|
||||||
max-relations-per-doc: 200
|
|
||||||
# 文本分片大小(用于长文档)
|
|
||||||
chunk-size: 2000
|
|
||||||
# 分片重叠大小
|
|
||||||
chunk-overlap: 200
|
|
||||||
|
|
||||||
# 查询配置
|
|
||||||
query:
|
|
||||||
# 默认查询限制数量
|
|
||||||
default-limit: 100
|
|
||||||
# 最大查询限制数量
|
|
||||||
max-limit: 1000
|
|
||||||
# 路径查询最大深度
|
|
||||||
max-path-depth: 5
|
|
||||||
# 查询超时时间(秒)
|
|
||||||
timeout-seconds: 30
|
|
||||||
# 是否启用查询缓存
|
|
||||||
cache-enabled: true
|
|
||||||
# 缓存过期时间(分钟)
|
|
||||||
cache-expire-minutes: 60
|
|
||||||
|
|
||||||
--- # MCP 模块配置
|
|
||||||
app:
|
|
||||||
mcp:
|
|
||||||
client:
|
|
||||||
# 请求超时时间(秒)
|
|
||||||
request-timeout: 30
|
|
||||||
# 连接超时时间(秒)
|
|
||||||
connection-timeout: 10
|
|
||||||
# 最大重试次数
|
|
||||||
max-retries: 3
|
|
||||||
|
|||||||
@@ -48,11 +48,6 @@ public class ChatMessageBo extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
/**
|
|
||||||
* 扣除金额
|
|
||||||
*/
|
|
||||||
private Long deductCost;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 累计 Tokens
|
* 累计 Tokens
|
||||||
*/
|
*/
|
||||||
@@ -63,11 +58,6 @@ public class ChatMessageBo extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String modelName;
|
private String modelName;
|
||||||
|
|
||||||
/**
|
|
||||||
* 计费类型(1-token计费,2-次数计费)
|
|
||||||
*/
|
|
||||||
private String billingType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 备注
|
* 备注
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -45,30 +45,15 @@ public class ChatModelBo extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String modelDescribe;
|
private String modelDescribe;
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型价格
|
|
||||||
*/
|
|
||||||
private Long modelPrice;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计费类型
|
|
||||||
*/
|
|
||||||
private String modelType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示
|
* 是否显示
|
||||||
*/
|
*/
|
||||||
private String modelShow;
|
private String modelShow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否免费
|
* 向量维度
|
||||||
*/
|
*/
|
||||||
private String modelFree;
|
private Integer modelDimension;
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型优先级(值越大优先级越高)
|
|
||||||
*/
|
|
||||||
private Long priority;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求地址
|
* 请求地址
|
||||||
|
|||||||
@@ -56,12 +56,6 @@ public class ChatMessageVo implements Serializable {
|
|||||||
@ExcelProperty(value = "对话角色")
|
@ExcelProperty(value = "对话角色")
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
/**
|
|
||||||
* 扣除金额
|
|
||||||
*/
|
|
||||||
@ExcelProperty(value = "扣除金额")
|
|
||||||
private Long deductCost;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 累计 Tokens
|
* 累计 Tokens
|
||||||
*/
|
*/
|
||||||
@@ -74,12 +68,6 @@ public class ChatMessageVo implements Serializable {
|
|||||||
@ExcelProperty(value = "模型名称")
|
@ExcelProperty(value = "模型名称")
|
||||||
private String modelName;
|
private String modelName;
|
||||||
|
|
||||||
/**
|
|
||||||
* 计费类型(1-token计费,2-次数计费)
|
|
||||||
*/
|
|
||||||
@ExcelProperty(value = "计费类型", converter = ExcelDictConvert.class)
|
|
||||||
@ExcelDictFormat(readConverterExp = "1=-token计费,2-次数计费")
|
|
||||||
private String billingType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 备注
|
* 备注
|
||||||
|
|||||||
@@ -54,17 +54,6 @@ public class ChatModelVo implements Serializable {
|
|||||||
@ExcelProperty(value = "模型描述")
|
@ExcelProperty(value = "模型描述")
|
||||||
private String modelDescribe;
|
private String modelDescribe;
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型价格
|
|
||||||
*/
|
|
||||||
@ExcelProperty(value = "模型价格")
|
|
||||||
private Long modelPrice;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计费类型
|
|
||||||
*/
|
|
||||||
@ExcelProperty(value = "计费类型")
|
|
||||||
private String modelType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示
|
* 是否显示
|
||||||
@@ -73,16 +62,10 @@ public class ChatModelVo implements Serializable {
|
|||||||
private String modelShow;
|
private String modelShow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否免费
|
* 向量维度
|
||||||
*/
|
*/
|
||||||
@ExcelProperty(value = "是否免费")
|
@ExcelProperty(value = "向量维度")
|
||||||
private String modelFree;
|
private Integer modelDimension;
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型优先级(值越大优先级越高)
|
|
||||||
*/
|
|
||||||
@ExcelProperty(value = "模型优先级(值越大优先级越高)")
|
|
||||||
private Long priority;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求地址
|
* 请求地址
|
||||||
@@ -102,11 +85,5 @@ public class ChatModelVo implements Serializable {
|
|||||||
@ExcelProperty(value = "备注")
|
@ExcelProperty(value = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型维度
|
|
||||||
*/
|
|
||||||
private Integer dimension;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
|
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,4 +55,9 @@ public class ChatContext {
|
|||||||
* 响应处理器
|
* 响应处理器
|
||||||
*/
|
*/
|
||||||
private StreamingChatResponseHandler handler;
|
private StreamingChatResponseHandler handler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天服务实例
|
||||||
|
*/
|
||||||
|
private IChatService chatService;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ public class ChatMessage extends TenantEntity {
|
|||||||
*/
|
*/
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
/**
|
|
||||||
* 扣除金额
|
|
||||||
*/
|
|
||||||
private Long deductCost;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 累计 Tokens
|
* 累计 Tokens
|
||||||
@@ -63,11 +59,6 @@ public class ChatMessage extends TenantEntity {
|
|||||||
*/
|
*/
|
||||||
private String modelName;
|
private String modelName;
|
||||||
|
|
||||||
/**
|
|
||||||
* 计费类型(1-token计费,2-次数计费)
|
|
||||||
*/
|
|
||||||
private String billingType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 备注
|
* 备注
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -48,15 +48,6 @@ public class ChatModel extends TenantEntity {
|
|||||||
*/
|
*/
|
||||||
private String modelDescribe;
|
private String modelDescribe;
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型价格
|
|
||||||
*/
|
|
||||||
private Long modelPrice;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计费类型
|
|
||||||
*/
|
|
||||||
private String modelType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示
|
* 是否显示
|
||||||
@@ -64,14 +55,9 @@ public class ChatModel extends TenantEntity {
|
|||||||
private String modelShow;
|
private String modelShow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否免费
|
* 向量维度
|
||||||
*/
|
*/
|
||||||
private String modelFree;
|
private Integer modelDimension;
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型优先级(值越大优先级越高)
|
|
||||||
*/
|
|
||||||
private Long priority;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求地址
|
* 请求地址
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ public abstract class AbstractChatMessageService {
|
|||||||
messageBO.setContent(content);
|
messageBO.setContent(content);
|
||||||
messageBO.setRole(role);
|
messageBO.setRole(role);
|
||||||
messageBO.setModelName(chatRequest.getModel());
|
messageBO.setModelName(chatRequest.getModel());
|
||||||
messageBO.setBillingType(chatModelVo.getModelType());
|
|
||||||
messageBO.setRemark(null);
|
messageBO.setRemark(null);
|
||||||
|
|
||||||
chatMessageService.insertByBo(messageBO);
|
chatMessageService.insertByBo(messageBO);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>ruoyi-ai-copilot</module>
|
|
||||||
<module>ruoyi-monitor-admin</module>
|
<module>ruoyi-monitor-admin</module>
|
||||||
<module>ruoyi-snailjob-server</module>
|
<module>ruoyi-snailjob-server</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
|
||||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<groupId>com.example</groupId>
|
|
||||||
<artifactId>ruoyi-ai-copilot</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<name>copilot</name>
|
|
||||||
<description>SpringAI - Alibaba - Copilot</description>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<spring-boot.version>4.0.1</spring-boot.version>
|
|
||||||
<spring-ai.version>2.0.0-M2</spring-ai.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencyManagement>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-dependencies</artifactId>
|
|
||||||
<version>${spring-boot.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.ai</groupId>
|
|
||||||
<artifactId>spring-ai-bom</artifactId>
|
|
||||||
<version>${spring-ai.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</dependencyManagement>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<!-- Spring Boot Starters -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Spring AI -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.ai</groupId>
|
|
||||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.ai</groupId>
|
|
||||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springaicommunity</groupId>
|
|
||||||
<artifactId>spring-ai-agent-utils</artifactId>
|
|
||||||
<version>0.4.2</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- JSON Processing -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-databind</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- JSON Schema Validation -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.networknt</groupId>
|
|
||||||
<artifactId>json-schema-validator</artifactId>
|
|
||||||
<version>1.0.87</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Diff Utils -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.github.java-diff-utils</groupId>
|
|
||||||
<artifactId>java-diff-utils</artifactId>
|
|
||||||
<version>4.12</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Apache Commons -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.commons</groupId>
|
|
||||||
<artifactId>commons-lang3</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
</project>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package com.example.demo;
|
|
||||||
|
|
||||||
import com.example.demo.config.AppProperties;
|
|
||||||
import com.example.demo.utils.BrowserUtil;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主要功能:
|
|
||||||
* 1. 文件读取、写入、编辑
|
|
||||||
* 2. 目录列表和结构查看
|
|
||||||
* 4. 连续性文件操作
|
|
||||||
*/
|
|
||||||
@SpringBootApplication
|
|
||||||
@EnableConfigurationProperties(AppProperties.class)
|
|
||||||
public class CopilotApplication {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CopilotApplication.class);
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AppProperties appProperties;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private Environment environment;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(CopilotApplication.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用启动完成后的事件监听器
|
|
||||||
* 自动打开浏览器访问应用首页
|
|
||||||
*/
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void onApplicationReady() {
|
|
||||||
AppProperties.Browser browserConfig = appProperties.getBrowser();
|
|
||||||
|
|
||||||
if (!browserConfig.isAutoOpen()) {
|
|
||||||
logger.info("Browser auto-open is disabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取实际的服务器端口
|
|
||||||
String port = environment.getProperty("server.port", "8080");
|
|
||||||
String actualUrl = browserConfig.getUrl().replace("${server.port:8080}", port);
|
|
||||||
|
|
||||||
logger.info("Application started successfully!");
|
|
||||||
logger.info("Preparing to open browser in {} seconds...", browserConfig.getDelaySeconds());
|
|
||||||
|
|
||||||
// 在新线程中延迟打开浏览器,避免阻塞主线程
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
Thread.sleep(browserConfig.getDelaySeconds() * 1000L);
|
|
||||||
|
|
||||||
if (BrowserUtil.isValidUrl(actualUrl)) {
|
|
||||||
boolean success = BrowserUtil.openBrowser(actualUrl);
|
|
||||||
if (success) {
|
|
||||||
logger.info("✅ Browser opened successfully: {}", actualUrl);
|
|
||||||
System.out.println("🌐 Web interface opened: " + actualUrl);
|
|
||||||
} else {
|
|
||||||
logger.warn("❌ Failed to open browser automatically");
|
|
||||||
System.out.println("⚠️ Please manually open: " + actualUrl);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error("❌ Invalid URL: {}", actualUrl);
|
|
||||||
System.out.println("⚠️ Invalid URL configured: " + actualUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
logger.warn("Browser opening was interrupted", e);
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
package com.example.demo.config;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用配置属性
|
|
||||||
*/
|
|
||||||
@ConfigurationProperties(prefix = "app")
|
|
||||||
public class AppProperties {
|
|
||||||
|
|
||||||
private Workspace workspace = new Workspace();
|
|
||||||
private Security security = new Security();
|
|
||||||
private Tools tools = new Tools();
|
|
||||||
private Browser browser = new Browser();
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public Workspace getWorkspace() {
|
|
||||||
return workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWorkspace(Workspace workspace) {
|
|
||||||
this.workspace = workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Security getSecurity() {
|
|
||||||
return security;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSecurity(Security security) {
|
|
||||||
this.security = security;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Tools getTools() {
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTools(Tools tools) {
|
|
||||||
this.tools = tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Browser getBrowser() {
|
|
||||||
return browser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBrowser(Browser browser) {
|
|
||||||
this.browser = browser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 审批模式
|
|
||||||
*/
|
|
||||||
public enum ApprovalMode {
|
|
||||||
DEFAULT, // 默认模式,危险操作需要确认
|
|
||||||
AUTO_EDIT, // 自动编辑模式,文件编辑不需要确认
|
|
||||||
YOLO // 完全自动模式,所有操作都不需要确认
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作空间配置
|
|
||||||
*/
|
|
||||||
public static class Workspace {
|
|
||||||
// 使用 Paths.get() 和 File.separator 实现跨平台兼容
|
|
||||||
private String rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
|
||||||
private long maxFileSize = 10485760L; // 10MB
|
|
||||||
private List<String> allowedExtensions = List.of(
|
|
||||||
".txt", ".md", ".java", ".js", ".ts", ".json", ".xml",
|
|
||||||
".yml", ".yaml", ".properties", ".html", ".css", ".sql"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getRootDirectory() {
|
|
||||||
return rootDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRootDirectory(String rootDirectory) {
|
|
||||||
// 确保设置的路径也是跨平台兼容的
|
|
||||||
this.rootDirectory = Paths.get(rootDirectory).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getMaxFileSize() {
|
|
||||||
return maxFileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaxFileSize(long maxFileSize) {
|
|
||||||
this.maxFileSize = maxFileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getAllowedExtensions() {
|
|
||||||
return allowedExtensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAllowedExtensions(List<String> allowedExtensions) {
|
|
||||||
this.allowedExtensions = allowedExtensions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安全配置
|
|
||||||
*/
|
|
||||||
public static class Security {
|
|
||||||
private ApprovalMode approvalMode = ApprovalMode.DEFAULT;
|
|
||||||
private List<String> dangerousCommands = List.of("rm", "del", "format", "fdisk", "mkfs");
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public ApprovalMode getApprovalMode() {
|
|
||||||
return approvalMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setApprovalMode(ApprovalMode approvalMode) {
|
|
||||||
this.approvalMode = approvalMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getDangerousCommands() {
|
|
||||||
return dangerousCommands;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDangerousCommands(List<String> dangerousCommands) {
|
|
||||||
this.dangerousCommands = dangerousCommands;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工具配置
|
|
||||||
*/
|
|
||||||
public static class Tools {
|
|
||||||
private ToolConfig readFile = new ToolConfig(true);
|
|
||||||
private ToolConfig writeFile = new ToolConfig(true);
|
|
||||||
private ToolConfig editFile = new ToolConfig(true);
|
|
||||||
private ToolConfig listDirectory = new ToolConfig(true);
|
|
||||||
private ToolConfig shell = new ToolConfig(true);
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public ToolConfig getReadFile() {
|
|
||||||
return readFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReadFile(ToolConfig readFile) {
|
|
||||||
this.readFile = readFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ToolConfig getWriteFile() {
|
|
||||||
return writeFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWriteFile(ToolConfig writeFile) {
|
|
||||||
this.writeFile = writeFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ToolConfig getEditFile() {
|
|
||||||
return editFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEditFile(ToolConfig editFile) {
|
|
||||||
this.editFile = editFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ToolConfig getListDirectory() {
|
|
||||||
return listDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListDirectory(ToolConfig listDirectory) {
|
|
||||||
this.listDirectory = listDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ToolConfig getShell() {
|
|
||||||
return shell;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setShell(ToolConfig shell) {
|
|
||||||
this.shell = shell;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工具配置
|
|
||||||
*/
|
|
||||||
public static class ToolConfig {
|
|
||||||
private boolean enabled;
|
|
||||||
|
|
||||||
public ToolConfig() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public ToolConfig(boolean enabled) {
|
|
||||||
this.enabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnabled(boolean enabled) {
|
|
||||||
this.enabled = enabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 浏览器配置
|
|
||||||
*/
|
|
||||||
public static class Browser {
|
|
||||||
private boolean autoOpen = true;
|
|
||||||
private String url = "http://localhost:8080";
|
|
||||||
private int delaySeconds = 2;
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public boolean isAutoOpen() {
|
|
||||||
return autoOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAutoOpen(boolean autoOpen) {
|
|
||||||
this.autoOpen = autoOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDelaySeconds() {
|
|
||||||
return delaySeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDelaySeconds(int delaySeconds) {
|
|
||||||
this.delaySeconds = delaySeconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package com.example.demo.config;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
||||||
import org.springframework.web.context.request.WebRequest;
|
|
||||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局异常处理器
|
|
||||||
*/
|
|
||||||
@ControllerAdvice
|
|
||||||
public class GlobalExceptionHandler {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理超时异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler({TimeoutException.class, AsyncRequestTimeoutException.class})
|
|
||||||
public ResponseEntity<ErrorResponse> handleTimeoutException(Exception e, WebRequest request) {
|
|
||||||
logger.error("Request timeout occurred", e);
|
|
||||||
|
|
||||||
ErrorResponse errorResponse = new ErrorResponse(
|
|
||||||
"TIMEOUT_ERROR",
|
|
||||||
"Request timed out. The operation took too long to complete.",
|
|
||||||
"Please try again with a simpler request or check your network connection."
|
|
||||||
);
|
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(errorResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理AI相关异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(RuntimeException.class)
|
|
||||||
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e, WebRequest request) {
|
|
||||||
logger.error("Runtime exception occurred", e);
|
|
||||||
|
|
||||||
// 检查是否是AI调用相关的异常
|
|
||||||
String message = e.getMessage();
|
|
||||||
if (message != null && (message.contains("tool") || message.contains("function") || message.contains("AI"))) {
|
|
||||||
ErrorResponse errorResponse = new ErrorResponse(
|
|
||||||
"AI_TOOL_ERROR",
|
|
||||||
"An error occurred during AI tool execution: " + message,
|
|
||||||
"The AI encountered an issue while processing your request. Please try rephrasing your request or try again."
|
|
||||||
);
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorResponse errorResponse = new ErrorResponse(
|
|
||||||
"RUNTIME_ERROR",
|
|
||||||
"An unexpected error occurred: " + message,
|
|
||||||
"Please try again. If the problem persists, contact support."
|
|
||||||
);
|
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理所有其他异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(Exception.class)
|
|
||||||
public ResponseEntity<ErrorResponse> handleGenericException(Exception e, WebRequest request) {
|
|
||||||
logger.error("Unexpected exception occurred", e);
|
|
||||||
|
|
||||||
ErrorResponse errorResponse = new ErrorResponse(
|
|
||||||
"INTERNAL_ERROR",
|
|
||||||
"An internal server error occurred",
|
|
||||||
"Something went wrong on our end. Please try again later."
|
|
||||||
);
|
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误响应类
|
|
||||||
*/
|
|
||||||
public static class ErrorResponse {
|
|
||||||
private String errorCode;
|
|
||||||
private String message;
|
|
||||||
private String suggestion;
|
|
||||||
private long timestamp;
|
|
||||||
|
|
||||||
public ErrorResponse(String errorCode, String message, String suggestion) {
|
|
||||||
this.errorCode = errorCode;
|
|
||||||
this.message = message;
|
|
||||||
this.suggestion = suggestion;
|
|
||||||
this.timestamp = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and setters
|
|
||||||
public String getErrorCode() {
|
|
||||||
return errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setErrorCode(String errorCode) {
|
|
||||||
this.errorCode = errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMessage(String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSuggestion() {
|
|
||||||
return suggestion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSuggestion(String suggestion) {
|
|
||||||
this.suggestion = suggestion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(long timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.example.demo.config;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志配置类
|
|
||||||
* 确保日志目录存在并记录应用启动信息
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class LoggingConfiguration {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LoggingConfiguration.class);
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void onApplicationReady() {
|
|
||||||
// 确保日志目录存在
|
|
||||||
File logsDir = new File("logs");
|
|
||||||
if (!logsDir.exists()) {
|
|
||||||
boolean created = logsDir.mkdirs();
|
|
||||||
if (created) {
|
|
||||||
logger.info("📁 创建日志目录: {}", logsDir.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录应用启动信息
|
|
||||||
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
|
||||||
logger.info("🎉 ========================================");
|
|
||||||
logger.info("🚀 (♥◠‿◠)ノ゙ AI Copilot启动成功 ლ(´ڡ`ლ)゙");
|
|
||||||
logger.info("🕐 启动时间: {}", startTime);
|
|
||||||
logger.info("📝 日志级别: DEBUG (工具调用详细日志已启用)");
|
|
||||||
logger.info("📁 日志文件: logs/copilot-file-ops.log");
|
|
||||||
logger.info("🔧 支持的工具:");
|
|
||||||
logger.info(" 📖 read_file - 读取文件内容");
|
|
||||||
logger.info(" ✏️ write_file - 写入文件内容");
|
|
||||||
logger.info(" 📝 edit_file - 编辑文件内容");
|
|
||||||
logger.info(" 🔍 analyze_project - 分析项目结构");
|
|
||||||
logger.info(" 🏗️ scaffold_project - 创建项目脚手架");
|
|
||||||
logger.info(" 🧠 smart_edit - 智能编辑项目");
|
|
||||||
logger.info("🎉 ========================================");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package com.example.demo.config;
|
|
||||||
|
|
||||||
import org.springaicommunity.agent.tools.FileSystemTools;
|
|
||||||
import org.springaicommunity.agent.tools.ShellTools;
|
|
||||||
import org.springaicommunity.agent.tools.SkillsTool;
|
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
|
||||||
import org.springframework.ai.chat.model.ChatModel;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spring AI 配置 - 使用Spring AI 1.0.0规范
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class SpringAIConfiguration {
|
|
||||||
|
|
||||||
@Value("${agent.skills.dirs:Unknown}") List<Resource> agentSkillsDirs;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public ChatClient chatClient(ChatModel chatModel, AppProperties appProperties) {
|
|
||||||
// 动态获取工作目录路径
|
|
||||||
ChatClient.Builder chatClientBuilder = ChatClient.builder(chatModel);
|
|
||||||
|
|
||||||
return chatClientBuilder
|
|
||||||
.defaultSystem("Always use the available skills to assist the user in their requests.")
|
|
||||||
// Skills tool callbacks
|
|
||||||
.defaultToolCallbacks(SkillsTool.builder().addSkillsResources(agentSkillsDirs).build())
|
|
||||||
// Built-in tools
|
|
||||||
.defaultTools(
|
|
||||||
// FileSystemTools.builder().build(),
|
|
||||||
ShellTools.builder().build()
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.example.demo.config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务上下文持有者
|
|
||||||
* 使用ThreadLocal存储当前任务ID,供AOP切面使用
|
|
||||||
*/
|
|
||||||
public class TaskContextHolder {
|
|
||||||
|
|
||||||
private static final ThreadLocal<String> taskIdHolder = new ThreadLocal<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前任务ID
|
|
||||||
*/
|
|
||||||
public static String getCurrentTaskId() {
|
|
||||||
return taskIdHolder.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前任务ID
|
|
||||||
*/
|
|
||||||
public static void setCurrentTaskId(String taskId) {
|
|
||||||
taskIdHolder.set(taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除当前任务ID
|
|
||||||
*/
|
|
||||||
public static void clearCurrentTaskId() {
|
|
||||||
taskIdHolder.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否有当前任务ID
|
|
||||||
*/
|
|
||||||
public static boolean hasCurrentTaskId() {
|
|
||||||
return taskIdHolder.get() != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
package com.example.demo.controller;
|
|
||||||
|
|
||||||
import com.example.demo.dto.ChatRequestDto;
|
|
||||||
import com.example.demo.service.ContinuousConversationService;
|
|
||||||
import com.example.demo.service.ToolExecutionLogger;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
|
||||||
import org.springframework.ai.chat.messages.Message;
|
|
||||||
import org.springframework.ai.chat.messages.UserMessage;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 聊天控制器
|
|
||||||
* 处理与AI的对话和工具调用
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/chat")
|
|
||||||
@CrossOrigin(origins = "*")
|
|
||||||
public class ChatController {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
|
|
||||||
|
|
||||||
private final ChatClient chatClient;
|
|
||||||
private final ContinuousConversationService continuousConversationService;
|
|
||||||
private final ToolExecutionLogger executionLogger;
|
|
||||||
|
|
||||||
// 简单的会话存储(生产环境应该使用数据库或Redis)
|
|
||||||
private final List<Message> conversationHistory = new ArrayList<>();
|
|
||||||
|
|
||||||
public ChatController(ChatClient chatClient, ContinuousConversationService continuousConversationService, ToolExecutionLogger executionLogger) {
|
|
||||||
this.chatClient = chatClient;
|
|
||||||
this.continuousConversationService = continuousConversationService;
|
|
||||||
this.executionLogger = executionLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 流式聊天 - 直接返回流式数据
|
|
||||||
*/
|
|
||||||
@PostMapping(value = "/message", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
||||||
public Flux<String> streamMessage(@RequestBody ChatRequestDto request) {
|
|
||||||
logger.info("📨 开始流式聊天: {}", request.getMessage());
|
|
||||||
|
|
||||||
return Flux.create(sink -> {
|
|
||||||
try {
|
|
||||||
UserMessage userMessage = new UserMessage(request.getMessage());
|
|
||||||
conversationHistory.add(userMessage);
|
|
||||||
|
|
||||||
// 使用Spring AI的流式API
|
|
||||||
Flux<String> contentStream = chatClient.prompt()
|
|
||||||
.messages(conversationHistory)
|
|
||||||
.stream()
|
|
||||||
.content();
|
|
||||||
|
|
||||||
// 订阅流式内容并转发给前端
|
|
||||||
contentStream
|
|
||||||
.doOnNext(content -> {
|
|
||||||
logger.debug("📨 流式内容片段: {}", content);
|
|
||||||
// 发送内容片段(SSE格式会自动添加 "data: " 前缀)
|
|
||||||
sink.next(content);
|
|
||||||
})
|
|
||||||
.doOnComplete(() -> {
|
|
||||||
logger.info("✅ 流式聊天完成");
|
|
||||||
// 发送完成标记
|
|
||||||
sink.next("[DONE]");
|
|
||||||
sink.complete();
|
|
||||||
})
|
|
||||||
.doOnError(error -> {
|
|
||||||
logger.error("❌ 流式聊天错误: {}", error.getMessage());
|
|
||||||
sink.error(error);
|
|
||||||
})
|
|
||||||
.subscribe();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("❌ 流式聊天启动失败: {}", e.getMessage());
|
|
||||||
sink.error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除对话历史
|
|
||||||
*/
|
|
||||||
@PostMapping("/clear")
|
|
||||||
public Mono<Map<String, String>> clearHistory() {
|
|
||||||
conversationHistory.clear();
|
|
||||||
logger.info("Conversation history cleared");
|
|
||||||
return Mono.just(Map.of("status", "success", "message", "Conversation history cleared"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取对话历史
|
|
||||||
*/
|
|
||||||
@GetMapping("/history")
|
|
||||||
public Mono<List<MessageDto>> getHistory() {
|
|
||||||
List<MessageDto> history = conversationHistory.stream()
|
|
||||||
.map(message -> {
|
|
||||||
MessageDto dto = new MessageDto();
|
|
||||||
dto.setContent(message.getText());
|
|
||||||
dto.setRole(message instanceof UserMessage ? "user" : "assistant");
|
|
||||||
return dto;
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return Mono.just(history);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注意:Spring AI 1.0.0 使用不同的函数调用方式
|
|
||||||
// 函数需要在配置中注册,而不是在运行时动态创建
|
|
||||||
|
|
||||||
public static class ChatResponseDto {
|
|
||||||
private String taskId;
|
|
||||||
private String message;
|
|
||||||
private boolean success;
|
|
||||||
private boolean asyncTask;
|
|
||||||
private boolean streamResponse; // 新增:标识是否为流式响应
|
|
||||||
private int totalTurns;
|
|
||||||
private boolean reachedMaxTurns;
|
|
||||||
private String stopReason;
|
|
||||||
private long totalDurationMs;
|
|
||||||
|
|
||||||
public String getTaskId() {
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTaskId(String taskId) {
|
|
||||||
this.taskId = taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMessage(String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSuccess() {
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSuccess(boolean success) {
|
|
||||||
this.success = success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAsyncTask() {
|
|
||||||
return asyncTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAsyncTask(boolean asyncTask) {
|
|
||||||
this.asyncTask = asyncTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isStreamResponse() {
|
|
||||||
return streamResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStreamResponse(boolean streamResponse) {
|
|
||||||
this.streamResponse = streamResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalTurns() {
|
|
||||||
return totalTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalTurns(int totalTurns) {
|
|
||||||
this.totalTurns = totalTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isReachedMaxTurns() {
|
|
||||||
return reachedMaxTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReachedMaxTurns(boolean reachedMaxTurns) {
|
|
||||||
this.reachedMaxTurns = reachedMaxTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStopReason() {
|
|
||||||
return stopReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStopReason(String stopReason) {
|
|
||||||
this.stopReason = stopReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTotalDurationMs() {
|
|
||||||
return totalDurationMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalDurationMs(long totalDurationMs) {
|
|
||||||
this.totalDurationMs = totalDurationMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MessageDto {
|
|
||||||
private String content;
|
|
||||||
private String role;
|
|
||||||
|
|
||||||
public String getContent() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContent(String content) {
|
|
||||||
this.content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRole() {
|
|
||||||
return role;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRole(String role) {
|
|
||||||
this.role = role;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package com.example.demo.controller;
|
|
||||||
|
|
||||||
import com.example.demo.service.LogStreamService;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SSE日志流控制器
|
|
||||||
* 提供SSE连接端点
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/logs")
|
|
||||||
@CrossOrigin(origins = "*")
|
|
||||||
public class LogStreamController {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LogStreamController.class);
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private LogStreamService logStreamService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 建立SSE连接
|
|
||||||
* 前端通过此端点建立实时日志推送连接
|
|
||||||
*/
|
|
||||||
@GetMapping(value = "/stream/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
||||||
public SseEmitter streamLogs(@PathVariable("taskId") String taskId) {
|
|
||||||
logger.info("🔗 收到SSE连接请求: taskId={}", taskId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
SseEmitter emitter = logStreamService.createConnection(taskId);
|
|
||||||
logger.info("✅ SSE连接建立成功: taskId={}", taskId);
|
|
||||||
return emitter;
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("❌ SSE连接建立失败: taskId={}, error={}", taskId, e.getMessage());
|
|
||||||
throw new RuntimeException("Failed to create SSE connection: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭SSE连接
|
|
||||||
*/
|
|
||||||
@PostMapping("/close/{taskId}")
|
|
||||||
public void closeConnection(@PathVariable("taskId") String taskId) {
|
|
||||||
logger.info("🔚 收到关闭SSE连接请求: taskId={}", taskId);
|
|
||||||
logStreamService.closeConnection(taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取连接状态
|
|
||||||
*/
|
|
||||||
@GetMapping("/status")
|
|
||||||
public ConnectionStatus getConnectionStatus() {
|
|
||||||
int activeConnections = logStreamService.getActiveConnectionCount();
|
|
||||||
logger.debug("📊 当前活跃SSE连接数: {}", activeConnections);
|
|
||||||
|
|
||||||
ConnectionStatus status = new ConnectionStatus();
|
|
||||||
status.setActiveConnections(activeConnections);
|
|
||||||
status.setStatus("OK");
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接状态DTO
|
|
||||||
*/
|
|
||||||
public static class ConnectionStatus {
|
|
||||||
private int activeConnections;
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
public int getActiveConnections() {
|
|
||||||
return activeConnections;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setActiveConnections(int activeConnections) {
|
|
||||||
this.activeConnections = activeConnections;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
package com.example.demo.controller;
|
|
||||||
|
|
||||||
import com.example.demo.model.TaskStatus;
|
|
||||||
import com.example.demo.service.ContinuousConversationService;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/task")
|
|
||||||
@CrossOrigin(origins = "*")
|
|
||||||
public class TaskStatusController {
|
|
||||||
|
|
||||||
private final ContinuousConversationService conversationService;
|
|
||||||
|
|
||||||
public TaskStatusController(ContinuousConversationService conversationService) {
|
|
||||||
this.conversationService = conversationService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取任务状态
|
|
||||||
*/
|
|
||||||
@GetMapping("/status/{taskId}")
|
|
||||||
public Mono<TaskStatusDto> getTaskStatus(@PathVariable("taskId") String taskId) {
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
TaskStatus status = conversationService.getTaskStatus(taskId);
|
|
||||||
if (status == null) {
|
|
||||||
throw new RuntimeException("Task not found: " + taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskStatusDto dto = new TaskStatusDto();
|
|
||||||
dto.setTaskId(status.getTaskId());
|
|
||||||
dto.setStatus(status.getStatus());
|
|
||||||
dto.setCurrentAction(status.getCurrentAction());
|
|
||||||
dto.setSummary(status.getSummary());
|
|
||||||
dto.setCurrentTurn(status.getCurrentTurn());
|
|
||||||
dto.setTotalEstimatedTurns(status.getTotalEstimatedTurns());
|
|
||||||
dto.setProgressPercentage(status.getProgressPercentage());
|
|
||||||
dto.setElapsedTime(status.getElapsedTime());
|
|
||||||
dto.setErrorMessage(status.getErrorMessage());
|
|
||||||
|
|
||||||
return dto;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取对话结果
|
|
||||||
*/
|
|
||||||
@GetMapping("/result/{taskId}")
|
|
||||||
public Mono<ConversationResultDto> getConversationResult(@PathVariable("taskId") String taskId) {
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
ContinuousConversationService.ConversationResult result = conversationService.getConversationResult(taskId);
|
|
||||||
if (result == null) {
|
|
||||||
throw new RuntimeException("Conversation result not found: " + taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConversationResultDto dto = new ConversationResultDto();
|
|
||||||
dto.setTaskId(taskId);
|
|
||||||
dto.setFullResponse(result.getFullResponse());
|
|
||||||
dto.setTurnResponses(result.getTurnResponses());
|
|
||||||
dto.setTotalTurns(result.getTotalTurns());
|
|
||||||
dto.setReachedMaxTurns(result.isReachedMaxTurns());
|
|
||||||
dto.setStopReason(result.getStopReason());
|
|
||||||
dto.setTotalDurationMs(result.getTotalDurationMs());
|
|
||||||
|
|
||||||
return dto;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// DTO类
|
|
||||||
public static class TaskStatusDto {
|
|
||||||
private String taskId;
|
|
||||||
private String status;
|
|
||||||
private String currentAction;
|
|
||||||
private String summary;
|
|
||||||
private int currentTurn;
|
|
||||||
private int totalEstimatedTurns;
|
|
||||||
private double progressPercentage;
|
|
||||||
private long elapsedTime;
|
|
||||||
private String errorMessage;
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getTaskId() {
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTaskId(String taskId) {
|
|
||||||
this.taskId = taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCurrentAction() {
|
|
||||||
return currentAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurrentAction(String currentAction) {
|
|
||||||
this.currentAction = currentAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSummary() {
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSummary(String summary) {
|
|
||||||
this.summary = summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCurrentTurn() {
|
|
||||||
return currentTurn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurrentTurn(int currentTurn) {
|
|
||||||
this.currentTurn = currentTurn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalEstimatedTurns() {
|
|
||||||
return totalEstimatedTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalEstimatedTurns(int totalEstimatedTurns) {
|
|
||||||
this.totalEstimatedTurns = totalEstimatedTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getProgressPercentage() {
|
|
||||||
return progressPercentage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgressPercentage(double progressPercentage) {
|
|
||||||
this.progressPercentage = progressPercentage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getElapsedTime() {
|
|
||||||
return elapsedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setElapsedTime(long elapsedTime) {
|
|
||||||
this.elapsedTime = elapsedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getErrorMessage() {
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setErrorMessage(String errorMessage) {
|
|
||||||
this.errorMessage = errorMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对话结果DTO类
|
|
||||||
public static class ConversationResultDto {
|
|
||||||
private String taskId;
|
|
||||||
private String fullResponse;
|
|
||||||
private java.util.List<String> turnResponses;
|
|
||||||
private int totalTurns;
|
|
||||||
private boolean reachedMaxTurns;
|
|
||||||
private String stopReason;
|
|
||||||
private long totalDurationMs;
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getTaskId() {
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTaskId(String taskId) {
|
|
||||||
this.taskId = taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFullResponse() {
|
|
||||||
return fullResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFullResponse(String fullResponse) {
|
|
||||||
this.fullResponse = fullResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public java.util.List<String> getTurnResponses() {
|
|
||||||
return turnResponses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTurnResponses(java.util.List<String> turnResponses) {
|
|
||||||
this.turnResponses = turnResponses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalTurns() {
|
|
||||||
return totalTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalTurns(int totalTurns) {
|
|
||||||
this.totalTurns = totalTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isReachedMaxTurns() {
|
|
||||||
return reachedMaxTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReachedMaxTurns(boolean reachedMaxTurns) {
|
|
||||||
this.reachedMaxTurns = reachedMaxTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStopReason() {
|
|
||||||
return stopReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStopReason(String stopReason) {
|
|
||||||
this.stopReason = stopReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTotalDurationMs() {
|
|
||||||
return totalDurationMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalDurationMs(long totalDurationMs) {
|
|
||||||
this.totalDurationMs = totalDurationMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.example.demo.controller;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Web页面控制器
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
public class WebController {
|
|
||||||
|
|
||||||
@GetMapping("/")
|
|
||||||
public String index() {
|
|
||||||
return "index";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理favicon.ico请求,避免404错误
|
|
||||||
*/
|
|
||||||
@GetMapping("/favicon.ico")
|
|
||||||
public ResponseEntity<Void> favicon() {
|
|
||||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package com.example.demo.dto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 聊天请求数据传输对象
|
|
||||||
*/
|
|
||||||
public class ChatRequestDto {
|
|
||||||
private String message;
|
|
||||||
private String sessionId; // 可选:用于会话管理
|
|
||||||
|
|
||||||
public ChatRequestDto() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatRequestDto(String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatRequestDto(String message, String sessionId) {
|
|
||||||
this.message = message;
|
|
||||||
this.sessionId = sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMessage(String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSessionId() {
|
|
||||||
return sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionId(String sessionId) {
|
|
||||||
this.sessionId = sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ChatRequestDto{" +
|
|
||||||
"message='" + message + '\'' +
|
|
||||||
", sessionId='" + sessionId + '\'' +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
package com.example.demo.model;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project context information
|
|
||||||
* Contains complete project analysis results for AI understanding
|
|
||||||
*/
|
|
||||||
public class ProjectContext {
|
|
||||||
private Path projectRoot;
|
|
||||||
private ProjectType projectType;
|
|
||||||
private ProjectStructure projectStructure;
|
|
||||||
private List<DependencyInfo> dependencies;
|
|
||||||
private List<ConfigFile> configFiles;
|
|
||||||
private CodeStatistics codeStatistics;
|
|
||||||
private Map<String, Object> metadata;
|
|
||||||
private String contextSummary;
|
|
||||||
|
|
||||||
public ProjectContext() {
|
|
||||||
this.dependencies = new ArrayList<>();
|
|
||||||
this.configFiles = new ArrayList<>();
|
|
||||||
this.metadata = new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProjectContext(Path projectRoot) {
|
|
||||||
this();
|
|
||||||
this.projectRoot = projectRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate project context summary
|
|
||||||
*/
|
|
||||||
public String generateContextSummary() {
|
|
||||||
StringBuilder summary = new StringBuilder();
|
|
||||||
|
|
||||||
// Basic information
|
|
||||||
summary.append("=== PROJECT CONTEXT ===\n");
|
|
||||||
summary.append("Project: ").append(projectRoot != null ? projectRoot.getFileName() : "Unknown").append("\n");
|
|
||||||
summary.append("Type: ").append(projectType != null ? projectType.getDisplayName() : "Unknown").append("\n");
|
|
||||||
summary.append("Language: ").append(projectType != null ? projectType.getPrimaryLanguage() : "Unknown").append("\n");
|
|
||||||
summary.append("Package Manager: ").append(projectType != null ? projectType.getPackageManager() : "Unknown").append("\n\n");
|
|
||||||
|
|
||||||
// Project structure
|
|
||||||
if (projectStructure != null) {
|
|
||||||
summary.append("=== PROJECT STRUCTURE ===\n");
|
|
||||||
summary.append(projectStructure.getStructureSummary()).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dependencies
|
|
||||||
if (!dependencies.isEmpty()) {
|
|
||||||
summary.append("=== DEPENDENCIES ===\n");
|
|
||||||
dependencies.stream()
|
|
||||||
.filter(DependencyInfo::isDirectDependency)
|
|
||||||
.limit(10)
|
|
||||||
.forEach(dep -> summary.append("- ").append(dep.toString()).append("\n"));
|
|
||||||
if (dependencies.size() > 10) {
|
|
||||||
summary.append("... and ").append(dependencies.size() - 10).append(" more dependencies\n");
|
|
||||||
}
|
|
||||||
summary.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration files
|
|
||||||
if (!configFiles.isEmpty()) {
|
|
||||||
summary.append("=== CONFIGURATION FILES ===\n");
|
|
||||||
configFiles.stream()
|
|
||||||
.filter(ConfigFile::isMainConfig)
|
|
||||||
.forEach(config -> summary.append("- ").append(config.getFileName())
|
|
||||||
.append(" (").append(config.getFileType()).append(")\n"));
|
|
||||||
summary.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Code statistics
|
|
||||||
if (codeStatistics != null) {
|
|
||||||
summary.append("=== CODE STATISTICS ===\n");
|
|
||||||
summary.append("Total Lines: ").append(codeStatistics.getTotalLines()).append("\n");
|
|
||||||
summary.append("Code Lines: ").append(codeStatistics.getCodeLines()).append("\n");
|
|
||||||
if (codeStatistics.getTotalClasses() > 0) {
|
|
||||||
summary.append("Classes: ").append(codeStatistics.getTotalClasses()).append("\n");
|
|
||||||
}
|
|
||||||
if (codeStatistics.getTotalMethods() > 0) {
|
|
||||||
summary.append("Methods: ").append(codeStatistics.getTotalMethods()).append("\n");
|
|
||||||
}
|
|
||||||
summary.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.contextSummary = summary.toString();
|
|
||||||
return this.contextSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get dependency summary
|
|
||||||
*/
|
|
||||||
public String getDependencySummary() {
|
|
||||||
if (dependencies.isEmpty()) {
|
|
||||||
return "No dependencies found";
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies.stream()
|
|
||||||
.filter(DependencyInfo::isDirectDependency)
|
|
||||||
.limit(5)
|
|
||||||
.map(DependencyInfo::getName)
|
|
||||||
.reduce((a, b) -> a + ", " + b)
|
|
||||||
.orElse("No direct dependencies");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public Path getProjectRoot() {
|
|
||||||
return projectRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProjectRoot(Path projectRoot) {
|
|
||||||
this.projectRoot = projectRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProjectType getProjectType() {
|
|
||||||
return projectType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProjectType(ProjectType projectType) {
|
|
||||||
this.projectType = projectType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProjectStructure getProjectStructure() {
|
|
||||||
return projectStructure;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProjectStructure(ProjectStructure projectStructure) {
|
|
||||||
this.projectStructure = projectStructure;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DependencyInfo> getDependencies() {
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDependencies(List<DependencyInfo> dependencies) {
|
|
||||||
this.dependencies = dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ConfigFile> getConfigFiles() {
|
|
||||||
return configFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setConfigFiles(List<ConfigFile> configFiles) {
|
|
||||||
this.configFiles = configFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CodeStatistics getCodeStatistics() {
|
|
||||||
return codeStatistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCodeStatistics(CodeStatistics codeStatistics) {
|
|
||||||
this.codeStatistics = codeStatistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> getMetadata() {
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMetadata(Map<String, Object> metadata) {
|
|
||||||
this.metadata = metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContextSummary() {
|
|
||||||
return contextSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContextSummary(String contextSummary) {
|
|
||||||
this.contextSummary = contextSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency information class
|
|
||||||
*/
|
|
||||||
public static class DependencyInfo {
|
|
||||||
private String name;
|
|
||||||
private String version;
|
|
||||||
private String type; // "compile", "test", "runtime", etc.
|
|
||||||
private String scope;
|
|
||||||
private boolean isDirectDependency;
|
|
||||||
|
|
||||||
public DependencyInfo(String name, String version, String type) {
|
|
||||||
this.name = name;
|
|
||||||
this.version = version;
|
|
||||||
this.type = type;
|
|
||||||
this.isDirectDependency = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getVersion() {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVersion(String version) {
|
|
||||||
this.version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setType(String type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getScope() {
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setScope(String scope) {
|
|
||||||
this.scope = scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDirectDependency() {
|
|
||||||
return isDirectDependency;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDirectDependency(boolean directDependency) {
|
|
||||||
isDirectDependency = directDependency;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("%s:%s (%s)", name, version, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration file information class
|
|
||||||
*/
|
|
||||||
public static class ConfigFile {
|
|
||||||
private String fileName;
|
|
||||||
private String relativePath;
|
|
||||||
private String fileType; // "properties", "yaml", "json", "xml", etc.
|
|
||||||
private Map<String, Object> keySettings;
|
|
||||||
private boolean isMainConfig;
|
|
||||||
|
|
||||||
public ConfigFile(String fileName, String relativePath, String fileType) {
|
|
||||||
this.fileName = fileName;
|
|
||||||
this.relativePath = relativePath;
|
|
||||||
this.fileType = fileType;
|
|
||||||
this.keySettings = new HashMap<>();
|
|
||||||
this.isMainConfig = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getFileName() {
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFileName(String fileName) {
|
|
||||||
this.fileName = fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRelativePath() {
|
|
||||||
return relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRelativePath(String relativePath) {
|
|
||||||
this.relativePath = relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFileType() {
|
|
||||||
return fileType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFileType(String fileType) {
|
|
||||||
this.fileType = fileType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> getKeySettings() {
|
|
||||||
return keySettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeySettings(Map<String, Object> keySettings) {
|
|
||||||
this.keySettings = keySettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMainConfig() {
|
|
||||||
return isMainConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMainConfig(boolean mainConfig) {
|
|
||||||
isMainConfig = mainConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addSetting(String key, Object value) {
|
|
||||||
this.keySettings.put(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Code statistics information class
|
|
||||||
*/
|
|
||||||
public static class CodeStatistics {
|
|
||||||
private int totalLines;
|
|
||||||
private int codeLines;
|
|
||||||
private int commentLines;
|
|
||||||
private int blankLines;
|
|
||||||
private Map<String, Integer> languageLines;
|
|
||||||
private int totalClasses;
|
|
||||||
private int totalMethods;
|
|
||||||
private int totalFunctions;
|
|
||||||
|
|
||||||
public CodeStatistics() {
|
|
||||||
this.languageLines = new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public int getTotalLines() {
|
|
||||||
return totalLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalLines(int totalLines) {
|
|
||||||
this.totalLines = totalLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCodeLines() {
|
|
||||||
return codeLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCodeLines(int codeLines) {
|
|
||||||
this.codeLines = codeLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCommentLines() {
|
|
||||||
return commentLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCommentLines(int commentLines) {
|
|
||||||
this.commentLines = commentLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getBlankLines() {
|
|
||||||
return blankLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBlankLines(int blankLines) {
|
|
||||||
this.blankLines = blankLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Integer> getLanguageLines() {
|
|
||||||
return languageLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLanguageLines(Map<String, Integer> languageLines) {
|
|
||||||
this.languageLines = languageLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalClasses() {
|
|
||||||
return totalClasses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalClasses(int totalClasses) {
|
|
||||||
this.totalClasses = totalClasses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalMethods() {
|
|
||||||
return totalMethods;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalMethods(int totalMethods) {
|
|
||||||
this.totalMethods = totalMethods;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalFunctions() {
|
|
||||||
return totalFunctions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalFunctions(int totalFunctions) {
|
|
||||||
this.totalFunctions = totalFunctions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addLanguageLines(String language, int lines) {
|
|
||||||
this.languageLines.put(language, this.languageLines.getOrDefault(language, 0) + lines);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
package com.example.demo.model;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project structure information model
|
|
||||||
* Contains project directory structure, file statistics and other information
|
|
||||||
*/
|
|
||||||
public class ProjectStructure {
|
|
||||||
private Path projectRoot;
|
|
||||||
private ProjectType projectType;
|
|
||||||
private List<DirectoryInfo> directories;
|
|
||||||
private Map<String, Integer> fileTypeCount;
|
|
||||||
private List<String> keyFiles;
|
|
||||||
private int totalFiles;
|
|
||||||
private int totalDirectories;
|
|
||||||
private long totalSize;
|
|
||||||
|
|
||||||
public ProjectStructure() {
|
|
||||||
this.directories = new ArrayList<>();
|
|
||||||
this.fileTypeCount = new HashMap<>();
|
|
||||||
this.keyFiles = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProjectStructure(Path projectRoot, ProjectType projectType) {
|
|
||||||
this();
|
|
||||||
this.projectRoot = projectRoot;
|
|
||||||
this.projectType = projectType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add directory information
|
|
||||||
*/
|
|
||||||
public void addDirectory(DirectoryInfo directoryInfo) {
|
|
||||||
this.directories.add(directoryInfo);
|
|
||||||
this.totalDirectories++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add file type statistics
|
|
||||||
*/
|
|
||||||
public void addFileType(String extension, int count) {
|
|
||||||
this.fileTypeCount.put(extension, this.fileTypeCount.getOrDefault(extension, 0) + count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add key file
|
|
||||||
*/
|
|
||||||
public void addKeyFile(String fileName) {
|
|
||||||
if (!this.keyFiles.contains(fileName)) {
|
|
||||||
this.keyFiles.add(fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get project structure summary
|
|
||||||
*/
|
|
||||||
public String getStructureSummary() {
|
|
||||||
StringBuilder summary = new StringBuilder();
|
|
||||||
summary.append("Project: ").append(projectRoot != null ? projectRoot.getFileName() : "Unknown").append("\n");
|
|
||||||
summary.append("Type: ").append(projectType != null ? projectType.getDisplayName() : "Unknown").append("\n");
|
|
||||||
summary.append("Directories: ").append(totalDirectories).append("\n");
|
|
||||||
summary.append("Files: ").append(totalFiles).append("\n");
|
|
||||||
|
|
||||||
if (!keyFiles.isEmpty()) {
|
|
||||||
summary.append("Key Files: ").append(String.join(", ", keyFiles)).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileTypeCount.isEmpty()) {
|
|
||||||
summary.append("File Types: ");
|
|
||||||
fileTypeCount.entrySet().stream()
|
|
||||||
.sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
|
|
||||||
.limit(5)
|
|
||||||
.forEach(entry -> summary.append(entry.getKey()).append("(").append(entry.getValue()).append(") "));
|
|
||||||
summary.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return summary.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get important directories list
|
|
||||||
*/
|
|
||||||
public List<DirectoryInfo> getImportantDirectories() {
|
|
||||||
return directories.stream()
|
|
||||||
.filter(DirectoryInfo::isImportant)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark important directories based on project type
|
|
||||||
*/
|
|
||||||
public void markImportantDirectories() {
|
|
||||||
if (projectType == null) return;
|
|
||||||
|
|
||||||
for (DirectoryInfo dir : directories) {
|
|
||||||
String dirName = dir.getName().toLowerCase();
|
|
||||||
|
|
||||||
// Common important directories
|
|
||||||
if (dirName.equals("src") || dirName.equals("source") ||
|
|
||||||
dirName.equals("test") || dirName.equals("tests") ||
|
|
||||||
dirName.equals("config") || dirName.equals("conf") ||
|
|
||||||
dirName.equals("docs") || dirName.equals("doc")) {
|
|
||||||
dir.setImportant(true);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Project type specific important directories
|
|
||||||
switch (projectType) {
|
|
||||||
case JAVA_MAVEN:
|
|
||||||
case JAVA_GRADLE:
|
|
||||||
case SPRING_BOOT:
|
|
||||||
if (dirName.equals("main") || dirName.equals("resources") ||
|
|
||||||
dirName.equals("webapp") || dirName.equals("target") ||
|
|
||||||
dirName.equals("build")) {
|
|
||||||
dir.setImportant(true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NODE_JS:
|
|
||||||
case REACT:
|
|
||||||
case VUE:
|
|
||||||
case ANGULAR:
|
|
||||||
case NEXT_JS:
|
|
||||||
if (dirName.equals("node_modules") || dirName.equals("public") ||
|
|
||||||
dirName.equals("dist") || dirName.equals("build") ||
|
|
||||||
dirName.equals("components") || dirName.equals("pages")) {
|
|
||||||
dir.setImportant(true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PYTHON:
|
|
||||||
case DJANGO:
|
|
||||||
case FLASK:
|
|
||||||
case FASTAPI:
|
|
||||||
if (dirName.equals("venv") || dirName.equals("env") ||
|
|
||||||
dirName.equals("__pycache__") || dirName.equals("migrations") ||
|
|
||||||
dirName.equals("static") || dirName.equals("templates")) {
|
|
||||||
dir.setImportant(true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public Path getProjectRoot() {
|
|
||||||
return projectRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProjectRoot(Path projectRoot) {
|
|
||||||
this.projectRoot = projectRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProjectType getProjectType() {
|
|
||||||
return projectType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProjectType(ProjectType projectType) {
|
|
||||||
this.projectType = projectType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DirectoryInfo> getDirectories() {
|
|
||||||
return directories;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDirectories(List<DirectoryInfo> directories) {
|
|
||||||
this.directories = directories;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Integer> getFileTypeCount() {
|
|
||||||
return fileTypeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFileTypeCount(Map<String, Integer> fileTypeCount) {
|
|
||||||
this.fileTypeCount = fileTypeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getKeyFiles() {
|
|
||||||
return keyFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeyFiles(List<String> keyFiles) {
|
|
||||||
this.keyFiles = keyFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalFiles() {
|
|
||||||
return totalFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalFiles(int totalFiles) {
|
|
||||||
this.totalFiles = totalFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalDirectories() {
|
|
||||||
return totalDirectories;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalDirectories(int totalDirectories) {
|
|
||||||
this.totalDirectories = totalDirectories;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTotalSize() {
|
|
||||||
return totalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalSize(long totalSize) {
|
|
||||||
this.totalSize = totalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Directory information inner class
|
|
||||||
*/
|
|
||||||
public static class DirectoryInfo {
|
|
||||||
private String name;
|
|
||||||
private String relativePath;
|
|
||||||
private int fileCount;
|
|
||||||
private List<String> files;
|
|
||||||
private boolean isImportant; // Whether it's an important directory (like src, test, etc.)
|
|
||||||
|
|
||||||
public DirectoryInfo(String name, String relativePath) {
|
|
||||||
this.name = name;
|
|
||||||
this.relativePath = relativePath;
|
|
||||||
this.files = new ArrayList<>();
|
|
||||||
this.isImportant = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRelativePath() {
|
|
||||||
return relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRelativePath(String relativePath) {
|
|
||||||
this.relativePath = relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getFileCount() {
|
|
||||||
return fileCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFileCount(int fileCount) {
|
|
||||||
this.fileCount = fileCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getFiles() {
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFiles(List<String> files) {
|
|
||||||
this.files = files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isImportant() {
|
|
||||||
return isImportant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setImportant(boolean important) {
|
|
||||||
isImportant = important;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addFile(String fileName) {
|
|
||||||
this.files.add(fileName);
|
|
||||||
this.fileCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package com.example.demo.model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project type enumeration
|
|
||||||
* Supports mainstream project type detection
|
|
||||||
*/
|
|
||||||
public enum ProjectType {
|
|
||||||
// Java projects
|
|
||||||
JAVA_MAVEN("Java Maven", "pom.xml", "Maven-based Java project"),
|
|
||||||
JAVA_GRADLE("Java Gradle", "build.gradle", "Gradle-based Java project"),
|
|
||||||
SPRING_BOOT("Spring Boot", "pom.xml", "Spring Boot application"),
|
|
||||||
|
|
||||||
// JavaScript/Node.js projects
|
|
||||||
NODE_JS("Node.js", "package.json", "Node.js project"),
|
|
||||||
REACT("React", "package.json", "React application"),
|
|
||||||
VUE("Vue.js", "package.json", "Vue.js application"),
|
|
||||||
ANGULAR("Angular", "package.json", "Angular application"),
|
|
||||||
NEXT_JS("Next.js", "package.json", "Next.js application"),
|
|
||||||
|
|
||||||
// Python projects
|
|
||||||
PYTHON("Python", "requirements.txt", "Python project"),
|
|
||||||
DJANGO("Django", "manage.py", "Django web application"),
|
|
||||||
FLASK("Flask", "app.py", "Flask web application"),
|
|
||||||
FASTAPI("FastAPI", "main.py", "FastAPI application"),
|
|
||||||
|
|
||||||
// Other project types
|
|
||||||
DOTNET("ASP.NET", "*.csproj", ".NET project"),
|
|
||||||
GO("Go", "go.mod", "Go project"),
|
|
||||||
RUST("Rust", "Cargo.toml", "Rust project"),
|
|
||||||
PHP("PHP", "composer.json", "PHP project"),
|
|
||||||
|
|
||||||
// Web frontend
|
|
||||||
HTML_STATIC("Static HTML", "index.html", "Static HTML website"),
|
|
||||||
|
|
||||||
// Unknown type
|
|
||||||
UNKNOWN("Unknown", "", "Unknown project type");
|
|
||||||
|
|
||||||
private final String displayName;
|
|
||||||
private final String keyFile;
|
|
||||||
private final String description;
|
|
||||||
|
|
||||||
ProjectType(String displayName, String keyFile, String description) {
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.keyFile = keyFile;
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayName() {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getKeyFile() {
|
|
||||||
return keyFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if it's a Java project
|
|
||||||
*/
|
|
||||||
public boolean isJavaProject() {
|
|
||||||
return this == JAVA_MAVEN || this == JAVA_GRADLE || this == SPRING_BOOT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if it's a JavaScript project
|
|
||||||
*/
|
|
||||||
public boolean isJavaScriptProject() {
|
|
||||||
return this == NODE_JS || this == REACT || this == VUE ||
|
|
||||||
this == ANGULAR || this == NEXT_JS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if it's a Python project
|
|
||||||
*/
|
|
||||||
public boolean isPythonProject() {
|
|
||||||
return this == PYTHON || this == DJANGO || this == FLASK || this == FASTAPI;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if it's a Web project
|
|
||||||
*/
|
|
||||||
public boolean isWebProject() {
|
|
||||||
return isJavaScriptProject() || this == HTML_STATIC ||
|
|
||||||
this == DJANGO || this == FLASK || this == FASTAPI || this == SPRING_BOOT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the primary programming language of the project
|
|
||||||
*/
|
|
||||||
public String getPrimaryLanguage() {
|
|
||||||
if (isJavaProject()) return "Java";
|
|
||||||
if (isJavaScriptProject()) return "JavaScript";
|
|
||||||
if (isPythonProject()) return "Python";
|
|
||||||
|
|
||||||
switch (this) {
|
|
||||||
case DOTNET:
|
|
||||||
return "C#";
|
|
||||||
case GO:
|
|
||||||
return "Go";
|
|
||||||
case RUST:
|
|
||||||
return "Rust";
|
|
||||||
case PHP:
|
|
||||||
return "PHP";
|
|
||||||
case HTML_STATIC:
|
|
||||||
return "HTML";
|
|
||||||
default:
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the recommended package manager
|
|
||||||
*/
|
|
||||||
public String getPackageManager() {
|
|
||||||
switch (this) {
|
|
||||||
case JAVA_MAVEN:
|
|
||||||
case SPRING_BOOT:
|
|
||||||
return "Maven";
|
|
||||||
case JAVA_GRADLE:
|
|
||||||
return "Gradle";
|
|
||||||
case NODE_JS:
|
|
||||||
case REACT:
|
|
||||||
case VUE:
|
|
||||||
case ANGULAR:
|
|
||||||
case NEXT_JS:
|
|
||||||
return "npm/yarn";
|
|
||||||
case PYTHON:
|
|
||||||
case DJANGO:
|
|
||||||
case FLASK:
|
|
||||||
case FASTAPI:
|
|
||||||
return "pip";
|
|
||||||
case DOTNET:
|
|
||||||
return "NuGet";
|
|
||||||
case GO:
|
|
||||||
return "go mod";
|
|
||||||
case RUST:
|
|
||||||
return "Cargo";
|
|
||||||
case PHP:
|
|
||||||
return "Composer";
|
|
||||||
default:
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package com.example.demo.model;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class TaskStatus {
|
|
||||||
private String taskId;
|
|
||||||
private String status; // RUNNING, COMPLETED, ERROR
|
|
||||||
private String currentAction;
|
|
||||||
private String summary;
|
|
||||||
private int currentTurn;
|
|
||||||
private int totalEstimatedTurns;
|
|
||||||
private long startTime;
|
|
||||||
private long lastUpdateTime;
|
|
||||||
private List<String> actionHistory;
|
|
||||||
private String errorMessage;
|
|
||||||
private double progressPercentage;
|
|
||||||
|
|
||||||
public TaskStatus(String taskId) {
|
|
||||||
this.taskId = taskId;
|
|
||||||
this.status = "RUNNING";
|
|
||||||
this.startTime = System.currentTimeMillis();
|
|
||||||
this.lastUpdateTime = this.startTime;
|
|
||||||
this.actionHistory = new ArrayList<>();
|
|
||||||
this.progressPercentage = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getTaskId() {
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTaskId(String taskId) {
|
|
||||||
this.taskId = taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
|
||||||
this.lastUpdateTime = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCurrentAction() {
|
|
||||||
return currentAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurrentAction(String currentAction) {
|
|
||||||
this.currentAction = currentAction;
|
|
||||||
this.lastUpdateTime = System.currentTimeMillis();
|
|
||||||
if (currentAction != null && !currentAction.trim().isEmpty()) {
|
|
||||||
this.actionHistory.add(currentAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSummary() {
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSummary(String summary) {
|
|
||||||
this.summary = summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCurrentTurn() {
|
|
||||||
return currentTurn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurrentTurn(int currentTurn) {
|
|
||||||
this.currentTurn = currentTurn;
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalEstimatedTurns() {
|
|
||||||
return totalEstimatedTurns;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalEstimatedTurns(int totalEstimatedTurns) {
|
|
||||||
this.totalEstimatedTurns = totalEstimatedTurns;
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getStartTime() {
|
|
||||||
return startTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLastUpdateTime() {
|
|
||||||
return lastUpdateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getActionHistory() {
|
|
||||||
return actionHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getErrorMessage() {
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setErrorMessage(String errorMessage) {
|
|
||||||
this.errorMessage = errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getProgressPercentage() {
|
|
||||||
return progressPercentage;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateProgress() {
|
|
||||||
if (totalEstimatedTurns > 0) {
|
|
||||||
this.progressPercentage = Math.min(100.0, (double) currentTurn / totalEstimatedTurns * 100.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getElapsedTime() {
|
|
||||||
return System.currentTimeMillis() - startTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
package com.example.demo.schema;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Schema definition class
|
|
||||||
* Used to define tool parameter structure and validation rules
|
|
||||||
*/
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
public class JsonSchema {
|
|
||||||
|
|
||||||
private String type;
|
|
||||||
private String description;
|
|
||||||
private String pattern;
|
|
||||||
private Number minimum;
|
|
||||||
private Number maximum;
|
|
||||||
private List<Object> enumValues;
|
|
||||||
|
|
||||||
@JsonProperty("properties")
|
|
||||||
private Map<String, JsonSchema> properties;
|
|
||||||
|
|
||||||
@JsonProperty("required")
|
|
||||||
private List<String> requiredFields;
|
|
||||||
|
|
||||||
@JsonProperty("items")
|
|
||||||
private JsonSchema items;
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
public JsonSchema() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static factory methods
|
|
||||||
public static JsonSchema object() {
|
|
||||||
JsonSchema schema = new JsonSchema();
|
|
||||||
schema.type = "object";
|
|
||||||
schema.properties = new HashMap<>();
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JsonSchema string(String description) {
|
|
||||||
JsonSchema schema = new JsonSchema();
|
|
||||||
schema.type = "string";
|
|
||||||
schema.description = description;
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JsonSchema number(String description) {
|
|
||||||
JsonSchema schema = new JsonSchema();
|
|
||||||
schema.type = "number";
|
|
||||||
schema.description = description;
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JsonSchema integer(String description) {
|
|
||||||
JsonSchema schema = new JsonSchema();
|
|
||||||
schema.type = "integer";
|
|
||||||
schema.description = description;
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JsonSchema bool(String description) {
|
|
||||||
JsonSchema schema = new JsonSchema();
|
|
||||||
schema.type = "boolean";
|
|
||||||
schema.description = description;
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JsonSchema array(JsonSchema items) {
|
|
||||||
JsonSchema schema = new JsonSchema();
|
|
||||||
schema.type = "array";
|
|
||||||
schema.items = items;
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JsonSchema array(String description, JsonSchema items) {
|
|
||||||
JsonSchema schema = new JsonSchema();
|
|
||||||
schema.type = "array";
|
|
||||||
schema.description = description;
|
|
||||||
schema.items = items;
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fluent methods
|
|
||||||
public JsonSchema addProperty(String name, JsonSchema property) {
|
|
||||||
if (this.properties == null) {
|
|
||||||
this.properties = new HashMap<>();
|
|
||||||
}
|
|
||||||
this.properties.put(name, property);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonSchema required(String... fields) {
|
|
||||||
this.requiredFields = Arrays.asList(fields);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonSchema pattern(String pattern) {
|
|
||||||
this.pattern = pattern;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonSchema minimum(Number minimum) {
|
|
||||||
this.minimum = minimum;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonSchema maximum(Number maximum) {
|
|
||||||
this.maximum = maximum;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonSchema enumValues(Object... values) {
|
|
||||||
this.enumValues = Arrays.asList(values);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setType(String type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDescription(String description) {
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPattern() {
|
|
||||||
return pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPattern(String pattern) {
|
|
||||||
this.pattern = pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Number getMinimum() {
|
|
||||||
return minimum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMinimum(Number minimum) {
|
|
||||||
this.minimum = minimum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Number getMaximum() {
|
|
||||||
return maximum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaximum(Number maximum) {
|
|
||||||
this.maximum = maximum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Object> getEnumValues() {
|
|
||||||
return enumValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnumValues(List<Object> enumValues) {
|
|
||||||
this.enumValues = enumValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, JsonSchema> getProperties() {
|
|
||||||
return properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProperties(Map<String, JsonSchema> properties) {
|
|
||||||
this.properties = properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getRequiredFields() {
|
|
||||||
return requiredFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRequiredFields(List<String> requiredFields) {
|
|
||||||
this.requiredFields = requiredFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonSchema getItems() {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setItems(JsonSchema items) {
|
|
||||||
this.items = items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package com.example.demo.schema;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.networknt.schema.JsonSchemaFactory;
|
|
||||||
import com.networknt.schema.SpecVersion;
|
|
||||||
import com.networknt.schema.ValidationMessage;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Schema validator
|
|
||||||
* Used to validate tool parameters against defined schema
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class SchemaValidator {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SchemaValidator.class);
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
private final JsonSchemaFactory schemaFactory;
|
|
||||||
|
|
||||||
public SchemaValidator() {
|
|
||||||
this.objectMapper = new ObjectMapper();
|
|
||||||
this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate data against schema
|
|
||||||
*
|
|
||||||
* @param schema JSON Schema definition
|
|
||||||
* @param data Data to validate
|
|
||||||
* @return Validation error message, null means validation passed
|
|
||||||
*/
|
|
||||||
public String validate(JsonSchema schema, Object data) {
|
|
||||||
try {
|
|
||||||
// Convert custom JsonSchema to standard JSON Schema string
|
|
||||||
String schemaJson = objectMapper.writeValueAsString(schema);
|
|
||||||
logger.debug("Schema JSON: {}", schemaJson);
|
|
||||||
|
|
||||||
// Create JSON Schema validator
|
|
||||||
com.networknt.schema.JsonSchema jsonSchema = schemaFactory.getSchema(schemaJson);
|
|
||||||
|
|
||||||
// Convert data to JsonNode
|
|
||||||
String dataJson = objectMapper.writeValueAsString(data);
|
|
||||||
JsonNode dataNode = objectMapper.readTree(dataJson);
|
|
||||||
logger.debug("Data JSON: {}", dataJson);
|
|
||||||
|
|
||||||
// Execute validation
|
|
||||||
Set<ValidationMessage> errors = jsonSchema.validate(dataNode);
|
|
||||||
|
|
||||||
if (errors.isEmpty()) {
|
|
||||||
logger.debug("Schema validation passed");
|
|
||||||
return null; // Validation passed
|
|
||||||
} else {
|
|
||||||
String errorMessage = errors.stream()
|
|
||||||
.map(ValidationMessage::getMessage)
|
|
||||||
.collect(Collectors.joining("; "));
|
|
||||||
logger.warn("Schema validation failed: {}", errorMessage);
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
String errorMessage = "Schema validation error: " + e.getMessage();
|
|
||||||
logger.error(errorMessage, e);
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple type validation (fallback solution)
|
|
||||||
* Used when JSON Schema validation fails
|
|
||||||
*/
|
|
||||||
public String validateSimple(JsonSchema schema, Object data) {
|
|
||||||
if (schema == null || data == null) {
|
|
||||||
return "Schema or data is null";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic type checking
|
|
||||||
String expectedType = schema.getType();
|
|
||||||
if (expectedType != null) {
|
|
||||||
String actualType = getDataType(data);
|
|
||||||
if (!isTypeCompatible(expectedType, actualType)) {
|
|
||||||
return String.format("Type mismatch: expected %s, got %s", expectedType, actualType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required field checking (only for object type)
|
|
||||||
if ("object".equals(expectedType) && schema.getRequiredFields() != null) {
|
|
||||||
if (!(data instanceof java.util.Map)) {
|
|
||||||
return "Expected object type for required field validation";
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
java.util.Map<String, Object> dataMap = (java.util.Map<String, Object>) data;
|
|
||||||
|
|
||||||
for (String requiredField : schema.getRequiredFields()) {
|
|
||||||
if (!dataMap.containsKey(requiredField) || dataMap.get(requiredField) == null) {
|
|
||||||
return "Missing required field: " + requiredField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // Validation passed
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDataType(Object data) {
|
|
||||||
if (data == null) return "null";
|
|
||||||
if (data instanceof String) return "string";
|
|
||||||
if (data instanceof Integer || data instanceof Long) return "integer";
|
|
||||||
if (data instanceof Number) return "number";
|
|
||||||
if (data instanceof Boolean) return "boolean";
|
|
||||||
if (data instanceof java.util.List) return "array";
|
|
||||||
if (data instanceof java.util.Map) return "object";
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTypeCompatible(String expectedType, String actualType) {
|
|
||||||
if (expectedType.equals(actualType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number type compatibility
|
|
||||||
if ("number".equals(expectedType) && "integer".equals(actualType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,80 +0,0 @@
|
|||||||
package com.example.demo.service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志事件基类
|
|
||||||
*/
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
public class LogEvent {
|
|
||||||
|
|
||||||
private String type;
|
|
||||||
private String taskId;
|
|
||||||
private String message;
|
|
||||||
private String timestamp;
|
|
||||||
|
|
||||||
// Constructors
|
|
||||||
public LogEvent() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public LogEvent(String type, String taskId, String message, String timestamp) {
|
|
||||||
this.type = type;
|
|
||||||
this.taskId = taskId;
|
|
||||||
this.message = message;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static factory methods
|
|
||||||
public static LogEvent createConnectionEvent(String taskId) {
|
|
||||||
LogEvent event = new LogEvent();
|
|
||||||
event.setType("CONNECTION_ESTABLISHED");
|
|
||||||
event.setTaskId(taskId);
|
|
||||||
event.setMessage("SSE连接已建立");
|
|
||||||
event.setTimestamp(java.time.LocalDateTime.now().format(
|
|
||||||
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setType(String type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTaskId() {
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTaskId(String taskId) {
|
|
||||||
this.taskId = taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMessage(String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(String timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "LogEvent{" +
|
|
||||||
"type='" + type + '\'' +
|
|
||||||
", taskId='" + taskId + '\'' +
|
|
||||||
", message='" + message + '\'' +
|
|
||||||
", timestamp='" + timestamp + '\'' +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
package com.example.demo.service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SSE日志推送服务
|
|
||||||
* 负责将AOP日志实时推送到前端
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class LogStreamService {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LogStreamService.class);
|
|
||||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
||||||
|
|
||||||
// 活跃的SSE连接 taskId -> SseEmitter
|
|
||||||
private final Map<String, SseEmitter> activeConnections = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
// JSON序列化器
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 建立SSE连接
|
|
||||||
*/
|
|
||||||
public SseEmitter createConnection(String taskId) {
|
|
||||||
logger.info("🔗 建立SSE连接: taskId={}", taskId);
|
|
||||||
|
|
||||||
SseEmitter emitter = new SseEmitter(0L); // 无超时
|
|
||||||
|
|
||||||
// 设置连接事件处理
|
|
||||||
emitter.onCompletion(() -> {
|
|
||||||
logger.info("✅ SSE连接完成: taskId={}", taskId);
|
|
||||||
activeConnections.remove(taskId);
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.onTimeout(() -> {
|
|
||||||
logger.warn("⏰ SSE连接超时: taskId={}", taskId);
|
|
||||||
activeConnections.remove(taskId);
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.onError((ex) -> {
|
|
||||||
logger.error("❌ SSE连接错误: taskId={}, error={}", taskId, ex.getMessage());
|
|
||||||
activeConnections.remove(taskId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存连接
|
|
||||||
activeConnections.put(taskId, emitter);
|
|
||||||
|
|
||||||
// 发送连接成功消息
|
|
||||||
sendLogEvent(taskId, LogEvent.createConnectionEvent(taskId));
|
|
||||||
|
|
||||||
return emitter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭SSE连接
|
|
||||||
*/
|
|
||||||
public void closeConnection(String taskId) {
|
|
||||||
SseEmitter emitter = activeConnections.remove(taskId);
|
|
||||||
if (emitter != null) {
|
|
||||||
try {
|
|
||||||
emitter.complete();
|
|
||||||
logger.info("🔚 关闭SSE连接: taskId={}", taskId);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("关闭SSE连接失败: taskId={}, error={}", taskId, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推送工具开始执行事件
|
|
||||||
*/
|
|
||||||
public void pushToolStart(String taskId, String toolName, String filePath, String message) {
|
|
||||||
ToolLogEvent event = new ToolLogEvent();
|
|
||||||
event.setType("TOOL_START");
|
|
||||||
event.setTaskId(taskId);
|
|
||||||
event.setToolName(toolName);
|
|
||||||
event.setFilePath(filePath);
|
|
||||||
event.setMessage(message);
|
|
||||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
|
||||||
event.setIcon(getToolIcon(toolName));
|
|
||||||
event.setStatus("RUNNING");
|
|
||||||
|
|
||||||
sendLogEvent(taskId, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推送工具执行成功事件
|
|
||||||
*/
|
|
||||||
public void pushToolSuccess(String taskId, String toolName, String filePath, String message, long executionTime) {
|
|
||||||
ToolLogEvent event = new ToolLogEvent();
|
|
||||||
event.setType("TOOL_SUCCESS");
|
|
||||||
event.setTaskId(taskId);
|
|
||||||
event.setToolName(toolName);
|
|
||||||
event.setFilePath(filePath);
|
|
||||||
event.setMessage(message);
|
|
||||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
|
||||||
event.setIcon(getToolIcon(toolName));
|
|
||||||
event.setStatus("SUCCESS");
|
|
||||||
event.setExecutionTime(executionTime);
|
|
||||||
|
|
||||||
sendLogEvent(taskId, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推送工具执行失败事件
|
|
||||||
*/
|
|
||||||
public void pushToolError(String taskId, String toolName, String filePath, String message, long executionTime) {
|
|
||||||
ToolLogEvent event = new ToolLogEvent();
|
|
||||||
event.setType("TOOL_ERROR");
|
|
||||||
event.setTaskId(taskId);
|
|
||||||
event.setToolName(toolName);
|
|
||||||
event.setFilePath(filePath);
|
|
||||||
event.setMessage(message);
|
|
||||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
|
||||||
event.setIcon("❌");
|
|
||||||
event.setStatus("ERROR");
|
|
||||||
event.setExecutionTime(executionTime);
|
|
||||||
|
|
||||||
sendLogEvent(taskId, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推送任务完成事件
|
|
||||||
*/
|
|
||||||
public void pushTaskComplete(String taskId) {
|
|
||||||
LogEvent event = new LogEvent();
|
|
||||||
event.setType("TASK_COMPLETE");
|
|
||||||
event.setTaskId(taskId);
|
|
||||||
event.setMessage("任务执行完成");
|
|
||||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
|
||||||
|
|
||||||
sendLogEvent(taskId, event);
|
|
||||||
|
|
||||||
// 延迟关闭连接
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
Thread.sleep(2000); // 等待2秒让前端处理完成事件
|
|
||||||
closeConnection(taskId);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送日志事件到前端
|
|
||||||
*/
|
|
||||||
private void sendLogEvent(String taskId, Object event) {
|
|
||||||
SseEmitter emitter = activeConnections.get(taskId);
|
|
||||||
if (emitter != null) {
|
|
||||||
try {
|
|
||||||
String jsonData = objectMapper.writeValueAsString(event);
|
|
||||||
logger.info("📤 准备推送日志事件: taskId={}, type={}, data={}", taskId,
|
|
||||||
event instanceof LogEvent ? ((LogEvent) event).getType() : "unknown", jsonData);
|
|
||||||
|
|
||||||
emitter.send(SseEmitter.event()
|
|
||||||
.name("log")
|
|
||||||
.data(jsonData));
|
|
||||||
|
|
||||||
logger.info("✅ 日志事件推送成功: taskId={}", taskId);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("推送日志事件失败: taskId={}, error={}", taskId, e.getMessage());
|
|
||||||
activeConnections.remove(taskId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn("⚠️ 未找到SSE连接: taskId={}, 无法推送事件", taskId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取工具图标
|
|
||||||
*/
|
|
||||||
private String getToolIcon(String toolName) {
|
|
||||||
switch (toolName) {
|
|
||||||
case "readFile":
|
|
||||||
return "📖";
|
|
||||||
case "writeFile":
|
|
||||||
return "✏️";
|
|
||||||
case "editFile":
|
|
||||||
return "📝";
|
|
||||||
case "listDirectory":
|
|
||||||
return "📁";
|
|
||||||
case "analyzeProject":
|
|
||||||
return "🔍";
|
|
||||||
case "scaffoldProject":
|
|
||||||
return "🏗️";
|
|
||||||
case "smartEdit":
|
|
||||||
return "🧠";
|
|
||||||
default:
|
|
||||||
return "⚙️";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取活跃连接数
|
|
||||||
*/
|
|
||||||
public int getActiveConnectionCount() {
|
|
||||||
return activeConnections.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,354 +0,0 @@
|
|||||||
package com.example.demo.service;
|
|
||||||
|
|
||||||
import com.example.demo.model.ProjectContext;
|
|
||||||
import com.example.demo.model.ProjectStructure;
|
|
||||||
import com.example.demo.model.ProjectType;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目上下文分析器
|
|
||||||
* 提供完整的项目分析功能,生成AI可理解的项目上下文
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class ProjectContextAnalyzer {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ProjectContextAnalyzer.class);
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public ProjectTypeDetector projectTypeDetector;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public ProjectDiscoveryService projectDiscoveryService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析项目并生成完整上下文
|
|
||||||
*
|
|
||||||
* @param projectRoot 项目根目录
|
|
||||||
* @return 项目上下文信息
|
|
||||||
*/
|
|
||||||
public ProjectContext analyzeProject(Path projectRoot) {
|
|
||||||
logger.info("Starting comprehensive project analysis for: {}", projectRoot);
|
|
||||||
|
|
||||||
ProjectContext context = new ProjectContext(projectRoot);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 检测项目类型
|
|
||||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
|
||||||
context.setProjectType(projectType);
|
|
||||||
logger.debug("Detected project type: {}", projectType);
|
|
||||||
|
|
||||||
// 2. 分析项目结构
|
|
||||||
ProjectStructure structure = projectDiscoveryService.analyzeProjectStructure(projectRoot);
|
|
||||||
context.setProjectStructure(structure);
|
|
||||||
logger.debug("Analyzed project structure with {} directories",
|
|
||||||
structure.getDirectories().size());
|
|
||||||
|
|
||||||
// 3. 分析依赖关系
|
|
||||||
List<ProjectContext.DependencyInfo> dependencies =
|
|
||||||
projectDiscoveryService.analyzeDependencies(projectRoot);
|
|
||||||
context.setDependencies(dependencies);
|
|
||||||
logger.debug("Found {} dependencies", dependencies.size());
|
|
||||||
|
|
||||||
// 4. 查找配置文件
|
|
||||||
List<ProjectContext.ConfigFile> configFiles =
|
|
||||||
projectDiscoveryService.findConfigurationFiles(projectRoot);
|
|
||||||
context.setConfigFiles(configFiles);
|
|
||||||
logger.debug("Found {} configuration files", configFiles.size());
|
|
||||||
|
|
||||||
// 5. 分析代码统计
|
|
||||||
ProjectContext.CodeStatistics codeStats = analyzeCodeStatistics(projectRoot, projectType);
|
|
||||||
context.setCodeStatistics(codeStats);
|
|
||||||
logger.debug("Code statistics: {} total lines", codeStats.getTotalLines());
|
|
||||||
|
|
||||||
// 6. 收集项目元数据
|
|
||||||
Map<String, Object> metadata = collectProjectMetadata(projectRoot, projectType);
|
|
||||||
context.setMetadata(metadata);
|
|
||||||
|
|
||||||
// 7. 生成上下文摘要
|
|
||||||
String summary = context.generateContextSummary();
|
|
||||||
logger.debug("Generated context summary with {} characters", summary.length());
|
|
||||||
|
|
||||||
logger.info("Project analysis completed successfully for: {}", projectRoot);
|
|
||||||
return context;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Error during project analysis for: " + projectRoot, e);
|
|
||||||
// 返回部分分析结果
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析代码统计信息
|
|
||||||
*/
|
|
||||||
private ProjectContext.CodeStatistics analyzeCodeStatistics(Path projectRoot, ProjectType projectType) {
|
|
||||||
logger.debug("Analyzing code statistics for: {}", projectRoot);
|
|
||||||
|
|
||||||
ProjectContext.CodeStatistics stats = new ProjectContext.CodeStatistics();
|
|
||||||
|
|
||||||
try {
|
|
||||||
analyzeCodeInDirectory(projectRoot, stats, projectType, 0, 3);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Error analyzing code statistics", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 递归分析目录中的代码
|
|
||||||
*/
|
|
||||||
private void analyzeCodeInDirectory(Path directory, ProjectContext.CodeStatistics stats,
|
|
||||||
ProjectType projectType, int currentDepth, int maxDepth) {
|
|
||||||
if (currentDepth > maxDepth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Stream<Path> paths = Files.list(directory)) {
|
|
||||||
paths.forEach(path -> {
|
|
||||||
try {
|
|
||||||
if (Files.isDirectory(path)) {
|
|
||||||
String dirName = path.getFileName().toString();
|
|
||||||
// 跳过不需要分析的目录
|
|
||||||
if (!shouldSkipDirectory(dirName)) {
|
|
||||||
analyzeCodeInDirectory(path, stats, projectType, currentDepth + 1, maxDepth);
|
|
||||||
}
|
|
||||||
} else if (Files.isRegularFile(path)) {
|
|
||||||
analyzeCodeFile(path, stats, projectType);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Error processing path during code analysis: " + path, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Error listing directory: " + directory, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析单个代码文件
|
|
||||||
*/
|
|
||||||
private void analyzeCodeFile(Path filePath, ProjectContext.CodeStatistics stats, ProjectType projectType) {
|
|
||||||
String fileName = filePath.getFileName().toString();
|
|
||||||
String extension = getFileExtension(fileName).toLowerCase();
|
|
||||||
|
|
||||||
// 只分析代码文件
|
|
||||||
if (!isCodeFile(extension, projectType)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<String> lines = Files.readAllLines(filePath);
|
|
||||||
int totalLines = lines.size();
|
|
||||||
int codeLines = 0;
|
|
||||||
int commentLines = 0;
|
|
||||||
int blankLines = 0;
|
|
||||||
|
|
||||||
for (String line : lines) {
|
|
||||||
String trimmedLine = line.trim();
|
|
||||||
if (trimmedLine.isEmpty()) {
|
|
||||||
blankLines++;
|
|
||||||
} else if (isCommentLine(trimmedLine, extension)) {
|
|
||||||
commentLines++;
|
|
||||||
} else {
|
|
||||||
codeLines++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新统计信息
|
|
||||||
stats.setTotalLines(stats.getTotalLines() + totalLines);
|
|
||||||
stats.setCodeLines(stats.getCodeLines() + codeLines);
|
|
||||||
stats.setCommentLines(stats.getCommentLines() + commentLines);
|
|
||||||
stats.setBlankLines(stats.getBlankLines() + blankLines);
|
|
||||||
|
|
||||||
// 按语言统计
|
|
||||||
String language = getLanguageByExtension(extension);
|
|
||||||
stats.addLanguageLines(language, totalLines);
|
|
||||||
|
|
||||||
// 分析类和方法(简单实现)
|
|
||||||
if (extension.equals(".java")) {
|
|
||||||
analyzeJavaFile(lines, stats);
|
|
||||||
} else if (extension.equals(".js") || extension.equals(".ts")) {
|
|
||||||
analyzeJavaScriptFile(lines, stats);
|
|
||||||
} else if (extension.equals(".py")) {
|
|
||||||
analyzePythonFile(lines, stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Error reading file for code analysis: " + filePath, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析Java文件
|
|
||||||
*/
|
|
||||||
private void analyzeJavaFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
|
||||||
for (String line : lines) {
|
|
||||||
String trimmedLine = line.trim();
|
|
||||||
if (trimmedLine.matches(".*\\bclass\\s+\\w+.*")) {
|
|
||||||
stats.setTotalClasses(stats.getTotalClasses() + 1);
|
|
||||||
}
|
|
||||||
if (trimmedLine.matches(".*\\b(public|private|protected)\\s+.*\\s+\\w+\\s*\\(.*\\).*")) {
|
|
||||||
stats.setTotalMethods(stats.getTotalMethods() + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析JavaScript文件
|
|
||||||
*/
|
|
||||||
private void analyzeJavaScriptFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
|
||||||
for (String line : lines) {
|
|
||||||
String trimmedLine = line.trim();
|
|
||||||
if (trimmedLine.matches(".*\\bfunction\\s+\\w+.*") ||
|
|
||||||
trimmedLine.matches(".*\\w+\\s*:\\s*function.*") ||
|
|
||||||
trimmedLine.matches(".*\\w+\\s*=\\s*\\(.*\\)\\s*=>.*")) {
|
|
||||||
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析Python文件
|
|
||||||
*/
|
|
||||||
private void analyzePythonFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
|
||||||
for (String line : lines) {
|
|
||||||
String trimmedLine = line.trim();
|
|
||||||
if (trimmedLine.matches("^class\\s+\\w+.*:")) {
|
|
||||||
stats.setTotalClasses(stats.getTotalClasses() + 1);
|
|
||||||
}
|
|
||||||
if (trimmedLine.matches("^def\\s+\\w+.*:")) {
|
|
||||||
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 收集项目元数据
|
|
||||||
*/
|
|
||||||
private Map<String, Object> collectProjectMetadata(Path projectRoot, ProjectType projectType) {
|
|
||||||
Map<String, Object> metadata = new HashMap<>();
|
|
||||||
|
|
||||||
metadata.put("projectName", projectRoot.getFileName().toString());
|
|
||||||
metadata.put("projectType", projectType.name());
|
|
||||||
metadata.put("primaryLanguage", projectType.getPrimaryLanguage());
|
|
||||||
metadata.put("packageManager", projectType.getPackageManager());
|
|
||||||
metadata.put("analysisTimestamp", System.currentTimeMillis());
|
|
||||||
|
|
||||||
// 检查版本控制
|
|
||||||
if (Files.exists(projectRoot.resolve(".git"))) {
|
|
||||||
metadata.put("versionControl", "Git");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查CI/CD配置
|
|
||||||
if (Files.exists(projectRoot.resolve(".github"))) {
|
|
||||||
metadata.put("cicd", "GitHub Actions");
|
|
||||||
} else if (Files.exists(projectRoot.resolve(".gitlab-ci.yml"))) {
|
|
||||||
metadata.put("cicd", "GitLab CI");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查Docker支持
|
|
||||||
if (Files.exists(projectRoot.resolve("Dockerfile"))) {
|
|
||||||
metadata.put("containerization", "Docker");
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成编辑上下文
|
|
||||||
*/
|
|
||||||
public String buildEditContext(Path projectRoot, String editDescription) {
|
|
||||||
logger.debug("Building edit context for: {}", projectRoot);
|
|
||||||
|
|
||||||
ProjectContext context = analyzeProject(projectRoot);
|
|
||||||
|
|
||||||
StringBuilder contextBuilder = new StringBuilder();
|
|
||||||
contextBuilder.append("=== EDIT CONTEXT ===\n");
|
|
||||||
contextBuilder.append("Edit Request: ").append(editDescription).append("\n\n");
|
|
||||||
contextBuilder.append(context.generateContextSummary());
|
|
||||||
|
|
||||||
return contextBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助方法
|
|
||||||
private boolean shouldSkipDirectory(String dirName) {
|
|
||||||
return dirName.equals(".git") || dirName.equals("node_modules") ||
|
|
||||||
dirName.equals("target") || dirName.equals("build") ||
|
|
||||||
dirName.equals("dist") || dirName.equals("__pycache__") ||
|
|
||||||
dirName.startsWith(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getFileExtension(String fileName) {
|
|
||||||
int lastDot = fileName.lastIndexOf('.');
|
|
||||||
return lastDot > 0 ? fileName.substring(lastDot) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCodeFile(String extension, ProjectType projectType) {
|
|
||||||
return extension.equals(".java") || extension.equals(".js") || extension.equals(".ts") ||
|
|
||||||
extension.equals(".py") || extension.equals(".html") || extension.equals(".css") ||
|
|
||||||
extension.equals(".jsx") || extension.equals(".tsx") || extension.equals(".vue") ||
|
|
||||||
extension.equals(".go") || extension.equals(".rs") || extension.equals(".php") ||
|
|
||||||
extension.equals(".cs") || extension.equals(".cpp") || extension.equals(".c");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCommentLine(String line, String extension) {
|
|
||||||
switch (extension) {
|
|
||||||
case ".java":
|
|
||||||
case ".js":
|
|
||||||
case ".ts":
|
|
||||||
case ".jsx":
|
|
||||||
case ".tsx":
|
|
||||||
case ".css":
|
|
||||||
return line.startsWith("//") || line.startsWith("/*") || line.startsWith("*");
|
|
||||||
case ".py":
|
|
||||||
return line.startsWith("#");
|
|
||||||
case ".html":
|
|
||||||
return line.startsWith("<!--");
|
|
||||||
default:
|
|
||||||
return line.startsWith("#") || line.startsWith("//");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getLanguageByExtension(String extension) {
|
|
||||||
switch (extension) {
|
|
||||||
case ".java":
|
|
||||||
return "Java";
|
|
||||||
case ".js":
|
|
||||||
case ".jsx":
|
|
||||||
return "JavaScript";
|
|
||||||
case ".ts":
|
|
||||||
case ".tsx":
|
|
||||||
return "TypeScript";
|
|
||||||
case ".py":
|
|
||||||
return "Python";
|
|
||||||
case ".html":
|
|
||||||
return "HTML";
|
|
||||||
case ".css":
|
|
||||||
return "CSS";
|
|
||||||
case ".vue":
|
|
||||||
return "Vue";
|
|
||||||
case ".go":
|
|
||||||
return "Go";
|
|
||||||
case ".rs":
|
|
||||||
return "Rust";
|
|
||||||
case ".php":
|
|
||||||
return "PHP";
|
|
||||||
case ".cs":
|
|
||||||
return "C#";
|
|
||||||
default:
|
|
||||||
return "Other";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
package com.example.demo.service;
|
|
||||||
|
|
||||||
import com.example.demo.model.ProjectContext;
|
|
||||||
import com.example.demo.model.ProjectStructure;
|
|
||||||
import com.example.demo.model.ProjectType;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目发现和分析服务
|
|
||||||
* 负责分析项目结构、依赖关系和配置信息
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class ProjectDiscoveryService {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ProjectDiscoveryService.class);
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
@Autowired
|
|
||||||
private ProjectTypeDetector projectTypeDetector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析项目结构
|
|
||||||
*
|
|
||||||
* @param projectRoot 项目根目录
|
|
||||||
* @return 项目结构信息
|
|
||||||
*/
|
|
||||||
public ProjectStructure analyzeProjectStructure(Path projectRoot) {
|
|
||||||
logger.debug("Analyzing project structure for: {}", projectRoot);
|
|
||||||
|
|
||||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
|
||||||
ProjectStructure structure = new ProjectStructure(projectRoot, projectType);
|
|
||||||
|
|
||||||
try {
|
|
||||||
analyzeDirectoryStructure(projectRoot, structure, 0, 3); // 最大深度3层
|
|
||||||
structure.markImportantDirectories();
|
|
||||||
|
|
||||||
logger.info("Project structure analysis completed for: {}", projectRoot);
|
|
||||||
return structure;
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("Error analyzing project structure for: " + projectRoot, e);
|
|
||||||
return structure; // 返回部分分析结果
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 递归分析目录结构
|
|
||||||
*/
|
|
||||||
private void analyzeDirectoryStructure(Path currentPath, ProjectStructure structure,
|
|
||||||
int currentDepth, int maxDepth) throws IOException {
|
|
||||||
if (currentDepth > maxDepth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Stream<Path> paths = Files.list(currentPath)) {
|
|
||||||
paths.forEach(path -> {
|
|
||||||
try {
|
|
||||||
if (Files.isDirectory(path)) {
|
|
||||||
String dirName = path.getFileName().toString();
|
|
||||||
String relativePath = structure.getProjectRoot().relativize(path).toString();
|
|
||||||
|
|
||||||
// 跳过常见的忽略目录
|
|
||||||
if (shouldIgnoreDirectory(dirName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProjectStructure.DirectoryInfo dirInfo =
|
|
||||||
new ProjectStructure.DirectoryInfo(dirName, relativePath);
|
|
||||||
|
|
||||||
// 分析目录中的文件
|
|
||||||
analyzeDirectoryFiles(path, dirInfo);
|
|
||||||
structure.addDirectory(dirInfo);
|
|
||||||
|
|
||||||
// 递归分析子目录
|
|
||||||
if (currentDepth < maxDepth) {
|
|
||||||
analyzeDirectoryStructure(path, structure, currentDepth + 1, maxDepth);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (Files.isRegularFile(path)) {
|
|
||||||
// 处理根目录下的文件
|
|
||||||
String fileName = path.getFileName().toString();
|
|
||||||
String extension = getFileExtension(fileName);
|
|
||||||
|
|
||||||
structure.addFileType(extension, 1);
|
|
||||||
structure.setTotalFiles(structure.getTotalFiles() + 1);
|
|
||||||
|
|
||||||
// 检查是否为关键文件
|
|
||||||
if (isKeyFile(fileName, structure.getProjectType())) {
|
|
||||||
structure.addKeyFile(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 累计文件大小
|
|
||||||
try {
|
|
||||||
structure.setTotalSize(structure.getTotalSize() + Files.size(path));
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Could not get size for file: {}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Error processing path: " + path, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析目录中的文件
|
|
||||||
*/
|
|
||||||
private void analyzeDirectoryFiles(Path directory, ProjectStructure.DirectoryInfo dirInfo) {
|
|
||||||
try (Stream<Path> files = Files.list(directory)) {
|
|
||||||
files.filter(Files::isRegularFile)
|
|
||||||
.forEach(file -> {
|
|
||||||
String fileName = file.getFileName().toString();
|
|
||||||
dirInfo.addFile(fileName);
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Error analyzing files in directory: {}", directory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析项目依赖
|
|
||||||
*/
|
|
||||||
public List<ProjectContext.DependencyInfo> analyzeDependencies(Path projectRoot) {
|
|
||||||
logger.debug("Analyzing dependencies for: {}", projectRoot);
|
|
||||||
|
|
||||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
|
||||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (projectType) {
|
|
||||||
case JAVA_MAVEN:
|
|
||||||
case SPRING_BOOT:
|
|
||||||
dependencies.addAll(analyzeMavenDependencies(projectRoot));
|
|
||||||
break;
|
|
||||||
case NODE_JS:
|
|
||||||
case REACT:
|
|
||||||
case VUE:
|
|
||||||
case ANGULAR:
|
|
||||||
case NEXT_JS:
|
|
||||||
dependencies.addAll(analyzeNpmDependencies(projectRoot));
|
|
||||||
break;
|
|
||||||
case PYTHON:
|
|
||||||
case DJANGO:
|
|
||||||
case FLASK:
|
|
||||||
case FASTAPI:
|
|
||||||
dependencies.addAll(analyzePythonDependencies(projectRoot));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logger.info("Dependency analysis not supported for project type: {}", projectType);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Error analyzing dependencies for: " + projectRoot, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Found {} dependencies for project: {}", dependencies.size(), projectRoot);
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析Maven依赖
|
|
||||||
*/
|
|
||||||
private List<ProjectContext.DependencyInfo> analyzeMavenDependencies(Path projectRoot) {
|
|
||||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
|
||||||
Path pomFile = projectRoot.resolve("pom.xml");
|
|
||||||
|
|
||||||
if (!Files.exists(pomFile)) {
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String pomContent = Files.readString(pomFile);
|
|
||||||
// 简单的XML解析 - 在实际项目中应该使用专门的XML解析器
|
|
||||||
if (pomContent.contains("spring-boot-starter-web")) {
|
|
||||||
dependencies.add(new ProjectContext.DependencyInfo(
|
|
||||||
"spring-boot-starter-web", "auto", "compile"));
|
|
||||||
}
|
|
||||||
if (pomContent.contains("spring-boot-starter-data-jpa")) {
|
|
||||||
dependencies.add(new ProjectContext.DependencyInfo(
|
|
||||||
"spring-boot-starter-data-jpa", "auto", "compile"));
|
|
||||||
}
|
|
||||||
if (pomContent.contains("spring-boot-starter-test")) {
|
|
||||||
dependencies.add(new ProjectContext.DependencyInfo(
|
|
||||||
"spring-boot-starter-test", "auto", "test"));
|
|
||||||
}
|
|
||||||
// 可以添加更多依赖检测逻辑
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Error reading pom.xml", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析NPM依赖
|
|
||||||
*/
|
|
||||||
private List<ProjectContext.DependencyInfo> analyzeNpmDependencies(Path projectRoot) {
|
|
||||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
|
||||||
Path packageJsonPath = projectRoot.resolve("package.json");
|
|
||||||
|
|
||||||
if (!Files.exists(packageJsonPath)) {
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String content = Files.readString(packageJsonPath);
|
|
||||||
JsonNode packageJson = objectMapper.readTree(content);
|
|
||||||
|
|
||||||
// 分析生产依赖
|
|
||||||
JsonNode deps = packageJson.get("dependencies");
|
|
||||||
if (deps != null) {
|
|
||||||
deps.fields().forEachRemaining(entry -> {
|
|
||||||
dependencies.add(new ProjectContext.DependencyInfo(
|
|
||||||
entry.getKey(), entry.getValue().asText(), "production"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分析开发依赖
|
|
||||||
JsonNode devDeps = packageJson.get("devDependencies");
|
|
||||||
if (devDeps != null) {
|
|
||||||
devDeps.fields().forEachRemaining(entry -> {
|
|
||||||
ProjectContext.DependencyInfo depInfo = new ProjectContext.DependencyInfo(
|
|
||||||
entry.getKey(), entry.getValue().asText(), "development");
|
|
||||||
depInfo.setDirectDependency(true);
|
|
||||||
dependencies.add(depInfo);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Error reading package.json", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析Python依赖
|
|
||||||
*/
|
|
||||||
private List<ProjectContext.DependencyInfo> analyzePythonDependencies(Path projectRoot) {
|
|
||||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
|
||||||
Path requirementsFile = projectRoot.resolve("requirements.txt");
|
|
||||||
|
|
||||||
if (Files.exists(requirementsFile)) {
|
|
||||||
try {
|
|
||||||
List<String> lines = Files.readAllLines(requirementsFile);
|
|
||||||
for (String line : lines) {
|
|
||||||
line = line.trim();
|
|
||||||
if (!line.isEmpty() && !line.startsWith("#")) {
|
|
||||||
String[] parts = line.split("==|>=|<=|>|<");
|
|
||||||
String name = parts[0].trim();
|
|
||||||
String version = parts.length > 1 ? parts[1].trim() : "latest";
|
|
||||||
dependencies.add(new ProjectContext.DependencyInfo(name, version, "runtime"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Error reading requirements.txt", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查找配置文件
|
|
||||||
*/
|
|
||||||
public List<ProjectContext.ConfigFile> findConfigurationFiles(Path projectRoot) {
|
|
||||||
logger.debug("Finding configuration files for: {}", projectRoot);
|
|
||||||
|
|
||||||
List<ProjectContext.ConfigFile> configFiles = new ArrayList<>();
|
|
||||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 通用配置文件
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "application.properties", "properties");
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "application.yml", "yaml");
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "application.yaml", "yaml");
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "config.json", "json");
|
|
||||||
|
|
||||||
// 项目类型特定的配置文件
|
|
||||||
switch (projectType) {
|
|
||||||
case JAVA_MAVEN:
|
|
||||||
case SPRING_BOOT:
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "pom.xml", "xml");
|
|
||||||
break;
|
|
||||||
case NODE_JS:
|
|
||||||
case REACT:
|
|
||||||
case VUE:
|
|
||||||
case ANGULAR:
|
|
||||||
case NEXT_JS:
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "package.json", "json");
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "webpack.config.js", "javascript");
|
|
||||||
break;
|
|
||||||
case PYTHON:
|
|
||||||
case DJANGO:
|
|
||||||
case FLASK:
|
|
||||||
case FASTAPI:
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "requirements.txt", "text");
|
|
||||||
addConfigFileIfExists(configFiles, projectRoot, "setup.py", "python");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Error finding configuration files for: " + projectRoot, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Found {} configuration files for project: {}", configFiles.size(), projectRoot);
|
|
||||||
return configFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加配置文件(如果存在)
|
|
||||||
*/
|
|
||||||
private void addConfigFileIfExists(List<ProjectContext.ConfigFile> configFiles,
|
|
||||||
Path projectRoot, String fileName, String fileType) {
|
|
||||||
Path configPath = projectRoot.resolve(fileName);
|
|
||||||
if (Files.exists(configPath)) {
|
|
||||||
String relativePath = projectRoot.relativize(configPath).toString();
|
|
||||||
ProjectContext.ConfigFile configFile =
|
|
||||||
new ProjectContext.ConfigFile(fileName, relativePath, fileType);
|
|
||||||
|
|
||||||
// 标记主要配置文件
|
|
||||||
if (fileName.equals("pom.xml") || fileName.equals("package.json") ||
|
|
||||||
fileName.startsWith("application.")) {
|
|
||||||
configFile.setMainConfig(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
configFiles.add(configFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否应该忽略目录
|
|
||||||
*/
|
|
||||||
private boolean shouldIgnoreDirectory(String dirName) {
|
|
||||||
return dirName.equals(".git") || dirName.equals(".svn") ||
|
|
||||||
dirName.equals("node_modules") || dirName.equals("target") ||
|
|
||||||
dirName.equals("build") || dirName.equals("dist") ||
|
|
||||||
dirName.equals("__pycache__") || dirName.equals(".idea") ||
|
|
||||||
dirName.equals(".vscode") || dirName.startsWith(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取文件扩展名
|
|
||||||
*/
|
|
||||||
private String getFileExtension(String fileName) {
|
|
||||||
int lastDot = fileName.lastIndexOf('.');
|
|
||||||
return lastDot > 0 ? fileName.substring(lastDot) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否为关键文件
|
|
||||||
*/
|
|
||||||
private boolean isKeyFile(String fileName, ProjectType projectType) {
|
|
||||||
// 通用关键文件
|
|
||||||
if (fileName.equals("README.md") || fileName.equals("LICENSE") ||
|
|
||||||
fileName.equals("Dockerfile") || fileName.equals(".gitignore")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 项目类型特定的关键文件
|
|
||||||
if (projectType != null) {
|
|
||||||
String keyFile = projectType.getKeyFile();
|
|
||||||
if (keyFile != null && !keyFile.isEmpty()) {
|
|
||||||
return fileName.equals(keyFile) || fileName.matches(keyFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
package com.example.demo.service;
|
|
||||||
|
|
||||||
import com.example.demo.model.ProjectType;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目模板扩展服务
|
|
||||||
* 生成README、gitignore等通用文件模板
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class ProjectTemplateExtensions {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成README.md内容
|
|
||||||
*/
|
|
||||||
public String generateReadmeContent(Map<String, String> variables) {
|
|
||||||
return String.format("""
|
|
||||||
# %s
|
|
||||||
|
|
||||||
%s
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Java 17 or higher (for Java projects)
|
|
||||||
- Node.js 16+ (for JavaScript projects)
|
|
||||||
- Python 3.8+ (for Python projects)
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd %s
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install dependencies:
|
|
||||||
```bash
|
|
||||||
# For Java Maven projects
|
|
||||||
mvn clean install
|
|
||||||
|
|
||||||
# For Node.js projects
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# For Python projects
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run the application:
|
|
||||||
```bash
|
|
||||||
# For Java Maven projects
|
|
||||||
mvn spring-boot:run
|
|
||||||
|
|
||||||
# For Node.js projects
|
|
||||||
npm start
|
|
||||||
|
|
||||||
# For Python projects
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
%s/
|
|
||||||
├── src/ # Source code
|
|
||||||
├── test/ # Test files
|
|
||||||
├── docs/ # Documentation
|
|
||||||
├── README.md # This file
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Development
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For Java projects
|
|
||||||
mvn test
|
|
||||||
|
|
||||||
# For Node.js projects
|
|
||||||
npm test
|
|
||||||
|
|
||||||
# For Python projects
|
|
||||||
python -m pytest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For Java projects
|
|
||||||
mvn clean package
|
|
||||||
|
|
||||||
# For Node.js projects
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# For Python projects
|
|
||||||
python setup.py build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Features
|
|
||||||
|
|
||||||
- Feature 1: Description
|
|
||||||
- Feature 2: Description
|
|
||||||
- Feature 3: Description
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
1. Fork the project
|
|
||||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
||||||
5. Open a Pull Request
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
## 👥 Authors
|
|
||||||
|
|
||||||
- **%s** - *Initial work* - [%s](mailto:%s)
|
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
|
||||||
|
|
||||||
- Hat tip to anyone whose code was used
|
|
||||||
- Inspiration
|
|
||||||
- etc
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Created with ❤️ by %s
|
|
||||||
""",
|
|
||||||
variables.get("PROJECT_NAME_PASCAL"),
|
|
||||||
variables.get("DESCRIPTION"),
|
|
||||||
variables.get("PROJECT_NAME"),
|
|
||||||
variables.get("PROJECT_NAME"),
|
|
||||||
variables.get("AUTHOR"),
|
|
||||||
variables.get("AUTHOR"),
|
|
||||||
variables.get("EMAIL"),
|
|
||||||
variables.get("AUTHOR")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成.gitignore内容
|
|
||||||
*/
|
|
||||||
public String generateGitignoreContent(ProjectType projectType) {
|
|
||||||
StringBuilder gitignore = new StringBuilder();
|
|
||||||
|
|
||||||
// 通用忽略规则
|
|
||||||
gitignore.append("""
|
|
||||||
# General
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
._*
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
ehthumbs.db
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 项目类型特定的忽略规则
|
|
||||||
switch (projectType) {
|
|
||||||
case JAVA_MAVEN:
|
|
||||||
case JAVA_GRADLE:
|
|
||||||
case SPRING_BOOT:
|
|
||||||
gitignore.append("""
|
|
||||||
# Java
|
|
||||||
*.class
|
|
||||||
*.jar
|
|
||||||
*.war
|
|
||||||
*.ear
|
|
||||||
*.nar
|
|
||||||
hs_err_pid*
|
|
||||||
|
|
||||||
# Maven
|
|
||||||
target/
|
|
||||||
pom.xml.tag
|
|
||||||
pom.xml.releaseBackup
|
|
||||||
pom.xml.versionsBackup
|
|
||||||
pom.xml.next
|
|
||||||
release.properties
|
|
||||||
dependency-reduced-pom.xml
|
|
||||||
buildNumber.properties
|
|
||||||
.mvn/timing.properties
|
|
||||||
.mvn/wrapper/maven-wrapper.jar
|
|
||||||
|
|
||||||
# Gradle
|
|
||||||
.gradle
|
|
||||||
build/
|
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
|
||||||
!**/src/main/**/build/
|
|
||||||
!**/src/test/**/build/
|
|
||||||
|
|
||||||
# Spring Boot
|
|
||||||
spring-boot-*.log
|
|
||||||
|
|
||||||
""");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NODE_JS:
|
|
||||||
case REACT:
|
|
||||||
case VUE:
|
|
||||||
case ANGULAR:
|
|
||||||
case NEXT_JS:
|
|
||||||
gitignore.append("""
|
|
||||||
# Node.js
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage/
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# parcel-bundler cache
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
public
|
|
||||||
|
|
||||||
# Storybook build outputs
|
|
||||||
.out
|
|
||||||
.storybook-out
|
|
||||||
|
|
||||||
# Temporary folders
|
|
||||||
tmp/
|
|
||||||
temp/
|
|
||||||
|
|
||||||
""");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PYTHON:
|
|
||||||
case DJANGO:
|
|
||||||
case FLASK:
|
|
||||||
case FASTAPI:
|
|
||||||
gitignore.append("""
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
.python-version
|
|
||||||
|
|
||||||
# celery beat schedule file
|
|
||||||
celerybeat-schedule
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
""");
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// 基本忽略规则已经添加
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return gitignore.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user