add agent
This commit is contained in:
parent
8f2165d324
commit
514de7f3b7
@ -19,6 +19,7 @@ cloud/users/**
|
|||||||
# Logs and local IDE files
|
# Logs and local IDE files
|
||||||
logs/
|
logs/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.devcontainer/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
@ -30,3 +31,6 @@ archive/
|
|||||||
|
|
||||||
# Ignore local environment files
|
# Ignore local environment files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Ignore Windows solution files
|
||||||
|
*.sln
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -18,6 +18,9 @@ node_modules/
|
|||||||
# Logs
|
# Logs
|
||||||
logs/
|
logs/
|
||||||
cloud/*
|
cloud/*
|
||||||
|
|
||||||
|
# Dev Container (not needed for local or Docker-only workflows)
|
||||||
|
.devcontainer/
|
||||||
# 但是不要忽略 custom_nodes 文件夹
|
# 但是不要忽略 custom_nodes 文件夹
|
||||||
!cloud/custom_nodes/
|
!cloud/custom_nodes/
|
||||||
|
|
||||||
|
|||||||
46
.vscode/launch.json
vendored
Normal file
46
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "FastAPI: Uvicorn (Local Debug)",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "uvicorn",
|
||||||
|
"args": [
|
||||||
|
"server.main:app",
|
||||||
|
"--host",
|
||||||
|
"127.0.0.1",
|
||||||
|
"--port",
|
||||||
|
"8000",
|
||||||
|
"--reload"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"PYTHONPATH": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"justMyCode": true,
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FastAPI: Debug Mode (debugpy wait)",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/server/debug_run.py",
|
||||||
|
"env": {
|
||||||
|
"PYTHONPATH": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"justMyCode": true,
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Attach to debugpy (5678)",
|
||||||
|
"type": "python",
|
||||||
|
"request": "attach",
|
||||||
|
"connect": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5678
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
308
DEPLOYMENT.md
308
DEPLOYMENT.md
@ -1,300 +1,88 @@
|
|||||||
# TraceStudio 部署说明(Windows + Docker Desktop)
|
# TraceStudio 部署说明
|
||||||
|
|
||||||
本文档包含将 TraceStudio 部署到共享 Windows 机器(使用 Docker Desktop)的步骤、建议的卷挂载、端口说明、回滚方法与常见故障排查。
|
本文档说明本地开发调试和 Docker 部署的方式。
|
||||||
|
|
||||||
## 前提
|
## 目录
|
||||||
- 已安装 Docker Desktop(Windows)并启用 WSL2 或 Hyper-V。确保 Docker Desktop 有权限访问 `D:` 驱动器(Settings → Resources → File Sharing)。
|
1. [本地调试模式](#本地调试模式)
|
||||||
- 项目位于 `D:/XGame/TraceStudio`。
|
2. [Docker 部署](#docker-部署)
|
||||||
- 确认 `archive/`、`dist/` 在 `.gitignore` 中被忽略。
|
3. [故障排查](#故障排查)
|
||||||
|
|
||||||
## 构建镜像(从源码)
|
---
|
||||||
在项目根运行:
|
|
||||||
|
|
||||||
|
## 本地调试模式
|
||||||
|
|
||||||
|
**前置条件**:虚拟环境已激活,依赖已安装
|
||||||
```powershell
|
```powershell
|
||||||
docker compose build
|
|
||||||
```
|
|
||||||
|
|
||||||
开关说明:`docker-compose.yml` 已配置将 `D:/XGame/TraceStudio/cloud` 挂载到容器内 `/opt/tracestudio/cloud`,以便 `server` 访问共享文件。
|
|
||||||
|
|
||||||
## 启动服务
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
docker compose logs -f server
|
|
||||||
docker compose logs -f web
|
|
||||||
```
|
|
||||||
|
|
||||||
前端默认通过 `http://<host>` (80) 访问,后端 API 通过 `http://<host>:8000` 访问。根据部署时的 `docker-compose.yml` 端口映射调整。
|
|
||||||
|
|
||||||
# TraceStudio — 标准作业程序 (SOP)
|
|
||||||
|
|
||||||
此文档为 TraceStudio 的部署与运维标准作业程序(SOP),适用于在共享 Windows 服务器上使用 Docker Desktop 进行部署,以及本地开发和打包流程。它覆盖先决条件、开发指南、构建打包流程(`prepare_dist.py`)、生产部署(Windows + Docker Desktop)、常见故障排查与回滚步骤。
|
|
||||||
|
|
||||||
重要:请勿将 `dist/` 提交到版本库。`dist/` 是由 `scripts/prepare_dist.py` 生成的构建产物,仅作打包和发布之用。
|
|
||||||
|
|
||||||
文件速查:
|
|
||||||
- `scripts/prepare_dist.py` — 生成干净部署包(copy / archive 模式)
|
|
||||||
- `scripts/build_release.py` — 自动化生成 timestamped release zip
|
|
||||||
- `docker-compose.yml` — 推荐的本地与生产 compose 配置
|
|
||||||
- `server/Dockerfile` — 生产多阶段 Dockerfile(参考)
|
|
||||||
- `server/requirements.txt`、`server/dev-requirements.txt`
|
|
||||||
- `archive/` — 被归档的原始配置与脚本备份
|
|
||||||
|
|
||||||
**目录结构与约定**
|
|
||||||
- `cloud/`:数据与自定义节点(应作为主机卷挂载到容器)
|
|
||||||
- `dist/`:临时构建输出(不应加入 Git)
|
|
||||||
- `archive/`:备份被移除或替换的文件
|
|
||||||
|
|
||||||
**目录权限与挂载约定(默认)**
|
|
||||||
- Host path: `D:/XGame/TraceStudio/cloud` → Container path: `/opt/tracestudio/cloud`
|
|
||||||
|
|
||||||
**修订**: 2026-01-12
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
**1. 环境准备 (Prerequisites)**
|
|
||||||
|
|
||||||
- 操作系统:Windows 10/11 或 Windows Server 2019/2022
|
|
||||||
- Docker:Docker Desktop(最新稳定版),建议启用 WSL2 后端
|
|
||||||
- Docker Compose:v2(随 Docker Desktop 提供)
|
|
||||||
- Python:3.9+(建议 3.10 / 3.11)
|
|
||||||
- Node.js:16+(用于前端构建)
|
|
||||||
- Git:用于源码管理与回退
|
|
||||||
|
|
||||||
推荐硬件(最小 / 建议):
|
|
||||||
- CPU:4 cores / 8 cores
|
|
||||||
- 内存:8 GB / 16 GB
|
|
||||||
- 磁盘:50 GB 可用(取决于 `cloud/` 数据大小,若存放大量 trace 数据请预留更多)
|
|
||||||
|
|
||||||
权限与网络
|
|
||||||
- 在 Docker Desktop 中允许对包含仓库与 `cloud/` 的主机驱动器授权(Settings → Resources → File Sharing)。
|
|
||||||
- 有权限运行 Docker Desktop、访问 `D:/XGame/TraceStudio` 路径,并写入 `archive/` 与 `dist/`。
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
**2. 开发指南 (Local Development)**
|
|
||||||
|
|
||||||
快速搭建本地开发环境:
|
|
||||||
|
|
||||||
1) 克隆仓库并创建 Python 虚拟环境
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git clone <repo-url> TraceStudio
|
|
||||||
cd TraceStudio
|
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
.\.venv\Scripts\Activate.ps1 # PowerShell
|
.\.venv\Scripts\Activate.ps1
|
||||||
```
|
|
||||||
|
|
||||||
2) 安装依赖(生产 + 开发)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pip install -r server/requirements.txt
|
pip install -r server/requirements.txt
|
||||||
pip install -r server/dev-requirements.txt
|
pip install -r server/dev-requirements.txt
|
||||||
cd web
|
cd web && npm install && cd ..
|
||||||
npm install
|
|
||||||
cd ..
|
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:`server/dev-requirements.txt` 包含开发工具(例如 `debugpy`, `pytest`, `black`, `mypy`, `pre-commit`, `flake8`)。用于本地调试与代码质量检查,不应被包含在生产镜像中。
|
**启动后端**(终端 1)
|
||||||
|
|
||||||
3) 配置本地环境变量
|
|
||||||
|
|
||||||
- 参考 `web/.env.example` 创建 `web/.env`(不要把 `.env` 提交到 Git)。
|
|
||||||
- 根据需要调整 `server/system_config.yaml` 中非敏感设置。
|
|
||||||
|
|
||||||
4) 本地启动(Debug 模式)
|
|
||||||
|
|
||||||
- 启动后端(带自动重载)
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd server
|
cd server
|
||||||
python -m uvicorn main:app --reload --host 127.0.0.1 --port 8000
|
python -m uvicorn main:app --reload --host 127.0.0.1 --port 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
- 使用 IDE 调试:`debugpy` 已包含在 `server/dev-requirements.txt`,可在 IDE 中设置远程调试端口并 attach。
|
**启动前端**(终端 2)
|
||||||
|
|
||||||
- 启动前端开发服务器
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd web
|
cd web
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
调试提示:
|
访问地址:
|
||||||
- 修改 `cloud/custom_nodes` 中的自定义节点后,若后端未自动热重载,请重启后端服务以加载新节点。
|
- 后端 API:`http://localhost:8000`
|
||||||
|
- 前端:`http://localhost:5173`
|
||||||
|
|
||||||
4.1) 基于 Docker 的开发(推荐)
|
**VS Code 调试**(可选)
|
||||||
|
- Ctrl+Shift+D 打开 Run and Debug
|
||||||
|
- 选择 **"FastAPI: Uvicorn (Local Debug)"**
|
||||||
|
- F5 启动,支持打断点
|
||||||
|
|
||||||
如果你希望在与生产镜像更接近的环境中开发、避免在主机上直接安装依赖,我们提供 `docker-compose.dev.yml`,它会:
|
**调试配置**(`.vscode/launch.json`)
|
||||||
- 把仓库作为卷挂载到容器内,便于热重载与编辑
|
- **FastAPI: Uvicorn (Local Debug)** ⭐ 推荐
|
||||||
- 在容器内安装并运行 `uvicorn --reload`(后端)和 Vite dev server(前端)
|
- **FastAPI: Debug Mode (debugpy wait)**
|
||||||
|
- **Attach to debugpy (5678)**
|
||||||
|
|
||||||
在仓库根运行:
|
---
|
||||||
|
|
||||||
|
## Docker 部署
|
||||||
|
|
||||||
|
**启动 Docker 服务**
|
||||||
```powershell
|
```powershell
|
||||||
docker compose -f docker-compose.dev.yml up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
然后:
|
|
||||||
- 后端 API 在 `http://localhost:8000`
|
|
||||||
- 前端 dev server 在 `http://localhost:5173`(Vite)
|
|
||||||
|
|
||||||
开发注意事项:
|
|
||||||
- 第一次运行会在容器内执行 `pip install -r server/requirements.txt` 与 `npm install`,根据网络与依赖大小可能需要几分钟。
|
|
||||||
- `docker-compose.dev.yml` 将宿主 `cloud/` 挂载到容器,确保 Docker Desktop 授权访问所在驱动器(如 `D:`)。
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
**3. 构建与发布 (Build & Ship)**
|
|
||||||
|
|
||||||
目标:生成一个只包含运行时所需文件的 `dist/` 包并(可选)创建 release zip,供部署或镜像构建使用。
|
|
||||||
|
|
||||||
为什么需要 `dist/`?
|
|
||||||
- `dist/` 是受控的、最小化的部署快照。它避免将开发依赖、缓存、临时文件或本地大数据误打包进生产镜像,并为构建过程提供可复现的输入。
|
|
||||||
|
|
||||||
生成 `dist/`(推荐流程)
|
|
||||||
|
|
||||||
1. 激活虚拟环境
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\.venv\Scripts\Activate.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 运行准备脚本(copy 模式)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python .\scripts\prepare_dist.py --mode copy
|
|
||||||
```
|
|
||||||
|
|
||||||
脚本说明:
|
|
||||||
- `--mode copy`:将需要的文件/目录复制到 `dist/`(不会删除源文件)。
|
|
||||||
- `--mode archive`:把标记为过大的或不应纳入 `dist/` 的文件移动到 `archive/`(脚本会记录操作并生成回滚信息)。
|
|
||||||
- 日志:脚本会将操作记录到 `scripts/prepare_dist.log`,包括回滚脚本路径。
|
|
||||||
|
|
||||||
3. (可选)创建 release ZIP
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python .\scripts\build_release.py
|
|
||||||
# 结果示例: release_20260112_202437.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 验证 `dist/` 内容
|
|
||||||
- `dist/` 应只包含:生产 `server/` 代码、构建后的 `web/dist`(或前端静态文件)、必要的配置(`system_config.yaml` 的非敏感部分)、`cloud/custom_nodes`(如果需要)。
|
|
||||||
|
|
||||||
安全建议:在运行 `prepare_dist.py --mode archive` 前,请确认 `archive/` 的访问与备份策略,以便在误移除时可恢复。
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
**4. 服务器部署 (Production Deploy) — 共享 Windows 服务器**
|
|
||||||
|
|
||||||
建议使用 Docker Compose 来管理 `server` 与 `web` 服务。以下步骤假定目标机器为共享的 Windows 主机,已安装 Docker Desktop 并允许访问托管驱动器。
|
|
||||||
|
|
||||||
1) 在目标机器上准备文件
|
|
||||||
|
|
||||||
- 选项 A(推荐):在开发机上运行 `scripts/build_release.py`,将生成的 `release_*.zip` 复制到目标机器并解压。
|
|
||||||
- 选项 B:在目标机器上从 Git 克隆并运行 `scripts/prepare_dist.py --mode copy`。
|
|
||||||
|
|
||||||
2) 检查 Docker Desktop 文件共享
|
|
||||||
|
|
||||||
- 打开 Docker Desktop → Settings → Resources → File Sharing,确认包含 `D:`(或项目所在驱动器)的条目。
|
|
||||||
|
|
||||||
3) 启动服务
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd <release-or-repo-root>
|
|
||||||
docker compose build
|
docker compose build
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
4) 卷挂载说明(重要)
|
访问地址:
|
||||||
|
- 后端 API:`http://localhost:8000`
|
||||||
- Compose 默认将 `D:/XGame/TraceStudio/cloud` 挂载到容器 `/opt/tracestudio/cloud`。若路径不同,请修改 `docker-compose.yml` 中的 volume 映射。
|
- 前端:`http://localhost:5173`
|
||||||
- 挂载为 read-write(:rw),并确保宿主机上的文件权限允许容器进程读写。
|
|
||||||
|
|
||||||
5) 运行时检查
|
|
||||||
|
|
||||||
|
**查看日志**
|
||||||
```powershell
|
```powershell
|
||||||
docker compose ps
|
|
||||||
docker compose logs -f server
|
docker compose logs -f server
|
||||||
|
docker compose logs -f web
|
||||||
```
|
```
|
||||||
|
|
||||||
--
|
**停止服务**
|
||||||
|
|
||||||
**5. 故障排查 (Troubleshooting)**
|
|
||||||
|
|
||||||
- 容器无法访问主机卷
|
|
||||||
- 确认 Docker Desktop 已授予驱动器访问权限并重启 Docker。
|
|
||||||
- 检查宿主机目录权限(使用管理员权限查看)。
|
|
||||||
|
|
||||||
- 应用异常退出或 `uvicorn` 找不到 `main:app`
|
|
||||||
- 查看 `server/Dockerfile` 是否将代码拷贝到镜像内部正确的路径。
|
|
||||||
- 在容器内运行 `python -c "import main; print(main)"` 检查 import 错误。
|
|
||||||
|
|
||||||
- 端口被占用
|
|
||||||
- 执行 `netstat -ano | findstr :8000` 查找占用进程,或修改 `docker-compose.yml` 端口映射。
|
|
||||||
|
|
||||||
- 构建失败因依赖
|
|
||||||
- 检查 `server/requirements.txt` 是否包含生产所需的库。
|
|
||||||
- 在本地虚拟环境运行 `pip install -r server/requirements.txt` 验证。
|
|
||||||
|
|
||||||
- 日志采集
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker compose logs server --no-log-prefix --tail 200
|
|
||||||
docker compose logs web --no-log-prefix --tail 200
|
|
||||||
```
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
**6. 回滚 (Rollback)**
|
|
||||||
|
|
||||||
步骤(快速)
|
|
||||||
|
|
||||||
1. 停止服务
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
docker compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 从历史 release 恢复
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Expand-Archive -Path .\release_<YYYYMMDD_HHMMSS>.zip -DestinationPath .\dist -Force
|
|
||||||
cd dist
|
|
||||||
docker compose build
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 恢复被归档的文件(如适用)
|
|
||||||
|
|
||||||
- 检查 `archive/` 中的备份,按需复制回原位或使用脚本中记录的回滚路径。
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
**附录:常用命令速查**
|
|
||||||
|
|
||||||
- 生成 dist:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python .\scripts\prepare_dist.py --mode copy
|
|
||||||
```
|
|
||||||
|
|
||||||
- 生成 release zip:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python .\scripts\build_release.py
|
|
||||||
```
|
|
||||||
|
|
||||||
- 运行生产堆栈:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker compose down
|
|
||||||
docker compose build
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
若需把 SOP 定制为企业内部流程(例如 CI/CD、私有镜像仓库、自动化回滚或与公司监控系统整合),请告知要接入的工具/平台,我可继续为你扩展自动化脚本与 CI 配置。
|
## 故障排查
|
||||||
|
|
||||||
|
| 问题 | 解决方案 |
|
||||||
|
|------|--------|
|
||||||
|
| 端口 8000 被占用 | `netstat -ano \| findstr :8000` 找到进程后关闭 |
|
||||||
|
| 容器启动失败 | `docker compose logs server` 查看错误日志 |
|
||||||
|
| 虚拟环境找不到依赖 | 重新激活并运行 `pip install -r server/requirements.txt` |
|
||||||
|
| 自定义节点未加载 | 本地:重启后端;Docker:`docker compose restart server` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**:2026-01-13
|
||||||
|
|
||||||
|
|||||||
18
agent/config.yaml
Normal file
18
agent/config.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Agent 配置示例
|
||||||
|
base_workdir: "D:/XGame/TraceStudio" # 作为默认工作目录(可被请求覆盖)
|
||||||
|
shared_root: "D:/XGame/TraceStudio/cloud" # 文件类输出请写到此目录,供 Docker 挂载访问
|
||||||
|
|
||||||
|
# 可用工具清单(按名称映射到可执行文件)
|
||||||
|
tools:
|
||||||
|
unreal_editor:
|
||||||
|
path: "C:/Program Files/Epic Games/UE_5.3/Engine/Binaries/Win64/UnrealEditor.exe"
|
||||||
|
unreal_insights:
|
||||||
|
path: "D:/XGame/unrealengine/Engine/Binaries/Win64/UnrealInsights.exe"
|
||||||
|
python:
|
||||||
|
path: "C:/Python311/python.exe"
|
||||||
|
|
||||||
|
# 运行默认值
|
||||||
|
run_defaults:
|
||||||
|
timeout: 300
|
||||||
|
capture_output: true
|
||||||
|
strip_output: true
|
||||||
0
agent/core/__init__.py
Normal file
0
agent/core/__init__.py
Normal file
23
agent/core/models.py
Normal file
23
agent/core/models.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from typing import Dict, List, Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class RunRequest(BaseModel):
|
||||||
|
tool: str = Field(..., description="工具名称,对应 config.yaml 的 tools key")
|
||||||
|
args: List[str] = Field(default_factory=list, description="传给可执行文件的参数")
|
||||||
|
workdir: Optional[str] = Field(None, description="工作目录,默认使用 config.base_workdir")
|
||||||
|
timeout: Optional[int] = Field(None, description="超时时间秒,默认取 config.run_defaults")
|
||||||
|
env: Optional[Dict[str, str]] = Field(None, description="额外环境变量")
|
||||||
|
capture_output: Optional[bool] = Field(None, description="是否捕获 stdout/stderr")
|
||||||
|
strip_output: Optional[bool] = Field(None, description="是否 strip 输出换行/空白")
|
||||||
|
|
||||||
|
|
||||||
|
class RunResult(BaseModel):
|
||||||
|
success: bool
|
||||||
|
return_code: Optional[int] = None
|
||||||
|
stdout: Optional[str] = None
|
||||||
|
stderr: Optional[str] = None
|
||||||
|
elapsed: Optional[float] = None
|
||||||
|
tool_path: Optional[str] = None
|
||||||
|
workdir: Optional[str] = None
|
||||||
|
output_path: Optional[str] = None # 若生成文件,返回落盘路径(由调用方基于共享卷读取)
|
||||||
88
agent/core/runner.py
Normal file
88
agent/core/runner.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from .models import RunRequest, RunResult
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_path: str | Path = None) -> Dict[str, Any]:
|
||||||
|
"""加载配置,默认使用 AGENT_CONFIG 或 ./config.yaml。"""
|
||||||
|
if config_path is None:
|
||||||
|
env_path = os.environ.get("AGENT_CONFIG")
|
||||||
|
config_path = Path(env_path) if env_path else Path(__file__).resolve().parent.parent / "config.yaml"
|
||||||
|
config_path = Path(config_path)
|
||||||
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_tool(config: Dict[str, Any], tool_name: str) -> Path:
|
||||||
|
tools = config.get("tools", {})
|
||||||
|
if tool_name not in tools:
|
||||||
|
raise ValueError(f"未知工具: {tool_name}")
|
||||||
|
tool_path = Path(tools[tool_name]["path"])
|
||||||
|
if not tool_path.exists():
|
||||||
|
raise FileNotFoundError(f"工具不存在: {tool_path}")
|
||||||
|
return tool_path
|
||||||
|
|
||||||
|
|
||||||
|
def run_tool(config: Dict[str, Any], req: RunRequest) -> RunResult:
|
||||||
|
start = time.time()
|
||||||
|
defaults = config.get("run_defaults", {})
|
||||||
|
|
||||||
|
tool_path = resolve_tool(config, req.tool)
|
||||||
|
workdir = Path(req.workdir or config.get("base_workdir", ".")).resolve()
|
||||||
|
workdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 处理参数与环境
|
||||||
|
args = [str(tool_path)] + req.args
|
||||||
|
env = os.environ.copy()
|
||||||
|
if config.get("env"):
|
||||||
|
env.update({k: str(v) for k, v in config["env"].items()})
|
||||||
|
if req.env:
|
||||||
|
env.update({k: str(v) for k, v in req.env.items()})
|
||||||
|
|
||||||
|
timeout = req.timeout or defaults.get("timeout")
|
||||||
|
capture_output = req.capture_output if req.capture_output is not None else defaults.get("capture_output", True)
|
||||||
|
strip_output = req.strip_output if req.strip_output is not None else defaults.get("strip_output", True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(
|
||||||
|
args,
|
||||||
|
cwd=str(workdir),
|
||||||
|
env=env,
|
||||||
|
timeout=timeout,
|
||||||
|
capture_output=capture_output,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
stdout = completed.stdout if capture_output else None
|
||||||
|
stderr = completed.stderr if capture_output else None
|
||||||
|
if strip_output:
|
||||||
|
stdout = stdout.strip() if stdout is not None else None
|
||||||
|
stderr = stderr.strip() if stderr is not None else None
|
||||||
|
|
||||||
|
elapsed = time.time() - start
|
||||||
|
return RunResult(
|
||||||
|
success=completed.returncode == 0,
|
||||||
|
return_code=completed.returncode,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
elapsed=elapsed,
|
||||||
|
tool_path=str(tool_path),
|
||||||
|
workdir=str(workdir),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
elapsed = time.time() - start
|
||||||
|
return RunResult(
|
||||||
|
success=False,
|
||||||
|
return_code=None,
|
||||||
|
stdout=None,
|
||||||
|
stderr=str(exc),
|
||||||
|
elapsed=elapsed,
|
||||||
|
tool_path=str(tool_path),
|
||||||
|
workdir=str(workdir),
|
||||||
|
)
|
||||||
45
agent/main.py
Normal file
45
agent/main.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from core.runner import load_config, run_tool
|
||||||
|
from core.models import RunRequest, RunResult
|
||||||
|
|
||||||
|
CONFIG = load_config()
|
||||||
|
|
||||||
|
app = FastAPI(title="TraceStudio Agent", version="0.1.0")
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"] ,
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
tools = list((CONFIG.get("tools") or {}).keys())
|
||||||
|
return {"status": "ok", "tools": tools}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/run", response_model=RunResult)
|
||||||
|
def run(req: RunRequest):
|
||||||
|
try:
|
||||||
|
return run_tool(CONFIG, req)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
host = os.environ.get("AGENT_HOST", "0.0.0.0")
|
||||||
|
port = int(os.environ.get("AGENT_PORT", "8100"))
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run("main:app", host=host, port=port, reload=False)
|
||||||
3
agent/requirements.txt
Normal file
3
agent/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
pyyaml==6.0.1
|
||||||
@ -8,8 +8,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./:/app:cached
|
- ./:/app:cached
|
||||||
- ./cloud:/opt/tracestudio/cloud:rw
|
- ./cloud:/opt/tracestudio/cloud:rw
|
||||||
working_dir: /app/server
|
working_dir: /app
|
||||||
command: ["/bin/sh", "-c", "pip install -r /app/server/requirements.txt && python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload"]
|
command: ["/bin/sh", "-c", "pip install -r /app/server/requirements.txt && PYTHONPATH=/app python -m uvicorn server.main:app --host 0.0.0.0 --port 8000 --reload"]
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
environment:
|
||||||
@ -26,3 +26,4 @@ services:
|
|||||||
command: ["/bin/sh", "-c", "npm install --no-audit --no-fund && npm run dev -- --host 0.0.0.0"]
|
command: ["/bin/sh", "-c", "npm install --no-audit --no-fund && npm run dev -- --host 0.0.0.0"]
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,9 @@ IGNORE_PATTERNS = [
|
|||||||
"node_modules",
|
"node_modules",
|
||||||
"*.pyc",
|
"*.pyc",
|
||||||
"*.pyo",
|
"*.pyo",
|
||||||
|
".vscode",
|
||||||
|
".devcontainer",
|
||||||
|
"*.sln",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
13
scripts/start_debug_server.ps1
Normal file
13
scripts/start_debug_server.ps1
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 调试模式启动:只启动前端 + idle 的后端容器,便于手动/VS Code 调试
|
||||||
|
# 用法:在仓库根执行
|
||||||
|
# ./scripts/start_debug_server.ps1
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ComposeDev = "docker-compose.dev.yml",
|
||||||
|
[string]$ComposeDebug = "docker-compose.debug.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "Starting web + server (debug override) ..." -ForegroundColor Cyan
|
||||||
|
& docker compose -f $ComposeDev -f $ComposeDebug up -d web server
|
||||||
|
|
||||||
|
Write-Host "Done. Server is idle. You can now start uvicorn manually inside the container or via VS Code launch configurations." -ForegroundColor Green
|
||||||
@ -19,6 +19,7 @@ from server.app.core.node_base import (
|
|||||||
CachePolicy
|
CachePolicy
|
||||||
)
|
)
|
||||||
from server.app.core.node_registry import register_node
|
from server.app.core.node_registry import register_node
|
||||||
|
from server.app.services.agent_client import AgentClient
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +32,10 @@ def load_system_config():
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _agent_client() -> AgentClient:
|
||||||
|
return AgentClient.from_env(container_cloud_root=CLOUD_ROOT)
|
||||||
|
|
||||||
|
|
||||||
@register_node
|
@register_node
|
||||||
class TraceLoader_Metadata(TraceNode):
|
class TraceLoader_Metadata(TraceNode):
|
||||||
"""
|
"""
|
||||||
@ -84,47 +89,36 @@ class TraceLoader_Metadata(TraceNode):
|
|||||||
if not utrace_full_path.exists():
|
if not utrace_full_path.exists():
|
||||||
raise FileNotFoundError(f"Utrace 文件不存在:{utrace_full_path}")
|
raise FileNotFoundError(f"Utrace 文件不存在:{utrace_full_path}")
|
||||||
|
|
||||||
# 构建输出 CSV 完整路径
|
# 构建输出 CSV 完整路径(容器内路径 + 宿主机路径,宿主机路径给 Agent 用)
|
||||||
csv_full_path = CLOUD_ROOT / output_csv
|
csv_full_path = CLOUD_ROOT / output_csv
|
||||||
|
agent = _agent_client()
|
||||||
|
host_csv_full_path = agent.map_cloud_to_host(output_csv)
|
||||||
|
|
||||||
# 确保输出目录存在
|
# 确保输出目录存在(容器内),Agent 写宿主路径时也会自动创建
|
||||||
csv_full_path.parent.mkdir(parents=True, exist_ok=True)
|
csv_full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if csv_full_path.exists() and False:
|
|
||||||
df = pl.read_csv(csv_full_path)
|
|
||||||
return {
|
|
||||||
"metadata": df,
|
|
||||||
"file_path": str(csv_full_path)
|
|
||||||
}
|
|
||||||
io.safe_delete(csv_full_path)
|
io.safe_delete(csv_full_path)
|
||||||
# 构建命令
|
|
||||||
# UnrealInsights.exe -OpenTraceFile="path/to/file.utrace" -ExecOnAnalysisCompleteCmd="TimingInsights.ExportMetadata path/to/output.csv"
|
|
||||||
cmd = [
|
|
||||||
str(insights_path),
|
|
||||||
f'-OpenTraceFile="{utrace_full_path}"',
|
|
||||||
f'-ExecOnAnalysisCompleteCmd="TimingInsights.ExportMetadata {csv_full_path} -tokens={tokens}"'
|
|
||||||
]
|
|
||||||
print("TraceLoader_Metadata: 2.Execute::", " ".join(cmd))
|
|
||||||
# 执行命令
|
|
||||||
result = subprocess.run(
|
|
||||||
' '.join(cmd),
|
|
||||||
shell=True,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
encoding='utf-8',
|
|
||||||
errors='replace'
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查执行结果
|
# 通过 Agent 调用宿主机 UnrealInsights
|
||||||
if result.returncode != 0:
|
host_utrace_full_path = agent.map_cloud_to_host(utrace_full_path)
|
||||||
error_msg = f"UnrealInsights 执行失败 (退出码: {result.returncode})\n"
|
cmd_args = [
|
||||||
if result.stderr:
|
f'-OpenTraceFile="{host_utrace_full_path}"',
|
||||||
error_msg += f"错误输出:\n{result.stderr}"
|
f'-ExecOnAnalysisCompleteCmd="TimingInsights.ExportMetadata {host_csv_full_path} -tokens={tokens}"'
|
||||||
raise RuntimeError(error_msg)
|
]
|
||||||
|
print("TraceLoader_Metadata: call agent with", cmd_args)
|
||||||
|
resp = agent.run("unreal_insights", args=cmd_args, timeout=None)
|
||||||
|
|
||||||
|
if not resp.get("success", False):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"UnrealInsights 通过 Agent 执行失败 (code={resp.get('return_code')})\n"
|
||||||
|
f"stdout: {resp.get('stdout','')}\n"
|
||||||
|
f"stderr: {resp.get('stderr','')}"
|
||||||
|
)
|
||||||
|
|
||||||
if not csv_full_path.exists():
|
if not csv_full_path.exists():
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"CSV 文件未生成:{csv_full_path}\n"
|
f"CSV 文件未生成:{csv_full_path}\n"
|
||||||
f"命令输出: {result.stdout}"
|
f"Agent stdout: {resp.get('stdout','')}\n"
|
||||||
|
f"Agent stderr: {resp.get('stderr','')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 读取 CSV 文件
|
# 读取 CSV 文件
|
||||||
@ -196,36 +190,31 @@ class TraceLoader_Events(TraceNode):
|
|||||||
if not utrace_full_path.exists():
|
if not utrace_full_path.exists():
|
||||||
raise FileNotFoundError(f"Utrace 文件不存在:{utrace_full_path}")
|
raise FileNotFoundError(f"Utrace 文件不存在:{utrace_full_path}")
|
||||||
|
|
||||||
|
agent = _agent_client()
|
||||||
if Path(output_csv).is_absolute():
|
if Path(output_csv).is_absolute():
|
||||||
csv_full_path = Path(output_csv)
|
csv_full_path = Path(output_csv)
|
||||||
|
host_csv_full_path = agent.map_cloud_to_host(output_csv)
|
||||||
else:
|
else:
|
||||||
csv_full_path = cloud_root / output_csv
|
csv_full_path = cloud_root / output_csv
|
||||||
|
host_csv_full_path = agent.map_cloud_to_host(output_csv)
|
||||||
|
|
||||||
csv_full_path.parent.mkdir(parents=True, exist_ok=True)
|
csv_full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# 构建命令(导出事件数据)
|
# 通过 Agent 执行导出
|
||||||
cmd = [
|
host_utrace_full_path = agent.map_cloud_to_host(utrace_full_path)
|
||||||
str(insights_path),
|
cmd_args = [
|
||||||
f'-OpenTraceFile="{utrace_full_path}"',
|
f'-OpenTraceFile="{host_utrace_full_path}"',
|
||||||
f'-ExecOnAnalysisCompleteCmd="TimingInsights.ExportTimingEvents {csv_full_path}"'
|
f'-ExecOnAnalysisCompleteCmd="TimingInsights.ExportTimingEvents {host_csv_full_path}"'
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
resp = agent.run("unreal_insights", args=cmd_args, timeout=timeout)
|
||||||
' '.join(cmd),
|
|
||||||
shell=True,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=timeout,
|
|
||||||
encoding='utf-8',
|
|
||||||
errors='replace'
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
if not resp.get("success", False):
|
||||||
error_msg = f"UnrealInsights 执行失败 (退出码: {result.returncode})\n"
|
raise RuntimeError(
|
||||||
if result.stderr:
|
f"UnrealInsights 通过 Agent 执行失败 (code={resp.get('return_code')})\n"
|
||||||
error_msg += f"错误输出:\n{result.stderr}"
|
f"stdout: {resp.get('stdout','')}\n"
|
||||||
raise RuntimeError(error_msg)
|
f"stderr: {resp.get('stderr','')}"
|
||||||
|
)
|
||||||
|
|
||||||
# 等待文件生成
|
# 等待文件生成
|
||||||
wait_time = 0
|
wait_time = 0
|
||||||
@ -234,7 +223,11 @@ class TraceLoader_Events(TraceNode):
|
|||||||
wait_time += 0.5
|
wait_time += 0.5
|
||||||
|
|
||||||
if not csv_full_path.exists():
|
if not csv_full_path.exists():
|
||||||
raise FileNotFoundError(f"CSV 文件未生成:{csv_full_path}")
|
raise FileNotFoundError(
|
||||||
|
f"CSV 文件未生成:{csv_full_path}\n"
|
||||||
|
f"Agent stdout: {resp.get('stdout','')}\n"
|
||||||
|
f"Agent stderr: {resp.get('stderr','')}"
|
||||||
|
)
|
||||||
|
|
||||||
# 读取 CSV(使用 polars,保持后端一致)
|
# 读取 CSV(使用 polars,保持后端一致)
|
||||||
df = pl.read_csv(csv_full_path)
|
df = pl.read_csv(csv_full_path)
|
||||||
@ -258,9 +251,7 @@ class TraceLoader_Events(TraceNode):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
result_context["cleanup_error"] = str(e)
|
result_context["cleanup_error"] = str(e)
|
||||||
|
|
||||||
out = outputs
|
return outputs
|
||||||
# 可选地将 context 信息并入 outputs.meta 或单独管理;这里仅返回 outputs
|
|
||||||
return out
|
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
raise TimeoutError(f"命令执行超时(超过 {timeout} 秒)")
|
raise TimeoutError(f"命令执行超时(超过 {timeout} 秒)")
|
||||||
|
|||||||
127
server/app/services/agent_client.py
Normal file
127
server/app/services/agent_client.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""Agent 客户端:通过 HTTP 调用宿主上的 Agent 服务,并提供路径映射工具。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||||
|
from typing import Any, Dict, List, Optional, Union, Union
|
||||||
|
|
||||||
|
|
||||||
|
class AgentClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str = "http://localhost:8100",
|
||||||
|
*,
|
||||||
|
host_cloud_root: Optional[Path | str] = None,
|
||||||
|
container_cloud_root: Optional[Path | str] = None,
|
||||||
|
):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.host_cloud_root = Path(host_cloud_root) if host_cloud_root else None
|
||||||
|
self.container_cloud_root = Path(container_cloud_root) if container_cloud_root else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_env(cls, *, container_cloud_root: Optional[Path | str] = None) -> "AgentClient":
|
||||||
|
base_url = os.environ.get("AGENT_BASE_URL", "http://host.docker.internal:8100")
|
||||||
|
host_root = os.environ.get("AGENT_HOST_CLOUD_ROOT")
|
||||||
|
return cls(base_url=base_url, host_cloud_root=host_root, container_cloud_root=container_cloud_root)
|
||||||
|
|
||||||
|
# ---------------------- 路径映射 (核心修正) ----------------------
|
||||||
|
def map_cloud_to_host(self, path: Union[str, Path]) -> str:
|
||||||
|
"""
|
||||||
|
【Docker -> Host】
|
||||||
|
将容器内的路径(/opt/...)转换为宿主机的路径(D:\...)。
|
||||||
|
返回 str 以便直接传给 Agent API。
|
||||||
|
"""
|
||||||
|
# 输入可能是 pathlib.Path (Posix) 或 字符串
|
||||||
|
p_obj = PurePosixPath(path)
|
||||||
|
|
||||||
|
# 如果没有配置映射,直接返回字符串
|
||||||
|
if not self.container_cloud_root or not self.host_cloud_root:
|
||||||
|
return str(p_obj)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 计算相对路径 (Linux 逻辑)
|
||||||
|
# 例如: /opt/tracestudio/cloud/traces/test.utrace -> traces/test.utrace
|
||||||
|
rel = p_obj.relative_to(self.container_cloud_root)
|
||||||
|
|
||||||
|
# 2. 拼接到 Windows 根路径 (Windows 逻辑)
|
||||||
|
# 例如: D:\XGame\...\cloud + traces/test.utrace -> D:\XGame\...\cloud\traces\test.utrace
|
||||||
|
return str(self.host_cloud_root / rel)
|
||||||
|
except ValueError:
|
||||||
|
# 如果路径不在 container_cloud_root 下,原样返回(可能是绝对路径或其他挂载)
|
||||||
|
return str(p_obj)
|
||||||
|
|
||||||
|
def map_host_to_container(self, path: Union[str, Path]) -> Path:
|
||||||
|
"""
|
||||||
|
【Host -> Docker】
|
||||||
|
将 Agent 返回的 Windows 路径(D:\...)转换为容器内可读的路径(/opt/...)。
|
||||||
|
返回 Path 对象以便 Python 打开文件。
|
||||||
|
"""
|
||||||
|
# 输入是 Windows 格式字符串
|
||||||
|
p_obj = PureWindowsPath(path)
|
||||||
|
|
||||||
|
if not self.host_cloud_root or not self.container_cloud_root:
|
||||||
|
# 无法映射,尝试直接转换(大概率读不到,但保持类型一致)
|
||||||
|
return Path(str(p_obj))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 计算相对路径 (Windows 逻辑)
|
||||||
|
# Windows 不区分大小写,但 relative_to 默认区分。
|
||||||
|
# 如果路径来源可靠,通常没问题。
|
||||||
|
rel = p_obj.relative_to(self.host_cloud_root)
|
||||||
|
|
||||||
|
# 2. 拼接到 Linux 容器路径
|
||||||
|
# 这里的 Path 是具体平台的 Path (在 Docker 里就是 PosixPath)
|
||||||
|
return Path(self.container_cloud_root / rel)
|
||||||
|
except ValueError:
|
||||||
|
return Path(str(p_obj))
|
||||||
|
|
||||||
|
# ---------------------- 调用 ----------------------
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
tool: str,
|
||||||
|
args: Optional[List[str]] = None,
|
||||||
|
*,
|
||||||
|
workdir: Optional[str] = None,
|
||||||
|
timeout: Optional[int] = None,
|
||||||
|
env: Optional[Dict[str, str]] = None,
|
||||||
|
capture_output: Optional[bool] = None,
|
||||||
|
strip_output: Optional[bool] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
payload = {
|
||||||
|
"tool": tool,
|
||||||
|
"args": args or [],
|
||||||
|
}
|
||||||
|
if workdir:
|
||||||
|
payload["workdir"] = workdir
|
||||||
|
if timeout is not None:
|
||||||
|
payload["timeout"] = timeout
|
||||||
|
if env:
|
||||||
|
payload["env"] = env
|
||||||
|
if capture_output is not None:
|
||||||
|
payload["capture_output"] = capture_output
|
||||||
|
if strip_output is not None:
|
||||||
|
payload["strip_output"] = strip_output
|
||||||
|
|
||||||
|
url = f"{self.base_url}/run"
|
||||||
|
data = json.dumps(payload).encode("utf-8")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout or 30) as resp:
|
||||||
|
body = resp.read().decode("utf-8")
|
||||||
|
return json.loads(body)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
detail = e.read().decode("utf-8") if e.fp else str(e)
|
||||||
|
raise RuntimeError(f"Agent HTTP {e.code}: {detail}") from e
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Agent 调用失败: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["AgentClient"]
|
||||||
@ -3,15 +3,7 @@
|
|||||||
* 对接后端 /api/files/* 端点,提供文件列表、上传、操作、下载URL。
|
* 对接后端 /api/files/* 端点,提供文件列表、上传、操作、下载URL。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 动态获取API基础URL(与 core/api/api.ts 保持一致,但本地定义以降低耦合)
|
import { API_BASE_URL } from './base'
|
||||||
function getApiBaseUrl(): string {
|
|
||||||
if (import.meta.env.VITE_API_BASE_URL) return import.meta.env.VITE_API_BASE_URL
|
|
||||||
const { protocol, hostname } = window.location
|
|
||||||
const apiPort = '8000'
|
|
||||||
return `${protocol}//${hostname}:${apiPort}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_BASE_URL = getApiBaseUrl()
|
|
||||||
|
|
||||||
export type FileInfo = {
|
export type FileInfo = {
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
|
import { API_BASE_URL } from './base'
|
||||||
|
|
||||||
export class SchemaApi {
|
export class SchemaApi {
|
||||||
static async fetchManifest(): Promise<any> {
|
static async fetchManifest(): Promise<any> {
|
||||||
//const res = await fetch('/api/schema/manifest');
|
const res = await fetch(`${API_BASE_URL}/api/plugins`)
|
||||||
const res = await fetch('/api/plugins');
|
if (!res.ok) throw new Error(`Failed to fetch manifest: ${res.status}`)
|
||||||
if (!res.ok) throw new Error(`Failed to fetch manifest: ${res.status}`);
|
return await res.json()
|
||||||
return await res.json();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,29 +3,7 @@
|
|||||||
* 统一管理所有后端请求,处理 CORS 和错误
|
* 统一管理所有后端请求,处理 CORS 和错误
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 动态获取API基础URL
|
import { API_BASE_URL, WS_BASE_URL } from './base'
|
||||||
function getApiBaseUrl(): string {
|
|
||||||
// 1. 优先使用环境变量
|
|
||||||
if (import.meta.env.VITE_API_BASE_URL) {
|
|
||||||
return import.meta.env.VITE_API_BASE_URL
|
|
||||||
}
|
|
||||||
// 2. 生产环境使用相对路径(前后端同域)
|
|
||||||
const { protocol, hostname } = window.location
|
|
||||||
const apiPort = '8000'
|
|
||||||
|
|
||||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
||||||
return `${protocol}//${hostname}:${apiPort}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${protocol}//${hostname}:${apiPort}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_BASE_URL = getApiBaseUrl()
|
|
||||||
const WS_BASE_URL = (() => {
|
|
||||||
const http = new URL(API_BASE_URL)
|
|
||||||
const isHttps = http.protocol === 'https:'
|
|
||||||
return `${isHttps ? 'wss' : 'ws'}://${http.host}`
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.log('🔗 API Base URL:', API_BASE_URL)
|
console.log('🔗 API Base URL:', API_BASE_URL)
|
||||||
|
|||||||
17
web/src/core/api/base.ts
Normal file
17
web/src/core/api/base.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* API 基础地址与协议工具
|
||||||
|
*/
|
||||||
|
export function getApiBaseUrl(): string {
|
||||||
|
if (import.meta.env.VITE_API_BASE_URL) return import.meta.env.VITE_API_BASE_URL
|
||||||
|
const { protocol, hostname } = window.location
|
||||||
|
const apiPort = '8000'
|
||||||
|
return `${protocol}//${hostname}:${apiPort}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const API_BASE_URL = getApiBaseUrl()
|
||||||
|
|
||||||
|
export const WS_BASE_URL = (() => {
|
||||||
|
const http = new URL(API_BASE_URL)
|
||||||
|
const isHttps = http.protocol === 'https:'
|
||||||
|
return `${isHttps ? 'wss' : 'ws'}://${http.host}`
|
||||||
|
})()
|
||||||
Loading…
Reference in New Issue
Block a user