mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-06 16:27:32 +00:00
Compare commits
2 Commits
e601eb6db5
...
accac603cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
accac603cf | ||
|
|
7c96c730e6 |
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
@@ -80,10 +80,16 @@ public class AuthController {
|
||||
// 授权类型和客户端id
|
||||
String clientId = loginBody.getClientId();
|
||||
String grantType = loginBody.getGrantType();
|
||||
log.info("登录请求 - clientId: {}, grantType: {}", clientId, grantType);
|
||||
SysClientVo client = clientService.queryByClientId(clientId);
|
||||
log.info("查询客户端结果 - client: {}, grantType: {}", client, client != null ? client.getGrantType() : "null");
|
||||
// 查询不到 client 或 client 内不包含 grantType
|
||||
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
|
||||
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
|
||||
if (ObjectUtil.isNull(client)) {
|
||||
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"));
|
||||
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
|
||||
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
|
||||
|
||||
@@ -58,15 +58,15 @@ spring:
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
|
||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi_ai_agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: root
|
||||
agent:
|
||||
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
# url: jdbc:mysql://localhost:3306/agent_db
|
||||
username: root
|
||||
password: root
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# agent:
|
||||
# url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
# # url: jdbc:mysql://localhost:3306/agent_db
|
||||
# username: root
|
||||
# password: root
|
||||
# driverClassName: com.mysql.cj.jdbc.Driver
|
||||
|
||||
hikari:
|
||||
# 最大连接池数量
|
||||
@@ -268,4 +268,4 @@ justauth:
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
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: ""
|
||||
|
||||
@@ -7,6 +7,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -54,4 +55,9 @@ public class ChatContext {
|
||||
* 响应处理器
|
||||
*/
|
||||
private StreamingChatResponseHandler handler;
|
||||
|
||||
/**
|
||||
* 聊天服务实例
|
||||
*/
|
||||
private IChatService chatService;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>ruoyi-ai-copilot</module>
|
||||
<module>ruoyi-monitor-admin</module>
|
||||
<module>ruoyi-snailjob-server</module>
|
||||
</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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,285 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectType;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 项目类型检测器
|
||||
* 基于文件特征自动识别项目类型
|
||||
*/
|
||||
@Component
|
||||
public class ProjectTypeDetector {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectTypeDetector.class);
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 检测项目类型
|
||||
*
|
||||
* @param projectRoot 项目根目录
|
||||
* @return 检测到的项目类型
|
||||
*/
|
||||
public ProjectType detectProjectType(Path projectRoot) {
|
||||
if (!Files.exists(projectRoot) || !Files.isDirectory(projectRoot)) {
|
||||
logger.warn("Project root does not exist or is not a directory: {}", projectRoot);
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("Detecting project type for: {}", projectRoot);
|
||||
|
||||
// 按优先级检测项目类型
|
||||
ProjectType detectedType = detectByKeyFiles(projectRoot);
|
||||
if (detectedType != ProjectType.UNKNOWN) {
|
||||
logger.info("Detected project type: {} for {}", detectedType, projectRoot);
|
||||
return detectedType;
|
||||
}
|
||||
|
||||
// 如果关键文件检测失败,尝试基于目录结构检测
|
||||
detectedType = detectByDirectoryStructure(projectRoot);
|
||||
if (detectedType != ProjectType.UNKNOWN) {
|
||||
logger.info("Detected project type by structure: {} for {}", detectedType, projectRoot);
|
||||
return detectedType;
|
||||
}
|
||||
|
||||
logger.info("Could not determine project type for: {}", projectRoot);
|
||||
return ProjectType.UNKNOWN;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error detecting project type for: " + projectRoot, e);
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于关键文件检测项目类型
|
||||
*/
|
||||
private ProjectType detectByKeyFiles(Path projectRoot) throws IOException {
|
||||
// Java Maven项目
|
||||
if (Files.exists(projectRoot.resolve("pom.xml"))) {
|
||||
// 检查是否为Spring Boot项目
|
||||
if (isSpringBootProject(projectRoot)) {
|
||||
return ProjectType.SPRING_BOOT;
|
||||
}
|
||||
return ProjectType.JAVA_MAVEN;
|
||||
}
|
||||
|
||||
// Java Gradle项目
|
||||
if (Files.exists(projectRoot.resolve("build.gradle")) ||
|
||||
Files.exists(projectRoot.resolve("build.gradle.kts"))) {
|
||||
return ProjectType.JAVA_GRADLE;
|
||||
}
|
||||
|
||||
// Node.js项目
|
||||
if (Files.exists(projectRoot.resolve("package.json"))) {
|
||||
return analyzeNodeJsProject(projectRoot);
|
||||
}
|
||||
|
||||
// Python项目
|
||||
if (Files.exists(projectRoot.resolve("requirements.txt")) ||
|
||||
Files.exists(projectRoot.resolve("setup.py")) ||
|
||||
Files.exists(projectRoot.resolve("pyproject.toml"))) {
|
||||
return analyzePythonProject(projectRoot);
|
||||
}
|
||||
|
||||
// .NET项目
|
||||
try (Stream<Path> files = Files.list(projectRoot)) {
|
||||
if (files.anyMatch(path -> path.toString().endsWith(".csproj") ||
|
||||
path.toString().endsWith(".sln"))) {
|
||||
return ProjectType.DOTNET;
|
||||
}
|
||||
}
|
||||
|
||||
// Go项目
|
||||
if (Files.exists(projectRoot.resolve("go.mod"))) {
|
||||
return ProjectType.GO;
|
||||
}
|
||||
|
||||
// Rust项目
|
||||
if (Files.exists(projectRoot.resolve("Cargo.toml"))) {
|
||||
return ProjectType.RUST;
|
||||
}
|
||||
|
||||
// PHP项目
|
||||
if (Files.exists(projectRoot.resolve("composer.json"))) {
|
||||
return ProjectType.PHP;
|
||||
}
|
||||
|
||||
// 静态HTML项目
|
||||
if (Files.exists(projectRoot.resolve("index.html"))) {
|
||||
return ProjectType.HTML_STATIC;
|
||||
}
|
||||
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为Spring Boot项目
|
||||
*/
|
||||
private boolean isSpringBootProject(Path projectRoot) {
|
||||
try {
|
||||
Path pomFile = projectRoot.resolve("pom.xml");
|
||||
if (!Files.exists(pomFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String pomContent = Files.readString(pomFile);
|
||||
return pomContent.contains("spring-boot-starter") ||
|
||||
pomContent.contains("org.springframework.boot");
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading pom.xml for Spring Boot detection", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Node.js项目类型
|
||||
*/
|
||||
private ProjectType analyzeNodeJsProject(Path projectRoot) {
|
||||
try {
|
||||
Path packageJsonPath = projectRoot.resolve("package.json");
|
||||
String content = Files.readString(packageJsonPath);
|
||||
JsonNode packageJson = objectMapper.readTree(content);
|
||||
|
||||
// 检查依赖来确定具体的框架类型
|
||||
JsonNode dependencies = packageJson.get("dependencies");
|
||||
JsonNode devDependencies = packageJson.get("devDependencies");
|
||||
|
||||
if (hasDependency(dependencies, "react") || hasDependency(devDependencies, "react")) {
|
||||
return ProjectType.REACT;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "vue") || hasDependency(devDependencies, "vue")) {
|
||||
return ProjectType.VUE;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "@angular/core") ||
|
||||
hasDependency(devDependencies, "@angular/cli")) {
|
||||
return ProjectType.ANGULAR;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "next") || hasDependency(devDependencies, "next")) {
|
||||
return ProjectType.NEXT_JS;
|
||||
}
|
||||
|
||||
return ProjectType.NODE_JS;
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error analyzing package.json", e);
|
||||
return ProjectType.NODE_JS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Python项目类型
|
||||
*/
|
||||
private ProjectType analyzePythonProject(Path projectRoot) {
|
||||
// 检查Django项目
|
||||
if (Files.exists(projectRoot.resolve("manage.py"))) {
|
||||
return ProjectType.DJANGO;
|
||||
}
|
||||
|
||||
// 检查Flask项目
|
||||
if (Files.exists(projectRoot.resolve("app.py")) ||
|
||||
Files.exists(projectRoot.resolve("application.py"))) {
|
||||
return ProjectType.FLASK;
|
||||
}
|
||||
|
||||
// 检查FastAPI项目
|
||||
if (Files.exists(projectRoot.resolve("main.py"))) {
|
||||
try {
|
||||
String content = Files.readString(projectRoot.resolve("main.py"));
|
||||
if (content.contains("from fastapi import") || content.contains("import fastapi")) {
|
||||
return ProjectType.FASTAPI;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading main.py for FastAPI detection", e);
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectType.PYTHON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于目录结构检测项目类型
|
||||
*/
|
||||
private ProjectType detectByDirectoryStructure(Path projectRoot) {
|
||||
try {
|
||||
List<String> directories = Files.list(projectRoot)
|
||||
.filter(Files::isDirectory)
|
||||
.map(path -> path.getFileName().toString().toLowerCase())
|
||||
.toList();
|
||||
|
||||
// Java项目特征目录
|
||||
if (directories.contains("src") &&
|
||||
(directories.contains("target") || directories.contains("build"))) {
|
||||
return ProjectType.JAVA_MAVEN; // 默认为Maven
|
||||
}
|
||||
|
||||
// Node.js项目特征目录
|
||||
if (directories.contains("node_modules") ||
|
||||
directories.contains("public") ||
|
||||
directories.contains("dist")) {
|
||||
return ProjectType.NODE_JS;
|
||||
}
|
||||
|
||||
// Python项目特征目录
|
||||
if (directories.contains("venv") ||
|
||||
directories.contains("env") ||
|
||||
directories.contains("__pycache__")) {
|
||||
return ProjectType.PYTHON;
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error analyzing directory structure", e);
|
||||
}
|
||||
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在特定依赖
|
||||
*/
|
||||
private boolean hasDependency(JsonNode dependencies, String dependencyName) {
|
||||
return dependencies != null && dependencies.has(dependencyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目类型的详细信息
|
||||
*/
|
||||
public String getProjectTypeDetails(Path projectRoot, ProjectType projectType) {
|
||||
StringBuilder details = new StringBuilder();
|
||||
details.append("Project Type: ").append(projectType.getDisplayName()).append("\n");
|
||||
details.append("Primary Language: ").append(projectType.getPrimaryLanguage()).append("\n");
|
||||
details.append("Package Manager: ").append(projectType.getPackageManager()).append("\n");
|
||||
|
||||
// 添加特定项目类型的详细信息
|
||||
switch (projectType) {
|
||||
case SPRING_BOOT:
|
||||
details.append("Framework: Spring Boot\n");
|
||||
details.append("Build Tool: Maven\n");
|
||||
break;
|
||||
case REACT:
|
||||
details.append("Framework: React\n");
|
||||
details.append("Runtime: Node.js\n");
|
||||
break;
|
||||
case DJANGO:
|
||||
details.append("Framework: Django\n");
|
||||
details.append("Language: Python\n");
|
||||
break;
|
||||
// 可以添加更多项目类型的详细信息
|
||||
}
|
||||
|
||||
return details.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class TaskSummaryService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TaskSummaryService.class);
|
||||
|
||||
private static final Pattern[] ACTION_PATTERNS = {
|
||||
Pattern.compile("(?i)creating?\\s+(?:a\\s+)?(?:new\\s+)?(.{1,50}?)(?:\\s+file|\\s+directory|\\s+project)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)writing?\\s+(?:to\\s+)?(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)reading?\\s+(?:from\\s+)?(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)editing?\\s+(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)listing?\\s+(?:the\\s+)?(.{1,50}?)(?:\\s+directory)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)analyzing?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)generating?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)building?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE)
|
||||
};
|
||||
|
||||
private static final String[] ACTION_VERBS = {
|
||||
"创建", "写入", "读取", "编辑", "列出", "分析", "生成", "构建",
|
||||
"creating", "writing", "reading", "editing", "listing", "analyzing", "generating", "building"
|
||||
};
|
||||
|
||||
/**
|
||||
* 从AI响应中提取任务摘要
|
||||
*/
|
||||
public String extractTaskSummary(String aiResponse) {
|
||||
if (aiResponse == null || aiResponse.trim().isEmpty()) {
|
||||
return "处理中...";
|
||||
}
|
||||
|
||||
// 清理响应文本
|
||||
String cleanResponse = aiResponse.replaceAll("```[\\s\\S]*?```", "").trim();
|
||||
|
||||
// 尝试匹配具体操作
|
||||
for (Pattern pattern : ACTION_PATTERNS) {
|
||||
Matcher matcher = pattern.matcher(cleanResponse);
|
||||
if (matcher.find()) {
|
||||
String action = matcher.group(0).trim();
|
||||
if (action.length() > 50) {
|
||||
action = action.substring(0, 47) + "...";
|
||||
}
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
// 查找动作词汇
|
||||
String lowerResponse = cleanResponse.toLowerCase();
|
||||
for (String verb : ACTION_VERBS) {
|
||||
if (lowerResponse.contains(verb.toLowerCase())) {
|
||||
// 提取包含动作词的句子
|
||||
String[] sentences = cleanResponse.split("[.!?\\n]");
|
||||
for (String sentence : sentences) {
|
||||
if (sentence.toLowerCase().contains(verb.toLowerCase())) {
|
||||
String summary = sentence.trim();
|
||||
if (summary.length() > 60) {
|
||||
summary = summary.substring(0, 57) + "...";
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到具体操作,返回通用描述
|
||||
if (cleanResponse.length() > 60) {
|
||||
return cleanResponse.substring(0, 57) + "...";
|
||||
}
|
||||
|
||||
return cleanResponse.isEmpty() ? "处理中..." : cleanResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算任务复杂度和预期轮数
|
||||
*/
|
||||
public int estimateTaskComplexity(String initialMessage) {
|
||||
if (initialMessage == null) return 1;
|
||||
|
||||
String lowerMessage = initialMessage.toLowerCase();
|
||||
int complexity = 1;
|
||||
|
||||
// 基于关键词估算复杂度
|
||||
if (lowerMessage.contains("project") || lowerMessage.contains("项目")) complexity += 3;
|
||||
if (lowerMessage.contains("complete") || lowerMessage.contains("完整")) complexity += 2;
|
||||
if (lowerMessage.contains("multiple") || lowerMessage.contains("多个")) complexity += 2;
|
||||
if (lowerMessage.contains("full-stack") || lowerMessage.contains("全栈")) complexity += 4;
|
||||
if (lowerMessage.contains("website") || lowerMessage.contains("网站")) complexity += 2;
|
||||
if (lowerMessage.contains("api") || lowerMessage.contains("接口")) complexity += 2;
|
||||
|
||||
// 基于文件操作数量估算
|
||||
long fileOperations = lowerMessage.chars()
|
||||
.mapToObj(c -> String.valueOf((char) c))
|
||||
.filter(s -> s.matches(".*(?:create|write|edit|file|directory).*"))
|
||||
.count();
|
||||
|
||||
complexity += (int) Math.min(fileOperations / 2, 5);
|
||||
|
||||
return Math.min(complexity, 15); // 最大15轮
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成当前状态的用户友好描述
|
||||
*/
|
||||
public String generateStatusDescription(String status, String currentAction, int currentTurn, int totalTurns) {
|
||||
StringBuilder desc = new StringBuilder();
|
||||
|
||||
switch (status) {
|
||||
case "RUNNING":
|
||||
if (currentAction != null && !currentAction.trim().isEmpty()) {
|
||||
desc.append("🔄 ").append(currentAction);
|
||||
} else {
|
||||
desc.append("🤔 AI正在思考...");
|
||||
}
|
||||
|
||||
if (totalTurns > 1) {
|
||||
desc.append(String.format(" (第%d/%d轮)", currentTurn, totalTurns));
|
||||
}
|
||||
break;
|
||||
|
||||
case "COMPLETED":
|
||||
desc.append("✅ 任务完成");
|
||||
if (totalTurns > 1) {
|
||||
desc.append(String.format(" (共%d轮)", currentTurn));
|
||||
}
|
||||
break;
|
||||
|
||||
case "ERROR":
|
||||
desc.append("❌ 执行出错");
|
||||
break;
|
||||
|
||||
default:
|
||||
desc.append("⏳ 处理中...");
|
||||
}
|
||||
|
||||
return desc.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 工具执行日志记录服务
|
||||
* 记录所有工具调用的详细信息,使用中文日志
|
||||
*/
|
||||
@Service
|
||||
public class ToolExecutionLogger {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToolExecutionLogger.class);
|
||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
// 工具调用计数器
|
||||
private final AtomicLong callCounter = new AtomicLong(0);
|
||||
|
||||
// 工具执行统计
|
||||
private final Map<String, ToolStats> toolStats = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 记录工具调用开始
|
||||
*/
|
||||
public long logToolStart(String toolName, String description, Object parameters) {
|
||||
long callId = callCounter.incrementAndGet();
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.info("🚀 [工具调用-{}] 开始执行工具: {}", callId, toolName);
|
||||
logger.info("📝 [工具调用-{}] 工具描述: {}", callId, description);
|
||||
logger.info("⚙️ [工具调用-{}] 调用参数: {}", callId, formatParameters(parameters));
|
||||
logger.info("🕐 [工具调用-{}] 开始时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
toolStats.computeIfAbsent(toolName, k -> new ToolStats()).incrementCalls();
|
||||
|
||||
return callId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用成功
|
||||
*/
|
||||
public void logToolSuccess(long callId, String toolName, String result, long executionTimeMs) {
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.info("✅ [工具调用-{}] 工具执行成功: {}", callId, toolName);
|
||||
logger.info("📊 [工具调用-{}] 执行结果: {}", callId, truncateResult(result));
|
||||
logger.info("⏱️ [工具调用-{}] 执行耗时: {}ms", callId, executionTimeMs);
|
||||
logger.info("🕐 [工具调用-{}] 完成时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
ToolStats stats = toolStats.get(toolName);
|
||||
if (stats != null) {
|
||||
stats.incrementSuccess();
|
||||
stats.addExecutionTime(executionTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用失败
|
||||
*/
|
||||
public void logToolError(long callId, String toolName, String error, long executionTimeMs) {
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.error("❌ [工具调用-{}] 工具执行失败: {}", callId, toolName);
|
||||
logger.error("🚨 [工具调用-{}] 错误信息: {}", callId, error);
|
||||
logger.error("⏱️ [工具调用-{}] 执行耗时: {}ms", callId, executionTimeMs);
|
||||
logger.error("🕐 [工具调用-{}] 失败时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
ToolStats stats = toolStats.get(toolName);
|
||||
if (stats != null) {
|
||||
stats.incrementError();
|
||||
stats.addExecutionTime(executionTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用的详细步骤
|
||||
*/
|
||||
public void logToolStep(long callId, String toolName, String step, String details) {
|
||||
logger.debug("🔄 [工具调用-{}] [{}] 执行步骤: {} - {}", callId, toolName, step, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录文件操作
|
||||
*/
|
||||
public void logFileOperation(long callId, String operation, String filePath, String details) {
|
||||
logger.info("📁 [工具调用-{}] 文件操作: {} - 文件: {} - 详情: {}", callId, operation, filePath, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录项目分析
|
||||
*/
|
||||
public void logProjectAnalysis(long callId, String projectPath, String projectType, String details) {
|
||||
logger.info("🔍 [工具调用-{}] 项目分析: 路径={}, 类型={}, 详情={}", callId, projectPath, projectType, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录项目创建
|
||||
*/
|
||||
public void logProjectCreation(long callId, String projectName, String projectType, String projectPath) {
|
||||
logger.info("🏗️ [工具调用-{}] 项目创建: 名称={}, 类型={}, 路径={}", callId, projectName, projectType, projectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具执行统计
|
||||
*/
|
||||
public void logToolStatistics() {
|
||||
logger.info("📈 ========== 工具执行统计 ==========");
|
||||
toolStats.forEach((toolName, stats) -> {
|
||||
logger.info("🔧 工具: {} | 调用次数: {} | 成功: {} | 失败: {} | 平均耗时: {}ms",
|
||||
toolName, stats.getTotalCalls(), stats.getSuccessCount(),
|
||||
stats.getErrorCount(), stats.getAverageExecutionTime());
|
||||
});
|
||||
logger.info("📈 ================================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化参数显示
|
||||
*/
|
||||
private String formatParameters(Object parameters) {
|
||||
if (parameters == null) {
|
||||
return "无参数";
|
||||
}
|
||||
String paramStr = parameters.toString();
|
||||
return paramStr.length() > 200 ? paramStr.substring(0, 200) + "..." : paramStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断结果显示
|
||||
*/
|
||||
private String truncateResult(String result) {
|
||||
if (result == null) {
|
||||
return "无结果";
|
||||
}
|
||||
return result.length() > 300 ? result.substring(0, 300) + "..." : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具统计信息内部类
|
||||
*/
|
||||
private static class ToolStats {
|
||||
private long totalCalls = 0;
|
||||
private long successCount = 0;
|
||||
private long errorCount = 0;
|
||||
private long totalExecutionTime = 0;
|
||||
|
||||
public void incrementCalls() {
|
||||
totalCalls++;
|
||||
}
|
||||
|
||||
public void incrementSuccess() {
|
||||
successCount++;
|
||||
}
|
||||
|
||||
public void incrementError() {
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
public void addExecutionTime(long time) {
|
||||
totalExecutionTime += time;
|
||||
}
|
||||
|
||||
public long getTotalCalls() {
|
||||
return totalCalls;
|
||||
}
|
||||
|
||||
public long getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public long getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
public long getAverageExecutionTime() {
|
||||
return totalCalls > 0 ? totalExecutionTime / totalCalls : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* 工具日志事件类
|
||||
* 继承自LogEvent,添加工具相关的字段
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ToolLogEvent extends LogEvent {
|
||||
|
||||
private String toolName;
|
||||
private String filePath;
|
||||
private String icon;
|
||||
private String status; // RUNNING, SUCCESS, ERROR
|
||||
private Long executionTime; // 执行时间(毫秒)
|
||||
private String summary; // 操作摘要
|
||||
|
||||
// Constructors
|
||||
public ToolLogEvent() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ToolLogEvent(String type, String taskId, String toolName, String filePath,
|
||||
String message, String timestamp, String icon, String status) {
|
||||
super(type, taskId, message, timestamp);
|
||||
this.toolName = toolName;
|
||||
this.filePath = filePath;
|
||||
this.icon = icon;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getToolName() {
|
||||
return toolName;
|
||||
}
|
||||
|
||||
public void setToolName(String toolName) {
|
||||
this.toolName = toolName;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getExecutionTime() {
|
||||
return executionTime;
|
||||
}
|
||||
|
||||
public void setExecutionTime(Long executionTime) {
|
||||
this.executionTime = executionTime;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ToolLogEvent{" +
|
||||
"toolName='" + toolName + '\'' +
|
||||
", filePath='" + filePath + '\'' +
|
||||
", icon='" + icon + '\'' +
|
||||
", status='" + status + '\'' +
|
||||
", executionTime=" + executionTime +
|
||||
", summary='" + summary + '\'' +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.example.demo.utils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
/**
|
||||
* 浏览器工具类
|
||||
* 用于跨平台打开默认浏览器
|
||||
*/
|
||||
public class BrowserUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BrowserUtil.class);
|
||||
|
||||
/**
|
||||
* 打开默认浏览器访问指定URL
|
||||
*
|
||||
* @param url 要访问的URL
|
||||
* @return 是否成功打开
|
||||
*/
|
||||
public static boolean openBrowser(String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
logger.warn("URL is null or empty, cannot open browser");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 方法1: 使用Desktop API (推荐)
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
Desktop desktop = Desktop.getDesktop();
|
||||
if (desktop.isSupported(Desktop.Action.BROWSE)) {
|
||||
desktop.browse(new URI(url));
|
||||
logger.info("Successfully opened browser with URL: {}", url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 方法2: 使用系统命令 (备用方案)
|
||||
return openBrowserWithCommand(url);
|
||||
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
logger.error("Failed to open browser with Desktop API for URL: {}", url, e);
|
||||
// 尝试备用方案
|
||||
return openBrowserWithCommand(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用系统命令打开浏览器 (备用方案)
|
||||
*
|
||||
* @param url 要访问的URL
|
||||
* @return 是否成功打开
|
||||
*/
|
||||
private static boolean openBrowserWithCommand(String url) {
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
ProcessBuilder processBuilder;
|
||||
|
||||
if (os.contains("win")) {
|
||||
// Windows
|
||||
processBuilder = new ProcessBuilder("rundll32", "url.dll,FileProtocolHandler", url);
|
||||
} else if (os.contains("mac")) {
|
||||
// macOS
|
||||
processBuilder = new ProcessBuilder("open", url);
|
||||
} else {
|
||||
// Linux/Unix
|
||||
processBuilder = new ProcessBuilder("xdg-open", url);
|
||||
}
|
||||
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// 等待一小段时间确保命令执行
|
||||
Thread.sleep(500);
|
||||
|
||||
logger.info("Successfully opened browser using system command for URL: {}", url);
|
||||
return true;
|
||||
|
||||
} catch (IOException | InterruptedException e) {
|
||||
logger.error("Failed to open browser using system command for URL: {}", url, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查URL是否有效
|
||||
*
|
||||
* @param url 要检查的URL
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValidUrl(String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
new URI(url);
|
||||
return url.startsWith("http://") || url.startsWith("https://");
|
||||
} catch (URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package com.example.demo.utils;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 跨平台路径处理工具类
|
||||
*/
|
||||
public class PathUtils {
|
||||
|
||||
/**
|
||||
* 构建跨平台兼容的绝对路径
|
||||
*
|
||||
* @param basePath 基础路径
|
||||
* @param relativePath 相对路径部分
|
||||
* @return 跨平台兼容的绝对路径
|
||||
*/
|
||||
public static String buildPath(String basePath, String... relativePath) {
|
||||
return Paths.get(basePath, relativePath).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径,确保跨平台兼容
|
||||
*
|
||||
* @param path 原始路径
|
||||
* @return 规范化后的路径
|
||||
*/
|
||||
public static String normalizePath(String path) {
|
||||
return Paths.get(path).normalize().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否为绝对路径
|
||||
*
|
||||
* @param path 要检查的路径
|
||||
* @return 是否为绝对路径
|
||||
*/
|
||||
public static boolean isAbsolute(String path) {
|
||||
return Paths.get(path).isAbsolute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前工作目录
|
||||
*
|
||||
* @return 当前工作目录的绝对路径
|
||||
*/
|
||||
public static String getCurrentWorkingDirectory() {
|
||||
return System.getProperty("user.dir");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工作空间路径
|
||||
*
|
||||
* @return 工作空间的绝对路径
|
||||
*/
|
||||
public static String buildWorkspacePath() {
|
||||
return buildPath(getCurrentWorkingDirectory(), "workspace");
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,31 +0,0 @@
|
||||
# ai-tutor-skill
|
||||
|
||||
A Claude skill for explaining complex AI and ML concepts in accessible, plain English. This skill transforms abstract technical ideas into clear explanations using structured narrative frameworks, making it ideal for teaching and learning technical topics.
|
||||
|
||||
Resources:
|
||||
- [YouTube Explainer](https://youtu.be/vEvytl7wrGM)
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Python**: >= 3.12
|
||||
- **Package Manager**: [uv](https://github.com/astral-sh/uv)
|
||||
- **Dependencies**: youtube-transcript-api (installed automatically)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd ai-tutor-skill
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
## Important Note: YouTube Transcript Limitations
|
||||
|
||||
> **The YouTube transcript functionality only works when Claude Code is running locally.**
|
||||
>
|
||||
> YouTube blocks requests from Claude's servers, so transcript extraction will fail when using Claude Code in cloud/remote mode. To use this feature, ensure you're running Claude Code on your local machine.
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
name: ai-tutor
|
||||
description: Use when user asks to explain, break down, or help understand technical concepts (AI, ML, or other technical topics). Makes complex ideas accessible through plain English and narrative structure. Use the provided scripts to transcribe videos
|
||||
---
|
||||
|
||||
# AI Tutor
|
||||
|
||||
Transform complex technical concepts into clear, accessible explanations using narrative storytelling frameworks.
|
||||
|
||||
## Before Responding: Think Hard
|
||||
|
||||
Before crafting your explanation:
|
||||
|
||||
1. **Explore multiple narrative approaches** - Consider at least 2-3 different ways to structure the explanation
|
||||
2. **Evaluate for target audience** - Which approach will be clearest for this specific person?
|
||||
3. **Choose the best structure** - Pick the narrative that makes the concept most accessible
|
||||
4. **Plan your examples** - Identify concrete, specific examples before writing
|
||||
|
||||
Take time to think through these options. A well-chosen structure is more valuable than a quick response.
|
||||
|
||||
**If concept is unfamiliar or requires research:** Load `research_methodology.md` for detailed guidance.
|
||||
**If user provides YouTube video:** Call `uv run scripts/get_youtube_transcript.py <video_url_or_id>` for video's transcript.
|
||||
|
||||
## Core Teaching Framework
|
||||
|
||||
Use one of three narrative structures:
|
||||
|
||||
### Status Quo → Problem → Solution
|
||||
1. **Status Quo**: Describe the existing situation or baseline approach
|
||||
2. **Problem**: Explain what's broken, inefficient, or limiting
|
||||
3. **Solution**: Show how the concept solves the problem
|
||||
|
||||
This is the primary go-to structure.
|
||||
|
||||
### What → Why → How
|
||||
1. **What**: Define the concept in simple terms (what it is)
|
||||
2. **Why**: Explain the motivation and importance (why it matters)
|
||||
3. **How**: Break down the mechanics (how it works)
|
||||
|
||||
### What → So What → What Now
|
||||
1. **What**: State the situation or finding
|
||||
2. **So What**: Explain the implications or impact
|
||||
3. **What Now**: Describe next steps or actions
|
||||
|
||||
Use for business contexts and practical applications.
|
||||
|
||||
## Teaching Principles
|
||||
|
||||
### Plain English First
|
||||
Replace technical jargon with clear, direct explanations of the core concept.
|
||||
|
||||
**Example:**
|
||||
- ❌ "The gradient descent algorithm optimizes the loss function via backpropagation"
|
||||
- ✅ "Gradient descent is a way to find the model parameters that make the best predictions based on real-world data"
|
||||
|
||||
Plain English means explaining the concept directly without jargon—not just using analogies.
|
||||
|
||||
### Concrete Examples Ground Abstract Ideas
|
||||
Always provide at least one concrete example with specific details, numbers, or real instances.
|
||||
|
||||
**Example:**
|
||||
- Abstract: "Features are things we use to make predictions"
|
||||
- Concrete: "For our customer churn model, features include age of account and number of logins in the past 90 days"
|
||||
|
||||
### Use Analogies Judiciously
|
||||
Analogies map the unfamiliar to the familiar, but use them sparingly and strategically—not as the primary explanation method.
|
||||
|
||||
**When to use:**
|
||||
- After explaining the concept in plain English
|
||||
- When the technical concept has a strong parallel to everyday experience
|
||||
- To create memorable mental models
|
||||
|
||||
Avoid over-relying on analogies. Start with direct, plain English explanations.
|
||||
|
||||
### Progressive Complexity
|
||||
- Start with the intuition and big picture
|
||||
- Add details layer by layer
|
||||
- Use concrete examples before abstractions
|
||||
- Build from familiar to unfamiliar
|
||||
|
||||
### Less is More
|
||||
Attention and mental effort are finite. Be economical with your audience's cognitive resources.
|
||||
- Cut unnecessary fluff
|
||||
- Every word should earn its place
|
||||
- Focus attention on key information
|
||||
|
||||
### Use Numbered Lists Strategically
|
||||
Numbers help navigate information and make it more digestible (e.g., "3 ways to fine-tune", "System 1 and System 2").
|
||||
|
||||
### Know Thy Audience
|
||||
Adjust technical depth, terminology, and focus based on who you're talking to.
|
||||
|
||||
**C-Suite / Business Leaders:**
|
||||
- Use high-level terms (e.g., "AI")
|
||||
- Focus on what and why, emphasize business impact
|
||||
- Keep it high-level, skip implementation details
|
||||
|
||||
**BI Analysts / Technical Adjacent:**
|
||||
- Use more specific terms (e.g., "LLM")
|
||||
- Cover what and why with more technical context
|
||||
- Discuss workflow relevance, include moderate technical details
|
||||
|
||||
**Data Scientists / Technical Peers:**
|
||||
- Use precise terminology (e.g., "Llama 3 8B")
|
||||
- Cover what, why, AND how
|
||||
- Dive into technical details, discuss specific implementation
|
||||
- Still emphasize business impact (everyone wants to know why)
|
||||
|
||||
**If audience level is unclear:** Assume the lowest level of understanding and explain accordingly. Don't ask the user to clarify—just start with fundamentals. You can always go deeper if they ask for more detail.
|
||||
|
||||
## Response Style
|
||||
|
||||
- Start with the big picture before diving into details
|
||||
- Use conversational, friendly tone
|
||||
- Offer to explain subsections in more depth
|
||||
- Use bullet points sparingly—prefer flowing narrative prose
|
||||
- Include concrete examples with specific details
|
||||
- Connect concepts to real-world applications
|
||||
- Be economical with words—every sentence should add value
|
||||
|
||||
## Workflow Summary
|
||||
|
||||
1. **Think hard**: Explore 2-3 narrative structures, choose the clearest for the audience
|
||||
2. **Identify audience**: Assess knowledge level (if unclear, assume beginner level)
|
||||
3. **Check if research needed**:
|
||||
- Can you explain this with your existing knowledge? → Proceed to step 4
|
||||
- Unfamiliar/cutting-edge topic? → Load `research_methodology.md` first
|
||||
4. **Craft explanation**: Plain English first, no jargon
|
||||
5. **Add concrete example**: Specific details, numbers, real instances
|
||||
6. **Optional analogy**: Only if it adds value beyond direct explanation
|
||||
7. **Offer to dive deeper**: Invite questions on specific aspects
|
||||
@@ -1,9 +0,0 @@
|
||||
[project]
|
||||
name = "ai-tutor"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"youtube-transcript-api>=1.2.3",
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user