From cadeb2dc323815e269d496f59a56e15c131dd2f3 Mon Sep 17 00:00:00 2001 From: renjianbo <18691577328@163.com> Date: Thu, 30 Apr 2026 00:10:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=83=AD=E7=82=B9?= =?UTF-8?q?=E6=91=98=E8=A6=81=E8=B6=85=E9=95=BF=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E5=B9=B6=E7=BB=9F=E4=B8=80=20Windows=20=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 http_request 增加响应体截断与头部精简,避免门户首页触发 LLM 上下文超限;同时新增政务/媒体及教育批量 Agent 脚本,并将 Windows 启停说明合并为唯一指南,补充本次超时故障复盘与标准重启流程。 Made-with: Cursor --- (红头)Windows服务器启动与重启唯一指南.md | 195 ++++++++ (红头)上传图片和识别的实现文档.md | 137 ++++++ (红头)前后端服务器启动和停止.md | 105 +---- (红头)服务器启动的注意事项.md | 81 +--- Windows启动指南.md | 15 + backend/app/core/config.py | 3 + backend/app/services/builtin_tools.py | 99 +++- .../scripts/create_education_agents_batch.py | 434 ++++++++++++++++++ .../scripts/create_gov_media_agents_batch.py | 368 +++++++++++++++ windows启动和停止用法.md | 15 + 10 files changed, 1265 insertions(+), 187 deletions(-) create mode 100644 (红头)Windows服务器启动与重启唯一指南.md create mode 100644 (红头)上传图片和识别的实现文档.md create mode 100644 backend/scripts/create_education_agents_batch.py create mode 100644 backend/scripts/create_gov_media_agents_batch.py diff --git a/(红头)Windows服务器启动与重启唯一指南.md b/(红头)Windows服务器启动与重启唯一指南.md new file mode 100644 index 0000000..3ea2887 --- /dev/null +++ b/(红头)Windows服务器启动与重启唯一指南.md @@ -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 启动/重启内容只维护本文件。 +- 其他旧文档仅保留跳转,不再写重复步骤。 +- 修改启动逻辑后,先更新本文件再通知团队。 + diff --git a/(红头)上传图片和识别的实现文档.md b/(红头)上传图片和识别的实现文档.md new file mode 100644 index 0000000..9d184b0 --- /dev/null +++ b/(红头)上传图片和识别的实现文档.md @@ -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`) + +| 能力 | 说明 | +|------|------| +| 选择文件 | 隐藏 ``,回形针触发;`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 等运维说明 | + +--- + +文档版本:与当前仓库实现一致时可随代码迭代更新本文。 diff --git a/(红头)前后端服务器启动和停止.md b/(红头)前后端服务器启动和停止.md index 1831463..996e0b2 100644 --- a/(红头)前后端服务器启动和停止.md +++ b/(红头)前后端服务器启动和停止.md @@ -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`(或对应服务名)查看报错并排查。 | diff --git a/(红头)服务器启动的注意事项.md b/(红头)服务器启动的注意事项.md index 8786596..747a179 100644 --- a/(红头)服务器启动的注意事项.md +++ b/(红头)服务器启动的注意事项.md @@ -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」便于以后排查。 diff --git a/Windows启动指南.md b/Windows启动指南.md index c242d5e..01eb2fc 100644 --- a/Windows启动指南.md +++ b/Windows启动指南.md @@ -1,3 +1,18 @@ +# Windows 启动指南(已合并) + +本文件已停止维护,请只看以下唯一文档: + +- `D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md` + +说明:历史内容已合并至上方文档,后续不再在本文件更新。 + +# Windows 启动指南(已合并) + +本文件已停止维护。 +请改看唯一文档:`(红头)Windows服务器启动与重启唯一指南.md` + +路径:`D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md` + # Windows 本地启动指南 ## 前置要求 diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 979d3f4..72630ce 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -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 + # 图片 OCR(file_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 则自动使用 diff --git a/backend/app/services/builtin_tools.py b/backend/app/services/builtin_tools.py index fa06f48..c25b93b 100644 --- a/backend/app/services/builtin_tools.py +++ b/backend/app/services/builtin_tools.py @@ -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"] } diff --git a/backend/scripts/create_education_agents_batch.py b/backend/scripts/create_education_agents_batch.py new file mode 100644 index 0000000..166e32b --- /dev/null +++ b/backend/scripts/create_education_agents_batch.py @@ -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. 给出 **1~2 道同类变式** 的方向描述或自检问题(不必编造具体数字题除非用户需要且合理)。 +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 表格输出计划时保持可打印、可勾选。 +- 不保证分数;提醒睡眠与劳逸结合。 + +【原则】 +- 若信息不足,先问 1~2 个关键问题再出计划。 +""", + 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()) diff --git a/backend/scripts/create_gov_media_agents_batch.py b/backend/scripts/create_gov_media_agents_batch.py new file mode 100644 index 0000000..ef5d004 --- /dev/null +++ b/backend/scripts/create_gov_media_agents_batch.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python3 +""" +批量创建或更新「政务 / 公共服务(办事指引)」与「媒体 / 市场 / 运营」场景 Agent(Start → 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 中的矛盾或缺失项,列出需客户/内部确认的 **澄清问题**(一次3~5 个)。 +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** 标注摘要基准日或「截至今日」表述。 + +【边界】 +- **不编造**引文、数据、发言人言论;原文没有则写「原文未提及」。 +- 付费墙、登录墙内容可能无法抓取,提示用户粘贴正文或换公开来源。 +- 不做投资建议;涉政敏感解读保持克制,以信息整理为主。 + +【输出】 +- 中文;先3~6 条 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()) diff --git a/windows启动和停止用法.md b/windows启动和停止用法.md index f4ff167..7dfdb2a 100644 --- a/windows启动和停止用法.md +++ b/windows启动和停止用法.md @@ -1,3 +1,18 @@ +# Windows 启动和停止用法(已合并) + +本文件已停止维护,请只看以下唯一文档: + +- `D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md` + +说明:历史内容已合并至上方文档,后续不再在本文件更新。 + +# Windows 启动和停止用法(已合并) + +本文件已停止维护。 +请改看唯一文档:`(红头)Windows服务器启动与重启唯一指南.md` + +路径:`D:\aaa\aiagent\(红头)Windows服务器启动与重启唯一指南.md` + # AIAgent Windows 启动和停止用法 ## 一键启动