Compare commits
166 Commits
9e9486b7c2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91f68576dd | ||
|
|
f84dc1d44e | ||
|
|
d0b7223b53 | ||
|
|
3a41eb40d0 | ||
|
|
fd06f3bce3 | ||
|
|
de33710423 | ||
|
|
87b440e93f | ||
|
|
fd3b149172 | ||
|
|
ff7576f33d | ||
|
|
053a321ec1 | ||
|
|
6ad88cb6c4 | ||
|
|
e57971488c | ||
|
|
642521035b | ||
|
|
b64f8e35c3 | ||
|
|
a62b749ef2 | ||
|
|
cc55fd819f | ||
|
|
39371861f3 | ||
|
|
c01c699410 | ||
|
|
89eec8afc1 | ||
|
|
8b651e4ae1 | ||
|
|
dd6c10f44b | ||
|
|
797ffeee82 | ||
|
|
4fdc95da75 | ||
|
|
b9f49d2c0e | ||
|
|
7ea963dc9a | ||
|
|
5ab6bc0fd0 | ||
|
|
c9d8aa3b93 | ||
|
|
95838dba04 | ||
|
|
f08b0c39d9 | ||
|
|
de501c0252 | ||
|
|
b6b0a9dcb3 | ||
|
|
d5025c0ebc | ||
|
|
d86f82a4c5 | ||
|
|
b0f0b95c2a | ||
|
|
1daf58a94c | ||
|
|
479e2e209d | ||
|
|
ca746366f6 | ||
|
|
8ed4e0ce3b | ||
|
|
bd7d7b4913 | ||
|
|
26fbf51fc8 | ||
|
|
8a5d5d6474 | ||
|
|
b3f21adecb | ||
|
|
be901ee112 | ||
|
|
48ac9bdf19 | ||
|
|
1a8e0fb51f | ||
|
|
871bd715c4 | ||
|
|
6323987a0d | ||
|
|
81d2797035 | ||
|
|
fffa0ebe50 | ||
|
|
9011b91dba | ||
|
|
3a159e2f5a | ||
|
|
e473b5c147 | ||
|
|
e57c94c19d | ||
|
|
84d34236e8 | ||
|
|
245a210b25 | ||
|
|
8278fdc81c | ||
|
|
4c862ef4b7 | ||
|
|
d292c34579 | ||
|
|
43418d0453 | ||
|
|
e9c76c59ca | ||
|
|
8ab3d73fb1 | ||
|
|
751ce45619 | ||
|
|
c400367d22 | ||
|
|
6f2ccdf783 | ||
|
|
909f11dae5 | ||
|
|
a6e0246418 | ||
|
|
d679639175 | ||
|
|
3ac350eb27 | ||
|
|
5b78189c9c | ||
|
|
1271e4967b | ||
|
|
5b2f14123e | ||
|
|
13e48df63d | ||
|
|
4f489f402a | ||
|
|
914ed81f19 | ||
|
|
d12db03216 | ||
|
|
047651e404 | ||
|
|
ed2f19d257 | ||
|
|
362f397e37 | ||
|
|
cc36d8dcaf | ||
|
|
91540dec0c | ||
|
|
0d2008b35c | ||
|
|
ba806f779e | ||
|
|
d1ae9421c4 | ||
|
|
755a8f041d | ||
|
|
23c3577ef4 | ||
|
|
dc8cd09535 | ||
|
|
5160a73254 | ||
|
|
4d8b8e4ad5 | ||
|
|
9ad7881378 | ||
|
|
702ea05d4d | ||
|
|
fd130f1f60 | ||
|
|
ddd39396f7 | ||
|
|
ef8252e2dd | ||
|
|
5ebc0a640b | ||
|
|
f35d945d9c | ||
|
|
e4715bbabb | ||
|
|
726369f5a9 | ||
|
|
2f9af0b173 | ||
|
|
eb8ea9974f | ||
|
|
16eb8c691e | ||
|
|
48e5830b9e | ||
|
|
451cda7aa6 | ||
|
|
913c6da8f3 | ||
|
|
38e5bdbc04 | ||
|
|
e4652ace1c | ||
|
|
820b1b6f13 | ||
|
|
76a82f6203 | ||
|
|
3bab6af63c | ||
|
|
ff25c2fef7 | ||
|
|
710bf72353 | ||
|
|
8dc9ce73b9 | ||
|
|
700f6a40b8 | ||
|
|
f4af77cb2b | ||
|
|
ee51fcdbeb | ||
|
|
0f0bffe695 | ||
|
|
e37024bdbe | ||
|
|
5e76828afe | ||
|
|
6deea42de1 | ||
|
|
09d65701a3 | ||
|
|
fa5522e32e | ||
|
|
757858a9aa | ||
|
|
aee8af0877 | ||
|
|
b07f133ed6 | ||
|
|
8ff1c57b83 | ||
|
|
bf25bc5a47 | ||
|
|
029ca6e7f6 | ||
|
|
7eb1498412 | ||
|
|
2bf9192fc5 | ||
|
|
df2ab51e70 | ||
|
|
f8564b631d | ||
|
|
bdcb5c6c14 | ||
|
|
6b033da313 | ||
|
|
38ece7e3d3 | ||
|
|
cef3fe4a43 | ||
|
|
646901b5fa | ||
|
|
508406ff10 | ||
|
|
0672e82fc7 | ||
|
|
ac1a6fd0e6 | ||
|
|
e2a91d88e8 | ||
|
|
dc14418963 | ||
|
|
17475bf0d7 | ||
|
|
b7aecad092 | ||
|
|
35d2cb5ea5 | ||
|
|
ab1d022905 | ||
|
|
b9132c421d | ||
|
|
f8048724c4 | ||
|
|
512c7ab095 | ||
|
|
2373e0b5f3 | ||
|
|
40151c3902 | ||
|
|
15a3285ed5 | ||
|
|
31b4b5a281 | ||
|
|
2eb9a99da3 | ||
|
|
c6fdff20e8 | ||
|
|
2c3f2c8c54 | ||
|
|
44fe160880 | ||
|
|
f9e090125c | ||
|
|
a01fc93594 | ||
|
|
f69fb46c59 | ||
|
|
873cd6ef53 | ||
|
|
a280774f50 | ||
|
|
142b8b78fa | ||
|
|
6ff9bfc6b5 | ||
|
|
4b1a580132 | ||
|
|
8092c4b1f8 | ||
|
|
24045274ad | ||
|
|
afb3d9f4e6 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -8,6 +8,12 @@
|
||||
!/qiming-mobile/
|
||||
!/qiming-claude-code-acp-ts/
|
||||
!/qiming-dev-inject/
|
||||
!/qiming-mcp-proxy/
|
||||
!/qiming-noVNC/
|
||||
!/qiming-rcoder/
|
||||
!/qiming-run_code_rmcp/
|
||||
!/qiming-vite-plugin-design-mode/
|
||||
!/qiming-xagi-frontend-templates/
|
||||
!/qimingclaw/
|
||||
!/qimingcode/
|
||||
|
||||
@@ -19,6 +25,8 @@ node_modules/
|
||||
# Build outputs and generated artifacts
|
||||
dist/
|
||||
build/
|
||||
!qiming-file-server/src/utils/build/
|
||||
!qiming-file-server/src/utils/build/**
|
||||
out/
|
||||
target/
|
||||
.umi/
|
||||
@@ -31,6 +39,8 @@ coverage/
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
**/.idea/
|
||||
**/logs/
|
||||
|
||||
# Local environment files
|
||||
.env
|
||||
|
||||
2088
development-environment-startup-manual.md
Normal file
2088
development-environment-startup-manual.md
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 403 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 403 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 403 KiB |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name:
|
||||
description:
|
||||
license:
|
||||
---
|
||||
|
||||
# Processing Guide
|
||||
|
||||
## Overview
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Skill template",
|
||||
"config": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# Helper script for this skill
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Process script placeholder")
|
||||
@@ -0,0 +1,3 @@
|
||||
# API Documentation
|
||||
|
||||
Add interface descriptions related to skills here.
|
||||
@@ -104,6 +104,10 @@
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bill-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<profiles>
|
||||
<profile>
|
||||
|
||||
@@ -216,13 +216,6 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
requestContext.setLangMap(userDto.getLangMap());
|
||||
requestContext.setLang(userDto.getLang());
|
||||
log.debug("JWT token OK, userId: {} ", userDto.getId());
|
||||
if (originalRequestUri.startsWith("/api/system/")) {
|
||||
//判断是不是管理员
|
||||
if (userDto.getRole() != User.Role.Admin) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
String header = request.getHeader("X-Client-Type");
|
||||
if (header != null || token.startsWith("ticket")) {
|
||||
authService.renewToken(token);
|
||||
@@ -358,4 +351,4 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
throw BizException.of(HttpStatusEnum.UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED,
|
||||
BizExceptionCodeEnum.systemUnauthorizedOrSessionExpired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
spring:
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ANT_PATH_MATCHER
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
|
||||
# 使用 dynamic-datasource-spring-boot-starter 配置
|
||||
datasource:
|
||||
# 动态SQL生成器配置
|
||||
sql-generator:
|
||||
# 指定使用的数据库类型:mysql 或 doris
|
||||
type: mysql
|
||||
dynamic:
|
||||
# 设置默认的数据源或者数据源组, 默认值即为 master
|
||||
primary: master
|
||||
# 设置严格模式,默认 false. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源 primary 配置的数据源
|
||||
strict: false
|
||||
# p6spy 开关, 默认 false
|
||||
p6spy: false
|
||||
# seata 开关, 默认 false
|
||||
seata: false
|
||||
# 数据源配置信息
|
||||
datasource:
|
||||
# 主数据源 (MySQL)
|
||||
master:
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:Byyagz@121}
|
||||
# 以下为 Druid 连接池配置 (如果使用)
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 300000
|
||||
validation-query: SELECT 1 FROM DUAL
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
filters: stat # 启用 stat filter 用于监控
|
||||
# Doris 数据源
|
||||
doris:
|
||||
url: jdbc:mysql://${DORIS_HOST:localhost}:3306/${DORIS_DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${DORIS_USERNAME:root}
|
||||
password: ${DORIS_PASSWORD:Byyagz@121}
|
||||
# Doris 表配置
|
||||
table:
|
||||
replication-num: 1 # 副本数,生产环境通常为3,开发环境可以为1
|
||||
bucket-num: 10 # 分桶数
|
||||
duplicate-key: id # 重复键字段名,默认使用第一个字段
|
||||
distributed-key: id # 分布键字段名,默认使用第一个字段
|
||||
# 可选: 如果 Doris 也用 Druid,可以添加 druid 配置
|
||||
druid:
|
||||
initial-size: 2
|
||||
min-idle: 2
|
||||
max-active: 10
|
||||
max-wait: 60000
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:Byyagz@121}
|
||||
database: ${REDIS_DB:1}
|
||||
lettuce:
|
||||
pool:
|
||||
max-idle: 16
|
||||
max-active: 32
|
||||
min-idle: 8
|
||||
|
||||
main:
|
||||
allow-circular-references: true
|
||||
log:
|
||||
level: DEBUG
|
||||
|
||||
server:
|
||||
port: 18081
|
||||
|
||||
jwt:
|
||||
secretKey: ${JWT_SECRET_KEY:qiming-local-dev-jwt-secret-key-32-chars}
|
||||
|
||||
file:
|
||||
uploadFolder: ${FILE_UPLOAD_FOLDER:D:/work/workspace/feitian-base/nvw/.dev/uploads}
|
||||
baseUrl: ${FILE_BASE_URL:http://localhost:18081/files/}
|
||||
|
||||
# Storage configuration
|
||||
storage:
|
||||
type: ${STORAGE_TYPE:local} # Storage type: local, cos(Tencent Cloud), oss(Alibaba Cloud), s3(S3 protocol/MinIO)
|
||||
|
||||
local:
|
||||
upload:
|
||||
folder: ${LOCAL_UPLOAD_FOLDER:D:/work/workspace/feitian-base/nvw/.dev/uploads}
|
||||
|
||||
# Tencent Cloud COS configuration (takes effect when storage.type=cos)
|
||||
cos:
|
||||
secret-id: ${COS_SECRET_ID:}
|
||||
secret-key: ${COS_SECRET_KEY:}
|
||||
region: ${COS_REGION:ap-chengdu}
|
||||
bucket-name: ${COS_BUCKET_NAME:}
|
||||
endpoint: ${COS_ENDPOINT:}
|
||||
# Legacy configuration below
|
||||
secretId: ${COS_SECRET_ID:}
|
||||
secretKey: ${COS_SECRET_KEY:}
|
||||
baseUrl: ${COS_BASE_URL:}
|
||||
|
||||
# Alibaba Cloud OSS configuration (takes effect when storage.type=oss)
|
||||
oss:
|
||||
endpoint: ${OSS_ENDPOINT:}
|
||||
access-key-id: ${OSS_ACCESS_KEY_ID:}
|
||||
access-key-secret: ${OSS_ACCESS_KEY_SECRET:}
|
||||
region-name: ${OSS_REGION_NAME:cn-chengdu}
|
||||
bucket-name: ${OSS_BUCKET_NAME:}
|
||||
|
||||
# S3 protocol configuration (takes effect when storage.type=s3, supports MinIO etc.)
|
||||
s3:
|
||||
endpoint: ${S3_ENDPOINT:}
|
||||
access-key: ${S3_ACCESS_KEY:}
|
||||
secret-key: ${S3_SECRET_KEY:}
|
||||
bucket-name: ${S3_BUCKET_NAME:}
|
||||
region: ${S3_REGION:us-east-1}
|
||||
|
||||
vector-store:
|
||||
engine: milvus
|
||||
|
||||
milvus:
|
||||
uri: ${MILVUS_URI:http://43.153.147.203:19530/}
|
||||
user: ${MILVUS_USER:}
|
||||
password: ${MILVUS_PASSWORD:}
|
||||
|
||||
code:
|
||||
execute:
|
||||
url: ${CODE_EXECUTE_URL:http://localhost:18020/api/run_code_with_log}
|
||||
|
||||
# 日志模块配置
|
||||
log-module:
|
||||
log:
|
||||
rust-service:
|
||||
# Rust 日志服务的基础 URL
|
||||
base-url: ${LOG_SERVICE_URL:http://localhost:8097}
|
||||
# 连接超时时间(秒)
|
||||
connect-timeout-seconds: 10
|
||||
# 写入超时时间(秒)
|
||||
write-timeout-seconds: 30
|
||||
# 读取超时时间(秒)
|
||||
read-timeout-seconds: 30
|
||||
# 是否启用连接池
|
||||
enable-connection-pool: true
|
||||
# 最大空闲连接数
|
||||
max-idle-connections: 5
|
||||
# 连接保持存活时间(分钟)
|
||||
keep-alive-duration-minutes: 5
|
||||
|
||||
search:
|
||||
elasticsearch:
|
||||
url: ${ES_URL:http://43.153.147.203:19200}
|
||||
username: ${ES_USERNAME:}
|
||||
password: ${ES_PASSWORD:}
|
||||
api_key: ${ES_API_KEY:}
|
||||
# 生态市场模块配置
|
||||
eco-market:
|
||||
# 服务端配置
|
||||
server:
|
||||
# 远程server端的 ip端口配置
|
||||
base-url: ${ECO_MARKET_SERVER_URL:https://test-agent-market-api.xspaceagi.com}
|
||||
client:
|
||||
# 客户端配置
|
||||
retry:
|
||||
#自动注册客户端到 server端, 重试间隔时间,单位:秒;
|
||||
interval: 300
|
||||
|
||||
mcp:
|
||||
proxy-base-url: ${MCP_PROXY_URL:http://localhost:18020}
|
||||
|
||||
knife4j:
|
||||
# 开启增强配置
|
||||
enable: true
|
||||
# 标识是否生产环境:true-生产环境关闭文档,false-显示文档
|
||||
production: false
|
||||
|
||||
custom-page:
|
||||
http-proxy:
|
||||
host: ${CUSTOM_PAGE_PROXY_HOST:0.0.0.0}
|
||||
port: ${CUSTOM_PAGE_PROXY_PORT:18082}
|
||||
dev-server-host: ${DEV_SERVER_HOST:http://localhost}
|
||||
prod-server-host: ${PROD_SERVER_HOST:http://localhost:18099}
|
||||
build-server:
|
||||
base-url: ${BUILD_SERVER_URL:http://localhost:16000/api}
|
||||
ai-agent:
|
||||
base-url: ${AI_AGENT_URL:http://43.153.147.203:18087}
|
||||
docker-proxy:
|
||||
enable: ${DOCKER_PROXY_ENABLE:true}
|
||||
base-url: ${DOCKER_PROXY_URL:http://43.153.147.203:18088}
|
||||
|
||||
computer:
|
||||
proxy:
|
||||
host: ${COMPUTER_PROXY_HOST:0.0.0.0}
|
||||
port: ${COMPUTER_PROXY_PORT:18085}
|
||||
|
||||
supportCustomDomain: true
|
||||
|
||||
# 内外穿透相关服务配置
|
||||
reverse:
|
||||
server:
|
||||
# 内部服务器配置
|
||||
inner:
|
||||
# 内部服务可访问地址,如果分布式部署,可以在每台主机上通过环境变量制定 SERVICE_HOST
|
||||
service-host: ${SERVICE_HOST:127.0.0.1}
|
||||
# 绑定主机地址,如果分布式部署,可以在每台主机上通过环境变量制定 BIND_HOST
|
||||
bind-host: ${BIND_HOST:0.0.0.0}
|
||||
# 内部服务器端口范围
|
||||
ports: ${REVERSE_PORTS:30000-40000}
|
||||
# 外部服务器配置
|
||||
outer:
|
||||
# 外部用于与客户端保持连接的服务器主机地址,多个用英文逗号隔开,例如 192.168.1.12,192.168.1.13,留空时使用平台服务器地址
|
||||
host: ${OUTER_HOST:}
|
||||
# 外部服务器端口
|
||||
port: ${OUTER_PORT:11076}
|
||||
|
||||
model-api-proxy:
|
||||
base-api-url: ${MODEL_API_BASE_URL:http://localhost:18086}
|
||||
enable-model-proxy: ${MODEL_PROXY_ENABLE:true}
|
||||
port: ${MODEL_PROXY_PORT:18086}
|
||||
#记录日志
|
||||
save-log: ${MODEL_PROXY_SAVE_LOG:true}
|
||||
@@ -48,7 +48,6 @@
|
||||
<logger name="org.redisson" level="ERROR"/>
|
||||
<logger name="com.alibaba" level="ERROR"/>
|
||||
<logger name="com.xspaceagi.system.domain.service.impl.ScheduleTaskDomainServiceImpl" level="ERROR"/>
|
||||
|
||||
<!-- 缺省应用日志输出 -->
|
||||
<root level="${LOG_LEVEL}">
|
||||
<if condition='"${DEV_MODE}".equals("dev")'>
|
||||
@@ -68,4 +67,4 @@
|
||||
</then>
|
||||
</if>
|
||||
</root>
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.xspaceagi.agent.core.adapter.application;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBillingEstimateDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public interface DigitalEmployeeSyncApplicationService {
|
||||
DigitalEmployeeSyncResponseDto syncOutbox(DigitalEmployeeSyncRequestDto request);
|
||||
|
||||
DigitalEmployeeSyncRecordPageDto queryRecords(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String outboxId,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeeBusinessViewPageDto queryBusinessViews(
|
||||
String entityType,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityId,
|
||||
String status,
|
||||
String syncStatus,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeePlanBusinessViewDetailDto queryPlanBusinessViewDetail(
|
||||
String planId,
|
||||
String clientKey,
|
||||
String deviceId
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminWorkbenchPageDto queryAdminWorkbench(
|
||||
String view,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String status,
|
||||
String kind,
|
||||
String category,
|
||||
String level,
|
||||
String priority,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
String policyKind,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request);
|
||||
|
||||
DigitalEmployeeAdminActionPageDto queryPendingAdminActions(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminActionResultDto markAdminActionResult(
|
||||
Long actionId,
|
||||
String status,
|
||||
List<String> reasonCodes
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminRecordResultDto recordDailyReportSendResult(DigitalEmployeeAdminRecordRequestDto request);
|
||||
|
||||
DigitalEmployeeBillingEstimateDto queryBillingEstimates(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
Date updatedFrom,
|
||||
Date updatedTo
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminRecordResultDto recordAuditArchive(DigitalEmployeeAdminRecordRequestDto request);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeAdminActionDto {
|
||||
private Long id;
|
||||
@JsonProperty("client_key_masked")
|
||||
private String clientKeyMasked;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
private String action;
|
||||
private String status;
|
||||
@JsonProperty("reason_codes")
|
||||
private List<String> reasonCodes;
|
||||
@JsonProperty("request_payload")
|
||||
private Map<String, Object> requestPayload;
|
||||
@JsonProperty("created_at")
|
||||
private Date createdAt;
|
||||
@JsonProperty("processed_at")
|
||||
private Date processedAt;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeAdminActionPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeeAdminActionDto> records;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class DigitalEmployeeAdminActionRequestDto {
|
||||
@JsonProperty("sync_record_id")
|
||||
private Long syncRecordId;
|
||||
@JsonProperty("client_key")
|
||||
private String clientKey;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
private String action;
|
||||
@JsonProperty("request_payload")
|
||||
private Map<String, Object> requestPayload;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeAdminActionResultDto {
|
||||
private Boolean ok;
|
||||
private String status;
|
||||
@JsonProperty("reason_codes")
|
||||
private List<String> reasonCodes;
|
||||
private DigitalEmployeeAdminActionDto action;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class DigitalEmployeeAdminRecordRequestDto {
|
||||
@JsonAlias("sync_record_id")
|
||||
private Long syncRecordId;
|
||||
@JsonAlias("client_key")
|
||||
private String clientKey;
|
||||
@JsonAlias("device_id")
|
||||
private String deviceId;
|
||||
@JsonAlias("entity_type")
|
||||
private String entityType;
|
||||
@JsonAlias("entity_id")
|
||||
private String entityId;
|
||||
private String status;
|
||||
private String endpoint;
|
||||
@JsonAlias("remote_id")
|
||||
private String remoteId;
|
||||
@JsonAlias("archive_location")
|
||||
private String archiveLocation;
|
||||
@JsonAlias("archive_summary")
|
||||
private Map<String, Object> archiveSummary;
|
||||
private String reason;
|
||||
@JsonProperty("request_payload")
|
||||
@JsonAlias("requestPayload")
|
||||
private Map<String, Object> requestPayload;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeAdminRecordResultDto {
|
||||
private Boolean ok;
|
||||
private String status;
|
||||
@JsonProperty("reason_codes")
|
||||
private List<String> reasonCodes;
|
||||
private DigitalEmployeeAdminActionDto record;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeAdminWorkbenchPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeeAdminWorkbenchRowDto> records;
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeAdminWorkbenchRowDto {
|
||||
private String view;
|
||||
@JsonProperty("sync_record_id")
|
||||
private Long syncRecordId;
|
||||
@JsonProperty("sync_record_url")
|
||||
private String syncRecordUrl;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
private String operation;
|
||||
private String title;
|
||||
private String status;
|
||||
private String kind;
|
||||
private String category;
|
||||
private String level;
|
||||
private String priority;
|
||||
private String summary;
|
||||
@JsonProperty("plan_id")
|
||||
private String planId;
|
||||
@JsonProperty("task_id")
|
||||
private String taskId;
|
||||
@JsonProperty("run_id")
|
||||
private String runId;
|
||||
@JsonProperty("step_id")
|
||||
private String stepId;
|
||||
@JsonProperty("dependency_status")
|
||||
private String dependencyStatus;
|
||||
@JsonProperty("lease_state")
|
||||
private String leaseState;
|
||||
@JsonProperty("lease_source")
|
||||
private String leaseSource;
|
||||
@JsonProperty("dispatch_status")
|
||||
private String dispatchStatus;
|
||||
@JsonProperty("dispatch_policy_key")
|
||||
private String dispatchPolicyKey;
|
||||
@JsonProperty("orchestration_id")
|
||||
private String orchestrationId;
|
||||
@JsonProperty("trigger_source")
|
||||
private String triggerSource;
|
||||
@JsonProperty("approval_id")
|
||||
private String approvalId;
|
||||
@JsonProperty("approval_policy_id")
|
||||
private String approvalPolicyId;
|
||||
@JsonProperty("conflict_policy_id")
|
||||
private String conflictPolicyId;
|
||||
@JsonProperty("decision_guard_status")
|
||||
private String decisionGuardStatus;
|
||||
@JsonProperty("batch_accept_remote_disabled")
|
||||
private Boolean batchAcceptRemoteDisabled;
|
||||
@JsonProperty("auto_decision_disabled")
|
||||
private Boolean autoDecisionDisabled;
|
||||
@JsonProperty("policy_reason")
|
||||
private String policyReason;
|
||||
@JsonProperty("governance_policy_id")
|
||||
private String governancePolicyId;
|
||||
@JsonProperty("policy_guard_status")
|
||||
private String policyGuardStatus;
|
||||
@JsonProperty("hard_interrupt_disabled")
|
||||
private Boolean hardInterruptDisabled;
|
||||
@JsonProperty("artifact_id")
|
||||
private String artifactId;
|
||||
@JsonProperty("artifact_graph_id")
|
||||
private String artifactGraphId;
|
||||
@JsonProperty("artifact_group_id")
|
||||
private String artifactGroupId;
|
||||
@JsonProperty("group_key")
|
||||
private String groupKey;
|
||||
@JsonProperty("related_artifact_ids")
|
||||
private List<String> relatedArtifactIds;
|
||||
@JsonProperty("artifact_count")
|
||||
private Long artifactCount;
|
||||
@JsonProperty("device_count")
|
||||
private Long deviceCount;
|
||||
@JsonProperty("version_count")
|
||||
private Long versionCount;
|
||||
@JsonProperty("latest_artifact_id")
|
||||
private String latestArtifactId;
|
||||
@JsonProperty("version_key")
|
||||
private String versionKey;
|
||||
@JsonProperty("cleanup_request_id")
|
||||
private String cleanupRequestId;
|
||||
@JsonProperty("cleanup_eligible_count")
|
||||
private Long cleanupEligibleCount;
|
||||
@JsonProperty("batch_cleanup_disabled")
|
||||
private Boolean batchCleanupDisabled;
|
||||
@JsonProperty("retention_conflict_status")
|
||||
private String retentionConflictStatus;
|
||||
@JsonProperty("cleanup_conflict_status")
|
||||
private String cleanupConflictStatus;
|
||||
@JsonProperty("remote_delete_disabled")
|
||||
private Boolean remoteDeleteDisabled;
|
||||
@JsonProperty("notification_id")
|
||||
private String notificationId;
|
||||
@JsonProperty("collaboration_thread_id")
|
||||
private String collaborationThreadId;
|
||||
@JsonProperty("subscription_policy_id")
|
||||
private String subscriptionPolicyId;
|
||||
@JsonProperty("subscription_conflict_status")
|
||||
private String subscriptionConflictStatus;
|
||||
@JsonProperty("notification_policy_id")
|
||||
private String notificationPolicyId;
|
||||
@JsonProperty("subscription_status")
|
||||
private String subscriptionStatus;
|
||||
@JsonProperty("desktop_enabled")
|
||||
private Boolean desktopEnabled;
|
||||
@JsonProperty("minimum_level")
|
||||
private String minimumLevel;
|
||||
private String channel;
|
||||
@JsonProperty("delivery_status")
|
||||
private String deliveryStatus;
|
||||
@JsonProperty("delivery_attempt_id")
|
||||
private String deliveryAttemptId;
|
||||
@JsonProperty("send_request_id")
|
||||
private String sendRequestId;
|
||||
@JsonProperty("real_push_disabled")
|
||||
private Boolean realPushDisabled;
|
||||
@JsonProperty("conflict_status")
|
||||
private String conflictStatus;
|
||||
@JsonProperty("report_id")
|
||||
private String reportId;
|
||||
@JsonProperty("daily_report_fact_id")
|
||||
private String dailyReportFactId;
|
||||
@JsonProperty("daily_report_table")
|
||||
private String dailyReportTable;
|
||||
@JsonProperty("daily_report_materialized")
|
||||
private Boolean dailyReportMaterialized;
|
||||
@JsonProperty("daily_report_physical_fact_status")
|
||||
private String dailyReportPhysicalFactStatus;
|
||||
@JsonProperty("send_record_id")
|
||||
private String sendRecordId;
|
||||
@JsonProperty("send_channel")
|
||||
private String sendChannel;
|
||||
@JsonProperty("send_request_status")
|
||||
private String sendRequestStatus;
|
||||
@JsonProperty("send_record_status")
|
||||
private String sendRecordStatus;
|
||||
@JsonProperty("formal_daily_report_page_disabled")
|
||||
private Boolean formalDailyReportPageDisabled;
|
||||
@JsonProperty("external_send_disabled")
|
||||
private Boolean externalSendDisabled;
|
||||
@JsonProperty("third_party_channel_disabled")
|
||||
private Boolean thirdPartyChannelDisabled;
|
||||
@JsonProperty("approval_decision_status")
|
||||
private String approvalDecisionStatus;
|
||||
@JsonProperty("advanced_query_supported")
|
||||
private Boolean advancedQuerySupported;
|
||||
@JsonProperty("aggregation_scope")
|
||||
private String aggregationScope;
|
||||
@JsonProperty("materialized_source")
|
||||
private String materializedSource;
|
||||
@JsonProperty("daily_report_approval_id")
|
||||
private String dailyReportApprovalId;
|
||||
@JsonProperty("daily_report_approval_action")
|
||||
private String dailyReportApprovalAction;
|
||||
@JsonProperty("daily_report_approval_actor")
|
||||
private String dailyReportApprovalActor;
|
||||
@JsonProperty("daily_report_approval_reason")
|
||||
private String dailyReportApprovalReason;
|
||||
@JsonProperty("daily_report_approval_recorded_at")
|
||||
private String dailyReportApprovalRecordedAt;
|
||||
@JsonProperty("route_decision_id")
|
||||
private String routeDecisionId;
|
||||
@JsonProperty("service_id")
|
||||
private String serviceId;
|
||||
@JsonProperty("service_lease_id")
|
||||
private String serviceLeaseId;
|
||||
@JsonProperty("lease_holder")
|
||||
private String leaseHolder;
|
||||
@JsonProperty("lease_expires_at")
|
||||
private String leaseExpiresAt;
|
||||
@JsonProperty("restart_record_id")
|
||||
private String restartRecordId;
|
||||
@JsonProperty("restart_count")
|
||||
private Long restartCount;
|
||||
@JsonProperty("last_restart_at")
|
||||
private String lastRestartAt;
|
||||
@JsonProperty("recovery_policy_id")
|
||||
private String recoveryPolicyId;
|
||||
@JsonProperty("auto_recovery_status")
|
||||
private String autoRecoveryStatus;
|
||||
@JsonProperty("service_start_disabled")
|
||||
private Boolean serviceStartDisabled;
|
||||
@JsonProperty("service_stop_disabled")
|
||||
private Boolean serviceStopDisabled;
|
||||
@JsonProperty("lease_takeover_disabled")
|
||||
private Boolean leaseTakeoverDisabled;
|
||||
@JsonProperty("auto_recovery_disabled")
|
||||
private Boolean autoRecoveryDisabled;
|
||||
@JsonProperty("recovery_action_request_id")
|
||||
private String recoveryActionRequestId;
|
||||
@JsonProperty("start_request_id")
|
||||
private String startRequestId;
|
||||
@JsonProperty("stop_request_id")
|
||||
private String stopRequestId;
|
||||
@JsonProperty("lease_takeover_request_id")
|
||||
private String leaseTakeoverRequestId;
|
||||
@JsonProperty("auto_recovery_request_id")
|
||||
private String autoRecoveryRequestId;
|
||||
@JsonProperty("start_service_disabled")
|
||||
private Boolean startServiceDisabled;
|
||||
@JsonProperty("stop_service_disabled")
|
||||
private Boolean stopServiceDisabled;
|
||||
@JsonProperty("memory_id")
|
||||
private String memoryId;
|
||||
@JsonProperty("memory_key")
|
||||
private String memoryKey;
|
||||
@JsonProperty("governance_status")
|
||||
private String governanceStatus;
|
||||
@JsonProperty("merge_candidate_count")
|
||||
private Long mergeCandidateCount;
|
||||
@JsonProperty("duplicate_memory_ids")
|
||||
private List<String> duplicateMemoryIds;
|
||||
@JsonProperty("latest_memory_id")
|
||||
private String latestMemoryId;
|
||||
@JsonProperty("last_governance_action")
|
||||
private String lastGovernanceAction;
|
||||
@JsonProperty("last_reviewed_at")
|
||||
private String lastReviewedAt;
|
||||
@JsonProperty("memory_policy_id")
|
||||
private String memoryPolicyId;
|
||||
@JsonProperty("memory_policy_status")
|
||||
private String memoryPolicyStatus;
|
||||
@JsonProperty("merge_policy_id")
|
||||
private String mergePolicyId;
|
||||
@JsonProperty("merge_action_disabled")
|
||||
private Boolean mergeActionDisabled;
|
||||
@JsonProperty("remote_memory_policy_disabled")
|
||||
private Boolean remoteMemoryPolicyDisabled;
|
||||
@JsonProperty("skill_id")
|
||||
private String skillId;
|
||||
@JsonProperty("skill_version")
|
||||
private String skillVersion;
|
||||
@JsonProperty("install_status")
|
||||
private String installStatus;
|
||||
@JsonProperty("permission_scope")
|
||||
private String permissionScope;
|
||||
@JsonProperty("policy_source")
|
||||
private String policySource;
|
||||
@JsonProperty("policy_status")
|
||||
private String policyStatus;
|
||||
@JsonProperty("skill_install_request_id")
|
||||
private String skillInstallRequestId;
|
||||
@JsonProperty("skill_upgrade_request_id")
|
||||
private String skillUpgradeRequestId;
|
||||
@JsonProperty("permission_policy_id")
|
||||
private String permissionPolicyId;
|
||||
@JsonProperty("permission_edit_request_id")
|
||||
private String permissionEditRequestId;
|
||||
@JsonProperty("install_action_disabled")
|
||||
private Boolean installActionDisabled;
|
||||
@JsonProperty("upgrade_action_disabled")
|
||||
private Boolean upgradeActionDisabled;
|
||||
@JsonProperty("permission_edit_disabled")
|
||||
private Boolean permissionEditDisabled;
|
||||
@JsonProperty("tool_name")
|
||||
private String toolName;
|
||||
@JsonProperty("server_id")
|
||||
private String serverId;
|
||||
@JsonProperty("audit_archive_id")
|
||||
private String auditArchiveId;
|
||||
@JsonProperty("archive_status")
|
||||
private String archiveStatus;
|
||||
@JsonProperty("archive_location")
|
||||
private String archiveLocation;
|
||||
@JsonProperty("billing_estimate_id")
|
||||
private String billingEstimateId;
|
||||
@JsonProperty("billing_source")
|
||||
private String billingSource;
|
||||
private Boolean estimated;
|
||||
@JsonProperty("diagnostic_id")
|
||||
private String diagnosticId;
|
||||
@JsonProperty("diagnostic_kind")
|
||||
private String diagnosticKind;
|
||||
@JsonProperty("issue_code")
|
||||
private String issueCode;
|
||||
@JsonProperty("repair_action")
|
||||
private String repairAction;
|
||||
@JsonProperty("repair_status")
|
||||
private String repairStatus;
|
||||
@JsonProperty("repair_reason")
|
||||
private String repairReason;
|
||||
@JsonProperty("safe_repair_disabled")
|
||||
private Boolean safeRepairDisabled;
|
||||
@JsonProperty("billing_actual_disabled")
|
||||
private Boolean billingActualDisabled;
|
||||
@JsonProperty("sandbox_archive_file_disabled")
|
||||
private Boolean sandboxArchiveFileDisabled;
|
||||
@JsonProperty("auto_repair_execution_disabled")
|
||||
private Boolean autoRepairExecutionDisabled;
|
||||
@JsonProperty("repair_approval_required")
|
||||
private Boolean repairApprovalRequired;
|
||||
@JsonProperty("recommended_action")
|
||||
private String recommendedAction;
|
||||
@JsonProperty("policy_kind")
|
||||
private String policyKind;
|
||||
@JsonProperty("policy_key")
|
||||
private String policyKey;
|
||||
@JsonProperty("policy_edit_request_id")
|
||||
private String policyEditRequestId;
|
||||
@JsonProperty("policy_edit_status")
|
||||
private String policyEditStatus;
|
||||
@JsonProperty("policy_edit_reason")
|
||||
private String policyEditReason;
|
||||
@JsonProperty("merge_request_id")
|
||||
private String mergeRequestId;
|
||||
@JsonProperty("memory_merge_request_id")
|
||||
private String memoryMergeRequestId;
|
||||
@JsonProperty("remote_memory_policy_request_id")
|
||||
private String remoteMemoryPolicyRequestId;
|
||||
@JsonProperty("merge_recommendation")
|
||||
private String mergeRecommendation;
|
||||
@JsonProperty("remote_policy_edit_disabled")
|
||||
private Boolean remotePolicyEditDisabled;
|
||||
@JsonProperty("auto_merge_disabled")
|
||||
private Boolean autoMergeDisabled;
|
||||
@JsonProperty("approval_policy_edit_disabled")
|
||||
private Boolean approvalPolicyEditDisabled;
|
||||
@JsonProperty("route_policy_edit_disabled")
|
||||
private Boolean routePolicyEditDisabled;
|
||||
@JsonProperty("route_recovery_disabled")
|
||||
private Boolean routeRecoveryDisabled;
|
||||
@JsonProperty("high_risk_execution_disabled")
|
||||
private Boolean highRiskExecutionDisabled;
|
||||
@JsonProperty("schedule_id")
|
||||
private String scheduleId;
|
||||
@JsonProperty("job_id")
|
||||
private String jobId;
|
||||
@JsonProperty("revision_id")
|
||||
private String revisionId;
|
||||
@JsonProperty("revision_fact_id")
|
||||
private String revisionFactId;
|
||||
@JsonProperty("revision_status")
|
||||
private String revisionStatus;
|
||||
@JsonProperty("revision_materialized")
|
||||
private Boolean revisionMaterialized;
|
||||
@JsonProperty("base_revision_id")
|
||||
private String baseRevisionId;
|
||||
@JsonProperty("changed_fields")
|
||||
private List<String> changedFields;
|
||||
@JsonProperty("occurred_at")
|
||||
private String occurredAt;
|
||||
@JsonProperty("synced_at")
|
||||
private Date syncedAt;
|
||||
@JsonProperty("client_key_masked")
|
||||
private String clientKeyMasked;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("business_view")
|
||||
private Map<String, Object> businessView;
|
||||
private Map<String, Object> payload;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeBillingEstimateDto {
|
||||
@JsonProperty("total_tokens")
|
||||
private Long totalTokens;
|
||||
@JsonProperty("input_tokens")
|
||||
private Long inputTokens;
|
||||
@JsonProperty("output_tokens")
|
||||
private Long outputTokens;
|
||||
@JsonProperty("estimated_cost_usd")
|
||||
private BigDecimal estimatedCostUsd;
|
||||
@JsonProperty("request_count")
|
||||
private Long requestCount;
|
||||
private Boolean estimated;
|
||||
@JsonProperty("by_model")
|
||||
private Map<String, ModelEstimate> byModel;
|
||||
@JsonProperty("updated_at")
|
||||
private Date updatedAt;
|
||||
@JsonProperty("source_records")
|
||||
private List<Long> sourceRecords;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class ModelEstimate {
|
||||
@JsonProperty("total_tokens")
|
||||
private Long totalTokens;
|
||||
@JsonProperty("input_tokens")
|
||||
private Long inputTokens;
|
||||
@JsonProperty("output_tokens")
|
||||
private Long outputTokens;
|
||||
@JsonProperty("estimated_cost_usd")
|
||||
private BigDecimal estimatedCostUsd;
|
||||
@JsonProperty("request_count")
|
||||
private Long requestCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeBusinessViewPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeeBusinessViewRowDto> records;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeBusinessViewRowDto {
|
||||
@JsonProperty("sync_record_id")
|
||||
private Long syncRecordId;
|
||||
@JsonProperty("sync_record_url")
|
||||
private String syncRecordUrl;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
private String operation;
|
||||
private String title;
|
||||
private String status;
|
||||
private String summary;
|
||||
@JsonProperty("plan_id")
|
||||
private String planId;
|
||||
@JsonProperty("task_id")
|
||||
private String taskId;
|
||||
@JsonProperty("run_id")
|
||||
private String runId;
|
||||
private String kind;
|
||||
@JsonProperty("occurred_at")
|
||||
private String occurredAt;
|
||||
@JsonProperty("started_at")
|
||||
private String startedAt;
|
||||
@JsonProperty("finished_at")
|
||||
private String finishedAt;
|
||||
@JsonProperty("duration_ms")
|
||||
private Long durationMs;
|
||||
@JsonProperty("sync_status")
|
||||
private String syncStatus;
|
||||
@JsonProperty("client_key_masked")
|
||||
private String clientKeyMasked;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("synced_at")
|
||||
private Date syncedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
@JsonProperty("business_view")
|
||||
private Map<String, Object> businessView;
|
||||
private Map<String, Object> payload;
|
||||
@JsonProperty("task_count")
|
||||
private Integer taskCount;
|
||||
@JsonProperty("run_count")
|
||||
private Integer runCount;
|
||||
@JsonProperty("latest_event_at")
|
||||
private String latestEventAt;
|
||||
@JsonProperty("last_event_message")
|
||||
private String lastEventMessage;
|
||||
@JsonProperty("advanced_query_supported")
|
||||
private Boolean advancedQuerySupported;
|
||||
@JsonProperty("aggregation_scope")
|
||||
private String aggregationScope;
|
||||
@JsonProperty("materialized_source")
|
||||
private String materializedSource;
|
||||
private Boolean materialized;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeePlanBusinessViewDetailDto {
|
||||
private DigitalEmployeeBusinessViewRowDto plan;
|
||||
private List<DigitalEmployeeBusinessViewRowDto> tasks;
|
||||
private List<DigitalEmployeeBusinessViewRowDto> runs;
|
||||
private List<DigitalEmployeeBusinessViewRowDto> events;
|
||||
private List<DigitalEmployeeBusinessViewRowDto> artifacts;
|
||||
private List<DigitalEmployeeBusinessViewRowDto> approvals;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeePolicySnapshotPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeePolicySnapshotRowDto> records;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeePolicySnapshotRowDto {
|
||||
@JsonProperty("policy_kind")
|
||||
private String policyKind;
|
||||
@JsonProperty("policy_key")
|
||||
private String policyKey;
|
||||
private String status;
|
||||
private String summary;
|
||||
@JsonProperty("client_count")
|
||||
private Integer clientCount;
|
||||
@JsonProperty("device_count")
|
||||
private Integer deviceCount;
|
||||
@JsonProperty("conflict_keys")
|
||||
private List<String> conflictKeys;
|
||||
@JsonProperty("latest_synced_at")
|
||||
private Date latestSyncedAt;
|
||||
@JsonProperty("recommended_resolution")
|
||||
private String recommendedResolution;
|
||||
@JsonProperty("route_decision_count")
|
||||
private Integer routeDecisionCount;
|
||||
@JsonProperty("blocked_count")
|
||||
private Integer blockedCount;
|
||||
@JsonProperty("approval_required_count")
|
||||
private Integer approvalRequiredCount;
|
||||
@JsonProperty("missing_context_count")
|
||||
private Integer missingContextCount;
|
||||
@JsonProperty("not_mapped_count")
|
||||
private Integer notMappedCount;
|
||||
@JsonProperty("intervention_conflict_status")
|
||||
private String interventionConflictStatus;
|
||||
@JsonProperty("approval_policy_id")
|
||||
private String approvalPolicyId;
|
||||
@JsonProperty("conflict_policy_id")
|
||||
private String conflictPolicyId;
|
||||
@JsonProperty("decision_guard_status")
|
||||
private String decisionGuardStatus;
|
||||
@JsonProperty("batch_accept_remote_disabled")
|
||||
private Boolean batchAcceptRemoteDisabled;
|
||||
@JsonProperty("auto_decision_disabled")
|
||||
private Boolean autoDecisionDisabled;
|
||||
@JsonProperty("policy_reason")
|
||||
private String policyReason;
|
||||
@JsonProperty("dispatch_policy_key")
|
||||
private String dispatchPolicyKey;
|
||||
@JsonProperty("max_per_sweep")
|
||||
private Long maxPerSweep;
|
||||
@JsonProperty("auto_dispatch_ready_steps")
|
||||
private Boolean autoDispatchReadySteps;
|
||||
@JsonProperty("catch_up_on_startup")
|
||||
private Boolean catchUpOnStartup;
|
||||
@JsonProperty("max_run_history")
|
||||
private Long maxRunHistory;
|
||||
@JsonProperty("sync_record_url")
|
||||
private String syncRecordUrl;
|
||||
@JsonProperty("client_key_masked")
|
||||
private String clientKeyMasked;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("business_view")
|
||||
private Map<String, Object> businessView;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeSyncAckDto {
|
||||
@JsonProperty("outbox_id")
|
||||
private String outboxId;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
@JsonProperty("remote_id")
|
||||
private String remoteId;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class DigitalEmployeeSyncItemDto {
|
||||
@JsonAlias("outbox_id")
|
||||
private String outboxId;
|
||||
@JsonAlias("entity_type")
|
||||
private String entityType;
|
||||
@JsonAlias("entity_id")
|
||||
private String entityId;
|
||||
private String operation;
|
||||
@JsonAlias("contract_version")
|
||||
private String contractVersion;
|
||||
@JsonAlias("entity_summary")
|
||||
private Map<String, Object> entitySummary;
|
||||
@JsonAlias("business_view")
|
||||
private Map<String, Object> businessView;
|
||||
private Map<String, Object> payload;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeSyncRecordDto {
|
||||
private Long id;
|
||||
@JsonProperty("client_key")
|
||||
private String clientKey;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("outbox_id")
|
||||
private String outboxId;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
private String operation;
|
||||
@JsonProperty("contract_version")
|
||||
private String contractVersion;
|
||||
@JsonProperty("entity_summary")
|
||||
private String entitySummary;
|
||||
@JsonProperty("business_view")
|
||||
private String businessView;
|
||||
private String payload;
|
||||
@JsonProperty("synced_at")
|
||||
private Date syncedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeSyncRecordPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeeSyncRecordDto> records;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class DigitalEmployeeSyncRequestDto {
|
||||
@JsonAlias("client_key")
|
||||
private String clientKey;
|
||||
@JsonAlias("device_id")
|
||||
private String deviceId;
|
||||
private List<DigitalEmployeeSyncItemDto> items;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeSyncResponseDto {
|
||||
private List<DigitalEmployeeSyncAckDto> acked;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
|
||||
|
||||
public interface DigitalEmployeeAdminActionRepository extends IService<DigitalEmployeeAdminAction> {
|
||||
IPage<DigitalEmployeeAdminAction> queryActions(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle;
|
||||
|
||||
public interface DigitalEmployeeArtifactLifecycleRepository extends IService<DigitalEmployeeArtifactLifecycle> {
|
||||
void upsertBySyncRecord(DigitalEmployeeArtifactLifecycle lifecycle);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public interface DigitalEmployeeBusinessFactRepository extends IService<DigitalEmployeeBusinessFact> {
|
||||
void upsertBySyncRecord(DigitalEmployeeBusinessFact fact);
|
||||
|
||||
IPage<DigitalEmployeeBusinessFact> queryFacts(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
String syncStatus,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact;
|
||||
|
||||
public interface DigitalEmployeeDailyReportFactRepository extends IService<DigitalEmployeeDailyReportFact> {
|
||||
void upsertBySyncRecord(DigitalEmployeeDailyReportFact fact);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public interface DigitalEmployeeSyncRecordRepository extends IService<DigitalEmployeeSyncRecord> {
|
||||
IPage<DigitalEmployeeSyncRecord> queryRecords(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String outboxId,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
);
|
||||
|
||||
IPage<DigitalEmployeeSyncRecord> queryBusinessRecords(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
String syncStatus,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@TableName("digital_employee_admin_action")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class DigitalEmployeeAdminAction {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
@TableField(value = "_tenant_id")
|
||||
private Long tenantId;
|
||||
private Long userId;
|
||||
private String clientKey;
|
||||
private String deviceId;
|
||||
private String entityType;
|
||||
private String entityId;
|
||||
private String action;
|
||||
private String status;
|
||||
private String reasonCodes;
|
||||
private String requestPayload;
|
||||
private Date processedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@TableName("digital_employee_artifact_lifecycle")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class DigitalEmployeeArtifactLifecycle {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
@TableField(value = "_tenant_id")
|
||||
private Long tenantId;
|
||||
private Long syncRecordId;
|
||||
private String clientKey;
|
||||
private String deviceId;
|
||||
private String artifactId;
|
||||
private String planId;
|
||||
private String taskId;
|
||||
private String runId;
|
||||
private String status;
|
||||
private String retentionStatus;
|
||||
private String cleanupStatus;
|
||||
private String deletedAt;
|
||||
private String versionKey;
|
||||
private String source;
|
||||
private String businessView;
|
||||
private Date syncedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@TableName("digital_employee_business_fact")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class DigitalEmployeeBusinessFact {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
@TableField(value = "_tenant_id")
|
||||
private Long tenantId;
|
||||
private Long userId;
|
||||
private Long syncRecordId;
|
||||
private String clientKey;
|
||||
private String deviceId;
|
||||
private String outboxId;
|
||||
private String entityType;
|
||||
private String entityId;
|
||||
private String operation;
|
||||
private String title;
|
||||
private String status;
|
||||
private String summary;
|
||||
private String planId;
|
||||
private String taskId;
|
||||
private String runId;
|
||||
private String kind;
|
||||
private String category;
|
||||
private String occurredAt;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
private Long durationMs;
|
||||
private String syncStatus;
|
||||
private String businessView;
|
||||
private String payload;
|
||||
private Date syncedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@TableName("digital_employee_daily_report_fact")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class DigitalEmployeeDailyReportFact {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
@TableField(value = "_tenant_id")
|
||||
private Long tenantId;
|
||||
private Long syncRecordId;
|
||||
private String clientKey;
|
||||
private String deviceId;
|
||||
private String reportId;
|
||||
private String dailyReportFactId;
|
||||
private String planId;
|
||||
private String status;
|
||||
private String physicalFactStatus;
|
||||
private String deliveryStatus;
|
||||
private String approvalStatus;
|
||||
private String approvalDecisionStatus;
|
||||
private String confirmedAt;
|
||||
private String reportDate;
|
||||
private String sendRecordId;
|
||||
private String sendChannel;
|
||||
private String sendRequestStatus;
|
||||
private String sendRecordStatus;
|
||||
private String materializedSource;
|
||||
private String businessView;
|
||||
private Date syncedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@TableName("digital_employee_sync_record")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class DigitalEmployeeSyncRecord {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
@TableField(value = "_tenant_id")
|
||||
private Long tenantId;
|
||||
private Long userId;
|
||||
private String clientKey;
|
||||
private String deviceId;
|
||||
private String outboxId;
|
||||
private String entityType;
|
||||
private String entityId;
|
||||
private String operation;
|
||||
private String contractVersion;
|
||||
private String entitySummary;
|
||||
private String businessView;
|
||||
private String payload;
|
||||
private Date syncedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
package com.xspaceagi.agent.core.infra.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DigitalEmployeeAdminActionMapper extends BaseMapper<DigitalEmployeeAdminAction> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xspaceagi.agent.core.infra.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DigitalEmployeeArtifactLifecycleMapper extends BaseMapper<DigitalEmployeeArtifactLifecycle> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xspaceagi.agent.core.infra.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DigitalEmployeeBusinessFactMapper extends BaseMapper<DigitalEmployeeBusinessFact> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xspaceagi.agent.core.infra.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DigitalEmployeeDailyReportFactMapper extends BaseMapper<DigitalEmployeeDailyReportFact> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xspaceagi.agent.core.infra.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DigitalEmployeeSyncRecordMapper extends BaseMapper<DigitalEmployeeSyncRecord> {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.xspaceagi.agent.core.infra.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeAdminActionRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
|
||||
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeAdminActionMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeAdminActionRepositoryImpl
|
||||
extends ServiceImpl<DigitalEmployeeAdminActionMapper, DigitalEmployeeAdminAction>
|
||||
implements DigitalEmployeeAdminActionRepository {
|
||||
|
||||
@Override
|
||||
public IPage<DigitalEmployeeAdminAction> queryActions(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
) {
|
||||
LambdaQueryWrapper<DigitalEmployeeAdminAction> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeAdminAction>()
|
||||
.eq(tenantId != null, DigitalEmployeeAdminAction::getTenantId, tenantId)
|
||||
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeAdminAction::getClientKey, clientKey)
|
||||
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeAdminAction::getDeviceId, deviceId)
|
||||
.eq(StringUtils.isNotBlank(status), DigitalEmployeeAdminAction::getStatus, status)
|
||||
.orderByAsc(DigitalEmployeeAdminAction::getCreated)
|
||||
.orderByAsc(DigitalEmployeeAdminAction::getId);
|
||||
return page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.xspaceagi.agent.core.infra.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeArtifactLifecycleRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle;
|
||||
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeArtifactLifecycleMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeArtifactLifecycleRepositoryImpl
|
||||
extends ServiceImpl<DigitalEmployeeArtifactLifecycleMapper, DigitalEmployeeArtifactLifecycle>
|
||||
implements DigitalEmployeeArtifactLifecycleRepository {
|
||||
|
||||
@Override
|
||||
public void upsertBySyncRecord(DigitalEmployeeArtifactLifecycle lifecycle) {
|
||||
if (lifecycle == null || lifecycle.getTenantId() == null || lifecycle.getSyncRecordId() == null) return;
|
||||
DigitalEmployeeArtifactLifecycle existing = getOne(new LambdaQueryWrapper<DigitalEmployeeArtifactLifecycle>()
|
||||
.eq(DigitalEmployeeArtifactLifecycle::getTenantId, lifecycle.getTenantId())
|
||||
.eq(DigitalEmployeeArtifactLifecycle::getSyncRecordId, lifecycle.getSyncRecordId()), false);
|
||||
if (existing == null) {
|
||||
save(lifecycle);
|
||||
} else {
|
||||
lifecycle.setId(existing.getId());
|
||||
lifecycle.setCreated(existing.getCreated());
|
||||
updateById(lifecycle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.xspaceagi.agent.core.infra.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeBusinessFactRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact;
|
||||
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeBusinessFactMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeBusinessFactRepositoryImpl
|
||||
extends ServiceImpl<DigitalEmployeeBusinessFactMapper, DigitalEmployeeBusinessFact>
|
||||
implements DigitalEmployeeBusinessFactRepository {
|
||||
|
||||
@Override
|
||||
public void upsertBySyncRecord(DigitalEmployeeBusinessFact fact) {
|
||||
if (fact == null || fact.getTenantId() == null || fact.getSyncRecordId() == null) return;
|
||||
DigitalEmployeeBusinessFact existing = getOne(new LambdaQueryWrapper<DigitalEmployeeBusinessFact>()
|
||||
.eq(DigitalEmployeeBusinessFact::getTenantId, fact.getTenantId())
|
||||
.eq(DigitalEmployeeBusinessFact::getSyncRecordId, fact.getSyncRecordId()), false);
|
||||
if (existing == null) {
|
||||
save(fact);
|
||||
} else {
|
||||
fact.setId(existing.getId());
|
||||
fact.setCreated(existing.getCreated());
|
||||
updateById(fact);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<DigitalEmployeeBusinessFact> queryFacts(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
String syncStatus,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
) {
|
||||
LambdaQueryWrapper<DigitalEmployeeBusinessFact> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeBusinessFact>()
|
||||
.eq(tenantId != null, DigitalEmployeeBusinessFact::getTenantId, tenantId)
|
||||
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeBusinessFact::getClientKey, clientKey)
|
||||
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeBusinessFact::getDeviceId, deviceId)
|
||||
.eq(StringUtils.isNotBlank(entityType), DigitalEmployeeBusinessFact::getEntityType, entityType)
|
||||
.eq(StringUtils.isNotBlank(entityId), DigitalEmployeeBusinessFact::getEntityId, entityId)
|
||||
.eq(StringUtils.isNotBlank(syncStatus), DigitalEmployeeBusinessFact::getSyncStatus, syncStatus)
|
||||
.ge(updatedFrom != null, DigitalEmployeeBusinessFact::getSyncedAt, updatedFrom)
|
||||
.le(updatedTo != null, DigitalEmployeeBusinessFact::getSyncedAt, updatedTo)
|
||||
.orderByDesc(DigitalEmployeeBusinessFact::getSyncedAt)
|
||||
.orderByDesc(DigitalEmployeeBusinessFact::getId);
|
||||
return page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.xspaceagi.agent.core.infra.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeDailyReportFactRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact;
|
||||
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeDailyReportFactMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeDailyReportFactRepositoryImpl
|
||||
extends ServiceImpl<DigitalEmployeeDailyReportFactMapper, DigitalEmployeeDailyReportFact>
|
||||
implements DigitalEmployeeDailyReportFactRepository {
|
||||
|
||||
@Override
|
||||
public void upsertBySyncRecord(DigitalEmployeeDailyReportFact fact) {
|
||||
if (fact == null || fact.getTenantId() == null || fact.getSyncRecordId() == null) return;
|
||||
DigitalEmployeeDailyReportFact existing = getOne(new LambdaQueryWrapper<DigitalEmployeeDailyReportFact>()
|
||||
.eq(DigitalEmployeeDailyReportFact::getTenantId, fact.getTenantId())
|
||||
.eq(DigitalEmployeeDailyReportFact::getSyncRecordId, fact.getSyncRecordId()), false);
|
||||
if (existing == null) {
|
||||
save(fact);
|
||||
} else {
|
||||
fact.setId(existing.getId());
|
||||
fact.setCreated(existing.getCreated());
|
||||
updateById(fact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.xspaceagi.agent.core.infra.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeSyncRecordRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
|
||||
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeSyncRecordMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeSyncRecordRepositoryImpl
|
||||
extends ServiceImpl<DigitalEmployeeSyncRecordMapper, DigitalEmployeeSyncRecord>
|
||||
implements DigitalEmployeeSyncRecordRepository {
|
||||
|
||||
@Override
|
||||
public IPage<DigitalEmployeeSyncRecord> queryRecords(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String outboxId,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
) {
|
||||
LambdaQueryWrapper<DigitalEmployeeSyncRecord> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeSyncRecord>()
|
||||
.eq(tenantId != null, DigitalEmployeeSyncRecord::getTenantId, tenantId)
|
||||
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeSyncRecord::getClientKey, clientKey)
|
||||
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeSyncRecord::getDeviceId, deviceId)
|
||||
.eq(StringUtils.isNotBlank(entityType), DigitalEmployeeSyncRecord::getEntityType, entityType)
|
||||
.eq(StringUtils.isNotBlank(entityId), DigitalEmployeeSyncRecord::getEntityId, entityId)
|
||||
.eq(StringUtils.isNotBlank(outboxId), DigitalEmployeeSyncRecord::getOutboxId, outboxId)
|
||||
.orderByDesc(DigitalEmployeeSyncRecord::getSyncedAt)
|
||||
.orderByDesc(DigitalEmployeeSyncRecord::getId);
|
||||
return page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<DigitalEmployeeSyncRecord> queryBusinessRecords(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
String syncStatus,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
) {
|
||||
LambdaQueryWrapper<DigitalEmployeeSyncRecord> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeSyncRecord>()
|
||||
.eq(tenantId != null, DigitalEmployeeSyncRecord::getTenantId, tenantId)
|
||||
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeSyncRecord::getClientKey, clientKey)
|
||||
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeSyncRecord::getDeviceId, deviceId)
|
||||
.eq(StringUtils.isNotBlank(entityType), DigitalEmployeeSyncRecord::getEntityType, entityType)
|
||||
.eq(StringUtils.isNotBlank(entityId), DigitalEmployeeSyncRecord::getEntityId, entityId)
|
||||
.ge(updatedFrom != null, DigitalEmployeeSyncRecord::getSyncedAt, updatedFrom)
|
||||
.le(updatedTo != null, DigitalEmployeeSyncRecord::getSyncedAt, updatedTo)
|
||||
.orderByDesc(DigitalEmployeeSyncRecord::getSyncedAt)
|
||||
.orderByDesc(DigitalEmployeeSyncRecord::getId);
|
||||
return page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/admin/actions")
|
||||
public class DigitalEmployeeAdminActionController {
|
||||
|
||||
private static final String CLIENT_KEY_HEADER = "X-Qiming-Client-Key";
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeAdminActionResultDto> createAction(
|
||||
@RequestBody(required = false) DigitalEmployeeAdminActionRequestDto request
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.createAdminAction(request));
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/pending", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeAdminActionPageDto> pendingActions(
|
||||
@RequestHeader(name = CLIENT_KEY_HEADER, required = false) String headerClientKey,
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
String normalizedClientKey = StringUtils.defaultIfBlank(clientKey, headerClientKey);
|
||||
if (StringUtils.isBlank(normalizedClientKey)) {
|
||||
return ReqResult.create("0001", "客户端 Key 缺失", null);
|
||||
}
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryPendingAdminActions(
|
||||
normalizedClientKey.trim(),
|
||||
deviceId,
|
||||
pageNo,
|
||||
pageSize
|
||||
));
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/{actionId}/result", method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeAdminActionResultDto> markResult(
|
||||
@PathVariable("actionId") Long actionId,
|
||||
@RequestBody(required = false) Map<String, Object> body
|
||||
) {
|
||||
String status = body == null ? null : String.valueOf(body.get("status"));
|
||||
List<String> reasonCodes = reasonCodes(body == null ? null : body.get("reason_codes"));
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.markAdminActionResult(actionId, status, reasonCodes));
|
||||
}
|
||||
|
||||
private List<String> reasonCodes(Object value) {
|
||||
if (value instanceof List<?> list) {
|
||||
return list.stream().map(String::valueOf).toList();
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/admin/policies")
|
||||
public class DigitalEmployeeAdminPolicyController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/snapshots", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeePolicySnapshotPageDto> querySnapshots(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "policy_kind", required = false) String policyKind,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryAdminPolicySnapshots(
|
||||
clientKey,
|
||||
deviceId,
|
||||
status,
|
||||
policyKind,
|
||||
parseDate(updatedFrom),
|
||||
parseDate(updatedTo),
|
||||
pageNo,
|
||||
pageSize
|
||||
));
|
||||
}
|
||||
|
||||
private Date parseDate(String value) {
|
||||
if (StringUtils.isBlank(value)) return null;
|
||||
String trimmed = value.trim();
|
||||
try {
|
||||
return Date.from(Instant.parse(trimmed));
|
||||
} catch (Exception ignored) {
|
||||
try {
|
||||
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} catch (Exception ignoredAgain) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBillingEstimateDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/admin")
|
||||
public class DigitalEmployeeAdminRecordController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/daily-reports/send-records", method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeAdminRecordResultDto> recordDailyReportSendResult(
|
||||
@RequestBody(required = false) DigitalEmployeeAdminRecordRequestDto request
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.recordDailyReportSendResult(request));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/billing/estimates", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeBillingEstimateDto> queryBillingEstimates(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryBillingEstimates(
|
||||
clientKey,
|
||||
deviceId,
|
||||
parseDate(updatedFrom),
|
||||
parseDate(updatedTo)
|
||||
));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/audit-archives", method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeAdminRecordResultDto> recordAuditArchive(
|
||||
@RequestBody(required = false) DigitalEmployeeAdminRecordRequestDto request
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.recordAuditArchive(request));
|
||||
}
|
||||
|
||||
private Date parseDate(String value) {
|
||||
if (StringUtils.isBlank(value)) return null;
|
||||
String trimmed = value.trim();
|
||||
try {
|
||||
return Date.from(Instant.parse(trimmed));
|
||||
} catch (Exception ignored) {
|
||||
try {
|
||||
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} catch (Exception ignoredAgain) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/admin/workbench")
|
||||
public class DigitalEmployeeAdminWorkbenchController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/{view}", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeAdminWorkbenchPageDto> queryWorkbench(
|
||||
@PathVariable("view") String view,
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_type", required = false) String entityType,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "kind", required = false) String kind,
|
||||
@RequestParam(name = "category", required = false) String category,
|
||||
@RequestParam(name = "level", required = false) String level,
|
||||
@RequestParam(name = "priority", required = false) String priority,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryAdminWorkbench(
|
||||
view,
|
||||
clientKey,
|
||||
deviceId,
|
||||
entityType,
|
||||
entityId,
|
||||
status,
|
||||
kind,
|
||||
category,
|
||||
level,
|
||||
priority,
|
||||
parseDate(updatedFrom),
|
||||
parseDate(updatedTo),
|
||||
pageNo,
|
||||
pageSize
|
||||
));
|
||||
}
|
||||
|
||||
private Date parseDate(String value) {
|
||||
if (StringUtils.isBlank(value)) return null;
|
||||
String trimmed = value.trim();
|
||||
try {
|
||||
return Date.from(Instant.parse(trimmed));
|
||||
} catch (Exception ignored) {
|
||||
try {
|
||||
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} catch (Exception ignoredAgain) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee")
|
||||
public class DigitalEmployeeBusinessViewController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
|
||||
@RequestMapping(path = "/plans", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryPlans(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "sync_status", required = false) String syncStatus,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(query("plan", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
|
||||
@RequestMapping(path = "/plans/{plan_id}", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeePlanBusinessViewDetailDto> queryPlanDetail(
|
||||
@PathVariable("plan_id") String planId,
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryPlanBusinessViewDetail(planId, clientKey, deviceId));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
|
||||
@RequestMapping(path = "/tasks", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryTasks(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "sync_status", required = false) String syncStatus,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(query("task", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
|
||||
@RequestMapping(path = "/runs", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryRuns(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "sync_status", required = false) String syncStatus,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(query("run", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
|
||||
@RequestMapping(path = "/events", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryEvents(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "sync_status", required = false) String syncStatus,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(query("event", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
|
||||
@RequestMapping(path = "/artifacts", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryArtifacts(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "sync_status", required = false) String syncStatus,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(query("artifact", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
|
||||
@RequestMapping(path = "/approvals", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryApprovals(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "sync_status", required = false) String syncStatus,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(query("approval", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
|
||||
}
|
||||
|
||||
private DigitalEmployeeBusinessViewPageDto query(
|
||||
String entityType,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityId,
|
||||
String status,
|
||||
String syncStatus,
|
||||
String updatedFrom,
|
||||
String updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
) {
|
||||
return digitalEmployeeSyncApplicationService.queryBusinessViews(
|
||||
entityType,
|
||||
clientKey,
|
||||
deviceId,
|
||||
entityId,
|
||||
status,
|
||||
syncStatus,
|
||||
parseDate(updatedFrom),
|
||||
parseDate(updatedTo),
|
||||
pageNo,
|
||||
pageSize
|
||||
);
|
||||
}
|
||||
|
||||
private Date parseDate(String value) {
|
||||
if (StringUtils.isBlank(value)) return null;
|
||||
String trimmed = value.trim();
|
||||
try {
|
||||
return Date.from(Instant.parse(trimmed));
|
||||
} catch (Exception ignored) {
|
||||
try {
|
||||
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} catch (Exception ignoredAgain) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/sync")
|
||||
public class DigitalEmployeeSyncController {
|
||||
|
||||
private static final String CLIENT_KEY_HEADER = "X-Qiming-Client-Key";
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequestMapping(path = "/outbox", method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeSyncResponseDto> syncOutbox(
|
||||
@RequestHeader(name = CLIENT_KEY_HEADER, required = false) String headerClientKey,
|
||||
@RequestBody(required = false) DigitalEmployeeSyncRequestDto request
|
||||
) {
|
||||
if (StringUtils.isBlank(headerClientKey)) {
|
||||
return ReqResult.create("0001", "客户端 Key header 缺失", null);
|
||||
}
|
||||
if (request == null) {
|
||||
request = new DigitalEmployeeSyncRequestDto();
|
||||
}
|
||||
String normalizedHeaderClientKey = headerClientKey.trim();
|
||||
if (StringUtils.isBlank(request.getClientKey())) {
|
||||
request.setClientKey(normalizedHeaderClientKey);
|
||||
} else if (!StringUtils.equals(request.getClientKey().trim(), normalizedHeaderClientKey)) {
|
||||
return ReqResult.create("0001", "客户端 Key header 与请求体不一致", null);
|
||||
} else {
|
||||
request.setClientKey(request.getClientKey().trim());
|
||||
}
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.syncOutbox(request));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
|
||||
@RequestMapping(path = "/records", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeSyncRecordPageDto> queryRecords(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_type", required = false) String entityType,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "outbox_id", required = false) String outboxId,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryRecords(
|
||||
clientKey,
|
||||
deviceId,
|
||||
entityType,
|
||||
entityId,
|
||||
outboxId,
|
||||
pageNo,
|
||||
pageSize
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.xspaceagi.sandbox.application.dto;
|
||||
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "在线监控聚合信息")
|
||||
@Data
|
||||
public class OnlineMonitorDto {
|
||||
|
||||
@Schema(description = "客户端总数")
|
||||
private long totalClientCount;
|
||||
|
||||
@Schema(description = "在线客户端数")
|
||||
private long onlineClientCount;
|
||||
|
||||
@Schema(description = "离线客户端数")
|
||||
private long offlineClientCount;
|
||||
|
||||
@Schema(description = "已停用客户端数")
|
||||
private long inactiveClientCount;
|
||||
|
||||
@Schema(description = "用户总数")
|
||||
private long totalUserCount;
|
||||
|
||||
@Schema(description = "启用用户数")
|
||||
private long enabledUserCount;
|
||||
|
||||
@Schema(description = "禁用用户数")
|
||||
private long disabledUserCount;
|
||||
|
||||
@Schema(description = "用户组监控信息")
|
||||
private List<GroupMonitorDto> groups;
|
||||
|
||||
@Schema(description = "用户监控信息")
|
||||
private List<UserMonitorDto> users;
|
||||
|
||||
@Schema(description = "客户端监控信息")
|
||||
private List<ClientMonitorDto> clients;
|
||||
|
||||
@Schema(description = "用户组监控信息")
|
||||
@Data
|
||||
public static class GroupMonitorDto {
|
||||
private Long groupId;
|
||||
private String groupName;
|
||||
private Integer authEnabled;
|
||||
private Integer status;
|
||||
private Date expireTime;
|
||||
private Integer maxUserCount;
|
||||
private long currentUserCount;
|
||||
private Integer maxClientCount;
|
||||
private long currentClientCount;
|
||||
private long onlineClientCount;
|
||||
private long offlineClientCount;
|
||||
private long inactiveClientCount;
|
||||
}
|
||||
|
||||
@Schema(description = "用户监控信息")
|
||||
@Data
|
||||
public static class UserMonitorDto {
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private String nickName;
|
||||
private User.Role role;
|
||||
private User.Status status;
|
||||
private List<String> groupNames;
|
||||
private long clientCount;
|
||||
private long onlineClientCount;
|
||||
private long inactiveClientCount;
|
||||
private Date lastLoginTime;
|
||||
}
|
||||
|
||||
@Schema(description = "客户端监控信息")
|
||||
@Data
|
||||
public static class ClientMonitorDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private String nickName;
|
||||
private Long groupId;
|
||||
private String groupName;
|
||||
private Boolean isActive;
|
||||
private boolean online;
|
||||
private Integer maxAgentCount;
|
||||
private Date created;
|
||||
private Date modified;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
package com.xspaceagi.sandbox.ui.web.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.xspaceagi.sandbox.application.dto.OnlineMonitorDto;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxConfigDto;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxConfigQueryDto;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxGlobalConfigDto;
|
||||
import com.xspaceagi.sandbox.application.service.SandboxConfigApplicationService;
|
||||
import com.xspaceagi.system.application.dto.UserDto;
|
||||
import com.xspaceagi.system.application.service.SysGroupApplicationService;
|
||||
import com.xspaceagi.system.application.service.SysRoleApplicationService;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysGroup;
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
import com.xspaceagi.system.infra.dao.service.UserService;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.enums.GroupEnum;
|
||||
import com.xspaceagi.system.spec.enums.RoleEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -21,7 +26,12 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -41,6 +51,12 @@ public class SandboxConfigManageController {
|
||||
@Resource
|
||||
private SysGroupApplicationService sysGroupApplicationService;
|
||||
|
||||
@Resource
|
||||
private SysRoleApplicationService sysRoleApplicationService;
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@RequireResource(SANDBOX_CONFIG_QUERY)
|
||||
@Operation(summary = "分页查询配置列表")
|
||||
@PostMapping("/page")
|
||||
@@ -148,6 +164,154 @@ public class SandboxConfigManageController {
|
||||
return ReqResult.success(globalConfig);
|
||||
}
|
||||
|
||||
@RequireResource(ONLINE_MONITOR_QUERY)
|
||||
@Operation(summary = "查询在线监控信息")
|
||||
@GetMapping("/online-monitor")
|
||||
public ReqResult<OnlineMonitorDto> onlineMonitor() {
|
||||
boolean platformAdmin = isPlatformAdmin();
|
||||
List<SysGroup> groups = visibleGroups();
|
||||
Set<Long> groupIds = groups.stream().map(SysGroup::getId).collect(Collectors.toSet());
|
||||
|
||||
List<User> users = visibleUsers(platformAdmin, groupIds);
|
||||
Set<Long> userIds = users.stream().map(User::getId).collect(Collectors.toSet());
|
||||
|
||||
SandboxConfigQueryDto queryDto = new SandboxConfigQueryDto();
|
||||
queryDto.setPageNum(1);
|
||||
queryDto.setPageSize(100000);
|
||||
if (!platformAdmin && !groupIds.isEmpty()) {
|
||||
queryDto.setGroupIds(new ArrayList<>(groupIds));
|
||||
} else if (!platformAdmin) {
|
||||
queryDto.setGroupIds(List.of(-1L));
|
||||
}
|
||||
List<SandboxConfigDto> configs = sandboxConfigApplicationService.pageQuery(queryDto).getRecords().stream()
|
||||
.filter(config -> config.getUserId() != null)
|
||||
.filter(config -> platformAdmin || userIds.contains(config.getUserId()))
|
||||
.toList();
|
||||
|
||||
return ReqResult.success(buildOnlineMonitor(groups, users, configs));
|
||||
}
|
||||
|
||||
private OnlineMonitorDto buildOnlineMonitor(List<SysGroup> groups, List<User> users, List<SandboxConfigDto> configs) {
|
||||
Map<Long, SysGroup> groupMap = groups.stream()
|
||||
.filter(group -> group.getId() != null)
|
||||
.collect(Collectors.toMap(SysGroup::getId, group -> group, (a, b) -> a));
|
||||
Map<Long, User> userMap = users.stream()
|
||||
.filter(user -> user.getId() != null)
|
||||
.collect(Collectors.toMap(User::getId, user -> user, (a, b) -> a));
|
||||
Map<Long, List<SysGroup>> userGroupMap = new HashMap<>();
|
||||
groups.forEach(group -> sysGroupApplicationService.getUserListByGroupId(group.getId())
|
||||
.forEach(user -> userGroupMap.computeIfAbsent(user.getId(), key -> new ArrayList<>()).add(group)));
|
||||
|
||||
OnlineMonitorDto dto = new OnlineMonitorDto();
|
||||
dto.setTotalClientCount(configs.size());
|
||||
dto.setOnlineClientCount(configs.stream().filter(config -> Boolean.TRUE.equals(config.getIsActive()) && config.isOnline()).count());
|
||||
dto.setInactiveClientCount(configs.stream().filter(config -> !Boolean.TRUE.equals(config.getIsActive())).count());
|
||||
dto.setOfflineClientCount(configs.stream().filter(config -> Boolean.TRUE.equals(config.getIsActive()) && !config.isOnline()).count());
|
||||
dto.setTotalUserCount(users.size());
|
||||
dto.setEnabledUserCount(users.stream().filter(user -> user.getStatus() == User.Status.Enabled).count());
|
||||
dto.setDisabledUserCount(users.stream().filter(user -> user.getStatus() == User.Status.Disabled).count());
|
||||
|
||||
dto.setGroups(groups.stream()
|
||||
.map(group -> buildGroupMonitor(group, users, configs))
|
||||
.sorted(Comparator.comparing(OnlineMonitorDto.GroupMonitorDto::getGroupId, Comparator.nullsLast(Long::compareTo)))
|
||||
.toList());
|
||||
dto.setUsers(users.stream()
|
||||
.map(user -> buildUserMonitor(user, userGroupMap, configs))
|
||||
.sorted(Comparator.comparing(OnlineMonitorDto.UserMonitorDto::getUserId, Comparator.nullsLast(Long::compareTo)))
|
||||
.toList());
|
||||
dto.setClients(configs.stream()
|
||||
.map(config -> buildClientMonitor(config, userMap, groupMap))
|
||||
.sorted(Comparator.comparing(OnlineMonitorDto.ClientMonitorDto::getId, Comparator.nullsLast(Long::compareTo)))
|
||||
.toList());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private OnlineMonitorDto.GroupMonitorDto buildGroupMonitor(SysGroup group, List<User> users, List<SandboxConfigDto> configs) {
|
||||
Set<Long> groupUserIds = sysGroupApplicationService.getUserListByGroupId(group.getId()).stream()
|
||||
.map(User::getId)
|
||||
.collect(Collectors.toSet());
|
||||
List<SandboxConfigDto> groupConfigs = configs.stream()
|
||||
.filter(config -> Objects.equals(config.getGroupId(), group.getId()))
|
||||
.toList();
|
||||
OnlineMonitorDto.GroupMonitorDto dto = new OnlineMonitorDto.GroupMonitorDto();
|
||||
dto.setGroupId(group.getId());
|
||||
dto.setGroupName(group.getName());
|
||||
dto.setAuthEnabled(group.getAuthEnabled());
|
||||
dto.setStatus(group.getStatus());
|
||||
dto.setExpireTime(group.getExpireTime());
|
||||
dto.setMaxUserCount(group.getMaxUserCount());
|
||||
dto.setCurrentUserCount(users.stream().filter(user -> groupUserIds.contains(user.getId())).count());
|
||||
dto.setMaxClientCount(group.getMaxClientCount());
|
||||
dto.setCurrentClientCount(groupConfigs.size());
|
||||
dto.setOnlineClientCount(groupConfigs.stream().filter(config -> Boolean.TRUE.equals(config.getIsActive()) && config.isOnline()).count());
|
||||
dto.setInactiveClientCount(groupConfigs.stream().filter(config -> !Boolean.TRUE.equals(config.getIsActive())).count());
|
||||
dto.setOfflineClientCount(groupConfigs.stream().filter(config -> Boolean.TRUE.equals(config.getIsActive()) && !config.isOnline()).count());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private OnlineMonitorDto.UserMonitorDto buildUserMonitor(User user, Map<Long, List<SysGroup>> userGroupMap, List<SandboxConfigDto> configs) {
|
||||
List<SandboxConfigDto> userConfigs = configs.stream()
|
||||
.filter(config -> Objects.equals(config.getUserId(), user.getId()))
|
||||
.toList();
|
||||
OnlineMonitorDto.UserMonitorDto dto = new OnlineMonitorDto.UserMonitorDto();
|
||||
dto.setUserId(user.getId());
|
||||
dto.setUserName(user.getUserName());
|
||||
dto.setNickName(user.getNickName());
|
||||
dto.setRole(user.getRole());
|
||||
dto.setStatus(user.getStatus());
|
||||
dto.setGroupNames(userGroupMap.getOrDefault(user.getId(), List.of()).stream().map(SysGroup::getName).toList());
|
||||
dto.setClientCount(userConfigs.size());
|
||||
dto.setOnlineClientCount(userConfigs.stream().filter(config -> Boolean.TRUE.equals(config.getIsActive()) && config.isOnline()).count());
|
||||
dto.setInactiveClientCount(userConfigs.stream().filter(config -> !Boolean.TRUE.equals(config.getIsActive())).count());
|
||||
dto.setLastLoginTime(user.getLastLoginTime());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private OnlineMonitorDto.ClientMonitorDto buildClientMonitor(SandboxConfigDto config, Map<Long, User> userMap, Map<Long, SysGroup> groupMap) {
|
||||
User user = userMap.get(config.getUserId());
|
||||
SysGroup group = groupMap.get(config.getGroupId());
|
||||
OnlineMonitorDto.ClientMonitorDto dto = new OnlineMonitorDto.ClientMonitorDto();
|
||||
dto.setId(config.getId());
|
||||
dto.setName(config.getName());
|
||||
dto.setUserId(config.getUserId());
|
||||
dto.setUserName(user == null ? null : user.getUserName());
|
||||
dto.setNickName(user == null ? null : user.getNickName());
|
||||
dto.setGroupId(config.getGroupId());
|
||||
dto.setGroupName(group == null ? null : group.getName());
|
||||
dto.setIsActive(config.getIsActive());
|
||||
dto.setOnline(config.isOnline());
|
||||
dto.setMaxAgentCount(config.getMaxAgentCount());
|
||||
dto.setCreated(config.getCreated());
|
||||
dto.setModified(config.getModified());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private List<SysGroup> visibleGroups() {
|
||||
if (isPlatformAdmin()) {
|
||||
return sysGroupApplicationService.getGroupList(new SysGroup()).stream()
|
||||
.filter(group -> !GroupEnum.DEFAULT_GROUP.getCode().equals(group.getCode()))
|
||||
.toList();
|
||||
}
|
||||
Set<Long> groupIds = currentUserGroupIds();
|
||||
if (groupIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return sysGroupApplicationService.listGroupsByIds(new ArrayList<>(groupIds));
|
||||
}
|
||||
|
||||
private List<User> visibleUsers(boolean platformAdmin, Set<Long> groupIds) {
|
||||
if (platformAdmin) {
|
||||
return userService.list().stream()
|
||||
.filter(user -> user.getStatus() != User.Status.Deleted)
|
||||
.toList();
|
||||
}
|
||||
return groupIds.stream()
|
||||
.flatMap(groupId -> sysGroupApplicationService.getUserListByGroupId(groupId).stream())
|
||||
.filter(user -> user.getStatus() != User.Status.Deleted)
|
||||
.collect(Collectors.toMap(User::getId, user -> user, (a, b) -> a))
|
||||
.values().stream().toList();
|
||||
}
|
||||
|
||||
private void applySandboxConfigManageScope(SandboxConfigQueryDto queryDto) {
|
||||
if (queryDto == null || isPlatformAdmin()) {
|
||||
return;
|
||||
@@ -172,13 +336,18 @@ public class SandboxConfigManageController {
|
||||
}
|
||||
|
||||
private Set<Long> currentUserGroupIds() {
|
||||
return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream()
|
||||
return sysGroupApplicationService.getGroupListByUserId(getCurrentUserDto().getId()).stream()
|
||||
.filter(group -> !GroupEnum.DEFAULT_GROUP.getCode().equals(group.getCode()))
|
||||
.map(SysGroup::getId)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private boolean isPlatformAdmin() {
|
||||
return getCurrentUserDto().getRole() == User.Role.Admin;
|
||||
UserDto currentUser = getCurrentUserDto();
|
||||
return currentUser.getRole() == User.Role.Admin
|
||||
&& (currentUserGroupIds().isEmpty()
|
||||
|| sysRoleApplicationService.getRoleListByUserId(currentUser.getId()).stream()
|
||||
.anyMatch(role -> RoleEnum.SUPER_ADMIN.getCode().equals(role.getCode())));
|
||||
}
|
||||
|
||||
private UserDto getCurrentUserDto() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserQueryDto implements Serializable {
|
||||
@@ -29,4 +30,7 @@ public class UserQueryDto implements Serializable {
|
||||
|
||||
@Schema(description = "角色")
|
||||
private User.Role role;
|
||||
|
||||
@Schema(description = "用户ID范围,后端数据权限过滤使用")
|
||||
private List<Long> userIds;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.xspaceagi.system.application.dto.permission;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -28,6 +29,7 @@ public class SysGroupAddDto implements Serializable {
|
||||
private Integer authEnabled;
|
||||
|
||||
@Schema(description = "授权到期时间,空表示不过期")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date expireTime;
|
||||
|
||||
@Schema(description = "来源,1:系统内置 2:用户自定义", hidden = true)
|
||||
@@ -38,4 +40,4 @@ public class SysGroupAddDto implements Serializable {
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sortIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.xspaceagi.system.application.dto.permission;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -31,6 +32,7 @@ public class SysGroupUpdateDto implements Serializable {
|
||||
private Integer authEnabled;
|
||||
|
||||
@Schema(description = "授权到期时间,空表示不过期")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date expireTime;
|
||||
|
||||
@Schema(description = "来源,1:系统内置 2:用户自定义", hidden = true)
|
||||
@@ -41,4 +43,4 @@ public class SysGroupUpdateDto implements Serializable {
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sortIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,12 @@ public class UserApplicationServiceImpl implements UserApplicationService {
|
||||
UserQueryDto userQueryDto = pageQueryVo.getQueryFilter();
|
||||
if (userQueryDto != null) {
|
||||
BeanUtils.copyProperties(userQueryDto, user);
|
||||
if (userQueryDto.getUserIds() != null) {
|
||||
if (userQueryDto.getUserIds().isEmpty()) {
|
||||
return new Page<>(pageQueryVo.getPageNo(), pageQueryVo.getPageSize(), 0);
|
||||
}
|
||||
queryWrapper.in(User::getId, userQueryDto.getUserIds());
|
||||
}
|
||||
}
|
||||
if ("".equals(user.getUserName())) {
|
||||
user.setUserName(null);
|
||||
|
||||
@@ -145,13 +145,22 @@
|
||||
"sortIndex": 16,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"code": "online_monitor",
|
||||
"name": "在线监控模块",
|
||||
"description": "在线监控模块",
|
||||
"source": 1,
|
||||
"type": 1,
|
||||
"sortIndex": 17,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"code": "category_config",
|
||||
"name": "分类管理模块",
|
||||
"description": "分类管理模块",
|
||||
"source": 1,
|
||||
"type": 1,
|
||||
"sortIndex": 17,
|
||||
"sortIndex": 18,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
@@ -924,6 +933,16 @@
|
||||
"sortIndex": 1,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"code": "online_monitor_query",
|
||||
"name": "查询",
|
||||
"description": "查询在线监控",
|
||||
"source": 1,
|
||||
"type": 2,
|
||||
"parentCode": "online_monitor",
|
||||
"sortIndex": 1,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"code": "subscription_points_query",
|
||||
"name": "查询",
|
||||
@@ -2645,6 +2664,18 @@
|
||||
"sortIndex": 10,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"parentCode": "system_manage",
|
||||
"code": "online_monitor",
|
||||
"name": "在线监控",
|
||||
"description": "在线监控",
|
||||
"source": 1,
|
||||
"path": "/system/online-monitor",
|
||||
"openType": 1,
|
||||
"icon": "",
|
||||
"sortIndex": 11,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"parentCode": "workspace",
|
||||
"code": "member_setting",
|
||||
@@ -2664,7 +2695,7 @@
|
||||
"source": 1,
|
||||
"openType": 1,
|
||||
"icon": "",
|
||||
"sortIndex": 11,
|
||||
"sortIndex": 12,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
@@ -2675,7 +2706,7 @@
|
||||
"source": 1,
|
||||
"openType": 1,
|
||||
"icon": "",
|
||||
"sortIndex": 12,
|
||||
"sortIndex": 13,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
@@ -3203,6 +3234,11 @@
|
||||
"resourceCode": "sandbox_config",
|
||||
"resourceBindType": 1
|
||||
},
|
||||
{
|
||||
"menuCode": "online_monitor",
|
||||
"resourceCode": "online_monitor",
|
||||
"resourceBindType": 1
|
||||
},
|
||||
{
|
||||
"menuCode": "category_config",
|
||||
"resourceCode": "category_config",
|
||||
@@ -4016,6 +4052,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"roleCode": "super_admin",
|
||||
"menuCode": "online_monitor",
|
||||
"menuBindType": 1,
|
||||
"resourceTree": [
|
||||
{
|
||||
"code": "online_monitor",
|
||||
"resourceBindType": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"roleCode": "super_admin",
|
||||
"menuCode": "category_config",
|
||||
@@ -4354,4 +4401,4 @@
|
||||
"pageDailyPromptLimit": -1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +267,11 @@ public enum ResourceEnum {
|
||||
SANDBOX_CONFIG_DELETE(ResourceTypeEnum.OPERATION, "sandbox_config_delete", "删除", "sandbox_config"),
|
||||
// SANDBOX_CONFIG_ENABLE(ResourceTypeEnum.OPERATION, "sandbox_config_enable", "启用/禁用", "sandbox_config"),
|
||||
|
||||
// ================== 在线监控模块 ==================
|
||||
ONLINE_MONITOR(ResourceTypeEnum.MODULE, "online_monitor", "在线监控模块", "root"),
|
||||
|
||||
ONLINE_MONITOR_QUERY(ResourceTypeEnum.OPERATION, "online_monitor_query", "查询", "online_monitor"),
|
||||
|
||||
// ================== 分类管理模块 ==================
|
||||
CATEGORY_CONFIG(ResourceTypeEnum.MODULE, "category_config", "分类管理模块", "root"),
|
||||
|
||||
@@ -412,6 +417,16 @@ public enum ResourceEnum {
|
||||
CONTENT_SKILL_QUERY_DETAIL(ResourceTypeEnum.OPERATION, "content_skill_query_detail", "查询详情", "content_skill"),
|
||||
CONTENT_SKILL_DELETE(ResourceTypeEnum.OPERATION, "content_skill_delete", "删除", "content_skill"),
|
||||
|
||||
// 2级模块 - 数字员工同步记录
|
||||
CONTENT_DIGITAL_EMPLOYEE_SYNC(ResourceTypeEnum.MODULE, "content_digital_employee_sync", "数字员工同步记录", "content_manage"),
|
||||
|
||||
CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST(ResourceTypeEnum.OPERATION, "content_digital_employee_sync_query_list", "查询列表", "content_digital_employee_sync"),
|
||||
|
||||
// 2级模块 - 数字员工运营台
|
||||
CONTENT_DIGITAL_EMPLOYEE_OPS(ResourceTypeEnum.MODULE, "content_digital_employee_ops", "数字员工运营台", "content_manage"),
|
||||
|
||||
CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST(ResourceTypeEnum.OPERATION, "content_digital_employee_ops_query_list", "查询列表", "content_digital_employee_ops"),
|
||||
|
||||
// ================== 多语言管理模块 ==================
|
||||
I18N_LANG_MANAGE(ResourceTypeEnum.MODULE, "i18n_lang_manage", "多语言管理模块", "root"),
|
||||
I18N_LANG_QUERY(ResourceTypeEnum.OPERATION, "i18n_lang_query", "查询", "i18n_lang_manage"),
|
||||
|
||||
@@ -5,15 +5,22 @@ import com.xspaceagi.system.application.dto.SendNotifyMessageDto;
|
||||
import com.xspaceagi.system.application.dto.UserDto;
|
||||
import com.xspaceagi.system.application.dto.UserQueryDto;
|
||||
import com.xspaceagi.system.application.service.NotifyMessageApplicationService;
|
||||
import com.xspaceagi.system.application.service.SysGroupApplicationService;
|
||||
import com.xspaceagi.system.application.service.SysRoleApplicationService;
|
||||
import com.xspaceagi.system.application.service.UserApplicationService;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysGroup;
|
||||
import com.xspaceagi.system.infra.dao.entity.NotifyMessage;
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
import com.xspaceagi.system.infra.dao.mapper.UserMapper;
|
||||
import com.xspaceagi.system.infra.dao.service.UserService;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.dto.PageQueryVo;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.enums.GroupEnum;
|
||||
import com.xspaceagi.system.spec.enums.RoleEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.web.dto.UserAddDto;
|
||||
@@ -28,6 +35,7 @@ import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
|
||||
@@ -40,17 +48,35 @@ public class UserManageController {
|
||||
@Resource
|
||||
private UserApplicationService userApplicationService;
|
||||
|
||||
@Resource
|
||||
private SysGroupApplicationService sysGroupApplicationService;
|
||||
|
||||
@Resource
|
||||
private SysRoleApplicationService sysRoleApplicationService;
|
||||
|
||||
@Resource
|
||||
private NotifyMessageApplicationService notifyMessageApplicationService;
|
||||
|
||||
@Resource
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@RequireResource(USER_MANAGE_QUERY)
|
||||
@Operation(summary = "查询用户列表")
|
||||
@RequestMapping(path = "/list", method = RequestMethod.POST)
|
||||
public ReqResult<IPage<UserDto>> listQuery(@RequestBody PageQueryVo<UserQueryDto> pageQueryVo) {
|
||||
checkAdmin();
|
||||
if (pageQueryVo == null) {
|
||||
pageQueryVo = new PageQueryVo<>();
|
||||
}
|
||||
if (pageQueryVo.getPageNo() == null) {
|
||||
pageQueryVo.setPageNo(1L);
|
||||
}
|
||||
if (pageQueryVo.getPageSize() == null) {
|
||||
pageQueryVo.setPageSize(10L);
|
||||
}
|
||||
applyUserManageScope(pageQueryVo);
|
||||
return ReqResult.success(userApplicationService.listQuery(pageQueryVo));
|
||||
}
|
||||
|
||||
@@ -58,8 +84,6 @@ public class UserManageController {
|
||||
@Operation(summary = "获取用户统计")
|
||||
@RequestMapping(path = "/stats", method = RequestMethod.GET)
|
||||
public ReqResult<UserStatsDto> getUserStats() {
|
||||
checkAdmin();
|
||||
|
||||
Long totalUserCount = userMapper.countTotalUsers();
|
||||
Long todayNewUserCount = userMapper.countTodayNewUsers();
|
||||
|
||||
@@ -90,13 +114,17 @@ public class UserManageController {
|
||||
@Operation(summary = "添加用户")
|
||||
@RequestMapping(path = "/add", method = RequestMethod.POST)
|
||||
public ReqResult<Void> addUser(@RequestBody UserAddDto userAddDto) {
|
||||
checkAdmin();
|
||||
UserDto userDto = new UserDto();
|
||||
BeanUtils.copyProperties(userAddDto, userDto);
|
||||
if (StringUtils.isNotBlank(userAddDto.getPassword())) {
|
||||
userDto.setResetPass(1);
|
||||
}
|
||||
userDto.setRole(User.Role.User);
|
||||
userApplicationService.add(userDto);
|
||||
List<Long> groupIds = currentUserGroupIds().stream().toList();
|
||||
if (CollectionUtils.isNotEmpty(groupIds)) {
|
||||
groupIds.forEach(groupId -> sysGroupApplicationService.groupAddUser(groupId, userDto.getId(), getCurrentUserContext()));
|
||||
}
|
||||
return ReqResult.success();
|
||||
}
|
||||
|
||||
@@ -104,7 +132,7 @@ public class UserManageController {
|
||||
@Operation(summary = "更新用户信息")
|
||||
@RequestMapping(path = "/updateById/{id}", method = RequestMethod.POST)
|
||||
public ReqResult<Void> updateUserById(@PathVariable Long id, @RequestBody UserUpdateDto userUpdateDto) {
|
||||
checkAdmin();
|
||||
checkCurrentTenantUser(id);
|
||||
UserDto userUpdate = new UserDto();
|
||||
userUpdate.setId(id);
|
||||
userUpdate.setAvatar(userUpdateDto.getAvatar());
|
||||
@@ -122,7 +150,7 @@ public class UserManageController {
|
||||
@Operation(summary = "禁用用户")
|
||||
@RequestMapping(path = "/disable/{id}", method = RequestMethod.POST)
|
||||
public ReqResult<Void> disableUserById(@PathVariable Long id) {
|
||||
checkAdmin();
|
||||
checkCurrentTenantUser(id);
|
||||
UserDto userUpdate = new UserDto();
|
||||
userUpdate.setId(id);
|
||||
userUpdate.setStatus(User.Status.Disabled);
|
||||
@@ -134,7 +162,7 @@ public class UserManageController {
|
||||
@Operation(summary = "启用用户")
|
||||
@RequestMapping(path = "/enable/{id}", method = RequestMethod.POST)
|
||||
public ReqResult<Void> enableUserById(@PathVariable Long id) {
|
||||
checkAdmin();
|
||||
checkCurrentTenantUser(id);
|
||||
UserDto userUpdate = new UserDto();
|
||||
userUpdate.setId(id);
|
||||
userUpdate.setStatus(User.Status.Enabled);
|
||||
@@ -146,7 +174,7 @@ public class UserManageController {
|
||||
@Operation(summary = "删除用户")
|
||||
@RequestMapping(path = "/delete/{id}", method = RequestMethod.POST)
|
||||
public ReqResult<Void> deleteUserById(@PathVariable Long id) {
|
||||
checkAdmin();
|
||||
checkCurrentTenantUser(id);
|
||||
userApplicationService.logicDelete(id);
|
||||
return ReqResult.success();
|
||||
}
|
||||
@@ -165,12 +193,69 @@ public class UserManageController {
|
||||
return ReqResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前简单的在商家范围支持普通用户和管理员两种角色;后续再迭代完整的权限角色
|
||||
*/
|
||||
private void checkAdmin() {
|
||||
if (((UserDto) RequestContext.get().getUser()).getRole() != User.Role.Admin) {
|
||||
private void checkCurrentTenantUser(Long userId) {
|
||||
User user = userId == null ? null : userService.getById(userId);
|
||||
if (user == null || !isCurrentTenantUser(user) || (!isPlatformAdmin() && !visibleUserIds().contains(userId))) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyUserManageScope(PageQueryVo<UserQueryDto> pageQueryVo) {
|
||||
if (isPlatformAdmin()) {
|
||||
return;
|
||||
}
|
||||
if (pageQueryVo.getQueryFilter() == null) {
|
||||
pageQueryVo.setQueryFilter(new UserQueryDto());
|
||||
}
|
||||
pageQueryVo.getQueryFilter().setUserIds(visibleUserIds().stream().toList());
|
||||
}
|
||||
|
||||
private Set<Long> visibleUserIds() {
|
||||
return currentUserGroupIds().stream()
|
||||
.flatMap(groupId -> sysGroupApplicationService.getUserListByGroupId(groupId).stream())
|
||||
.map(User::getId)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Set<Long> currentUserGroupIds() {
|
||||
return sysGroupApplicationService.getGroupListByUserId(getCurrentUserDto().getId()).stream()
|
||||
.filter(group -> !GroupEnum.DEFAULT_GROUP.getCode().equals(group.getCode()))
|
||||
.map(SysGroup::getId)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private boolean isPlatformAdmin() {
|
||||
UserDto currentUser = getCurrentUserDto();
|
||||
return currentUser.getRole() == User.Role.Admin
|
||||
&& (currentUserGroupIds().isEmpty()
|
||||
|| sysRoleApplicationService.getRoleListByUserId(currentUser.getId()).stream()
|
||||
.anyMatch(role -> RoleEnum.SUPER_ADMIN.getCode().equals(role.getCode())));
|
||||
}
|
||||
|
||||
private boolean isCurrentTenantUser(User user) {
|
||||
return user != null && (getCurrentUserDto().getTenantId() == null || getCurrentUserDto().getTenantId().equals(user.getTenantId()));
|
||||
}
|
||||
|
||||
private UserDto getCurrentUserDto() {
|
||||
UserDto userDto = (UserDto) RequestContext.get().getUser();
|
||||
if (userDto == null) {
|
||||
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED, BizExceptionCodeEnum.systemUserNotLoggedInWeb);
|
||||
}
|
||||
return userDto;
|
||||
}
|
||||
|
||||
private UserContext getCurrentUserContext() {
|
||||
UserDto userDto = getCurrentUserDto();
|
||||
return UserContext.builder()
|
||||
.userId(userDto.getId())
|
||||
.userName(userDto.getUserName())
|
||||
.nickName(userDto.getNickName())
|
||||
.avatar(userDto.getAvatar())
|
||||
.email(userDto.getEmail())
|
||||
.phone(userDto.getPhone())
|
||||
.status(userDto.getStatus() == User.Status.Enabled ? 1 : -1)
|
||||
.tenantId(userDto.getTenantId())
|
||||
.roleType(userDto.getRole() == User.Role.Admin ? 1 : 2)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.xspaceagi.system.application.converter.SysDataPermissionConverter;
|
||||
import com.xspaceagi.system.application.dto.permission.*;
|
||||
import com.xspaceagi.system.application.service.SysDataPermissionApplicationService;
|
||||
import com.xspaceagi.system.application.service.SysGroupApplicationService;
|
||||
import com.xspaceagi.system.application.service.SysRoleApplicationService;
|
||||
import com.xspaceagi.system.application.service.SysSubjectPermissionApplicationService;
|
||||
import com.xspaceagi.system.application.dto.UserDto;
|
||||
import com.xspaceagi.system.domain.model.GroupBindMenuModel;
|
||||
@@ -52,6 +53,8 @@ public class SysGroupController extends BaseController {
|
||||
@Resource
|
||||
private SysGroupApplicationService sysGroupApplicationService;
|
||||
@Resource
|
||||
private SysRoleApplicationService sysRoleApplicationService;
|
||||
@Resource
|
||||
private SysDataPermissionApplicationService sysDataPermissionApplicationService;
|
||||
@Resource
|
||||
private SysSubjectPermissionApplicationService sysSubjectPermissionApplicationService;
|
||||
@@ -344,8 +347,8 @@ public class SysGroupController extends BaseController {
|
||||
return ReqResult.error("参数不能为空");
|
||||
}
|
||||
checkGroupManageScope(dto.getGroupId());
|
||||
// 用户组不允许绑定 生态市场/系统管理
|
||||
checkForbiddenMenuCodes(dto.getMenuTree());
|
||||
// 用户组不允许绑定生态市场和系统管理整棵子树。
|
||||
checkForbiddenMenuCodes(dto.getMenuTree(), false);
|
||||
|
||||
GroupBindMenuModel model = GroupBindMenuModelConverter.convertToModel(dto);
|
||||
sysGroupApplicationService.bindMenu(model, getUser());
|
||||
@@ -370,29 +373,34 @@ public class SysGroupController extends BaseController {
|
||||
return ReqResult.success(menuDtoList);
|
||||
}
|
||||
|
||||
private void checkForbiddenMenuCodes(List<MenuNodeDto> nodes) {
|
||||
private void checkForbiddenMenuCodes(List<MenuNodeDto> nodes, boolean underForbiddenMenu) {
|
||||
if (CollectionUtils.isEmpty(nodes)) {
|
||||
return;
|
||||
}
|
||||
for (MenuNodeDto node : nodes) {
|
||||
boolean currentForbidden = underForbiddenMenu;
|
||||
if (StringUtils.isNotBlank(node.getCode())) {
|
||||
if (node.getCode().equals(MenuEnum.ECO_MARKET.getCode()) && node.getMenuBindType() > 0) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupCannotBindForbiddenMenu,
|
||||
MenuEnum.ECO_MARKET.getName());
|
||||
}
|
||||
if (node.getCode().equals(MenuEnum.SYSTEM_MANAGE.getCode()) && node.getMenuBindType() > 0) {
|
||||
currentForbidden = currentForbidden || node.getCode().equals(MenuEnum.SYSTEM_MANAGE.getCode());
|
||||
if (currentForbidden && node.getMenuBindType() > 0) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupCannotBindForbiddenMenu,
|
||||
MenuEnum.SYSTEM_MANAGE.getName());
|
||||
}
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(node.getChildren())) {
|
||||
checkForbiddenMenuCodes(node.getChildren());
|
||||
checkForbiddenMenuCodes(node.getChildren(), currentForbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<SysGroup> filterGroupManageScope(List<SysGroup> groupList) {
|
||||
if (isPlatformAdmin() || CollectionUtils.isEmpty(groupList)) {
|
||||
if (CollectionUtils.isEmpty(groupList)) {
|
||||
return groupList;
|
||||
}
|
||||
if (isPlatformAdmin()) {
|
||||
return groupList;
|
||||
}
|
||||
Set<Long> allowedGroupIds = currentUserGroupIds();
|
||||
@@ -411,13 +419,18 @@ public class SysGroupController extends BaseController {
|
||||
}
|
||||
|
||||
private Set<Long> currentUserGroupIds() {
|
||||
return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream()
|
||||
return sysGroupApplicationService.getGroupListByUserId(getCurrentUserDto().getId()).stream()
|
||||
.filter(group -> !GroupEnum.DEFAULT_GROUP.getCode().equals(group.getCode()))
|
||||
.map(SysGroup::getId)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private boolean isPlatformAdmin() {
|
||||
return getCurrentUserDto().getRole() == User.Role.Admin;
|
||||
UserDto currentUser = getCurrentUserDto();
|
||||
return currentUser.getRole() == User.Role.Admin
|
||||
&& (currentUserGroupIds().isEmpty()
|
||||
|| sysRoleApplicationService.getRoleListByUserId(currentUser.getId()).stream()
|
||||
.anyMatch(role -> RoleEnum.SUPER_ADMIN.getCode().equals(role.getCode())));
|
||||
}
|
||||
|
||||
private UserDto getCurrentUserDto() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.xspaceagi.system.application.dto.permission.*;
|
||||
import com.xspaceagi.system.application.service.SysDataPermissionApplicationService;
|
||||
import com.xspaceagi.system.application.service.SysGroupApplicationService;
|
||||
import com.xspaceagi.system.application.service.SysRoleApplicationService;
|
||||
import com.xspaceagi.system.application.service.UserApplicationService;
|
||||
import com.xspaceagi.system.application.service.impl.SysUserPermissionCacheServiceImpl;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysGroup;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysRole;
|
||||
@@ -15,6 +16,8 @@ import com.xspaceagi.system.spec.annotation.SaasAdmin;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.enums.GroupEnum;
|
||||
import com.xspaceagi.system.spec.enums.RoleEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.utils.I18nUtil;
|
||||
@@ -28,6 +31,7 @@ import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -47,6 +51,8 @@ public class SysUserController extends BaseController {
|
||||
private SysDataPermissionApplicationService sysDataPermissionApplicationService;
|
||||
@Resource
|
||||
private SysUserPermissionCacheServiceImpl sysUserPermissionCacheService;
|
||||
@Resource
|
||||
private UserApplicationService userApplicationService;
|
||||
|
||||
@RequireResource(USER_MANAGE_BIND_ROLE)
|
||||
@Operation(summary = "查询用户绑定的角色列表")
|
||||
@@ -56,6 +62,7 @@ public class SysUserController extends BaseController {
|
||||
return ReqResult.error("参数不能为空");
|
||||
}
|
||||
checkUserManageScope(userId);
|
||||
checkRoleAuthorizationTarget(userId);
|
||||
|
||||
List<SysRole> roleList = sysRoleApplicationService.getRoleListByUserId(userId);
|
||||
if (CollectionUtils.isEmpty(roleList)) {
|
||||
@@ -79,6 +86,7 @@ public class SysUserController extends BaseController {
|
||||
return ReqResult.error("参数不能为空");
|
||||
}
|
||||
checkUserManageScope(dto.getUserId());
|
||||
checkRoleAuthorizationTarget(dto.getUserId());
|
||||
checkRoleBindScope(dto.getRoleIds());
|
||||
sysRoleApplicationService.userBindRole(dto.getUserId(), dto.getRoleIds(), getUser());
|
||||
return ReqResult.success();
|
||||
@@ -146,11 +154,16 @@ public class SysUserController extends BaseController {
|
||||
}
|
||||
|
||||
private void checkUserManageScope(Long userId) {
|
||||
UserDto targetUser = userApplicationService.queryById(userId);
|
||||
if (targetUser == null || !Objects.equals(targetUser.getTenantId(), RequestContext.get().getTenantId())) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
|
||||
}
|
||||
if (isPlatformAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Long> allowedGroupIds = currentUserGroupIds();
|
||||
boolean inScope = sysGroupApplicationService.getEffectiveGroupListByUserId(userId).stream()
|
||||
boolean inScope = sysGroupApplicationService.getGroupListByUserId(userId).stream()
|
||||
.map(SysGroup::getId)
|
||||
.anyMatch(allowedGroupIds::contains);
|
||||
if (!inScope) {
|
||||
@@ -159,7 +172,10 @@ public class SysUserController extends BaseController {
|
||||
}
|
||||
|
||||
private void checkGroupBindScope(List<Long> groupIds) {
|
||||
if (isPlatformAdmin() || CollectionUtils.isEmpty(groupIds)) {
|
||||
if (CollectionUtils.isEmpty(groupIds)) {
|
||||
return;
|
||||
}
|
||||
if (isPlatformAdmin()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> allowedGroupIds = currentUserGroupIds();
|
||||
@@ -170,7 +186,10 @@ public class SysUserController extends BaseController {
|
||||
}
|
||||
|
||||
private void checkRoleBindScope(List<Long> roleIds) {
|
||||
if (isPlatformAdmin() || CollectionUtils.isEmpty(roleIds)) {
|
||||
if (CollectionUtils.isEmpty(roleIds)) {
|
||||
return;
|
||||
}
|
||||
if (isPlatformAdmin()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> allowedRoleIds = sysRoleApplicationService.getRoleListByUserId(getCurrentUserDto().getId()).stream()
|
||||
@@ -182,14 +201,26 @@ public class SysUserController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRoleAuthorizationTarget(Long userId) {
|
||||
UserDto targetUser = userApplicationService.queryById(userId);
|
||||
if (targetUser == null || targetUser.getRole() != User.Role.Admin) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Long> currentUserGroupIds() {
|
||||
return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream()
|
||||
return sysGroupApplicationService.getGroupListByUserId(getCurrentUserDto().getId()).stream()
|
||||
.filter(group -> !GroupEnum.DEFAULT_GROUP.getCode().equals(group.getCode()))
|
||||
.map(SysGroup::getId)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private boolean isPlatformAdmin() {
|
||||
return getCurrentUserDto().getRole() == User.Role.Admin;
|
||||
UserDto currentUser = getCurrentUserDto();
|
||||
return currentUser.getRole() == User.Role.Admin
|
||||
&& (currentUserGroupIds().isEmpty()
|
||||
|| sysRoleApplicationService.getRoleListByUserId(currentUser.getId()).stream()
|
||||
.anyMatch(role -> RoleEnum.SUPER_ADMIN.getCode().equals(role.getCode())));
|
||||
}
|
||||
|
||||
private UserDto getCurrentUserDto() {
|
||||
|
||||
74
qiming-backend/sql/update-20260602.sql
Normal file
74
qiming-backend/sql/update-20260602.sql
Normal file
@@ -0,0 +1,74 @@
|
||||
-- Add system online monitor menu and resource.
|
||||
-- Idempotent for existing tenants: creates menu/resource rows, binds menu to resource,
|
||||
-- and grants super_admin default access. User-group admins can be granted this menu
|
||||
-- from the database menu permission management UI.
|
||||
|
||||
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT 0, 'online_monitor', '在线监控模块', '在线监控模块', 1, 1, 17, 1, t._tenant_id, 0, 'system', 1
|
||||
FROM (SELECT DISTINCT `_tenant_id` FROM sys_resource) t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_resource r
|
||||
WHERE r.`_tenant_id` = t.`_tenant_id` AND r.`code` = 'online_monitor' AND r.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'online_monitor_query', '查询', '查询在线监控', 1, 2, 1, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_resource parent
|
||||
WHERE parent.`code` = 'online_monitor' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_resource r
|
||||
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'online_monitor_query' AND r.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (`parent_id`, `code`, `name`, `description`, `source`, `path`, `open_type`, `icon`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'online_monitor', '在线监控', '在线监控', 1, '/system/online-monitor', 1, '', 11, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_menu parent
|
||||
WHERE parent.`code` = 'system_manage' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu m
|
||||
WHERE m.`_tenant_id` = parent.`_tenant_id` AND m.`code` = 'online_monitor' AND m.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu_resource (`menu_id`, `resource_id`, `resource_bind_type`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT m.id, r.id, 1, m.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_menu m
|
||||
JOIN sys_resource r ON r.`_tenant_id` = m.`_tenant_id` AND r.`code` = 'online_monitor' AND r.`yn` = 1
|
||||
WHERE m.`code` = 'online_monitor' AND m.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu_resource mr
|
||||
WHERE mr.`_tenant_id` = m.`_tenant_id` AND mr.`menu_id` = m.id AND mr.`resource_id` = r.id AND mr.`yn` = 1
|
||||
);
|
||||
|
||||
UPDATE sys_menu_resource mr
|
||||
JOIN sys_menu m ON m.id = mr.menu_id AND m.`_tenant_id` = mr.`_tenant_id` AND m.`code` = 'online_monitor' AND m.`yn` = 1
|
||||
JOIN sys_resource r ON r.id = mr.resource_id AND r.`_tenant_id` = mr.`_tenant_id` AND r.`code` = 'online_monitor' AND r.`yn` = 1
|
||||
SET mr.`resource_bind_type` = 1,
|
||||
mr.`modified` = CURRENT_TIMESTAMP
|
||||
WHERE mr.`yn` = 1;
|
||||
|
||||
INSERT INTO sys_role_menu (`role_id`, `menu_id`, `menu_bind_type`, `resource_tree_json`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT role.id,
|
||||
menu.id,
|
||||
1,
|
||||
JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
|
||||
menu.`_tenant_id`,
|
||||
0,
|
||||
'system',
|
||||
1
|
||||
FROM sys_role role
|
||||
JOIN sys_menu menu ON menu.`_tenant_id` = role.`_tenant_id` AND menu.`code` = 'online_monitor' AND menu.`yn` = 1
|
||||
JOIN sys_resource resource ON resource.`_tenant_id` = role.`_tenant_id` AND resource.`code` = 'online_monitor' AND resource.`yn` = 1
|
||||
WHERE role.`code` = 'super_admin' AND role.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.`_tenant_id` = role.`_tenant_id` AND rm.`role_id` = role.id AND rm.`menu_id` = menu.id AND rm.`yn` = 1
|
||||
);
|
||||
|
||||
UPDATE sys_role_menu rm
|
||||
JOIN sys_role role ON role.id = rm.role_id AND role.`_tenant_id` = rm.`_tenant_id` AND role.`code` = 'super_admin' AND role.`yn` = 1
|
||||
JOIN sys_menu menu ON menu.id = rm.menu_id AND menu.`_tenant_id` = rm.`_tenant_id` AND menu.`code` = 'online_monitor' AND menu.`yn` = 1
|
||||
JOIN sys_resource resource ON resource.`_tenant_id` = rm.`_tenant_id` AND resource.`code` = 'online_monitor' AND resource.`yn` = 1
|
||||
SET rm.`menu_bind_type` = 1,
|
||||
rm.`resource_tree_json` = JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
|
||||
rm.`modified` = CURRENT_TIMESTAMP
|
||||
WHERE rm.`yn` = 1;
|
||||
24
qiming-backend/sql/update-20260605.sql
Normal file
24
qiming-backend/sql/update-20260605.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE `digital_employee_sync_record`
|
||||
(
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`_tenant_id` BIGINT DEFAULT NULL COMMENT '租户ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '用户ID',
|
||||
`client_key` VARCHAR(128) NOT NULL COMMENT '客户端配置Key',
|
||||
`device_id` VARCHAR(128) DEFAULT NULL COMMENT '设备ID',
|
||||
`outbox_id` VARCHAR(255) NOT NULL COMMENT '客户端outbox ID',
|
||||
`entity_type` VARCHAR(32) NOT NULL COMMENT '实体类型:plan/task/run/event/artifact/approval',
|
||||
`entity_id` VARCHAR(255) NOT NULL COMMENT '客户端实体ID',
|
||||
`operation` VARCHAR(32) NOT NULL COMMENT '操作类型',
|
||||
`payload` JSON DEFAULT NULL COMMENT '实体快照',
|
||||
`synced_at` DATETIME NOT NULL COMMENT '最近同步时间',
|
||||
`modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_client_outbox` (`client_key`, `outbox_id`),
|
||||
KEY `idx_client_entity` (`client_key`, `entity_type`, `entity_id`),
|
||||
KEY `idx_tenant_client_entity_time` (`_tenant_id`, `client_key`, `entity_type`, `entity_id`, `synced_at`),
|
||||
KEY `idx_tenant_user_time` (`_tenant_id`, `user_id`, `synced_at`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci
|
||||
COMMENT = '数字员工客户端同步记录';
|
||||
75
qiming-backend/sql/update-20260606.sql
Normal file
75
qiming-backend/sql/update-20260606.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- Add digital employee sync records menu and resource.
|
||||
-- Idempotent for existing tenants: creates menu/resource rows, binds menu to resource,
|
||||
-- and grants super_admin default access. Other roles can be granted this menu
|
||||
-- from the menu permission management UI.
|
||||
|
||||
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_sync', '数字员工同步记录', '数字员工同步记录', 1, 1, 90, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_resource parent
|
||||
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_resource r
|
||||
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_sync' AND r.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_sync_query_list', '查询列表', '查询数字员工同步记录', 1, 2, 1, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_resource parent
|
||||
WHERE parent.`code` = 'content_digital_employee_sync' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_resource r
|
||||
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_sync_query_list' AND r.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (`parent_id`, `code`, `name`, `description`, `source`, `path`, `open_type`, `icon`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_sync', '数字员工同步记录', '数字员工同步记录', 1, '/system/content/content-digital-employee-sync', 1, '', 90, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_menu parent
|
||||
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu m
|
||||
WHERE m.`_tenant_id` = parent.`_tenant_id` AND m.`code` = 'content_digital_employee_sync' AND m.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu_resource (`menu_id`, `resource_id`, `resource_bind_type`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT m.id, r.id, 1, m.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_menu m
|
||||
JOIN sys_resource r ON r.`_tenant_id` = m.`_tenant_id` AND r.`code` = 'content_digital_employee_sync' AND r.`yn` = 1
|
||||
WHERE m.`code` = 'content_digital_employee_sync' AND m.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu_resource mr
|
||||
WHERE mr.`_tenant_id` = m.`_tenant_id` AND mr.`menu_id` = m.id AND mr.`resource_id` = r.id AND mr.`yn` = 1
|
||||
);
|
||||
|
||||
UPDATE sys_menu_resource mr
|
||||
JOIN sys_menu m ON m.id = mr.menu_id AND m.`_tenant_id` = mr.`_tenant_id` AND m.`code` = 'content_digital_employee_sync' AND m.`yn` = 1
|
||||
JOIN sys_resource r ON r.id = mr.resource_id AND r.`_tenant_id` = mr.`_tenant_id` AND r.`code` = 'content_digital_employee_sync' AND r.`yn` = 1
|
||||
SET mr.`resource_bind_type` = 1,
|
||||
mr.`modified` = CURRENT_TIMESTAMP
|
||||
WHERE mr.`yn` = 1;
|
||||
|
||||
INSERT INTO sys_role_menu (`role_id`, `menu_id`, `menu_bind_type`, `resource_tree_json`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT role.id,
|
||||
menu.id,
|
||||
1,
|
||||
JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
|
||||
menu.`_tenant_id`,
|
||||
0,
|
||||
'system',
|
||||
1
|
||||
FROM sys_role role
|
||||
JOIN sys_menu menu ON menu.`_tenant_id` = role.`_tenant_id` AND menu.`code` = 'content_digital_employee_sync' AND menu.`yn` = 1
|
||||
JOIN sys_resource resource ON resource.`_tenant_id` = role.`_tenant_id` AND resource.`code` = 'content_digital_employee_sync' AND resource.`yn` = 1
|
||||
WHERE role.`code` = 'super_admin' AND role.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.`_tenant_id` = role.`_tenant_id` AND rm.`role_id` = role.id AND rm.`menu_id` = menu.id AND rm.`yn` = 1
|
||||
);
|
||||
|
||||
UPDATE sys_role_menu rm
|
||||
JOIN sys_role role ON role.id = rm.role_id AND role.`_tenant_id` = rm.`_tenant_id` AND role.`code` = 'super_admin' AND role.`yn` = 1
|
||||
JOIN sys_menu menu ON menu.id = rm.menu_id AND menu.`_tenant_id` = rm.`_tenant_id` AND menu.`code` = 'content_digital_employee_sync' AND menu.`yn` = 1
|
||||
JOIN sys_resource resource ON resource.`_tenant_id` = rm.`_tenant_id` AND resource.`code` = 'content_digital_employee_sync' AND resource.`yn` = 1
|
||||
SET rm.`menu_bind_type` = 1,
|
||||
rm.`resource_tree_json` = JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
|
||||
rm.`modified` = CURRENT_TIMESTAMP
|
||||
WHERE rm.`yn` = 1;
|
||||
193
qiming-backend/sql/update-20260612.sql
Normal file
193
qiming-backend/sql/update-20260612.sql
Normal file
@@ -0,0 +1,193 @@
|
||||
ALTER TABLE `digital_employee_sync_record`
|
||||
ADD COLUMN `contract_version` VARCHAR(128) DEFAULT NULL COMMENT '客户端同步契约版本' AFTER `operation`,
|
||||
ADD COLUMN `entity_summary` JSON DEFAULT NULL COMMENT '客户端实体摘要' AFTER `contract_version`,
|
||||
ADD COLUMN `business_view` JSON DEFAULT NULL COMMENT '客户端业务视图投影' AFTER `entity_summary`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `digital_employee_admin_action` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`_tenant_id` BIGINT DEFAULT NULL COMMENT '租户 ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '创建用户 ID',
|
||||
`client_key` VARCHAR(128) DEFAULT NULL COMMENT '客户端 Key',
|
||||
`device_id` VARCHAR(128) DEFAULT NULL COMMENT '设备 ID',
|
||||
`entity_type` VARCHAR(64) DEFAULT NULL COMMENT '实体类型',
|
||||
`entity_id` VARCHAR(128) DEFAULT NULL COMMENT '实体 ID',
|
||||
`action` VARCHAR(64) DEFAULT NULL COMMENT '管理端动作',
|
||||
`status` VARCHAR(32) DEFAULT NULL COMMENT '状态 pending/applied/rejected/failed',
|
||||
`reason_codes` JSON DEFAULT NULL COMMENT '原因码',
|
||||
`request_payload` JSON DEFAULT NULL COMMENT '请求附加参数',
|
||||
`processed_at` DATETIME DEFAULT NULL COMMENT '客户端处理时间',
|
||||
`modified` DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
`created` DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_de_admin_action_pending` (`_tenant_id`, `client_key`, `device_id`, `status`, `created`),
|
||||
KEY `idx_de_admin_action_entity` (`_tenant_id`, `entity_type`, `entity_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工管理端远程动作意图';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `digital_employee_business_fact` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`_tenant_id` BIGINT NOT NULL COMMENT '租户ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '用户ID',
|
||||
`sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID',
|
||||
`client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key',
|
||||
`device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID',
|
||||
`outbox_id` VARCHAR(255) DEFAULT NULL COMMENT '客户端outbox ID',
|
||||
`entity_type` VARCHAR(64) NOT NULL COMMENT '实体类型',
|
||||
`entity_id` VARCHAR(255) NOT NULL COMMENT '实体ID',
|
||||
`operation` VARCHAR(64) DEFAULT NULL COMMENT '操作',
|
||||
`title` VARCHAR(512) DEFAULT NULL COMMENT '标题',
|
||||
`status` VARCHAR(128) DEFAULT NULL COMMENT '业务状态',
|
||||
`summary` VARCHAR(1024) DEFAULT NULL COMMENT '摘要',
|
||||
`plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID',
|
||||
`task_id` VARCHAR(255) DEFAULT NULL COMMENT '任务ID',
|
||||
`run_id` VARCHAR(255) DEFAULT NULL COMMENT '运行ID',
|
||||
`kind` VARCHAR(255) DEFAULT NULL COMMENT '类型',
|
||||
`category` VARCHAR(128) DEFAULT NULL COMMENT '分类',
|
||||
`occurred_at` VARCHAR(64) DEFAULT NULL COMMENT '发生时间',
|
||||
`started_at` VARCHAR(64) DEFAULT NULL COMMENT '开始时间',
|
||||
`finished_at` VARCHAR(64) DEFAULT NULL COMMENT '结束时间',
|
||||
`duration_ms` BIGINT DEFAULT NULL COMMENT '耗时毫秒',
|
||||
`sync_status` VARCHAR(128) DEFAULT NULL COMMENT '同步状态',
|
||||
`business_view` JSON DEFAULT NULL COMMENT '业务视图快照',
|
||||
`payload` JSON DEFAULT NULL COMMENT '安全payload快照',
|
||||
`synced_at` DATETIME DEFAULT NULL COMMENT '同步时间',
|
||||
`modified` DATETIME NOT NULL COMMENT '修改时间',
|
||||
`created` DATETIME NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_de_business_fact_record` (`_tenant_id`, `sync_record_id`),
|
||||
KEY `idx_de_business_fact_entity` (`_tenant_id`, `entity_type`, `entity_id`),
|
||||
KEY `idx_de_business_fact_plan` (`_tenant_id`, `plan_id`, `entity_type`),
|
||||
KEY `idx_de_business_fact_client` (`_tenant_id`, `client_key`, `device_id`, `synced_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工业务事实物化表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `digital_employee_artifact_lifecycle` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`_tenant_id` BIGINT NOT NULL COMMENT '租户ID',
|
||||
`sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID',
|
||||
`client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key',
|
||||
`device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID',
|
||||
`artifact_id` VARCHAR(255) NOT NULL COMMENT '产物ID',
|
||||
`plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID',
|
||||
`task_id` VARCHAR(255) DEFAULT NULL COMMENT '任务ID',
|
||||
`run_id` VARCHAR(255) DEFAULT NULL COMMENT '运行ID',
|
||||
`status` VARCHAR(128) DEFAULT NULL COMMENT '状态',
|
||||
`retention_status` VARCHAR(128) DEFAULT NULL COMMENT '保留状态',
|
||||
`cleanup_status` VARCHAR(128) DEFAULT NULL COMMENT '清理状态',
|
||||
`deleted_at` VARCHAR(64) DEFAULT NULL COMMENT '删除时间',
|
||||
`version_key` VARCHAR(255) DEFAULT NULL COMMENT '版本Key',
|
||||
`source` VARCHAR(255) DEFAULT NULL COMMENT '来源',
|
||||
`business_view` JSON DEFAULT NULL COMMENT '业务视图快照',
|
||||
`synced_at` DATETIME DEFAULT NULL COMMENT '同步时间',
|
||||
`modified` DATETIME NOT NULL COMMENT '修改时间',
|
||||
`created` DATETIME NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_de_artifact_lifecycle_record` (`_tenant_id`, `sync_record_id`),
|
||||
KEY `idx_de_artifact_lifecycle_artifact` (`_tenant_id`, `artifact_id`),
|
||||
KEY `idx_de_artifact_lifecycle_plan` (`_tenant_id`, `plan_id`, `task_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工产物生命周期物化表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `digital_employee_daily_report_fact` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`_tenant_id` BIGINT NOT NULL COMMENT '租户ID',
|
||||
`sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID',
|
||||
`client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key',
|
||||
`device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID',
|
||||
`report_id` VARCHAR(255) NOT NULL COMMENT '日报ID',
|
||||
`daily_report_fact_id` VARCHAR(255) DEFAULT NULL COMMENT '日报事实ID',
|
||||
`plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID',
|
||||
`status` VARCHAR(128) DEFAULT NULL COMMENT '状态',
|
||||
`physical_fact_status` VARCHAR(128) DEFAULT NULL COMMENT '物理事实状态',
|
||||
`delivery_status` VARCHAR(128) DEFAULT NULL COMMENT '交付状态',
|
||||
`approval_status` VARCHAR(128) DEFAULT NULL COMMENT '审批状态',
|
||||
`approval_decision_status` VARCHAR(128) DEFAULT NULL COMMENT '审批裁决状态',
|
||||
`confirmed_at` VARCHAR(64) DEFAULT NULL COMMENT '确认时间',
|
||||
`report_date` VARCHAR(64) DEFAULT NULL COMMENT '日报日期',
|
||||
`send_record_id` VARCHAR(255) DEFAULT NULL COMMENT '发送记录ID',
|
||||
`send_channel` VARCHAR(128) DEFAULT NULL COMMENT '发送渠道',
|
||||
`send_request_status` VARCHAR(128) DEFAULT NULL COMMENT '发送请求状态',
|
||||
`send_record_status` VARCHAR(128) DEFAULT NULL COMMENT '发送记录状态',
|
||||
`materialized_source` VARCHAR(128) DEFAULT NULL COMMENT '物化来源',
|
||||
`business_view` JSON DEFAULT NULL COMMENT '业务视图快照',
|
||||
`synced_at` DATETIME DEFAULT NULL COMMENT '同步时间',
|
||||
`modified` DATETIME NOT NULL COMMENT '修改时间',
|
||||
`created` DATETIME NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_de_daily_report_fact_record` (`_tenant_id`, `sync_record_id`),
|
||||
KEY `idx_de_daily_report_fact_report` (`_tenant_id`, `report_id`),
|
||||
KEY `idx_de_daily_report_fact_client` (`_tenant_id`, `client_key`, `device_id`, `synced_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工日报事实物化表';
|
||||
|
||||
-- Add digital employee admin operations workbench menu and resource.
|
||||
-- Idempotent for existing tenants: creates menu/resource rows, binds menu to resource,
|
||||
-- and grants super_admin default access. Other roles can be granted this menu
|
||||
-- from the menu permission management UI.
|
||||
|
||||
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_ops', '数字员工运营台', '数字员工运营台', 1, 1, 91, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_resource parent
|
||||
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_resource r
|
||||
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_ops_query_list', '查询列表', '查询数字员工运营台', 1, 2, 1, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_resource parent
|
||||
WHERE parent.`code` = 'content_digital_employee_ops' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_resource r
|
||||
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_ops_query_list' AND r.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (`parent_id`, `code`, `name`, `description`, `source`, `path`, `open_type`, `icon`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_ops', '数字员工运营台', '数字员工运营台', 1, '/system/content/content-digital-employee-ops', 1, '', 91, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_menu parent
|
||||
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu m
|
||||
WHERE m.`_tenant_id` = parent.`_tenant_id` AND m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu_resource (`menu_id`, `resource_id`, `resource_bind_type`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT m.id, r.id, 1, m.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_menu m
|
||||
JOIN sys_resource r ON r.`_tenant_id` = m.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
|
||||
WHERE m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu_resource mr
|
||||
WHERE mr.`_tenant_id` = m.`_tenant_id` AND mr.`menu_id` = m.id AND mr.`resource_id` = r.id AND mr.`yn` = 1
|
||||
);
|
||||
|
||||
UPDATE sys_menu_resource mr
|
||||
JOIN sys_menu m ON m.id = mr.menu_id AND m.`_tenant_id` = mr.`_tenant_id` AND m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
|
||||
JOIN sys_resource r ON r.id = mr.resource_id AND r.`_tenant_id` = mr.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
|
||||
SET mr.`resource_bind_type` = 1,
|
||||
mr.`modified` = CURRENT_TIMESTAMP
|
||||
WHERE mr.`yn` = 1;
|
||||
|
||||
INSERT INTO sys_role_menu (`role_id`, `menu_id`, `menu_bind_type`, `resource_tree_json`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT role.id,
|
||||
menu.id,
|
||||
1,
|
||||
JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
|
||||
menu.`_tenant_id`,
|
||||
0,
|
||||
'system',
|
||||
1
|
||||
FROM sys_role role
|
||||
JOIN sys_menu menu ON menu.`_tenant_id` = role.`_tenant_id` AND menu.`code` = 'content_digital_employee_ops' AND menu.`yn` = 1
|
||||
JOIN sys_resource resource ON resource.`_tenant_id` = role.`_tenant_id` AND resource.`code` = 'content_digital_employee_ops' AND resource.`yn` = 1
|
||||
WHERE role.`code` = 'super_admin' AND role.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.`_tenant_id` = role.`_tenant_id` AND rm.`role_id` = role.id AND rm.`menu_id` = menu.id AND rm.`yn` = 1
|
||||
);
|
||||
|
||||
UPDATE sys_role_menu rm
|
||||
JOIN sys_role role ON role.id = rm.role_id AND role.`_tenant_id` = rm.`_tenant_id` AND role.`code` = 'super_admin' AND role.`yn` = 1
|
||||
JOIN sys_menu menu ON menu.id = rm.menu_id AND menu.`_tenant_id` = rm.`_tenant_id` AND menu.`code` = 'content_digital_employee_ops' AND menu.`yn` = 1
|
||||
JOIN sys_resource resource ON resource.`_tenant_id` = rm.`_tenant_id` AND resource.`code` = 'content_digital_employee_ops' AND resource.`yn` = 1
|
||||
SET rm.`menu_bind_type` = 1,
|
||||
rm.`resource_tree_json` = JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
|
||||
rm.`modified` = CURRENT_TIMESTAMP
|
||||
WHERE rm.`yn` = 1;
|
||||
276
qiming-file-server/src/utils/build/buildProjectUtils.js
Normal file
276
qiming-file-server/src/utils/build/buildProjectUtils.js
Normal file
@@ -0,0 +1,276 @@
|
||||
import { exec } from "child_process";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { log, logBuild } from "../log/logUtils.js";
|
||||
import BuildErrorParser from "../error/buildErrorParser.js";
|
||||
import config from "../../appConfig/index.js";
|
||||
import {
|
||||
BusinessError,
|
||||
SystemError,
|
||||
FileError,
|
||||
ResourceError,
|
||||
} from "../error/errorHandler.js";
|
||||
import { installDependencies } from "../buildDependency/dependencyManager.js";
|
||||
import {
|
||||
extractIsolationContext,
|
||||
resolveProjectPath,
|
||||
} from "../common/projectPathUtils.js";
|
||||
|
||||
const distTargetDir = config.DIST_TARGET_DIR;
|
||||
|
||||
// 构建并发控制(无队列)
|
||||
const buildingProjects = new Set(); // 正在构建中的项目
|
||||
let currentBuilds = 0; // 当前并行构建数
|
||||
|
||||
// 将dist目录拷贝到指定目录
|
||||
async function copyBuildOutputToTarget({
|
||||
req,
|
||||
projectPath,
|
||||
projectId,
|
||||
outStream,
|
||||
}) {
|
||||
try {
|
||||
const sourceDir = path.join(projectPath, "dist");
|
||||
const targetBase = distTargetDir;
|
||||
const targetDir = path.join(targetBase, projectId, "dist");
|
||||
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
const msg = `dist directory not found: ${sourceDir}`;
|
||||
log(projectId, "WARN", msg, { projectId });
|
||||
outStream && outStream.write(`${msg}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保目标父目录存在
|
||||
if (!fs.existsSync(targetBase)) {
|
||||
fs.mkdirSync(targetBase, { recursive: true });
|
||||
}
|
||||
|
||||
// 先清空旧目录
|
||||
if (fs.existsSync(targetDir)) {
|
||||
await fs.promises.rm(targetDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// 复制 dist -> 目标目录
|
||||
if (fs.promises.cp) {
|
||||
await fs.promises.cp(sourceDir, targetDir, { recursive: true });
|
||||
} else {
|
||||
// Node 版本不支持 fs.promises.cp 时的降级方案
|
||||
const copyRecursiveSync = (src, dest) => {
|
||||
const stat = fs.statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
}
|
||||
for (const entry of fs.readdirSync(src)) {
|
||||
copyRecursiveSync(path.join(src, entry), path.join(dest, entry));
|
||||
}
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
};
|
||||
copyRecursiveSync(sourceDir, targetDir);
|
||||
}
|
||||
|
||||
const okMsg = `dist directory copied to: ${targetDir}`;
|
||||
log(projectId, "INFO", okMsg, { projectId });
|
||||
outStream && outStream.write(`${okMsg}\n`);
|
||||
} catch (err) {
|
||||
const errMsg = `Failed to copy dist directory: ${err.message}`;
|
||||
log(projectId, "ERROR", errMsg, { projectId });
|
||||
outStream && outStream.write(`${errMsg}\n`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
//执行build脚本
|
||||
function runBuildScript(projectId, projectPath, scriptName, extraArgs = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const argsPart =
|
||||
Array.isArray(extraArgs) && extraArgs.length > 0
|
||||
? " -- " + extraArgs.map((s) => String(s)).join(" ")
|
||||
: "";
|
||||
const command = `cd ${projectPath} && pnpm run ${scriptName}${argsPart}`;
|
||||
logBuild(projectId, "INFO", "Execute command", { command });
|
||||
// 同步输出到普通日志也打印一次,便于从统一日志流检索
|
||||
try {
|
||||
log(projectId, "INFO", "Execute build script", { command, cwd: projectPath });
|
||||
} catch (_) {}
|
||||
|
||||
exec(
|
||||
command,
|
||||
{
|
||||
env: process.env, // 继承父进程的环境变量,包括 pnpm 配置
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB 缓冲区
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logBuild(projectId, "ERROR", "Execution error", {
|
||||
error: error.message,
|
||||
stderr,
|
||||
});
|
||||
|
||||
// 使用错误解析器提供用户友好的错误信息
|
||||
const errorParser = new BuildErrorParser();
|
||||
const errorMessage = stderr || error.message;
|
||||
const userFriendlyMessage = errorParser.parseBuildError(
|
||||
errorMessage,
|
||||
projectId
|
||||
);
|
||||
|
||||
// 创建包含用户友好信息的构建错误
|
||||
const buildError = new SystemError(userFriendlyMessage, {
|
||||
originalError: error.message,
|
||||
command: command,
|
||||
});
|
||||
|
||||
return reject(buildError);
|
||||
}
|
||||
logBuild(projectId, "INFO", "Script execution completed", { stdout });
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建项目
|
||||
* @param {Object} req 请求对象
|
||||
* @param {string} projectId 项目ID
|
||||
* @returns {Promise<Object>} 构建结果
|
||||
*/
|
||||
async function buildProject(req, projectId) {
|
||||
const isolationContext = extractIsolationContext(req?.query || {});
|
||||
const projectPath = resolveProjectPath(projectId, isolationContext);
|
||||
const jsonFilePath = path.join(projectPath, "package.json");
|
||||
|
||||
const exists = fs.existsSync(jsonFilePath);
|
||||
if (!exists) {
|
||||
throw new ResourceError("Project missing package.json file", {
|
||||
projectId,
|
||||
projectPath,
|
||||
});
|
||||
}
|
||||
|
||||
let jsonContent;
|
||||
try {
|
||||
jsonContent = JSON.parse(fs.readFileSync(jsonFilePath, "utf8"));
|
||||
} catch (error) {
|
||||
throw new FileError("package.json file format error", {
|
||||
projectId,
|
||||
jsonFilePath,
|
||||
originalError: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const jsonScripts = jsonContent.scripts;
|
||||
const buildScript = jsonScripts.build;
|
||||
if (!buildScript) {
|
||||
log(projectId, "WARN", "Project missing build script", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
throw new BusinessError("Project missing build script", { projectId });
|
||||
}
|
||||
|
||||
// 项目级互斥:同一项目仅允许一个构建
|
||||
if (buildingProjects.has(projectId)) {
|
||||
throw new BusinessError("This project is being built", { projectId });
|
||||
}
|
||||
|
||||
// 全局并发限制
|
||||
const max = Number.isFinite(config.MAX_BUILD_CONCURRENCY)
|
||||
? config.MAX_BUILD_CONCURRENCY
|
||||
: 20;
|
||||
if (currentBuilds >= max) {
|
||||
throw new BusinessError("Concurrency is full, please try again later", {
|
||||
currentBuilds,
|
||||
maxConcurrency: max,
|
||||
});
|
||||
}
|
||||
|
||||
// 直接执行同步构建(空闲)
|
||||
buildingProjects.add(projectId);
|
||||
currentBuilds += 1;
|
||||
try {
|
||||
// 读取并规范化 basePath(仅对 Vite 有效)
|
||||
let basePath = "";
|
||||
if (req && req.query && typeof req.query.basePath === "string") {
|
||||
basePath = req.query.basePath;
|
||||
}
|
||||
if (basePath) {
|
||||
if (!basePath.startsWith("/")) basePath = "/" + basePath;
|
||||
if (!basePath.endsWith("/")) basePath = basePath + "/";
|
||||
}
|
||||
|
||||
const buildExtraArgs = [];
|
||||
// 若脚本包含 vite,则传入 --base
|
||||
if (
|
||||
typeof buildScript === "string" &&
|
||||
buildScript.includes("vite") &&
|
||||
basePath
|
||||
) {
|
||||
buildExtraArgs.push("--base", basePath);
|
||||
}
|
||||
|
||||
// 若使用 Vite 构建,追加 --debug 以输出调试信息
|
||||
if (typeof buildScript === "string" && buildScript.includes("vite")) {
|
||||
buildExtraArgs.push("--debug");
|
||||
}
|
||||
|
||||
// 安装依赖
|
||||
log(projectId, "INFO", "Start installing dependencies", { projectId });
|
||||
await installDependencies(req, projectId, projectPath);
|
||||
|
||||
// 执行构建:Vite 直接使用 pnpm exec,避免 npm-run 参数分隔影响
|
||||
if (typeof buildScript === "string" && buildScript.includes("vite")) {
|
||||
const viteArgs = ["exec", "vite", "build", ...buildExtraArgs, "--debug"];
|
||||
const command = `cd ${projectPath} && pnpm ${viteArgs.join(" ")}`;
|
||||
logBuild(projectId, "INFO", "Execute command(direct vite)", { command });
|
||||
try {
|
||||
log(projectId, "INFO", "Execute build script(direct vite)", {
|
||||
command,
|
||||
cwd: projectPath,
|
||||
});
|
||||
} catch (_) {}
|
||||
await new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logBuild(projectId, "ERROR", "Execution error", {
|
||||
error: error.message,
|
||||
stderr,
|
||||
});
|
||||
const errorParser = new BuildErrorParser();
|
||||
const errorMessage = stderr || error.message;
|
||||
const userFriendlyMessage = errorParser.parseBuildError(
|
||||
errorMessage,
|
||||
projectId
|
||||
);
|
||||
const buildError = new SystemError(userFriendlyMessage, {
|
||||
originalError: error.message,
|
||||
command,
|
||||
});
|
||||
return reject(buildError);
|
||||
}
|
||||
logBuild(projectId, "INFO", "Script execution completed", { stdout });
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
log(projectId, "INFO", "Start synchronously executing build script", { projectId });
|
||||
await runBuildScript(projectId, projectPath, "build", buildExtraArgs);
|
||||
}
|
||||
|
||||
// 拷贝 dist
|
||||
await copyBuildOutputToTarget({ req, projectPath, projectId });
|
||||
return {
|
||||
success: true,
|
||||
message: "Build completed",
|
||||
projectId,
|
||||
};
|
||||
} finally {
|
||||
buildingProjects.delete(projectId);
|
||||
currentBuilds -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
export { buildProject, copyBuildOutputToTarget, runBuildScript };
|
||||
71
qiming-file-server/src/utils/build/keepAliveDevUtils.js
Normal file
71
qiming-file-server/src/utils/build/keepAliveDevUtils.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { isPortListening } from "../buildArg/portUtils.js";
|
||||
import { log } from "../log/logUtils.js";
|
||||
import { deleteRunningProcess, isProcessRunning } from "./processManager.js";
|
||||
import { isProjectAlive } from "../buildJudge/aliveJudgeUtils.js";
|
||||
import { restartDevServer } from "./restartDevUtils.js";
|
||||
import { startDevServer } from "./startDevUtils.js";
|
||||
import { stopDevServer } from "./stopDevUtils.js";
|
||||
|
||||
/**
|
||||
* 保持开发服务器活跃
|
||||
* @param {Object} req 请求对象
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {number|string} pid 进程ID
|
||||
* @param {number|string} port 端口号
|
||||
* @returns {Promise<Object>} 检查结果
|
||||
*/
|
||||
async function keepAliveDevServer(req, projectId, pid, port, basePath) {
|
||||
const pidNum = Number(pid);
|
||||
const portNum = Number(port);
|
||||
|
||||
log(projectId, "INFO", "Start checking development server status", {
|
||||
projectId,
|
||||
pid: pidNum,
|
||||
port: portNum,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
// 检查项目是否存活
|
||||
const projectAlive = await isProjectAlive(projectId, portNum, basePath);
|
||||
if (projectAlive) {
|
||||
log(projectId, "INFO", "Development server is alive, returning success", {
|
||||
projectId,
|
||||
pid: pidNum,
|
||||
port: portNum,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Development server is alive",
|
||||
projectId,
|
||||
pid: pidNum,
|
||||
port: portNum,
|
||||
};
|
||||
}
|
||||
|
||||
log(projectId, "INFO", "Development server is not alive, restarting", {
|
||||
projectId,
|
||||
pid: pidNum,
|
||||
port: portNum,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
deleteRunningProcess(projectId);
|
||||
|
||||
if(pidNum > 0) {
|
||||
await stopDevServer(req, projectId, pidNum, {
|
||||
strict: false,
|
||||
waitForStop: true,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await startDevServer(req, projectId);
|
||||
return {
|
||||
...result,
|
||||
action: "start",
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export { keepAliveDevServer };
|
||||
920
qiming-file-server/src/utils/build/processManager.js
Normal file
920
qiming-file-server/src/utils/build/processManager.js
Normal file
@@ -0,0 +1,920 @@
|
||||
import { spawn } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { log, getLogDir, getCSTDateString, getCSTTimestampString } from "../log/logUtils.js";
|
||||
import logCacheManager from "../log/logCacheManager.js";
|
||||
import { ProcessError, BusinessError, ValidationError } from "../error/errorHandler.js";
|
||||
import ERROR_CODES from "../error/errorCodes.js";
|
||||
import { sanitizeSensitivePaths } from "../common/sensitiveUtils.js";
|
||||
import config from "../../appConfig/index.js";
|
||||
import {
|
||||
waitPortListening,
|
||||
waitPortFromPid,
|
||||
waitPortFromLog,
|
||||
getPidsByPort,
|
||||
} from "../buildArg/portUtils.js";
|
||||
import ExtraArgsUtils from "../buildArg/extraArgsUtils.js";
|
||||
import { ensureDevBinariesExecutable } from "../buildPermission/permissionManager.js";
|
||||
import { installDependencies } from "../buildDependency/dependencyManager.js";
|
||||
import portPool from "../buildArg/portPool.js";
|
||||
import { isProjectAlive } from "../buildJudge/aliveJudgeUtils.js";
|
||||
|
||||
/**
|
||||
* 从日志文件中提取执行命令后的所有行
|
||||
* 支持多种构建工具和框架:
|
||||
* - 构建工具: vite, webpack, rollup, parcel, esbuild, tsc, ts-node
|
||||
* - 框架: next, nuxt, astro, svelte, remix
|
||||
* - 包管理器: pnpm, npm, yarn, bun
|
||||
* - 脚本执行: node, 自定义脚本
|
||||
*
|
||||
* @param {string} logPath 日志文件路径
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {number} pid 进程ID
|
||||
* @returns {string} 执行命令后的所有输出(已脱敏)
|
||||
*/
|
||||
function extractCommandOutputFromLog(logPath, projectId, pid) {
|
||||
let commandOutput = "开发服务器启动失败";
|
||||
try {
|
||||
if (fs.existsSync(logPath)) {
|
||||
const logContent = fs.readFileSync(logPath, "utf8");
|
||||
const lines = logContent.split("\n");
|
||||
|
||||
// 查找执行命令的行,支持多种构建工具和框架
|
||||
let commandStartIndex = -1;
|
||||
|
||||
// 第一轮:优先查找明确的命令执行行
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (
|
||||
// 直接命令格式: > vite, > webpack, > next, > nuxt 等
|
||||
line.match(
|
||||
/^>\s*(vite|webpack|next|nuxt|rollup|parcel|esbuild|tsc|ts-node|astro|svelte|remix)/
|
||||
) ||
|
||||
// 包管理器运行命令: > pnpm run dev, > npm run start 等
|
||||
line.match(
|
||||
/^>\s*(pnpm|npm|yarn|bun)\s+(run\s+)?(dev|start|serve|build|watch|start:dev)/
|
||||
) ||
|
||||
// 直接执行脚本: > node server.js, > node index.js 等
|
||||
line.match(/^>\s*node\s+\S+\.(js|ts|mjs)$/) ||
|
||||
// 自定义脚本执行标记
|
||||
line.includes("开始执行脚本: dev") ||
|
||||
line.includes("开始执行脚本: start") ||
|
||||
line.includes("开始执行脚本: serve") ||
|
||||
line.includes("开始执行脚本: build")
|
||||
) {
|
||||
commandStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 第二轮:如果没有找到命令行,查找构建工具输出标记
|
||||
if (commandStartIndex === -1) {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (
|
||||
// 构建工具特定输出标记
|
||||
line.includes("VITE") ||
|
||||
line.includes("Webpack") ||
|
||||
line.includes("Next.js") ||
|
||||
line.includes("Nuxt") ||
|
||||
line.includes("Rollup") ||
|
||||
line.includes("Parcel") ||
|
||||
line.includes("Astro") ||
|
||||
line.includes("Svelte") ||
|
||||
line.includes("Remix") ||
|
||||
// 开发服务器启动标记
|
||||
line.includes("Local:") ||
|
||||
line.includes("Network:") ||
|
||||
line.includes("ready in") ||
|
||||
line.includes("compiled successfully") ||
|
||||
line.includes("dev server running") ||
|
||||
line.includes("server started")
|
||||
) {
|
||||
commandStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第三轮:如果仍然没有找到,查找错误输出标记
|
||||
if (commandStartIndex === -1) {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (
|
||||
line.includes("Error") ||
|
||||
line.includes("error") ||
|
||||
line.includes("failed") ||
|
||||
line.includes("Cannot find") ||
|
||||
line.includes("ERR_") ||
|
||||
line.includes("Command failed") ||
|
||||
line.includes("ELIFECYCLE") ||
|
||||
line.includes("npm ERR!") ||
|
||||
line.includes("pnpm ERR!")
|
||||
) {
|
||||
commandStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (commandStartIndex >= 0) {
|
||||
// 提取命令执行后的所有行(包含错误行本身)
|
||||
const outputLines = lines.slice(commandStartIndex);
|
||||
commandOutput = outputLines.join("\n").trim();
|
||||
|
||||
// 如果输出为空,返回默认信息
|
||||
if (!commandOutput) {
|
||||
commandOutput = "命令已执行,但无输出信息";
|
||||
}
|
||||
} else {
|
||||
// 如果没有找到命令开始标记,返回所有日志内容
|
||||
commandOutput = logContent.trim();
|
||||
}
|
||||
|
||||
// 脱敏处理:移除敏感路径信息
|
||||
commandOutput = sanitizeSensitivePaths(commandOutput);
|
||||
|
||||
// 如果输出太长,截取最后的部分(保留更多信息)
|
||||
if (commandOutput.length > 1000) {
|
||||
commandOutput = commandOutput.substring(commandOutput.length - 1000);
|
||||
}
|
||||
}
|
||||
} catch (readError) {
|
||||
log(projectId, "WARN", "读取日志文件失败", {
|
||||
projectId,
|
||||
pid: pid,
|
||||
error: readError.message,
|
||||
});
|
||||
}
|
||||
|
||||
return commandOutput;
|
||||
}
|
||||
|
||||
// 进程注册表:维护运行dev的项目
|
||||
// key: projectId
|
||||
// value: { pid, logPath, startedAt, port}
|
||||
const runningDevProcesses = new Map();
|
||||
|
||||
// 项目级启动锁,避免并发重复启动同一项目
|
||||
const startingProjects = new Set();
|
||||
|
||||
/**
|
||||
* 获取运行中的进程信息
|
||||
* @param {string} projectId 项目ID
|
||||
* @returns {Object|null} 进程信息
|
||||
*/
|
||||
function getRunningProcess(projectId) {
|
||||
return runningDevProcesses.get(projectId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行中的进程信息
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {Object} processInfo 进程信息
|
||||
*/
|
||||
function setRunningProcess(projectId, processInfo) {
|
||||
runningDevProcesses.set(projectId, processInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除运行中的进程信息
|
||||
* @param {string} projectId 项目ID
|
||||
*/
|
||||
function deleteRunningProcess(projectId) {
|
||||
runningDevProcesses.delete(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查项目是否正在启动中
|
||||
* @param {string} projectId 项目ID
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isProjectStarting(projectId) {
|
||||
return startingProjects.has(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加项目到启动锁
|
||||
* @param {string} projectId 项目ID
|
||||
*/
|
||||
function addStartingProject(projectId) {
|
||||
startingProjects.add(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从启动锁中移除项目
|
||||
* @param {string} projectId 项目ID
|
||||
*/
|
||||
function removeStartingProject(projectId) {
|
||||
startingProjects.delete(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用spawn非阻塞启动开发服务器,并把日志写入文件
|
||||
* @param {Object} options 启动选项
|
||||
* @param {Object} options.req 请求对象
|
||||
* @param {string} options.projectId 项目ID
|
||||
* @param {string} options.projectPath 项目路径
|
||||
* @param {string} options.devScript dev脚本内容
|
||||
* @param {Object} options.envExtra 额外的环境变量
|
||||
* @param {Array} options.extraArgs 额外的参数
|
||||
* @returns {Object} { pid, logPath, port }
|
||||
*/
|
||||
async function startDev_NonBlocking({
|
||||
req,
|
||||
projectId,
|
||||
projectPath,
|
||||
devScript,
|
||||
}) {
|
||||
const lowerScript = (devScript || "").toLowerCase();
|
||||
const isVite = lowerScript.includes("vite");
|
||||
const isNext = lowerScript.includes("next");
|
||||
|
||||
if (!isVite && !isNext) {
|
||||
throw new BusinessError("不支持的脚本类型" + devScript + ",请使用vite或next脚本", {
|
||||
projectId,
|
||||
code: ERROR_CODES.INVALID_SCRIPT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建日志目录
|
||||
const logDir = getLogDir(projectId);
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
// 主日志文件
|
||||
const today = getCSTDateString(); // 格式: YYYY-MM-DD (东八区)
|
||||
const logPath = path.join(logDir, `dev-${today}.log`);
|
||||
|
||||
// 临时日志用于端口解析
|
||||
const tempLogPath = path.join(
|
||||
logDir,
|
||||
`dev-temp-${Date.now().toString()}.log`
|
||||
);
|
||||
|
||||
let outStream, tempOutStream, child;
|
||||
let streamsClosed = false;
|
||||
let childExited = false;
|
||||
let allocatedPort = null; // 跟踪分配的端口,用于失败时释放
|
||||
|
||||
// 标记缓存是否已失效(避免频繁删除缓存)
|
||||
let cacheInvalidated = false;
|
||||
|
||||
// 创建安全的写入函数
|
||||
const safeWrite = (stream, data, streamName, flush = false) => {
|
||||
// 分别检查流的状态,而不是使用共享的 streamsClosed
|
||||
if (!stream || stream.destroyed) {
|
||||
// 只有在需要时才输出调试信息,避免日志过多
|
||||
if (stream && stream.destroyed) {
|
||||
log(projectId, "DEBUG", `${streamName}流已销毁,跳过写入`, {
|
||||
destroyed: true,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果全局标记已关闭,也不写入
|
||||
if (streamsClosed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 在data前拼接时间戳 [2025/10/12 11:51:09] (东八区)
|
||||
const timestamp = getCSTTimestampString();
|
||||
const dataWithTimestamp = `[${timestamp}] ` + data;
|
||||
const result = stream.write(dataWithTimestamp);
|
||||
|
||||
// 如果需要立即刷新(关键日志),使用 cork/uncork 机制强制刷新缓冲区
|
||||
if (flush && typeof stream.cork === 'function' && typeof stream.uncork === 'function') {
|
||||
// cork() 暂停写入,uncork() 立即刷新所有缓冲数据
|
||||
// 这里我们先 cork 再 uncork,强制刷新当前缓冲区
|
||||
stream.cork();
|
||||
// 使用 setImmediate 确保在下一个事件循环刷新
|
||||
setImmediate(() => {
|
||||
try {
|
||||
if (!stream.destroyed) {
|
||||
stream.uncork();
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略 uncork 错误
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果缓存已启用,标记缓存失效(只标记一次)
|
||||
// 避免频繁删除缓存,让下次读取时自动重新加载
|
||||
if (logCacheManager.isEnabled() && !cacheInvalidated) {
|
||||
logCacheManager.delete(String(projectId));
|
||||
cacheInvalidated = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", `${streamName}写入错误`, {
|
||||
error: err.message,
|
||||
code: err.code,
|
||||
streamName: streamName,
|
||||
});
|
||||
// 只有在严重错误时才关闭所有流
|
||||
// 临时日志的错误不应该影响主日志
|
||||
if (err.code === "ERR_STREAM_WRITE_AFTER_END" || err.code === "EPIPE") {
|
||||
// 只关闭出错的流,不影响其他流
|
||||
try {
|
||||
if (stream && !stream.destroyed) {
|
||||
stream.end();
|
||||
}
|
||||
} catch (closeErr) {
|
||||
log(projectId, "WARN", `关闭${streamName}流时出错`, { error: closeErr.message });
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 安全关闭流的函数
|
||||
const safeCloseStreams = () => {
|
||||
if (streamsClosed) return;
|
||||
streamsClosed = true;
|
||||
|
||||
try {
|
||||
if (outStream && !outStream.destroyed) {
|
||||
outStream.end();
|
||||
}
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "关闭主日志流时出错", { error: err.message });
|
||||
}
|
||||
|
||||
try {
|
||||
if (tempOutStream && !tempOutStream.destroyed) {
|
||||
tempOutStream.end();
|
||||
}
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "关闭临时日志流时出错", { error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 添加流错误处理
|
||||
const handleStreamError = (streamName, err) => {
|
||||
log(projectId, "WARN", `${streamName}流错误`, { error: err.message });
|
||||
// 如果是写入错误且子进程已退出,则关闭流
|
||||
if (
|
||||
(err.code === "ERR_STREAM_WRITE_AFTER_END" || err.code === "EPIPE") &&
|
||||
childExited
|
||||
) {
|
||||
safeCloseStreams();
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// 在启动前确保可执行权限
|
||||
try {
|
||||
await ensureDevBinariesExecutable(projectPath);
|
||||
} catch (_) {}
|
||||
|
||||
// 创建日志文件写入流
|
||||
outStream = fs.createWriteStream(logPath, { flags: "a" });
|
||||
tempOutStream = fs.createWriteStream(tempLogPath, { flags: "a" });
|
||||
|
||||
// 组合为单个子进程内串行执行:先 dev-inject,再 vite-plugin-design-mode
|
||||
// 使用 set +e 忽略错误,确保两个命令都会执行,无论第一个是否成功
|
||||
// 注意:即使预处理命令失败,也不会阻塞后续依赖安装和 dev 启动,仅做“尽力而为”的处理
|
||||
const preCmd = "set +e ; pnpm dlx @xagi/dev-inject@latest install --framework ; pnpm dlx @xagi/vite-plugin-design-mode@latest install ; set -e";
|
||||
|
||||
safeWrite(outStream, preCmd, "Main log");
|
||||
safeWrite(tempOutStream, preCmd, "Temp log");
|
||||
|
||||
// 在安装依赖之前先执行预处理命令(失败不影响后续流程)
|
||||
await new Promise((resolve) => {
|
||||
try {
|
||||
const preProcess = spawn("bash", ["-lc", preCmd], {
|
||||
cwd: projectPath,
|
||||
env: { ...process.env },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
preProcess.stdout.on("data", (data) => {
|
||||
const msg = data.toString();
|
||||
safeWrite(outStream, msg, "Main log");
|
||||
safeWrite(tempOutStream, msg, "Temp log");
|
||||
});
|
||||
|
||||
preProcess.stderr.on("data", (data) => {
|
||||
const msg = data.toString();
|
||||
safeWrite(outStream, msg, "Main log");
|
||||
safeWrite(tempOutStream, msg, "Temp log");
|
||||
});
|
||||
|
||||
preProcess.on("error", (err) => {
|
||||
const errorMessage = `预处理命令执行出错(忽略并继续后续流程): ${err.message}\n`;
|
||||
safeWrite(outStream, errorMessage, "Main log", true);
|
||||
safeWrite(tempOutStream, errorMessage, "Temp log", true);
|
||||
resolve();
|
||||
});
|
||||
|
||||
preProcess.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
const errorMessage = `Preprocessing command exit code is ${code} (ignore and continue subsequent process)\n`;
|
||||
safeWrite(outStream, errorMessage, "主日志", true);
|
||||
safeWrite(tempOutStream, errorMessage, "临时日志", true);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = `预处理命令启动失败(忽略并继续后续流程): ${error.message}\n`;
|
||||
safeWrite(outStream, errorMessage, "Main log", true);
|
||||
safeWrite(tempOutStream, errorMessage, "Temp log", true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// 安装依赖(日志会同时写入主日志和临时日志)
|
||||
try {
|
||||
await installDependencies(req, projectId, projectPath, {
|
||||
outStream,
|
||||
tempOutStream,
|
||||
safeWrite,
|
||||
});
|
||||
} catch (error) {
|
||||
// 安装失败时记录错误并抛出(safeWrite 会自动添加时间戳)- 立即刷新到磁盘
|
||||
const errorMessage = `Dependency installation failed: ${error.message}\n`;
|
||||
safeWrite(outStream, errorMessage, "Main log", true); // flush = true
|
||||
safeWrite(tempOutStream, errorMessage, "Temp log", true); // flush = true
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 在日志文件开头写入开始执行
|
||||
const startMessage = `Start executing script: dev\n`;
|
||||
safeWrite(outStream, startMessage, "Main log");
|
||||
safeWrite(tempOutStream, startMessage, "Temp log");
|
||||
|
||||
// 构建 dev 命令参数
|
||||
const npmArgs = [];
|
||||
|
||||
// 额外参数和环境变量(端口将在内部从端口池获取)
|
||||
const { extraArgs, envExtra, port } = await ExtraArgsUtils.processExtraArgs({
|
||||
devScript,
|
||||
projectId,
|
||||
req,
|
||||
});
|
||||
|
||||
// 记录分配的端口,用于失败时释放
|
||||
allocatedPort = port;
|
||||
|
||||
// 透传额外参数到脚本
|
||||
if (extraArgs && extraArgs.length > 0) {
|
||||
npmArgs.push(...extraArgs);
|
||||
}
|
||||
|
||||
const escapeArg = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
||||
const extraArgsEscaped = (npmArgs && npmArgs.length > 0)
|
||||
? npmArgs.map(escapeArg).join(" ")
|
||||
: "";
|
||||
|
||||
// 记录参数信息以便调试
|
||||
if (extraArgsEscaped) {
|
||||
log(projectId, "INFO", "Generated extra arguments", {
|
||||
extraArgs: extraArgs,
|
||||
npmArgs: npmArgs,
|
||||
extraArgsEscaped: extraArgsEscaped,
|
||||
});
|
||||
}
|
||||
|
||||
// 检测命令是否需要通过 npx 执行
|
||||
// 如果命令是纯命令名(如 vite, next)而不是路径,应该使用 npx
|
||||
const needsNpx = (script) => {
|
||||
if (!script || typeof script !== "string") return false;
|
||||
const trimmed = script.trim();
|
||||
// 如果包含路径分隔符(/ 或 \),说明是路径,不需要 npx
|
||||
if (trimmed.includes("/") || trimmed.includes("\\")) return false;
|
||||
// 提取第一个词(命令名)
|
||||
const firstWord = trimmed.split(/\s+/)[0];
|
||||
// 如果是常见的构建工具命令名,且不是路径,需要 npx
|
||||
const commandsNeedingNpx = ["vite", "next", "webpack", "rollup", "parcel", "esbuild", "tsc", "ts-node", "astro", "svelte", "remix", "nuxt"];
|
||||
return commandsNeedingNpx.includes(firstWord);
|
||||
};
|
||||
|
||||
let fullCommand;
|
||||
let execCommand = ""; // 用于记录实际执行的命令
|
||||
if (isVite) {
|
||||
// 移除脚本中已存在的 --host / --base(包含等号或跟随值),避免重复冲突
|
||||
const sanitizeCliFlags = (script, flagsToRemove) => {
|
||||
if (!script || typeof script !== "string") return script;
|
||||
let result = script;
|
||||
for (const flag of flagsToRemove) {
|
||||
// 1) 移除 --flag=value 形式
|
||||
const eqPattern = new RegExp(`${flag.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}=\\S+`, "g");
|
||||
result = result.replace(eqPattern, "");
|
||||
// 2) 移除 --flag <value> 形式,<value> 为非以 - 开头的单词(含路径)
|
||||
const spacedPattern = new RegExp(`${flag.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\s+([^\s-][^\s]*)`, "g");
|
||||
result = result.replace(spacedPattern, "");
|
||||
// 3) 移除仅有 --flag(无值)
|
||||
const barePattern = new RegExp(`${flag.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\b`, "g");
|
||||
result = result.replace(barePattern, "");
|
||||
}
|
||||
// 归一多余空白
|
||||
return result.replace(/\s{2,}/g, " ").trim();
|
||||
};
|
||||
|
||||
const cleanedScript = sanitizeCliFlags(devScript, ["--host", "--base"]);
|
||||
const appended = extraArgsEscaped ? ` ${extraArgsEscaped}` : "";
|
||||
// 如果需要 npx,使用 npx 执行命令
|
||||
const commandPrefix = needsNpx(cleanedScript) ? "npx" : "";
|
||||
execCommand = commandPrefix ? `${commandPrefix} ${cleanedScript}${appended}` : `${cleanedScript}${appended}`;
|
||||
// 使用 exec 替换 shell 进程,确保 child.pid 直接对应 vite 进程
|
||||
fullCommand = `exec ${execCommand}`;
|
||||
} else if (isNext) {
|
||||
const appended = extraArgsEscaped ? ` ${extraArgsEscaped}` : "";
|
||||
// 如果需要 npx,使用 npx 执行命令
|
||||
const commandPrefix = needsNpx(devScript) ? "npx" : "";
|
||||
execCommand = commandPrefix ? `${commandPrefix} ${devScript}${appended}` : `${devScript}${appended}`;
|
||||
fullCommand = `exec ${execCommand}`;
|
||||
} else {
|
||||
const extraForNpm = extraArgsEscaped ? ` -- ${extraArgsEscaped}` : "";
|
||||
execCommand = `pnpm run dev${extraForNpm}`;
|
||||
fullCommand = `exec ${execCommand}`;
|
||||
}
|
||||
|
||||
// 将实际执行的命令写入日志文件
|
||||
if (execCommand) {
|
||||
const commandMessage = `> ${execCommand}\n`;
|
||||
safeWrite(outStream, commandMessage, "Main log");
|
||||
safeWrite(tempOutStream, commandMessage, "Temp log");
|
||||
}
|
||||
|
||||
// 打印将要执行的完整命令与上下文,便于排查
|
||||
try {
|
||||
log(projectId, "INFO", "Start child process, sequentially execute preprocessing and start dev:", {
|
||||
command: fullCommand,
|
||||
cwd: projectPath,
|
||||
devScript,
|
||||
extraArgs,
|
||||
envExtraKeys: Object.keys(envExtra || {}),
|
||||
});
|
||||
} catch (_) {}
|
||||
|
||||
// 使用 shell 执行,以便在同一子进程中串行执行两步
|
||||
child = spawn("sh", ["-c", fullCommand], {
|
||||
cwd: projectPath,
|
||||
env: {
|
||||
PATH: process.env.PATH, // 必需:找到命令
|
||||
//HOME: process.env.HOME, // 用户配置目录
|
||||
NODE_ENV: 'development', //不能指定production,否则hmr会失效
|
||||
...envExtra, // 项目特定变量
|
||||
},
|
||||
detached: true,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
// 添加日志流的错误处理
|
||||
outStream.on("error", (err) => {
|
||||
log(projectId, "ERROR", "Main log stream error", {
|
||||
error: err.message,
|
||||
code: err.code,
|
||||
path: logPath,
|
||||
destroyed: outStream.destroyed,
|
||||
});
|
||||
handleStreamError("Main log", err);
|
||||
});
|
||||
tempOutStream.on("error", (err) => {
|
||||
log(projectId, "ERROR", "Temp log stream error", {
|
||||
error: err.message,
|
||||
code: err.code,
|
||||
path: tempLogPath,
|
||||
destroyed: tempOutStream.destroyed,
|
||||
});
|
||||
handleStreamError("Temp log", err);
|
||||
});
|
||||
|
||||
// 安全地重定向子进程输出到双日志文件
|
||||
if (child.stdout) {
|
||||
child.stdout.on("data", (data) => {
|
||||
// 使用安全的写入函数,避免向已关闭的流写入
|
||||
const mainWriteOk = safeWrite(outStream, data, "Main log");
|
||||
const tempWriteOk = safeWrite(tempOutStream, data, "Temp log");
|
||||
|
||||
// 如果任一写入失败,记录详细信息(但不影响另一个流)
|
||||
if (!mainWriteOk || !tempWriteOk) {
|
||||
log(projectId, "DEBUG", "Log writing status", {
|
||||
mainWriteOk,
|
||||
tempWriteOk,
|
||||
mainDestroyed: outStream ? outStream.destroyed : null,
|
||||
tempDestroyed: tempOutStream ? tempOutStream.destroyed : null,
|
||||
streamsClosed,
|
||||
});
|
||||
}
|
||||
});
|
||||
child.stdout.on("error", (err) => {
|
||||
log(projectId, "WARN", "Child process stdout error", { error: err.message });
|
||||
});
|
||||
child.stdout.on("end", () => {
|
||||
// stdout 流结束时,不立即关闭日志流,等待子进程完全退出
|
||||
log(projectId, "INFO", "子进程stdout流已结束", { pid: child.pid });
|
||||
});
|
||||
}
|
||||
|
||||
if (child.stderr) {
|
||||
child.stderr.on("data", (data) => {
|
||||
// 使用安全的写入函数,避免向已关闭的流写入
|
||||
const mainWriteOk = safeWrite(outStream, data, "Main log");
|
||||
const tempWriteOk = safeWrite(tempOutStream, data, "Temp log");
|
||||
|
||||
// 如果任一写入失败,记录详细信息(但不影响另一个流)
|
||||
if (!mainWriteOk || !tempWriteOk) {
|
||||
log(projectId, "DEBUG", "Log writing status(stderr)", {
|
||||
mainWriteOk,
|
||||
tempWriteOk,
|
||||
mainDestroyed: outStream ? outStream.destroyed : null,
|
||||
tempDestroyed: tempOutStream ? tempOutStream.destroyed : null,
|
||||
streamsClosed,
|
||||
});
|
||||
}
|
||||
});
|
||||
child.stderr.on("error", (err) => {
|
||||
log(projectId, "WARN", "Child process stderr error", { error: err.message });
|
||||
});
|
||||
child.stderr.on("end", () => {
|
||||
// stderr 流结束时,不立即关闭日志流,等待子进程完全退出
|
||||
log(projectId, "INFO", "Child process stderr stream ended", { pid: child.pid });
|
||||
});
|
||||
}
|
||||
|
||||
// 监听子进程退出事件,安全关闭日志流
|
||||
child.on("exit", (code, signal) => {
|
||||
childExited = true;
|
||||
log(projectId, "INFO", "Child process exited", {
|
||||
pid: child.pid,
|
||||
code,
|
||||
signal,
|
||||
});
|
||||
// 延迟关闭流,确保所有数据都已写入
|
||||
setTimeout(() => {
|
||||
safeCloseStreams();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
log(projectId, "WARN", "Child process error", { error: err.message });
|
||||
childExited = true;
|
||||
log(projectId, "ERROR", "Child process startup failed", {
|
||||
pid: child.pid,
|
||||
error: err.message,
|
||||
});
|
||||
safeCloseStreams();
|
||||
});
|
||||
|
||||
runningDevProcesses.set(projectId, {
|
||||
pid: child.pid,
|
||||
logPath,
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
|
||||
child.unref();// 解除子进程与父进程的关联,子进程可以独立运行
|
||||
|
||||
// 如果使用了 exec 命令(替换进程),需要等待 exec 完成进程替换
|
||||
// exec 会在 preCmd 成功后替换 shell 进程为实际服务进程(如 vite)
|
||||
// 这种情况下 child.pid 会直接对应服务进程,但仍需稍等确保替换完成
|
||||
const usesExec = fullCommand && fullCommand.includes("exec ");
|
||||
if (usesExec) {
|
||||
// 等待 200ms 确保 exec 完成进程替换
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
}
|
||||
|
||||
log(projectId, "INFO", "Using port pool allocated port and current process ID", {
|
||||
projectId,
|
||||
pid: child.pid,
|
||||
port: port
|
||||
});
|
||||
|
||||
// 轮询等待项目对外可访问,最多 30s
|
||||
const basePathFromReq =
|
||||
(req && req.query && req.query.basePath) ||
|
||||
(req && req.body && req.body.basePath) ||
|
||||
undefined;
|
||||
const resolvedBasePath = basePathFromReq || "/";
|
||||
const maxAliveWaitMs = 30000;
|
||||
const alivePollIntervalMs = 1000;
|
||||
const aliveCheckTimeoutPerRequest = 1500;
|
||||
|
||||
let projectAlive = false;
|
||||
const aliveStartedAt = Date.now();
|
||||
// 启动后先等待 1s 再开始轮询,给框架预留初始化时间
|
||||
await new Promise((resolve) => setTimeout(resolve, alivePollIntervalMs));
|
||||
while (Date.now() - aliveStartedAt < maxAliveWaitMs) {
|
||||
projectAlive = await isProjectAlive(projectId, port, basePathFromReq, {
|
||||
timeoutMs: aliveCheckTimeoutPerRequest,
|
||||
});
|
||||
if (projectAlive) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, alivePollIntervalMs));
|
||||
}
|
||||
|
||||
if (!projectAlive) {
|
||||
const waitSeconds = Math.round(maxAliveWaitMs / 1000);
|
||||
log(projectId, "WARN", "Development server is not accessible within the limited time", {
|
||||
port,
|
||||
pid: child.pid,
|
||||
basePath: resolvedBasePath,
|
||||
waitSeconds,
|
||||
});
|
||||
} else {
|
||||
log(projectId, "INFO", "Project accessibility verification passed", {
|
||||
port,
|
||||
basePath: resolvedBasePath,
|
||||
elapsedMs: Date.now() - aliveStartedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// 回填端口和实际进程ID
|
||||
const info = runningDevProcesses.get(projectId);
|
||||
if (info) {
|
||||
info.port = port;
|
||||
info.pid = child.pid; // 进程组pid
|
||||
runningDevProcesses.set(projectId, info);
|
||||
}
|
||||
|
||||
log(projectId, "INFO", "Development server startup successfully", {
|
||||
projectId,
|
||||
pid: child.pid,
|
||||
port: port,
|
||||
});
|
||||
|
||||
//要返回进程组pid和端口,因为后续需要通过进程组pid来停止进程
|
||||
return { pid: child.pid, port: port };
|
||||
} catch (error) {
|
||||
// 启动失败,释放已分配的端口
|
||||
if (allocatedPort) {
|
||||
portPool.release(String(projectId));
|
||||
log(projectId, "INFO", "Startup failed, port released", {
|
||||
port: allocatedPort,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
// 重新抛出错误
|
||||
throw error;
|
||||
} finally {
|
||||
// 只有在发生异常或子进程启动失败时才立即清理资源
|
||||
// 正常情况下,流会在子进程退出时自动关闭
|
||||
if (childExited || !child || !child.pid) {
|
||||
safeCloseStreams();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止指定的进程
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {number} pid 进程ID
|
||||
* @returns {Promise<boolean>} 是否成功停止
|
||||
*/
|
||||
async function killProcess(projectId, pid) {
|
||||
const pidNum = Number(pid);
|
||||
if (!Number.isFinite(pidNum)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 首先检查进程是否存在
|
||||
if (!isProcessRunning(pidNum)) {
|
||||
log(projectId, "INFO", "Process does not exist", { pid: pidNum });
|
||||
runningDevProcesses.delete(projectId);
|
||||
return true;
|
||||
}
|
||||
|
||||
let killed = false;
|
||||
let killMethod = "";
|
||||
|
||||
try {
|
||||
// 优先杀进程组(detached)
|
||||
process.kill(-pidNum);
|
||||
killed = true;
|
||||
killMethod = "Process group";
|
||||
log(projectId, "INFO", "通过进程组杀死进程", { pid: pidNum });
|
||||
} catch (e) {
|
||||
if (e && (e.code === "ESRCH" || e.errno === "ESRCH")) {
|
||||
// 进程组不存在,不能认为已杀死;继续尝试对单个进程发送信号
|
||||
killed = false;
|
||||
killMethod = "Process group(not exist)";
|
||||
log(projectId, "INFO", "Process group does not exist, back to try single process kill", { pid: pidNum });
|
||||
} else {
|
||||
log(projectId, "WARN", "Failed to kill process group", {
|
||||
pid: pidNum,
|
||||
error: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!killed) {
|
||||
try {
|
||||
process.kill(pidNum);
|
||||
killed = true;
|
||||
killMethod = "单个进程";
|
||||
log(projectId, "INFO", "Kill process through single process", { pid: pidNum });
|
||||
} catch (e) {
|
||||
if (e && (e.code === "ESRCH" || e.errno === "ESRCH")) {
|
||||
killed = true;
|
||||
killMethod = "Single process(not exist)";
|
||||
log(projectId, "INFO", "Process does not exist,视为已停止", { pid: pidNum });
|
||||
} else {
|
||||
log(projectId, "ERROR", "Failed to kill process", {
|
||||
pid: pidNum,
|
||||
error: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证进程是否真的被杀死了
|
||||
if (killed) {
|
||||
// 等待一小段时间让进程完全退出
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
if (!isProcessRunning(pidNum)) {
|
||||
log(projectId, "INFO", "Process stopped successfully", {
|
||||
pid: pidNum,
|
||||
method: killMethod,
|
||||
});
|
||||
runningDevProcesses.delete(projectId);
|
||||
return true;
|
||||
} else {
|
||||
log(projectId, "WARN", "Process still exists, kill may have failed", {
|
||||
pid: pidNum,
|
||||
method: killMethod,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return killed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查进程是否正在运行
|
||||
* @param {number} pid 进程ID
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isProcessRunning(pid) {
|
||||
try {
|
||||
process.kill(pid, 0); // 发送信号0检查进程是否存在
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err && (err.code === 'EPERM' || err.code === 'EACCES')) {
|
||||
// 进程存在,但当前用户无权限发送信号
|
||||
return true;
|
||||
}
|
||||
if (err && err.code === 'ESRCH') {
|
||||
// 进程不存在
|
||||
return false;
|
||||
}
|
||||
// 其他错误,保守返回 false,或根据需要上报日志
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待进程停止
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {number} pid 进程ID
|
||||
* @returns {Promise<Object>} { stopped: boolean, attempts: number }
|
||||
*/
|
||||
async function waitForProcessStop(projectId, pid) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = config.DEV_SERVER_STOP_MAX_ATTEMPTS;
|
||||
while (isProcessRunning(pid) && attempts < maxAttempts) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, config.DEV_SERVER_STOP_CHECK_INTERVAL)
|
||||
);
|
||||
attempts++;
|
||||
}
|
||||
|
||||
const stopped = !isProcessRunning(pid);
|
||||
return { stopped, attempts };
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有运行中的进程
|
||||
* @returns {Array} 进程列表
|
||||
*/
|
||||
function listRunningProcesses() {
|
||||
const list = Array.from(runningDevProcesses.entries()).map(([id, info]) => ({
|
||||
projectId: id,
|
||||
pid: info.pid,
|
||||
type: info.type,
|
||||
startedAt: info.startedAt,
|
||||
port: info.port,
|
||||
}));
|
||||
return list;
|
||||
}
|
||||
|
||||
export {
|
||||
getRunningProcess,
|
||||
setRunningProcess,
|
||||
deleteRunningProcess,
|
||||
isProjectStarting,
|
||||
addStartingProject,
|
||||
removeStartingProject,
|
||||
startDev_NonBlocking,
|
||||
killProcess,
|
||||
isProcessRunning,
|
||||
waitForProcessStop,
|
||||
listRunningProcesses,
|
||||
};
|
||||
129
qiming-file-server/src/utils/build/restartDevUtils.js
Normal file
129
qiming-file-server/src/utils/build/restartDevUtils.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { log } from "../log/logUtils.js";
|
||||
import { BusinessError, FileError, ResourceError } from "../error/errorHandler.js";
|
||||
import {
|
||||
isProjectStarting,
|
||||
addStartingProject,
|
||||
removeStartingProject,
|
||||
startDev_NonBlocking,
|
||||
} from "./processManager.js";
|
||||
import ERROR_CODES from "../error/errorCodes.js";
|
||||
import { stopDevServer } from "./stopDevUtils.js";
|
||||
import { removeNodeModules } from "../buildDependency/dependencyManager.js";
|
||||
import { createPnpmNpmrc } from "../common/npmrcUtils.js";
|
||||
import {
|
||||
extractIsolationContext,
|
||||
resolveProjectPath,
|
||||
} from "../common/projectPathUtils.js";
|
||||
|
||||
// 重启开发服务器
|
||||
async function restartDevServer(req, projectId) {
|
||||
log(projectId, "INFO", "Start restarting development server", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
// 检查项目是否存在
|
||||
const isolationContext = extractIsolationContext(req?.query || {});
|
||||
const projectPath = resolveProjectPath(projectId, isolationContext);
|
||||
const jsonFilePath = path.join(projectPath, "package.json");
|
||||
const exists = fs.existsSync(jsonFilePath);
|
||||
if (!exists) {
|
||||
log(projectId, "WARN", "Project missing package.json file", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
throw new ResourceError("Project missing package.json file", {
|
||||
projectId,
|
||||
projectPath,
|
||||
});
|
||||
}
|
||||
|
||||
let jsonContent;
|
||||
try {
|
||||
jsonContent = JSON.parse(fs.readFileSync(jsonFilePath, "utf8"));
|
||||
} catch (error) {
|
||||
throw new FileError("package.json file format error", {
|
||||
projectId,
|
||||
jsonFilePath,
|
||||
originalError: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const jsonScripts = jsonContent.scripts;
|
||||
const devScript = jsonScripts.dev;
|
||||
if (!devScript) {
|
||||
log(projectId, "WARN", "Project missing dev script", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
throw new BusinessError("Project missing dev script", { projectId });
|
||||
}
|
||||
|
||||
// 如果项目正在启动中,等待完成
|
||||
// if (isProjectStarting(projectId)) {
|
||||
// throw new BusinessError("Project is starting, please try again later", {
|
||||
// projectId,
|
||||
// code: ERROR_CODES.PROJECT_STARTING,
|
||||
// });
|
||||
// }
|
||||
|
||||
addStartingProject(projectId);
|
||||
|
||||
try {
|
||||
// 1. 停止现有的开发服务器
|
||||
const pidFromQuery = req.query.pid;
|
||||
|
||||
await stopDevServer(req, projectId, pidFromQuery, {
|
||||
strict: false, // 非严格模式,停止失败不抛出异常
|
||||
waitForStop: true, // 等待进程完全停止
|
||||
});
|
||||
|
||||
// 2. 删除node_modules
|
||||
log(projectId, "INFO", "Start deleting node_modules and lock file", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
await removeNodeModules(projectPath, projectId);
|
||||
|
||||
// 3. 创建.npmrc文件
|
||||
log(projectId, "INFO", "Create .npmrc file", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
await createPnpmNpmrc(projectPath, projectId);
|
||||
|
||||
// 4. 启动dev服务器(依赖安装会在 startDev_NonBlocking 中执行)
|
||||
log(projectId, "INFO", "Start starting dev server", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
const { pid, port: actualPort } = await startDev_NonBlocking({
|
||||
req,
|
||||
projectId,
|
||||
projectPath,
|
||||
devScript,
|
||||
});
|
||||
|
||||
log(projectId, "INFO", "Dev server restart completed", {
|
||||
projectId,
|
||||
pid,
|
||||
port: actualPort,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Development server restart successfully",
|
||||
projectId,
|
||||
pid,
|
||||
port: actualPort,
|
||||
};
|
||||
} finally {
|
||||
removeStartingProject(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
export { restartDevServer };
|
||||
150
qiming-file-server/src/utils/build/startDevUtils.js
Normal file
150
qiming-file-server/src/utils/build/startDevUtils.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { log } from "../log/logUtils.js";
|
||||
import { BusinessError, FileError, ResourceError } from "../error/errorHandler.js";
|
||||
import ERROR_CODES from "../error/errorCodes.js";
|
||||
import {
|
||||
getRunningProcess,
|
||||
isProjectStarting,
|
||||
addStartingProject,
|
||||
removeStartingProject,
|
||||
startDev_NonBlocking,
|
||||
} from "./processManager.js";
|
||||
import { removeNodeModules } from "../buildDependency/dependencyManager.js";
|
||||
import {
|
||||
extractIsolationContext,
|
||||
resolveProjectPath,
|
||||
} from "../common/projectPathUtils.js";
|
||||
|
||||
// 启动开发服务器
|
||||
async function startDevServer(req, projectId) {
|
||||
|
||||
// if (isProjectStarting(projectId)) {
|
||||
// throw new BusinessError("该项目正在启动中,请稍后重试", {
|
||||
// projectId,
|
||||
// code: ERROR_CODES.PROJECT_STARTING,
|
||||
// });
|
||||
// }
|
||||
|
||||
addStartingProject(projectId);
|
||||
try {
|
||||
log(projectId, "INFO", "Start starting development server", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
const isolationContext = extractIsolationContext(req?.query || {});
|
||||
const projectPath = resolveProjectPath(projectId, isolationContext);
|
||||
const jsonFilePath = path.join(projectPath, "package.json");
|
||||
|
||||
const exists = fs.existsSync(jsonFilePath);
|
||||
if (!exists) {
|
||||
log(projectId, "WARN", "Project missing package.json file", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
throw new ResourceError("Project missing package.json file", {
|
||||
projectId,
|
||||
projectPath,
|
||||
});
|
||||
}
|
||||
|
||||
let jsonContent;
|
||||
try {
|
||||
jsonContent = JSON.parse(fs.readFileSync(jsonFilePath, "utf8"));
|
||||
} catch (error) {
|
||||
throw new FileError("package.json file format error", {
|
||||
projectId,
|
||||
jsonFilePath,
|
||||
originalError: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const jsonScripts = jsonContent.scripts;
|
||||
const devScript = jsonScripts.dev;
|
||||
if (!devScript) {
|
||||
log(projectId, "WARN", "Project missing dev script", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
throw new BusinessError("Project missing dev script, please add dev script in package.json", { projectId });
|
||||
}
|
||||
|
||||
// Linux 环境下:检测 libc 类型与已安装 Rollup 原生包是否匹配,若不匹配则清理依赖
|
||||
try {
|
||||
const isLinux = process.platform === "linux";
|
||||
if (isLinux) {
|
||||
const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
|
||||
const glibcVersion = report && report.header && report.header.glibcVersionRuntime;
|
||||
const isMusl = !glibcVersion; // 没有 glibc 版本通常意味着 musl(如 Alpine)
|
||||
|
||||
const pnpmDir = path.join(projectPath, "node_modules", ".pnpm");
|
||||
if (fs.existsSync(pnpmDir)) {
|
||||
const entries = await fs.promises.readdir(pnpmDir, { withFileTypes: true });
|
||||
const hasRollupGnu = entries.some((ent) => ent.isDirectory() && (ent.name || "").includes("@rollup+rollup-linux-x64-gnu"));
|
||||
const hasRollupMusl = entries.some((ent) => ent.isDirectory() && (ent.name || "").includes("@rollup+rollup-linux-x64-musl"));
|
||||
|
||||
// 在 musl 系统上若装了 gnu 变体,或在 glibc 系统上若装了 musl 变体,则清理
|
||||
const mismatch = (isMusl && hasRollupGnu) || (!isMusl && hasRollupMusl);
|
||||
if (mismatch) {
|
||||
log(projectId, "WARN", "Detected Rollup native package does not match libc, clean dependencies and reinstall", {
|
||||
projectId,
|
||||
isMusl,
|
||||
glibcVersion: glibcVersion || null,
|
||||
});
|
||||
await removeNodeModules(projectPath, projectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(projectId, "WARN", "Linux native package matching detection failed (ignore continue)", {
|
||||
error: e && e.message,
|
||||
});
|
||||
}
|
||||
|
||||
// 尝试为后续 dev 进程注入回退环境,优先使用 WASM/JS,避免 .node 装载
|
||||
try {
|
||||
process.env.ROLLUP_WASM = process.env.ROLLUP_WASM || "1";
|
||||
process.env.ROLLUP_DISABLE_NATIVE = process.env.ROLLUP_DISABLE_NATIVE || "1";
|
||||
} catch (_) {}
|
||||
|
||||
// 如果已在运行,则直接返回信息
|
||||
// if (getRunningProcess(projectId)) {
|
||||
// const p = getRunningProcess(projectId);
|
||||
// log(projectId, "INFO", "项目已在运行,直接返回信息", {
|
||||
// projectId,
|
||||
// requestId: req.requestId,
|
||||
// });
|
||||
// return {
|
||||
// success: true,
|
||||
// message: "已在运行",
|
||||
// projectId,
|
||||
// pid: p.pid,
|
||||
// port: p.port,
|
||||
// };
|
||||
// }
|
||||
|
||||
log(projectId, "INFO", "Start executing dev script in non-blocking mode", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
const { pid, port: actualPort } = await startDev_NonBlocking({
|
||||
req,
|
||||
projectId,
|
||||
projectPath,
|
||||
devScript,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Development server started",
|
||||
projectId,
|
||||
pid,
|
||||
port: actualPort,
|
||||
};
|
||||
} finally {
|
||||
// 无论成功、失败都清理锁
|
||||
removeStartingProject(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
export { startDevServer };
|
||||
428
qiming-file-server/src/utils/build/stopDevUtils.js
Normal file
428
qiming-file-server/src/utils/build/stopDevUtils.js
Normal file
@@ -0,0 +1,428 @@
|
||||
import { ValidationError, ProcessError } from "../error/errorHandler.js";
|
||||
import {
|
||||
killProcess,
|
||||
getRunningProcess,
|
||||
waitForProcessStop,
|
||||
} from "./processManager.js";
|
||||
import { log, getLogDir } from "../log/logUtils.js";
|
||||
import logCacheManager from "../log/logCacheManager.js";
|
||||
import portPool from "../buildArg/portPool.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
|
||||
|
||||
function findPidsByProjectId(projectId) {
|
||||
try {
|
||||
const cmd = "ps -Ao pid,command -ww";
|
||||
const output = execSync(cmd, { encoding: "utf8" });
|
||||
const lines = output.split("\n");
|
||||
const pids = [];
|
||||
const preciseNeedle = "/" + String(projectId);
|
||||
|
||||
// 匹配包含 /{projectId} 的路径片段,避免误匹配
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
if (line.includes(preciseNeedle)) {
|
||||
const trimmed = line.trim();
|
||||
const firstSpace = trimmed.indexOf(" ");
|
||||
const pidStr = firstSpace > 0 ? trimmed.slice(0, firstSpace) : trimmed;
|
||||
const pidNum = Number(pidStr);
|
||||
if (Number.isFinite(pidNum)) {
|
||||
pids.push(pidNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:使用宽松的包含 projectId 匹配(可能带来噪声)
|
||||
if (pids.length === 0) {
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
if (line.includes(String(projectId))) {
|
||||
const trimmed = line.trim();
|
||||
const firstSpace = trimmed.indexOf(" ");
|
||||
const pidStr = firstSpace > 0 ? trimmed.slice(0, firstSpace) : trimmed;
|
||||
const pidNum = Number(pidStr);
|
||||
if (Number.isFinite(pidNum)) {
|
||||
pids.push(pidNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(new Set(pids));
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止开发服务器
|
||||
* @param {Object} req 请求对象
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {number|string} pid 进程ID(可选,如果不提供会从运行表中获取)
|
||||
* @param {Object} options 选项
|
||||
* @param {boolean} options.strict 是否严格模式,严格模式下停止失败会抛出异常
|
||||
* @param {boolean} options.waitForStop 是否等待进程完全停止
|
||||
* @returns {Promise<Object>} 停止结果
|
||||
*
|
||||
* 功能说明:
|
||||
* 1. 确定要停止的进程ID
|
||||
* 2. 停止进程
|
||||
* 3. 等待进程完全停止(可选)
|
||||
* 4. 清理项目下的所有临时日志文件(dev-temp-*.log)
|
||||
*/
|
||||
async function stopDevServerByPid(req, projectId, pid, options = {}) {
|
||||
const { strict = true, waitForStop = false } = options;
|
||||
|
||||
log(projectId, "INFO", "Start stopping development server", {
|
||||
projectId,
|
||||
pid,
|
||||
strict,
|
||||
waitForStop,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
let pidToKill = null;
|
||||
let existingProcess = null;
|
||||
|
||||
// 1. 确定要停止的进程ID
|
||||
if (pid !== undefined && pid !== null) {
|
||||
const pidNum = Number(pid);
|
||||
if (Number.isFinite(pidNum)) {
|
||||
pidToKill = pidNum;
|
||||
log(projectId, "INFO", "Stop development server using incoming pid", {
|
||||
projectId,
|
||||
pid: pidToKill,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
} else {
|
||||
log(projectId, "WARN", "Incoming pid is invalid", {
|
||||
projectId,
|
||||
invalidPid: pid,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
if (strict) {
|
||||
throw new ValidationError("Process ID is invalid", { field: "pid", value: pid });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果传入的pid无效或未提供,从运行表中获取
|
||||
if (!pidToKill) {
|
||||
existingProcess = getRunningProcess(projectId);
|
||||
if (existingProcess) {
|
||||
pidToKill = existingProcess.pid;
|
||||
log(projectId, "INFO", "Get pid to stop development server from running table", {
|
||||
projectId,
|
||||
pid: pidToKill,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
} else {
|
||||
log(projectId, "INFO", "No development server process found to stop", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
if (strict) {
|
||||
throw new ProcessError("No running development server process found", { projectId });
|
||||
}
|
||||
|
||||
// 即使未找到进程,也尝试释放端口(可能进程已意外退出)
|
||||
portPool.release(String(projectId));
|
||||
log(projectId, "INFO", "Port released (process not running)", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "No running process found",
|
||||
projectId,
|
||||
pid: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 停止进程
|
||||
const killed = await killProcess(projectId, pidToKill);
|
||||
|
||||
if (killed) {
|
||||
log(projectId, "INFO", "Development server stopped", {
|
||||
projectId,
|
||||
pid: pidToKill,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
} else {
|
||||
log(projectId, "WARN", "Failed to stop development server", {
|
||||
projectId,
|
||||
pid: pidToKill,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
if (strict) {
|
||||
throw new ProcessError("Failed to stop process", { projectId, pid: pidToKill });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 等待进程完全停止(可选)
|
||||
if (waitForStop && killed) {
|
||||
const { stopped, attempts } = await waitForProcessStop(
|
||||
projectId,
|
||||
pidToKill
|
||||
);
|
||||
|
||||
if (!stopped) {
|
||||
log(projectId, "WARN", "Process stop timeout", {
|
||||
projectId,
|
||||
pid: pidToKill,
|
||||
attempts,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
if (strict) {
|
||||
throw new ProcessError("Process stop timeout", {
|
||||
projectId,
|
||||
pid: pidToKill,
|
||||
attempts,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log(projectId, "INFO", "Process confirmed stopped", {
|
||||
projectId,
|
||||
pid: pidToKill,
|
||||
attempts,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 清理项目下的所有临时日志文件
|
||||
try {
|
||||
const logDir = getLogDir(projectId);
|
||||
if (fs.existsSync(logDir)) {
|
||||
const files = fs.readdirSync(logDir);
|
||||
const tempFiles = files.filter(
|
||||
(file) => file.startsWith("dev-temp-") && file.endsWith(".log")
|
||||
);
|
||||
|
||||
if (tempFiles.length > 0) {
|
||||
let deletedCount = 0;
|
||||
for (const tempFile of tempFiles) {
|
||||
try {
|
||||
const tempFilePath = path.join(logDir, tempFile);
|
||||
fs.unlinkSync(tempFilePath);
|
||||
deletedCount++;
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "Failed to delete temporary log file", {
|
||||
projectId,
|
||||
tempFile,
|
||||
error: err.message,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log(projectId, "INFO", "Temporary log file cleanup completed", {
|
||||
projectId,
|
||||
deletedCount,
|
||||
totalTempFiles: tempFiles.length,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "Error cleaning temporary log files", {
|
||||
projectId,
|
||||
error: err.message,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 删除该项目的日志缓存
|
||||
try {
|
||||
if (logCacheManager.isEnabled()) {
|
||||
logCacheManager.delete(projectId);
|
||||
log(projectId, "INFO", "Log cache cleaned", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "Error cleaning log cache", {
|
||||
projectId,
|
||||
error: err.message,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 停止成功后释放端口
|
||||
if (killed) {
|
||||
portPool.release(String(projectId));
|
||||
log(projectId, "INFO", "Port released", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: killed ? "Stopped" : "Failed to stop but continue execution",
|
||||
projectId,
|
||||
pid: pidToKill,
|
||||
};
|
||||
}
|
||||
|
||||
async function stopDevServerByProjectId(req, projectId, options = {}) {
|
||||
const { strict = true, waitForStop = false } = options;
|
||||
|
||||
log(projectId, "INFO", "Start stopping development server(stop all by projectId)", {
|
||||
projectId,
|
||||
strict,
|
||||
waitForStop,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
// 忽略传入的 pid,按 projectId 通过系统进程检索
|
||||
const uniquePids = findPidsByProjectId(projectId);
|
||||
const candidates = uniquePids.map((pid) => ({ pid }));
|
||||
|
||||
if (!candidates || candidates.length === 0) {
|
||||
log(projectId, "INFO", "No development server process found to stop", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
// if (strict) {
|
||||
// throw new ProcessError("未找到运行中的开发服务器进程", { projectId });
|
||||
// }
|
||||
|
||||
// 即使未找到进程,也尝试释放端口(可能进程已意外退出)
|
||||
portPool.release(String(projectId));
|
||||
log(projectId, "INFO", "Port released (process not running)", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "No running process found",
|
||||
projectId,
|
||||
pid: null,
|
||||
};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const thePid of uniquePids) {
|
||||
const killed = await killProcess(projectId, thePid);
|
||||
results.push({ pid: thePid, killed });
|
||||
|
||||
if (waitForStop && killed) {
|
||||
const { stopped, attempts } = await waitForProcessStop(projectId, thePid);
|
||||
log(projectId, stopped ? "INFO" : "WARN", stopped ? "Process confirmed stopped" : "Process stop timeout", {
|
||||
projectId,
|
||||
pid: thePid,
|
||||
attempts,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
|
||||
if (!stopped && strict) {
|
||||
throw new ProcessError("Process stop timeout", {
|
||||
projectId,
|
||||
pid: thePid,
|
||||
attempts,
|
||||
});
|
||||
}
|
||||
} else if (!killed && strict) {
|
||||
throw new ProcessError("Failed to stop process", { projectId, pid: thePid });
|
||||
}
|
||||
}
|
||||
|
||||
// 清理项目下的所有临时日志文件
|
||||
try {
|
||||
const logDir = getLogDir(projectId);
|
||||
if (fs.existsSync(logDir)) {
|
||||
const files = fs.readdirSync(logDir);
|
||||
const tempFiles = files.filter(
|
||||
(file) => file.startsWith("dev-temp-") && file.endsWith(".log")
|
||||
);
|
||||
|
||||
if (tempFiles.length > 0) {
|
||||
let deletedCount = 0;
|
||||
for (const tempFile of tempFiles) {
|
||||
try {
|
||||
const tempFilePath = path.join(logDir, tempFile);
|
||||
fs.unlinkSync(tempFilePath);
|
||||
deletedCount++;
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "Failed to delete temporary log file", {
|
||||
projectId,
|
||||
tempFile,
|
||||
error: err.message,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log(projectId, "INFO", "Temporary log file cleanup completed", {
|
||||
projectId,
|
||||
deletedCount,
|
||||
totalTempFiles: tempFiles.length,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "Error cleaning temporary log files", {
|
||||
projectId,
|
||||
error: err.message,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除该项目的日志缓存
|
||||
try {
|
||||
if (logCacheManager.isEnabled()) {
|
||||
logCacheManager.delete(projectId);
|
||||
log(projectId, "INFO", "Log cache cleaned", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "Error cleaning log cache", {
|
||||
projectId,
|
||||
error: err.message,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
const allKilled = results.every((r) => r.killed === true);
|
||||
|
||||
// 停止成功后释放端口
|
||||
if (allKilled && results.length > 0) {
|
||||
portPool.release(String(projectId));
|
||||
log(projectId, "INFO", "Port released", {
|
||||
projectId,
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: allKilled ? "Stopped" : "Partially stopped but continue execution",
|
||||
projectId,
|
||||
pid: null,
|
||||
killedPids: results,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async function stopDevServer(req, projectId, pid, options = {}) {
|
||||
return await stopDevServerByProjectId(req, projectId, options);
|
||||
}
|
||||
|
||||
export { stopDevServer };
|
||||
847
qiming-file-server/src/utils/build/syntaxCheckUtils.js
Normal file
847
qiming-file-server/src/utils/build/syntaxCheckUtils.js
Normal file
@@ -0,0 +1,847 @@
|
||||
import { spawn } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { log } from "../log/logUtils.js";
|
||||
|
||||
/**
|
||||
* 在启动开发服务器前进行语法检查
|
||||
* 根据项目配置自动选择检查方式:
|
||||
* - TypeScript 项目:运行 tsc --noEmit
|
||||
* - 纯 JavaScript 项目:使用 esbuild 进行快速语法检查
|
||||
* - HTML 文件:使用 html-validate 进行 HTML 语法检查
|
||||
*
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {string} projectPath 项目路径
|
||||
* @param {number} timeoutMs 超时时间(毫秒)
|
||||
* @returns {Promise<Object>} { passed: boolean, error?: string, method?: string }
|
||||
*/
|
||||
async function runSyntaxCheck(projectId, projectPath, timeoutMs = 15000) {
|
||||
try {
|
||||
// 检查项目类型
|
||||
const projectType = detectProjectType(projectPath);
|
||||
|
||||
log(projectId, "INFO", "Start syntax check", {
|
||||
projectId,
|
||||
projectType,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
const results = [];
|
||||
|
||||
// 1. 代码检查(TypeScript/JavaScript)
|
||||
if (projectType === "typescript") {
|
||||
const tsResult = await runTypeScriptCheck(projectId, projectPath, timeoutMs);
|
||||
results.push(tsResult);
|
||||
|
||||
// 如果 TS 检查失败,立即返回
|
||||
if (!tsResult.passed) {
|
||||
return tsResult;
|
||||
}
|
||||
} else if (projectType === "javascript") {
|
||||
const jsResult = await runJavaScriptCheck(projectId, projectPath, timeoutMs);
|
||||
results.push(jsResult);
|
||||
|
||||
// 如果 JS 检查失败,立即返回
|
||||
if (!jsResult.passed) {
|
||||
return jsResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. HTML 文件检查
|
||||
const htmlResult = await runHtmlCheck(projectId, projectPath, timeoutMs);
|
||||
results.push(htmlResult);
|
||||
|
||||
// 如果 HTML 检查失败,返回失败
|
||||
if (!htmlResult.passed) {
|
||||
return htmlResult;
|
||||
}
|
||||
|
||||
// 所有检查都通过
|
||||
if (results.length === 0) {
|
||||
log(projectId, "INFO", "Skip syntax check: no source code files detected", {
|
||||
projectId,
|
||||
});
|
||||
return { passed: true, method: "skipped" };
|
||||
}
|
||||
|
||||
// 返回综合结果
|
||||
const methods = results.map(r => r.method).filter(Boolean).join(", ");
|
||||
const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
|
||||
|
||||
log(projectId, "INFO", "All syntax checks passed", {
|
||||
projectId,
|
||||
methods,
|
||||
totalDuration,
|
||||
});
|
||||
|
||||
return {
|
||||
passed: true,
|
||||
method: methods,
|
||||
duration: totalDuration,
|
||||
};
|
||||
} catch (error) {
|
||||
log(projectId, "WARN", "Syntax check execution failed", {
|
||||
projectId,
|
||||
error: error.message,
|
||||
});
|
||||
// 检查失败不阻止启动
|
||||
return { passed: true, method: "error", error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测项目类型
|
||||
* @param {string} projectPath 项目路径
|
||||
* @returns {string} "typescript" | "javascript" | "unknown"
|
||||
*/
|
||||
function detectProjectType(projectPath) {
|
||||
// 检查是否有 tsconfig.json
|
||||
const tsconfigPath = path.join(projectPath, "tsconfig.json");
|
||||
if (fs.existsSync(tsconfigPath)) {
|
||||
// 检查是否有 .ts 或 .tsx 文件
|
||||
const hasTsFiles = hasFilesWithExtension(projectPath, [".ts", ".tsx"]);
|
||||
if (hasTsFiles) {
|
||||
return "typescript";
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有 .js 或 .jsx 文件
|
||||
const hasJsFiles = hasFilesWithExtension(projectPath, [".js", ".jsx"]);
|
||||
if (hasJsFiles) {
|
||||
return "javascript";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查目录下是否存在指定扩展名的文件
|
||||
* @param {string} dir 目录路径
|
||||
* @param {Array<string>} extensions 扩展名列表
|
||||
* @param {number} maxDepth 最大搜索深度
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasFilesWithExtension(dir, extensions, maxDepth = 3) {
|
||||
try {
|
||||
// 排除的目录
|
||||
const excludeDirs = ["node_modules", ".git", "dist", "build", ".next", ".nuxt"];
|
||||
|
||||
function searchDir(currentDir, depth) {
|
||||
if (depth > maxDepth) return false;
|
||||
|
||||
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!excludeDirs.includes(entry.name)) {
|
||||
if (searchDir(fullPath, depth + 1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name);
|
||||
if (extensions.includes(ext)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return searchDir(dir, 0);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 TypeScript 类型检查
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {string} projectPath 项目路径
|
||||
* @param {number} timeoutMs 超时时间
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function runTypeScriptCheck(projectId, projectPath, timeoutMs) {
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const tsconfigPath = path.join(projectPath, "tsconfig.json");
|
||||
const checkConfigPath = path.join(projectPath, "tsconfig.check.json");
|
||||
let createdCheckConfig = false;
|
||||
let createdTempTsconfig = false;
|
||||
|
||||
// 创建专门用于语法检查的配置
|
||||
const createCheckConfig = (hasTsconfig) => {
|
||||
const config = {
|
||||
// 明确指定要检查的所有文件模式
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.js",
|
||||
"src/**/*.jsx",
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", "build", ".next", ".nuxt", "**/*.spec.ts", "**/*.test.ts"]
|
||||
};
|
||||
|
||||
// 如果用户有 tsconfig.json,继承它的 compilerOptions
|
||||
if (hasTsconfig) {
|
||||
config.extends = "./tsconfig.json";
|
||||
log(projectId, "INFO", "Check configuration will inherit user's tsconfig.json", {
|
||||
projectId,
|
||||
});
|
||||
} else {
|
||||
// 如果没有 tsconfig.json,需要提供完整的 compilerOptions
|
||||
config.compilerOptions = {
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const hasTsconfig = fs.existsSync(tsconfigPath);
|
||||
|
||||
// 始终创建 tsconfig.check.json 用于检查
|
||||
try {
|
||||
const checkConfig = createCheckConfig(hasTsconfig);
|
||||
fs.writeFileSync(checkConfigPath, JSON.stringify(checkConfig, null, 2), "utf8");
|
||||
createdCheckConfig = true;
|
||||
|
||||
log(projectId, "INFO", "Create temporary tsconfig.check.json for syntax check", {
|
||||
projectId,
|
||||
hasTsconfig,
|
||||
extends: checkConfig.extends || "无",
|
||||
include: checkConfig.include,
|
||||
});
|
||||
} catch (error) {
|
||||
log(projectId, "WARN", "Failed to create tsconfig.check.json", {
|
||||
projectId,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
// 降级方案:如果无法创建检查配置且没有 tsconfig.json,创建临时的
|
||||
if (!hasTsconfig) {
|
||||
try {
|
||||
fs.writeFileSync(tsconfigPath, JSON.stringify(createCheckConfig(false), null, 2), "utf8");
|
||||
createdTempTsconfig = true;
|
||||
log(projectId, "INFO", "Create temporary tsconfig.json as fallback", {
|
||||
projectId,
|
||||
});
|
||||
} catch (e) {
|
||||
log(projectId, "ERROR", "Failed to create any TypeScript configuration file", {
|
||||
projectId,
|
||||
error: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用项目本地的 tsc,如果不存在则使用 npx
|
||||
const localTscPath = path.join(projectPath, "node_modules", ".bin", "tsc");
|
||||
const usesLocalTsc = fs.existsSync(localTscPath);
|
||||
|
||||
const command = usesLocalTsc ? localTscPath : "npx";
|
||||
// 使用 --project 参数指定检查配置文件
|
||||
const configToUse = createdCheckConfig ? "tsconfig.check.json" : "tsconfig.json";
|
||||
const args = usesLocalTsc
|
||||
? ["--project", configToUse, "--noEmit", "--skipLibCheck"]
|
||||
: ["--yes", "tsc", "--project", configToUse, "--noEmit", "--skipLibCheck"];
|
||||
|
||||
log(projectId, "INFO", "Run TypeScript type check", {
|
||||
projectId,
|
||||
command,
|
||||
args: args.join(" "),
|
||||
usesLocalTsc,
|
||||
configFile: configToUse,
|
||||
});
|
||||
|
||||
const child = spawn(command, args, {
|
||||
cwd: projectPath,
|
||||
shell: true,
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout?.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
// 清理临时文件的辅助函数
|
||||
const cleanupTempConfig = () => {
|
||||
// 清理检查配置文件
|
||||
if (createdCheckConfig) {
|
||||
try {
|
||||
if (fs.existsSync(checkConfigPath)) {
|
||||
fs.unlinkSync(checkConfigPath);
|
||||
log(projectId, "INFO", "Cleaned temporary tsconfig.check.json", {
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log(projectId, "WARN", "Failed to clean tsconfig.check.json", {
|
||||
projectId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 清理临时创建的 tsconfig.json(降级方案)
|
||||
if (createdTempTsconfig) {
|
||||
try {
|
||||
if (fs.existsSync(tsconfigPath)) {
|
||||
fs.unlinkSync(tsconfigPath);
|
||||
log(projectId, "INFO", "Cleaned temporary tsconfig.json", {
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log(projectId, "WARN", "Failed to clean tsconfig.json", {
|
||||
projectId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
child.on("close", (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// 清理临时配置
|
||||
cleanupTempConfig();
|
||||
|
||||
if (code === 0) {
|
||||
log(projectId, "INFO", "TypeScript type check passed", {
|
||||
projectId,
|
||||
duration,
|
||||
});
|
||||
resolve({ passed: true, method: "typescript", duration });
|
||||
} else {
|
||||
// 提取错误信息
|
||||
const errorOutput = stderr || stdout;
|
||||
const errorSummary = extractTypeScriptErrors(errorOutput);
|
||||
|
||||
log(projectId, "ERROR", "TypeScript type check failed", {
|
||||
projectId,
|
||||
code,
|
||||
duration,
|
||||
errorSummary,
|
||||
});
|
||||
|
||||
resolve({
|
||||
passed: false,
|
||||
method: "typescript",
|
||||
error: errorSummary,
|
||||
fullOutput: errorOutput.substring(0, 2000), // 限制长度
|
||||
duration,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
// 清理临时配置
|
||||
cleanupTempConfig();
|
||||
|
||||
log(projectId, "WARN", "TypeScript check execution failed", {
|
||||
projectId,
|
||||
error: error.message,
|
||||
});
|
||||
// 执行失败不阻止启动
|
||||
resolve({ passed: true, method: "typescript-error", error: error.message });
|
||||
});
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
try {
|
||||
child.kill();
|
||||
// 清理临时配置
|
||||
cleanupTempConfig();
|
||||
|
||||
log(projectId, "WARN", "TypeScript check timeout", {
|
||||
projectId,
|
||||
timeoutMs,
|
||||
});
|
||||
resolve({ passed: true, method: "typescript-timeout" });
|
||||
} catch (e) {
|
||||
cleanupTempConfig();
|
||||
resolve({ passed: true, method: "typescript-timeout-error" });
|
||||
}
|
||||
}, timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 JavaScript 语法检查(使用 esbuild)
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {string} projectPath 项目路径
|
||||
* @param {number} timeoutMs 超时时间
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function runJavaScriptCheck(projectId, projectPath, timeoutMs) {
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 查找入口文件
|
||||
const entryFiles = findEntryFiles(projectPath);
|
||||
|
||||
if (entryFiles.length === 0) {
|
||||
log(projectId, "INFO", "Skip JavaScript syntax check: no entry files found", {
|
||||
projectId,
|
||||
});
|
||||
resolve({ passed: true, method: "javascript-skipped" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 esbuild 进行快速语法检查(不输出文件)
|
||||
const args = [
|
||||
"--yes",
|
||||
"esbuild",
|
||||
...entryFiles,
|
||||
"--bundle",
|
||||
"--write=false",
|
||||
"--outdir=/tmp",
|
||||
"--format=esm",
|
||||
];
|
||||
|
||||
log(projectId, "INFO", "Run JavaScript syntax check", {
|
||||
projectId,
|
||||
entryFiles,
|
||||
});
|
||||
|
||||
const child = spawn("npx", args, {
|
||||
cwd: projectPath,
|
||||
shell: true,
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout?.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (code === 0) {
|
||||
log(projectId, "INFO", "JavaScript syntax check passed", {
|
||||
projectId,
|
||||
duration,
|
||||
});
|
||||
resolve({ passed: true, method: "javascript", duration });
|
||||
} else {
|
||||
const errorOutput = stderr || stdout;
|
||||
const errorSummary = errorOutput.substring(0, 1000);
|
||||
|
||||
log(projectId, "ERROR", "JavaScript syntax check failed", {
|
||||
projectId,
|
||||
code,
|
||||
duration,
|
||||
errorSummary,
|
||||
});
|
||||
|
||||
resolve({
|
||||
passed: false,
|
||||
method: "javascript",
|
||||
error: errorSummary,
|
||||
fullOutput: errorOutput.substring(0, 2000),
|
||||
duration,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
log(projectId, "WARN", "JavaScript check execution failed", {
|
||||
projectId,
|
||||
error: error.message,
|
||||
});
|
||||
resolve({ passed: true, method: "javascript-error", error: error.message });
|
||||
});
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
try {
|
||||
child.kill();
|
||||
log(projectId, "WARN", "JavaScript check timeout", {
|
||||
projectId,
|
||||
timeoutMs,
|
||||
});
|
||||
resolve({ passed: true, method: "javascript-timeout" });
|
||||
} catch (e) {
|
||||
resolve({ passed: true, method: "javascript-timeout-error" });
|
||||
}
|
||||
}, timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找入口文件
|
||||
* @param {string} projectPath 项目路径
|
||||
* @returns {Array<string>} 入口文件路径列表
|
||||
*/
|
||||
function findEntryFiles(projectPath) {
|
||||
const possibleEntries = [
|
||||
"src/main.js",
|
||||
"src/main.jsx",
|
||||
"src/main.ts",
|
||||
"src/main.tsx",
|
||||
"src/index.js",
|
||||
"src/index.jsx",
|
||||
"src/index.ts",
|
||||
"src/index.tsx",
|
||||
"src/app.js",
|
||||
"src/app.jsx",
|
||||
"src/App.js",
|
||||
"src/App.jsx",
|
||||
"index.js",
|
||||
"index.jsx",
|
||||
"index.ts",
|
||||
"index.tsx",
|
||||
];
|
||||
|
||||
const entries = [];
|
||||
for (const entry of possibleEntries) {
|
||||
const fullPath = path.join(projectPath, entry);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保项目有 HTML 验证配置文件(如果不存在则创建宽松配置)
|
||||
* @param {string} projectPath 项目路径
|
||||
* @returns {boolean} 是否创建了新配置
|
||||
*/
|
||||
function ensureHtmlValidateConfig(projectPath) {
|
||||
const configPath = path.join(projectPath, ".htmlvalidate.json");
|
||||
|
||||
// 如果已经存在配置文件,不覆盖
|
||||
if (fs.existsSync(configPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建宽松的默认配置
|
||||
const defaultConfig = {
|
||||
"extends": ["html-validate:recommended"],
|
||||
"rules": {
|
||||
// 允许 void elements 不自闭合(HTML5 标准)
|
||||
"void-style": "off",
|
||||
// 允许不需要 SRI
|
||||
"require-sri": "off",
|
||||
// 允许尾部空白
|
||||
"no-trailing-whitespace": "off",
|
||||
// 允许内联样式(开发时常用)
|
||||
"no-inline-style": "off",
|
||||
// 允许重复的 ID(某些框架会动态处理)
|
||||
"no-dup-id": "warn",
|
||||
// 允许未使用的 disable 指令
|
||||
"no-unused-disable": "off"
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), "utf8");
|
||||
return true;
|
||||
} catch (error) {
|
||||
// 创建失败不影响检查,使用默认规则
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 HTML 语法检查
|
||||
* @param {string} projectId 项目ID
|
||||
* @param {string} projectPath 项目路径
|
||||
* @param {number} timeoutMs 超时时间
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function runHtmlCheck(projectId, projectPath, timeoutMs) {
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 查找 HTML 文件
|
||||
const htmlFiles = findHtmlFiles(projectPath);
|
||||
|
||||
if (htmlFiles.length === 0) {
|
||||
log(projectId, "INFO", "Skip HTML syntax check: no HTML files found", {
|
||||
projectId,
|
||||
});
|
||||
resolve({ passed: true, method: "html-skipped" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保有宽松的验证配置
|
||||
const configCreated = ensureHtmlValidateConfig(projectPath);
|
||||
if (configCreated) {
|
||||
log(projectId, "INFO", "Automatically create宽松的 HTML 验证配置", {
|
||||
projectId,
|
||||
configPath: ".htmlvalidate.json",
|
||||
});
|
||||
}
|
||||
|
||||
// 使用 html-validate 进行 HTML 语法检查
|
||||
const args = [
|
||||
"--yes",
|
||||
"html-validate",
|
||||
...htmlFiles,
|
||||
"--formatter=text",
|
||||
];
|
||||
|
||||
log(projectId, "INFO", "Run HTML syntax check", {
|
||||
projectId,
|
||||
htmlFiles,
|
||||
fileCount: htmlFiles.length,
|
||||
});
|
||||
|
||||
const child = spawn("npx", args, {
|
||||
cwd: projectPath,
|
||||
shell: true,
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout?.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (code === 0) {
|
||||
log(projectId, "INFO", "HTML syntax check passed", {
|
||||
projectId,
|
||||
duration,
|
||||
fileCount: htmlFiles.length,
|
||||
});
|
||||
resolve({ passed: true, method: "html", duration });
|
||||
} else {
|
||||
const errorOutput = stdout || stderr;
|
||||
const errorSummary = extractHtmlErrors(errorOutput);
|
||||
|
||||
log(projectId, "ERROR", "HTML syntax check failed", {
|
||||
projectId,
|
||||
code,
|
||||
duration,
|
||||
errorSummary,
|
||||
});
|
||||
|
||||
resolve({
|
||||
passed: false,
|
||||
method: "html",
|
||||
error: errorSummary,
|
||||
fullOutput: errorOutput.substring(0, 2000),
|
||||
duration,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
log(projectId, "WARN", "HTML check execution failed", {
|
||||
projectId,
|
||||
error: error.message,
|
||||
});
|
||||
// 执行失败不阻止启动
|
||||
resolve({ passed: true, method: "html-error", error: error.message });
|
||||
});
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
try {
|
||||
child.kill();
|
||||
log(projectId, "WARN", "HTML check timeout", {
|
||||
projectId,
|
||||
timeoutMs,
|
||||
});
|
||||
resolve({ passed: true, method: "html-timeout" });
|
||||
} catch (e) {
|
||||
resolve({ passed: true, method: "html-timeout-error" });
|
||||
}
|
||||
}, timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找 HTML 文件
|
||||
* @param {string} projectPath 项目路径
|
||||
* @returns {Array<string>} HTML 文件相对路径列表
|
||||
*/
|
||||
function findHtmlFiles(projectPath) {
|
||||
const htmlFiles = [];
|
||||
|
||||
// 常见的 HTML 文件位置
|
||||
const possibleLocations = [
|
||||
"index.html",
|
||||
"public/index.html",
|
||||
"src/index.html",
|
||||
"dist/index.html",
|
||||
];
|
||||
|
||||
// 检查常见位置
|
||||
for (const location of possibleLocations) {
|
||||
const fullPath = path.join(projectPath, location);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
htmlFiles.push(location);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到,递归搜索 public 和 src 目录
|
||||
if (htmlFiles.length === 0) {
|
||||
const dirsToSearch = ["public", "src", "."];
|
||||
|
||||
for (const dir of dirsToSearch) {
|
||||
const dirPath = path.join(projectPath, dir);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
const found = searchHtmlFilesInDir(dirPath, projectPath, 2);
|
||||
htmlFiles.push(...found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重
|
||||
return [...new Set(htmlFiles)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归搜索目录中的 HTML 文件
|
||||
* @param {string} dir 要搜索的目录
|
||||
* @param {string} basePath 基础路径(用于生成相对路径)
|
||||
* @param {number} maxDepth 最大搜索深度
|
||||
* @returns {Array<string>} HTML 文件相对路径列表
|
||||
*/
|
||||
function searchHtmlFilesInDir(dir, basePath, maxDepth = 2) {
|
||||
const htmlFiles = [];
|
||||
const excludeDirs = ["node_modules", ".git", "dist", "build", ".next", ".nuxt"];
|
||||
|
||||
function search(currentDir, depth) {
|
||||
if (depth > maxDepth) return;
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!excludeDirs.includes(entry.name)) {
|
||||
search(fullPath, depth + 1);
|
||||
}
|
||||
} else if (entry.isFile() && entry.name.endsWith(".html")) {
|
||||
// 生成相对路径
|
||||
const relativePath = path.relative(basePath, fullPath);
|
||||
htmlFiles.push(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略读取错误
|
||||
}
|
||||
}
|
||||
|
||||
search(dir, 0);
|
||||
return htmlFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 HTML 错误输出中提取关键错误信息
|
||||
* @param {string} output 错误输出
|
||||
* @returns {string} 错误摘要
|
||||
*/
|
||||
function extractHtmlErrors(output) {
|
||||
if (!output) return "Unknown error";
|
||||
|
||||
const lines = output.split("\n");
|
||||
const errorLines = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// 提取 html-validate 的错误行
|
||||
// 格式: "error: ..." 或 "✖ ..." 或包含文件路径和行号
|
||||
if (
|
||||
trimmed.includes("error:") ||
|
||||
trimmed.includes("✖") ||
|
||||
/\.html:\d+:\d+/.test(trimmed) ||
|
||||
trimmed.includes("Element") ||
|
||||
trimmed.includes("Attribute")
|
||||
) {
|
||||
errorLines.push(trimmed);
|
||||
if (errorLines.length >= 10) break; // 保留前10个错误
|
||||
}
|
||||
}
|
||||
|
||||
if (errorLines.length > 0) {
|
||||
return errorLines.join("\n");
|
||||
}
|
||||
|
||||
// 如果没有找到特定格式的错误,返回前几行
|
||||
return lines.slice(0, 15).join("\n").trim().substring(0, 800);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 TypeScript 错误输出中提取关键错误信息
|
||||
* @param {string} output 错误输出
|
||||
* @returns {string} 错误摘要
|
||||
*/
|
||||
function extractTypeScriptErrors(output) {
|
||||
if (!output) return "Unknown error";
|
||||
|
||||
const lines = output.split("\n");
|
||||
const errorLines = [];
|
||||
|
||||
for (const line of lines) {
|
||||
// 提取错误行(包含文件路径和错误信息)
|
||||
if (line.includes("error TS") || line.includes("): error")) {
|
||||
errorLines.push(line.trim());
|
||||
if (errorLines.length >= 5) break; // 只保留前5个错误
|
||||
}
|
||||
}
|
||||
|
||||
if (errorLines.length > 0) {
|
||||
return errorLines.join("\n");
|
||||
}
|
||||
|
||||
// 如果没有找到特定格式的错误,返回前几行
|
||||
return lines.slice(0, 10).join("\n").trim().substring(0, 500);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runSyntaxCheck,
|
||||
detectProjectType,
|
||||
findHtmlFiles,
|
||||
ensureHtmlValidateConfig,
|
||||
};
|
||||
|
||||
2
qiming-mcp-proxy/.cargo/config.toml
Normal file
2
qiming-mcp-proxy/.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[env]
|
||||
CMAKE_CUDA_ARCHITECTURES = "60-real;61-real;62-real;70-real;75-real;80-real;86-real"
|
||||
38
qiming-mcp-proxy/.devcontainer/devcontainer.json
Normal file
38
qiming-mcp-proxy/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,38 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
|
||||
{
|
||||
"name": "Rust",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": {},
|
||||
"ghcr.io/devcontainers-community/features/deno:1": {},
|
||||
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
||||
"ghcr.io/devcontainers-extra/features/curl-apt-get:1": {},
|
||||
"ghcr.io/devcontainers-extra/features/wget-apt-get:1": {}
|
||||
}
|
||||
|
||||
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
|
||||
// "mounts": [
|
||||
// {
|
||||
// "source": "devcontainer-cargo-cache-${devcontainerId}",
|
||||
// "target": "/usr/local/cargo",
|
||||
// "type": "volume"
|
||||
// }
|
||||
// ]
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "rustc --version",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
74
qiming-mcp-proxy/.dockerignore
Normal file
74
qiming-mcp-proxy/.dockerignore
Normal file
@@ -0,0 +1,74 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Build artifacts
|
||||
target/
|
||||
dist/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Temp files
|
||||
temp/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Cargo
|
||||
.cargo/
|
||||
|
||||
# Docs
|
||||
*.md
|
||||
README*
|
||||
CHANGELOG*
|
||||
LICENSE*
|
||||
LICENSE-*
|
||||
|
||||
# Config
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# Tests
|
||||
**/tests/
|
||||
benches/
|
||||
examples/
|
||||
fixtures/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
Thumb.db
|
||||
|
||||
# Project specific
|
||||
data/
|
||||
assets/
|
||||
scripts/
|
||||
spec/
|
||||
rmcp_code/
|
||||
.cursor/
|
||||
.devcontainer/
|
||||
.kiro/
|
||||
.trae/
|
||||
|
||||
# CI/CD
|
||||
.github/workflows/
|
||||
|
||||
# Config files
|
||||
.pre-commit-config.yaml
|
||||
_typos.toml
|
||||
cliff.toml
|
||||
deny.toml
|
||||
|
||||
# Keep essential files
|
||||
!Cargo.toml
|
||||
!Cargo.lock
|
||||
479
qiming-mcp-proxy/.github/workflows/release.yml
vendored
Normal file
479
qiming-mcp-proxy/.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,479 @@
|
||||
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
# CI that:
|
||||
#
|
||||
# * checks for a Git Tag that looks like a release
|
||||
# * builds artifacts with dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
#
|
||||
# Note that the GitHub Release will be created with a generated
|
||||
# title/body based on your changelogs.
|
||||
|
||||
name: Release
|
||||
permissions:
|
||||
"contents": "write"
|
||||
|
||||
# This task will run whenever you push a git tag that looks like a version
|
||||
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||
#
|
||||
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (dist-able) packages in the workspace with that version (this mode is
|
||||
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||
# packages versioned/released in lockstep).
|
||||
#
|
||||
# If you push multiple tags at once, separate instances of this workflow will
|
||||
# spin up, creating an independent announcement for each one. However, GitHub
|
||||
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||
# mistake.
|
||||
#
|
||||
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||
# will be marked as a prerelease.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||
publishing: ${{ !github.event.pull_request }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
# sure would be cool if github gave us proper conditionals...
|
||||
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
run: |
|
||||
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
|
||||
# Build and packages all the platform-specific things
|
||||
build-local-artifacts:
|
||||
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||
# Let the initial task tell us to not run (currently very blunt)
|
||||
needs:
|
||||
- plan
|
||||
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Target platforms/runners are computed by dist in create-release.
|
||||
# Each member of the matrix has the following arguments:
|
||||
#
|
||||
# - runner: the github runner
|
||||
# - dist-args: cli flags to pass to dist
|
||||
# - install-dist: expression to run to install dist on the runner
|
||||
#
|
||||
# Typically there will be:
|
||||
# - 1 "global" task that builds universal installers
|
||||
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
container: ${{ matrix.container && matrix.container.image || null }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||
steps:
|
||||
- name: enable windows longpaths
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install Rust non-interactively if not already installed
|
||||
if: ${{ matrix.container }}
|
||||
run: |
|
||||
if ! command -v cargo > /dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
fi
|
||||
- name: Install dist
|
||||
run: ${{ matrix.install_dist.run }}
|
||||
# Get the dist-manifest
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
${{ matrix.packages_install }}
|
||||
- name: Build artifacts
|
||||
run: |
|
||||
# Actually do builds and make zips and whatnot
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
- id: cargo-dist
|
||||
name: Post-build
|
||||
# We force bash here just because github makes it really hard to get values up
|
||||
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||
# inconsistent syntax between shell and powershell.
|
||||
shell: bash
|
||||
run: |
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
|
||||
# Build and package all the platform-agnostic(ish) things
|
||||
build-global-artifacts:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
# Determines if we should publish/announce
|
||||
host:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
- build-global-artifacts
|
||||
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
path: dist-manifest.json
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||
RELEASE_COMMIT: "${{ github.sha }}"
|
||||
run: |
|
||||
# Write and read notes from a file to avoid quoting breaking things
|
||||
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
|
||||
sync-to-oss:
|
||||
name: Sync release assets to Alibaba Cloud OSS
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
runs-on: "ubuntu-22.04"
|
||||
# Only run during actual release, not in PR mode
|
||||
if: ${{ needs.plan.outputs.publishing == 'true' }}
|
||||
continue-on-error: true
|
||||
outputs:
|
||||
oss_upload_success: ${{ steps.verify.outputs.oss_upload_success }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ needs.plan.outputs.tag }}
|
||||
steps:
|
||||
- name: Install ossutil v2
|
||||
run: |
|
||||
curl -fSL -o /tmp/ossutil.zip \
|
||||
"https://gosspublic.alicdn.com/ossutil/v2/2.2.0/ossutil-2.2.0-linux-amd64.zip"
|
||||
unzip -q /tmp/ossutil.zip -d /tmp
|
||||
sudo mv /tmp/ossutil-2.2.0-linux-amd64/ossutil /usr/local/bin/
|
||||
ossutil version
|
||||
|
||||
- name: Download release assets
|
||||
run: |
|
||||
mkdir -p /tmp/release-assets
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir /tmp/release-assets
|
||||
echo "==> Downloaded assets:"
|
||||
ls -lh /tmp/release-assets/
|
||||
|
||||
- name: Upload assets to OSS
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
run: |
|
||||
OSS_ENDPOINT="https://oss-rg-china-mainland.aliyuncs.com"
|
||||
OSS_REGION="rg-china-mainland"
|
||||
OSS_BUCKET="oss://nuwa-packages"
|
||||
OSS_PREFIX="mcp-stdio-proxy/${TAG}"
|
||||
|
||||
for file in /tmp/release-assets/*.tar.xz /tmp/release-assets/*.zip; do
|
||||
if [ -f "$file" ]; then
|
||||
filename=$(basename "$file")
|
||||
echo "Uploading: ${filename}"
|
||||
ossutil cp --force "$file" "${OSS_BUCKET}/${OSS_PREFIX}/${filename}" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Verify OSS upload
|
||||
id: verify
|
||||
run: |
|
||||
VERIFY_URL="https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/mcp-stdio-proxy/${TAG}/mcp-stdio-proxy-x86_64-apple-darwin.tar.xz"
|
||||
HTTP_STATUS=$(curl -sS -o /tmp/verify.txt -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> OSS verification passed (HTTP ${HTTP_STATUS})"
|
||||
echo "oss_upload_success=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::OSS verification failed: ${VERIFY_URL} returned HTTP ${HTTP_STATUS}"
|
||||
echo "oss_upload_success=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
modify-npm-packages:
|
||||
name: Modify npm package download URLs
|
||||
needs:
|
||||
- plan
|
||||
- sync-to-oss
|
||||
runs-on: "ubuntu-22.04"
|
||||
# Only run during publishing
|
||||
if: ${{ needs.plan.outputs.publishing == 'true' }}
|
||||
outputs:
|
||||
modified: ${{ steps.modify.outputs.modified }}
|
||||
env:
|
||||
PLAN: ${{ needs.plan.outputs.val }}
|
||||
TAG: ${{ needs.plan.outputs.tag }}
|
||||
OSS_SYNC_SUCCESS: ${{ needs.sync-to-oss.result == 'success' && needs.sync-to-oss.outputs.oss_upload_success == 'true' }}
|
||||
steps:
|
||||
- name: Fetch npm packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: npm/
|
||||
merge-multiple: true
|
||||
|
||||
- id: modify
|
||||
name: Modify npm package URLs
|
||||
run: |
|
||||
GITHUB_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}"
|
||||
OSS_URL="https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/mcp-stdio-proxy/${TAG}"
|
||||
|
||||
# 检查 OSS 同步是否成功
|
||||
if [ "$OSS_SYNC_SUCCESS" != "true" ]; then
|
||||
echo "==> OSS sync failed or was skipped, using original packages"
|
||||
echo "modified=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> OSS sync successful, modifying npm packages to use OSS URLs"
|
||||
echo "modified=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# 查找 npm 包并处理
|
||||
for release in $(echo "$PLAN" | jq --compact-output '.releases[]'); do
|
||||
# 从 release 对象中提取 artifacts 数组,并查找 npm 包
|
||||
pkg=$(echo "$release" | jq -r '.artifacts[] | select(. | test("-npm-package\\.tar\\.gz$"))')
|
||||
|
||||
if [ -n "$pkg" ]; then
|
||||
echo "==> Processing: $pkg"
|
||||
|
||||
# 创建临时目录
|
||||
mkdir -p temp-workspace
|
||||
|
||||
# 解压 npm 包
|
||||
tar -xzf "npm/${pkg}" -C temp-workspace
|
||||
|
||||
# 修改 package.json 中的 artifactDownloadUrl
|
||||
sed -i "s|\"artifactDownloadUrl\": \"${GITHUB_URL}\"|\"artifactDownloadUrl\": \"${OSS_URL}\"|g" temp-workspace/package/package.json
|
||||
|
||||
# 验证修改
|
||||
echo "==> Modified artifactDownloadUrl:"
|
||||
grep -A1 "artifactDownloadUrl" temp-workspace/package/package.json || true
|
||||
|
||||
# 重新打包
|
||||
tar -czf "npm/${pkg}" -C temp-workspace package
|
||||
rm -rf temp-workspace
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload modified npm packages
|
||||
if: ${{ steps.modify.outputs.modified == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifacts-npm-packages
|
||||
path: npm/*-npm-package.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
publish-npm:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
- modify-npm-packages
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PLAN: ${{ needs.plan.outputs.val }}
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
steps:
|
||||
# Try to fetch modified npm packages (with OSS URLs)
|
||||
- name: Fetch modified npm packages
|
||||
id: fetch-modified
|
||||
uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: artifacts-npm-packages
|
||||
path: npm/
|
||||
|
||||
# If modified packages don't exist, fetch original packages
|
||||
- name: Fetch original npm packages
|
||||
if: ${{ steps.fetch-modified.outcome == 'failure' || steps.fetch-modified.outcome == 'skipped' || needs.modify-npm-packages.outputs.modified != 'true' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: npm/
|
||||
merge-multiple: true
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- run: |
|
||||
for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith("-npm-package.tar.gz")] | any)'); do
|
||||
pkg=$(echo "$release" | jq '.artifacts[] | select(endswith("-npm-package.tar.gz"))' --raw-output)
|
||||
npm publish --access public "./npm/${pkg}"
|
||||
done
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
- publish-npm
|
||||
# sync-to-oss 和 modify-npm-packages 不需要等待(允许失败)
|
||||
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||
# "host" however must run to completion, no skipping allowed!
|
||||
if: ${{ always() && needs.host.result == 'success' && (needs.publish-npm.result == 'skipped' || needs.publish-npm.result == 'success') }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
53
qiming-mcp-proxy/.gitignore
vendored
Normal file
53
qiming-mcp-proxy/.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
/target
|
||||
.idea
|
||||
.vscode
|
||||
target
|
||||
.DS_Store
|
||||
**/target
|
||||
mcp-proxy/logs
|
||||
logs
|
||||
.venv
|
||||
.claude
|
||||
/tmp
|
||||
packages
|
||||
|
||||
|
||||
document-parser/data/*
|
||||
scripts/temp/*
|
||||
data/*
|
||||
temp/*
|
||||
server.log
|
||||
:memory:/*
|
||||
document-parser/:memory:/*
|
||||
|
||||
document-parser/temp/*
|
||||
models/*
|
||||
dist/
|
||||
voice-cli/temp/*
|
||||
voice-cli/cluster_metadata/*
|
||||
voice-cli/shared-voice-cli/*
|
||||
voice-cli/:memory:/*
|
||||
|
||||
cluster_metadata
|
||||
*.pid
|
||||
|
||||
voice-cli/build/*
|
||||
voice-cli/data/*
|
||||
voice-cli/server-config.yml
|
||||
voice-cli/server_debug.log
|
||||
voice-cli/test_audio.txt
|
||||
voice-cli/checkpoints/*
|
||||
mcp-proxy/tmp/*
|
||||
fastembed/.fastembed_cache
|
||||
.fastembed_cache/
|
||||
voice-cli/models/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Internal development tools and docs
|
||||
.qoder/
|
||||
spec/
|
||||
mcp-proxy/docs/
|
||||
61
qiming-mcp-proxy/.pre-commit-config.yaml
Normal file
61
qiming-mcp-proxy/.pre-commit-config.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
fail_fast: false
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-byte-order-marker
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
- id: check-symlinks
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-fmt
|
||||
name: cargo fmt
|
||||
description: Format files with rustfmt.
|
||||
entry: bash -c 'cargo fmt -- --check'
|
||||
language: rust
|
||||
files: \.rs$
|
||||
args: []
|
||||
- id: cargo-deny
|
||||
name: cargo deny check
|
||||
description: Check cargo dependencies
|
||||
entry: bash -c 'cargo deny check -d'
|
||||
language: rust
|
||||
files: \.rs$
|
||||
args: []
|
||||
- id: typos
|
||||
name: typos
|
||||
description: check typo
|
||||
entry: bash -c 'typos'
|
||||
language: rust
|
||||
files: \.*$
|
||||
pass_filenames: false
|
||||
- id: cargo-check
|
||||
name: cargo check
|
||||
description: Check the package for errors.
|
||||
entry: bash -c 'cargo check --all'
|
||||
language: rust
|
||||
files: \.rs$
|
||||
pass_filenames: false
|
||||
- id: cargo-clippy
|
||||
name: cargo clippy
|
||||
description: Lint rust sources
|
||||
entry: bash -c 'cargo clippy --all-targets --all-features --tests --benches -- -D warnings'
|
||||
language: rust
|
||||
files: \.rs$
|
||||
pass_filenames: false
|
||||
- id: cargo-test
|
||||
name: cargo test
|
||||
description: unit test for the project
|
||||
entry: bash -c 'cargo nextest run --all-features'
|
||||
language: rust
|
||||
files: \.rs$
|
||||
pass_filenames: false
|
||||
175
qiming-mcp-proxy/CARGO_DIST_SETUP.md
Normal file
175
qiming-mcp-proxy/CARGO_DIST_SETUP.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# cargo-dist 配置完成总结
|
||||
|
||||
## ✅ 已完成的配置
|
||||
|
||||
### 1. 初始化 cargo-dist
|
||||
|
||||
使用官方命令初始化:
|
||||
```bash
|
||||
dist init --yes
|
||||
```
|
||||
|
||||
这会自动创建:
|
||||
- `[profile.dist]` 到 `Cargo.toml`(工作区级别)
|
||||
- `dist-workspace.toml` 配置文件
|
||||
- `.github/workflows/release.yml` GitHub Actions 工作流
|
||||
|
||||
### 2. 配置文件
|
||||
|
||||
#### `dist-workspace.toml`
|
||||
```toml
|
||||
[workspace]
|
||||
members = ["cargo:."]
|
||||
|
||||
[dist]
|
||||
cargo-dist-version = "0.30.3"
|
||||
ci = "github"
|
||||
installers = ["shell", "powershell"]
|
||||
targets = [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-pc-windows-msvc"
|
||||
]
|
||||
```
|
||||
|
||||
#### 各子项目的 `Cargo.toml` 添加
|
||||
```toml
|
||||
[package]
|
||||
repository = "https://github.com/nuwax-ai/mcp-proxy"
|
||||
```
|
||||
|
||||
已更新:
|
||||
- ✅ `mcp-proxy/Cargo.toml` (已有 repository)
|
||||
- ✅ `document-parser/Cargo.toml`
|
||||
- ✅ `voice-cli/Cargo.toml`
|
||||
|
||||
### 3. GitHub Actions 工作流
|
||||
|
||||
文件:`.github/workflows/release.yml`
|
||||
|
||||
自动执行:
|
||||
1. **Plan** - 检测到 git tag 时,计算需要构建的内容
|
||||
2. **Build** - 为每个目标平台构建二进制文件
|
||||
3. **Host** - 创建 GitHub Release 并上传所有产物
|
||||
|
||||
### 4. 产物格式
|
||||
|
||||
每次发布会生成:
|
||||
|
||||
#### 每个 二进制文件
|
||||
- `{name}-{target}.tar.xz` (Linux/macOS)
|
||||
- `{name}-{target}.zip` (Windows)
|
||||
- 对应的 SHA256 校验和
|
||||
|
||||
#### 全局产物
|
||||
- `{name}-installer.sh` - Shell 安装脚本 (Linux/macOS)
|
||||
- `{name}-installer.ps1` - PowerShell 安装脚本 (Windows)
|
||||
- `sha256.sum` - 所有文件的校验和
|
||||
- `source.tar.gz` - 源码压缩包
|
||||
|
||||
## 🚀 发布流程
|
||||
|
||||
### 1. 更新版本号
|
||||
```bash
|
||||
# 更新各子项目的 version
|
||||
vim mcp-proxy/Cargo.toml
|
||||
vim document-parser/Cargo.toml
|
||||
vim voice-cli/Cargo.toml
|
||||
```
|
||||
|
||||
### 2. 提交并打标签
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "release: v0.2.0"
|
||||
git tag v0.2.0
|
||||
git push
|
||||
git push --tags
|
||||
```
|
||||
|
||||
### 3. 自动触发 GitHub Actions
|
||||
推送 tag 后,GitHub Actions 自动:
|
||||
- ✅ 构建所有平台
|
||||
- ✅ 生成安装脚本
|
||||
- ✅ 创建 Release
|
||||
- ✅ 上传产物
|
||||
|
||||
## 📥 用户安装方式
|
||||
|
||||
### 最简单(推荐)
|
||||
```bash
|
||||
# Linux/macOS
|
||||
curl --proto '=https' --tlsv1.2 -sSf \
|
||||
https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/mcp-stdio-proxy-installer.sh | sh
|
||||
|
||||
# Windows PowerShell
|
||||
irm https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/mcp-stdio-proxy-installer.ps1 | iex
|
||||
```
|
||||
|
||||
### 从 Releases 页面下载
|
||||
访问:https://github.com/nuwax-ai/mcp-proxy/releases
|
||||
|
||||
### cargo install
|
||||
```bash
|
||||
cargo install mcp-stdio-proxy
|
||||
```
|
||||
|
||||
### cargo-binstall
|
||||
```bash
|
||||
cargo binstall mcp-stdio-proxy
|
||||
```
|
||||
|
||||
## 🔧 本地测试发布
|
||||
|
||||
```bash
|
||||
# 查看发布计划
|
||||
dist plan --tag=v0.2.0
|
||||
|
||||
# 构建全局产物(安装脚本)
|
||||
dist build --tag=v0.2.0 --artifacts=global
|
||||
|
||||
# 查看生成的文件
|
||||
ls -la target/distrib/
|
||||
```
|
||||
|
||||
## 📝 文档
|
||||
|
||||
- `RELEASE.md` - 完整的发布指南
|
||||
- `README.md` - 已更新安装方式章节
|
||||
- `dist-workspace.toml` - cargo-dist 配置
|
||||
- `.github/workflows/release.yml` - 自动化工作流
|
||||
|
||||
## 🎯 支持的平台
|
||||
|
||||
| 平台 | 目标三元组 |
|
||||
|------|-----------|
|
||||
| Linux x86_64 | x86_64-unknown-linux-gnu |
|
||||
| Linux ARM64 | aarch64-unknown-linux-gnu |
|
||||
| macOS Intel | x86_64-apple-darwin |
|
||||
| macOS Apple Silicon | aarch64-apple-darwin |
|
||||
| Windows x86_64 | x86_64-pc-windows-msvc |
|
||||
|
||||
## 📦 发布的二进制
|
||||
|
||||
1. **mcp-stdio-proxy** - 主 MCP 代理服务
|
||||
2. **document-parser** - 文档解析服务
|
||||
3. **voice-cli** - 语音转文字服务
|
||||
|
||||
## 🔐 校验和验证
|
||||
|
||||
每个 release 都包含 `sha256.sum` 文件:
|
||||
|
||||
```bash
|
||||
# 下载校验和
|
||||
curl -L -O https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/sha256.sum
|
||||
|
||||
# 验证
|
||||
sha256sum -c sha256.sum
|
||||
```
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- [cargo-dist 官方文档](https://axodotdev.github.io/cargo-dist/)
|
||||
- [cargo-dist 配置参考](https://axodotdev.github.io/cargo-dist/book/reference/config.html)
|
||||
- [项目 GitHub Releases](https://github.com/nuwax-ai/mcp-proxy/releases)
|
||||
62
qiming-mcp-proxy/CHANGELOG.md
Normal file
62
qiming-mcp-proxy/CHANGELOG.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.1.52] - 2026-02-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- **PATH 按段去重**: `ensure_runtime_path` 从整段 `starts_with` 改为按分隔符拆段去重,
|
||||
解决上层多次前置导致的 `node/bin` 重复条目问题
|
||||
- **config PATH 覆盖**: 用户在 MCP config env 中指定自定义 PATH 时,
|
||||
仍然确保应用内置运行时路径(`NUWAX_APP_RUNTIME_PATH`)在最前面
|
||||
- **env value 泄露**: `log_command_details` 不再打印 env 变量的 value,
|
||||
仅输出 key 列表,避免泄露敏感信息(如 token、secret)
|
||||
|
||||
### Added
|
||||
|
||||
- **`mcp-common::diagnostic` 模块**: 提取子进程启动诊断日志为独立模块,
|
||||
提供 `log_stdio_spawn_context`、`format_spawn_error`、`format_path_summary` 等公共函数,
|
||||
减少业务代码中的日志侵入
|
||||
- **启动阶段环境诊断**: `env_init` 结束后输出 PATH 摘要和镜像环境变量最终值
|
||||
- **spawn 失败上下文**: SSE/Stream 子进程 spawn 失败时输出完整错误上下文
|
||||
(command、args、PATH),便于快速定位可执行文件找不到的问题
|
||||
- **build 失败上下文**: `mcp_start_task` 中 SSE/Stream server build 失败时
|
||||
通过 `anyhow::Context` 附加 MCP ID 和服务类型
|
||||
- **`ensure_runtime_path` 单元测试**: 新增 5 个测试覆盖前置、部分去重、
|
||||
全部已存在、双重重复等场景
|
||||
|
||||
### Changed
|
||||
|
||||
- **server_builder PATH 逻辑简化**: 两侧 `connect_stdio` 从三分支
|
||||
(继承/config/缺失)统一为:取基础 PATH → `ensure_runtime_path` → 传递给子进程
|
||||
- **无镜像提示**: 未配置镜像源时输出提示行,而非静默跳过
|
||||
|
||||
## [0.1.51] - 2026-02-14
|
||||
|
||||
### Changed
|
||||
|
||||
- 版本号更新
|
||||
|
||||
## [0.1.49] - 2026-02-13
|
||||
|
||||
### Added
|
||||
|
||||
- **镜像源配置**: 支持通过 `config.yml` 配置 npm/PyPI 镜像源,
|
||||
环境变量优先级高于配置文件
|
||||
- **环境变量初始化**: `env_init` 模块统一管理子进程环境(镜像源 + 内置运行时 PATH)
|
||||
- **`UV_INSECURE_HOST` 支持**: HTTP 类型的 PyPI 镜像自动提取 host 并设置
|
||||
|
||||
## [0.1.48] - 2026-02-12
|
||||
|
||||
### Added
|
||||
|
||||
- **跨平台进程管理**: `process_compat` 模块提供 `wrap_process_v8` / `wrap_process_v9` 宏,
|
||||
统一 Unix(ProcessGroup)和 Windows(JobObject + CREATE_NO_WINDOW)的进程包装
|
||||
|
||||
### Fixed
|
||||
|
||||
- Windows 平台隐藏控制台窗口配置
|
||||
365
qiming-mcp-proxy/CLAUDE.md
Normal file
365
qiming-mcp-proxy/CLAUDE.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Building and Testing
|
||||
```bash
|
||||
# Build all workspace crates
|
||||
cargo build
|
||||
|
||||
# Build specific crate
|
||||
cargo build -p mcp-proxy
|
||||
cargo build -p voice-cli
|
||||
cargo build -p document-parser
|
||||
cargo build -p oss-client
|
||||
|
||||
# Build in release mode
|
||||
cargo build --release
|
||||
|
||||
# Run tests for all crates
|
||||
cargo test
|
||||
|
||||
# Run tests for specific crate
|
||||
cargo test -p mcp-proxy
|
||||
cargo test -p voice-cli
|
||||
|
||||
# Run clippy for linting
|
||||
cargo clippy --all-targets --all-features
|
||||
|
||||
# Format code
|
||||
cargo fmt
|
||||
|
||||
# Check formatting
|
||||
cargo fmt --check
|
||||
```
|
||||
|
||||
### Cross-Platform Building (using Docker)
|
||||
```bash
|
||||
# Build document-parser for Linux x86_64
|
||||
make build-document-parser-x86_64
|
||||
|
||||
# Build document-parser for Linux ARM64
|
||||
make build-document-parser-arm64
|
||||
|
||||
# Build voice-cli for Linux x86_64
|
||||
make build-voice-cli-x86_64
|
||||
|
||||
# Build all components for x86_64
|
||||
make build-all-x86_64
|
||||
|
||||
# Build Docker runtime image
|
||||
make build-image
|
||||
|
||||
# Run Docker container
|
||||
make run
|
||||
```
|
||||
|
||||
### Service-Specific Commands
|
||||
|
||||
**Document Parser:**
|
||||
```bash
|
||||
# Initialize Python environment
|
||||
cd document-parser && cargo run --bin document-parser -- uv-init
|
||||
|
||||
# Check environment status
|
||||
cd document-parser && cargo run --bin document-parser -- check
|
||||
|
||||
# Start server
|
||||
cd document-parser && cargo run --bin document-parser -- server
|
||||
|
||||
# Troubleshoot issues
|
||||
cd document-parser && cargo run --bin document-parser -- troubleshoot
|
||||
```
|
||||
|
||||
**Voice CLI:**
|
||||
```bash
|
||||
# Initialize server configuration
|
||||
cd voice-cli && cargo run --bin voice-cli -- server init
|
||||
|
||||
# Run voice server
|
||||
cd voice-cli && cargo run --bin voice-cli -- server run
|
||||
|
||||
# List Whisper models
|
||||
cd voice-cli && cargo run --bin voice-cli -- model list
|
||||
|
||||
# Download model
|
||||
cd voice-cli && cargo run --bin voice-cli -- model download tiny
|
||||
```
|
||||
|
||||
**MCP Proxy:**
|
||||
```bash
|
||||
# Start MCP proxy server
|
||||
cd mcp-proxy && cargo run --bin mcp-proxy
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This is a Rust workspace implementing an MCP (Model Context Protocol) proxy system with multiple services:
|
||||
|
||||
### Core Services
|
||||
|
||||
**mcp-proxy**: Main MCP proxy service implementing SSE protocol
|
||||
- Provides HTTP API for MCP service management
|
||||
- Handles dynamic plugin loading and configuration
|
||||
- Implements Server-Sent Events for real-time communication
|
||||
- Uses `rmcp` crate for MCP protocol implementation
|
||||
|
||||
**document-parser**: High-performance document parsing service
|
||||
- Multi-format support: PDF, Word, Excel, PowerPoint via MinerU/MarkItDown
|
||||
- Python-based with uv dependency management
|
||||
- GPU acceleration support with CUDA
|
||||
- Automatic virtual environment management
|
||||
|
||||
**voice-cli**: Speech-to-text service with TTS capabilities
|
||||
- Whisper model integration for transcription
|
||||
- Python-based TTS service with uv management
|
||||
- Apalis-based async task queue
|
||||
- FFmpeg integration for metadata extraction
|
||||
|
||||
**oss-client**: Lightweight Alibaba Cloud OSS client
|
||||
- Simple interface for object storage operations
|
||||
- Workspace dependency for other services
|
||||
|
||||
### Key Integrations
|
||||
|
||||
**Workspace Management**: All dependencies managed through workspace Cargo.toml with centralized versioning. Sub-crates use `{ workspace = true }` for dependency references.
|
||||
|
||||
**Async Processing**: Uses `tokio` runtime throughout. Task processing via `apalis` with SQLite persistence for voice transcription and TTS tasks.
|
||||
|
||||
**HTTP Framework**: `axum` with `tower` middleware for all web services. OpenAPI documentation via `utoipa`.
|
||||
|
||||
**Error Handling**: Consistent error handling with `anyhow` for application code and `thiserror` for library code.
|
||||
|
||||
**Logging**: Structured logging with `tracing` and `tracing-subscriber`. Daily log rotation with `tracing-appender`.
|
||||
|
||||
**FFmpeg Integration**: Lightweight FFmpeg command execution via `ffmpeg-sidecar` for media metadata extraction. System FFmpeg installation required but provides graceful fallback.
|
||||
|
||||
**Python Integration**: Both `document-parser` and `voice-cli` use Python services with `uv` for dependency management and virtual environment handling:
|
||||
- Automatic virtual environment creation in `./venv/`
|
||||
- uv package manager for fast Python dependency installation
|
||||
- CUDA GPU acceleration support (optional)
|
||||
- Graceful degradation if Python/uv unavailable
|
||||
|
||||
**Task Queue & Persistence**: Voice services use `apalis` for background task processing:
|
||||
- SQLite-based persistence for task state tracking
|
||||
- Task retry mechanisms with exponential backoff
|
||||
- Support for task prioritization and status monitoring
|
||||
- Worker management with resource limits
|
||||
|
||||
### Configuration System
|
||||
|
||||
All services use hierarchical configuration:
|
||||
1. Default values in code
|
||||
2. Configuration files (YAML/JSON/TOML)
|
||||
3. Environment variables with service prefixes
|
||||
4. Command-line arguments
|
||||
|
||||
### Development Standards
|
||||
|
||||
**Code Organization**: Strict workspace structure - no code in root directory. All implementation in sub-crates with clear module boundaries.
|
||||
|
||||
**Formatting & Linting**:
|
||||
- Line length: 100 characters
|
||||
- 4-space indentation (no tabs)
|
||||
- Always run `cargo fmt` and `cargo clippy` before commits
|
||||
- Use `cargo audit` to check for security vulnerabilities
|
||||
- Use `typos-cli` to check spelling
|
||||
|
||||
**Error Handling**:
|
||||
- Prefer `anyhow` for application code, `thiserror` for libraries
|
||||
- Avoid `unwrap()` except in tests
|
||||
- Use `?` operator for error propagation
|
||||
- Add contextual error messages with `anyhow::Context`
|
||||
- Never include sensitive data in error messages
|
||||
|
||||
**Concurrency**:
|
||||
- Use `tokio` for async, `Arc<Mutex<T>>` or `Arc<RwLock<T>>` for shared state
|
||||
- Avoid blocking operations in async contexts
|
||||
- Use `tokio::spawn` for creating concurrent tasks
|
||||
|
||||
**Memory Management**:
|
||||
- Prefer borrowing over ownership
|
||||
- **Use `dashmap` for concurrent hashmaps** instead of `Arc<RwLock<HashMap<_, _>>>` (dashmap provides atomic operations and is more efficient)
|
||||
- Avoid unnecessary `clone()`, consider `Cow<T>` or reference counting
|
||||
|
||||
**Testing**:
|
||||
- Unit tests alongside implementation code
|
||||
- Integration tests where appropriate
|
||||
- Use `assert_eq!`, `assert_ne!` for assertions
|
||||
- Run specific tests: `cargo test <test_name> -p <crate>`
|
||||
|
||||
**Documentation**:
|
||||
- All public APIs must have documentation comments (`///`)
|
||||
- Include usage examples in complex API documentation
|
||||
- Keep README.md and other docs updated
|
||||
|
||||
## Cursor Rules Summary
|
||||
|
||||
**Development Standards**:
|
||||
- Line length: 100 characters
|
||||
- 4-space indentation (no tabs)
|
||||
- Documentation comments for all public APIs
|
||||
- Use `cargo fmt` and `cargo clippy` before commits
|
||||
|
||||
**Error Handling**:
|
||||
- `anyhow` for application code, `thiserror` for libraries
|
||||
- Contextual error messages with `anyhow::Context`
|
||||
- No sensitive data in error messages
|
||||
|
||||
**Module Organization**:
|
||||
- Clear module responsibilities
|
||||
- `pub(crate)` for internal visibility
|
||||
- Re-export public APIs in `lib.rs`
|
||||
|
||||
**Dependencies**:
|
||||
- Centralized workspace dependency management
|
||||
- Specific versions (no `*`)
|
||||
- Regular security audits with `cargo audit`
|
||||
|
||||
## Build System
|
||||
|
||||
The project uses a sophisticated Makefile with Docker buildx for cross-platform compilation:
|
||||
|
||||
### Docker Build Commands
|
||||
```bash
|
||||
# Check Docker buildx availability
|
||||
make check-buildx
|
||||
|
||||
# Setup buildx builder (if needed)
|
||||
make setup-buildx
|
||||
|
||||
# Build document-parser for specific platforms
|
||||
make build-document-parser-x86_64
|
||||
make build-document-parser-arm64
|
||||
make build-document-parser-multi
|
||||
|
||||
# Build voice-cli for specific platforms
|
||||
make build-voice-cli-x86_64
|
||||
make build-voice-cli-arm64
|
||||
make build-voice-cli-multi
|
||||
|
||||
# Build all components
|
||||
make build-all-x86_64
|
||||
make build-all-arm64
|
||||
make build-all-multi
|
||||
|
||||
# Build and run Docker runtime image
|
||||
make build-image
|
||||
make run
|
||||
```
|
||||
|
||||
**Build System Features**:
|
||||
- **Docker-based builds**: All compilation happens in containers for consistency
|
||||
- **Multi-platform support**: Linux x86_64 and ARM64 targets
|
||||
- **Export targets**: Separate build and runtime stages
|
||||
- **Automated dependency installation**: Python and Rust dependencies managed in containers
|
||||
- **Output directory**: `./dist/` contains all built binaries organized by platform
|
||||
|
||||
## Service-Specific Architecture Details
|
||||
|
||||
### Document Parser (`document-parser/`)
|
||||
- **Core Structure**: `app_state.rs`, `config.rs`, `main.rs`, `lib.rs`
|
||||
- **Submodules**: `handlers/`, `middleware/`, `models/`, `parsers/`, `processors/`, `services/`, `tests/`, `utils/`
|
||||
- **Python Integration**: MinerU for PDF parsing, MarkItDown for other formats
|
||||
- **Virtual Environment**: Auto-managed in `./venv/`, activated via `source ./venv/bin/activate`
|
||||
- **Server**: Axum-based HTTP server with multipart file upload support
|
||||
- **Configuration**: YAML/JSON/TOML support with environment variable overrides
|
||||
|
||||
### Voice CLI (`voice-cli/`)
|
||||
- **Core Components**:
|
||||
- `services/`: Model management, transcription engine, TTS service, task queue
|
||||
- `server/`: HTTP handlers, routes, middleware configuration
|
||||
- `models/`: Request/response data structures
|
||||
- **Whisper Integration**: Model download and management via `voice-toolkit`
|
||||
- **TTS Service**: Python-based with `uv` dependency management
|
||||
- **FFmpeg**: Metadata extraction via `ffmpeg-sidecar`
|
||||
- **Apalis**: Async task processing with SQLite persistence
|
||||
|
||||
### MCP Proxy (`mcp-proxy/`)
|
||||
- **Core Structure**: `config.rs`, `lib.rs`, `main.rs`, `mcp_error.rs`
|
||||
- **Submodules**: `client/`, `model/`, `proxy/`, `server/`, `tests/`
|
||||
- **SSE Protocol**: Real-time communication via Server-Sent Events
|
||||
- **Plugin System**: Dynamic MCP service loading and management
|
||||
- **HTTP API**: REST endpoints for service management and status checks
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Service Initialization**: All services follow similar patterns for configuration loading, logging setup, and graceful shutdown.
|
||||
|
||||
**HTTP API Design**: Consistent use of axum extractors, middleware configuration, and OpenAPI documentation.
|
||||
|
||||
**Async Task Processing**: Voice services use apalis for background task processing with retry mechanisms and SQLite persistence.
|
||||
|
||||
**Python Integration**: Both document-parser and voice-cli use Python services with uv for dependency management and virtual environment handling.
|
||||
|
||||
**Configuration Management**: Hierarchical configuration with environment variable overrides and command-line argument integration.
|
||||
|
||||
## Single Test Execution Examples
|
||||
```bash
|
||||
# Run tests for specific crate
|
||||
cargo test -p mcp-proxy
|
||||
cargo test -p voice-cli
|
||||
cargo test -p document-parser
|
||||
|
||||
# Run specific test
|
||||
cargo test test_extract_basic_metadata -p voice-cli
|
||||
cargo test <test_name> -p mcp-proxy
|
||||
|
||||
# Run tests in release mode
|
||||
cargo test --release -p mcp-proxy
|
||||
|
||||
# Run library tests only (excluding integration tests)
|
||||
cargo test --lib -p voice-cli
|
||||
|
||||
# Run tests with output
|
||||
cargo test -p mcp-proxy -- --nocapture
|
||||
```
|
||||
|
||||
## Python/uv Environment Management
|
||||
|
||||
### For Document Parser:
|
||||
```bash
|
||||
cd document-parser
|
||||
# Initialize Python environment (creates ./venv/)
|
||||
cargo run --bin document-parser -- uv-init
|
||||
|
||||
# Check environment status
|
||||
cargo run --bin document-parser -- check
|
||||
|
||||
# Start server
|
||||
cargo run --bin document-parser -- server
|
||||
|
||||
# Troubleshoot issues
|
||||
cargo run --bin document-parser -- troubleshoot
|
||||
```
|
||||
|
||||
### For Voice CLI TTS:
|
||||
```bash
|
||||
cd voice-cli
|
||||
# Install uv package manager
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Install Python dependencies
|
||||
uv sync
|
||||
|
||||
# Run TTS service directly
|
||||
python3 tts_service.py --help
|
||||
```
|
||||
|
||||
## Dependencies Management
|
||||
|
||||
All dependencies are managed centrally in the workspace `Cargo.toml`:
|
||||
- Sub-crates use `{ workspace = true }` for dependency references
|
||||
- Specific versions (no `*` wildcards)
|
||||
- Centralized feature flags
|
||||
- Regular security audits with `cargo audit`
|
||||
|
||||
Key workspace dependencies:
|
||||
- `rmcp`: MCP protocol implementation with SSE support
|
||||
- `tokio`: Async runtime
|
||||
- `axum`: Web framework with tower middleware
|
||||
- `tracing`: Structured logging
|
||||
- `apalis`: Async task queue
|
||||
- `dashmap`: Concurrent hashmap (preferred over `Arc<RwLock<HashMap>>`)
|
||||
7822
qiming-mcp-proxy/Cargo.lock
generated
Normal file
7822
qiming-mcp-proxy/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
108
qiming-mcp-proxy/Cargo.toml
Normal file
108
qiming-mcp-proxy/Cargo.toml
Normal file
@@ -0,0 +1,108 @@
|
||||
[workspace]
|
||||
# 排除 fastembed(ort-sys 不支持部分平台)
|
||||
members = ["document-parser", "mcp-common", "mcp-proxy", "mcp-sse-proxy", "mcp-streamable-proxy", "oss-client", "voice-cli"]
|
||||
# 默认只构建 mcp-proxy(排除 document-parser 等需要特殊环境的包)
|
||||
default-members = ["mcp-proxy", "mcp-common", "mcp-sse-proxy", "mcp-streamable-proxy"]
|
||||
resolver = "2"
|
||||
exclude = ["fastembed"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[workspace.dependencies]
|
||||
mcp-proxy = { path = "mcp-proxy" }
|
||||
oss-client = { path = "oss-client" }
|
||||
|
||||
# Note: rmcp 依赖由各子项目独立管理(mcp-sse-proxy 用 0.10,mcp-streamable-proxy 用 0.12)
|
||||
tokio = { version = "1.48", features = ["macros", "net", "rt", "rt-multi-thread"] }
|
||||
tokio-util = "0.7"
|
||||
|
||||
# Logging and tracing
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = "0.2"
|
||||
tracing-opentelemetry = "0.32"
|
||||
opentelemetry = { version = "0.31", features = ["trace"] }
|
||||
opentelemetry-jaeger = { version = "0.22", features = ["rt-tokio"] }
|
||||
opentelemetry-semantic-conventions = { version = "0.31", features = ["semconv_experimental"] }
|
||||
opentelemetry_sdk = "0.31"
|
||||
opentelemetry-otlp = { version = "0.31", features = ["grpc-tonic"] }
|
||||
tonic = "0.13"
|
||||
hostname = "0.4"
|
||||
uuid = { version = "1.19", features = ["v4", "v7"] }
|
||||
rand = "0.9"
|
||||
log = "0.4"
|
||||
anyhow = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde", "now"] }
|
||||
thiserror = "2.0"
|
||||
axum = { version = "0.8", features = [
|
||||
"http2",
|
||||
"query",
|
||||
"tracing",
|
||||
"ws",
|
||||
"multipart",
|
||||
"macros",
|
||||
] }
|
||||
tower = { version = "0.5" }
|
||||
tower-http = { version = "0.6", features = [
|
||||
"compression-full",
|
||||
"cors",
|
||||
"fs",
|
||||
"trace",
|
||||
"limit",
|
||||
] }
|
||||
axum-extra = { version = "0.12", features = ["typed-header"] }
|
||||
utoipa = { version = "5.4", features = ["axum_extras", "chrono", "uuid"] }
|
||||
utoipa-rapidoc = { version = "6", features = ["axum"] }
|
||||
utoipa-redoc = { version = "6", features = ["axum"] }
|
||||
utoipa-swagger-ui = { version = "9", features = ["axum"] }
|
||||
dashmap = "6.1"
|
||||
arc-swap = "1.7"
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
criterion = "0.7"
|
||||
once_cell = "1.21"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "=0.9.33"
|
||||
serde_with = "3.12"
|
||||
reqwest = { version = "0.13", features = ["json"] }
|
||||
http = "1.4"
|
||||
aliyun-oss-rust-sdk = { version = "0.2", default-features = false, features = ["async"] }
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
futures = "0.3"
|
||||
run_code_rmcp = "0.0.35"
|
||||
derive_builder = "0.20"
|
||||
bytes = "1.0"
|
||||
base64 = "0.22"
|
||||
tempfile = "3.23"
|
||||
dirs = "6.0"
|
||||
scopeguard = "1.2"
|
||||
|
||||
async-trait = "0.1"
|
||||
|
||||
# 自己开发的语音相关工具
|
||||
voice-toolkit = { version = "0.16", features = ["stt", "audio"] }
|
||||
|
||||
# Task queue and persistence for async processing
|
||||
apalis = { version = "0.7", features = ["tracing", "limit"] }
|
||||
apalis-sql = { version = "0.7", features = ["sqlite"] }
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] }
|
||||
sled = "0.34"
|
||||
|
||||
# 可执行文件查找
|
||||
which = "7.0"
|
||||
|
||||
# 国际化支持
|
||||
rust-i18n = "3"
|
||||
|
||||
# Audio format detection and processing
|
||||
symphonia = { version = "0.5", features = ["all"] }
|
||||
bincode = "2.0"
|
||||
tokio-stream = "0.1.18"
|
||||
backtrace = "0.3"
|
||||
tracing-futures = "0.2.5"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
# The profile that 'dist' will build with
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
42
qiming-mcp-proxy/LICENSE
Normal file
42
qiming-mcp-proxy/LICENSE
Normal file
@@ -0,0 +1,42 @@
|
||||
# Dual Licensing
|
||||
|
||||
This project is dual-licensed under either:
|
||||
|
||||
* **MIT License** - See [LICENSE-MIT](LICENSE-MIT) for details
|
||||
* **Apache License, Version 2.0** - See [LICENSE-APACHE](LICENSE-APACHE) for details
|
||||
|
||||
You may choose either license for your use of this project.
|
||||
|
||||
## MIT License Summary
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
|
||||
## Apache 2.0 License Summary
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
Copyright (c) 2025 nuwax-ai
|
||||
190
qiming-mcp-proxy/LICENSE-APACHE
Normal file
190
qiming-mcp-proxy/LICENSE-APACHE
Normal file
@@ -0,0 +1,190 @@
|
||||
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 the 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
|
||||
|
||||
Copyright 2025 nuwax-ai
|
||||
|
||||
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.
|
||||
21
qiming-mcp-proxy/LICENSE-MIT
Normal file
21
qiming-mcp-proxy/LICENSE-MIT
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 nuwax-ai
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
357
qiming-mcp-proxy/Makefile
Normal file
357
qiming-mcp-proxy/Makefile
Normal file
@@ -0,0 +1,357 @@
|
||||
# Makefile for cross-platform compilation of document-parser, voice-cli and mcp-proxy
|
||||
|
||||
# 默认目标平台
|
||||
TARGET_PLATFORM ?= linux/amd64
|
||||
|
||||
# Docker 镜像名称
|
||||
IMAGE_NAME = mcp-proxy-builder
|
||||
|
||||
# 输出目录
|
||||
OUTPUT_DIR = ./dist
|
||||
|
||||
# 通用构建函数
|
||||
define build_target
|
||||
@echo "🚀 构建 $(1) $(2) 版本..."
|
||||
@git pull
|
||||
@mkdir -p $(3)
|
||||
docker buildx build --platform $(4) --target export --output type=local,dest=$(3) -f docker/Dockerfile.document-parser ..
|
||||
@echo "✅ $(1) $(2) 版本构建完成"
|
||||
endef
|
||||
|
||||
# 默认目标
|
||||
.PHONY: all
|
||||
all: build-document-parser-x86_64
|
||||
|
||||
# 创建输出目录
|
||||
$(OUTPUT_DIR):
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
|
||||
# ============================================================================
|
||||
# Document Parser 构建目标
|
||||
# ============================================================================
|
||||
|
||||
# 构建 document-parser Linux x86_64 版本
|
||||
.PHONY: build-document-parser-x86_64
|
||||
build-document-parser-x86_64:
|
||||
$(call build_target,document-parser,Linux x86_64,./dist/document-parser-x86_64,linux/amd64)
|
||||
|
||||
# 构建 document-parser Linux ARM64 版本
|
||||
.PHONY: build-document-parser-arm64
|
||||
build-document-parser-arm64:
|
||||
$(call build_target,document-parser,Linux ARM64,./dist/document-parser-arm64,linux/arm64)
|
||||
|
||||
# 构建 document-parser 多平台版本
|
||||
.PHONY: build-document-parser-multi
|
||||
build-document-parser-multi: build-document-parser-x86_64 build-document-parser-arm64
|
||||
|
||||
# ============================================================================
|
||||
# Voice CLI 构建目标
|
||||
# ============================================================================
|
||||
|
||||
# 构建 voice-cli Linux x86_64 版本
|
||||
.PHONY: build-voice-cli-x86_64
|
||||
build-voice-cli-x86_64:
|
||||
$(call build_target,voice-cli,Linux x86_64,./dist/voice-cli-x86_64,linux/amd64)
|
||||
|
||||
# 构建 voice-cli Linux ARM64 版本
|
||||
.PHONY: build-voice-cli-arm64
|
||||
build-voice-cli-arm64:
|
||||
$(call build_target,voice-cli,Linux ARM64,./dist/voice-cli-arm64,linux/arm64)
|
||||
|
||||
# 构建 voice-cli 多平台版本
|
||||
.PHONY: build-voice-cli-multi
|
||||
build-voice-cli-multi: build-voice-cli-x86_64 build-voice-cli-arm64
|
||||
|
||||
# ============================================================================
|
||||
# MCP Proxy 构建目标
|
||||
# ============================================================================
|
||||
|
||||
# 构建 mcp-proxy(按照当前系统架构)
|
||||
.PHONY: build-mcp-proxy
|
||||
build-mcp-proxy:
|
||||
@echo "🚀 构建 mcp-proxy(当前系统架构)..."
|
||||
@git pull
|
||||
@mkdir -p ./dist/mcp-proxy
|
||||
docker buildx build \
|
||||
--target export \
|
||||
--output type=local,dest=./dist/mcp-proxy \
|
||||
-f docker/Dockerfile.mcp-proxy \
|
||||
..
|
||||
@echo "✅ mcp-proxy 构建完成"
|
||||
|
||||
# ============================================================================
|
||||
# 所有组件构建目标
|
||||
# ============================================================================
|
||||
|
||||
# 构建所有组件(当前系统架构)
|
||||
.PHONY: build-all
|
||||
build-all: build-document-parser-x86_64 build-voice-cli-x86_64 build-mcp-proxy
|
||||
|
||||
# 构建 Docker 镜像(用于运行)
|
||||
.PHONY: build-image
|
||||
build-image:
|
||||
@echo "🚀 构建 mcp-proxy Docker 运行镜像..."
|
||||
docker buildx build \
|
||||
--platform $(TARGET_PLATFORM) \
|
||||
--target runtime \
|
||||
-t mcp-proxy:latest \
|
||||
-f docker/Dockerfile.mcp-proxy \
|
||||
$(shell pwd)
|
||||
@echo "✅ Docker 镜像构建完成: mcp-proxy:latest"
|
||||
|
||||
# 构建 Docker 镜像(document-parser)
|
||||
.PHONY: build-image-document-parser
|
||||
build-image-document-parser:
|
||||
@echo "🚀 构建 document-parser Docker 运行镜像..."
|
||||
docker buildx build \
|
||||
--platform $(TARGET_PLATFORM) \
|
||||
--target runtime \
|
||||
-t document-parser:latest \
|
||||
-f docker/Dockerfile.document-parser \
|
||||
..
|
||||
@echo "✅ Docker 镜像构建完成: document-parser:latest"
|
||||
|
||||
# 运行 Docker 镜像(mcp-proxy)
|
||||
.PHONY: run
|
||||
run: build-image
|
||||
@echo "🚀 使用 docker-compose 后台启动 mcp-proxy..."
|
||||
cd docker && docker-compose up -d
|
||||
@echo "✅ mcp-proxy 已在后台启动"
|
||||
@echo "📋 查看日志: cd docker && docker-compose logs -f"
|
||||
@echo "🛑 停止服务: cd docker && docker-compose down"
|
||||
@echo "📊 查看状态: cd docker && docker-compose ps"
|
||||
|
||||
# 运行 Docker 镜像(mcp-proxy,前台模式)
|
||||
.PHONY: run-fg
|
||||
run-fg: build-image
|
||||
@echo "🚀 使用 docker-compose 前台启动 mcp-proxy..."
|
||||
cd docker && docker-compose up
|
||||
|
||||
# 运行 Docker 镜像(document-parser)
|
||||
.PHONY: run-document-parser
|
||||
run-document-parser:
|
||||
@echo "🚀 运行 document-parser..."
|
||||
docker run --rm -p 8080:8080 document-parser:latest
|
||||
|
||||
# 检查 Docker buildx 是否可用
|
||||
.PHONY: check-buildx
|
||||
check-buildx:
|
||||
@echo "🔍 检查 Docker buildx 状态..."
|
||||
@docker buildx version || (echo "❌ Docker buildx 不可用,请确保 Docker 版本支持 buildx" && exit 1)
|
||||
@docker buildx ls
|
||||
@echo "✅ Docker buildx 可用"
|
||||
|
||||
# 创建 buildx builder(如果需要)
|
||||
.PHONY: setup-buildx
|
||||
setup-buildx:
|
||||
@echo "🔧 设置 Docker buildx builder..."
|
||||
docker buildx create --name cross-builder --use --bootstrap || true
|
||||
@echo "✅ Docker buildx builder 设置完成"
|
||||
|
||||
# ============================================================================
|
||||
# MCP 包发布目标
|
||||
# ============================================================================
|
||||
|
||||
# 自动更新所有 MCP 包的版本号(小版本号加一)
|
||||
.PHONY: mcp-version-update
|
||||
mcp-version-update:
|
||||
@echo "🔄 开始更新 MCP 包版本号..."
|
||||
@echo ""
|
||||
@# 读取 mcp-common 的当前版本
|
||||
@COMMON_VERSION=$$(grep '^version = ' mcp-common/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/'); \
|
||||
COMMON_MAJOR=$$(echo $$COMMON_VERSION | cut -d. -f1); \
|
||||
COMMON_MINOR=$$(echo $$COMMON_VERSION | cut -d. -f2); \
|
||||
COMMON_PATCH=$$(echo $$COMMON_VERSION | cut -d. -f3); \
|
||||
COMMON_NEW_PATCH=$$((COMMON_PATCH + 1)); \
|
||||
COMMON_NEW_VERSION="$$COMMON_MAJOR.$$COMMON_MINOR.$$COMMON_NEW_PATCH"; \
|
||||
echo "mcp-common: $$COMMON_VERSION -> $$COMMON_NEW_VERSION"; \
|
||||
PROXY_VERSION=$$(grep '^version = ' mcp-proxy/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/'); \
|
||||
PROXY_MAJOR=$$(echo $$PROXY_VERSION | cut -d. -f1); \
|
||||
PROXY_MINOR=$$(echo $$PROXY_VERSION | cut -d. -f2); \
|
||||
PROXY_PATCH=$$(echo $$PROXY_VERSION | cut -d. -f3); \
|
||||
PROXY_NEW_PATCH=$$((PROXY_PATCH + 1)); \
|
||||
PROXY_NEW_VERSION="$$PROXY_MAJOR.$$PROXY_MINOR.$$PROXY_NEW_PATCH"; \
|
||||
echo "mcp-stdio-proxy: $$PROXY_VERSION -> $$PROXY_NEW_VERSION"; \
|
||||
echo ""; \
|
||||
echo "1️⃣ 更新 mcp-common 版本..."; \
|
||||
sed -i.bak "s/^version = \"$$COMMON_VERSION\"/version = \"$$COMMON_NEW_VERSION\"/" mcp-common/Cargo.toml && rm mcp-common/Cargo.toml.bak; \
|
||||
echo "2️⃣ 更新 mcp-sse-proxy 版本和依赖..."; \
|
||||
sed -i.bak "s/^version = \"$$COMMON_VERSION\"/version = \"$$COMMON_NEW_VERSION\"/" mcp-sse-proxy/Cargo.toml && rm mcp-sse-proxy/Cargo.toml.bak; \
|
||||
sed -i.bak "s/mcp-common = { version = \"$$COMMON_VERSION\"/mcp-common = { version = \"$$COMMON_NEW_VERSION\"/" mcp-sse-proxy/Cargo.toml && rm mcp-sse-proxy/Cargo.toml.bak; \
|
||||
echo "3️⃣ 更新 mcp-streamable-proxy 版本和依赖..."; \
|
||||
sed -i.bak "s/^version = \"$$COMMON_VERSION\"/version = \"$$COMMON_NEW_VERSION\"/" mcp-streamable-proxy/Cargo.toml && rm mcp-streamable-proxy/Cargo.toml.bak; \
|
||||
sed -i.bak "s/mcp-common = { version = \"$$COMMON_VERSION\"/mcp-common = { version = \"$$COMMON_NEW_VERSION\"/" mcp-streamable-proxy/Cargo.toml && rm mcp-streamable-proxy/Cargo.toml.bak; \
|
||||
echo "4️⃣ 更新 mcp-stdio-proxy 版本和依赖..."; \
|
||||
sed -i.bak "s/^version = \"$$PROXY_VERSION\"/version = \"$$PROXY_NEW_VERSION\"/" mcp-proxy/Cargo.toml && rm mcp-proxy/Cargo.toml.bak; \
|
||||
sed -i.bak "s/mcp-common = { version = \"$$COMMON_VERSION\"/mcp-common = { version = \"$$COMMON_NEW_VERSION\"/" mcp-proxy/Cargo.toml && rm mcp-proxy/Cargo.toml.bak; \
|
||||
sed -i.bak "s/mcp-streamable-proxy = { version = \"$$COMMON_VERSION\"/mcp-streamable-proxy = { version = \"$$COMMON_NEW_VERSION\"/" mcp-proxy/Cargo.toml && rm mcp-proxy/Cargo.toml.bak; \
|
||||
sed -i.bak "s/mcp-sse-proxy = { version = \"$$COMMON_VERSION\"/mcp-sse-proxy = { version = \"$$COMMON_NEW_VERSION\"/" mcp-proxy/Cargo.toml && rm mcp-proxy/Cargo.toml.bak; \
|
||||
echo ""; \
|
||||
echo "✅ 版本号更新完成!"
|
||||
|
||||
# 显示当前 MCP 包的版本号
|
||||
.PHONY: mcp-version-show
|
||||
mcp-version-show:
|
||||
@echo "📋 当前 MCP 包版本号:"
|
||||
@echo ""
|
||||
@echo " mcp-common: $$(grep '^version = ' mcp-common/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
|
||||
@echo " mcp-sse-proxy: $$(grep '^version = ' mcp-sse-proxy/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
|
||||
@echo " mcp-streamable-proxy: $$(grep '^version = ' mcp-streamable-proxy/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
|
||||
@echo " mcp-stdio-proxy: $$(grep '^version = ' mcp-proxy/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
|
||||
@echo ""
|
||||
@echo "📦 依赖版本号检查:"
|
||||
@echo ""
|
||||
@echo " mcp-sse-proxy 依赖的 mcp-common: $$(grep 'mcp-common = { version' mcp-sse-proxy/Cargo.toml | sed 's/.*version = "\([^"]*\)".*/\1/')"
|
||||
@echo " mcp-streamable-proxy 依赖的 mcp-common: $$(grep 'mcp-common = { version' mcp-streamable-proxy/Cargo.toml | sed 's/.*version = "\([^"]*\)".*/\1/')"
|
||||
@echo " mcp-stdio-proxy 依赖的 mcp-common: $$(grep 'mcp-common = { version' mcp-proxy/Cargo.toml | sed 's/.*version = "\([^"]*\)".*/\1/')"
|
||||
@echo " mcp-stdio-proxy 依赖的 mcp-sse-proxy: $$(grep 'mcp-sse-proxy = { version' mcp-proxy/Cargo.toml | sed 's/.*version = "\([^"]*\)".*/\1/')"
|
||||
@echo " mcp-stdio-proxy 依赖的 mcp-streamable-proxy: $$(grep 'mcp-streamable-proxy = { version' mcp-proxy/Cargo.toml | sed 's/.*version = "\([^"]*\)".*/\1/')"
|
||||
|
||||
# 发布所有 MCP 相关包(按依赖顺序)
|
||||
.PHONY: mcp-publish
|
||||
mcp-publish:
|
||||
@echo "📦 开始发布 MCP 相关包到 crates.io..."
|
||||
@echo ""
|
||||
@echo "1️⃣ 发布 mcp-common..."
|
||||
cd mcp-common && cargo publish
|
||||
@echo "⏳ 等待 10 秒让 crates.io 索引更新..."
|
||||
@sleep 10
|
||||
@echo ""
|
||||
@echo "2️⃣ 发布 mcp-sse-proxy..."
|
||||
cd mcp-sse-proxy && cargo publish
|
||||
@echo "⏳ 等待 10 秒让 crates.io 索引更新..."
|
||||
@sleep 10
|
||||
@echo ""
|
||||
@echo "3️⃣ 发布 mcp-streamable-proxy..."
|
||||
cd mcp-streamable-proxy && cargo publish
|
||||
@echo "⏳ 等待 10 秒让 crates.io 索引更新..."
|
||||
@sleep 10
|
||||
@echo ""
|
||||
@echo "4️⃣ 发布 mcp-stdio-proxy..."
|
||||
cd mcp-proxy && cargo publish
|
||||
@echo ""
|
||||
@echo "✅ 所有 MCP 包发布成功!"
|
||||
|
||||
# 预览将要发布的 MCP 包(dry-run)
|
||||
.PHONY: mcp-publish-dry-run
|
||||
mcp-publish-dry-run:
|
||||
@echo "🔍 预览将要发布的 MCP 包..."
|
||||
@echo ""
|
||||
@echo "1️⃣ mcp-common:"
|
||||
cd mcp-common && cargo publish --dry-run
|
||||
@echo ""
|
||||
@echo "2️⃣ mcp-sse-proxy:"
|
||||
cd mcp-sse-proxy && cargo publish --dry-run
|
||||
@echo ""
|
||||
@echo "3️⃣ mcp-streamable-proxy:"
|
||||
cd mcp-streamable-proxy && cargo publish --dry-run
|
||||
@echo ""
|
||||
@echo "4️⃣ mcp-stdio-proxy:"
|
||||
cd mcp-proxy && cargo publish --dry-run
|
||||
@echo ""
|
||||
@echo "✅ 预览完成(未实际发布)"
|
||||
|
||||
# 查看将要发布的文件列表
|
||||
.PHONY: mcp-package-list
|
||||
mcp-package-list:
|
||||
@echo "📋 查看各包将包含的文件..."
|
||||
@echo ""
|
||||
@echo "1️⃣ mcp-common:"
|
||||
cd mcp-common && cargo package --list
|
||||
@echo ""
|
||||
@echo "2️⃣ mcp-sse-proxy:"
|
||||
cd mcp-sse-proxy && cargo package --list
|
||||
@echo ""
|
||||
@echo "3️⃣ mcp-streamable-proxy:"
|
||||
cd mcp-streamable-proxy && cargo package --list
|
||||
@echo ""
|
||||
@echo "4️⃣ mcp-stdio-proxy:"
|
||||
cd mcp-proxy && cargo package --list
|
||||
|
||||
# 清理构建文件
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@echo "🧹 清理构建文件..."
|
||||
rm -rf $(OUTPUT_DIR)
|
||||
@echo "✅ 清理完成"
|
||||
|
||||
|
||||
|
||||
# 清理 Docker 镜像
|
||||
.PHONY: clean-images
|
||||
clean-images:
|
||||
@echo "🧹 清理 Docker 镜像..."
|
||||
docker rmi $(IMAGE_NAME):latest 2>/dev/null || true
|
||||
docker builder prune -f
|
||||
@echo "✅ Docker 镜像清理完成"
|
||||
|
||||
# 显示帮助信息
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "📖 可用的 Make 命令:"
|
||||
@echo ""
|
||||
@echo " 📄 Document Parser 构建:"
|
||||
@echo " make build-document-parser-x86_64 - 构建 document-parser Linux x86_64 版本(默认)"
|
||||
@echo " make build-document-parser-arm64 - 构建 document-parser Linux ARM64 版本"
|
||||
@echo " make build-document-parser-multi - 构建 document-parser 多平台版本"
|
||||
@echo ""
|
||||
@echo " 🎤 Voice CLI 构建:"
|
||||
@echo " make build-voice-cli-x86_64 - 构建 voice-cli Linux x86_64 版本"
|
||||
@echo " make build-voice-cli-arm64 - 构建 voice-cli Linux ARM64 版本"
|
||||
@echo " make build-voice-cli-multi - 构建 voice-cli 多平台版本"
|
||||
@echo ""
|
||||
@echo " 🔌 MCP Proxy 构建:"
|
||||
@echo " make build-mcp-proxy - 构建 mcp-proxy(当前系统架构)"
|
||||
@echo ""
|
||||
@echo " 🔧 所有组件构建:"
|
||||
@echo " make build-all - 构建所有组件(当前系统架构)"
|
||||
@echo ""
|
||||
@echo " 🐳 Docker 镜像:"
|
||||
@echo " make build-image - 构建 mcp-proxy Docker 运行镜像"
|
||||
@echo " make build-image-document-parser - 构建 document-parser Docker 运行镜像"
|
||||
@echo ""
|
||||
@echo " 🚀 运行命令:"
|
||||
@echo " make run - 构建 + 后台启动 mcp-proxy(docker-compose -d)"
|
||||
@echo " make run-fg - 构建 + 前台启动 mcp-proxy(docker-compose)"
|
||||
@echo " make run-document-parser - 运行 document-parser Docker 镜像"
|
||||
@echo ""
|
||||
@echo " 🛠️ 工具命令:"
|
||||
@echo " make check-buildx - 检查 Docker buildx 状态"
|
||||
@echo " make setup-buildx - 设置 Docker buildx builder"
|
||||
@echo ""
|
||||
@echo " 📦 MCP 发布命令:"
|
||||
@echo " make mcp-version-show - 显示当前所有 MCP 包的版本号"
|
||||
@echo " make mcp-version-update - 自动更新版本号(小版本号加一)"
|
||||
@echo " make mcp-publish - 发布所有 MCP 包到 crates.io(按依赖顺序)"
|
||||
@echo " make mcp-publish-dry-run - 预览将要发布的内容(不实际发布)"
|
||||
@echo " make mcp-package-list - 查看各包将包含的文件列表"
|
||||
@echo ""
|
||||
@echo " 🧹 清理命令:"
|
||||
@echo " make clean - 清理所有构建文件"
|
||||
@echo " make clean-images - 清理 Docker 镜像"
|
||||
@echo ""
|
||||
@echo " ❓ 其他:"
|
||||
@echo " make help - 显示此帮助信息"
|
||||
@echo ""
|
||||
@echo "📝 示例用法:"
|
||||
@echo " make # 构建 document-parser Linux x86_64 版本"
|
||||
@echo " make build-mcp-proxy # 构建 mcp-proxy(当前架构)"
|
||||
@echo " make build-all # 构建所有组件(当前架构)"
|
||||
@echo " make build-image # 构建 mcp-proxy Docker 镜像"
|
||||
@echo " make run # 构建 + 后台启动 mcp-proxy 服务"
|
||||
@echo " make run-fg # 构建 + 前台启动 mcp-proxy 服务"
|
||||
@echo " make mcp-version-show # 查看当前版本号"
|
||||
@echo " make mcp-publish-dry-run # 预览 MCP 发布(建议先运行此命令)"
|
||||
@echo ""
|
||||
@echo "📊 输出目录: ./dist/"
|
||||
@echo " mcp-proxy/ # MCP Proxy 二进制文件(当前架构)"
|
||||
@echo " document-parser-x86_64/ # Document Parser x86_64 二进制文件"
|
||||
@echo " document-parser-arm64/ # Document Parser ARM64 二进制文件"
|
||||
@echo " voice-cli-x86_64/ # Voice CLI x86_64 二进制文件"
|
||||
@echo " voice-cli-arm64/ # Voice CLI ARM64 二进制文件"
|
||||
@echo ""
|
||||
@echo "📁 Docker 目录: ./docker/"
|
||||
@echo " Dockerfile.mcp-proxy # mcp-proxy Docker 构建文件"
|
||||
@echo " Dockerfile.document-parser # document-parser/voice-cli Docker 构建文件"
|
||||
@echo " config.yml # mcp-proxy 默认配置文件"
|
||||
@echo " docker-compose.yml # Docker Compose 配置文件"
|
||||
281
qiming-mcp-proxy/README.md
Normal file
281
qiming-mcp-proxy/README.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# MCP-Proxy Workspace
|
||||
|
||||
**[English](README.md)** | **[简体中文](README_zh-CN.md)**
|
||||
|
||||
---
|
||||
|
||||
# MCP-Proxy Workspace
|
||||
|
||||
A comprehensive Rust workspace implementing MCP (Model Context Protocol) proxy system with multiple services including document parsing, voice transcription, and protocol conversion.
|
||||
|
||||
[](LICENSE)
|
||||
[](https://www.rust-lang.org/)
|
||||
|
||||
## Workspace Members
|
||||
|
||||
| Crate | Version | Description |
|
||||
|-------|---------|-------------|
|
||||
| **mcp-common** | 0.1.5 | Shared types and utilities for MCP proxy components |
|
||||
| **mcp-sse-proxy** | 0.1.5 | SSE (Server-Sent Events) proxy implementation using rmcp 0.10 |
|
||||
| **mcp-streamable-proxy** | 0.1.5 | Streamable HTTP proxy implementation using rmcp 0.12 |
|
||||
| **mcp-stdio-proxy** | 0.1.18 | Main MCP proxy server with CLI tool for protocol conversion |
|
||||
| **document-parser** | 0.1.0 | High-performance multi-format document parsing service |
|
||||
| **voice-cli** | 0.1.0 | Speech-to-text HTTP service with Whisper model support |
|
||||
| **oss-client** | 0.1.0 | Lightweight Alibaba Cloud OSS client library |
|
||||
| **fastembed** | 0.1.0 | Text embedding HTTP service using FastEmbed |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Rust**: 1.70 or later (recommended 1.75+)
|
||||
- **Python**: 3.8+ (for document-parser and voice-cli TTS)
|
||||
- **uv**: Python package manager (install via `curl -LsSf https://astral.sh/uv/install.sh | sh`)
|
||||
|
||||
### Installation
|
||||
|
||||
#### Method 1: Pre-built Binaries (Recommended)
|
||||
|
||||
**Using installation script (Linux/macOS):**
|
||||
```bash
|
||||
# Install mcp-proxy
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/mcp-stdio-proxy-installer.sh | sh
|
||||
|
||||
# Install document-parser
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/document-parser-installer.sh | sh
|
||||
|
||||
# Install voice-cli
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/voice-cli-installer.sh | sh
|
||||
```
|
||||
|
||||
**Using installation script (Windows PowerShell):**
|
||||
```powershell
|
||||
# Install mcp-proxy
|
||||
irm https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/mcp-stdio-proxy-installer.ps1 | iex
|
||||
|
||||
# Install document-parser
|
||||
irm https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/document-parser-installer.ps1 | iex
|
||||
|
||||
# Install voice-cli
|
||||
irm https://github.com/nuwax-ai/mcp-proxy/releases/latest/download/voice-cli-installer.ps1 | iex
|
||||
```
|
||||
|
||||
**Download from GitHub Releases:**
|
||||
Visit [GitHub Releases](https://github.com/nuwax-ai/mcp-proxy/releases) to download binaries for your platform.
|
||||
|
||||
Supported platforms:
|
||||
- Linux x86_64
|
||||
- Linux ARM64
|
||||
- macOS Intel (x86_64)
|
||||
- macOS Apple Silicon (ARM64)
|
||||
- Windows x86_64
|
||||
|
||||
#### Method 2: cargo install
|
||||
|
||||
```bash
|
||||
cargo install mcp-stdio-proxy
|
||||
```
|
||||
|
||||
#### Method 3: Build from Source
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/nuwax-ai/mcp-proxy.git
|
||||
cd mcp-proxy
|
||||
|
||||
# Build all workspace members
|
||||
cargo build --release
|
||||
|
||||
# Or build specific crates
|
||||
cargo build -p mcp-proxy
|
||||
cargo build -p document-parser
|
||||
cargo build -p voice-cli
|
||||
```
|
||||
|
||||
### MCP Proxy (mcp-stdio-proxy)
|
||||
|
||||
The main proxy service that converts SSE/Streamable HTTP to stdio protocol.
|
||||
|
||||
```bash
|
||||
# Install from source
|
||||
cargo install --path ./mcp-proxy
|
||||
|
||||
# Start the proxy server
|
||||
mcp-proxy
|
||||
|
||||
# Convert remote MCP service to stdio
|
||||
mcp-proxy convert https://example.com/mcp/sse
|
||||
|
||||
# Check service status
|
||||
mcp-proxy check https://example.com/mcp/sse
|
||||
|
||||
# Detect protocol type
|
||||
mcp-proxy detect https://example.com/mcp
|
||||
```
|
||||
|
||||
**See:** [mcp-proxy/README.md](./mcp-proxy/README.md) for detailed documentation.
|
||||
|
||||
### Document Parser
|
||||
|
||||
High-performance document parsing service supporting PDF, Word, Excel, and PowerPoint.
|
||||
|
||||
```bash
|
||||
cd document-parser
|
||||
|
||||
# Initialize Python environment (first time)
|
||||
document-parser uv-init
|
||||
|
||||
# Check environment status
|
||||
document-parser check
|
||||
|
||||
# Start HTTP server
|
||||
document-parser server
|
||||
```
|
||||
|
||||
**See:** [document-parser/README.md](./document-parser/README.md) for detailed documentation.
|
||||
|
||||
### Voice CLI
|
||||
|
||||
Speech-to-text HTTP service with Whisper model support.
|
||||
|
||||
```bash
|
||||
cd voice-cli
|
||||
|
||||
# Initialize server configuration
|
||||
voice-cli server init
|
||||
|
||||
# Run voice server
|
||||
voice-cli server run
|
||||
|
||||
# List Whisper models
|
||||
voice-cli model list
|
||||
|
||||
# Download model
|
||||
voice-cli model download tiny
|
||||
```
|
||||
|
||||
**See:** [voice-cli/README.md](./voice-cli/README.md) for detailed documentation.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Services
|
||||
|
||||
#### 1. MCP Proxy System
|
||||
|
||||
- **mcp-common**: Shared configuration types and utilities
|
||||
- **mcp-sse-proxy**: SSE protocol support (rmcp 0.10)
|
||||
- **mcp-streamable-proxy**: Streamable HTTP protocol support (rmcp 0.12)
|
||||
- **mcp-stdio-proxy**: Main CLI tool for protocol conversion
|
||||
|
||||
**Features:**
|
||||
- Multi-protocol support: SSE, Streamable HTTP, stdio
|
||||
- Dynamic plugin loading
|
||||
- Protocol auto-detection and conversion
|
||||
- OpenTelemetry integration with OTLP
|
||||
- Background health checks
|
||||
|
||||
#### 2. Document Parser
|
||||
|
||||
**Features:**
|
||||
- Multi-format support: PDF (MinerU), Word/Excel/PowerPoint (MarkItDown)
|
||||
- GPU acceleration via CUDA/sglang (optional)
|
||||
- Python environment management with uv
|
||||
- HTTP API with OpenAPI documentation
|
||||
- OSS integration for cloud storage
|
||||
|
||||
#### 3. Voice CLI
|
||||
|
||||
**Features:**
|
||||
- Whisper model integration (tiny/base/small/medium/large)
|
||||
- Multi-format audio support (MP3, WAV, FLAC, M4A, etc.)
|
||||
- Apalis-based async task queue with SQLite persistence
|
||||
- FFmpeg integration for metadata extraction
|
||||
- **TTS service (TODO - currently has issues)**
|
||||
|
||||
#### 4. Utility Libraries
|
||||
|
||||
- **oss-client**: Alibaba Cloud OSS client with unified interface
|
||||
- **fastembed**: Text embedding HTTP service using FastEmbed
|
||||
|
||||
## Development
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Build all workspace crates
|
||||
cargo build
|
||||
|
||||
# Build specific crate
|
||||
cargo build -p mcp-proxy
|
||||
|
||||
# Build in release mode
|
||||
cargo build --release
|
||||
|
||||
# Run tests for all crates
|
||||
cargo test
|
||||
|
||||
# Run tests for specific crate
|
||||
cargo test -p mcp-proxy
|
||||
|
||||
# Run clippy for linting
|
||||
cargo clippy --all-targets --all-features
|
||||
|
||||
# Format code
|
||||
cargo fmt
|
||||
```
|
||||
|
||||
### Cross-Platform Building (Docker)
|
||||
|
||||
```bash
|
||||
# Build document-parser for Linux x86_64
|
||||
make build-document-parser-x86_64
|
||||
|
||||
# Build document-parser for Linux ARM64
|
||||
make build-document-parser-arm64
|
||||
|
||||
# Build voice-cli for Linux x86_64
|
||||
make build-voice-cli-x86_64
|
||||
|
||||
# Build all components for x86_64
|
||||
make build-all-x86_64
|
||||
|
||||
# Build Docker runtime image
|
||||
make build-image
|
||||
|
||||
# Run Docker container
|
||||
make run
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
- Line length: 100 characters
|
||||
- 4-space indentation (no tabs)
|
||||
- Use `dashmap` for concurrent hashmaps instead of `Arc<RwLock<HashMap>>`
|
||||
- Follow KISS and SOLID principles
|
||||
- "Fail fast" error handling with `anyhow::Context`
|
||||
|
||||
## Documentation
|
||||
|
||||
- [CLAUDE.md](./CLAUDE.md) - Development guide for contributors
|
||||
- [mcp-proxy/README.md](./mcp-proxy/README.md) - MCP Proxy documentation
|
||||
- [document-parser/README.md](./document-parser/README.md) - Document Parser documentation
|
||||
- [voice-cli/README.md](./voice-cli/README.md) - Voice CLI documentation
|
||||
- [oss-client/README.md](./oss-client/README.md) - OSS Client documentation
|
||||
|
||||
## License
|
||||
|
||||
This project is dual-licensed under MIT OR Apache-2.0.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit issues and pull requests.
|
||||
|
||||
- **GitHub Repository**: https://github.com/nuwax-ai/mcp-proxy
|
||||
- **Issue Tracker**: https://github.com/nuwax-ai/mcp-proxy/issues
|
||||
- **Discussions**: https://github.com/nuwax-ai/mcp-proxy/discussions
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [MCP Official Documentation](https://modelcontextprotocol.io/)
|
||||
- [rmcp - Rust MCP Implementation](https://crates.io/crates/rmcp)
|
||||
- [MCP Servers List](https://github.com/modelcontextprotocol/servers)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user