chore: 项目环境升级为 JDK25,Spring 4.1,项目重构为多模块
This commit is contained in:
15
.env.example
15
.env.example
@@ -6,17 +6,4 @@ APP_ADMIN_PASSWORD=replace-with-a-strong-admin-password
|
||||
# 百炼知识库未启用时可以保留为空;模型 API Key 仍通过页面配置并加密保存。
|
||||
DASHSCOPE_API_KEY=
|
||||
|
||||
# 非敏感运行参数。
|
||||
POSTGRES_DB=smart_factory_agent
|
||||
POSTGRES_USER=smart_factory
|
||||
APP_ADMIN_USERNAME=admin
|
||||
APP_RUN_TIMEOUT=60m
|
||||
SESSION_COOKIE_SECURE=false
|
||||
MANUAGENT_DATA_ROOT=/srv/manuagent/data
|
||||
# 默认值即部署要求的 0.0.0.0:5173;普通 Linux 部署无需修改。
|
||||
FRONTEND_PORT=5173
|
||||
|
||||
# 可选镜像标签,便于私有镜像仓库或版本升级时覆盖。
|
||||
AGENT_RUNTIME_IMAGE=smart-factory-agent-runtime:0.1.0
|
||||
BACKEND_IMAGE=manuagent-backend:0.1.0
|
||||
FRONTEND_IMAGE=manuagent-frontend:0.1.0
|
||||
# 镜像、数据库名称、端口和数据目录等普通配置直接修改 docker-compose.yml。
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
server/target/
|
||||
web-ui/node_modules/
|
||||
web-ui/dist/
|
||||
server/**/target/
|
||||
web-ui/**/node_modules/
|
||||
web-ui/**/dist/
|
||||
sandbox/node_modules/
|
||||
*.tsbuildinfo
|
||||
data/
|
||||
@@ -18,4 +18,4 @@ deepseek_key.txt
|
||||
test_data/
|
||||
|
||||
# 部署镜像体积较大,只在本地或服务器之间传输,不提交到 Git 仓库。
|
||||
deploy/*.tar
|
||||
/agent-sandbox-*.tar
|
||||
|
||||
104
README.md
104
README.md
@@ -4,21 +4,53 @@
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Spring Boot 3.5.16、JDK 21、AgentScope Java 2.0.1、AG-UI
|
||||
- Spring Boot 4.1.1、JDK 25、AgentScope Java 2.0.1、AG-UI
|
||||
- MyBatis-Flex 1.11.8(Boot 4 starter)、业务与 HTTP 使用 Jackson 3;AgentScope 内部保留 Jackson 2
|
||||
- Vue 3.5、Vite、Element Plus、Vue Element Plus X
|
||||
- PostgreSQL 17、Flyway
|
||||
|
||||
## 本地启动
|
||||
|
||||
需要 JDK 25、Maven、Node.js 22 和 Docker。先设置 `JAVA_HOME` 指向 JDK 25;IDEA 从
|
||||
`server/pom.xml` 导入 Maven reactor,项目 SDK 与 Maven runner 均选 JDK 25,运行
|
||||
`manuagent-web` 的 `ManuAgentApplication`,工作目录设为仓库根目录。
|
||||
IDEA 运行配置同时设置 `APP_DATA_ROOT` 为既有数据目录的绝对路径,以及
|
||||
`APP_DASHSCOPE_KEY_FILE` 为知识库密钥文件的绝对路径,与下面的命令行启动配置保持一致。
|
||||
|
||||
后端通过 `SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_USERNAME`、`SPRING_DATASOURCE_PASSWORD`
|
||||
连接可用的 PostgreSQL 17。根 Compose 的数据库端口只供容器内部使用;宿主机开发需使用已发布
|
||||
端口的本地数据库,或在本地 Compose override 中增加 PostgreSQL 端口映射。
|
||||
|
||||
```bash
|
||||
docker build -t smart-factory-agent-runtime:0.1.0 -f sandbox/Dockerfile sandbox
|
||||
docker compose up -d postgres
|
||||
mvn -q -f server/pom.xml spring-boot:run
|
||||
npm --prefix web-ui install
|
||||
docker build -t agent-sandbox:0.1 -f sandbox/Dockerfile sandbox
|
||||
mvn -f server/pom.xml clean package
|
||||
export APP_DATA_ROOT="$PWD/data"
|
||||
export APP_DASHSCOPE_KEY_FILE="$PWD/dashscope_key.txt"
|
||||
java -jar server/manuagent-web/target/manuagent-web.jar
|
||||
# 在另一个终端启动前端;依赖只从 workspace 根目录安装。
|
||||
npm --prefix web-ui ci
|
||||
npm --prefix web-ui run dev
|
||||
```
|
||||
|
||||
本地开发服务器默认使用 Vite 配置的端口。容器化部署固定从 <http://127.0.0.1:5173> 访问。
|
||||
本地开发入口为 <http://127.0.0.1:15173>,代理后端 8080。唯一可执行产物是
|
||||
`server/manuagent-web/target/manuagent-web.jar`。上述命令从仓库根目录执行,将 `APP_DATA_ROOT` 设为
|
||||
根目录下 `data` 的绝对路径;已有数据时改为对应绝对路径,避免移动启动目录后找不到历史文件。
|
||||
容器化部署固定从 <http://127.0.0.1:5173> 访问。
|
||||
|
||||
Agent 会话状态保存在 `APP_DATA_ROOT/agent-state`,与项目文件和沙箱快照一起持久化。
|
||||
启动时,会为数据目录内的已有项目从当前运行用户的 `~/.agentscope/state`(或 JVM `agentscope.state.home`
|
||||
指定目录)分别复制会话与沙箱状态,补齐此前只迁移会话的项目;保留旧目录,不覆盖已有新状态。
|
||||
跨机器或从旧容器升级时,须先将旧状态目录复制到新环境的对应位置,并保留快照原有绝对路径;
|
||||
仅保留项目文件不能恢复完整会话。旧进程停止后再迁移,避免继续写入旧位置。
|
||||
|
||||
沙箱镜像统一为 `agent-sandbox:0.1`。升级前停止旧后端并确认旧沙箱容器已退出;历史会话恢复时
|
||||
会将旧 `smart-factory-agent-runtime` 镜像引用迁移到当前 `APP_SANDBOX_IMAGE` 配置,保留会话和快照。
|
||||
|
||||
`APP_RUN_TIMEOUT` 限制整个 Run(含重连与结果修复)的总时长,默认 60 分钟;结构化结果最多修复
|
||||
3 次。`APP_MAX_CONCURRENT_RUNS` 限制单个后端同时运行的任务数,默认 2,新增任务满额时返回 429。
|
||||
同项目的模型切换、停止后继续及阶段接续共用原运行名额,等待旧任务释放资源、保存快照后再启动。
|
||||
修改沙箱文件后须重建 `sandbox/Dockerfile` 镜像;其中 `docx-example.cjs` 提供与已安装依赖
|
||||
一致的表格、原生批注示例及可执行验证。
|
||||
|
||||
模型连接只能在“模型配置”页面新增并持久化到 PostgreSQL;API Key 会使用 `APP_MASTER_KEY`
|
||||
环境变量提供的主密钥加密后保存。`dashscope_key.txt` 仅供百炼知识库使用,不参与模型配置。
|
||||
@@ -31,23 +63,23 @@ $env:APP_MASTER_KEY = '<使用独立生成的高强度密钥>'
|
||||
|
||||
## Docker Compose 部署
|
||||
|
||||
部署包含 Nginx 前端、Spring Boot 后端、Agent Runtime 镜像和 PostgreSQL。Agent Runtime
|
||||
不是常驻 API 服务:镜像在开发机预先构建并导入服务器,服务器上的 `agent-runtime` 服务只校验
|
||||
部署包含 Nginx 前端、Spring Boot 后端、沙箱镜像和 PostgreSQL。沙箱
|
||||
不是常驻 API 服务:镜像在开发机预先构建并导入服务器,服务器上的 `sandbox` 服务只校验
|
||||
镜像存在,随后以状态码 0 退出;后端再通过 Docker Socket 为每个 Agent Session 动态创建隔离容器。
|
||||
|
||||
1. 在开发机构建并导出 Agent Runtime 镜像:
|
||||
1. 在开发机构建并导出沙箱镜像:
|
||||
|
||||
```bash
|
||||
docker build -t smart-factory-agent-runtime:0.1.0 -f sandbox/Dockerfile sandbox
|
||||
docker save -o smart-factory-agent-runtime-0.1.0.tar smart-factory-agent-runtime:0.1.0
|
||||
docker build -t agent-sandbox:0.1 -f sandbox/Dockerfile sandbox
|
||||
docker save -o agent-sandbox-0.1.tar agent-sandbox:0.1
|
||||
```
|
||||
|
||||
将 Tar 文件传到服务器后导入。Compose 设置了 `pull_policy: never`,若镜像不存在会直接报错,
|
||||
不会在服务器自动拉取或构建:
|
||||
|
||||
```bash
|
||||
docker load -i smart-factory-agent-runtime-0.1.0.tar
|
||||
docker image inspect smart-factory-agent-runtime:0.1.0
|
||||
docker load -i agent-sandbox-0.1.tar
|
||||
docker image inspect agent-sandbox:0.1
|
||||
```
|
||||
|
||||
2. 复制环境变量模板并替换其中的三个必填密码/密钥;生产环境建议使用密码管理系统生成随机值。
|
||||
@@ -56,9 +88,9 @@ $env:APP_MASTER_KEY = '<使用独立生成的高强度密钥>'
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. 确认 `MANUAGENT_DATA_ROOT` 是 Docker 宿主机上的绝对路径。这个路径会以完全相同的路径挂载到
|
||||
后端容器,供动态 Agent Runtime 继续挂载项目材料、工作目录和产物。Linux 默认值为
|
||||
`/srv/manuagent/data`。
|
||||
3. Compose 中的镜像、数据库名称、端口和数据目录均使用静态值;调整时直接修改根目录的 `docker-compose.yml`。
|
||||
数据目录固定为 `/srv/manuagent/data`;修改时须同步更新后端的 `APP_DATA_ROOT` 和对应挂载的
|
||||
`source`、`target`,保持容器内外使用同一个宿主机绝对路径,供动态沙箱挂载项目材料和产物。
|
||||
|
||||
4. 构建前后端并启动完整服务:
|
||||
|
||||
@@ -67,7 +99,11 @@ $env:APP_MASTER_KEY = '<使用独立生成的高强度密钥>'
|
||||
docker compose -f docker-compose.yml ps
|
||||
```
|
||||
|
||||
5. 打开 <http://127.0.0.1:5173>,使用 `.env` 中的 `APP_ADMIN_USERNAME` 和
|
||||
根目录的 `docker-compose.yml` 是唯一部署入口。后端镜像使用 Maven 3.9.13 / Temurin 25.0.2
|
||||
构建和 Temurin 25.0.2 JRE 运行;前端镜像从
|
||||
`web-ui/apps/web/dist` 复制静态文件。
|
||||
|
||||
5. 打开 <http://127.0.0.1:5173>,使用 Compose 中的管理员账号 `admin` 和 `.env` 中的
|
||||
`APP_ADMIN_PASSWORD` 登录。查看日志或停止服务:
|
||||
|
||||
```bash
|
||||
@@ -75,22 +111,42 @@ $env:APP_MASTER_KEY = '<使用独立生成的高强度密钥>'
|
||||
docker compose -f docker-compose.yml down
|
||||
```
|
||||
|
||||
默认端口映射为 `0.0.0.0:5173:80`。若 Windows 的动态端口排除范围占用了 5173,可仅在本机
|
||||
验证时临时执行 `$env:FRONTEND_PORT = '15173'`;Linux 服务器部署应保留默认的 5173。
|
||||
端口映射为 `0.0.0.0:5173:80`。若 Windows 的动态端口排除范围占用了 5173,可在本机验证时
|
||||
将 Compose 的端口映射临时改为 `0.0.0.0:15173:80`;Linux 服务器部署应保留 5173。
|
||||
|
||||
PostgreSQL 数据保存在 `postgres-data` 命名卷中;项目材料、Agent 工作区、快照和生成文件保存在
|
||||
`MANUAGENT_DATA_ROOT`。`docker compose down` 不会删除它们,只有显式增加 `--volumes` 才会删除
|
||||
Compose 配置的数据目录。`docker compose down` 不会删除它们,只有显式增加 `--volumes` 才会删除
|
||||
PostgreSQL 卷。后端挂载 Docker Socket 等价于授予其管理宿主机容器的高权限,应只在受信任的
|
||||
Docker 主机上运行,并限制 5173 端口的网络访问范围。通过 HTTPS 反向代理部署时,请将
|
||||
`SESSION_COOKIE_SECURE` 设为 `true`。
|
||||
Compose 中的 `SERVER_SERVLET_SESSION_COOKIE_SECURE` 设为字符串 `"true"`。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
mvn -q -f server/pom.xml test
|
||||
npm --prefix web-ui run test
|
||||
mvn -f server/pom.xml verify
|
||||
npm --prefix web-ui test
|
||||
npm --prefix web-ui run build
|
||||
docker compose -f docker-compose.yml config --quiet
|
||||
```
|
||||
|
||||
产品、数据库与验收设计见 [docs](docs/)。内置 Skill 与资源由 Flyway 种子迁移写入数据库,无需额外导入文件。
|
||||
以上命令分别验证后端测试/打包、前端测试、四个 workspace 的类型检查及单应用构建、Compose 配置。
|
||||
模块依赖边界仍需在代码审查中核对,当前没有自动边界检查命令。
|
||||
|
||||
## 模块边界
|
||||
|
||||
| 后端模块 | 前端 workspace | 职责 |
|
||||
| --- | --- | --- |
|
||||
| `manuagent-common` | `packages/common` | 错误、TypeHandler;前端共享请求、CSRF、基础样式 |
|
||||
| `manuagent-admin` | `packages/admin` | 用户服务与用户持久化;登录页面 |
|
||||
| `manuagent-agent` | `packages/agent` | 项目、文件、模型、Skill、Agent 执行、规划与产物;业务页面和侧栏 |
|
||||
| `manuagent-web` | `apps/web` | HTTP、认证上下文、基础设施装配与启动;单应用壳和路由 |
|
||||
|
||||
Agent 和 Admin 只依赖 Common,Web 装配三者。仍是一个后端进程、一个数据源和一个事务体系。
|
||||
Controller 从登录身份解析用户 UUID,业务服务负责事务及审计字段。`AdminProperties` 和
|
||||
`AgentProperties` 继续绑定原有 `app.*` 键,数据库配置和环境变量名保持兼容。
|
||||
|
||||
历史 Flyway 文件保留在 `server/src/main/resources/db/migration`,由 Web 原样打包一份;
|
||||
请勿迁移、重命名或改写这些已共享脚本。Agent 的 Mapper XML 和提示词随普通库 JAR 加载。
|
||||
前端只有根目录一份 `package-lock.json`,业务包通过 `@manuagent/*` 的公开 exports 引用。
|
||||
|
||||
本次改造的证据与验收边界见 [MANU-1 实现与验收](docs/MANU-1-implementation.md)。内置 Skill 与资源由 Flyway 种子迁移写入数据库,无需额外导入文件。
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# 复制为 .env 后必须替换以下三个敏感值;不要把真实 .env 提交到 Git。
|
||||
POSTGRES_PASSWORD=replace-with-a-strong-database-password
|
||||
APP_MASTER_KEY=replace-with-at-least-32-random-characters
|
||||
APP_ADMIN_PASSWORD=replace-with-a-strong-admin-password
|
||||
|
||||
# 百炼知识库未启用时可以保留为空;模型 API Key 仍通过页面配置并加密保存。
|
||||
DASHSCOPE_API_KEY=
|
||||
|
||||
# 非敏感运行参数。
|
||||
POSTGRES_DB=smart_factory_agent
|
||||
POSTGRES_USER=smart_factory
|
||||
APP_ADMIN_USERNAME=admin
|
||||
APP_RUN_TIMEOUT=60m
|
||||
SESSION_COOKIE_SECURE=false
|
||||
MANUAGENT_DATA_ROOT=/srv/manuagent/data
|
||||
# 默认值即部署要求的 0.0.0.0:5173;普通 Linux 部署无需修改。
|
||||
FRONTEND_PORT=5173
|
||||
|
||||
# 可选镜像标签,便于私有镜像仓库或版本升级时覆盖。
|
||||
AGENT_RUNTIME_IMAGE=smart-factory-agent-runtime:0.1.0
|
||||
BACKEND_IMAGE=manuagent-backend:0.1.0
|
||||
FRONTEND_IMAGE=manuagent-frontend:0.1.0
|
||||
@@ -1,130 +0,0 @@
|
||||
name: manuagent
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-smart_factory_agent}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-smart_factory}
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
|
||||
secrets:
|
||||
- postgres_password
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- manuagent-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
|
||||
# 此服务负责校验并登记 AgentScope 使用的 Runtime 镜像。
|
||||
# Runtime 镜像在开发机预先构建并导入服务器;此服务只校验镜像存在,不在服务器构建或拉取。
|
||||
# 它成功退出后,后端会通过 Docker Socket 按 Session 动态创建真正执行任务的 Runtime 容器。
|
||||
agent-runtime:
|
||||
image: ${AGENT_RUNTIME_IMAGE:-agent-runtime:0.1.0}
|
||||
pull_policy: never
|
||||
command: ["/bin/true"]
|
||||
restart: "no"
|
||||
networks:
|
||||
- manuagent-network
|
||||
|
||||
backend:
|
||||
image: ${BACKEND_IMAGE:-manuagent-backend:0.1.0}
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
agent-runtime:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-smart_factory_agent}
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-smart_factory}
|
||||
# Spring Boot 将 secrets 目录中的点分文件名作为配置属性加载,避免把密码写进普通环境变量。
|
||||
SPRING_CONFIG_IMPORT: optional:configtree:/run/secrets/
|
||||
APP_DATA_ROOT: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
|
||||
APP_DASHSCOPE_KEY_FILE: /run/secrets/dashscope_key
|
||||
APP_ADMIN_USERNAME: ${APP_ADMIN_USERNAME:-admin}
|
||||
APP_SANDBOX_IMAGE: ${AGENT_RUNTIME_IMAGE:-smart-factory-agent-runtime:0.1.0}
|
||||
APP_SANDBOX_NETWORK: manuagent-network
|
||||
APP_RUN_TIMEOUT: ${APP_RUN_TIMEOUT:-60m}
|
||||
SERVER_SERVLET_SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-false}
|
||||
secrets:
|
||||
- source: postgres_password
|
||||
target: spring.datasource.password
|
||||
- source: app_master_key
|
||||
target: app.master-key
|
||||
- source: admin_password
|
||||
target: app.admin-password
|
||||
- source: dashscope_api_key
|
||||
target: dashscope_key
|
||||
volumes:
|
||||
# DockerSandbox 的 bind mount 源路径由宿主机 daemon 解释,因此容器内外必须使用相同绝对路径。
|
||||
- type: bind
|
||||
source: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
|
||||
target: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
|
||||
# AgentScope 需要通过宿主机 Docker Engine 创建、执行并销毁隔离的 Runtime 容器。
|
||||
- type: bind
|
||||
source: /var/run/docker.sock
|
||||
target: /var/run/docker.sock
|
||||
expose:
|
||||
- "8080"
|
||||
networks:
|
||||
- manuagent-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--fail", "--silent", "--show-error", "http://127.0.0.1:8080/api/auth/csrf"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 18
|
||||
start_period: 30s
|
||||
stop_grace_period: 30s
|
||||
|
||||
frontend:
|
||||
image: ${FRONTEND_IMAGE:-manuagent-frontend:0.1.0}
|
||||
build:
|
||||
context: ./web-ui
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
ports:
|
||||
# 默认严格监听 0.0.0.0:5173;仅当宿主机保留该端口时,才通过 FRONTEND_PORT 临时覆盖。
|
||||
- "0.0.0.0:${FRONTEND_PORT:-5173}:80"
|
||||
networks:
|
||||
- manuagent-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--output-document=/dev/null", "http://127.0.0.1/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
start_period: 10s
|
||||
|
||||
networks:
|
||||
manuagent-network:
|
||||
# 固定网络名,确保后端动态创建的 Agent Runtime 容器可以加入同一个网络。
|
||||
name: manuagent-network
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
||||
secrets:
|
||||
postgres_password:
|
||||
environment: POSTGRES_PASSWORD
|
||||
app_master_key:
|
||||
environment: APP_MASTER_KEY
|
||||
admin_password:
|
||||
environment: APP_ADMIN_PASSWORD
|
||||
dashscope_api_key:
|
||||
environment: DASHSCOPE_API_KEY
|
||||
@@ -5,8 +5,8 @@ services:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-smart_factory_agent}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-smart_factory}
|
||||
POSTGRES_DB: smart_factory_agent
|
||||
POSTGRES_USER: smart_factory
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
|
||||
secrets:
|
||||
- postgres_password
|
||||
@@ -15,17 +15,17 @@ services:
|
||||
networks:
|
||||
- manuagent-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
test: ["CMD", "pg_isready", "-U", "smart_factory", "-d", "smart_factory_agent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
|
||||
# 此服务负责校验并登记 AgentScope 使用的 Runtime 镜像。
|
||||
# Runtime 镜像在开发机预先构建并导入服务器;此服务只校验镜像存在,不在服务器构建或拉取。
|
||||
# 它成功退出后,后端会通过 Docker Socket 按 Session 动态创建真正执行任务的 Runtime 容器。
|
||||
agent-runtime:
|
||||
image: ${AGENT_RUNTIME_IMAGE:-agent-runtime:0.1.0}
|
||||
# 此服务负责校验并登记 AgentScope 使用的沙箱镜像。
|
||||
# 沙箱镜像在开发机预先构建并导入服务器;此服务只校验镜像存在,不在服务器构建或拉取。
|
||||
# 它成功退出后,后端会通过 Docker Socket 按 Session 动态创建真正执行任务的沙箱容器。
|
||||
sandbox:
|
||||
image: agent-sandbox:0.1
|
||||
pull_policy: never
|
||||
command: ["/bin/true"]
|
||||
restart: "no"
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
- manuagent-network
|
||||
|
||||
backend:
|
||||
image: ${BACKEND_IMAGE:-manuagent-backend:0.1.0}
|
||||
image: manuagent-backend:0.1.0
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
@@ -43,20 +43,21 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
agent-runtime:
|
||||
sandbox:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-smart_factory_agent}
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-smart_factory}
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/smart_factory_agent
|
||||
SPRING_DATASOURCE_USERNAME: smart_factory
|
||||
# Spring Boot 将 secrets 目录中的点分文件名作为配置属性加载,避免把密码写进普通环境变量。
|
||||
SPRING_CONFIG_IMPORT: optional:configtree:/run/secrets/
|
||||
APP_DATA_ROOT: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
|
||||
APP_DATA_ROOT: /srv/manuagent/data
|
||||
APP_DASHSCOPE_KEY_FILE: /run/secrets/dashscope_key
|
||||
APP_ADMIN_USERNAME: ${APP_ADMIN_USERNAME:-admin}
|
||||
APP_SANDBOX_IMAGE: ${AGENT_RUNTIME_IMAGE:-smart-factory-agent-runtime:0.1.0}
|
||||
APP_ADMIN_USERNAME: admin
|
||||
APP_SANDBOX_IMAGE: agent-sandbox:0.1
|
||||
APP_SANDBOX_NETWORK: manuagent-network
|
||||
APP_RUN_TIMEOUT: ${APP_RUN_TIMEOUT:-60m}
|
||||
SERVER_SERVLET_SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-false}
|
||||
APP_RUN_TIMEOUT: 60m
|
||||
APP_MAX_CONCURRENT_RUNS: "2"
|
||||
SERVER_SERVLET_SESSION_COOKIE_SECURE: "false"
|
||||
secrets:
|
||||
- source: postgres_password
|
||||
target: spring.datasource.password
|
||||
@@ -69,9 +70,9 @@ services:
|
||||
volumes:
|
||||
# DockerSandbox 的 bind mount 源路径由宿主机 daemon 解释,因此容器内外必须使用相同绝对路径。
|
||||
- type: bind
|
||||
source: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
|
||||
target: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
|
||||
# AgentScope 需要通过宿主机 Docker Engine 创建、执行并销毁隔离的 Runtime 容器。
|
||||
source: /srv/manuagent/data
|
||||
target: /srv/manuagent/data
|
||||
# AgentScope 需要通过宿主机 Docker Engine 创建、执行并销毁隔离的沙箱容器。
|
||||
- type: bind
|
||||
source: /var/run/docker.sock
|
||||
target: /var/run/docker.sock
|
||||
@@ -88,7 +89,7 @@ services:
|
||||
stop_grace_period: 30s
|
||||
|
||||
frontend:
|
||||
image: ${FRONTEND_IMAGE:-manuagent-frontend:0.1.0}
|
||||
image: manuagent-frontend:0.1.0
|
||||
build:
|
||||
context: ./web-ui
|
||||
dockerfile: Dockerfile
|
||||
@@ -99,8 +100,8 @@ services:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
ports:
|
||||
# 默认严格监听 0.0.0.0:5173;仅当宿主机保留该端口时,才通过 FRONTEND_PORT 临时覆盖。
|
||||
- "0.0.0.0:${FRONTEND_PORT:-5173}:80"
|
||||
# 固定监听 0.0.0.0:5173;需要调整端口时直接修改此映射。
|
||||
- "0.0.0.0:5173:80"
|
||||
networks:
|
||||
- manuagent-network
|
||||
healthcheck:
|
||||
@@ -112,7 +113,7 @@ services:
|
||||
|
||||
networks:
|
||||
manuagent-network:
|
||||
# 固定网络名,确保后端动态创建的 Agent Runtime 容器可以加入同一个网络。
|
||||
# 固定网络名,确保后端动态创建的沙箱容器可以加入同一个网络。
|
||||
name: manuagent-network
|
||||
driver: bridge
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ COPY --from=node /usr/local /usr/local
|
||||
|
||||
ENV JAVA_HOME=/opt/java/openjdk \
|
||||
PATH=/opt/java/openjdk/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin \
|
||||
NODE_PATH=/opt/agent-runtime/node_modules \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/agent-runtime/ms-playwright \
|
||||
NODE_PATH=/opt/sandbox/node_modules \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/sandbox/ms-playwright \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /opt/agent-runtime
|
||||
WORKDIR /opt/sandbox
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev \
|
||||
&& pip install --no-cache-dir \
|
||||
@@ -34,7 +34,7 @@ RUN ln -s /opt/java/openjdk/bin/java /usr/local/bin/java \
|
||||
COPY fonts-local.conf /etc/fonts/local.conf
|
||||
RUN fc-cache -f
|
||||
|
||||
COPY --chown=agent:agent document_view.py ./
|
||||
COPY --chown=agent:agent document_view.py docx-example.cjs ./
|
||||
USER agent
|
||||
WORKDIR /workspace
|
||||
|
||||
|
||||
30
sandbox/docx-example.cjs
Normal file
30
sandbox/docx-example.cjs
Normal file
@@ -0,0 +1,30 @@
|
||||
// 使用 Runtime 实际安装的 docx API;可作为表格与原生批注的最小验证脚本。
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const { Document, Paragraph, TextRun, Table, TableRow, TableCell, Packer,
|
||||
CommentRangeStart, CommentRangeEnd, CommentReference } = require('docx');
|
||||
|
||||
async function main() {
|
||||
const output = process.argv[2];
|
||||
assert(output, '用法:node docx-example.cjs <输出.docx>');
|
||||
const document = new Document({
|
||||
comments: { children: [{ id: 0, author: '材料评审', date: new Date(),
|
||||
children: [new Paragraph({ children: [new TextRun('请企业确认设备数量。')] })] }] },
|
||||
sections: [{ children: [
|
||||
new Paragraph({ children: [
|
||||
new CommentRangeStart(0), new TextRun('待企业提供设备数量。'),
|
||||
new CommentRangeEnd(0), new TextRun({ children: [new CommentReference(0)] })
|
||||
] }),
|
||||
new Table({ rows: [new TableRow({ children: ['项目', '待确认'].map(text =>
|
||||
new TableCell({ children: [new Paragraph({ children: [new TextRun(text)] })] })) })] })
|
||||
] }]
|
||||
});
|
||||
const buffer = await Packer.toBuffer(document);
|
||||
assert.equal(buffer.subarray(0, 2).toString(), 'PK');
|
||||
fs.mkdirSync(path.dirname(output), { recursive: true });
|
||||
fs.writeFileSync(output, buffer);
|
||||
console.log(`DOCX API 验证通过:${buffer.length} 字节`);
|
||||
}
|
||||
|
||||
main().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
4
sandbox/package-lock.json
generated
4
sandbox/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "smart-factory-agent-runtime",
|
||||
"name": "agent-sandbox",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "smart-factory-agent-runtime",
|
||||
"name": "agent-sandbox",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"docx": "^9.5.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "smart-factory-agent-runtime",
|
||||
"name": "agent-sandbox",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
target
|
||||
**/target
|
||||
.idea
|
||||
*.iml
|
||||
*.log
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Maven 构建阶段使用与项目一致的 JDK 21,并利用 BuildKit 缓存减少重复下载依赖的时间。
|
||||
FROM maven:3.9.13-eclipse-temurin-21 AS builder
|
||||
# Maven 构建阶段使用与项目一致的 JDK 25,并利用 BuildKit 缓存减少重复下载依赖的时间。
|
||||
FROM maven:3.9.13-eclipse-temurin-25@sha256:ade3c87e3cdfbe04932afa16b31814cbf60b0122d21d78a76530684a1eeb7cc2 AS builder
|
||||
|
||||
WORKDIR /workspace
|
||||
COPY pom.xml ./
|
||||
COPY src ./src
|
||||
COPY manuagent-common/pom.xml manuagent-common/pom.xml
|
||||
COPY manuagent-admin/pom.xml manuagent-admin/pom.xml
|
||||
COPY manuagent-agent/pom.xml manuagent-agent/pom.xml
|
||||
COPY manuagent-web/pom.xml manuagent-web/pom.xml
|
||||
COPY src/main/resources/db ./src/main/resources/db
|
||||
COPY manuagent-common/src ./manuagent-common/src
|
||||
COPY manuagent-admin/src ./manuagent-admin/src
|
||||
COPY manuagent-agent/src ./manuagent-agent/src
|
||||
COPY manuagent-web/src ./manuagent-web/src
|
||||
# 直接打包只解析项目真正需要的依赖;独立 go-offline 会额外下载大量未参与构建的报告插件。
|
||||
RUN --mount=type=cache,target=/root/.m2 mvn -B -DskipTests package
|
||||
|
||||
# AgentScope DockerSandbox 通过 docker 命令创建 Runtime,直接复用官方镜像中的 CLI 二进制。
|
||||
FROM docker:29-cli AS docker-cli
|
||||
|
||||
FROM eclipse-temurin:21-jre-jammy
|
||||
FROM eclipse-temurin:25.0.2_10-jre-jammy
|
||||
|
||||
# curl 用于容器健康检查;gosu 用于完成目录和 Docker Socket 权限初始化后降权运行 Java。
|
||||
RUN apt-get update \
|
||||
@@ -27,7 +35,7 @@ RUN groupadd --gid 10001 manuagent \
|
||||
&& chown -R manuagent:manuagent /opt/manuagent /srv/manuagent
|
||||
|
||||
WORKDIR /opt/manuagent
|
||||
COPY --from=builder /workspace/target/*.jar app.jar
|
||||
COPY --from=builder /workspace/manuagent-web/target/manuagent-web.jar app.jar
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Agent Runtime 的目录通过宿主机 Docker daemon 再次挂载,因此后端必须能写入共享数据根目录。
|
||||
# 沙箱的目录通过宿主机 Docker daemon 再次挂载,因此后端必须能写入共享数据根目录。
|
||||
data_root="${APP_DATA_ROOT:-/srv/manuagent/data}"
|
||||
mkdir -p "$data_root"
|
||||
chown manuagent:manuagent "$data_root"
|
||||
@@ -10,7 +10,7 @@ chown manuagent:manuagent "$data_root"
|
||||
# 启动时读取真实组 ID 并把低权限应用用户加入对应组,避免以 root 身份运行 Spring Boot。
|
||||
docker_socket="/var/run/docker.sock"
|
||||
if [ ! -S "$docker_socket" ]; then
|
||||
echo "错误:未挂载 $docker_socket,后端无法创建 Agent Runtime 容器。" >&2
|
||||
echo "错误:未挂载 $docker_socket,后端无法创建沙箱容器。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
46
server/manuagent-admin/pom.xml
Normal file
46
server/manuagent-admin/pom.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-parent</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</parent>
|
||||
<artifactId>manuagent-admin</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mybatis-flex</groupId>
|
||||
<artifactId>mybatis-flex-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.admin.auth;
|
||||
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
@@ -7,7 +7,7 @@ import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.core.keygen.KeyGenerators;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.app_user} 表的管理员用户实体。
|
||||
@@ -1,7 +1,7 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.admin.auth;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.manuagent.entity.AppUserEntity;
|
||||
import tech.easyflow.manuagent.admin.auth.AppUserEntity;
|
||||
|
||||
/**
|
||||
* 提供 {@code app.app_user} 表的 MyBatis-Flex 基础数据访问能力。
|
||||
@@ -9,5 +9,6 @@ import tech.easyflow.manuagent.entity.AppUserEntity;
|
||||
* <p>用户表只有简单单表操作,因此直接使用 {@link BaseMapper} 和 Lambda QueryWrapper,
|
||||
* 不额外维护 Mapper XML。</p>
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface AppUserMapper extends BaseMapper<AppUserEntity> {
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package tech.easyflow.manuagent.auth;
|
||||
package tech.easyflow.manuagent.admin.auth;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.util.UUID;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
@@ -13,20 +10,19 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.entity.AppUserEntity;
|
||||
import tech.easyflow.manuagent.mapper.AppUserMapper;
|
||||
import tech.easyflow.manuagent.admin.config.AdminProperties;
|
||||
import tech.easyflow.manuagent.admin.auth.AppUserEntity;
|
||||
import tech.easyflow.manuagent.admin.auth.AppUserMapper;
|
||||
|
||||
/**
|
||||
* 管理单管理员账户和当前用户标识。
|
||||
*/
|
||||
@Service
|
||||
@Order(1)
|
||||
public class UserService implements UserDetailsService, ApplicationRunner {
|
||||
public class UserService implements UserDetailsService {
|
||||
|
||||
private final AppUserMapper userMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final AppProperties properties;
|
||||
private final AdminProperties properties;
|
||||
|
||||
/**
|
||||
* 创建用户服务。
|
||||
@@ -35,7 +31,7 @@ public class UserService implements UserDetailsService, ApplicationRunner {
|
||||
* @param passwordEncoder 密码编码器
|
||||
* @param properties 应用配置
|
||||
*/
|
||||
public UserService(AppUserMapper userMapper, PasswordEncoder passwordEncoder, AppProperties properties) {
|
||||
public UserService(AppUserMapper userMapper, PasswordEncoder passwordEncoder, AdminProperties properties) {
|
||||
this.userMapper = userMapper;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.properties = properties;
|
||||
@@ -44,10 +40,8 @@ public class UserService implements UserDetailsService, ApplicationRunner {
|
||||
/**
|
||||
* 初始化本地管理员账户。
|
||||
*
|
||||
* @param args 启动参数
|
||||
*/
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
public void initializeAdministrator() {
|
||||
long count = userMapper.selectCountByQuery(QueryWrapper.create());
|
||||
if (count > 0) {
|
||||
return;
|
||||
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.manuagent.admin.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/** 首次启动时的管理员账号配置。 */
|
||||
@ConfigurationProperties(prefix = "app")
|
||||
public record AdminProperties(String adminUsername, String adminPassword) {
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.auth;
|
||||
package tech.easyflow.manuagent.admin.auth;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -21,10 +21,10 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.entity.AppUserEntity;
|
||||
import tech.easyflow.manuagent.mapper.AppUserMapper;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.admin.config.AdminProperties;
|
||||
import tech.easyflow.manuagent.admin.auth.AppUserEntity;
|
||||
import tech.easyflow.manuagent.admin.auth.AppUserMapper;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 验证 {@link UserService} 在迁移到 MyBatis-Flex 后保持原有管理员初始化与认证语义。
|
||||
@@ -33,7 +33,7 @@ class UserServiceTest {
|
||||
|
||||
private AppUserMapper mapper;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
private AppProperties properties;
|
||||
private AdminProperties properties;
|
||||
private UserService service;
|
||||
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ class UserServiceTest {
|
||||
|
||||
mapper = mock(AppUserMapper.class);
|
||||
passwordEncoder = mock(PasswordEncoder.class);
|
||||
properties = mock(AppProperties.class);
|
||||
properties = mock(AdminProperties.class);
|
||||
service = new UserService(mapper, passwordEncoder, properties);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class UserServiceTest {
|
||||
when(properties.adminPassword()).thenReturn("plain-password");
|
||||
when(passwordEncoder.encode("plain-password")).thenReturn("encoded-password");
|
||||
|
||||
service.run(null);
|
||||
service.initializeAdministrator();
|
||||
|
||||
ArgumentCaptor<AppUserEntity> captor = ArgumentCaptor.forClass(AppUserEntity.class);
|
||||
verify(mapper).insertSelective(captor.capture());
|
||||
@@ -84,7 +84,7 @@ class UserServiceTest {
|
||||
void shouldNotCreateAdministratorWhenAnyUserExists() {
|
||||
when(mapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(1L);
|
||||
|
||||
service.run(null);
|
||||
service.initializeAdministrator();
|
||||
|
||||
verify(mapper, never()).insertSelective(any(AppUserEntity.class));
|
||||
}
|
||||
83
server/manuagent-agent/pom.xml
Normal file
83
server/manuagent-agent/pom.xml
Normal file
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-parent</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</parent>
|
||||
<artifactId>manuagent-agent</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mybatis-flex</groupId>
|
||||
<artifactId>mybatis-flex-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-tx</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tools.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.agentscope</groupId>
|
||||
<artifactId>agentscope-harness</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.agentscope</groupId>
|
||||
<artifactId>agentscope-extensions-model-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.agentscope</groupId>
|
||||
<artifactId>agentscope-extensions-agui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.agentscope</groupId>
|
||||
<artifactId>agentscope-extensions-skill-postgresql-repository</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.artifact;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -7,8 +7,8 @@ import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.keygen.KeyGenerators;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.artifact} 表的最终产物实体。
|
||||
@@ -1,12 +1,13 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.artifact;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import tech.easyflow.manuagent.entity.ArtifactEntity;
|
||||
import tech.easyflow.manuagent.agent.artifact.ArtifactEntity;
|
||||
|
||||
/**
|
||||
* 提供产物基础查询以及 PostgreSQL 原子 upsert 能力。
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface ArtifactMapper extends BaseMapper<ArtifactEntity> {
|
||||
|
||||
/**
|
||||
@@ -1,11 +1,11 @@
|
||||
package tech.easyflow.manuagent.artifact;
|
||||
package tech.easyflow.manuagent.agent.artifact;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ArtifactEntity;
|
||||
import tech.easyflow.manuagent.mapper.ArtifactMapper;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import tech.easyflow.manuagent.agent.artifact.ArtifactEntity;
|
||||
import tech.easyflow.manuagent.agent.artifact.ArtifactMapper;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
@@ -78,9 +78,9 @@ public class ArtifactService {
|
||||
}
|
||||
throw new ApiException(HttpStatus.INTERNAL_SERVER_ERROR, "ARTIFACT_COPY_FAILED", "申报书发布失败");
|
||||
}
|
||||
com.fasterxml.jackson.databind.node.ObjectNode enriched = metadata.isObject()
|
||||
? ((com.fasterxml.jackson.databind.node.ObjectNode) metadata).deepCopy()
|
||||
: com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode();
|
||||
tools.jackson.databind.node.ObjectNode enriched = metadata.isObject()
|
||||
? ((tools.jackson.databind.node.ObjectNode) metadata).deepCopy()
|
||||
: tools.jackson.databind.node.JsonNodeFactory.instance.objectNode();
|
||||
enriched.put("docxValidated", true);
|
||||
enriched.put("docxEntries", validation.entryCount());
|
||||
enriched.put("commentCount", validation.commentCount());
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.artifact;
|
||||
package tech.easyflow.manuagent.agent.artifact;
|
||||
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import java.io.IOException;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.config;
|
||||
package tech.easyflow.manuagent.agent.config;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
@@ -10,19 +10,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
* @param dataRoot 项目材料、工作区和产物根目录
|
||||
* @param dashscopeKeyFile 百炼 Key 文件
|
||||
* @param masterKey 模型密钥加密主密钥
|
||||
* @param adminUsername 本地管理员用户名
|
||||
* @param adminPassword 本地管理员初始密码
|
||||
* @param sandboxImage Agent Docker 运行镜像
|
||||
* @param sandboxNetwork Agent Docker 网络
|
||||
* @param runTimeout 单次 Agent 运行超时
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "app")
|
||||
public record AppProperties(
|
||||
public record AgentProperties(
|
||||
Path dataRoot,
|
||||
Path dashscopeKeyFile,
|
||||
String masterKey,
|
||||
String adminUsername,
|
||||
String adminPassword,
|
||||
String sandboxImage,
|
||||
String sandboxNetwork,
|
||||
Duration runTimeout) {
|
||||
@@ -1,7 +1,7 @@
|
||||
package tech.easyflow.manuagent.model;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.agent.config.AgentProperties;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
@@ -31,7 +31,7 @@ public class KeyCipher {
|
||||
*
|
||||
* @param properties 应用配置
|
||||
*/
|
||||
public KeyCipher(AppProperties properties) {
|
||||
public KeyCipher(AgentProperties properties) {
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||
.digest(properties.masterKey().getBytes(StandardCharsets.UTF_8));
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -6,7 +6,7 @@ import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.model_assignment} 表的 Agent 角色模型分配实体。
|
||||
@@ -1,12 +1,13 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import tech.easyflow.manuagent.entity.ModelAssignmentEntity;
|
||||
import tech.easyflow.manuagent.agent.model.ModelAssignmentEntity;
|
||||
|
||||
/**
|
||||
* 提供 Agent 角色模型分配及 PostgreSQL 原子 upsert。
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface ModelAssignmentMapper extends BaseMapper<ModelAssignmentEntity> {
|
||||
|
||||
/** 按角色插入或更新模型分配。 */
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -8,8 +8,8 @@ import com.mybatisflex.core.keygen.KeyGenerators;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.model_config} 表的模型配置与加密密钥实体。
|
||||
@@ -1,8 +1,8 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigEntity;
|
||||
|
||||
/**
|
||||
* 提供模型配置的 MyBatis-Flex CRUD 能力。
|
||||
@@ -10,6 +10,7 @@ import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
* <p>普通查询和条件更新继续复用 {@link BaseMapper};包含 PostgreSQL JSONB 参数的新增、更新
|
||||
* 使用 XML 显式声明 TypeHandler,避免写入行为依赖 MyBatis-Flex 全局表元数据的初始化顺序。</p>
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface ModelConfigMapper extends BaseMapper<ModelConfigEntity> {
|
||||
|
||||
/**
|
||||
@@ -1,21 +1,19 @@
|
||||
package tech.easyflow.manuagent.model;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ModelAssignmentEntity;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.entity.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelAssignmentEntity;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
@@ -38,7 +36,6 @@ public class ModelService {
|
||||
private final ModelConfigMapper modelMapper;
|
||||
private final ModelAssignmentMapper assignmentMapper;
|
||||
private final AgentRunMapper runMapper;
|
||||
private final UserService userService;
|
||||
private final KeyCipher keyCipher;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
@@ -49,7 +46,6 @@ public class ModelService {
|
||||
* @param modelMapper 模型配置 Mapper
|
||||
* @param assignmentMapper 角色模型分配 Mapper
|
||||
* @param runMapper Agent Run Mapper
|
||||
* @param userService 用户服务
|
||||
* @param keyCipher 密钥加密器
|
||||
* @param objectMapper JSON 映射器
|
||||
*/
|
||||
@@ -58,14 +54,12 @@ public class ModelService {
|
||||
ModelConfigMapper modelMapper,
|
||||
ModelAssignmentMapper assignmentMapper,
|
||||
AgentRunMapper runMapper,
|
||||
UserService userService,
|
||||
KeyCipher keyCipher,
|
||||
ObjectMapper objectMapper) {
|
||||
this(
|
||||
modelMapper,
|
||||
assignmentMapper,
|
||||
runMapper,
|
||||
userService,
|
||||
keyCipher,
|
||||
objectMapper,
|
||||
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build());
|
||||
@@ -77,7 +71,6 @@ public class ModelService {
|
||||
* @param modelMapper 模型配置 Mapper
|
||||
* @param assignmentMapper 角色模型分配 Mapper
|
||||
* @param runMapper Agent Run Mapper
|
||||
* @param userService 用户服务
|
||||
* @param keyCipher 密钥加密器
|
||||
* @param objectMapper JSON 映射器
|
||||
* @param httpClient 模型连接使用的 HTTP 客户端
|
||||
@@ -86,14 +79,12 @@ public class ModelService {
|
||||
ModelConfigMapper modelMapper,
|
||||
ModelAssignmentMapper assignmentMapper,
|
||||
AgentRunMapper runMapper,
|
||||
UserService userService,
|
||||
KeyCipher keyCipher,
|
||||
ObjectMapper objectMapper,
|
||||
HttpClient httpClient) {
|
||||
this.modelMapper = modelMapper;
|
||||
this.assignmentMapper = assignmentMapper;
|
||||
this.runMapper = runMapper;
|
||||
this.userService = userService;
|
||||
this.keyCipher = keyCipher;
|
||||
this.objectMapper = objectMapper;
|
||||
this.httpClient = httpClient;
|
||||
@@ -118,12 +109,12 @@ public class ModelService {
|
||||
*
|
||||
* @param id 可选模型 ID
|
||||
* @param input 模型输入
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 保存后的模型
|
||||
*/
|
||||
@Transactional
|
||||
public ModelView save(UUID id, ModelInput input, Principal principal) {
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
public ModelView save(UUID id, ModelInput input, UUID userId) {
|
||||
|
||||
contextWindow(input.capabilities());
|
||||
if (id == null) {
|
||||
if (input.apiKey() == null || input.apiKey().isBlank()) {
|
||||
@@ -165,15 +156,15 @@ public class ModelService {
|
||||
* 将模型设置为所有角色默认模型。
|
||||
*
|
||||
* @param id 模型 ID
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
*/
|
||||
@Transactional
|
||||
public void setDefault(UUID id, Principal principal) {
|
||||
public void setDefault(UUID id, UUID userId) {
|
||||
ModelView target = require(id);
|
||||
if (!target.enabled()) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "MODEL_DISABLED", "停用模型不能设为默认模型");
|
||||
}
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
|
||||
modelMapper.clearDefault();
|
||||
if (modelMapper.setDefault(id) != 1) {
|
||||
throw new ApiException(HttpStatus.NOT_FOUND, "MODEL_NOT_FOUND", "模型配置不存在");
|
||||
@@ -387,7 +378,7 @@ public class ModelService {
|
||||
private Map<String, Object> parseCapabilities(String json) {
|
||||
try {
|
||||
return objectMapper.readValue(json, Map.class);
|
||||
} catch (IOException exception) {
|
||||
} catch (tools.jackson.core.JacksonException exception) {
|
||||
throw new ApiException(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"MODEL_CAPABILITIES_INVALID",
|
||||
@@ -528,7 +519,7 @@ public class ModelService {
|
||||
private String json(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value == null ? Map.of() : value);
|
||||
} catch (IOException exception) {
|
||||
} catch (tools.jackson.core.JacksonException exception) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "INVALID_MODEL_CONFIG", "模型配置无法序列化");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -7,7 +7,7 @@ import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.keygen.KeyGenerators;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.project} 表的企业申报项目实体。
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -7,7 +7,7 @@ import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.keygen.KeyGenerators;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.project_file} 表的项目材料实体。
|
||||
@@ -1,10 +1,11 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.manuagent.entity.ProjectFileEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileEntity;
|
||||
|
||||
/**
|
||||
* 提供 {@code app.project_file} 表的单表持久化能力。
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface ProjectFileMapper extends BaseMapper<ProjectFileEntity> {
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package tech.easyflow.manuagent.project;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.entity.ProjectFileEntity;
|
||||
import tech.easyflow.manuagent.mapper.ProjectFileMapper;
|
||||
import tech.easyflow.manuagent.agent.config.AgentProperties;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileMapper;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -15,7 +14,6 @@ import java.nio.file.Path;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Principal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
@@ -42,7 +40,6 @@ public class ProjectFileService {
|
||||
"png", "jpg", "jpeg", "webp", "vsdx", "dwg");
|
||||
|
||||
private final ProjectFileMapper fileMapper;
|
||||
private final UserService userService;
|
||||
private final ProjectService projectService;
|
||||
private final Path dataRoot;
|
||||
private final Tika tika = new Tika();
|
||||
@@ -51,17 +48,14 @@ public class ProjectFileService {
|
||||
* 创建材料服务。
|
||||
*
|
||||
* @param fileMapper 项目材料 Mapper
|
||||
* @param userService 用户服务
|
||||
* @param projectService 项目服务
|
||||
* @param properties 应用配置
|
||||
*/
|
||||
public ProjectFileService(
|
||||
ProjectFileMapper fileMapper,
|
||||
UserService userService,
|
||||
ProjectService projectService,
|
||||
AppProperties properties) {
|
||||
AgentProperties properties) {
|
||||
this.fileMapper = fileMapper;
|
||||
this.userService = userService;
|
||||
this.projectService = projectService;
|
||||
this.dataRoot = properties.dataRoot().toAbsolutePath().normalize();
|
||||
}
|
||||
@@ -110,11 +104,11 @@ public class ProjectFileService {
|
||||
* @param projectId 项目 ID
|
||||
* @param file 上传文件
|
||||
* @param relativePath 浏览器提供的文件夹内相对路径;单文件上传时可为空
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 文件元数据
|
||||
*/
|
||||
@Transactional
|
||||
public FileView upload(UUID projectId, MultipartFile file, String relativePath, Principal principal) {
|
||||
public FileView upload(UUID projectId, MultipartFile file, String relativePath, UUID userId) {
|
||||
projectService.require(projectId);
|
||||
String originalName = safeName(file.getOriginalFilename());
|
||||
String extension = extension(originalName);
|
||||
@@ -143,7 +137,7 @@ public class ProjectFileService {
|
||||
}
|
||||
Files.move(temporary, target);
|
||||
moved = true;
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
|
||||
// 数据库只保存受控路径和摘要;selective insert 继续使用状态、时间字段的数据库默认值。
|
||||
ProjectFileEntity entity = new ProjectFileEntity();
|
||||
entity.setId(fileId);
|
||||
@@ -1,13 +1,14 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import tech.easyflow.manuagent.entity.ProjectEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectEntity;
|
||||
|
||||
/**
|
||||
* 提供项目基础 CRUD、阶段更新和项目级联清理所需的显式 SQL 接口。
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface ProjectMapper extends BaseMapper<ProjectEntity> {
|
||||
|
||||
/** 判断项目是否仍有运行中的 Agent。 */
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -8,8 +8,8 @@ import com.mybatisflex.core.keygen.KeyGenerators;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.project_plan} 表的不可变规划版本实体。
|
||||
@@ -1,13 +1,14 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import tech.easyflow.manuagent.entity.ProjectPlanEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectPlanEntity;
|
||||
|
||||
/**
|
||||
* 提供规划版本查询以及带条件的草稿写入、确认能力。
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface ProjectPlanMapper extends BaseMapper<ProjectPlanEntity> {
|
||||
|
||||
/** 插入项目下一版草稿并返回完整记录。 */
|
||||
@@ -1,16 +1,14 @@
|
||||
package tech.easyflow.manuagent.project;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ProjectEntity;
|
||||
import tech.easyflow.manuagent.entity.ProjectPlanEntity;
|
||||
import tech.easyflow.manuagent.mapper.ProjectMapper;
|
||||
import tech.easyflow.manuagent.mapper.ProjectPlanMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.security.Principal;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectPlanEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectMapper;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectPlanMapper;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -26,7 +24,6 @@ public class ProjectService {
|
||||
|
||||
private final ProjectMapper projectMapper;
|
||||
private final ProjectPlanMapper planMapper;
|
||||
private final UserService userService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
@@ -34,17 +31,14 @@ public class ProjectService {
|
||||
*
|
||||
* @param projectMapper 项目 Mapper
|
||||
* @param planMapper 规划版本 Mapper
|
||||
* @param userService 用户服务
|
||||
* @param objectMapper JSON 映射器
|
||||
*/
|
||||
public ProjectService(
|
||||
ProjectMapper projectMapper,
|
||||
ProjectPlanMapper planMapper,
|
||||
UserService userService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.projectMapper = projectMapper;
|
||||
this.planMapper = planMapper;
|
||||
this.userService = userService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -53,14 +47,14 @@ public class ProjectService {
|
||||
*
|
||||
* @param companyName 企业名称
|
||||
* @param applicationLevel 申报等级
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 新项目
|
||||
*/
|
||||
@Transactional
|
||||
public ProjectView create(String companyName, String applicationLevel, Principal principal) {
|
||||
public ProjectView create(String companyName, String applicationLevel, UUID userId) {
|
||||
String level = normalizeLevel(applicationLevel);
|
||||
UUID id = UUID.randomUUID();
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
|
||||
String threadId = "project-" + id;
|
||||
ProjectEntity entity = new ProjectEntity();
|
||||
entity.setId(id);
|
||||
@@ -176,12 +170,12 @@ public class ProjectService {
|
||||
* @param projectId 项目 ID
|
||||
* @param planId 草稿规划 ID
|
||||
* @param plan 用户确认后的完整规划
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 已确认规划
|
||||
*/
|
||||
@Transactional
|
||||
public PlanView confirmPlan(UUID projectId, UUID planId, JsonNode plan, Principal principal) {
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
public PlanView confirmPlan(UUID projectId, UUID planId, JsonNode plan, UUID userId) {
|
||||
|
||||
ProjectPlanEntity confirmed = planMapper.confirmDraft(projectId, planId, plan.toString(), userId);
|
||||
if (confirmed == null) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "PLAN_ALREADY_CONFIRMED", "规划已确认或版本不存在");
|
||||
@@ -201,7 +195,7 @@ public class ProjectService {
|
||||
objectMapper.readTree(entity.getPlanJson()),
|
||||
entity.getConfirmedAt(),
|
||||
entity.getCreatedAt());
|
||||
} catch (JsonProcessingException exception) {
|
||||
} catch (JacksonException exception) {
|
||||
// 迁移前 ResultSet 映射会将损坏的存量 JSON 作为未预期数据库读取异常处理,不新增业务错误码。
|
||||
throw new IllegalStateException("规划 JSON 无法解析", exception);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -7,8 +7,8 @@ import com.mybatisflex.annotation.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.agent_event} 表的持久化 Agent 事件。
|
||||
@@ -1,13 +1,14 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import tech.easyflow.manuagent.entity.AgentEventEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentEventEntity;
|
||||
|
||||
/**
|
||||
* 提供 Agent 事件写入、游标回放及运行恢复所需的持久化能力。
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface AgentEventMapper extends BaseMapper<AgentEventEntity> {
|
||||
|
||||
/**
|
||||
@@ -1,8 +1,8 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
@@ -17,8 +17,8 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import tech.easyflow.manuagent.entity.AgentEventEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentEventMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentEventEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentEventMapper;
|
||||
|
||||
/**
|
||||
* 持久化并查询项目级 AG-UI 事件。
|
||||
@@ -175,7 +175,7 @@ public class AgentEventService {
|
||||
entity.getEventType(),
|
||||
objectMapper.readTree(entity.getPayloadJson()),
|
||||
entity.getCreatedAt());
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException exception) {
|
||||
} catch (tools.jackson.core.JacksonException exception) {
|
||||
throw new IllegalStateException("Agent 事件 JSON 无法解析", exception);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.project.ProjectService;
|
||||
import tech.easyflow.manuagent.skill.SkillService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectService;
|
||||
import tech.easyflow.manuagent.agent.skill.SkillService;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
import io.agentscope.core.agui.event.AguiEvent;
|
||||
import io.agentscope.core.agui.model.AguiMessage;
|
||||
import io.agentscope.core.agui.model.RunAgentInput;
|
||||
import io.agentscope.core.model.ModelHttpException;
|
||||
import io.agentscope.core.model.transport.HttpTransportException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import tech.easyflow.manuagent.agent.config.AgentProperties;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -36,6 +40,7 @@ public class AgentExecutionService {
|
||||
private final AgentEventService eventService;
|
||||
private final ProjectFileService fileService;
|
||||
private final SkillService skillService;
|
||||
private final Duration runTimeout;
|
||||
|
||||
/**
|
||||
* 创建 Agent 执行服务。
|
||||
@@ -45,18 +50,24 @@ public class AgentExecutionService {
|
||||
* @param eventService 事件服务
|
||||
* @param fileService 工作区服务
|
||||
* @param skillService Skill 服务
|
||||
* @param properties Run 总时限配置
|
||||
*/
|
||||
public AgentExecutionService(
|
||||
ObjectMapper objectMapper,
|
||||
AgentFactory agentFactory,
|
||||
AgentEventService eventService,
|
||||
ProjectFileService fileService,
|
||||
SkillService skillService) {
|
||||
SkillService skillService,
|
||||
AgentProperties properties) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.agentFactory = agentFactory;
|
||||
this.eventService = eventService;
|
||||
this.fileService = fileService;
|
||||
this.skillService = skillService;
|
||||
this.runTimeout = properties.runTimeout();
|
||||
if (runTimeout == null || runTimeout.isZero() || runTimeout.isNegative()) {
|
||||
throw new IllegalArgumentException("app.run-timeout 必须大于 0");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,6 +90,7 @@ public class AgentExecutionService {
|
||||
EventAccumulator accumulator = new EventAccumulator(project.id(), run.id());
|
||||
for (int reconnects = 0; ; reconnects++) {
|
||||
ensureRunning.run();
|
||||
remainingTime(run);
|
||||
String attemptPrompt = reconnects == 0 ? prompt : """
|
||||
模型连接刚刚中断。请恢复同一线程的会话状态,读取 MEMORY.md 和工作区已有成果,
|
||||
检查未完成的输出后从中断处继续;复用已经完成的工具结果,不要重复已完成操作。
|
||||
@@ -90,13 +102,15 @@ public class AgentExecutionService {
|
||||
.build();
|
||||
try (AgentFactory.AgentHandle handle = agentFactory.create(
|
||||
project.id(), run.modelConfigId(), skillService.enabledNames())) {
|
||||
Mono<Void> deadline = Mono.delay(remainingTime(run)).then(Mono.error(timeoutFailure()));
|
||||
handle.adapter().run(input)
|
||||
.takeUntilOther(stopSignal)
|
||||
.takeUntilOther(Mono.firstWithSignal(stopSignal, deadline))
|
||||
.bufferTimeout(64, Duration.ofMillis(120))
|
||||
.doOnNext(accumulator::accept)
|
||||
.doOnNext(batch -> accumulator.accept(batch, handle.runtime()))
|
||||
.blockLast();
|
||||
accumulator.flush();
|
||||
ensureRunning.run();
|
||||
remainingTime(run);
|
||||
return;
|
||||
} catch (RuntimeException exception) {
|
||||
accumulator.flush();
|
||||
@@ -109,7 +123,7 @@ public class AgentExecutionService {
|
||||
int attempt = reconnects + 1;
|
||||
eventService.append(project.id(), run.id(), "MODEL_RETRY", Map.of(
|
||||
"attempt", attempt, "maxAttempts", MAX_MODEL_RECONNECTS));
|
||||
pauseBeforeReconnect(attempt, interrupted);
|
||||
pauseBeforeReconnect(attempt, interrupted, remainingTime(run));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,9 +151,19 @@ public class AgentExecutionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void pauseBeforeReconnect(int attempt, BooleanSupplier interrupted) {
|
||||
Duration remainingTime(AgentRunService.RunView run) {
|
||||
Duration remaining = Duration.between(Instant.now(), run.startedAt().toInstant().plus(runTimeout));
|
||||
if (remaining.isNegative() || remaining.isZero()) throw timeoutFailure();
|
||||
return remaining;
|
||||
}
|
||||
|
||||
private ApiException timeoutFailure() {
|
||||
return new ApiException(HttpStatus.REQUEST_TIMEOUT, "AGENT_RUN_TIMEOUT", "任务已超过运行时限,现有成果已保留,可稍后继续");
|
||||
}
|
||||
|
||||
private void pauseBeforeReconnect(int attempt, BooleanSupplier interrupted, Duration remaining) {
|
||||
try {
|
||||
Thread.sleep(Math.min(8_000L, 500L << (attempt - 1)));
|
||||
Thread.sleep(Math.min(remaining.toMillis(), Math.min(8_000L, 500L << (attempt - 1))));
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (interrupted.getAsBoolean()) {
|
||||
@@ -173,18 +197,24 @@ public class AgentExecutionService {
|
||||
this.runId = runId;
|
||||
}
|
||||
|
||||
private void accept(List<AguiEvent> batch) {
|
||||
private void accept(List<AguiEvent> batch, AgentRuntimeMiddleware runtime) {
|
||||
for (AguiEvent event : batch) {
|
||||
accept(event);
|
||||
accept(event, runtime);
|
||||
}
|
||||
if (System.nanoTime() - lastFlushNanos >= Duration.ofMillis(400).toNanos()) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void accept(AguiEvent event) {
|
||||
private void accept(AguiEvent event, AgentRuntimeMiddleware runtime) {
|
||||
String type = event.getType().name();
|
||||
if (type.equals("RUN_STARTED") || type.equals("RUN_FINISHED") || type.equals("RUN_ERROR")) {
|
||||
// Adapter 将基础设施异常转成事件;必须终止执行,不能当作缺少 JSON 再次调用 Agent。
|
||||
if (event instanceof AguiEvent.RunError error) {
|
||||
String message = sanitize(objectMapper.getNodeFactory().textNode(error.message()),
|
||||
fileService.projectRoot(projectId).toString()).asText();
|
||||
throw new IllegalStateException("Agent 运行错误:" + message, runtime == null ? null : runtime.modelFailure());
|
||||
}
|
||||
if (type.equals("RUN_STARTED") || type.equals("RUN_FINISHED")) {
|
||||
return;
|
||||
}
|
||||
ObjectNode payload = (ObjectNode) sanitize(
|
||||
@@ -1,9 +1,9 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.model.ModelService;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tech.easyflow.manuagent.agent.config.AgentProperties;
|
||||
import tech.easyflow.manuagent.agent.model.ModelService;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import io.agentscope.core.agui.adapter.AguiAdapterConfig;
|
||||
import io.agentscope.core.agui.adapter.AguiAgentAdapter;
|
||||
import io.agentscope.core.skill.AgentSkill;
|
||||
@@ -11,6 +11,8 @@ import io.agentscope.core.skill.repository.AgentSkillRepository;
|
||||
import io.agentscope.core.skill.repository.AgentSkillRepositoryInfo;
|
||||
import io.agentscope.core.skill.repository.postgresql.PostgresSkillRepository;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
import io.agentscope.core.state.JsonFileAgentStateStore;
|
||||
import io.agentscope.harness.agent.filesystem.sandbox.AbstractSandboxFilesystem;
|
||||
import io.agentscope.extensions.model.openai.OpenAIChatModel;
|
||||
import io.agentscope.harness.agent.IsolationScope;
|
||||
import io.agentscope.harness.agent.HarnessAgent;
|
||||
@@ -45,7 +47,7 @@ public class AgentFactory {
|
||||
private final ModelService modelService;
|
||||
private final PostgresSkillRepository skillRepository;
|
||||
private final ProjectFileService fileService;
|
||||
private final AppProperties properties;
|
||||
private final AgentProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final String systemPrompt;
|
||||
private final String compactionPrompt;
|
||||
@@ -63,7 +65,7 @@ public class AgentFactory {
|
||||
ModelService modelService,
|
||||
PostgresSkillRepository skillRepository,
|
||||
ProjectFileService fileService,
|
||||
AppProperties properties,
|
||||
AgentProperties properties,
|
||||
ObjectMapper objectMapper) {
|
||||
this.modelService = modelService;
|
||||
this.skillRepository = skillRepository;
|
||||
@@ -72,6 +74,7 @@ public class AgentFactory {
|
||||
this.objectMapper = objectMapper;
|
||||
this.systemPrompt = readPrompt("prompts/smart-factory-agent-system.md", "Agent 全局提示词");
|
||||
this.compactionPrompt = readPrompt("prompts/smart-factory-compaction.md", "上下文压缩提示词");
|
||||
AgentStateFiles.migrateExisting(properties.dataRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +99,7 @@ public class AgentFactory {
|
||||
Map<String, String> environment = new HashMap<>();
|
||||
environment.put("DASHSCOPE_API_KEY", readOptionalKey(properties.dashscopeKeyFile()));
|
||||
environment.put("PATH", "/opt/java/openjdk/bin:/usr/local/bin:/usr/bin:/bin");
|
||||
environment.put("NODE_PATH", "/opt/agent-runtime/node_modules");
|
||||
environment.put("NODE_PATH", "/opt/sandbox/node_modules");
|
||||
environment.put("SKILL_SESSION_ID", "project-" + projectId);
|
||||
|
||||
WorkspaceSpec workspace = new WorkspaceSpec();
|
||||
@@ -115,6 +118,7 @@ public class AgentFactory {
|
||||
throw new IllegalStateException("无法创建 Agent 沙箱快照目录", exception);
|
||||
}
|
||||
DockerFilesystemSpec filesystem = new DockerFilesystemSpec()
|
||||
.client(new RecoverableDockerClient(properties.sandboxImage()))
|
||||
.image(properties.sandboxImage())
|
||||
.workspaceRoot("/workspace")
|
||||
.environment(environment)
|
||||
@@ -133,13 +137,17 @@ public class AgentFactory {
|
||||
|
||||
CompactionConfig compaction = compactionFor(model.contextWindow(), compactionPrompt);
|
||||
Toolkit toolkit = new Toolkit();
|
||||
DocumentViewTool documentView = new DocumentViewTool(objectMapper);
|
||||
DocumentViewTool documentView = new DocumentViewTool(objectMapper, fileService, projectId);
|
||||
toolkit.registerTool(documentView);
|
||||
AgentRuntimeMiddleware runtime = new AgentRuntimeMiddleware();
|
||||
Path stateRoot = AgentStateFiles.prepare(properties.dataRoot(), AgentStateFiles.legacyRoot(), projectId);
|
||||
HarnessAgent agent = HarnessAgent.builder()
|
||||
.name("smart-factory-agent")
|
||||
.description("智能工厂申报书规划、编写与评审 Agent")
|
||||
.sysPrompt(systemPrompt)
|
||||
.model(chatModel)
|
||||
.stateStore(new JsonFileAgentStateStore(stateRoot))
|
||||
.middleware(runtime)
|
||||
.toolkit(toolkit)
|
||||
.workspace(projectRoot)
|
||||
.filesystem(filesystem)
|
||||
@@ -148,6 +156,8 @@ public class AgentFactory {
|
||||
.toolResultEviction(ToolResultEvictionConfig.defaults())
|
||||
.maxContextTokens(model.contextWindow())
|
||||
.maxIters(96)
|
||||
// 中断的工具调用以失败结果补齐,由模型检查已有成果后继续;不绕过权限确认。
|
||||
.enablePendingToolRecovery(true)
|
||||
.enableAgentTracingLog(false)
|
||||
.disableSubagents()
|
||||
.build();
|
||||
@@ -155,15 +165,16 @@ public class AgentFactory {
|
||||
agent.getWorkspaceManager().getFilesystem(),
|
||||
WorkspacePathNormalizer.of("/workspace"),
|
||||
objectMapper));
|
||||
agent.getToolkit().registerTool(new SandboxTools(
|
||||
(AbstractSandboxFilesystem) agent.getWorkspaceManager().getFilesystem(), objectMapper));
|
||||
documentView.bind(agent);
|
||||
AguiAdapterConfig config = AguiAdapterConfig.builder()
|
||||
.enableReasoning(true)
|
||||
.emitToolCallArgs(true)
|
||||
.emitStateEvents(false)
|
||||
.runTimeout(properties.runTimeout())
|
||||
.defaultAgentId("smart-factory-agent")
|
||||
.build();
|
||||
return new AgentHandle(new AguiAgentAdapter(agent, config), agent);
|
||||
return new AgentHandle(new AguiAgentAdapter(agent, config), agent, runtime);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,8 +251,9 @@ public class AgentFactory {
|
||||
*
|
||||
* @param adapter AG-UI 适配器
|
||||
* @param agent Harness Agent
|
||||
* @param runtime 模型调用约束及原始异常
|
||||
*/
|
||||
public record AgentHandle(AguiAgentAdapter adapter, HarnessAgent agent) implements AutoCloseable {
|
||||
public record AgentHandle(AguiAgentAdapter adapter, HarnessAgent agent, AgentRuntimeMiddleware runtime) implements AutoCloseable {
|
||||
|
||||
/**
|
||||
* 结束 Agent 并释放 Docker 沙箱资源。
|
||||
@@ -1,10 +1,10 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -8,8 +8,8 @@ import com.mybatisflex.core.keygen.KeyGenerators;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射 {@code app.agent_run} 表的 Agent 运行状态实体。
|
||||
@@ -1,13 +1,14 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import tech.easyflow.manuagent.entity.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunEntity;
|
||||
|
||||
/**
|
||||
* 提供 Agent Run 查询以及带前置状态条件的原子状态迁移。
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface AgentRunMapper extends BaseMapper<AgentRunEntity> {
|
||||
|
||||
/** 将等待输入的 Run 标记为已完成。 */
|
||||
@@ -1,22 +1,24 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import tech.easyflow.manuagent.artifact.ArtifactService;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.agent.artifact.ArtifactService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.project.ProjectService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectService;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -26,7 +28,7 @@ import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
|
||||
/**
|
||||
* 驱动材料检验、规划 Ask 和自动编写 Run。
|
||||
@@ -43,10 +45,11 @@ public class AgentRunService {
|
||||
private final AgentEventService eventService;
|
||||
private final ProjectService projectService;
|
||||
private final ProjectFileService fileService;
|
||||
private final UserService userService;
|
||||
private final ArtifactService artifactService;
|
||||
private final ExecutorService executor;
|
||||
private final TransactionTemplate transactions;
|
||||
static final int MAX_OUTPUT_REPAIRS = 3;
|
||||
private final Semaphore runSlots;
|
||||
private final ConcurrentMap<UUID, RunControl> activeRuns = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
@@ -60,10 +63,10 @@ public class AgentRunService {
|
||||
* @param eventService 事件服务
|
||||
* @param projectService 项目服务
|
||||
* @param fileService 材料与工作区服务
|
||||
* @param userService 用户服务
|
||||
* @param artifactService 产物服务
|
||||
* @param applicationExecutor 虚拟线程执行器
|
||||
* @param transactions 编程式事务模板
|
||||
* @param maxConcurrentRuns 单进程同时运行的任务上限
|
||||
*/
|
||||
public AgentRunService(
|
||||
AgentRunMapper runMapper,
|
||||
@@ -74,10 +77,10 @@ public class AgentRunService {
|
||||
AgentEventService eventService,
|
||||
ProjectService projectService,
|
||||
ProjectFileService fileService,
|
||||
UserService userService,
|
||||
ArtifactService artifactService,
|
||||
ExecutorService applicationExecutor,
|
||||
TransactionTemplate transactions) {
|
||||
TransactionTemplate transactions,
|
||||
@Value("${app.max-concurrent-runs:2}") int maxConcurrentRuns) {
|
||||
this.runMapper = runMapper;
|
||||
this.objectMapper = objectMapper;
|
||||
this.executionService = executionService;
|
||||
@@ -86,26 +89,27 @@ public class AgentRunService {
|
||||
this.eventService = eventService;
|
||||
this.projectService = projectService;
|
||||
this.fileService = fileService;
|
||||
this.userService = userService;
|
||||
this.artifactService = artifactService;
|
||||
this.executor = applicationExecutor;
|
||||
this.transactions = transactions;
|
||||
if (maxConcurrentRuns < 1) throw new IllegalArgumentException("app.max-concurrent-runs 必须大于 0");
|
||||
this.runSlots = new Semaphore(maxConcurrentRuns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台启动材料检验。
|
||||
*
|
||||
* @param projectId 项目 ID
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 新 Run
|
||||
*/
|
||||
@Transactional
|
||||
public RunView startMaterialCheck(UUID projectId, Principal principal) {
|
||||
public RunView startMaterialCheck(UUID projectId, UUID userId) {
|
||||
ProjectService.ProjectView project = projectService.require(projectId);
|
||||
userService.requireUserId(principal.getName());
|
||||
RunView previous = latest(projectId);
|
||||
RunView run = runStore.create(projectId, "INITIAL", null);
|
||||
projectService.updateStatus(projectId, "MATERIAL_CHECK");
|
||||
afterCommit(run.id(), () -> executeMaterialRun(project, run, false));
|
||||
afterCommit(run.id(), () -> executeMaterialRun(project, run, false), previous == null ? null : previous.id());
|
||||
return run;
|
||||
}
|
||||
|
||||
@@ -114,13 +118,13 @@ public class AgentRunService {
|
||||
*
|
||||
* @param projectId 项目 ID
|
||||
* @param response 用户对材料缺口的处理结果
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 新规划 Run
|
||||
*/
|
||||
@Transactional
|
||||
public RunView confirmMaterials(UUID projectId, JsonNode response, Principal principal) {
|
||||
public RunView confirmMaterials(UUID projectId, JsonNode response, UUID userId) {
|
||||
ProjectService.ProjectView project = projectService.require(projectId);
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
|
||||
RunView waiting = runStore.requireWaiting(projectId, "material_check");
|
||||
if (!response.isObject() || !response.path("decisions").isArray()) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "MATERIAL_RESPONSE_INVALID", "请确认每项材料缺口");
|
||||
@@ -129,7 +133,7 @@ public class AgentRunService {
|
||||
runStore.completeWaiting(waiting.id());
|
||||
RunView run = runStore.create(projectId, "RESUME", waiting.id());
|
||||
projectService.updateStatus(projectId, "PLANNING");
|
||||
afterCommit(run.id(), () -> executePlanningRun(project, run, userId, response, false));
|
||||
afterCommit(run.id(), () -> executePlanningRun(project, run, userId, response, false), waiting.id());
|
||||
return run;
|
||||
}
|
||||
|
||||
@@ -139,7 +143,7 @@ public class AgentRunService {
|
||||
* @param projectId 项目 ID
|
||||
* @param planId 规划 ID
|
||||
* @param confirmedPlan 用户确认后的规划
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 已确认规划及编写 Run
|
||||
*/
|
||||
@Transactional
|
||||
@@ -147,7 +151,7 @@ public class AgentRunService {
|
||||
UUID projectId,
|
||||
UUID planId,
|
||||
JsonNode confirmedPlan,
|
||||
Principal principal) {
|
||||
UUID userId) {
|
||||
ProjectService.ProjectView project = projectService.require(projectId);
|
||||
ProjectService.PlanView current = projectService.currentPlan(projectId);
|
||||
if (current != null && current.id().equals(planId) && "CONFIRMED".equals(current.status())) {
|
||||
@@ -158,11 +162,11 @@ public class AgentRunService {
|
||||
}
|
||||
outputService.validateConfirmedPlan(confirmedPlan);
|
||||
RunView waiting = runStore.requireWaiting(projectId, "planning");
|
||||
ProjectService.PlanView plan = projectService.confirmPlan(projectId, planId, confirmedPlan, principal);
|
||||
ProjectService.PlanView plan = projectService.confirmPlan(projectId, planId, confirmedPlan, userId);
|
||||
eventService.append(projectId, waiting.id(), "ASK_RESPONDED", Map.of("planId", planId));
|
||||
runStore.completeWaiting(waiting.id());
|
||||
RunView run = runStore.create(projectId, "RESUME", waiting.id());
|
||||
afterCommit(run.id(), () -> executeWritingRun(project, plan, run, false));
|
||||
afterCommit(run.id(), () -> executeWritingRun(project, plan, run, false), waiting.id());
|
||||
return new ConfirmPlanResult(plan, run);
|
||||
}
|
||||
|
||||
@@ -188,7 +192,7 @@ public class AgentRunService {
|
||||
}
|
||||
RunView run = runStore.create(projectId, "RESUME", latest == null ? null : latest.id());
|
||||
projectService.updateStatus(projectId, "WRITING");
|
||||
afterCommit(run.id(), () -> executeWritingRun(project, plan, run, false));
|
||||
afterCommit(run.id(), () -> executeWritingRun(project, plan, run, false), latest == null ? null : latest.id());
|
||||
return run;
|
||||
}
|
||||
|
||||
@@ -196,13 +200,13 @@ public class AgentRunService {
|
||||
* 立即停止当前 Run,并保留项目阶段和工作区成果。
|
||||
*
|
||||
* @param projectId 项目 ID
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 已中断的 Run
|
||||
*/
|
||||
@Transactional
|
||||
public RunView stop(UUID projectId, Principal principal) {
|
||||
public RunView stop(UUID projectId, UUID userId) {
|
||||
projectService.require(projectId);
|
||||
userService.requireUserId(principal.getName());
|
||||
|
||||
RunView run = latest(projectId);
|
||||
if (run == null || !"RUNNING".equals(run.status())) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "RUN_NOT_ACTIVE", "当前没有正在执行的任务");
|
||||
@@ -223,12 +227,12 @@ public class AgentRunService {
|
||||
* 从已中断 Run 的原阶段继续,复用同一线程状态和工作区成果。
|
||||
*
|
||||
* @param projectId 项目 ID
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 新的恢复 Run
|
||||
*/
|
||||
@Transactional
|
||||
public RunView resume(UUID projectId, Principal principal) {
|
||||
return resume(projectId, null, principal);
|
||||
public RunView resume(UUID projectId, UUID userId) {
|
||||
return resume(projectId, null, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,13 +240,13 @@ public class AgentRunService {
|
||||
*
|
||||
* @param projectId 项目 ID
|
||||
* @param modelConfigId 替代模型;为空时使用当前默认模型
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 新的恢复 Run
|
||||
*/
|
||||
@Transactional
|
||||
public RunView resume(UUID projectId, UUID modelConfigId, Principal principal) {
|
||||
public RunView resume(UUID projectId, UUID modelConfigId, UUID userId) {
|
||||
ProjectService.ProjectView project = projectService.require(projectId);
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
|
||||
RunView interrupted = latest(projectId);
|
||||
if (interrupted == null || !"INTERRUPTED".equals(interrupted.status())) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "RUN_NOT_INTERRUPTED", "当前没有可继续的任务");
|
||||
@@ -250,7 +254,7 @@ public class AgentRunService {
|
||||
String phase = runStore.interruptedPhase(interrupted, project);
|
||||
RunView run = runStore.create(projectId, "RESUME", interrupted.id(), modelConfigId);
|
||||
projectService.updateStatus(projectId, phase);
|
||||
scheduleResume(project, run, userId, phase);
|
||||
scheduleResume(project, run, userId, phase, interrupted.id());
|
||||
return run;
|
||||
}
|
||||
|
||||
@@ -263,13 +267,13 @@ public class AgentRunService {
|
||||
*
|
||||
* @param projectId 项目 ID
|
||||
* @param modelConfigId 替代模型 ID
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 绑定替代模型的新恢复 Run
|
||||
*/
|
||||
@Transactional
|
||||
public RunView switchModel(UUID projectId, UUID modelConfigId, Principal principal) {
|
||||
public RunView switchModel(UUID projectId, UUID modelConfigId, UUID userId) {
|
||||
ProjectService.ProjectView project = projectService.require(projectId);
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
|
||||
RunView current = latest(projectId);
|
||||
if (current == null || !"RUNNING".equals(current.status())) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "RUN_NOT_ACTIVE", "当前没有正在执行的任务");
|
||||
@@ -291,7 +295,7 @@ public class AgentRunService {
|
||||
control.cancel();
|
||||
}
|
||||
});
|
||||
scheduleResume(project, replacement, userId, phase);
|
||||
scheduleResume(project, replacement, userId, phase, current.id());
|
||||
return replacement;
|
||||
}
|
||||
|
||||
@@ -302,21 +306,22 @@ public class AgentRunService {
|
||||
ProjectService.ProjectView project,
|
||||
RunView run,
|
||||
UUID userId,
|
||||
String phase) {
|
||||
String phase,
|
||||
UUID previousRunId) {
|
||||
switch (phase) {
|
||||
case "MATERIAL_CHECK" -> afterCommit(
|
||||
run.id(), () -> executeMaterialRun(project, run, true));
|
||||
run.id(), () -> executeMaterialRun(project, run, true), previousRunId);
|
||||
case "PLANNING" -> {
|
||||
JsonNode materialResponse = runStore.latestMaterialResponse(project.id());
|
||||
afterCommit(run.id(), () -> executePlanningRun(
|
||||
project, run, userId, materialResponse, true));
|
||||
project, run, userId, materialResponse, true), previousRunId);
|
||||
}
|
||||
case "WRITING" -> {
|
||||
ProjectService.PlanView plan = projectService.currentPlan(project.id());
|
||||
if (plan == null || !"CONFIRMED".equals(plan.status())) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "PLAN_NOT_CONFIRMED", "无法恢复:建设规划尚未确认");
|
||||
}
|
||||
afterCommit(run.id(), () -> executeWritingRun(project, plan, run, true));
|
||||
afterCommit(run.id(), () -> executeWritingRun(project, plan, run, true), previousRunId);
|
||||
}
|
||||
default -> throw new ApiException(
|
||||
HttpStatus.CONFLICT, "RUN_PHASE_UNKNOWN", "无法识别中断前的执行阶段");
|
||||
@@ -383,8 +388,12 @@ public class AgentRunService {
|
||||
while (true) {
|
||||
try {
|
||||
return outputService.readMaterialCheck(project.id());
|
||||
} catch (ApiException | IOException exception) {
|
||||
} catch (ApiException | IOException | tools.jackson.core.JacksonException exception) {
|
||||
runStore.ensureRunning(run.id());
|
||||
if (repairRound >= MAX_OUTPUT_REPAIRS) {
|
||||
throw new ApiException(HttpStatus.UNPROCESSABLE_ENTITY, "AGENT_OUTPUT_INVALID",
|
||||
"结构化结果连续修复失败,现有成果已保留,请检查材料或模型后继续");
|
||||
}
|
||||
repairRound++;
|
||||
log.warn("Agent 未生成有效材料检验结果,继续同一线程修复,runId={},round={}",
|
||||
run.id(), repairRound, exception);
|
||||
@@ -457,8 +466,12 @@ public class AgentRunService {
|
||||
while (true) {
|
||||
try {
|
||||
return outputService.readProposedPlan(project.id());
|
||||
} catch (ApiException | IOException exception) {
|
||||
} catch (ApiException | IOException | tools.jackson.core.JacksonException exception) {
|
||||
runStore.ensureRunning(run.id());
|
||||
if (repairRound >= MAX_OUTPUT_REPAIRS) {
|
||||
throw new ApiException(HttpStatus.UNPROCESSABLE_ENTITY, "AGENT_OUTPUT_INVALID",
|
||||
"结构化结果连续修复失败,现有成果已保留,请检查材料或模型后继续");
|
||||
}
|
||||
repairRound++;
|
||||
log.warn("Agent 未生成有效建设规划,继续同一线程修复,runId={},round={}",
|
||||
run.id(), repairRound, exception);
|
||||
@@ -543,29 +556,57 @@ public class AgentRunService {
|
||||
* @param task 后台任务
|
||||
*/
|
||||
private void afterCommit(UUID runId, Runnable task) {
|
||||
RunControl control = new RunControl();
|
||||
activeRuns.put(runId, control);
|
||||
afterCommit(runId, task, null);
|
||||
}
|
||||
|
||||
private void afterCommit(UUID runId, Runnable task, UUID previousRunId) {
|
||||
// ponytail: 单进程并发上限;部署多个后端副本时改用数据库租约统一占位。
|
||||
RunControl control;
|
||||
synchronized (activeRuns) {
|
||||
RunControl previous = previousRunId == null ? null : activeRuns.get(previousRunId);
|
||||
if (previous == null && !runSlots.tryAcquire()) {
|
||||
throw new ApiException(HttpStatus.TOO_MANY_REQUESTS, "AGENT_CAPACITY_REACHED", "当前运行任务已达上限,请稍后重试");
|
||||
}
|
||||
control = new RunControl(previous);
|
||||
activeRuns.put(runId, control);
|
||||
}
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
task.run();
|
||||
} finally {
|
||||
activeRuns.remove(runId, control);
|
||||
}
|
||||
});
|
||||
try {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
// 同一项目接续复用名额,但须等旧 Agent 关闭并保存快照后再读写工作区。
|
||||
control.previousFinished.join();
|
||||
task.run();
|
||||
} finally {
|
||||
release(runId, control);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException exception) {
|
||||
release(runId, control);
|
||||
fail(runStore.require(runId), exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(int status) {
|
||||
if (status != TransactionSynchronization.STATUS_COMMITTED) {
|
||||
activeRuns.remove(runId, control);
|
||||
release(runId, control);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void release(UUID runId, RunControl control) {
|
||||
synchronized (activeRuns) {
|
||||
if (!activeRuns.remove(runId, control)) return;
|
||||
if (control.slotUsers.decrementAndGet() == 0) runSlots.release();
|
||||
}
|
||||
// 回滚或提交任务失败也不能让后继任务越过仍在释放资源的祖先任务。
|
||||
control.previousFinished.thenRun(() -> control.finished.complete(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前事务成功提交后执行短操作。
|
||||
*
|
||||
@@ -647,7 +688,7 @@ public class AgentRunService {
|
||||
int updated = runMapper.failRunning(run.id(), message);
|
||||
if (updated == 1) {
|
||||
eventService.append(run.projectId(), run.id(), "RUN_ERROR", Map.of(
|
||||
"code", "AGENT_RUN_FAILED", "message", message));
|
||||
"code", exception instanceof ApiException api ? api.code() : "AGENT_RUN_FAILED", "message", message));
|
||||
projectService.updateStatus(run.projectId(), "FAILED");
|
||||
}
|
||||
});
|
||||
@@ -708,6 +749,15 @@ public class AgentRunService {
|
||||
private static final class RunControl {
|
||||
|
||||
private final Sinks.One<Void> stopSignal = Sinks.one();
|
||||
private final AtomicInteger slotUsers;
|
||||
private final CompletableFuture<Void> previousFinished;
|
||||
private final CompletableFuture<Void> finished = new CompletableFuture<>();
|
||||
|
||||
private RunControl(RunControl previous) {
|
||||
slotUsers = previous == null ? new AtomicInteger(1) : previous.slotUsers;
|
||||
if (previous != null) slotUsers.incrementAndGet();
|
||||
previousFinished = previous == null ? CompletableFuture.completedFuture(null) : previous.finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消 Agent 流订阅,停止继续输出和后续工具调用。
|
||||
@@ -1,19 +1,19 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.project.ProjectService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectService;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.util.UUID;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.manuagent.entity.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentEventMapper;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentEventMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigMapper;
|
||||
|
||||
/**
|
||||
* 集中读写 Agent Run 持久化状态。
|
||||
@@ -172,7 +172,7 @@ public class AgentRunStore {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "ASK_TYPE_MISMATCH", "确认内容与当前阶段不一致");
|
||||
}
|
||||
return run;
|
||||
} catch (JsonProcessingException exception) {
|
||||
} catch (JacksonException exception) {
|
||||
throw new ApiException(HttpStatus.INTERNAL_SERVER_ERROR, "ASK_STATE_INVALID", "确认状态无法读取");
|
||||
}
|
||||
}
|
||||
@@ -228,7 +228,7 @@ public class AgentRunStore {
|
||||
}
|
||||
try {
|
||||
return objectMapper.readTree(value);
|
||||
} catch (JsonProcessingException exception) {
|
||||
} catch (JacksonException exception) {
|
||||
throw new ApiException(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"MATERIAL_RESPONSE_INVALID",
|
||||
@@ -0,0 +1,44 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import io.agentscope.core.agent.Agent;
|
||||
import io.agentscope.core.agent.RuntimeContext;
|
||||
import io.agentscope.core.event.AgentEvent;
|
||||
import io.agentscope.core.message.Msg;
|
||||
import io.agentscope.core.message.MsgRole;
|
||||
import io.agentscope.core.message.TextBlock;
|
||||
import io.agentscope.core.middleware.MiddlewareBase;
|
||||
import io.agentscope.core.middleware.ModelCallInput;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/** 在每次模型调用前刷新正文语言约束,并保留被 AG-UI 转换丢失的异常原因。 */
|
||||
final class AgentRuntimeMiddleware implements MiddlewareBase {
|
||||
private volatile Throwable modelFailure;
|
||||
|
||||
@Override
|
||||
public Flux<AgentEvent> onModelCall(Agent agent, RuntimeContext context, ModelCallInput input,
|
||||
Function<ModelCallInput, Flux<AgentEvent>> next) {
|
||||
var messages = new ArrayList<>(input.messages());
|
||||
messages.add(Msg.builder().role(MsgRole.USER).name("system")
|
||||
.content(TextBlock.builder().text("""
|
||||
<system-reminder>
|
||||
除非用户明确要求其他语言,本次及后续面向用户的正文、进度说明和最终回复均使用简体中文。
|
||||
英文 Skill、工具说明和历史英文消息不改变此要求;代码、命令、路径、标准原文和专有名词保留原语言。
|
||||
只说明业务进展和必要确认事项,不输出工具选择、脚本调试或内部文件操作的过程旁白。
|
||||
</system-reminder>
|
||||
""").build())
|
||||
.metadata(Map.of(Msg.METADATA_SYNTHETIC, true, Msg.METADATA_REMINDER_KIND, "response_language"))
|
||||
.build());
|
||||
return Flux.defer(() -> {
|
||||
modelFailure = null;
|
||||
return next.apply(new ModelCallInput(messages, input.tools(), input.options(), input.model()))
|
||||
.doOnError(error -> modelFailure = error);
|
||||
});
|
||||
}
|
||||
|
||||
Throwable modelFailure() {
|
||||
return modelFailure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
import java.util.UUID;
|
||||
|
||||
/** 状态随 APP_DATA_ROOT 持久化;按项目复制旧状态,保留原件且不覆盖已迁移会话。 */
|
||||
final class AgentStateFiles {
|
||||
static Path legacyRoot() {
|
||||
return Path.of(System.getProperty("agentscope.state.home",
|
||||
Path.of(System.getProperty("user.home"), ".agentscope", "state").toString()));
|
||||
}
|
||||
|
||||
static void migrateExisting(Path dataRoot) {
|
||||
Path projects = dataRoot.resolve("projects");
|
||||
if (!Files.isDirectory(projects)) return;
|
||||
try (var paths = Files.list(projects)) {
|
||||
for (Path project : paths.filter(Files::isDirectory).toList()) {
|
||||
String name = project.getFileName().toString();
|
||||
if (name.matches("[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}")) {
|
||||
prepare(dataRoot, legacyRoot(), UUID.fromString(name));
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("无法检查已有项目的 Agent 状态", exception);
|
||||
}
|
||||
}
|
||||
|
||||
static synchronized Path prepare(Path dataRoot, Path legacyRoot, UUID projectId) {
|
||||
Path root = dataRoot.toAbsolutePath().normalize().resolve("agent-state/smart-factory-agent");
|
||||
String session = "project-" + projectId;
|
||||
// AgentScope 2.0.1 将 SESSION 沙箱元数据放在独立的 Base64 URL 目录中。
|
||||
// 分别补迁,兼容此前只复制了会话的项目;保留已有沙箱状态(包括删除标记)。
|
||||
String sandboxSession = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(("sandbox/session/" + session).getBytes(StandardCharsets.UTF_8));
|
||||
for (String slot : new String[]{sandboxSession, session}) {
|
||||
Path relative = Path.of("__anon__", slot);
|
||||
copyMissing(legacyRoot.resolve("smart-factory-agent").resolve(relative), root.resolve(relative));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private static void copyMissing(Path source, Path target) {
|
||||
if (!Files.isDirectory(source) || Files.exists(target)) return;
|
||||
Path staging = null;
|
||||
try {
|
||||
Files.createDirectories(target.getParent());
|
||||
staging = Files.createTempDirectory(target.getParent(), ".migrating-");
|
||||
try (var files = Files.walk(source)) {
|
||||
for (Path file : files.toList()) {
|
||||
Path destination = staging.resolve(source.relativize(file));
|
||||
if (Files.isSymbolicLink(file)) throw new IOException("旧状态目录不能包含符号链接");
|
||||
if (Files.isDirectory(file)) Files.createDirectories(destination);
|
||||
else Files.copy(file, destination, StandardCopyOption.COPY_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
Files.move(staging, target, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("无法迁移 Agent 会话状态,原状态已保留", exception);
|
||||
} finally {
|
||||
if (staging != null && Files.exists(staging)) {
|
||||
try (var files = Files.walk(staging)) {
|
||||
for (Path file : files.sorted(Comparator.reverseOrder()).toList()) Files.delete(file);
|
||||
} catch (IOException exception) {
|
||||
org.slf4j.LoggerFactory.getLogger(AgentStateFiles.class).warn("清理状态迁移临时目录失败", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import io.agentscope.core.agent.RuntimeContext;
|
||||
import io.agentscope.core.message.Base64Source;
|
||||
import io.agentscope.core.message.ContentBlock;
|
||||
@@ -18,12 +18,15 @@ import io.agentscope.harness.agent.filesystem.model.FileDownloadResponse;
|
||||
import io.agentscope.harness.agent.filesystem.model.WriteResult;
|
||||
import io.agentscope.harness.agent.filesystem.sandbox.SandboxBackedFilesystem;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
|
||||
/**
|
||||
* 在当前 Harness 沙箱内把指定文档视图渲染为多模态图片。
|
||||
@@ -33,6 +36,8 @@ public final class DocumentViewTool {
|
||||
private static final int TOOL_TIMEOUT_SECONDS = 180;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ProjectFileService fileService;
|
||||
private final UUID projectId;
|
||||
private volatile HarnessAgent harness;
|
||||
|
||||
/**
|
||||
@@ -40,8 +45,10 @@ public final class DocumentViewTool {
|
||||
*
|
||||
* @param objectMapper JSON 映射器
|
||||
*/
|
||||
public DocumentViewTool(ObjectMapper objectMapper) {
|
||||
public DocumentViewTool(ObjectMapper objectMapper, ProjectFileService fileService, UUID projectId) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.fileService = fileService;
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +105,7 @@ public final class DocumentViewTool {
|
||||
}
|
||||
ExecuteResponse execution = sandbox.execute(
|
||||
runtimeContext,
|
||||
"python /opt/agent-runtime/document_view.py " + requestPath + " " + resultPath,
|
||||
"python /opt/sandbox/document_view.py " + requestPath + " " + resultPath,
|
||||
TOOL_TIMEOUT_SECONDS);
|
||||
FileDownloadResponse downloaded = filesystem.downloadFiles(runtimeContext, List.of(resultPath)).getFirst();
|
||||
if (!downloaded.isSuccess()) {
|
||||
@@ -114,14 +121,18 @@ public final class DocumentViewTool {
|
||||
List<Map<String, Object>> metadata = new ArrayList<>();
|
||||
for (JsonNode image : result.path("images")) {
|
||||
String path = image.path("path").asText();
|
||||
FileDownloadResponse imageFile = filesystem.downloadFiles(runtimeContext, List.of(path)).getFirst();
|
||||
if (!imageFile.isSuccess()) {
|
||||
byte[] imageBytes;
|
||||
try {
|
||||
imageBytes = readPreview(path, image.path("sizeBytes").asLong(-1));
|
||||
} catch (IOException | ApiException exception) {
|
||||
output.add(TextBlock.builder().text("预览图读取失败:"
|
||||
+ safeError(exception.getMessage(), null)).build());
|
||||
continue;
|
||||
}
|
||||
output.add(ImageBlock.builder()
|
||||
.source(Base64Source.builder()
|
||||
.mediaType(image.path("mediaType").asText("image/png"))
|
||||
.data(Base64.getEncoder().encodeToString(imageFile.content()))
|
||||
.data(Base64.getEncoder().encodeToString(imageBytes))
|
||||
.build())
|
||||
.build());
|
||||
metadata.add(objectMapper.convertValue(image, new TypeReference<LinkedHashMap<String, Object>>() { }));
|
||||
@@ -132,6 +143,20 @@ public final class DocumentViewTool {
|
||||
}
|
||||
}
|
||||
|
||||
byte[] readPreview(String path, long expectedSize) throws IOException {
|
||||
if (expectedSize <= 0 || expectedSize > 4 * 1024 * 1024) {
|
||||
throw new IOException("预览图大小无效或超过 4 MB");
|
||||
}
|
||||
// work 已绑定宿主机;复用预览路径校验,避免 SDK 的 512 KB stdout 截断 Base64。
|
||||
try (var input = fileService.preview(projectId, path).resource().getInputStream()) {
|
||||
byte[] bytes = input.readNBytes((int) expectedSize + 1);
|
||||
if (bytes.length != expectedSize) {
|
||||
throw new IOException("预览图字节数不一致,请重新渲染");
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择并限制返回给 Agent 的诊断信息。
|
||||
*
|
||||
@@ -1,7 +1,7 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import io.agentscope.core.agent.RuntimeContext;
|
||||
import io.agentscope.core.tool.Tool;
|
||||
import io.agentscope.core.tool.ToolParam;
|
||||
@@ -0,0 +1,35 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import io.agentscope.harness.agent.sandbox.Sandbox;
|
||||
import io.agentscope.harness.agent.sandbox.SandboxState;
|
||||
import io.agentscope.harness.agent.sandbox.impl.docker.DockerSandboxClient;
|
||||
import io.agentscope.harness.agent.sandbox.impl.docker.DockerSandboxState;
|
||||
import java.util.UUID;
|
||||
|
||||
/** AgentScope 2.0.1 在调用结束才保存容器 ID;进程中断后使用会话的稳定名称恢复。 */
|
||||
final class RecoverableDockerClient extends DockerSandboxClient {
|
||||
private final String image;
|
||||
|
||||
RecoverableDockerClient(String image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sandbox resume(SandboxState state) {
|
||||
if (state instanceof DockerSandboxState docker && docker.isContainerOwned()
|
||||
&& docker.getImage() != null && docker.getImage().startsWith("smart-factory-agent-runtime:")) {
|
||||
// SDK 恢复时沿用持久化镜像名;更名后保留原会话和快照,使用当前沙箱镜像。
|
||||
docker.setImage(image);
|
||||
}
|
||||
if (state instanceof DockerSandboxState docker && docker.isContainerOwned()
|
||||
&& docker.getSessionId() != null
|
||||
&& docker.getSessionId().matches("[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}")) {
|
||||
String name = "agentscope-sandbox-" + UUID.fromString(docker.getSessionId());
|
||||
if (name.equals(docker.getContainerName())) {
|
||||
// Docker 接受名称作为标识;不存在时仍由 SDK 从原快照重建,不丢弃会话。
|
||||
docker.setContainerId(name);
|
||||
}
|
||||
}
|
||||
return super.resume(state);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
|
||||
/**
|
||||
* 启动时终结因 JVM 中断而遗留的伪运行状态,并保留原业务阶段供继续执行。
|
||||
*/
|
||||
@Component
|
||||
@Order(0)
|
||||
public class RunRecoveryService implements ApplicationRunner {
|
||||
public class RunRecoveryService {
|
||||
|
||||
private final AgentRunMapper runMapper;
|
||||
|
||||
@@ -28,11 +24,9 @@ public class RunRecoveryService implements ApplicationRunner {
|
||||
/**
|
||||
* 将仍为 RUNNING 的旧 Run 标记为已中断。
|
||||
*
|
||||
* @param args 启动参数
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(ApplicationArguments args) {
|
||||
public void recoverInterruptedRuns() {
|
||||
runMapper.interruptRunningAfterRestart();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import io.agentscope.core.agent.RuntimeContext;
|
||||
import io.agentscope.core.tool.Tool;
|
||||
import io.agentscope.core.tool.ToolParam;
|
||||
import io.agentscope.harness.agent.filesystem.model.ExecuteResponse;
|
||||
import io.agentscope.harness.agent.filesystem.sandbox.AbstractSandboxFilesystem;
|
||||
import io.agentscope.harness.agent.workspace.WorkspacePathNormalizer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/** 修正 2.0.1 的编辑命令换行,并保留 shell 管道的真实退出状态。 */
|
||||
final class SandboxTools {
|
||||
private final AbstractSandboxFilesystem filesystem;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
SandboxTools(AbstractSandboxFilesystem filesystem, ObjectMapper mapper) {
|
||||
this.filesystem = filesystem;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Tool(name = "execute", description = "执行 Bash 命令,启用 errexit 和 pipefail。返回 success、exitCode、output;失败时先诊断再继续,不得使用 || true 掩盖必要步骤的错误。")
|
||||
public Map<String, Object> execute(RuntimeContext context,
|
||||
@ToolParam(name = "command", description = "Shell 命令") String command,
|
||||
@ToolParam(name = "working_directory", description = "工作区内相对目录", required = false) String directory,
|
||||
@ToolParam(name = "timeout", description = "超时秒数,默认 30", required = false) Integer timeout) {
|
||||
if (command == null || command.isBlank()) return failure("命令不能为空");
|
||||
if (directory != null && !directory.isBlank()) {
|
||||
if (!relativePath(directory)) return failure("working_directory 必须是工作区内相对路径");
|
||||
command = "cd " + quote(directory) + "\n" + command;
|
||||
}
|
||||
ExecuteResponse result = filesystem.execute(context,
|
||||
"bash -e -o pipefail -c " + quote(command), timeout != null && timeout > 0 ? timeout : 30);
|
||||
Map<String, Object> output = new LinkedHashMap<>();
|
||||
output.put("success", result.isSuccess());
|
||||
output.put("exitCode", result.exitCode());
|
||||
output.put("output", result.output() == null ? "" : result.output());
|
||||
output.put("truncated", result.truncated());
|
||||
return output;
|
||||
}
|
||||
|
||||
@Tool(name = "edit_file", description = "在 UTF-8 文件内精确替换。先读取文件;old_string 必须非空且唯一,除非 replace_all=true。修改已有文件应使用此工具,write_file 仅创建新文件。")
|
||||
public Map<String, Object> editFile(RuntimeContext context,
|
||||
@ToolParam(name = "path", description = "工作区内文件路径") String path,
|
||||
@ToolParam(name = "old_string", description = "要替换的原文") String oldString,
|
||||
@ToolParam(name = "new_string", description = "替换后的文本") String newString,
|
||||
@ToolParam(name = "replace_all", description = "替换全部匹配,默认 false", required = false) Boolean replaceAll) {
|
||||
path = WorkspacePathNormalizer.of("/workspace").normalize(path);
|
||||
if (!relativePath(path)) return failure("path 必须是工作区内文件路径");
|
||||
if (oldString == null || oldString.isEmpty() || newString == null) return failure("old_string 不能为空,new_string 不能为 null");
|
||||
String payload = Base64.getEncoder().encodeToString(mapper.writeValueAsString(Map.of(
|
||||
"path", path, "old", oldString, "new", newString, "all", Boolean.TRUE.equals(replaceAll)))
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
// 数据单独编码,避免文本中的引号、换行或 shell 字符改变脚本。
|
||||
String command = """
|
||||
python3 - <<'PY'
|
||||
import base64, fcntl, json, os
|
||||
p = json.loads(base64.b64decode('%s'))
|
||||
try:
|
||||
root = os.path.realpath('.')
|
||||
path = os.path.realpath(p['path'])
|
||||
if os.path.commonpath([root, path]) != root:
|
||||
raise ValueError('文件必须位于工作区内')
|
||||
with open(path, 'r+', encoding='utf-8', newline='') as f:
|
||||
fcntl.flock(f, fcntl.LOCK_EX)
|
||||
text = f.read()
|
||||
count = text.count(p['old'])
|
||||
if count == 0:
|
||||
raise ValueError('未找到原文,请重新读取文件后编辑')
|
||||
if count > 1 and not p['all']:
|
||||
raise ValueError('原文出现多次,请扩大匹配范围或使用 replace_all')
|
||||
updated = text.replace(p['old'], p['new'], -1 if p['all'] else 1)
|
||||
f.seek(0)
|
||||
f.write(updated)
|
||||
f.truncate()
|
||||
print(json.dumps({'success': True, 'path': p['path'], 'replacements': count if p['all'] else 1}, ensure_ascii=False))
|
||||
except (OSError, UnicodeError, ValueError) as e:
|
||||
print(json.dumps({'success': False, 'error': str(e)}, ensure_ascii=False))
|
||||
PY
|
||||
""".formatted(payload);
|
||||
ExecuteResponse result = filesystem.execute(context, command, 30);
|
||||
if (!result.isSuccess() || result.truncated()) return failure("编辑命令失败:" + result.output());
|
||||
try {
|
||||
return mapper.readValue(result.output(), new tools.jackson.core.type.TypeReference<Map<String, Object>>() { });
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IllegalStateException("无法解析编辑工具结果", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean relativePath(String path) {
|
||||
if (path == null || path.isBlank() || path.startsWith("~")) return false;
|
||||
try {
|
||||
Path value = Path.of(path);
|
||||
return !value.isAbsolute() && !value.normalize().startsWith("..");
|
||||
} catch (java.nio.file.InvalidPathException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> failure(String message) {
|
||||
return Map.of("success", false, "error", message);
|
||||
}
|
||||
|
||||
static String quote(String value) {
|
||||
return "'" + value.replace("'", "'\\''") + "'";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.entity;
|
||||
package tech.easyflow.manuagent.agent.skill;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
@@ -6,7 +6,7 @@ import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import tech.easyflow.manuagent.typehandler.UuidTypeHandler;
|
||||
import tech.easyflow.manuagent.common.typehandler.UuidTypeHandler;
|
||||
|
||||
/**
|
||||
* 映射应用自管的 {@code app.skill_config} 表。
|
||||
@@ -1,13 +1,14 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.skill;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import tech.easyflow.manuagent.entity.SkillConfigEntity;
|
||||
import tech.easyflow.manuagent.agent.skill.SkillConfigEntity;
|
||||
|
||||
/**
|
||||
* 提供应用 Skill 配置 CRUD 及对 AgentScope Skill 元数据的只读联查。
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface SkillConfigMapper extends BaseMapper<SkillConfigEntity> {
|
||||
|
||||
/** 列出全部 Skill 联合视图。 */
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.skill;
|
||||
package tech.easyflow.manuagent.agent.skill;
|
||||
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import io.agentscope.core.skill.AgentSkill;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.config;
|
||||
package tech.easyflow.manuagent.agent.skill;
|
||||
|
||||
import io.agentscope.core.skill.repository.postgresql.PostgresSkillRepository;
|
||||
import javax.sql.DataSource;
|
||||
@@ -1,19 +1,17 @@
|
||||
package tech.easyflow.manuagent.skill;
|
||||
package tech.easyflow.manuagent.agent.skill;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.entity.SkillConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.SkillConfigMapper;
|
||||
import tech.easyflow.manuagent.mapper.SkillViewRow;
|
||||
import tech.easyflow.manuagent.agent.config.AgentProperties;
|
||||
import tech.easyflow.manuagent.agent.skill.SkillConfigEntity;
|
||||
import tech.easyflow.manuagent.agent.skill.SkillConfigMapper;
|
||||
import tech.easyflow.manuagent.agent.skill.SkillViewRow;
|
||||
import io.agentscope.core.skill.AgentSkill;
|
||||
import io.agentscope.core.skill.repository.postgresql.PostgresSkillRepository;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.Principal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -38,8 +36,7 @@ public class SkillService {
|
||||
private final SkillConfigMapper skillMapper;
|
||||
private final PostgresSkillRepository repository;
|
||||
private final SkillPackageReader packageReader;
|
||||
private final UserService userService;
|
||||
private final AppProperties properties;
|
||||
private final AgentProperties properties;
|
||||
|
||||
/**
|
||||
* 创建 Skill 服务。
|
||||
@@ -47,19 +44,16 @@ public class SkillService {
|
||||
* @param skillMapper 应用 Skill 配置 Mapper
|
||||
* @param repository AgentScope PostgreSQL 仓库
|
||||
* @param packageReader Skill 包读取器
|
||||
* @param userService 用户服务
|
||||
* @param properties 应用配置
|
||||
*/
|
||||
public SkillService(
|
||||
SkillConfigMapper skillMapper,
|
||||
PostgresSkillRepository repository,
|
||||
SkillPackageReader packageReader,
|
||||
UserService userService,
|
||||
AppProperties properties) {
|
||||
AgentProperties properties) {
|
||||
this.skillMapper = skillMapper;
|
||||
this.repository = repository;
|
||||
this.packageReader = packageReader;
|
||||
this.userService = userService;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@@ -147,11 +141,11 @@ public class SkillService {
|
||||
* 导入管理员上传的标准 Skill ZIP。
|
||||
*
|
||||
* @param file ZIP 文件
|
||||
* @param principal 当前用户
|
||||
* @param userId 当前操作者 ID
|
||||
* @return 导入后的 Skill
|
||||
*/
|
||||
@Transactional
|
||||
public SkillView importZip(MultipartFile file, Principal principal) {
|
||||
public SkillView importZip(MultipartFile file, UUID userId) {
|
||||
if (file.isEmpty()) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "SKILL_ZIP_EMPTY", "Skill 压缩包为空");
|
||||
}
|
||||
@@ -168,7 +162,7 @@ public class SkillService {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "SKILL_NAME_CONFLICT", "同名 Skill 已存在");
|
||||
}
|
||||
repository.save(List.of(skillPackage.skill()), false);
|
||||
UUID userId = userService.requireUserId(principal.getName());
|
||||
|
||||
SkillConfigEntity config = new SkillConfigEntity();
|
||||
config.setSkillName(name);
|
||||
config.setVersion(skillPackage.version());
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.mapper;
|
||||
package tech.easyflow.manuagent.agent.skill;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.AgentEventMapper">
|
||||
<mapper namespace="tech.easyflow.manuagent.agent.runtime.AgentEventMapper">
|
||||
|
||||
<!-- 显式结果映射避免 payload 列与实体 payloadJson 属性名称不同而丢失事件负载。 -->
|
||||
<resultMap id="agentEventResultMap" type="tech.easyflow.manuagent.entity.AgentEventEntity">
|
||||
<resultMap id="agentEventResultMap" type="tech.easyflow.manuagent.agent.runtime.AgentEventEntity">
|
||||
<id property="id" column="id"/>
|
||||
<result property="projectId" column="project_id"
|
||||
typeHandler="tech.easyflow.manuagent.typehandler.UuidTypeHandler"/>
|
||||
typeHandler="tech.easyflow.manuagent.common.typehandler.UuidTypeHandler"/>
|
||||
<result property="runId" column="run_id"
|
||||
typeHandler="tech.easyflow.manuagent.typehandler.UuidTypeHandler"/>
|
||||
typeHandler="tech.easyflow.manuagent.common.typehandler.UuidTypeHandler"/>
|
||||
<result property="eventType" column="event_type"/>
|
||||
<result property="eventId" column="event_id"/>
|
||||
<result property="payloadJson" column="payload"
|
||||
typeHandler="tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler"/>
|
||||
typeHandler="tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler"/>
|
||||
<result property="createdAt" column="created_at"/>
|
||||
</resultMap>
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
<select id="insertReturning" resultMap="agentEventResultMap" affectData="true" flushCache="true">
|
||||
INSERT INTO app.agent_event(project_id, run_id, event_type, event_id, payload)
|
||||
VALUES (
|
||||
#{event.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{event.runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{event.projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
#{event.runId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
#{event.eventType},
|
||||
#{event.eventId},
|
||||
#{event.payloadJson, jdbcType=OTHER,
|
||||
typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler})
|
||||
typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler})
|
||||
<!-- 与迁移前 JDBC 返回字段一致;event_id 已完成持久化,但无需再次回传给业务层。 -->
|
||||
RETURNING id, project_id, run_id, event_type, payload, created_at
|
||||
</select>
|
||||
@@ -39,7 +39,7 @@
|
||||
<select id="selectLatestStartedPhase" resultType="string">
|
||||
SELECT payload ->> 'phase'
|
||||
FROM app.agent_event
|
||||
WHERE run_id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE run_id = #{runId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND event_type = 'RUN_STARTED'
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
@@ -48,7 +48,7 @@
|
||||
<select id="selectLatestMaterialResponseJson" resultType="string">
|
||||
SELECT payload::text
|
||||
FROM app.agent_event
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND event_type = 'ASK_RESPONDED'
|
||||
AND jsonb_typeof(payload -> 'decisions') = 'array'
|
||||
ORDER BY id DESC
|
||||
@@ -2,13 +2,13 @@
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.AgentRunMapper">
|
||||
<mapper namespace="tech.easyflow.manuagent.agent.runtime.AgentRunMapper">
|
||||
|
||||
<!-- 以下更新均将“当前状态”写进 WHERE,更新行数就是状态机竞争结果。 -->
|
||||
<update id="completeWaiting">
|
||||
UPDATE app.agent_run
|
||||
SET status = 'COMPLETED', pending_interrupt = NULL, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND status = 'WAITING_INPUT'
|
||||
</update>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
SET status = 'INTERRUPTED', pending_interrupt = NULL,
|
||||
error_code = 'USER_STOPPED', error_message = '用户已停止运行',
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
</update>
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
UPDATE app.agent_run
|
||||
SET status = 'WAITING_INPUT',
|
||||
pending_interrupt = #{interruptJson, jdbcType=OTHER,
|
||||
typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler},
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
</update>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
UPDATE app.agent_run
|
||||
SET status = 'COMPLETED', pending_interrupt = NULL,
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
</update>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
SET status = 'FAILED', pending_interrupt = NULL,
|
||||
error_code = 'AGENT_RUN_FAILED', error_message = #{message},
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
</update>
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ArtifactMapper">
|
||||
<mapper namespace="tech.easyflow.manuagent.agent.artifact.ArtifactMapper">
|
||||
|
||||
<!--
|
||||
PostgreSQL 的 INSERT ... RETURNING 属于会修改数据的查询语句。
|
||||
affectData 与 flushCache 确保 MyBatis 按 DML 事务语义处理并清理一级缓存。
|
||||
-->
|
||||
<select id="upsert"
|
||||
resultType="tech.easyflow.manuagent.entity.ArtifactEntity"
|
||||
resultType="tech.easyflow.manuagent.agent.artifact.ArtifactEntity"
|
||||
affectData="true"
|
||||
flushCache="true">
|
||||
INSERT INTO app.artifact(
|
||||
id, project_id, run_id, kind, name, relative_path, mime_type,
|
||||
size_bytes, sha256, metadata_json)
|
||||
VALUES (
|
||||
#{artifact.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{artifact.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{artifact.runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{artifact.id, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
#{artifact.projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
#{artifact.runId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
#{artifact.kind}, #{artifact.name}, #{artifact.relativePath}, #{artifact.mimeType},
|
||||
#{artifact.sizeBytes}, #{artifact.sha256},
|
||||
#{artifact.metadataJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler})
|
||||
#{artifact.metadataJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler})
|
||||
ON CONFLICT (project_id, relative_path) DO UPDATE SET
|
||||
run_id = EXCLUDED.run_id,
|
||||
kind = EXCLUDED.kind,
|
||||
@@ -1,15 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ModelAssignmentMapper">
|
||||
<mapper namespace="tech.easyflow.manuagent.agent.model.ModelAssignmentMapper">
|
||||
|
||||
<!-- 角色为主键,单语句 upsert 避免并发设置默认模型时出现先查后写竞态。 -->
|
||||
<insert id="upsert">
|
||||
INSERT INTO app.model_assignment(role, model_config_id, assigned_by)
|
||||
VALUES (
|
||||
#{assignment.role},
|
||||
#{assignment.modelConfigId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{assignment.assignedBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler})
|
||||
#{assignment.modelConfigId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
#{assignment.assignedBy, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler})
|
||||
ON CONFLICT (role) DO UPDATE SET
|
||||
model_config_id = EXCLUDED.model_config_id,
|
||||
assigned_by = EXCLUDED.assigned_by,
|
||||
@@ -19,6 +19,6 @@
|
||||
<delete id="deleteByModelConfigId">
|
||||
DELETE FROM app.model_assignment
|
||||
WHERE model_config_id = #{modelConfigId,
|
||||
typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ModelConfigMapper">
|
||||
<mapper namespace="tech.easyflow.manuagent.agent.model.ModelConfigMapper">
|
||||
|
||||
<!--
|
||||
JSONB 参数必须显式使用 JsonbStringTypeHandler。这样即使 Lambda Wrapper 在 Spring 初始化前
|
||||
@@ -13,7 +13,7 @@
|
||||
api_key_ciphertext, api_key_hint, key_version,
|
||||
config_json, capabilities_json, enabled, is_default, created_by)
|
||||
VALUES (
|
||||
#{model.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{model.id, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
#{model.name},
|
||||
#{model.provider},
|
||||
#{model.baseUrl},
|
||||
@@ -21,11 +21,11 @@
|
||||
#{model.apiKeyCiphertext},
|
||||
#{model.apiKeyHint},
|
||||
#{model.keyVersion},
|
||||
#{model.configJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
#{model.capabilitiesJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
#{model.configJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler},
|
||||
#{model.capabilitiesJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler},
|
||||
COALESCE(#{model.enabled}, TRUE),
|
||||
COALESCE(#{model.defaultModel}, FALSE),
|
||||
#{model.createdBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler})
|
||||
#{model.createdBy, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler})
|
||||
</insert>
|
||||
|
||||
<!--
|
||||
@@ -36,15 +36,15 @@
|
||||
SET name = #{model.name},
|
||||
base_url = #{model.baseUrl},
|
||||
model_id = #{model.modelId},
|
||||
config_json = #{model.configJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
capabilities_json = #{model.capabilitiesJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
config_json = #{model.configJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler},
|
||||
capabilities_json = #{model.capabilitiesJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler},
|
||||
<if test="model.apiKeyCiphertext != null">
|
||||
api_key_ciphertext = #{model.apiKeyCiphertext},
|
||||
api_key_hint = #{model.apiKeyHint},
|
||||
key_version = #{model.keyVersion},
|
||||
</if>
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{model.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{model.id, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</update>
|
||||
|
||||
<!-- 以下两条语句保持迁移前的执行顺序和条件,不额外引入模型启用状态判断。 -->
|
||||
@@ -58,18 +58,18 @@
|
||||
UPDATE app.model_config
|
||||
SET is_default = TRUE,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{id, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</update>
|
||||
|
||||
<update id="setEnabled">
|
||||
UPDATE app.model_config
|
||||
SET enabled = #{enabled},
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{id, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</update>
|
||||
|
||||
<delete id="deleteModel">
|
||||
DELETE FROM app.model_config
|
||||
WHERE id = #{id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{id, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -1,13 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ProjectMapper">
|
||||
<mapper namespace="tech.easyflow.manuagent.agent.project.ProjectMapper">
|
||||
|
||||
<!-- 项目删除前必须先阻止仍在运行的任务。 -->
|
||||
<select id="hasRunningRun" resultType="boolean">
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM app.agent_run
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
)
|
||||
</select>
|
||||
@@ -15,28 +15,28 @@
|
||||
<!-- 以下删除顺序与外键依赖顺序一致,并由 ProjectService 的 Spring 事务统一提交或回滚。 -->
|
||||
<delete id="deleteEvents">
|
||||
DELETE FROM app.agent_event
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
<delete id="deleteArtifacts">
|
||||
DELETE FROM app.artifact
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
<delete id="deletePlans">
|
||||
DELETE FROM app.project_plan
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
<delete id="deleteFiles">
|
||||
DELETE FROM app.project_file
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
<delete id="deleteRuns">
|
||||
DELETE FROM app.agent_run
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
|
||||
<update id="updateStatus">
|
||||
UPDATE app.project
|
||||
SET status = #{status}, version = version + 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -1,49 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ProjectPlanMapper">
|
||||
<mapper namespace="tech.easyflow.manuagent.agent.project.ProjectPlanMapper">
|
||||
|
||||
<!--
|
||||
版本号计算与写入保持在同一条 PostgreSQL 语句内;唯一约束继续作为并发冲突的最终保护。
|
||||
-->
|
||||
<select id="insertNextDraft"
|
||||
resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity"
|
||||
resultType="tech.easyflow.manuagent.agent.project.ProjectPlanEntity"
|
||||
affectData="true"
|
||||
flushCache="true">
|
||||
INSERT INTO app.project_plan(id, project_id, plan_version, status, plan_json, created_by)
|
||||
SELECT
|
||||
#{plan.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{plan.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{plan.id, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
#{plan.projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
COALESCE(MAX(plan_version), 0) + 1,
|
||||
'DRAFT',
|
||||
#{plan.planJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
#{plan.createdBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
#{plan.planJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler},
|
||||
#{plan.createdBy, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
FROM app.project_plan
|
||||
WHERE project_id = #{plan.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{plan.projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
RETURNING id, project_id, plan_version, status, plan_json, confirmed_at, created_at
|
||||
</select>
|
||||
|
||||
<select id="selectCurrent" resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity">
|
||||
<select id="selectCurrent" resultType="tech.easyflow.manuagent.agent.project.ProjectPlanEntity">
|
||||
SELECT id, project_id, plan_version, status, plan_json, confirmed_at, created_at
|
||||
FROM app.project_plan
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
ORDER BY CASE status WHEN 'CONFIRMED' THEN 0 ELSE 1 END, plan_version DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 条件更新和 RETURNING 在同一语句中完成,避免确认状态检查与写入之间出现竞态。 -->
|
||||
<select id="confirmDraft"
|
||||
resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity"
|
||||
resultType="tech.easyflow.manuagent.agent.project.ProjectPlanEntity"
|
||||
affectData="true"
|
||||
flushCache="true">
|
||||
UPDATE app.project_plan
|
||||
SET status = 'CONFIRMED',
|
||||
plan_json = #{planJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
confirmed_by = #{userId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
plan_json = #{planJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.common.typehandler.JsonbStringTypeHandler},
|
||||
confirmed_by = #{userId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler},
|
||||
confirmed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{planId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
WHERE id = #{planId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND project_id = #{projectId, typeHandler=tech.easyflow.manuagent.common.typehandler.UuidTypeHandler}
|
||||
AND status = 'DRAFT'
|
||||
RETURNING id, project_id, plan_version, status, plan_json, confirmed_at, created_at
|
||||
</select>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.SkillConfigMapper">
|
||||
<mapper namespace="tech.easyflow.manuagent.agent.skill.SkillConfigMapper">
|
||||
|
||||
<!-- AgentScope 表严格只读;应用只在 app.skill_config 保存启停、来源和校验状态。 -->
|
||||
<sql id="skillViewColumns">
|
||||
@@ -9,14 +9,14 @@
|
||||
c.validation_status, c.validation_message, c.updated_at
|
||||
</sql>
|
||||
|
||||
<select id="selectViews" resultType="tech.easyflow.manuagent.mapper.SkillViewRow">
|
||||
<select id="selectViews" resultType="tech.easyflow.manuagent.agent.skill.SkillViewRow">
|
||||
SELECT <include refid="skillViewColumns"/>
|
||||
FROM agentscope.agentscope_skills s
|
||||
JOIN app.skill_config c ON c.skill_name = s.name
|
||||
ORDER BY c.source_type, s.name
|
||||
</select>
|
||||
|
||||
<select id="selectView" resultType="tech.easyflow.manuagent.mapper.SkillViewRow">
|
||||
<select id="selectView" resultType="tech.easyflow.manuagent.agent.skill.SkillViewRow">
|
||||
SELECT <include refid="skillViewColumns"/>
|
||||
FROM agentscope.agentscope_skills s
|
||||
JOIN app.skill_config c ON c.skill_name = s.name
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
# 自主执行
|
||||
|
||||
生成 DOCX 前,读取并运行 `/opt/sandbox/docx-example.cjs`,将验证文件写入 `work/tmp/`;复用其中与当前 docx 版本匹配的表格、段落和原生批注 API。`comments` 必须是带 `children` 的对象,表格行的 `children` 必须是一维 TableCell 数组。需要核对其他 API 时读取 `/opt/sandbox/node_modules/docx/dist/index.d.ts`,不要猜测构造参数,也不要通过 `require('docx/package.json')` 获取版本。先用 `node --check` 检查脚本语法,再生成并验证 DOCX。必要步骤失败时立即修复,不能用管道尾部成功或 `|| true` 掩盖失败。
|
||||
|
||||
1. 先递归查看 `inputs/`,保留并利用上传目录、原文件名和材料分类之间的语义关系;同名文件必须结合完整相对路径判断来源。
|
||||
2. 主动选择与文件类型相符的 PDF、PPTX、XLS/XLSX、DOCX 等文档 Skill,先读取 Skill 的完整 `SKILL.md`,再按其方法做结构化读取。不得只凭文件名推断正文。
|
||||
3. `document_view` 是按需视觉补充工具,不是默认步骤。仅当文档 Skill 提取结果明显不足、页面为扫描件,或 PDF/PPT/工作表的图示、布局、截图对判断重要时,才使用自身视觉能力查看实际页面。由你决定页码、幻灯片、工作表与范围;建议每次最多渲染 5 张,可分批调用。大型工作表应主动拆分 range 查看,无需模拟滚动。
|
||||
@@ -1,6 +1,6 @@
|
||||
请将当前会话压缩为可继续执行的结构化工作记忆。必须保留:
|
||||
请使用简体中文将当前会话压缩为可继续执行的结构化工作记忆。必须保留:
|
||||
|
||||
1. 企业名称、申报等级,以及 C(已确认事实)、E(外部依据)、R(规划建议)、P(待实施)、U(待确认)边界。
|
||||
1. 企业名称、申报等级,以及 C(企业材料或用户确认的企业事实)、E(企业公开资料,保留来源并标记待企业确认)、R(政策、标准、行业方法和同行案例,仅支撑规划)、P(基于 R 的未来规划、建议目标和测算假设)、U(缺失、冲突或无法核实的企业现状)边界。
|
||||
2. 已确认并冻结的建设规划,包括建设方向、场景、KPI、投资区间、建设周期和版本。
|
||||
3. 材料之间的冲突、全部未解决 U 项、Word 批注要求和事实约束。
|
||||
4. 已调用 Skill、关键工具结果、已生成或已修改的工作区文件及其校验状态。
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.artifact;
|
||||
package tech.easyflow.manuagent.agent.artifact;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -13,9 +13,9 @@ import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ArtifactEntity;
|
||||
import tech.easyflow.manuagent.mapper.ArtifactMapper;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.agent.artifact.ArtifactEntity;
|
||||
import tech.easyflow.manuagent.agent.artifact.ArtifactMapper;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
|
||||
/**
|
||||
* 验证产物查询在 MyBatis-Flex 迁移后保持原 JDBC SQL 的最小字段范围。
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.artifact;
|
||||
package tech.easyflow.manuagent.agent.artifact;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.model;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -7,7 +7,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
@@ -16,12 +16,11 @@ import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigMapper;
|
||||
|
||||
/**
|
||||
* 验证模型草稿连接测试的密钥选择和外部请求边界。
|
||||
@@ -39,9 +38,8 @@ class ModelServiceConnectionTest {
|
||||
context.registerBean(ModelConfigMapper.class, () -> mock(ModelConfigMapper.class));
|
||||
context.registerBean(ModelAssignmentMapper.class, () -> mock(ModelAssignmentMapper.class));
|
||||
context.registerBean(AgentRunMapper.class, () -> mock(AgentRunMapper.class));
|
||||
context.registerBean(UserService.class, () -> mock(UserService.class));
|
||||
context.registerBean(KeyCipher.class, () -> mock(KeyCipher.class));
|
||||
context.registerBean(ObjectMapper.class, () -> new ObjectMapper());
|
||||
context.registerBean(ObjectMapper.class, () -> tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
context.register(ModelService.class);
|
||||
|
||||
context.refresh();
|
||||
@@ -164,9 +162,8 @@ class ModelServiceConnectionTest {
|
||||
modelMapper,
|
||||
mock(ModelAssignmentMapper.class),
|
||||
mock(AgentRunMapper.class),
|
||||
mock(UserService.class),
|
||||
keyCipher,
|
||||
new ObjectMapper(),
|
||||
keyCipher,
|
||||
tools.jackson.databind.json.JsonMapper.builder().build(),
|
||||
httpClient);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.model;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -6,7 +6,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
@@ -14,13 +14,11 @@ import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.AppUserMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.config.AgentProperties;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.agent.model.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
|
||||
/**
|
||||
* 验证模型管理接口使用最小字段投影,不把加密 API Key 读入普通请求内存。
|
||||
@@ -47,9 +45,8 @@ class ModelServiceQueryTest {
|
||||
modelMapper,
|
||||
mock(ModelAssignmentMapper.class),
|
||||
mock(AgentRunMapper.class),
|
||||
mock(UserService.class),
|
||||
mock(KeyCipher.class),
|
||||
new ObjectMapper());
|
||||
tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
|
||||
List<ModelService.ModelView> models = service.list();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package tech.easyflow.manuagent.model;
|
||||
package tech.easyflow.manuagent.agent.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import java.util.Map;
|
||||
@@ -13,11 +13,10 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigMapper;
|
||||
|
||||
/**
|
||||
* 验证模型配置在进入数据库和外部 HTTP 客户端之前具有明确、可审计的输入边界。
|
||||
@@ -67,7 +66,7 @@ class ModelServiceValidationTest {
|
||||
Map.of(),
|
||||
Map.of("contextWindow", 8_192));
|
||||
|
||||
assertThatThrownBy(() -> service.save(null, input, () -> "admin"))
|
||||
assertThatThrownBy(() -> service.save(null, input, UUID.randomUUID()))
|
||||
.as("地址应被拒绝:%s", baseUrl)
|
||||
.isInstanceOfSatisfying(ApiException.class, exception ->
|
||||
assertThat(exception.code()).isEqualTo("MODEL_BASE_URL_INVALID"));
|
||||
@@ -80,14 +79,12 @@ class ModelServiceValidationTest {
|
||||
* @return 配置了当前用户的模型服务
|
||||
*/
|
||||
private ModelService service() {
|
||||
UserService userService = mock(UserService.class);
|
||||
when(userService.requireUserId("admin")).thenReturn(UUID.randomUUID());
|
||||
|
||||
return new ModelService(
|
||||
mock(ModelConfigMapper.class),
|
||||
mock(ModelAssignmentMapper.class),
|
||||
mock(AgentRunMapper.class),
|
||||
userService,
|
||||
mock(KeyCipher.class),
|
||||
new ObjectMapper());
|
||||
tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.project;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -8,11 +8,10 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.entity.ProjectFileEntity;
|
||||
import tech.easyflow.manuagent.mapper.ProjectFileMapper;
|
||||
import tech.easyflow.manuagent.agent.config.AgentProperties;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
@@ -37,12 +36,11 @@ class ProjectFileServiceTest {
|
||||
*/
|
||||
@Test
|
||||
void shouldDeleteProjectWorkspace() throws Exception {
|
||||
AppProperties properties = new AppProperties(
|
||||
AgentProperties properties = new AgentProperties(
|
||||
temporaryDirectory, Path.of("dashscope"),
|
||||
"test-master", "admin", "admin",
|
||||
"runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
"test-master", "runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
ProjectFileService service = new ProjectFileService(
|
||||
mock(ProjectFileMapper.class), mock(UserService.class), mock(ProjectService.class), properties);
|
||||
mock(ProjectFileMapper.class), mock(ProjectService.class), properties);
|
||||
UUID projectId = UUID.randomUUID();
|
||||
Path file = service.projectRoot(projectId).resolve("inputs/company.txt");
|
||||
Files.createDirectories(file.getParent());
|
||||
@@ -92,7 +90,7 @@ class ProjectFileServiceTest {
|
||||
file.setSizeBytes(1L);
|
||||
when(mapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of(file));
|
||||
ProjectFileService service = new ProjectFileService(
|
||||
mapper, mock(UserService.class), projectService, properties());
|
||||
mapper, projectService, properties());
|
||||
|
||||
service.list(file.getProjectId());
|
||||
|
||||
@@ -111,7 +109,7 @@ class ProjectFileServiceTest {
|
||||
*/
|
||||
private ProjectFileService service() {
|
||||
return new ProjectFileService(
|
||||
mock(ProjectFileMapper.class), mock(UserService.class), mock(ProjectService.class), properties());
|
||||
mock(ProjectFileMapper.class), mock(ProjectService.class), properties());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,10 +117,9 @@ class ProjectFileServiceTest {
|
||||
*
|
||||
* @return 指向临时数据目录的配置
|
||||
*/
|
||||
private AppProperties properties() {
|
||||
return new AppProperties(
|
||||
private AgentProperties properties() {
|
||||
return new AgentProperties(
|
||||
temporaryDirectory, Path.of("dashscope"),
|
||||
"test-master", "admin", "admin",
|
||||
"runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
"test-master", "runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.project;
|
||||
package tech.easyflow.manuagent.agent.project;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -7,18 +7,17 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.entity.ProjectEntity;
|
||||
import tech.easyflow.manuagent.entity.ProjectPlanEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectEntity;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectPlanEntity;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.mapper.ProjectMapper;
|
||||
import tech.easyflow.manuagent.mapper.ProjectPlanMapper;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectMapper;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectPlanMapper;
|
||||
|
||||
/**
|
||||
* 验证项目查询在 MyBatis-Flex 迁移后保持原 JDBC SQL 的字段范围。
|
||||
@@ -72,7 +71,7 @@ class ProjectServiceQueryTest {
|
||||
plan.setPlanJson("{invalid-json");
|
||||
when(planMapper.selectCurrent(any(UUID.class))).thenReturn(plan);
|
||||
ProjectService service = new ProjectService(
|
||||
mock(ProjectMapper.class), planMapper, mock(UserService.class), new ObjectMapper());
|
||||
mock(ProjectMapper.class), planMapper, tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
|
||||
assertThatThrownBy(() -> service.currentPlan(UUID.randomUUID()))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
@@ -101,8 +100,7 @@ class ProjectServiceQueryTest {
|
||||
return new ProjectService(
|
||||
mapper,
|
||||
mock(ProjectPlanMapper.class),
|
||||
mock(UserService.class),
|
||||
new ObjectMapper());
|
||||
tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -6,14 +6,14 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.manuagent.entity.AgentEventEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentEventMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentEventEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentEventMapper;
|
||||
|
||||
/**
|
||||
* 验证 Agent 事件回放查询在 ORM 迁移后保持原 JDBC 字段和游标语义。
|
||||
@@ -31,7 +31,7 @@ class AgentEventServiceQueryTest {
|
||||
event.setProjectId(UUID.randomUUID());
|
||||
event.setPayloadJson("{}");
|
||||
when(mapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of(event));
|
||||
AgentEventService service = new AgentEventService(mapper, new ObjectMapper());
|
||||
AgentEventService service = new AgentEventService(mapper, tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
|
||||
service.listAfter(event.getProjectId(), 0L, 100);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import io.agentscope.core.agui.adapter.AguiAgentAdapter;
|
||||
import io.agentscope.core.agui.event.AguiEvent;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectService;
|
||||
import tech.easyflow.manuagent.agent.skill.SkillService;
|
||||
|
||||
/**
|
||||
* 验证 Agent 事件持久化的精简规则。
|
||||
*/
|
||||
class AgentExecutionServiceTest {
|
||||
|
||||
/**
|
||||
* Run 在创建时已经固化模型配置 ID;执行和模型重连都必须沿用该 ID,
|
||||
* 不能在工厂内部重新读取可能已经变化的全局默认模型。
|
||||
*/
|
||||
@Test
|
||||
void shouldCreateAgentWithModelBoundToRun() {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
UUID runId = UUID.randomUUID();
|
||||
UUID modelId = UUID.randomUUID();
|
||||
AgentFactory factory = mock(AgentFactory.class);
|
||||
AgentFactory.AgentHandle handle = mock(AgentFactory.AgentHandle.class);
|
||||
AguiAgentAdapter adapter = mock(AguiAgentAdapter.class);
|
||||
SkillService skillService = mock(SkillService.class);
|
||||
when(handle.adapter()).thenReturn(adapter);
|
||||
when(adapter.run(any())).thenReturn(Flux.empty());
|
||||
when(skillService.enabledNames()).thenReturn(new String[] {"document"});
|
||||
when(factory.create(eq(projectId), eq(modelId), any(String[].class))).thenReturn(handle);
|
||||
|
||||
ProjectFileService files = mock(ProjectFileService.class);
|
||||
when(files.projectRoot(projectId)).thenReturn(Path.of("/tmp/project"));
|
||||
AgentExecutionService service = new AgentExecutionService(
|
||||
tools.jackson.databind.json.JsonMapper.builder().build(),
|
||||
factory,
|
||||
mock(AgentEventService.class),
|
||||
files,
|
||||
skillService, new tech.easyflow.manuagent.agent.config.AgentProperties(
|
||||
Path.of("/tmp"), null, "test", "runtime", "bridge", java.time.Duration.ofMinutes(1)));
|
||||
ProjectService.ProjectView project = mock(ProjectService.ProjectView.class);
|
||||
when(project.id()).thenReturn(projectId);
|
||||
when(project.threadId()).thenReturn("project-" + projectId);
|
||||
AgentRunService.RunView run = new AgentRunService.RunView(
|
||||
runId, projectId, modelId, "INITIAL", "RUNNING", null, null, java.time.OffsetDateTime.now(), null);
|
||||
|
||||
service.execute(project, run, "执行测试", Mono.never(), () -> { }, () -> false);
|
||||
|
||||
verify(factory).create(eq(projectId), eq(modelId), any(String[].class));
|
||||
|
||||
when(adapter.run(any())).thenReturn(Flux.just(new AguiEvent.RunError(
|
||||
"project-" + projectId, runId.toString(), "sandbox failed: /tmp/project sk-testsecret123", "AGENT_ERROR")));
|
||||
assertThatThrownBy(() -> service.execute(project, run, "执行测试", Mono.never(), () -> { }, () -> false))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("sandbox failed")
|
||||
.hasMessageNotContaining("/tmp/project")
|
||||
.hasMessageNotContaining("sk-testsecret123");
|
||||
verify(factory, times(2)).create(eq(projectId), eq(modelId), any(String[].class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文档视觉结果保留图片路径元数据并丢弃 Base64 正文。
|
||||
*/
|
||||
@Test
|
||||
void shouldStripInlineImageDataFromPersistedToolResult() {
|
||||
String content = """
|
||||
document_view_result={"images":[{"path":"work/tmp/document-view/a/render-1.png"}]}
|
||||
{"type":"image","source":{"media_type":"image/png","data":"very-large-base64"}}
|
||||
""";
|
||||
|
||||
String result = AgentExecutionService.stripInlineImageData(content);
|
||||
|
||||
assertThat(result).contains("render-1.png");
|
||||
assertThat(result).doesNotContain("very-large-base64");
|
||||
}
|
||||
@Test
|
||||
void shouldRetryConvertedModelErrorsAndEnforceTotalDeadlineEvenWhileStreaming() {
|
||||
var factory = mock(AgentFactory.class);
|
||||
var handle = mock(AgentFactory.AgentHandle.class);
|
||||
var adapter = mock(AguiAgentAdapter.class);
|
||||
var runtime = mock(AgentRuntimeMiddleware.class);
|
||||
when(handle.adapter()).thenReturn(adapter);
|
||||
when(handle.runtime()).thenReturn(runtime);
|
||||
when(factory.create(any(), any(), any())).thenReturn(handle);
|
||||
var skills = mock(SkillService.class);
|
||||
when(skills.enabledNames()).thenReturn(new String[0]);
|
||||
var files = mock(ProjectFileService.class);
|
||||
when(files.projectRoot(any())).thenReturn(Path.of("/tmp/project"));
|
||||
var events = mock(AgentEventService.class);
|
||||
var properties = new tech.easyflow.manuagent.agent.config.AgentProperties(
|
||||
Path.of("/tmp"), null, "test", "runtime", "bridge", java.time.Duration.ofSeconds(5));
|
||||
var service = new AgentExecutionService(tools.jackson.databind.json.JsonMapper.builder().build(),
|
||||
factory, events, files, skills, properties);
|
||||
var project = mock(ProjectService.ProjectView.class);
|
||||
var projectId = UUID.randomUUID();
|
||||
when(project.id()).thenReturn(projectId);
|
||||
when(project.threadId()).thenReturn("test");
|
||||
var run = new AgentRunService.RunView(UUID.randomUUID(), projectId, UUID.randomUUID(),
|
||||
"INITIAL", "RUNNING", null, null, java.time.OffsetDateTime.now(), null);
|
||||
when(runtime.modelFailure()).thenReturn(new io.agentscope.core.model.transport.HttpTransportException("lost", 503, ""));
|
||||
when(adapter.run(any())).thenReturn(Flux.just(new AguiEvent.RunError("test", "run", "lost", "INTERNAL_ERROR")))
|
||||
.thenReturn(Flux.empty());
|
||||
service.execute(project, run, "开始", Mono.never(), () -> { }, () -> false);
|
||||
verify(factory, times(2)).create(any(), any(), any());
|
||||
verify(events).append(eq(projectId), eq(run.id()), eq("MODEL_RETRY"), any());
|
||||
|
||||
var shortService = new AgentExecutionService(tools.jackson.databind.json.JsonMapper.builder().build(),
|
||||
factory, events, files, skills, new tech.easyflow.manuagent.agent.config.AgentProperties(
|
||||
Path.of("/tmp"), null, "test", "runtime", "bridge", java.time.Duration.ofMillis(250)));
|
||||
var shortRun = new AgentRunService.RunView(UUID.randomUUID(), projectId, UUID.randomUUID(),
|
||||
"INITIAL", "RUNNING", null, null, java.time.OffsetDateTime.now(), null);
|
||||
when(adapter.run(any())).thenReturn(Flux.interval(java.time.Duration.ofMillis(20))
|
||||
.map(i -> new AguiEvent.RunStarted("test", "run")));
|
||||
assertThatThrownBy(() -> shortService.execute(project, shortRun, "开始", Mono.never(), () -> { }, () -> false))
|
||||
.isInstanceOf(tech.easyflow.manuagent.common.ApiException.class).hasMessageContaining("运行时限");
|
||||
verify(handle, times(3)).close();
|
||||
assertThatThrownBy(() -> shortService.remainingTime(shortRun)).hasMessageContaining("运行时限");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.springframework.test.util.ReflectionTestUtils.invokeMethod;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
import io.agentscope.core.model.transport.HttpTransportException;
|
||||
import io.agentscope.core.skill.AgentSkill;
|
||||
import java.nio.file.Path;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import tech.easyflow.manuagent.agent.artifact.ArtifactService;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectService;
|
||||
|
||||
/**
|
||||
* 验证 Agent Run 的模型重连边界。
|
||||
*/
|
||||
class AgentRunServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldRepairMalformedJsonInBothAgentOutputPaths() throws Exception {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
var mapper = tools.jackson.databind.json.JsonMapper.builder().build();
|
||||
tools.jackson.core.JacksonException malformed;
|
||||
try {
|
||||
mapper.readTree("{invalid");
|
||||
throw new AssertionError("Malformed JSON accepted");
|
||||
} catch (tools.jackson.core.JacksonException exception) {
|
||||
malformed = exception;
|
||||
}
|
||||
var repaired = mapper.createObjectNode().put("summary", "已修复");
|
||||
var output = mock(AgentOutputService.class);
|
||||
when(output.readMaterialCheck(projectId)).thenThrow(malformed).thenReturn(repaired);
|
||||
when(output.readProposedPlan(projectId)).thenThrow(malformed).thenReturn(repaired);
|
||||
var execution = mock(AgentExecutionService.class);
|
||||
var store = mock(AgentRunStore.class);
|
||||
var project = mock(ProjectService.ProjectView.class);
|
||||
when(project.id()).thenReturn(projectId);
|
||||
var run = new AgentRunService.RunView(UUID.randomUUID(), projectId, UUID.randomUUID(),
|
||||
"INITIAL", "RUNNING", null, null, OffsetDateTime.now(), null);
|
||||
var service = new AgentRunService(mock(AgentRunMapper.class), mapper, execution, output,
|
||||
store, mock(AgentEventService.class), mock(ProjectService.class),
|
||||
mock(ProjectFileService.class), mock(ArtifactService.class),
|
||||
mock(ExecutorService.class), mock(TransactionTemplate.class), 2);
|
||||
for (String method : java.util.List.of("readMaterialCheckWithRepair", "readProposedPlanWithRepair")) {
|
||||
Object result = org.springframework.test.util.ReflectionTestUtils.invokeMethod(service, method, project, run);
|
||||
assertThat(result).isSameAs(repaired);
|
||||
}
|
||||
verify(execution, org.mockito.Mockito.times(2)).execute(
|
||||
org.mockito.ArgumentMatchers.eq(project), org.mockito.ArgumentMatchers.eq(run),
|
||||
org.mockito.ArgumentMatchers.contains("修复"), org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||
verify(store, org.mockito.Mockito.times(2)).ensureRunning(run.id());
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行中模型配置被编辑后,用户应能选择同一模型 ID 创建新的恢复 Run,
|
||||
* 使新 Agent 客户端重新读取数据库中的最新地址、模型标识和密钥。
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {1, 2})
|
||||
void shouldRestartRunningTaskWhenSameModelConfigurationWasUpdatedAtCapacity(int limit) {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
UUID modelId = UUID.randomUUID();
|
||||
UUID currentRunId = UUID.randomUUID();
|
||||
UUID replacementRunId = UUID.randomUUID();
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
ProjectService.ProjectView project = new ProjectService.ProjectView(
|
||||
projectId, "测试企业", "测试项目", "thread-1", "ADVANCED", "MATERIAL_CHECK", 0L, now, now);
|
||||
AgentRunService.RunView current = new AgentRunService.RunView(
|
||||
currentRunId, projectId, modelId, "INITIAL", "RUNNING", null, null, now, null);
|
||||
AgentRunService.RunView replacement = new AgentRunService.RunView(
|
||||
replacementRunId, projectId, modelId, "RESUME", "RUNNING", null, null, now, null);
|
||||
|
||||
AgentRunMapper runMapper = mock(AgentRunMapper.class);
|
||||
AgentRunStore runStore = mock(AgentRunStore.class);
|
||||
ProjectService projectService = mock(ProjectService.class);
|
||||
|
||||
when(projectService.require(projectId)).thenReturn(project);
|
||||
|
||||
when(runStore.latest(projectId)).thenReturn(current);
|
||||
when(runStore.interruptedPhase(current, project)).thenReturn("MATERIAL_CHECK");
|
||||
when(runMapper.interruptRunning(currentRunId)).thenReturn(1);
|
||||
when(runStore.create(projectId, "RESUME", currentRunId, modelId)).thenReturn(replacement);
|
||||
|
||||
AgentRunService service = new AgentRunService(
|
||||
runMapper,
|
||||
tools.jackson.databind.json.JsonMapper.builder().build(),
|
||||
mock(AgentExecutionService.class),
|
||||
mock(AgentOutputService.class),
|
||||
runStore,
|
||||
mock(AgentEventService.class),
|
||||
projectService,
|
||||
mock(ProjectFileService.class),
|
||||
mock(ArtifactService.class),
|
||||
mock(ExecutorService.class),
|
||||
mock(TransactionTemplate.class), limit);
|
||||
|
||||
// 服务会注册“提交后取消旧流并启动新流”的回调;测试只验证注册前的事务内状态转换。
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
for (int i = 0; i < limit; i++) {
|
||||
invokeMethod(service, "afterCommit", i == 0 ? currentRunId : UUID.randomUUID(), (Runnable) () -> {});
|
||||
}
|
||||
assertThat(service.switchModel(projectId, modelId, UUID.randomUUID())).isEqualTo(replacement);
|
||||
verify(runMapper).interruptRunning(currentRunId);
|
||||
verify(runStore).create(projectId, "RESUME", currentRunId, modelId);
|
||||
} finally {
|
||||
TransactionSynchronizationManager.getSynchronizations().forEach(sync ->
|
||||
sync.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWaitForPreviousTaskCleanupBeforeStartingReplacement() throws Exception {
|
||||
var queue = new LinkedBlockingQueue<Runnable>();
|
||||
var executor = mock(ExecutorService.class);
|
||||
when(executor.submit(any(Runnable.class))).thenAnswer(call -> {
|
||||
queue.add(call.getArgument(0));
|
||||
return java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
});
|
||||
var service = schedulingService(executor, mock(AgentRunStore.class));
|
||||
var previous = UUID.randomUUID();
|
||||
var replacementStarted = new CountDownLatch(1);
|
||||
transaction(() -> invokeMethod(service, "afterCommit", previous, (Runnable) () -> {}), true);
|
||||
Runnable previousTask = queue.remove();
|
||||
transaction(() -> invokeMethod(service, "afterCommit", UUID.randomUUID(),
|
||||
(Runnable) replacementStarted::countDown, previous), true);
|
||||
var replacement = Thread.startVirtualThread(queue.remove());
|
||||
try {
|
||||
assertThat(replacementStarted.await(200, TimeUnit.MILLISECONDS)).isFalse();
|
||||
assertThatThrownBy(() -> transaction(() -> invokeMethod(service, "afterCommit",
|
||||
UUID.randomUUID(), (Runnable) () -> {}), true)).hasMessageContaining("已达上限");
|
||||
} finally {
|
||||
previousTask.run();
|
||||
replacement.join(5_000);
|
||||
}
|
||||
assertThat(replacement.isAlive()).isFalse();
|
||||
assertThat(replacementStarted.getCount()).isZero();
|
||||
// 接续完成后恢复一个名额。
|
||||
transaction(() -> invokeMethod(service, "afterCommit", UUID.randomUUID(), (Runnable) () -> {}), false);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void shouldRetainPredecessorCapacityWhenReplacementRollsBackOrIsRejected(boolean rejected) {
|
||||
var executor = mock(ExecutorService.class);
|
||||
var queue = new LinkedBlockingQueue<Runnable>();
|
||||
when(executor.submit(any(Runnable.class))).thenAnswer(call -> {
|
||||
queue.add(call.getArgument(0));
|
||||
return java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
});
|
||||
var store = mock(AgentRunStore.class);
|
||||
var previous = UUID.randomUUID();
|
||||
var replacement = UUID.randomUUID();
|
||||
when(store.require(replacement)).thenReturn(new AgentRunService.RunView(replacement,
|
||||
UUID.randomUUID(), UUID.randomUUID(), "RESUME", "RUNNING", null, null, OffsetDateTime.now(), null));
|
||||
var service = schedulingService(executor, store);
|
||||
transaction(() -> invokeMethod(service, "afterCommit", previous, (Runnable) () -> {}), true);
|
||||
Runnable previousTask = queue.remove();
|
||||
if (rejected) when(executor.submit(any(Runnable.class))).thenThrow(new RejectedExecutionException("test"));
|
||||
transaction(() -> invokeMethod(service, "afterCommit", replacement, (Runnable) () -> {}, previous), rejected);
|
||||
assertThatThrownBy(() -> transaction(() -> invokeMethod(service, "afterCommit",
|
||||
UUID.randomUUID(), (Runnable) () -> {}), false)).hasMessageContaining("已达上限");
|
||||
previousTask.run();
|
||||
transaction(() -> invokeMethod(service, "afterCommit", UUID.randomUUID(), (Runnable) () -> {}), false);
|
||||
}
|
||||
|
||||
private static AgentRunService schedulingService(ExecutorService executor, AgentRunStore store) {
|
||||
return new AgentRunService(mock(AgentRunMapper.class), tools.jackson.databind.json.JsonMapper.builder().build(),
|
||||
mock(AgentExecutionService.class), mock(AgentOutputService.class), store,
|
||||
mock(AgentEventService.class), mock(ProjectService.class), mock(ProjectFileService.class),
|
||||
mock(ArtifactService.class), executor, mock(TransactionTemplate.class), 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldShareCapacityWhenRestartingMaterialCheckWhileStoppedRunIsClosing() {
|
||||
UUID project = UUID.randomUUID();
|
||||
var previous = new AgentRunService.RunView(UUID.randomUUID(), project, UUID.randomUUID(),
|
||||
"INITIAL", "INTERRUPTED", null, null, OffsetDateTime.now(), OffsetDateTime.now());
|
||||
var next = new AgentRunService.RunView(UUID.randomUUID(), project, previous.modelConfigId(),
|
||||
"INITIAL", "RUNNING", null, null, OffsetDateTime.now(), null);
|
||||
var store = mock(AgentRunStore.class);
|
||||
when(store.latest(project)).thenReturn(previous);
|
||||
when(store.create(project, "INITIAL", null)).thenReturn(next);
|
||||
var service = schedulingService(mock(ExecutorService.class), store);
|
||||
transaction(() -> {
|
||||
invokeMethod(service, "afterCommit", previous.id(), (Runnable) () -> {});
|
||||
assertThat(service.startMaterialCheck(project, UUID.randomUUID())).isEqualTo(next);
|
||||
}, false);
|
||||
}
|
||||
|
||||
private static void transaction(Runnable action, boolean commit) {
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
int status = TransactionSynchronization.STATUS_ROLLED_BACK;
|
||||
try {
|
||||
action.run();
|
||||
if (commit) {
|
||||
status = TransactionSynchronization.STATUS_COMMITTED;
|
||||
TransactionSynchronizationManager.getSynchronizations().forEach(TransactionSynchronization::afterCommit);
|
||||
}
|
||||
} finally {
|
||||
for (var sync : TransactionSynchronizationManager.getSynchronizations()) sync.afterCompletion(status);
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络故障和服务端错误允许重连,参数错误保持原始失败。
|
||||
*/
|
||||
@Test
|
||||
void shouldRetryOnlyRecoverableModelFailures() {
|
||||
assertThat(AgentExecutionService.MAX_MODEL_RECONNECTS).isEqualTo(5);
|
||||
assertThat(AgentExecutionService.isRetryableModelFailure(
|
||||
new RuntimeException(new HttpTransportException("disconnected")))).isTrue();
|
||||
assertThat(AgentExecutionService.isRetryableModelFailure(
|
||||
new HttpTransportException("unavailable", 503, ""))).isTrue();
|
||||
assertThat(AgentExecutionService.isRetryableModelFailure(
|
||||
new HttpTransportException("invalid request", 400, ""))).isFalse();
|
||||
assertThat(AgentExecutionService.isRetryableModelFailure(
|
||||
new IllegalArgumentException("invalid prompt"))).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证上下文压缩只按模型窗口 90% Token 触发。
|
||||
*/
|
||||
@Test
|
||||
void shouldCompactAtNinetyPercentTokensOnly() {
|
||||
var config = AgentFactory.compactionFor(100_000, "summary");
|
||||
|
||||
assertThat(config.getTriggerTokens()).isEqualTo(90_000);
|
||||
assertThat(config.getTriggerMessages()).isZero();
|
||||
assertThat(config.getSummaryPrompt()).isEqualTo("summary");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Agent 调用 Skill 时使用无来源后缀的名称,并完整保留仓库信息。
|
||||
*/
|
||||
@Test
|
||||
void shouldExposeCanonicalSkillIdWithoutLosingSkillInformation() {
|
||||
AgentSkill source = new AgentSkill(
|
||||
Map.of("name", "pdf", "description", "读取 PDF", "version", "1.0"),
|
||||
"使用说明",
|
||||
Map.of("references/guide.md", "参考内容"),
|
||||
"imported",
|
||||
Path.of("/skills/pdf"));
|
||||
|
||||
AgentSkill canonical = AgentFactory.canonicalSkill(source);
|
||||
|
||||
assertThat(canonical.getSkillId()).isEqualTo("pdf");
|
||||
assertThat(canonical.getMetadata()).isEqualTo(source.getMetadata());
|
||||
assertThat(canonical.getSkillContent()).isEqualTo(source.getSkillContent());
|
||||
assertThat(canonical.getResources()).isEqualTo(source.getResources());
|
||||
assertThat(canonical.getSource()).isEqualTo("imported");
|
||||
assertThat(canonical.getOriginDir()).isEqualTo(source.getOriginDir());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模型输出年份数组时可归一化为前端和确认接口使用的规划年数。
|
||||
*/
|
||||
@Test
|
||||
void shouldNormalizePlanningYearArray() {
|
||||
ObjectNode plan = tools.jackson.databind.json.JsonMapper.builder().build().createObjectNode();
|
||||
plan.putArray("planningYears").add("2026").add("2027");
|
||||
|
||||
AgentOutputService.normalizePlanningYears(plan);
|
||||
|
||||
assertThat(plan.path("planningYears").asInt()).isEqualTo(2);
|
||||
assertThat(plan.path("planningPeriod").asText()).isEqualTo("2026-2027");
|
||||
}
|
||||
@Test
|
||||
void shouldBoundRepairsAndReleaseCapacityOnRollback() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
var output = mock(AgentOutputService.class);
|
||||
when(output.readMaterialCheck(projectId)).thenThrow(new java.io.IOException("broken JSON"));
|
||||
when(output.readProposedPlan(projectId)).thenThrow(new java.io.IOException("broken JSON"));
|
||||
var execution = mock(AgentExecutionService.class);
|
||||
var project = mock(ProjectService.ProjectView.class);
|
||||
when(project.id()).thenReturn(projectId);
|
||||
var run = new AgentRunService.RunView(UUID.randomUUID(), projectId, UUID.randomUUID(),
|
||||
"INITIAL", "RUNNING", null, null, OffsetDateTime.now(), null);
|
||||
var service = new AgentRunService(mock(AgentRunMapper.class), tools.jackson.databind.json.JsonMapper.builder().build(),
|
||||
execution, output, mock(AgentRunStore.class), mock(AgentEventService.class), mock(ProjectService.class),
|
||||
mock(ProjectFileService.class), mock(ArtifactService.class), mock(ExecutorService.class), mock(TransactionTemplate.class), 1);
|
||||
for (String method : java.util.List.of("readMaterialCheckWithRepair", "readProposedPlanWithRepair")) {
|
||||
org.assertj.core.api.Assertions.assertThatThrownBy(() -> org.springframework.test.util.ReflectionTestUtils.invokeMethod(
|
||||
service, method, project, run)).hasMessageContaining("连续修复失败");
|
||||
}
|
||||
verify(execution, org.mockito.Mockito.times(6)).execute(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
Runnable task = () -> { };
|
||||
org.springframework.test.util.ReflectionTestUtils.invokeMethod(service, "afterCommit", UUID.randomUUID(), task);
|
||||
org.assertj.core.api.Assertions.assertThatThrownBy(() -> org.springframework.test.util.ReflectionTestUtils.invokeMethod(
|
||||
service, "afterCommit", UUID.randomUUID(), task)).hasMessageContaining("已达上限");
|
||||
var synchronization = TransactionSynchronizationManager.getSynchronizations().getFirst();
|
||||
synchronization.afterCompletion(org.springframework.transaction.support.TransactionSynchronization.STATUS_ROLLED_BACK);
|
||||
org.springframework.test.util.ReflectionTestUtils.invokeMethod(service, "afterCommit", UUID.randomUUID(), task);
|
||||
} finally {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -7,17 +7,17 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.manuagent.entity.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentEventMapper;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentEventMapper;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.agent.model.ModelConfigMapper;
|
||||
|
||||
/**
|
||||
* 验证 Agent 执行热路径只读取判断运行状态所需的最小列。
|
||||
@@ -37,7 +37,7 @@ class AgentRunStoreQueryTest {
|
||||
runMapper,
|
||||
mock(AgentEventMapper.class),
|
||||
mock(ModelConfigMapper.class),
|
||||
new ObjectMapper());
|
||||
tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
|
||||
store.ensureRunning(UUID.randomUUID());
|
||||
|
||||
@@ -61,7 +61,7 @@ class AgentRunStoreQueryTest {
|
||||
runMapper,
|
||||
mock(AgentEventMapper.class),
|
||||
mock(ModelConfigMapper.class),
|
||||
new ObjectMapper());
|
||||
tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
|
||||
assertThatThrownBy(() -> store.create(UUID.randomUUID(), "INITIAL", null))
|
||||
.isInstanceOfSatisfying(ApiException.class, exception -> {
|
||||
@@ -99,7 +99,7 @@ class AgentRunStoreQueryTest {
|
||||
runMapper,
|
||||
mock(AgentEventMapper.class),
|
||||
modelMapper,
|
||||
new ObjectMapper());
|
||||
tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
|
||||
AgentRunService.RunView run = store.create(projectId, "RESUME", null, modelId);
|
||||
|
||||
@@ -118,7 +118,7 @@ class AgentRunStoreQueryTest {
|
||||
mock(AgentRunMapper.class),
|
||||
mock(AgentEventMapper.class),
|
||||
mock(ModelConfigMapper.class),
|
||||
new ObjectMapper());
|
||||
tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
UUID runId = UUID.randomUUID();
|
||||
|
||||
assertThatThrownBy(() -> store.require(runId))
|
||||
@@ -0,0 +1,51 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import io.agentscope.core.ReActAgent;
|
||||
import io.agentscope.core.agui.adapter.AguiAdapterConfig;
|
||||
import io.agentscope.core.agui.adapter.AguiAgentAdapter;
|
||||
import io.agentscope.core.agui.event.AguiEvent;
|
||||
import io.agentscope.core.agui.model.AguiMessage;
|
||||
import io.agentscope.core.agui.model.RunAgentInput;
|
||||
import io.agentscope.core.message.Msg;
|
||||
import io.agentscope.core.message.TextBlock;
|
||||
import io.agentscope.core.model.ChatResponse;
|
||||
import io.agentscope.core.model.Model;
|
||||
import io.agentscope.core.model.transport.HttpTransportException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
class AgentRuntimeMiddlewareTest {
|
||||
@Test
|
||||
void shouldKeepOriginalFailureAcrossRealAguiConversionAndRefreshLanguage() {
|
||||
var model = mock(Model.class);
|
||||
when(model.getModelName()).thenReturn("test-model");
|
||||
var failure = new HttpTransportException("unavailable", 503, "");
|
||||
var inputs = new ArrayList<List<Msg>>();
|
||||
when(model.stream(any(), any(), any())).thenAnswer(call -> {
|
||||
inputs.add(call.getArgument(0));
|
||||
return inputs.size() == 1 ? Flux.error(failure) : Flux.just(ChatResponse.builder()
|
||||
.content(List.of(TextBlock.builder().text("已完成").build())).build());
|
||||
});
|
||||
var middleware = new AgentRuntimeMiddleware();
|
||||
var agent = ReActAgent.builder().name("test-agent").model(model).middleware(middleware).build();
|
||||
var adapter = new AguiAgentAdapter(agent, AguiAdapterConfig.builder().build());
|
||||
var input = RunAgentInput.builder().threadId("language-test").runId("run-1")
|
||||
.messages(List.of(AguiMessage.userMessage("m1", "Please inspect the file."))).build();
|
||||
var events = adapter.run(input).collectList().block();
|
||||
assertThat(events).anyMatch(AguiEvent.RunError.class::isInstance);
|
||||
assertThat(middleware.modelFailure()).isSameAs(failure);
|
||||
adapter.run(input).blockLast();
|
||||
assertThat(middleware.modelFailure()).isNull();
|
||||
assertThat(inputs).hasSize(2);
|
||||
for (var messages : inputs) {
|
||||
assertThat(messages.getLast().getTextContent()).contains("简体中文", "英文 Skill", "专有名词");
|
||||
assertThat(messages.stream().filter(msg -> msg.getTextContent().contains("<system-reminder>"))).hasSize(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.agentscope.core.state.JsonFileAgentStateStore;
|
||||
import io.agentscope.core.state.AgentState;
|
||||
import io.agentscope.core.agent.RuntimeContext;
|
||||
import io.agentscope.harness.agent.IsolationScope;
|
||||
import io.agentscope.harness.agent.sandbox.SandboxIsolationKey;
|
||||
import io.agentscope.harness.agent.sandbox.SessionSandboxStateStore;
|
||||
import io.agentscope.harness.agent.sandbox.impl.docker.DockerSandboxClient;
|
||||
import io.agentscope.harness.agent.sandbox.impl.docker.DockerSandboxState;
|
||||
import io.agentscope.harness.agent.sandbox.snapshot.LocalSnapshotSpec;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
class AgentStateFilesTest {
|
||||
@TempDir Path root;
|
||||
|
||||
@Test
|
||||
void shouldPreserveLegacySessionAndReloadFromPersistentRoot() throws Exception {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
String session = "project-" + projectId;
|
||||
Path legacy = root.resolve("legacy");
|
||||
Path source = legacy.resolve("smart-factory-agent/__anon__").resolve(session);
|
||||
Files.createDirectories(source);
|
||||
Files.writeString(source.resolve("memory_messages.jsonl"), "历史记忆\n");
|
||||
var oldStore = new JsonFileAgentStateStore(legacy.resolve("smart-factory-agent"));
|
||||
oldStore.save(null, session, "agent_state", AgentState.builder().sessionId(session).build());
|
||||
Path target = AgentStateFiles.prepare(root.resolve("data"), legacy, projectId);
|
||||
var restored = new JsonFileAgentStateStore(target);
|
||||
assertThat(restored.get(null, session, "agent_state", AgentState.class)).isPresent();
|
||||
assertThat(Files.readString(target.resolve("__anon__").resolve(session).resolve("memory_messages.jsonl")))
|
||||
.isEqualTo("历史记忆\n");
|
||||
assertThat(source.resolve("agent_state.json")).exists();
|
||||
Files.writeString(source.resolve("memory_messages.jsonl"), "旧进程后来修改的内容");
|
||||
AgentStateFiles.prepare(root.resolve("data"), legacy, projectId);
|
||||
assertThat(Files.readString(target.resolve("__anon__").resolve(session).resolve("memory_messages.jsonl")))
|
||||
.isEqualTo("历史记忆\n");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void shouldRestoreSandboxSnapshotAndBackfillAlreadyMigratedSessions(boolean migratedBefore) throws Exception {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
String session = "project-" + projectId;
|
||||
Path legacy = root.resolve("legacy");
|
||||
Path data = root.resolve("data");
|
||||
var oldStore = new JsonFileAgentStateStore(legacy.resolve("smart-factory-agent"));
|
||||
var key = SandboxIsolationKey.resolve(IsolationScope.SESSION,
|
||||
RuntimeContext.builder().sessionId(session).build(), "smart-factory-agent").orElseThrow();
|
||||
var oldSandbox = new SessionSandboxStateStore(oldStore, "smart-factory-agent");
|
||||
oldStore.save(null, session, "agent_state", AgentState.builder().sessionId(session).build());
|
||||
if (migratedBefore) {
|
||||
// 模拟上一版本已经复制会话,但尚未复制沙箱独立状态槽。
|
||||
AgentStateFiles.prepare(data, legacy, projectId);
|
||||
}
|
||||
|
||||
var client = new DockerSandboxClient();
|
||||
var state = new DockerSandboxState();
|
||||
state.setSessionId(UUID.randomUUID().toString());
|
||||
state.setImage("agent-sandbox:0.1");
|
||||
state.setSnapshot(new LocalSnapshotSpec(data.resolve("sandbox-snapshots")).build(state.getSessionId()));
|
||||
byte[] archive = "snapshot-fixture".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
state.getSnapshot().persist(new ByteArrayInputStream(archive));
|
||||
String saved = client.serializeState(state);
|
||||
oldSandbox.save(key, saved);
|
||||
|
||||
Path target = AgentStateFiles.prepare(data, legacy, projectId);
|
||||
var restoredStore = new JsonFileAgentStateStore(target);
|
||||
var restoredSandbox = new SessionSandboxStateStore(restoredStore, "smart-factory-agent");
|
||||
assertThat(restoredStore.get(null, session, "agent_state", AgentState.class)).isPresent();
|
||||
assertThat(restoredSandbox.load(key)).contains(saved);
|
||||
try (var input = client.deserializeState(restoredSandbox.load(key).orElseThrow()).getSnapshot().restore()) {
|
||||
assertThat(input.readAllBytes()).isEqualTo(archive);
|
||||
}
|
||||
assertThat(oldSandbox.load(key)).contains(saved);
|
||||
|
||||
restoredSandbox.save(key, "newer-state");
|
||||
AgentStateFiles.prepare(data, legacy, projectId);
|
||||
assertThat(restoredSandbox.load(key)).contains("newer-state");
|
||||
restoredSandbox.delete(key);
|
||||
AgentStateFiles.prepare(data, legacy, projectId);
|
||||
assertThat(restoredSandbox.load(key)).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import io.agentscope.core.model.ToolSchema;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.UUID;
|
||||
import java.io.IOException;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import tech.easyflow.manuagent.agent.project.ProjectFileService;
|
||||
|
||||
/**
|
||||
* 验证文档视觉工具可以被 AgentScope 注册并暴露结构化参数。
|
||||
*/
|
||||
class DocumentViewToolTest {
|
||||
|
||||
/**
|
||||
* 验证多视图参数能够进入模型工具定义。
|
||||
*/
|
||||
@Test
|
||||
void shouldRegisterMultiViewSchema() {
|
||||
Toolkit toolkit = new Toolkit();
|
||||
|
||||
toolkit.registerTool(new DocumentViewTool(tools.jackson.databind.json.JsonMapper.builder().build(),
|
||||
mock(ProjectFileService.class), UUID.randomUUID()));
|
||||
|
||||
ToolSchema schema = toolkit.getToolSchemas().getFirst();
|
||||
assertThat(schema.getName()).isEqualTo("document_view");
|
||||
assertThat(schema.getParameters().toString()).contains("views", "path", "page", "sheet", "range");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreserveLargePreviewBytesAndRejectIncompleteImages() throws IOException {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
String path = "work/tmp/document-view/test/render-1.png";
|
||||
byte[] bytes = new byte[700_000];
|
||||
new java.util.Random(1).nextBytes(bytes);
|
||||
ProjectFileService files = mock(ProjectFileService.class);
|
||||
when(files.preview(projectId, path)).thenReturn(
|
||||
new ProjectFileService.Preview("image/png", new ByteArrayResource(bytes)));
|
||||
var tool = new DocumentViewTool(tools.jackson.databind.json.JsonMapper.builder().build(), files, projectId);
|
||||
assertThat(tool.readPreview(path, bytes.length)).isEqualTo(bytes);
|
||||
assertThatThrownBy(() -> tool.readPreview(path, bytes.length + 1)).isInstanceOf(IOException.class);
|
||||
assertThatThrownBy(() -> tool.readPreview(path, 5 * 1024 * 1024)).isInstanceOf(IOException.class);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -7,7 +7,7 @@ import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import io.agentscope.core.agent.RuntimeContext;
|
||||
import io.agentscope.harness.agent.filesystem.model.ExecuteResponse;
|
||||
import io.agentscope.harness.agent.filesystem.sandbox.AbstractSandboxFilesystem;
|
||||
@@ -35,7 +35,7 @@ class PagedReadFileToolTest {
|
||||
0,
|
||||
false));
|
||||
PagedReadFileTool tool = new PagedReadFileTool(
|
||||
filesystem, WorkspacePathNormalizer.of("/workspace"), new ObjectMapper());
|
||||
filesystem, WorkspacePathNormalizer.of("/workspace"), tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
|
||||
String result = tool.readFile(RuntimeContext.empty(), "/workspace/work/report.txt", 0, 2);
|
||||
|
||||
@@ -56,7 +56,7 @@ class PagedReadFileToolTest {
|
||||
0,
|
||||
true));
|
||||
PagedReadFileTool tool = new PagedReadFileTool(
|
||||
filesystem, WorkspacePathNormalizer.of("/workspace"), new ObjectMapper());
|
||||
filesystem, WorkspacePathNormalizer.of("/workspace"), tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
|
||||
String result = tool.readFile(RuntimeContext.empty(), "work/report.txt", 0, 10);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.agentscope.harness.agent.sandbox.impl.docker.DockerSandboxState;
|
||||
import io.agentscope.harness.agent.sandbox.WorkspaceSpec;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RecoverableDockerClientTest {
|
||||
@Test
|
||||
void shouldRecoverOnlyTheSdkOwnedCanonicalSessionContainer() {
|
||||
var client = new RecoverableDockerClient("agent-sandbox:0.1");
|
||||
var state = new DockerSandboxState();
|
||||
state.setWorkspaceSpec(new WorkspaceSpec());
|
||||
state.setSessionId("08726648-85b1-42c0-a9c8-161e6b44150f");
|
||||
state.setContainerName("agentscope-sandbox-" + state.getSessionId());
|
||||
state.setContainerId("obsolete-id");
|
||||
client.resume(state);
|
||||
assertThat(state.getContainerId()).isEqualTo(state.getContainerName());
|
||||
|
||||
state.setContainerOwned(false);
|
||||
state.setContainerId("external-id");
|
||||
client.resume(state);
|
||||
assertThat(state.getContainerId()).isEqualTo("external-id");
|
||||
|
||||
state.setContainerOwned(true);
|
||||
state.setContainerName("other-project");
|
||||
client.resume(state);
|
||||
assertThat(state.getContainerId()).isEqualTo("external-id");
|
||||
|
||||
state.setSessionId("untrusted-name");
|
||||
state.setContainerName("agentscope-sandbox-untrusted-name");
|
||||
client.resume(state);
|
||||
assertThat(state.getContainerId()).isEqualTo("external-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMigrateOnlyOwnedLegacyImagesToTheConfiguredImage() {
|
||||
var client = new RecoverableDockerClient("registry.example/agent-sandbox:0.1");
|
||||
var state = new DockerSandboxState();
|
||||
var workspace = new WorkspaceSpec();
|
||||
state.setWorkspaceSpec(workspace);
|
||||
state.setSessionId("08726648-85b1-42c0-a9c8-161e6b44150f");
|
||||
for (String tag : new String[]{"0.1.0", "pre-font-fix"}) {
|
||||
state.setImage("smart-factory-agent-runtime:" + tag);
|
||||
var sandbox = client.resume(state);
|
||||
assertThat(sandbox.getState()).isSameAs(state);
|
||||
assertThat(state.getImage()).isEqualTo("registry.example/agent-sandbox:0.1");
|
||||
assertThat(state.getWorkspaceSpec()).isSameAs(workspace);
|
||||
assertThat(state.getSessionId()).isEqualTo("08726648-85b1-42c0-a9c8-161e6b44150f");
|
||||
}
|
||||
|
||||
state.setImage("custom-sandbox:latest");
|
||||
client.resume(state);
|
||||
assertThat(state.getImage()).isEqualTo("custom-sandbox:latest");
|
||||
|
||||
state.setContainerOwned(false);
|
||||
state.setImage("smart-factory-agent-runtime:0.1.0");
|
||||
client.resume(state);
|
||||
assertThat(state.getImage()).isEqualTo("smart-factory-agent-runtime:0.1.0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package tech.easyflow.manuagent.agent.runtime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.agentscope.harness.agent.filesystem.model.ExecuteResponse;
|
||||
import io.agentscope.harness.agent.filesystem.sandbox.AbstractSandboxFilesystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class SandboxToolsTest {
|
||||
@TempDir Path root;
|
||||
|
||||
@Test
|
||||
void shouldExecuteRealEditsAndPreserveShellFailures() throws Exception {
|
||||
var filesystem = mock(AbstractSandboxFilesystem.class);
|
||||
when(filesystem.execute(any(), anyString(), anyInt())).thenAnswer(call -> {
|
||||
Process process = new ProcessBuilder("bash", "-c", call.getArgument(1, String.class))
|
||||
.directory(root.toFile()).redirectErrorStream(true).start();
|
||||
String output = new String(process.getInputStream().readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
|
||||
return new ExecuteResponse(output, process.waitFor(), false);
|
||||
});
|
||||
var sandboxTools = new SandboxTools(filesystem, tools.jackson.databind.json.JsonMapper.builder().build());
|
||||
Path file = root.resolve("含 ' 引号.txt");
|
||||
Files.writeString(file, "中文原文\n重复 重复\n");
|
||||
var edited = sandboxTools.editFile(null, file.getFileName().toString(), "中文原文", "引号'、反引号`、$(false)\n第二行", false);
|
||||
assertThat(edited.get("success")).isEqualTo(true);
|
||||
assertThat(Files.readString(file)).startsWith("引号'、反引号`、$(false)\n第二行");
|
||||
String before = Files.readString(file);
|
||||
assertThat(sandboxTools.editFile(null, file.getFileName().toString(), "重复", "替换", false).get("success")).isEqualTo(false);
|
||||
assertThat(Files.readString(file)).isEqualTo(before);
|
||||
assertThat(sandboxTools.editFile(null, file.getFileName().toString(), "重复", "替换", true).get("replacements")).isEqualTo(2);
|
||||
assertThat(sandboxTools.editFile(null, "../outside", "x", "y", false).get("success")).isEqualTo(false);
|
||||
Files.createSymbolicLink(root.resolve("outside"), root.getParent());
|
||||
assertThat(sandboxTools.editFile(null, "outside/no-file", "x", "y", false).get("success")).isEqualTo(false);
|
||||
|
||||
var failed = sandboxTools.execute(null, "false | tail -1\ntouch should-not-exist", null, 5);
|
||||
assertThat(failed.get("success")).isEqualTo(false);
|
||||
assertThat(failed.get("exitCode")).isEqualTo(1);
|
||||
assertThat(root.resolve("should-not-exist")).doesNotExist();
|
||||
assertThat(sandboxTools.execute(null, "printf '正常'", null, 5).get("output")).isEqualTo("正常");
|
||||
// 通过 SDK 的反射注册和调用链验证工具覆盖,避免只测 Java 方法而漏掉协议接入。
|
||||
var toolkit = new io.agentscope.core.tool.Toolkit();
|
||||
toolkit.registerTool(sandboxTools);
|
||||
var input = java.util.Map.<String, Object>of("path", file.getFileName().toString(), "old_string", "第二行", "new_string", "SDK 编辑成功");
|
||||
var call = io.agentscope.core.message.ToolUseBlock.builder().id("edit-check").name("edit_file")
|
||||
.input(input).content(tools.jackson.databind.json.JsonMapper.builder().build().writeValueAsString(input)).build();
|
||||
var result = toolkit.callTool(io.agentscope.core.tool.ToolCallParam.builder().toolUseBlock(call).build()).block();
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(((io.agentscope.core.message.TextBlock) result.getOutput().getFirst()).getText()).contains("\"success\":true");
|
||||
assertThat(Files.readString(file)).contains("SDK 编辑成功");
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.skill;
|
||||
package tech.easyflow.manuagent.agent.skill;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
21
server/manuagent-common/pom.xml
Normal file
21
server/manuagent-common/pom.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-parent</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</parent>
|
||||
<artifactId>manuagent-common</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.typehandler;
|
||||
package tech.easyflow.manuagent.common.typehandler;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.typehandler;
|
||||
package tech.easyflow.manuagent.common.typehandler;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
107
server/manuagent-web/pom.xml
Normal file
107
server/manuagent-web/pom.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-parent</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</parent>
|
||||
<artifactId>manuagent-web</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-agent</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>manuagent-admin</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-flyway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mybatis-flex</groupId>
|
||||
<artifactId>mybatis-flex-spring-boot4-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tools.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<finalName>manuagent-web</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>${project.basedir}/../src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
<includes><include>db/migration/**</include></includes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.easyflow.manuagent;
|
||||
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
import tech.easyflow.manuagent.agent.config.AgentProperties;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
@@ -9,7 +9,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
* 智造申报 Agent 服务入口。
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(AppProperties.class)
|
||||
@EnableConfigurationProperties({AgentProperties.class, tech.easyflow.manuagent.admin.config.AdminProperties.class})
|
||||
public class ManuAgentApplication {
|
||||
|
||||
/**
|
||||
@@ -1,9 +1,13 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
package tech.easyflow.manuagent.web.agent;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentRunService;
|
||||
import tech.easyflow.manuagent.agent.runtime.AgentEventService;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.security.Principal;
|
||||
import tech.easyflow.manuagent.admin.auth.UserService;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -24,6 +28,8 @@ import reactor.core.publisher.Flux;
|
||||
@RequestMapping("/api/projects/{projectId}")
|
||||
public class AgentController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final AgentRunService runService;
|
||||
private final AgentEventService eventService;
|
||||
|
||||
@@ -33,7 +39,8 @@ public class AgentController {
|
||||
* @param runService Run 服务
|
||||
* @param eventService 事件服务
|
||||
*/
|
||||
public AgentController(AgentRunService runService, AgentEventService eventService) {
|
||||
public AgentController(UserService userService, AgentRunService runService, AgentEventService eventService) {
|
||||
this.userService = userService;
|
||||
this.runService = runService;
|
||||
this.eventService = eventService;
|
||||
}
|
||||
@@ -47,7 +54,7 @@ public class AgentController {
|
||||
*/
|
||||
@PostMapping("/runs/material-check")
|
||||
public AgentRunService.RunView startMaterialCheck(@PathVariable UUID projectId, Principal principal) {
|
||||
return runService.startMaterialCheck(projectId, principal);
|
||||
return runService.startMaterialCheck(projectId, userService.requireUserId(principal.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +70,7 @@ public class AgentController {
|
||||
@PathVariable UUID projectId,
|
||||
@RequestBody JsonNode response,
|
||||
Principal principal) {
|
||||
return runService.confirmMaterials(projectId, response, principal);
|
||||
return runService.confirmMaterials(projectId, response, userService.requireUserId(principal.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +93,7 @@ public class AgentController {
|
||||
*/
|
||||
@PostMapping("/runs/stop")
|
||||
public AgentRunService.RunView stop(@PathVariable UUID projectId, Principal principal) {
|
||||
return runService.stop(projectId, principal);
|
||||
return runService.stop(projectId, userService.requireUserId(principal.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +108,7 @@ public class AgentController {
|
||||
@PathVariable UUID projectId,
|
||||
@Valid @RequestBody(required = false) ResumeInput input,
|
||||
Principal principal) {
|
||||
return runService.resume(projectId, input == null ? null : input.modelConfigId(), principal);
|
||||
return runService.resume(projectId, input == null ? null : input.modelConfigId(), userService.requireUserId(principal.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +124,7 @@ public class AgentController {
|
||||
@PathVariable UUID projectId,
|
||||
@Valid @RequestBody ResumeInput input,
|
||||
Principal principal) {
|
||||
return runService.switchModel(projectId, input.modelConfigId(), principal);
|
||||
return runService.switchModel(projectId, input.modelConfigId(), userService.requireUserId(principal.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,6 @@
|
||||
package tech.easyflow.manuagent.artifact;
|
||||
package tech.easyflow.manuagent.web.artifact;
|
||||
|
||||
import tech.easyflow.manuagent.agent.artifact.ArtifactService;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.easyflow.manuagent.auth;
|
||||
package tech.easyflow.manuagent.web.auth;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user