fix: 修复热点摘要超长上下文并统一 Windows 启动文档

为 http_request 增加响应体截断与头部精简,避免门户首页触发 LLM 上下文超限;同时新增政务/媒体及教育批量 Agent 脚本,并将 Windows 启停说明合并为唯一指南,补充本次超时故障复盘与标准重启流程。

Made-with: Cursor
This commit is contained in:
renjianbo
2026-04-30 00:10:19 +08:00
parent 63b54116a5
commit cadeb2dc32
10 changed files with 1265 additions and 187 deletions

View File

@@ -0,0 +1,195 @@
# (红头)Windows 服务器启动与重启唯一指南
本文是 `D:\aaa\aiagent` 在 Windows 本地开发环境下的**唯一启动/重启文档**。
后续只看这一份即可。
---
## 0. 统一结论(先看)
- 推荐端口:
- 前端:`3001`
- 后端 API`8037`
- Redis`6379`
- `backend/.env` 必须与实际 Redis 端口一致:
- 推荐:`REDIS_URL=redis://localhost:6379/0`
- 任何 `.env` / 依赖 / 工具代码变更后,至少重启:
- API + Celery`restart_backend_celery.ps1`
- 若出现 `timeout of 30000ms exceeded`,优先检查:
1) Redis 是否可连
2) Celery Worker 是否在跑
3) API 与 Worker 是否使用同一 `backend\venv`
---
## 1. 一键启动 / 停止 / 重启
在 PowerShell 中执行(仓库根目录):
```powershell
cd D:\aaa\aiagent
```
### 1.1 一键启动(全套)
```powershell
powershell -ExecutionPolicy Bypass -File .\start_aiagent.ps1
```
默认目标:
- 前端:`http://localhost:3001`
- 后端文档:`http://127.0.0.1:8037/docs`
- Redis`127.0.0.1:6379`
### 1.2 一键停止(全套)
```powershell
powershell -ExecutionPolicy Bypass -File .\stop_aiagent.ps1
```
### 1.3 仅重启后端 + Celery最常用
```powershell
powershell -ExecutionPolicy Bypass -File .\restart_backend_celery.ps1
```
适用场景:改了 `.env`、Python 依赖、内置工具实现、Agent 执行逻辑。
---
## 2. 标准“重启服务器”流程(推荐照抄)
```powershell
cd D:\aaa\aiagent
powershell -ExecutionPolicy Bypass -File .\stop_aiagent.ps1
powershell -ExecutionPolicy Bypass -File .\start_aiagent.ps1
```
完成后立刻验证:
```powershell
netstat -ano | findstr :6379
netstat -ano | findstr :8037
netstat -ano | findstr :3001
curl http://127.0.0.1:8037/health
```
---
## 3. 本次故障复盘(学生作业管理助手超时)
### 3.1 现象
- Agent 对话区报错:`发送失败: timeout of 30000ms exceeded`
- 前端可打开,但执行一直超时。
### 3.2 根因
1. `backend/.env` 配置为:
- `REDIS_URL=redis://localhost:6380/0`
2. 实际 Redis 监听在:
- `6379`
3. 导致 Celery 任务队列链路异常(或 Worker 无法稳定消费Agent 执行超时。
### 3.3 修复
1.`backend/.env` 改为:
- `REDIS_URL=redis://localhost:6379/0`
2. 执行:
- `powershell -ExecutionPolicy Bypass -File .\restart_backend_celery.ps1`
3. 验证:
- `6379/8037/3001` 监听正常
- `/health` 返回 `200`
- Celery worker 进程存在
### 3.4 预防
- 不要混用两套端口约定(`6379``6380`)。
- 每次重启后先做 1 分钟健康检查(端口 + `/health` + 1 条 Agent 测试消息)。
---
## 4. 常见问题与快速处理
### 4.1 执行策略拦截脚本
```powershell
Set-ExecutionPolicy -Scope Process Bypass
```
### 4.2 `start_aiagent.ps1` 报 PowerShell 解析错误
症状:出现 `ParserError`、字符串终止符缺失、`[OK]` 附近报错。
处理:
1. 临时手动启动(见 4.3
2. 将脚本保存为 **UTF-8建议无 BOM/ ASCII 兼容内容**,避免中文引号或异常字符
### 4.3 一键脚本不可用时的手动拉起(应急)
开 3~4 个终端:
1) Redis
```powershell
cd D:\aaa\aiagent\backend\redis
.\redis-server.exe --port 6379
```
2) API
```powershell
cd D:\aaa\aiagent\backend
.\venv\Scripts\Activate.ps1
python -m uvicorn app.main:app --host 0.0.0.0 --port 8037
```
3) Celery
```powershell
cd D:\aaa\aiagent\backend
.\venv\Scripts\Activate.ps1
python -m celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8
```
4) 前端
```powershell
cd D:\aaa\aiagent\frontend
$env:AIAGENT_API_PROXY='http://127.0.0.1:8037'
pnpm dev --port 3001
```
---
## 5. OCR上传图片识别必查项
`backend/.env` 建议:
```ini
TESSERACT_CMD=C:/Program Files/Tesseract-OCR/tesseract.exe
TESSERACT_TESSDATA_DIR=D:/aaa/aiagent/tessdata
```
自检:
```powershell
cd D:\aaa\aiagent\backend
.\venv\Scripts\python scripts\check_ocr_env.py
```
若新增依赖后仍报 OCR 缺失,重启 Celery。
---
## 6. 访问地址
- 前端:`http://localhost:3001`
- 后端 API`http://127.0.0.1:8037`
- 后端文档:`http://127.0.0.1:8037/docs`
- 健康检查:`http://127.0.0.1:8037/health`
---
## 7. 维护规则(强制)
- 所有 Windows 启动/重启内容只维护本文件。
- 其他旧文档仅保留跳转,不再写重复步骤。
- 修改启动逻辑后,先更新本文件再通知团队。

View File

@@ -0,0 +1,137 @@
# 上传图片与文字识别实现说明
本文说明平台中 **设计器/预览聊天** 场景下:**图片如何上传、如何随消息发给 Agent、后端如何读出图中文字OCR**,以及涉及的主要文件与配置。
---
## 1. 总体流程
```mermaid
sequenceDiagram
participant U as 浏览器
participant API as 后端 API
participant FS as 工作区磁盘
participant Q as消息队列
participant W as Worker
U->>API: POST /api/v1/uploads/preview (multipart, JWT)
API->>FS: 写入 uploads/preview/{user_id}/{uuid}_{safe_name}
API-->>U: relative_path, filename, content_type
U->>API: POST /api/v1/executions (input_data 含 query/USER_INPUT/attachments)
API->>Q: 入队执行任务
Q->>W: 执行 Agent 工作流
W->>W: LLM 可调用 file_read(relative_path)
W->>FS: 读同一工作区下文件
W-->>W: 图片走 OCR返回 JSON content
```
要点:
1. **上传**只负责把文件落到 **与工作区根一致** 的目录,并返回 **相对路径**
2. **识别**不发生在上传接口里,而是由工作流中的 **`file_read` 工具** 在读文件时,对图片扩展名走 **Tesseract OCR**
3. **Agent 是否真的会去 `file_read`** 取决于提示词与工作流设计;前端会把路径写进 `USER_INPUT` / `query` 便于模型使用。
---
## 2. 前端实现(`frontend/src/components/AgentChatPreview.vue`
| 能力 | 说明 |
|------|------|
| 选择文件 | 隐藏 `<input type="file">`,回形针触发;`accept` 含常见图片与文档扩展名。 |
| 拖放图片 | 在 `chat-input-area` 上监听 `dragenter/dragover/drop`**仅处理图片**(与 `isImageFile` 一致),非图片可提示忽略。 |
| 上传 | `uploadPreviewFiles``FormData` + `POST /api/v1/uploads/preview`需登录JWT。 |
| 本地缩略图 | 图片用 `URL.createObjectURL(file)` 作为 `thumbUrl`;发送成功且消息仍引用时延迟 `revoke`,避免 blob 提前失效。 |
| 随消息发送 | `POST /api/v1/executions``input_data` 包含:`USER_INPUT``query`(合并用户话 +附件路径说明)、`user_id`(预览会话)、`attachments`(含 `relative_path``filename``content_type` 等)。 |
| 历史回显 | `GET /api/v1/agents/{id}/preview-chat-history` 返回轮次中的 `attachments`;图片再通过 `GET /api/v1/uploads/preview/file?file_path=...` 拉 blob 生成缩略图。 |
| 交互 | 消息内多图横向滚动、缩略图点击放大(对话框)。 |
---
## 3. 后端:上传与下载
**文件**`backend/app/api/uploads.py`
| 接口 | 方法 | 作用 |
|------|------|------|
| `/api/v1/uploads/preview` | `POST` | 单文件上传;需 JWT。保存目录`{LOCAL_FILE_TOOLS_ROOT}/uploads/preview/{当前用户 id}/`。文件名经 `_safe_filename` 处理,前缀短 UUID 防冲突。响应:`relative_path`(相对工作区根)、`filename``size``content_type`。超大返回413上限见 `LOCAL_FILE_WRITE_MAX_BYTES`。 |
| `/api/v1/uploads/preview/file` | `GET` | 按 `file_path`(即 `relative_path`)读文件;**仅允许** `uploads/preview/{当前用户 id}/` 前缀,防止越权。用于历史消息图片回显。 |
**工作区根**:与 `file_read` / `file_write` 一致,由 `LOCAL_FILE_TOOLS_ROOT` 配置;未配置时默认为 **仓库根目录**(见 `builtin_tools._local_file_workspace_root`)。
---
## 4. 后端图片识别OCR
**文件**`backend/app/services/builtin_tools.py`
| 项目 | 说明 |
|------|------|
| 工具名 | `file_read`(异步,返回 JSON 字符串) |
| 图片扩展名 | `.png` `.jpg` `.jpeg` `.webp` `.gif` `.bmp` `.tif` `.tiff` |
| 识别实现 | `_read_image_ocr_sync``PIL.Image` 打开图片,`pytesseract.image_to_string`,语言依次尝试 **`chi_sim+eng`**、**`eng`**。 |
| Tesseract 路径 | `settings.TESSERACT_CMD` 非空时赋值给 `pytesseract.pytesseract.tesseract_cmd`。 |
| 语言包目录 | `settings.TESSERACT_TESSDATA_DIR`;若为空且仓库根下存在 `tessdata/*.traineddata`,会自动使用该目录(`_tessdata_dir_for_ocr`)。 |
| 返回 | 成功:`extract_mode``image_ocr``content` 为识别文本(有 UTF-8 字节上限截断。失败JSON 内 `error` 字段说明原因(缺 Pillow/pytesseract、未找到 Tesseract、其它 OCR 异常)。 |
**依赖**(需安装在与 **Celery Worker** 相同的 Python 环境中):
- `Pillow``pytesseract`(见 `backend/requirements.txt`
- 本机安装 **Tesseract 可执行文件**Windows 常见路径:`C:/Program Files/Tesseract-OCR/tesseract.exe`
**环境变量**`backend/.env` / `env.example`
- `TESSERACT_CMD`:指向 `tesseract.exe`
- `TESSERACT_TESSDATA_DIR`:可选,指向含 `chi_sim.traineddata` 的目录,中文效果更好
自检脚本:`backend/scripts/check_ocr_env.py`
---
## 5. 历史记录中的附件
**文件**`backend/app/services/agent_workspace_chat_log.py`
- `fetch_agent_preview_chat_turns` 从已完成执行的 `input_data` 中解析 **`attachments`** 列表(`relative_path``filename``content_type`),随每轮 `user_text` / `agent_text` 返回给前端。
**文件**`backend/app/api/agents.py`
- 响应模型 `PreviewChatTurnResponse`**`attachments`** 字段。
---
## 6. 与 Agent / 工作流的关系
- 前端把附件路径写进 **`USER_INPUT` / `query`**,并保留结构化 **`attachments`**,便于工作流引擎提取 `user_query`及工具参数。
- **真正执行 OCR** 的时机是:工作流中 LLM或工具节点调用 **`file_read`**且传入上传返回的 **`relative_path`**。
- 若 Agent 未配置调用 `file_read` 或提示词未引导使用路径,则可能出现「已上传但助手仍说读不了」——需改 Agent 配置或提示词,而非仅改上传代码。
---
## 7. 常见问题
| 现象 | 可能原因 | 处理方向 |
|------|----------|----------|
| 上传 401 | 未登录或 token 过期 | 重新登录 |
| 上传 413 | 超过 `LOCAL_FILE_WRITE_MAX_BYTES` | 压缩图片或调大配置(需评估安全) |
| 助手仍提示缺 Pillow/pytesseract | Worker 使用的 venv 未安装依赖 | 在 `backend\venv` 执行 `pip install -r requirements.txt`**重启 Celery** |
| 提示未找到 Tesseract | 未安装或未配置 PATH / `TESSERACT_CMD` | 安装 Tesseract 并配置 `.env` |
| 中文识别差 | 缺少 `chi_sim.traineddata` | 配置 `TESSERACT_TESSDATA_DIR` 或放置语言包到自动探测的 `tessdata/` |
| OpenAPI 无 uploads | 后端为旧进程 | 重启 API |
---
## 8. 相关文件索引
| 路径 | 作用 |
|------|------|
| `frontend/src/components/AgentChatPreview.vue` | 上传、拖放、发送、缩略图、历史回显 |
| `backend/app/api/uploads.py` | 预览上传、预览文件只读下载 |
| `backend/app/services/builtin_tools.py` | `file_read`、图片 OCR |
| `backend/app/services/agent_workspace_chat_log.py` | 历史轮次附带 `attachments` |
| `backend/app/api/agents.py` | `preview-chat-history` 响应模型 |
| `backend/scripts/check_ocr_env.py` | OCR 环境自检 |
| `启动的注意事项.md` | Redis、venv、重启 Celery 等运维说明 |
---
文档版本:与当前仓库实现一致时可随代码迭代更新本文。

View File

@@ -1,104 +1,7 @@
# 前后端服务器启动和停止说明
# 前后端服务器启动和停止(已合并)
## 一、使用 Docker Compose推荐
本文件已停止维护。
请改看唯一文档:`(红头)Windows服务器启动与重启唯一指南.md`
本项目前后端及依赖服务均通过 `docker-compose.dev.yml` 管理,需在项目根目录执行以下命令。
路径:`D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md`
### 1. 启动所有服务(前端 + 后端 + Redis + Celery
```bash
cd /home/renjianbo/aiagent
docker-compose -f docker-compose.dev.yml up -d
```
### 2. 停止所有服务
```bash
cd /home/renjianbo/aiagent
docker-compose -f docker-compose.dev.yml down
```
### 3. 重启所有服务
```bash
cd /home/renjianbo/aiagent
docker-compose -f docker-compose.dev.yml restart
```
### 4. 仅重启前端或后端
```bash
# 仅重启前端
docker-compose -f docker-compose.dev.yml restart frontend
# 仅重启后端
docker-compose -f docker-compose.dev.yml restart backend
```
---
## 二、查看服务状态与日志
### 查看运行状态
```bash
docker-compose -f docker-compose.dev.yml ps
```
### 查看日志
```bash
# 所有服务
docker-compose -f docker-compose.dev.yml logs -f
# 仅前端
docker-compose -f docker-compose.dev.yml logs -f frontend
# 仅后端
docker-compose -f docker-compose.dev.yml logs -f backend
# 仅 Celery
docker-compose -f docker-compose.dev.yml logs -f celery
# 仅 Redis
docker-compose -f docker-compose.dev.yml logs -f redis
```
---
## 三、服务与端口说明
| 服务 | 宿主机端口 | 说明 |
|--------|------------|----------------|
| 前端 | 8038 | 低代码智能体平台页面 |
| 后端 | 8037 | API 服务 |
| Redis | 6380 | 缓存/队列(避免与宿主机 6379 冲突) |
| Celery | — | 仅内网,无宿主机端口映射 |
---
## 四、访问地址
- **前端页面**: http://localhost:8038 或 http://101.43.95.130:8038
- **后端 API**: http://localhost:8037 或 http://101.43.95.130:8037
- **API 文档**: http://localhost:8037/docs
- **健康检查**: http://localhost:8037/health
---
## 五、注意事项
1. 所有 `docker-compose` 命令均需指定 `-f docker-compose.dev.yml`,且建议在项目根目录 `/home/renjianbo/aiagent` 下执行。
2. 停止服务使用 `down`,不会删除镜像和已创建的卷(如 Redis 数据卷)。
3. 若宿主机 6379 已被占用Redis 已改为使用宿主机端口 **6380**,无需再改配置。
4. 云服务器部署时,需在安全组中放行 **8038**(前端)和 **8037**(后端)端口。
---
## 六、常见问题
| 现象 | 处理建议 |
|----------------|----------|
| 端口被占用 | 检查 8037、8038、6380 是否被占用;必要时修改 `docker-compose.dev.yml` 中端口映射。 |
| 前端能开、登录报错 | 检查后端是否启动、8037 是否放行;在服务器上执行 `curl http://127.0.0.1:8037/health` 验证。 |
| 容器反复退出 | 使用 `docker-compose -f docker-compose.dev.yml logs backend`(或对应服务名)查看报错并排查。 |

View File

@@ -1,80 +1,7 @@
# 启动注意事项
# 启动注意事项(已合并)
本文面向在 **Windows** 上本地运行本仓库(后端 API、Celery、前端、Redis时的常见配置与排错要点。
本文件已停止维护。
请改看唯一文档:`(红头)Windows服务器启动与重启唯一指南.md`
---
路径:`D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md`
## 1. Redis 端口与 `.env` 必须一致
- `backend/.env` 中的 **`REDIS_URL`**(例如 `redis://localhost:6380/0`)必须与**实际监听的 Redis 端口**一致。
- 仓库内 `start_aiagent.ps1` 默认启动的是 **`6379`**。若 `.env` 写的是 **6380**,请要么:
- 在本机启动监听 **6380** 的 Redis要么
-`.env` 改为 **6379** 并使用脚本启动的 Redis。
- **症状**:创建执行返回 **503**、日志中出现无法连接 Redis / Celery 入队失败。
---
## 2. API 与 Celery 必须使用同一虚拟环境
- 工作流/Agent 执行由 **Celery Worker** 跑,与 **uvicorn API** 应共用 **`backend\venv`**。
- 更新依赖后请在 **`backend` 目录**执行:
```powershell
.\venv\Scripts\pip install -r requirements.txt
```
- **改完 `.env` 或 Python 依赖后**,需要**重启 API 和 Celery**,否则仍加载旧环境。
- 仓库提供**仅重启后端 + Celery**(不停止前端/本机 Redis的脚本
```powershell
powershell -ExecutionPolicy Bypass -File D:\aaa\aiagent\restart_backend_celery.ps1
```
---
## 3. 图片 OCR作业/聊天里识别图中文字)
- `file_read` 读图片依赖:**Pillow**、**pytesseract**,以及本机安装 **Tesseract 可执行文件**。
- 在 `backend/.env` 中建议配置(路径按本机修改):
- **`TESSERACT_CMD`**:指向 `tesseract.exe`(例如 `C:/Program Files/Tesseract-OCR/tesseract.exe`)。
- **`TESSERACT_TESSDATA_DIR`**(可选):指向含 **`chi_sim.traineddata`** 的目录,中文识别更稳定。
- 自检:
```powershell
cd D:\aaa\aiagent\backend
.\venv\Scripts\python scripts\check_ocr_env.py
```
- **症状**:助手回复里出现「请安装 Pillow / pytesseract」或无法识别图中文字 → 先检查 venv 是否已 `pip install`,再检查 Tesseract 与 `.env`,最后**重启 Celery**。
---
## 4. 前端与后端地址
- 前端开发服通过代理访问 API一键脚本里会通过环境变量指向当前 API 地址。
- 若 OpenAPI 里**没有**新加的路由(例如上传),多半是 **API 进程仍是旧代码/旧进程**,需要重启后端。
---
## 5. 鉴权与安全
- 上传、执行等接口需要 **JWT**;预览对话若出现 **401**,请重新登录后再试。
- **勿**在文档或 Git 中提交 **明文密码、密钥、完整 `.env`**`.env` 应留在本机并加入版本忽略。
---
## 6. 一键启停脚本(参考)
| 脚本 | 作用 |
|------|------|
| `start_aiagent.ps1` | 启动 Redis(6379)、API、Celery、前端按脚本内端口逻辑 |
| `stop_aiagent.ps1` | 停止 API、Celery、前端、以及匹配的 redis-server 进程 |
| `restart_backend_celery.ps1` | 仅重启 API(8037) + Celery适合改依赖或 `.env` 后快速生效 |
实际端口以你本机 **`start_aiagent.ps1` / `restart_backend_celery.ps1`** 及 `.env` 为准。
---
## 7. 修改记录建议
- 变更依赖或环境变量后:在本机记一句「已重启 API + Celery」便于以后排查。

View File

@@ -1,3 +1,18 @@
# Windows 启动指南(已合并)
本文件已停止维护,请只看以下唯一文档:
- `D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md`
说明:历史内容已合并至上方文档,后续不再在本文件更新。
# Windows 启动指南(已合并)
本文件已停止维护。
请改看唯一文档:`(红头)Windows服务器启动与重启唯一指南.md`
路径:`D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md`
# Windows 本地启动指南
## 前置要求

View File

@@ -34,6 +34,9 @@ class Settings(BaseSettings):
LOCAL_FILE_READ_MAX_BYTES: int = 2_097_152 # 单次读取上限(默认 2MB
LOCAL_FILE_WRITE_MAX_BYTES: int = 2_097_152 # 单次写入内容上限UTF-8 字节)
# http_request 工具:写入 LLM 上下文的响应体最大字符数HTML/JSON 过大时截断,避免超过模型 context
HTTP_REQUEST_MAX_BODY_CHARS: int = 32_000
# 图片 OCRfile_read 对 png/jpg 等Tesseract 可执行文件路径Windows 示例 C:/Program Files/Tesseract-OCR/tesseract.exe
TESSERACT_CMD: str = ""
# 自定义 tessdata 目录(内含 chi_sim.traineddata 等)。留空时若 LOCAL_FILE_TOOLS_ROOT/tessdata 下存在 .traineddata 则自动使用

View File

@@ -67,11 +67,74 @@ def _resolve_path_under_workspace(file_path: str) -> Tuple[Optional[Path], Optio
return p, None
_HTTP_RESPONSE_HEADER_ALLOWLIST = frozenset(
{
"content-type",
"content-length",
"content-encoding",
"date",
"last-modified",
"location",
"server",
"cache-control",
}
)
def _compact_http_response_headers(response: httpx.Response) -> Dict[str, str]:
"""减少 Set-Cookie 等大字段进入 LLM 上下文。"""
out: Dict[str, str] = {}
for key, value in response.headers.multi_items():
lk = key.lower()
if lk in ("set-cookie", "cookie"):
continue
if lk in _HTTP_RESPONSE_HEADER_ALLOWLIST:
out[key] = value if len(value) <= 2048 else value[:2048] + "..."
if not out:
n = 0
for key, value in response.headers.multi_items():
lk = key.lower()
if lk in ("set-cookie", "cookie"):
continue
if n >= 12:
break
out[key] = value if len(value) <= 512 else value[:512] + "..."
n += 1
return out
def _truncate_http_body_for_tool(body: Any, max_chars: int) -> Tuple[Any, bool, Optional[str]]:
"""
将 HTTP 正文限制在 max_chars 字符以内,避免门户首页等大 HTML 撑爆模型 context。
返回 (可能被截断后的 body, 是否截断, 说明文案)
"""
if max_chars <= 0:
max_chars = 32_000
note: Optional[str] = None
if isinstance(body, str):
if len(body) <= max_chars:
return body, False, None
note = (
f"正文已截断:原始约 {len(body)} 字符,仅保留前 {max_chars} 字符。"
"门户/频道首页通常过大,摘要请优先使用具体文章页 URL。"
)
return body[:max_chars], True, note
try:
serialized = json.dumps(body, ensure_ascii=False)
except (TypeError, ValueError):
serialized = str(body)
if len(serialized) <= max_chars:
return body, False, None
note = f"JSON 响应已截断:序列化长度约 {len(serialized)} 字符,仅保留前 {max_chars} 字符。"
return serialized[:max_chars], True, note
async def http_request_tool(
url: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
body: Any = None
body: Any = None,
max_body_chars: Optional[int] = None,
) -> str:
"""
HTTP请求工具
@@ -81,12 +144,18 @@ async def http_request_tool(
method: HTTP方法 (GET, POST, PUT, DELETE)
headers: 请求头
body: 请求体POST/PUT时使用
max_body_chars: 响应正文写入工具结果的最大字符数;默认读取配置 HTTP_REQUEST_MAX_BODY_CHARS
Returns:
JSON格式的响应结果
"""
try:
async with httpx.AsyncClient(timeout=30.0) as client:
limit = max_body_chars
if limit is None:
limit = int(getattr(settings, "HTTP_REQUEST_MAX_BODY_CHARS", 32_000) or 32_000)
limit = max(4096, min(limit, 200_000))
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
method_upper = method.upper()
if method_upper == "GET":
@@ -103,15 +172,20 @@ async def http_request_tool(
# 尝试解析JSON响应
try:
response_body = response.json()
except:
except Exception:
response_body = response.text
result = {
truncated_body, truncated, trunc_note = _truncate_http_body_for_tool(response_body, limit)
result: Dict[str, Any] = {
"status_code": response.status_code,
"headers": dict(response.headers),
"body": response_body
"headers": _compact_http_response_headers(response),
"body": truncated_body,
}
if truncated:
result["body_truncated"] = True
if trunc_note:
result["truncation_note"] = trunc_note
return json.dumps(result, ensure_ascii=False)
except Exception as e:
logger.error(f"HTTP请求工具执行失败: {str(e)}")
@@ -1036,7 +1110,14 @@ HTTP_REQUEST_SCHEMA = {
"body": {
"type": "object",
"description": "请求体POST/PUT时使用可选"
}
},
"max_body_chars": {
"type": "integer",
"description": (
"响应正文写入结果的最大字符数(可选)。"
"门户首页等大 HTML 默认会按平台配置截断;摘要单篇文章可适当调大(如 80000仍可能受模型总上下文限制。"
),
},
},
"required": ["url", "method"]
}

View File

@@ -0,0 +1,434 @@
#!/usr/bin/env python3
"""
批量创建或更新一批「教育行业」场景 Agent单链 Start → LLM → End
用法:
cd backend
.\\venv\\Scripts\\python.exe scripts/create_education_agents_batch.py
# 只创建/更新其中一个(名称需与内置列表完全一致):
set EDU_ONLY_AGENT=错题本分析助手
.\\venv\\Scripts\\python.exe scripts/create_education_agents_batch.py
环境变量:
PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD
EDU_LLM_PROVIDER / EDU_LLM_MODEL / EDU_LLM_TIMEOUT可选否则用 ENTERPRISE_* 或 deepseek-chat
EDU_ONLY_AGENT可选仅处理该名称的 Agent
"""
from __future__ import annotations
import json
import os
import re
import sys
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
import requests
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BACKEND_DIR not in sys.path:
sys.path.insert(0, BACKEND_DIR)
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
USER = os.getenv("PLATFORM_USERNAME", "admin")
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
PROVIDER = os.getenv(
"EDU_LLM_PROVIDER",
os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek"),
)
MODEL = os.getenv(
"EDU_LLM_MODEL",
os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat"),
)
REQ_TIMEOUT = max(
30,
int(
os.getenv(
"EDU_LLM_TIMEOUT",
os.getenv("ENTERPRISE_LLM_TIMEOUT", "180"),
)
),
)
ONLY = (os.getenv("EDU_ONLY_AGENT") or "").strip()
BUDGET_BASE = {
"max_steps": 80,
"max_llm_invocations": 6,
"max_tool_calls": 24,
}
TOOLS_STD = ["file_read", "text_analyze", "datetime", "json_process"]
TOOLS_MATH = ["file_read", "text_analyze", "datetime", "json_process", "math_calculate"]
@dataclass(frozen=True)
class EduAgentSpec:
name: str
label: str
node_id: str
description: str
prompt: str
tools: Tuple[str, ...]
temperature: float = 0.35
max_tool_iterations: int = 12
def _slug(s: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")[:40] or "agent"
AGENTS: List[EduAgentSpec] = [
EduAgentSpec(
name="错题本分析助手",
label="错题分析",
node_id="llm-edu-mistake",
description=(
"帮助学生整理错题:错因归类、知识点缺口、举一反三思路;"
"支持上传题目照片或文档后用 file_read 读原文;不代写整题标准答案,侧重方法与自查。"
),
prompt="""你是「错题本分析助手」,面向中小学与大学基础课程。
【任务】
1. 请用户说明学科、题型、自己的错选/错解(若上传了题目图片或文件,**先调用 file_read** 读取内容再分析)。
2. 归纳:**错因类型**(概念/审题/计算/步骤跳步/知识遗忘等)、**涉及知识点**、**正确思路提纲**(分步,不直接给可照抄的完整答卷)。
3. 给出 **12 道同类变式** 的方向描述或自检问题(不必编造具体数字题除非用户需要且合理)。
4. 可用 json_process 或表格输出结构化错题卡片(日期、科目、知识点、错因、复习提醒)。
【原则】
- 语气鼓励、具体;不羞辱式批评。
- 不确定题干时先确认,不臆造题目条件。
- 中文为主;公式用可读形式。
""",
tools=tuple(TOOLS_STD),
temperature=0.3,
),
EduAgentSpec(
name="语文阅读与写作助手",
label="语文读写",
node_id="llm-edu-chinese",
description=(
"语文阅读理解答题思路、作文立意与提纲、素材迁移;"
"可读取用户上传的文章或题目材料file_read不代写整篇可提交的作文正文。"
),
prompt="""你是「语文阅读与写作助手」。
【阅读】
- 概括主旨、结构、人物形象、关键手法;结合文本引用思路(若材料在附件中,**先 file_read**)。
- 教「如何组织答案」:观点句+依据+简要分析;不编造原文没有的引文。
【写作】
- 可提供立意角度、提纲、开头结尾范例句(短)、修改建议清单。
- **不代写**整篇参赛/考试作文;可示范片段并说明可替换处。
【输出】
- 分条清晰;尊重教材与考区差异,不确定时提示以教师要求为准。
""",
tools=tuple(TOOLS_STD),
temperature=0.4,
),
EduAgentSpec(
name="英语作文与语法助手",
label="英语习作",
node_id="llm-edu-english",
description=(
"英语作文结构、语法纠错说明、替换表达与连接词可读取用户上传的英文草稿file_read"
"不代写整篇可提交的英语作文。"
),
prompt="""你是「英语作文与语法助手」。
【能力】
- 审题与段落结构thesis / body / conclusion提供连接词与句式升级建议。
- 语法与用词:说明**规则**与**修改原因**,可给改写示例句(短)。
- 若用户上传 doc/txt/图片,**先 file_read** 再基于原文反馈。
【边界】
- 不生成整篇可一键提交的课堂/考试作文;可给框架与片段。
- 输出中英可混排,解释优先中文便于理解。
""",
tools=tuple(TOOLS_STD),
temperature=0.35,
),
EduAgentSpec(
name="考试复习规划助手",
label="复习规划",
node_id="llm-edu-exam",
description=(
"根据目标考试与剩余时间,拆复习阶段、每日任务模板与优先级;"
"可用 json_process 输出周计划表;结合 datetime 谈倒计时与节奏。"
),
prompt="""你是「考试复习规划助手」。
【输入】
- 考试科目/范围、当前水平自评、可用每日学习时长、考试日期(可用 datetime 对齐「今天」与截止日)。
【输出】
- 分阶段(基础→专题→模考→查漏);每周重点与**可执行**日任务(番茄钟量级即可)。
- 用 json_process 或 Markdown 表格输出计划时保持可打印、可勾选。
- 不保证分数;提醒睡眠与劳逸结合。
【原则】
- 若信息不足,先问 12 个关键问题再出计划。
""",
tools=tuple(TOOLS_STD),
temperature=0.35,
),
EduAgentSpec(
name="家校沟通话术助手",
label="家校沟通",
node_id="llm-edu-parent",
description=(
"面向教师/班主任:与家长微信、电话、家长会沟通的礼貌、清晰话术草稿;"
"情境包括成绩反馈、纪律、合作建议;不替代正式处分或法律意见。"
),
prompt="""你是「家校沟通话术助手」,读者主要是**教师**。
【能力】
- 根据情境生成:**简短微信**、**电话开场**、**家长会发言要点**(客观、合作、具体建议)。
- 语气:**尊重、不激化矛盾**;避免标签化学生与家长。
- 可提供「若家长情绪激动」的缓和句式与边界话术。
【边界】
- 不涉及具体法律结论;严重事件建议走学校流程与专业人士。
- 不编造学生隐私细节;缺信息时用占位并请老师补全。
【输出】
- 先给「目标」再给「话术草稿」与「可选修改点」。
""",
tools=tuple(TOOLS_STD),
temperature=0.3,
),
EduAgentSpec(
name="实验报告结构化助手",
label="实验报告",
node_id="llm-edu-lab",
description=(
"辅助理化生实验报告:目的、器材、步骤、数据表、误差与结论框架;"
"可读取用户上传的实验数据或草稿file_read不伪造实验数据。"
),
prompt="""你是「实验报告结构化助手」。
【能力】
- 按常见结构梳理:**目的、原理、器材、步骤、数据记录表、处理与误差、结论与讨论**。
- 用户上传数据/草稿时 **先 file_read**,再帮助排版与补全「讨论角度」(不编造未出现的测量值)。
- 可用 json_process 输出字段清单供粘贴到 Word。
【原则】
- **严禁伪造数据**;缺失数据处标注「请填写实测」。
- 涉及安全操作须提醒遵守实验室规范。
【输出】
- 中文;公式与单位规范;表格用 Markdown。
""",
tools=tuple(TOOLS_STD),
temperature=0.3,
),
EduAgentSpec(
name="数学解题思路助手",
label="数学辅导",
node_id="llm-edu-math",
description=(
"数学题型思路、关键步骤与检验方法;可用 math_calculate 做简单数值校验;"
"不输出可整卷照抄的解答,侧重引导与分步。"
),
prompt="""你是「数学解题思路助手」。
【能力】
- 根据题目(含上传图片 **file_read** OCR结果分析**考点、等价变形、推荐步骤链、易错点**。
- 需要时可用 **math_calculate** 做简单数值/表达式验算(步骤仍用文字说明)。
- 给「下一步提示」而非一次性完整标准答案,引导学生自算。
【边界】
- 竞赛/考试整卷代做请求应拒绝完整作答,改为方法提纲。
- 不确定题意时先澄清条件。
【输出】
- 分步编号;关键式子单独一行;中文说明。
""",
tools=tuple(TOOLS_MATH),
temperature=0.25,
max_tool_iterations=14,
),
EduAgentSpec(
name="生涯选科咨询助手",
label="选科咨询",
node_id="llm-edu-career",
description=(
"浅度选科/分科、专业兴趣自我梳理:优势学科、职业想象、信息清单与决策问题;"
"不提供唯一正确答案,不替代官方招生政策;引导查官方简章与老师。"
),
prompt="""你是「生涯选科咨询助手」,做**浅度、非决策替代**的引导。
【能力】
- 用结构化提问帮用户梳理:**兴趣、学科感受、时间投入、长远想象**。
- 输出「信息收集清单」(官方简章、学校开设组合、本校资源)与「决策维度表」,不替用户做唯一选择。
- 可用 json_process 整理自评表。
【边界】
- 不编造分数线、政策条款;涉及政策必提示以**教育部门与学校最新官方文件**为准。
- 心理危机倾向请引导寻求专业人士与学校心理老师。
【语气】
- 平等、务实;避免焦虑营销式表述。
""",
tools=tuple(TOOLS_STD),
temperature=0.4,
),
]
def _sanitize_edges(edges: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
seen: set = set()
out: List[Dict[str, Any]] = []
for e in edges or []:
s, t = e.get("source"), e.get("target")
if not s or not t or s == t:
continue
key = (s, t, e.get("sourceHandle") or "")
if key in seen:
continue
seen.add(key)
ne = dict(e)
if not ne.get("targetHandle"):
ne["targetHandle"] = "left"
if not ne.get("id"):
sh = ne.get("sourceHandle") or "r"
ne["id"] = f"e_{s}_{t}_{sh}"
out.append(ne)
return out
def build_workflow(spec: EduAgentSpec) -> Dict[str, Any]:
llm_pos: Tuple[int, int] = (380, 220)
tools = list(spec.tools)
nodes: List[Dict[str, Any]] = [
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 220}, "data": {"label": "开始"}},
{
"id": spec.node_id,
"type": "llm",
"position": {"x": llm_pos[0], "y": llm_pos[1]},
"data": {
"label": spec.label,
"prompt": spec.prompt,
"provider": PROVIDER,
"model": MODEL,
"temperature": spec.temperature,
"request_timeout": REQ_TIMEOUT,
"enable_tools": True,
"tools": tools,
"selected_tools": tools,
"max_tool_iterations": spec.max_tool_iterations,
},
},
{"id": "end-1", "type": "end", "position": {"x": llm_pos[0] + 260, "y": 220}, "data": {"label": "结束"}},
]
edges = _sanitize_edges(
[
{"source": "start-1", "target": spec.node_id, "sourceHandle": "right", "targetHandle": "left"},
{"source": spec.node_id, "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
]
)
return {"nodes": nodes, "edges": edges}
def _validate_local(wf: Dict[str, Any]) -> None:
from app.services.workflow_validator import validate_workflow
r = validate_workflow(wf.get("nodes") or [], wf.get("edges") or [])
if not r.get("valid"):
errs = r.get("errors") or []
raise ValueError("工作流校验失败: " + "; ".join(errs))
def _find_agent_id(h: Dict[str, str], name: str) -> Optional[str]:
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 80}, headers=h, timeout=45)
if r.status_code != 200:
return None
for a in r.json() or []:
if a.get("name") == name:
return a.get("id")
return None
def upsert_agent(h: Dict[str, str], spec: EduAgentSpec) -> Tuple[bool, str]:
wf = build_workflow(spec)
_validate_local(wf)
desc = spec.description + f" 默认模型 {PROVIDER}/{MODEL}"
existing = _find_agent_id(h, spec.name)
if existing:
ur = requests.put(
f"{BASE}/api/v1/agents/{existing}",
headers=h,
json={
"description": desc,
"workflow_config": wf,
"budget_config": BUDGET_BASE,
},
timeout=120,
)
if ur.status_code != 200:
return False, f"更新失败 {spec.name}: {ur.status_code} {ur.text[:500]}"
return True, f"更新 {spec.name} id={existing}"
cr = requests.post(
f"{BASE}/api/v1/agents",
headers=h,
json={
"name": spec.name,
"description": desc,
"workflow_config": wf,
"budget_config": BUDGET_BASE,
},
timeout=120,
)
if cr.status_code != 201:
return False, f"创建失败 {spec.name}: {cr.status_code} {cr.text[:500]}"
aid = cr.json()["id"]
return True, f"创建 {spec.name} id={aid}"
def main() -> int:
specs = AGENTS
if ONLY:
specs = [s for s in AGENTS if s.name == ONLY]
if not specs:
print(f"未找到名称完全匹配的 Agent 规格: {ONLY}", file=sys.stderr)
print("可选名称:", "".join(s.name for s in AGENTS), file=sys.stderr)
return 1
r = requests.post(
f"{BASE}/api/v1/auth/login",
data={"username": USER, "password": PWD},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=15,
)
if r.status_code != 200:
print("登录失败:", r.status_code, r.text[:500], file=sys.stderr)
return 1
token = r.json().get("access_token")
if not token:
print("无 access_token", file=sys.stderr)
return 1
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
ok_n = 0
results: List[Dict[str, str]] = []
for spec in specs:
try:
ok, msg = upsert_agent(h, spec)
print(msg)
results.append({"name": spec.name, "ok": str(ok), "message": msg})
if ok:
ok_n += 1
except ValueError as e:
print(spec.name, "校验失败:", e, file=sys.stderr)
results.append({"name": spec.name, "ok": "false", "message": str(e)})
print(json.dumps({"base": BASE, "succeeded": ok_n, "total": len(specs), "details": results}, ensure_ascii=False))
return 0 if ok_n == len(specs) else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,368 @@
#!/usr/bin/env python3
"""
批量创建或更新「政务 / 公共服务(办事指引)」与「媒体 / 市场 / 运营」场景 AgentStart → LLM → End
用法:
cd backend
.\\venv\\Scripts\\python.exe scripts/create_gov_media_agents_batch.py
只处理其中一个(名称需与内置列表完全一致):
set CROSS_ONLY_AGENT=政务办事指引助手
.\\venv\\Scripts\\python.exe scripts/create_gov_media_agents_batch.py
环境变量:
PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD
CROSS_LLM_PROVIDER / CROSS_LLM_MODEL / CROSS_LLM_TIMEOUT可选否则用 ENTERPRISE_*
CROSS_ONLY_AGENT可选
"""
from __future__ import annotations
import json
import os
import sys
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
import requests
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BACKEND_DIR not in sys.path:
sys.path.insert(0, BACKEND_DIR)
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
USER = os.getenv("PLATFORM_USERNAME", "admin")
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
PROVIDER = os.getenv(
"CROSS_LLM_PROVIDER",
os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek"),
)
MODEL = os.getenv(
"CROSS_LLM_MODEL",
os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat"),
)
REQ_TIMEOUT = max(
30,
int(
os.getenv(
"CROSS_LLM_TIMEOUT",
os.getenv("ENTERPRISE_LLM_TIMEOUT", "180"),
)
),
)
ONLY = (os.getenv("CROSS_ONLY_AGENT") or "").strip()
BUDGET_BASE = {
"max_steps": 80,
"max_llm_invocations": 6,
"max_tool_calls": 24,
}
TOOLS_GOV = ("file_read", "text_analyze", "datetime", "json_process")
TOOLS_MEDIA = ("file_read", "text_analyze", "json_process", "datetime")
TOOLS_MEDIA_HTTP = ("http_request", "text_analyze", "json_process", "datetime")
@dataclass(frozen=True)
class CrossAgentSpec:
name: str
label: str
node_id: str
description: str
prompt: str
tools: Tuple[str, ...]
temperature: float = 0.35
max_tool_iterations: int = 12
AGENTS: List[CrossAgentSpec] = [
CrossAgentSpec(
name="政务办事指引助手",
label="办事指引",
node_id="llm-gov-guide",
description=(
"面向公众办事咨询:流程步骤、所需材料清单、常见补正与办理渠道说明;"
"可用 json_process 输出可打印 checklist政策数字与条文以用户提供的官方材料或官网为准不编造。"
),
prompt="""你是「政务办事指引助手」,帮助用户理解**办事流程与材料准备**,语气清晰、中立、便民。
【能力】
1. 根据用户描述的办事类型(如落户、社保、执照、出入境等),整理:**办理条件要点、步骤顺序、材料清单、常见补正、线上/线下渠道提示**。
2. 优先使用 **json_process** 输出结构化清单(如 `items[]`:材料名称、原件/复印件、份数、备注),便于复制打印。
3. 需要「当前日期/工作日表述」时用 **datetime**;对用户上传的通知、办事指南 PDF/图片,**先 file_read** 再归纳,勿编造未读到的条款。
4. 对用户粘贴的长文,可用 **text_analyze** 做要点提取后再组织成步骤。
【边界(必须遵守)】
- **不编造**法律法规条文、收费标准、办理时限数字;若用户未提供官方依据,明确写「请以属地政务服务网/窗口最新公示为准」,并列出用户应核对的官方渠道类型。
- 不做**个案最终裁决**;涉及敏感资格认定提示以主管部门解释为准。
- 不索取不必要的身份证号等隐私;引导用户通过正规渠道提交。
【输出】
- 中文;先给「结论摘要」,再给分步与清单;关键处标注「需用户向 XX 部门核实」。
""",
tools=TOOLS_GOV,
temperature=0.3,
),
CrossAgentSpec(
name="政务表格填写说明助手",
label="表格说明",
node_id="llm-gov-form",
description=(
"解读政务与公共服务类表格字段含义、填写格式与常见错误;支持上传空白表或示例用 file_read"
"不替用户伪造信息,不保证与各地最新版式完全一致。"
),
prompt="""你是「政务表格填写说明助手」。
【能力】
1. 用户上传表格扫描件、PDF、Word或描述表名时**先 file_read**(若有路径/附件)识别字段名,逐字段说明:**含义、填写格式、示例(虚构示例需标注「示例」)、常见漏填**。
2. 用 **json_process** 输出「字段说明表」field, meaning, format, example, warning
3. 对日期、编号格式等可用 **datetime** 说明一般写法(具体规则仍以表格脚注为准)。
【边界】
- **不编造**某地区未提供的表格版本;若无法从材料识别字段,请用户补充表头或截图。
- **不指导伪造**证明材料、不代填真实个人信息。
- 与政策冲突时以官方表格备注与办事指南为准。
【输出】
- 分字段编号;末尾给「提交前自检 5 条」。
""",
tools=TOOLS_GOV,
temperature=0.28,
),
CrossAgentSpec(
name="市场多版本文案助手",
label="多版文案",
node_id="llm-mkt-copy",
description=(
"同一卖点下的多风格文案:朋友圈/短视频口播/电商详情要点等;"
"text_analyze 拆解用户给的旧稿或竞品片段json_process 输出多版本结构化稿;不虚假承诺。"
),
prompt="""你是「市场多版本文案助手」,服务市场与运营同学。
【能力】
1. 基于用户提供的**产品/活动信息**(可上传 Brief 或旧稿,**先 file_read**产出多版本短文案如「朋友圈2 条」「短视频口播 30s 提纲」「电商卖点 3 条」等。
2. 对用户粘贴的长 briefing用 **text_analyze** 抽核心卖点、受众、禁忌后再写。
3. 用 **json_process** 输出 JSON`versions[]` 含 channel、tone、copy、cta、字符数估计。
【边界】
- **不虚假承诺**疗效、收益、官方背书;涉及广告法敏感词(最、第一、治愈等)给替代表述或提示合规审核。
- 不确定的促销规则、价格以运营确认为准。
【输出】
- 中文为主;可附英文标题如需出海;每版标注适用渠道与语气。
""",
tools=TOOLS_MEDIA,
temperature=0.45,
),
CrossAgentSpec(
name="投放素材与Brief拆解助手",
label="Brief拆解",
node_id="llm-mkt-brief",
description=(
"拆解市场 Brief目标、受众、渠道、KPI、创意方向、交付物清单"
"text_analyze 与 json_process 结构化输出;可读取上传的 Brief 文档。"
),
prompt="""你是「投放素材与 Brief 拆解助手」。
【能力】
1. 用户粘贴或上传 Brief 时,**先 file_read**(若有),输出:**目标(认知/转化)、受众画像、主信息与副信息、渠道与版位、预算/周期若给定、KPI、创意禁忌、交付物列表尺寸/时长/格式)**。
2. 用 **text_analyze** 识别 Brief 中的矛盾或缺失项,列出需客户/内部确认的 **澄清问题**一次35 个)。
3. 用 **json_process** 生成标准拆解单 JSON`goal`, `audience`, `messages`, `channels`, `deliverables`, `risks`, `open_questions`。
【原则】
- 不编造未出现在 Brief 中的数字与承诺;缺失项标「待补充」。
- 涉及法务/代言人/竞品对比等,提示走内部合规流程。
【输出】
- 先一页「执行摘要」,再结构化表格或 JSON 块。
""",
tools=TOOLS_MEDIA,
temperature=0.35,
),
CrossAgentSpec(
name="热点资讯摘要助手",
label="热点摘要",
node_id="llm-mkt-news",
description=(
"对用户给出的公开 URL 使用 http_request 拉取可访问内容后做摘要与要点;"
"结合 text_analyze失败时如实说明不编造来源中不存在的引文。"
),
prompt="""你是「热点资讯摘要助手」,用于**公开网页/接口返回文本**的摘要(需用户或模型通过工具获得原文)。
【能力】
1. 当用户提供 **可访问的 http(s) URL** 时,使用 **http_request** 获取内容(遵守平台与工具限制);若失败(超时、非 200、需登录如实说明不要编造正文。
- **不要**用门户/频道**首页**做摘要HTML 体量极大,工具会截断且噪声多);请引导用户改为**具体文章页**链接。若用户只给首页,说明限制并请其提供文章 URL。
- 单篇长文若需更多正文可在工具参数中适当增大 `max_body_chars`(仍可能截断,以工具返回的 `truncation_note`、`body_truncated` 为准)。
2. 对工具返回的正文用 **text_analyze** 提取:核心事实、各方观点、时间线、对品牌/活动的**慎用关联**提示。
3. 用 **json_process** 输出:`title_guess`, `bullets[]`, `sources`(仅用户提供的 URL、`limitations`(如仅摘要可见段落)。
4. 用 **datetime** 标注摘要基准日或「截至今日」表述。
【边界】
- **不编造**引文、数据、发言人言论;原文没有则写「原文未提及」。
- 付费墙、登录墙内容可能无法抓取,提示用户粘贴正文或换公开来源。
- 不做投资建议;涉政敏感解读保持克制,以信息整理为主。
【输出】
- 中文先36 条 bullet再给「可进一步检索的关键词」。
""",
tools=TOOLS_MEDIA_HTTP,
temperature=0.35,
max_tool_iterations=14,
),
]
def _sanitize_edges(edges: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
seen: set = set()
out: List[Dict[str, Any]] = []
for e in edges or []:
s, t = e.get("source"), e.get("target")
if not s or not t or s == t:
continue
key = (s, t, e.get("sourceHandle") or "")
if key in seen:
continue
seen.add(key)
ne = dict(e)
if not ne.get("targetHandle"):
ne["targetHandle"] = "left"
if not ne.get("id"):
sh = ne.get("sourceHandle") or "r"
ne["id"] = f"e_{s}_{t}_{sh}"
out.append(ne)
return out
def build_workflow(spec: CrossAgentSpec) -> Dict[str, Any]:
llm_pos: Tuple[int, int] = (380, 220)
tools = list(spec.tools)
nodes: List[Dict[str, Any]] = [
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 220}, "data": {"label": "开始"}},
{
"id": spec.node_id,
"type": "llm",
"position": {"x": llm_pos[0], "y": llm_pos[1]},
"data": {
"label": spec.label,
"prompt": spec.prompt,
"provider": PROVIDER,
"model": MODEL,
"temperature": spec.temperature,
"request_timeout": REQ_TIMEOUT,
"enable_tools": True,
"tools": tools,
"selected_tools": tools,
"max_tool_iterations": spec.max_tool_iterations,
},
},
{"id": "end-1", "type": "end", "position": {"x": llm_pos[0] + 260, "y": 220}, "data": {"label": "结束"}},
]
edges = _sanitize_edges(
[
{"source": "start-1", "target": spec.node_id, "sourceHandle": "right", "targetHandle": "left"},
{"source": spec.node_id, "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
]
)
return {"nodes": nodes, "edges": edges}
def _validate_local(wf: Dict[str, Any]) -> None:
from app.services.workflow_validator import validate_workflow
r = validate_workflow(wf.get("nodes") or [], wf.get("edges") or [])
if not r.get("valid"):
errs = r.get("errors") or []
raise ValueError("工作流校验失败: " + "; ".join(errs))
def _find_agent_id(h: Dict[str, str], name: str) -> Optional[str]:
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 80}, headers=h, timeout=45)
if r.status_code != 200:
return None
for a in r.json() or []:
if a.get("name") == name:
return a.get("id")
return None
def upsert_agent(h: Dict[str, str], spec: CrossAgentSpec) -> Tuple[bool, str]:
wf = build_workflow(spec)
_validate_local(wf)
desc = spec.description + f" 默认模型 {PROVIDER}/{MODEL}"
existing = _find_agent_id(h, spec.name)
if existing:
ur = requests.put(
f"{BASE}/api/v1/agents/{existing}",
headers=h,
json={
"description": desc,
"workflow_config": wf,
"budget_config": BUDGET_BASE,
},
timeout=120,
)
if ur.status_code != 200:
return False, f"更新失败 {spec.name}: {ur.status_code} {ur.text[:500]}"
return True, f"更新 {spec.name} id={existing}"
cr = requests.post(
f"{BASE}/api/v1/agents",
headers=h,
json={
"name": spec.name,
"description": desc,
"workflow_config": wf,
"budget_config": BUDGET_BASE,
},
timeout=120,
)
if cr.status_code != 201:
return False, f"创建失败 {spec.name}: {cr.status_code} {cr.text[:500]}"
aid = cr.json()["id"]
return True, f"创建 {spec.name} id={aid}"
def main() -> int:
specs = AGENTS
if ONLY:
specs = [s for s in AGENTS if s.name == ONLY]
if not specs:
print(f"未找到名称完全匹配的 Agent 规格: {ONLY}", file=sys.stderr)
print("可选名称:", "".join(s.name for s in AGENTS), file=sys.stderr)
return 1
r = requests.post(
f"{BASE}/api/v1/auth/login",
data={"username": USER, "password": PWD},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=15,
)
if r.status_code != 200:
print("登录失败:", r.status_code, r.text[:500], file=sys.stderr)
return 1
token = r.json().get("access_token")
if not token:
print("无 access_token", file=sys.stderr)
return 1
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
ok_n = 0
results: List[Dict[str, str]] = []
for spec in specs:
try:
ok, msg = upsert_agent(h, spec)
print(msg)
results.append({"name": spec.name, "ok": str(ok), "message": msg})
if ok:
ok_n += 1
except ValueError as e:
print(spec.name, "校验失败:", e, file=sys.stderr)
results.append({"name": spec.name, "ok": "false", "message": str(e)})
print(json.dumps({"base": BASE, "succeeded": ok_n, "total": len(specs), "details": results}, ensure_ascii=False))
return 0 if ok_n == len(specs) else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,3 +1,18 @@
# Windows 启动和停止用法(已合并)
本文件已停止维护,请只看以下唯一文档:
- `D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md`
说明:历史内容已合并至上方文档,后续不再在本文件更新。
# Windows 启动和停止用法(已合并)
本文件已停止维护。
请改看唯一文档:`(红头)Windows服务器启动与重启唯一指南.md`
路径:`D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md`
# AIAgent Windows 启动和停止用法
## 一键启动