feat: add AI学习助手 agent (KG+RAG ideal) and renshenguo feishu bot
- Add AI学习助手 agent creation script with all 39 tools, 3-layer KG+RAG memory - Add renshenguo (人参果) feishu bot integration (app_service + ws_handler) - Register renshenguo WS client in main.py startup - Add RENSHENGUO_APP_ID / RENSHENGUO_APP_SECRET / RENSHENGUO_AGENT_ID config - Reorganize docs from root into docs/ subdirectories - Move startup scripts to scripts/startup/ - Various backend optimizations and tool improvements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
485
scripts/create_issues.py
Normal file
485
scripts/create_issues.py
Normal file
@@ -0,0 +1,485 @@
|
||||
"""批量创建 Gitea 工单 — 项目下一步需求清单"""
|
||||
import requests
|
||||
import json
|
||||
|
||||
TOKEN = "fbc9ee7f96635793f4844187eac5c0e573480721"
|
||||
BASE = "http://101.43.95.130:3001/api/v1/repos/admin/aiagent/issues"
|
||||
|
||||
HEADERS = {
|
||||
"Authorization": f"token {TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
ISSUES = [
|
||||
# ===== Phase 4: 容错与共享 =====
|
||||
{
|
||||
"title": "[Phase 4.1] 降级/回退链 — 模型/Agent 失败自动切换备用方案",
|
||||
"body": """## 目标
|
||||
主模型调用失败自动切换 fallback_llm,Agent 执行失败自动切换 fallback_agent。
|
||||
|
||||
## 涉及改动
|
||||
- AgentLLMConfig 添加 fallback_llm 字段
|
||||
- Agent 节点 data 添加 fallback_agent 字段
|
||||
- core.py LLM 调用失败后切换 fallback
|
||||
- workflow_engine.py Agent 节点失败后切换 fallback_agent
|
||||
|
||||
## 验收
|
||||
- 主模型不可用 → 自动切备用模型
|
||||
- Agent 执行失败 → 自动切备选 Agent
|
||||
- 所有备用方案都失败 → 抛出明确错误
|
||||
|
||||
## 预计工作量
|
||||
4-6 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[Phase 4.2] Agent 间知识共享 — 打破记忆隔离",
|
||||
"body": """## 目标
|
||||
打破 Agent 记忆隔离,Agent A 学到的知识 Agent B 也能检索使用。
|
||||
|
||||
## 方案
|
||||
- 新增 GlobalKnowledge 模型(content, embedding, source_agent_id, tags)
|
||||
- Agent 执行完成后自动提取关键知识写入全局知识池
|
||||
- Agent 初始化时从全局知识池检索相关知识
|
||||
- 自主进化创建的 Agent 自动继承创建者的知识
|
||||
|
||||
## 涉及文件
|
||||
- models/agent.py — 新增 GlobalKnowledge 模型
|
||||
- agent_runtime/memory.py — initialize() 添加全局知识加载
|
||||
- agent_runtime/core.py — 执行完毕提取知识写入全局
|
||||
|
||||
## 预计工作量
|
||||
8-12 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[Phase 4.3] Agent 异步执行 — 填空 execute_agent_task",
|
||||
"body": """## 问题
|
||||
backend/app/tasks/agent_tasks.py 中 execute_agent_task 当前是空壳占位符(返回 pending),定时调度无法真正异步执行 Agent。
|
||||
|
||||
## 方案
|
||||
1. 从 DB 加载 Agent 配置
|
||||
2. 构造 AgentRuntime
|
||||
3. 异步执行并更新 execution 记录
|
||||
4. 完成通知(飞书/WebSocket/站内通知)
|
||||
|
||||
## 涉及文件
|
||||
- tasks/agent_tasks.py — 实现 execute_agent_task
|
||||
- tasks/scheduler_tasks.py — 调度触发真正的异步执行
|
||||
|
||||
## 预计工作量
|
||||
4-6 小时""",
|
||||
},
|
||||
|
||||
# ===== 监控告警前端 =====
|
||||
{
|
||||
"title": "[监控] 系统监控面板 — CPU/内存/磁盘 + 执行统计",
|
||||
"body": """## 当前状态
|
||||
后端 API 已完成:GET /api/v1/monitoring/overview、GET /api/v1/monitoring/statistics
|
||||
前端界面缺失。
|
||||
|
||||
## 需要完成
|
||||
- 系统资源监控(CPU、内存、磁盘)
|
||||
- 执行统计图表(成功率、执行时间、错误率)
|
||||
- 实时执行状态看板
|
||||
|
||||
## 预计工作量
|
||||
4-6 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[监控] 告警规则管理页面",
|
||||
"body": """## 当前状态
|
||||
后端 API 已完成:GET/POST /api/v1/alert-rules、PUT/DELETE /api/v1/alert-rules/{id}
|
||||
前端界面缺失。
|
||||
|
||||
## 需要完成
|
||||
- 告警规则列表
|
||||
- 告警规则创建/编辑表单(条件、阈值、通知方式)
|
||||
- 启用/禁用开关
|
||||
|
||||
## 预计工作量
|
||||
4-6 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[监控] 告警日志查看页面",
|
||||
"body": """## 当前状态
|
||||
后端 API 已完成:GET /api/v1/alert-rules/{id}/logs
|
||||
前端界面缺失。
|
||||
|
||||
## 需要完成
|
||||
- 告警历史列表(时间、规则、级别、状态)
|
||||
- 告警详情查看 + 筛选分页
|
||||
- 告警通知配置(邮件、Webhook、飞书)
|
||||
|
||||
## 预计工作量
|
||||
4-6 小时""",
|
||||
},
|
||||
|
||||
# ===== DevOps / 生产就绪 =====
|
||||
{
|
||||
"title": "[DevOps] Docker 生产环境配置 + 多环境管理",
|
||||
"body": """## 目标
|
||||
从开发环境升级到生产就绪的容器化部署。
|
||||
|
||||
## 需要完成
|
||||
- Docker Compose 生产配置(资源限制、健康检查、重启策略)
|
||||
- 多环境配置管理(dev / staging / prod)
|
||||
- 配置文件加密(敏感信息不提交 Git)
|
||||
- Nginx 反向代理 + HTTPS
|
||||
|
||||
## 预计工作量
|
||||
8-12 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[DevOps] Prometheus + Grafana 监控集成",
|
||||
"body": """## 目标
|
||||
建立生产级指标收集和可视化。
|
||||
|
||||
## 需要完成
|
||||
- Prometheus 指标收集(业务指标:执行数/成功率/耗时 + 系统指标:CPU/内存/网络)
|
||||
- Grafana 仪表板(系统监控 + 业务监控)
|
||||
- FastAPI metrics 端点暴露
|
||||
- Celery worker 指标收集
|
||||
|
||||
## 预计工作量
|
||||
8-12 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[DevOps] ELK 日志聚合系统",
|
||||
"body": """## 目标
|
||||
集中化日志管理,替代本地 backend.log 文件。
|
||||
|
||||
## 需要完成
|
||||
- Elasticsearch + Logstash + Kibana 容器化部署
|
||||
- FastAPI 日志接入 Logstash
|
||||
- Celery 任务日志接入
|
||||
- 日志查询和分析界面(Kibana)
|
||||
|
||||
## 预计工作量
|
||||
8-12 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[DevOps] CI/CD 流水线 — GitHub Actions",
|
||||
"body": """## 目标
|
||||
自动化测试→构建→部署流程。
|
||||
|
||||
## 需要完成
|
||||
- 自动化测试流程(pytest + vitest)
|
||||
- 代码质量检查(ESLint + Pylint + Black)
|
||||
- 自动化 Docker 镜像构建
|
||||
- 自动化部署到 staging 环境
|
||||
|
||||
## 预计工作量
|
||||
10-15 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[安全] API 限流保护",
|
||||
"body": """## 目标
|
||||
防止 API 滥用,保护后端服务。
|
||||
|
||||
## 需要完成
|
||||
- 全局限流(每 IP 每分钟 N 次)
|
||||
- 用户级别限流(按 token/user_id)
|
||||
- Agent 对话接口更严格限流
|
||||
- 限流响应头(X-RateLimit-*)
|
||||
|
||||
## 技术方案
|
||||
可使用 slowapi(FastAPI 限流库)或 Redis + 滑动窗口。
|
||||
|
||||
## 预计工作量
|
||||
4-6 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[安全] 密钥管理 — 敏感信息加密存储",
|
||||
"body": """## 问题
|
||||
当前 API Key、数据库密码等敏感信息明文存储在 .env 文件中。
|
||||
|
||||
## 方案
|
||||
- 使用环境变量 + Docker secrets 管理生产密钥
|
||||
- 开发环境使用 .env.local(加入 .gitignore)
|
||||
- 数据库存储的 API Key 使用 AES 加密
|
||||
|
||||
## 预计工作量
|
||||
4-6 小时""",
|
||||
},
|
||||
|
||||
# ===== 用户体验 =====
|
||||
{
|
||||
"title": "[UX] 工作流编辑器优化 — 自动布局 + 节点搜索",
|
||||
"body": """## 目标
|
||||
提升复杂工作流的编辑效率。
|
||||
|
||||
## 需要完成
|
||||
- 节点自动布局(一键美化排列)
|
||||
- 节点搜索/筛选(按名称、类型快速定位)
|
||||
- 画布小地图(minimap)导航
|
||||
- 节点分组/折叠功能
|
||||
|
||||
## 预计工作量
|
||||
8-10 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[UX] Agent 快速测试功能",
|
||||
"body": """## 目标
|
||||
在 Agent 配置页面直接测试,不需跳转到对话页。
|
||||
|
||||
## 需要完成
|
||||
- Agent 配置页嵌入测试对话面板
|
||||
- 测试结果实时显示(思考链+工具调用+最终回答)
|
||||
- 测试历史记录
|
||||
- 快速切换模型对比测试
|
||||
|
||||
## 预计工作量
|
||||
6-8 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[UX] Agent 使用统计和分析",
|
||||
"body": """## 目标
|
||||
量化每个 Agent 的使用情况。
|
||||
|
||||
## 需要完成
|
||||
- 每个 Agent 调用次数、成功率、平均耗时
|
||||
- Token 消耗统计
|
||||
- 工具调用频次排行
|
||||
- 按时间维度(日/周/月)展示趋势图
|
||||
|
||||
## 预计工作量
|
||||
6-8 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[UX] 移动端适配",
|
||||
"body": """## 目标
|
||||
核心功能支持移动端访问。
|
||||
|
||||
## 需要完成
|
||||
- 响应式布局优化
|
||||
- 移动端 Agent 对话页面
|
||||
- 移动端执行状态查看(只读)
|
||||
- 移动端工作流查看(只读)
|
||||
|
||||
## 预计工作量
|
||||
15-20 小时""",
|
||||
},
|
||||
|
||||
# ===== 高级功能 =====
|
||||
{
|
||||
"title": "[高级] 主控台/应用商店 — 面向业务用户的一站式入口",
|
||||
"body": """## 目标
|
||||
让业务用户选模板→填参数→执行→看结果,不需了解工作流细节。
|
||||
|
||||
## 需要完成
|
||||
- MainConsole 主入口页面
|
||||
- TemplateMarket 模板市场(浏览、搜索、评分、收藏)
|
||||
- ExecutionBoard 父子执行链看板
|
||||
- 一键从模板创建并执行
|
||||
|
||||
## 预计工作量
|
||||
15-20 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[高级] 统一 DSL — 场景可编程输入",
|
||||
"body": """## 目标
|
||||
让不同模板复用统一的输入契约,降低场景迁移成本。
|
||||
|
||||
## 方案
|
||||
- 新增 scenario_dsl.py 定义标准输入:目标、约束、产物、验收标准
|
||||
- 工作流入口处做 DSL 校验与标准化映射
|
||||
- 所有模板脚本迁移到 DSL 输入
|
||||
|
||||
## 预计工作量
|
||||
8-12 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[高级] 成本预算治理 — 按 Agent 设置预算阈值",
|
||||
"body": """## 目标
|
||||
防止 Agent 执行失控,支持组织级运营。
|
||||
|
||||
## 方案
|
||||
- 增加预算控制:max_steps、工具调用上限、token 估算上限
|
||||
- 新增预算配置模型,按租户/项目/Agent 维度配置
|
||||
- 预算超限告警与熔断
|
||||
|
||||
## 预计工作量
|
||||
8-12 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[高级] 插件系统 — 第三方自定义节点 + 插件市场",
|
||||
"body": """## 目标
|
||||
让外部开发者可以开发自定义节点插件,扩展平台能力。
|
||||
|
||||
## 方案
|
||||
- 插件注册机制(插件清单、入口文件、依赖声明)
|
||||
- 自定义节点插件开发框架(SDK + 脚手架)
|
||||
- 插件市场(上传、下载、评分、版本管理)
|
||||
- 插件安全沙箱
|
||||
|
||||
## 预计工作量
|
||||
30-40 小时(大型功能,可分期交付)""",
|
||||
},
|
||||
{
|
||||
"title": "[高级] 多租户支持 — SaaS 化",
|
||||
"body": """## 目标
|
||||
支持多个组织/团队独立使用平台,数据隔离。
|
||||
|
||||
## 方案
|
||||
- 租户模型和 API
|
||||
- 数据隔离(数据库级 / Schema 级 / 行级)
|
||||
- 资源配额管理
|
||||
- 租户管理界面
|
||||
|
||||
## 预计工作量
|
||||
20-30 小时(大型功能)""",
|
||||
},
|
||||
|
||||
# ===== 质量保障 =====
|
||||
{
|
||||
"title": "[测试] 单元测试覆盖率提升至 80%+",
|
||||
"body": """## 当前状态
|
||||
测试框架已搭建(pytest + vitest),但覆盖率较低。
|
||||
|
||||
## 需要完成
|
||||
- 后端核心模块覆盖率提升(workflow_engine、agent_runtime、core)
|
||||
- 前端核心组件测试(AgentChat、WorkflowDesigner)
|
||||
- API 端点测试完善
|
||||
|
||||
## 预计工作量
|
||||
20-30 小时(持续投入)""",
|
||||
},
|
||||
{
|
||||
"title": "[测试] E2E 测试 — Playwright 核心流程",
|
||||
"body": """## 目标
|
||||
用 Playwright 覆盖核心用户流程的端到端测试。
|
||||
|
||||
## 测试场景
|
||||
- 用户登录→创建 Agent→对话→查看结果
|
||||
- 用户登录→创建工作流→添加节点→执行→查看日志
|
||||
- Agent 编排→多 Agent 协作→查看执行链
|
||||
|
||||
## 预计工作量
|
||||
10-15 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[测试] 性能压测 — Locust/k6",
|
||||
"body": """## 目标
|
||||
找到系统性能瓶颈,验证并发能力。
|
||||
|
||||
## 需要完成
|
||||
- Locust/k6 压测脚本编写
|
||||
- 压测场景:并发对话、并发工作流执行、并发 API 调用
|
||||
- 性能瓶颈分析 + 输出性能测试报告
|
||||
|
||||
## 预计工作量
|
||||
8-12 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[测试] 安全扫描 — OWASP ZAP + 依赖漏洞检查",
|
||||
"body": """## 目标
|
||||
发现并修复安全漏洞。
|
||||
|
||||
## 需要完成
|
||||
- OWASP ZAP 扫描 Web 应用
|
||||
- 依赖漏洞检查(pip-audit / npm audit)
|
||||
- Docker 镜像漏洞扫描
|
||||
- API 接口权限验证
|
||||
|
||||
## 预计工作量
|
||||
8-12 小时""",
|
||||
},
|
||||
|
||||
# ===== Agent 能力拓展 =====
|
||||
{
|
||||
"title": "[Agent] 多模态 Agent — 图片识别 + 语音输入",
|
||||
"body": """## 目标
|
||||
让 Agent 支持图片和语音输入,不只限于文本。
|
||||
|
||||
## 当前基础
|
||||
tessdata 目录已存在(OCR),图片上传功能已有。
|
||||
|
||||
## 需要完成
|
||||
- 图片理解 Agent(上传图片→OCR 提取文字→Agent 分析)
|
||||
- 语音转文字输入(Whisper API 集成)
|
||||
- 前端输入框添加图片/语音入口
|
||||
|
||||
## 预计工作量
|
||||
12-16 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[Agent] Agent 协作工作台 — 可视化多 Agent 编排",
|
||||
"body": """## 目标
|
||||
用可视化方式设计和执行多 Agent 协作流程。
|
||||
|
||||
## 需要完成
|
||||
- 协作流程图编辑器(拖拽 Agent 节点+连线)
|
||||
- 4 种协作模式可视化配置
|
||||
- 协作结果总览面板
|
||||
- 协作模板保存和复用
|
||||
|
||||
## 预计工作量
|
||||
12-16 小时""",
|
||||
},
|
||||
{
|
||||
"title": "[Agent] Agent 技能商店 — 公共市场共享",
|
||||
"body": """## 目标
|
||||
让用户发布和共享 Agent、工具、工作流模板。
|
||||
|
||||
## 需要完成
|
||||
- Agent 发布到公共市场(名称、描述、评分、下载数)
|
||||
- 工具和工作流模板发布
|
||||
- 评分和评论系统
|
||||
- 分类浏览和搜索
|
||||
|
||||
## 预计工作量
|
||||
15-20 小时""",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
print(f"准备创建 {len(ISSUES)} 个工单到 admin/aiagent ...\n")
|
||||
|
||||
created = []
|
||||
failed = []
|
||||
|
||||
for i, issue in enumerate(ISSUES):
|
||||
payload = {
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
"assignee": "admin",
|
||||
}
|
||||
try:
|
||||
r = requests.post(BASE, headers=HEADERS, json=payload, timeout=15)
|
||||
if r.status_code == 201:
|
||||
data = r.json()
|
||||
num = data["number"]
|
||||
print(f" [{i+1:2d}/{len(ISSUES)}] #{num} {issue['title']}")
|
||||
created.append(num)
|
||||
else:
|
||||
print(f" [{i+1:2d}/{len(ISSUES)}] FAIL ({r.status_code}): {issue['title']}")
|
||||
print(f" {r.text[:200]}")
|
||||
failed.append(issue["title"])
|
||||
except Exception as e:
|
||||
print(f" [{i+1:2d}/{len(ISSUES)}] ERROR: {e}")
|
||||
failed.append(issue["title"])
|
||||
|
||||
print(f"\n=== 完成 ===")
|
||||
print(f"成功: {len(created)} 个")
|
||||
print(f"失败: {len(failed)} 个")
|
||||
if created:
|
||||
print(f"工单编号: #{created[0]} ~ #{created[-1]}")
|
||||
if failed:
|
||||
print(f"失败列表: {failed}")
|
||||
|
||||
# 验证
|
||||
print("\n--- 验证:当前所有 Open 工单 ---")
|
||||
r = requests.get(
|
||||
f"{BASE}?state=open&limit=50",
|
||||
headers=HEADERS,
|
||||
timeout=10
|
||||
)
|
||||
if r.status_code == 200:
|
||||
issues = r.json()
|
||||
print(f"共 {len(issues)} 个开放工单:")
|
||||
for iss in issues:
|
||||
print(f" #{iss['number']} [{iss['state']}] {iss['title']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
38
scripts/startup/start.sh
Normal file
38
scripts/startup/start.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 低代码智能体平台启动脚本
|
||||
|
||||
echo "🚀 启动低代码智能体平台..."
|
||||
echo ""
|
||||
|
||||
# 检查Docker是否运行
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo "❌ Docker 未运行,请先启动 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查docker-compose是否可用
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "❌ docker-compose 未安装,请先安装 docker-compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 启动 Docker Compose 服务..."
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
echo ""
|
||||
echo "⏳ 等待服务启动..."
|
||||
sleep 5
|
||||
|
||||
echo ""
|
||||
echo "✅ 服务启动完成!"
|
||||
echo ""
|
||||
echo "📍 访问地址:"
|
||||
echo " - 前端: http://localhost:8038"
|
||||
echo " - 后端API: http://localhost:8037"
|
||||
echo " - API文档: http://localhost:8037/docs"
|
||||
echo " - 健康检查: http://localhost:8037/health"
|
||||
echo ""
|
||||
echo "📋 查看日志: docker-compose -f docker-compose.dev.yml logs -f"
|
||||
echo "🛑 停止服务: docker-compose -f docker-compose.dev.yml down"
|
||||
echo ""
|
||||
219
scripts/startup/start_windows.cmd
Normal file
219
scripts/startup/start_windows.cmd
Normal file
@@ -0,0 +1,219 @@
|
||||
@echo off
|
||||
echo ==============================================
|
||||
echo 低代码智能体平台 - Windows 启动脚本
|
||||
echo ==============================================
|
||||
echo.
|
||||
|
||||
REM 检查Python是否安装
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Python 未安装或未添加到系统PATH
|
||||
echo 请安装 Python 3.11+ 并确保在PATH中
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 检查Node.js是否安装
|
||||
node --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Node.js 未安装或未添加到系统PATH
|
||||
echo 请安装 Node.js 18+ 并确保在PATH中
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 检查pnpm是否安装
|
||||
pnpm --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ⚠️ pnpm 未安装,正在安装...
|
||||
npm install -g pnpm
|
||||
if errorlevel 1 (
|
||||
echo ❌ pnpm 安装失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo ✅ 环境检查通过
|
||||
echo.
|
||||
|
||||
REM 进入项目目录
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo ==============================================
|
||||
echo 1. Redis 检查
|
||||
echo ==============================================
|
||||
echo.
|
||||
|
||||
REM 检查Redis服务是否运行
|
||||
sc query Redis >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Redis 服务未运行
|
||||
echo.
|
||||
echo 请按以下步骤安装Redis:
|
||||
echo 1. 下载 Redis Windows 版本:https://github.com/microsoftarchive/redis/releases
|
||||
echo 2. 下载 Redis-x64-3.2.100.msi
|
||||
echo 3. 运行安装程序,按照默认设置安装
|
||||
echo 4. Redis 将作为 Windows 服务运行在 6379 端口
|
||||
echo.
|
||||
echo 安装完成后,请重新运行此脚本
|
||||
pause
|
||||
exit /b 1
|
||||
) else (
|
||||
echo ✅ Redis 服务正在运行
|
||||
)
|
||||
|
||||
echo ==============================================
|
||||
echo 2. 启动后端服务
|
||||
echo ==============================================
|
||||
echo.
|
||||
|
||||
REM 进入backend目录
|
||||
cd backend
|
||||
|
||||
REM 检查虚拟环境
|
||||
if not exist "venv\Scripts\activate" (
|
||||
echo ⚠️ 虚拟环境不存在,正在创建...
|
||||
python -m venv venv
|
||||
if errorlevel 1 (
|
||||
echo ❌ 虚拟环境创建失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo ✅ 虚拟环境检查通过
|
||||
|
||||
REM 激活虚拟环境并安装依赖
|
||||
call venv\Scripts\activate
|
||||
|
||||
echo 📦 检查Python依赖...
|
||||
pip list | findstr "fastapi" >nul
|
||||
if errorlevel 1 (
|
||||
echo ⚠️ 正在安装Python依赖...
|
||||
pip install -r requirements.txt
|
||||
if errorlevel 1 (
|
||||
echo ❌ Python依赖安装失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo ✅ Python依赖安装完成
|
||||
) else (
|
||||
echo ✅ Python依赖已安装
|
||||
)
|
||||
|
||||
echo.
|
||||
|
||||
echo 🔧 配置环境变量...
|
||||
if not exist ".env" (
|
||||
copy env.example .env >nul
|
||||
echo ⚠️ 已创建 .env 文件,请检查配置
|
||||
)
|
||||
|
||||
echo.
|
||||
|
||||
echo 🗄️ 运行数据库迁移...
|
||||
alembic upgrade head
|
||||
if errorlevel 1 (
|
||||
echo ⚠️ 数据库迁移失败,继续启动...
|
||||
)
|
||||
|
||||
echo.
|
||||
|
||||
echo 🌐 启动后端服务...
|
||||
echo 后端服务将在 http://localhost:8037 启动
|
||||
echo API文档:http://localhost:8037/docs
|
||||
echo.
|
||||
|
||||
start cmd /k "uvicorn app.main:app --host 0.0.0.0 --port 8037 --reload"
|
||||
|
||||
echo ⏳ 等待后端服务启动...
|
||||
timeout /t 3 /nobreak >nul
|
||||
|
||||
echo.
|
||||
|
||||
echo ==============================================
|
||||
echo 3. 启动 Celery Worker
|
||||
echo ==============================================
|
||||
echo.
|
||||
|
||||
echo 🔄 启动 Celery Worker...
|
||||
start cmd /k "celery -A app.core.celery_app worker --loglevel=info"
|
||||
|
||||
echo ⏳ 等待 Celery Worker 启动...
|
||||
timeout /t 2 /nobreak >nul
|
||||
|
||||
echo.
|
||||
|
||||
echo ==============================================
|
||||
echo 4. 启动前端服务
|
||||
echo ==============================================
|
||||
echo.
|
||||
|
||||
REM 返回项目根目录
|
||||
cd ..
|
||||
|
||||
REM 进入frontend目录
|
||||
cd frontend
|
||||
|
||||
echo 📦 检查前端依赖...
|
||||
if not exist "node_modules" (
|
||||
echo ⚠️ 正在安装前端依赖...
|
||||
pnpm install
|
||||
if errorlevel 1 (
|
||||
echo ❌ 前端依赖安装失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo ✅ 前端依赖安装完成
|
||||
) else (
|
||||
echo ✅ 前端依赖已安装
|
||||
)
|
||||
|
||||
echo.
|
||||
|
||||
echo 🖥️ 启动前端服务...
|
||||
echo 前端服务将在 http://localhost:3000 启动
|
||||
echo.
|
||||
|
||||
start cmd /k "pnpm dev"
|
||||
|
||||
echo ⏳ 等待前端服务启动...
|
||||
timeout /t 5 /nobreak >nul
|
||||
|
||||
echo.
|
||||
|
||||
echo ==============================================
|
||||
echo 🎉 启动完成!
|
||||
echo ==============================================
|
||||
echo.
|
||||
echo 服务访问地址:
|
||||
echo 📍 前端界面: http://localhost:3000
|
||||
echo 📍 后端API: http://localhost:8037
|
||||
echo 📍 API文档: http://localhost:8037/docs
|
||||
echo.
|
||||
echo 服务状态:
|
||||
echo ✅ Redis 服务: 运行中
|
||||
echo ✅ 后端服务: 已启动
|
||||
echo ✅ Celery Worker: 已启动
|
||||
echo ✅ 前端服务: 已启动
|
||||
echo.
|
||||
echo 📋 重要提示:
|
||||
echo 1. 首次访问需要注册新用户
|
||||
echo 2. 保持所有命令行窗口打开
|
||||
echo 3. 停止服务:关闭所有命令行窗口
|
||||
echo.
|
||||
echo ==============================================
|
||||
echo.
|
||||
|
||||
REM 返回项目根目录
|
||||
cd ..
|
||||
|
||||
echo 按任意键打开浏览器访问前端界面...
|
||||
pause >nul
|
||||
start http://localhost:3000
|
||||
|
||||
echo.
|
||||
echo 脚本执行完成!
|
||||
echo 按任意键退出...
|
||||
pause >nul
|
||||
239
scripts/startup/start_windows.ps1
Normal file
239
scripts/startup/start_windows.ps1
Normal file
@@ -0,0 +1,239 @@
|
||||
# 低代码智能体平台 - Windows PowerShell 启动脚本
|
||||
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host "低代码智能体平台 - Windows 启动脚本" -ForegroundColor Cyan
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 检查Python是否安装
|
||||
try {
|
||||
$pythonVersion = python --version 2>&1
|
||||
Write-Host "✅ Python 版本: $pythonVersion" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "❌ Python 未安装或未添加到系统PATH" -ForegroundColor Red
|
||||
Write-Host "请安装 Python 3.11+ 并确保在PATH中"
|
||||
pause
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 检查Node.js是否安装
|
||||
try {
|
||||
$nodeVersion = node --version
|
||||
Write-Host "✅ Node.js 版本: $nodeVersion" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "❌ Node.js 未安装或未添加到系统PATH" -ForegroundColor Red
|
||||
Write-Host "请安装 Node.js 18+ 并确保在PATH中"
|
||||
pause
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 检查pnpm是否安装
|
||||
try {
|
||||
$pnpmVersion = pnpm --version
|
||||
Write-Host "✅ pnpm 版本: $pnpmVersion" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "⚠️ pnpm 未安装,正在安装..." -ForegroundColor Yellow
|
||||
npm install -g pnpm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ pnpm 安装失败" -ForegroundColor Red
|
||||
pause
|
||||
exit 1
|
||||
}
|
||||
Write-Host "✅ pnpm 安装成功" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "✅ 环境检查通过" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 设置项目目录
|
||||
$projectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $projectRoot
|
||||
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host "1. Redis 检查" -ForegroundColor Cyan
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 检查Redis服务是否运行
|
||||
$redisService = Get-Service -Name "Redis" -ErrorAction SilentlyContinue
|
||||
if ($null -eq $redisService -or $redisService.Status -ne "Running") {
|
||||
Write-Host "❌ Redis 服务未运行" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host "请按以下步骤安装Redis:" -ForegroundColor Yellow
|
||||
Write-Host "1. 下载 Redis Windows 版本:https://github.com/microsoftarchive/redis/releases"
|
||||
Write-Host "2. 下载 Redis-x64-3.2.100.msi"
|
||||
Write-Host "3. 运行安装程序,按照默认设置安装"
|
||||
Write-Host "4. Redis 将作为 Windows 服务运行在 6379 端口"
|
||||
Write-Host ""
|
||||
Write-Host "安装完成后,请重新运行此脚本" -ForegroundColor Yellow
|
||||
pause
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host "✅ Redis 服务正在运行 (状态: $($redisService.Status))" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host "2. 启动后端服务" -ForegroundColor Cyan
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 进入backend目录
|
||||
Set-Location "$projectRoot\backend"
|
||||
|
||||
# 检查虚拟环境
|
||||
if (-not (Test-Path "venv\Scripts\activate")) {
|
||||
Write-Host "⚠️ 虚拟环境不存在,正在创建..." -ForegroundColor Yellow
|
||||
python -m venv venv
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ 虚拟环境创建失败" -ForegroundColor Red
|
||||
pause
|
||||
exit 1
|
||||
}
|
||||
Write-Host "✅ 虚拟环境创建成功" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "✅ 虚拟环境检查通过" -ForegroundColor Green
|
||||
|
||||
# 激活虚拟环境
|
||||
$activateScript = "$projectRoot\backend\venv\Scripts\Activate.ps1"
|
||||
if (Test-Path $activateScript) {
|
||||
& $activateScript
|
||||
} else {
|
||||
Write-Host "❌ 虚拟环境激活脚本不存在: $activateScript" -ForegroundColor Red
|
||||
pause
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "📦 检查Python依赖..." -ForegroundColor Cyan
|
||||
$fastapiInstalled = pip list | Select-String "fastapi"
|
||||
if (-not $fastapiInstalled) {
|
||||
Write-Host "⚠️ 正在安装Python依赖..." -ForegroundColor Yellow
|
||||
pip install -r requirements.txt
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Python依赖安装失败" -ForegroundColor Red
|
||||
pause
|
||||
exit 1
|
||||
}
|
||||
Write-Host "✅ Python依赖安装完成" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "✅ Python依赖已安装" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "🔧 配置环境变量..." -ForegroundColor Cyan
|
||||
if (-not (Test-Path ".env")) {
|
||||
Copy-Item env.example .env -ErrorAction SilentlyContinue
|
||||
Write-Host "⚠️ 已创建 .env 文件,请检查配置" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "🗄️ 运行数据库迁移..." -ForegroundColor Cyan
|
||||
alembic upgrade head
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "⚠️ 数据库迁移失败,继续启动..." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "🌐 启动后端服务..." -ForegroundColor Cyan
|
||||
Write-Host "后端服务将在 http://localhost:8037 启动" -ForegroundColor Green
|
||||
Write-Host "API文档:http://localhost:8037/docs" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 启动后端服务(新窗口)
|
||||
Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd '$projectRoot\backend'; .\venv\Scripts\Activate.ps1; uvicorn app.main:app --host 0.0.0.0 --port 8037 --reload"
|
||||
|
||||
Write-Host "⏳ 等待后端服务启动..." -ForegroundColor Cyan
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host "3. 启动 Celery Worker" -ForegroundColor Cyan
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "🔄 启动 Celery Worker..." -ForegroundColor Cyan
|
||||
# Windows 下 prefork 池易卡住任务(Redis 出现大量 unacked),使用线程池更稳定
|
||||
Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd '$projectRoot\backend'; .\venv\Scripts\Activate.ps1; celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8"
|
||||
|
||||
Write-Host "⏳ 等待 Celery Worker 启动..." -ForegroundColor Cyan
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host "4. 启动前端服务" -ForegroundColor Cyan
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 返回项目根目录
|
||||
Set-Location $projectRoot
|
||||
|
||||
# 进入frontend目录
|
||||
Set-Location "$projectRoot\frontend"
|
||||
|
||||
Write-Host "📦 检查前端依赖..." -ForegroundColor Cyan
|
||||
if (-not (Test-Path "node_modules")) {
|
||||
Write-Host "⚠️ 正在安装前端依赖..." -ForegroundColor Yellow
|
||||
pnpm install
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ 前端依赖安装失败" -ForegroundColor Red
|
||||
pause
|
||||
exit 1
|
||||
}
|
||||
Write-Host "✅ 前端依赖安装完成" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "✅ 前端依赖已安装" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "🖥️ 启动前端服务..." -ForegroundColor Cyan
|
||||
Write-Host "前端服务将在 http://localhost:3000 启动" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 启动前端服务(新窗口)
|
||||
Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd '$projectRoot\frontend'; pnpm dev"
|
||||
|
||||
Write-Host "⏳ 等待前端服务启动..." -ForegroundColor Cyan
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host "🎉 启动完成!" -ForegroundColor Green
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "服务访问地址:" -ForegroundColor White
|
||||
Write-Host " 📍 前端界面: http://localhost:3000" -ForegroundColor Yellow
|
||||
Write-Host " 📍 后端API: http://localhost:8037" -ForegroundColor Yellow
|
||||
Write-Host " 📍 API文档: http://localhost:8037/docs" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "服务状态:" -ForegroundColor White
|
||||
Write-Host " ✅ Redis 服务: 运行中" -ForegroundColor Green
|
||||
Write-Host " ✅ 后端服务: 已启动" -ForegroundColor Green
|
||||
Write-Host " ✅ Celery Worker: 已启动" -ForegroundColor Green
|
||||
Write-Host " ✅ 前端服务: 已启动" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "📋 重要提示:" -ForegroundColor White
|
||||
Write-Host " 1. 首次访问需要注册新用户" -ForegroundColor Gray
|
||||
Write-Host " 2. 保持所有PowerShell窗口打开" -ForegroundColor Gray
|
||||
Write-Host " 3. 停止服务:关闭所有PowerShell窗口" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 返回项目根目录
|
||||
Set-Location $projectRoot
|
||||
|
||||
Write-Host "是否要打开浏览器访问前端界面?(Y/N)" -ForegroundColor Cyan
|
||||
$response = Read-Host
|
||||
if ($response -eq "Y" -or $response -eq "y") {
|
||||
Start-Process "http://localhost:3000"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "脚本执行完成!" -ForegroundColor Green
|
||||
Write-Host "按任意键退出..." -ForegroundColor Gray
|
||||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||
9
scripts/startup/stop.sh
Normal file
9
scripts/startup/stop.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 低代码智能体平台停止脚本
|
||||
|
||||
echo "🛑 停止低代码智能体平台服务..."
|
||||
|
||||
docker-compose -f docker-compose.dev.yml down
|
||||
|
||||
echo "✅ 服务已停止"
|
||||
8
scripts/startup/view_logs.sh
Normal file
8
scripts/startup/view_logs.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# 查看工作流调试日志的脚本
|
||||
|
||||
echo "正在查看后端日志(包含 [rjb] 调试信息)..."
|
||||
echo "按 Ctrl+C 退出"
|
||||
echo ""
|
||||
|
||||
docker-compose -f docker-compose.dev.yml logs -f backend | grep --line-buffered "\[rjb\]"
|
||||
174
scripts/testing/test_adb_tool.py
Normal file
174
scripts/testing/test_adb_tool.py
Normal file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ADB工具验证脚本
|
||||
用于直接测试 adb_log_tool 是否能正常调用 ADB 命令
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.services.builtin_tools import adb_log_tool
|
||||
|
||||
|
||||
async def test_adb_devices():
|
||||
"""测试列出设备"""
|
||||
print("=" * 60)
|
||||
print("测试 1: 列出连接的设备 (adb devices)")
|
||||
print("=" * 60)
|
||||
try:
|
||||
result = await adb_log_tool(command="devices")
|
||||
result_data = json.loads(result)
|
||||
print(f"✅ 执行成功")
|
||||
print(f"结果:\n{json.dumps(result_data, ensure_ascii=False, indent=2)}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 执行失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_adb_logcat_recent():
|
||||
"""测试获取最近日志"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 2: 获取最近日志 (adb logcat -d -t 10)")
|
||||
print("=" * 60)
|
||||
try:
|
||||
result = await adb_log_tool(
|
||||
command="logcat",
|
||||
max_lines=10
|
||||
)
|
||||
result_data = json.loads(result)
|
||||
print(f"✅ 执行成功")
|
||||
if "error" in result_data:
|
||||
print(f"⚠️ 返回错误: {result_data['error']}")
|
||||
else:
|
||||
print(f"日志行数: {result_data.get('line_count', 0)}")
|
||||
if result_data.get('logs'):
|
||||
print(f"前3行日志预览:")
|
||||
for i, log in enumerate(result_data['logs'][:3], 1):
|
||||
print(f" {i}. {log[:100]}...")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 执行失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_adb_logcat_with_filter():
|
||||
"""测试带过滤的日志获取"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 3: 获取错误级别日志 (adb logcat -d *:E -t 5)")
|
||||
print("=" * 60)
|
||||
try:
|
||||
result = await adb_log_tool(
|
||||
command="logcat",
|
||||
level="E",
|
||||
max_lines=5
|
||||
)
|
||||
result_data = json.loads(result)
|
||||
print(f"✅ 执行成功")
|
||||
if "error" in result_data:
|
||||
print(f"⚠️ 返回错误: {result_data['error']}")
|
||||
else:
|
||||
print(f"错误日志行数: {result_data.get('line_count', 0)}")
|
||||
if result_data.get('logs'):
|
||||
print(f"错误日志预览:")
|
||||
for i, log in enumerate(result_data['logs'][:3], 1):
|
||||
print(f" {i}. {log[:100]}...")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 执行失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_adb_shell():
|
||||
"""测试执行shell命令"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 4: 执行shell命令 (adb shell getprop ro.build.version.release)")
|
||||
print("=" * 60)
|
||||
try:
|
||||
result = await adb_log_tool(
|
||||
command="shell",
|
||||
filter_tag="getprop ro.build.version.release"
|
||||
)
|
||||
result_data = json.loads(result)
|
||||
print(f"✅ 执行成功")
|
||||
if "error" in result_data:
|
||||
print(f"⚠️ 返回错误: {result_data['error']}")
|
||||
else:
|
||||
print(f"命令输出:\n{result_data.get('output', '')}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 执行失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_invalid_command():
|
||||
"""测试无效命令"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 5: 测试无效命令 (验证错误处理)")
|
||||
print("=" * 60)
|
||||
try:
|
||||
result = await adb_log_tool(command="invalid_command")
|
||||
result_data = json.loads(result)
|
||||
if "error" in result_data:
|
||||
print(f"✅ 正确返回错误: {result_data['error']}")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ 应该返回错误但未返回")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 执行失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
"""主测试函数"""
|
||||
print("\n" + "🔧 ADB工具验证测试")
|
||||
print("=" * 60)
|
||||
print("此脚本将测试 adb_log_tool 的各种功能")
|
||||
print("请确保:")
|
||||
print(" 1. 已安装 Android SDK Platform Tools")
|
||||
print(" 2. adb 命令在 PATH 中")
|
||||
print(" 3. 已连接 Android 设备或启动模拟器")
|
||||
print("=" * 60)
|
||||
print("\n开始测试...\n")
|
||||
|
||||
results = []
|
||||
|
||||
# 运行所有测试
|
||||
results.append(("列出设备", await test_adb_devices()))
|
||||
results.append(("获取最近日志", await test_adb_logcat_recent()))
|
||||
results.append(("获取错误日志", await test_adb_logcat_with_filter()))
|
||||
results.append(("执行shell命令", await test_adb_shell()))
|
||||
results.append(("错误处理", await test_invalid_command()))
|
||||
|
||||
# 汇总结果
|
||||
print("\n" + "=" * 60)
|
||||
print("测试结果汇总")
|
||||
print("=" * 60)
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
for test_name, result in results:
|
||||
status = "✅ 通过" if result else "❌ 失败"
|
||||
print(f"{status} - {test_name}")
|
||||
|
||||
print(f"\n总计: {passed}/{total} 测试通过")
|
||||
|
||||
if passed == total:
|
||||
print("\n🎉 所有测试通过!ADB工具工作正常。")
|
||||
return 0
|
||||
else:
|
||||
print(f"\n⚠️ 有 {total - passed} 个测试失败,请检查:")
|
||||
print(" 1. ADB 是否正确安装")
|
||||
print(" 2. 设备是否已连接 (运行 'adb devices' 检查)")
|
||||
print(" 3. 设备是否已启用 USB 调试")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
428
scripts/testing/test_agent_execution.py
Normal file
428
scripts/testing/test_agent_execution.py
Normal file
@@ -0,0 +1,428 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent工作流执行测试脚本
|
||||
用于测试Agent工作流的正常执行
|
||||
|
||||
与《工作流调用测试总结》一致:input_data 仅包含 query、USER_INPUT,便于 LLM 正确提取 user_query。
|
||||
|
||||
用法示例:
|
||||
python test_agent_execution.py
|
||||
python test_agent_execution.py <agent_id>
|
||||
python test_agent_execution.py <agent_id> "你好"
|
||||
python test_agent_execution.py --homework
|
||||
python test_agent_execution.py --homework --base-url http://127.0.0.1:8037
|
||||
python test_agent_execution.py --homework2
|
||||
python test_agent_execution.py --homework2 -m "记一下数学作业,周五交"
|
||||
python test_agent_execution.py --homework2 --base-url http://127.0.0.1:8037 --request-timeout 180 --max-wait 420
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
DEFAULT_BASE_URL = os.environ.get("API_BASE_URL", "http://localhost:8037")
|
||||
|
||||
|
||||
def _ensure_utf8_stdio() -> None:
|
||||
"""Windows 默认 GBK 控制台打印含 emoji 的模型回复会报错,尽量切到 UTF-8。"""
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
for name in ("stdout", "stderr"):
|
||||
stream = getattr(sys, name, None)
|
||||
if stream is not None and hasattr(stream, "reconfigure"):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
_ensure_utf8_stdio()
|
||||
|
||||
DEFAULT_ANDROID_PROMPT = "生成一个导出androidlog的脚本"
|
||||
HOMEWORK_AGENT_NAME = "学生作业管理助手"
|
||||
HOMEWORK_DEFAULT_MESSAGE = "你好"
|
||||
|
||||
HOMEWORK2_AGENT_NAME = "学生作业管理助手2号"
|
||||
HOMEWORK2_DEFAULT_MESSAGE = """4.27号作业:
|
||||
1.学过的所有字母每个字母每个写3遍,抄写本课的单词和单词每个6遍(在孩子作业本上)
|
||||
2.读第38-40页课本,录视频打卡
|
||||
3.拼读第43页,拼读,录视频打卡
|
||||
4.完成app里面布置的绘本打卡"""
|
||||
|
||||
|
||||
def print_section(title: str) -> None:
|
||||
print("\n" + "=" * 80)
|
||||
print(f" {title}")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
def _login(
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
timeout: int,
|
||||
) -> Optional[Dict[str, str]]:
|
||||
login_data = {"username": username, "password": password}
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{base_url}/api/v1/auth/login",
|
||||
data=login_data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=timeout,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"[FAIL] 登录失败: {response.status_code}")
|
||||
print(f"响应: {response.text[:800]}")
|
||||
return None
|
||||
token = response.json().get("access_token")
|
||||
if not token:
|
||||
print("[FAIL] 登录失败: 未获取到 token")
|
||||
return None
|
||||
print("[OK] 登录成功")
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
except Exception as e:
|
||||
print(f"[FAIL] 登录异常: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _find_agent_by_name(
|
||||
base_url: str,
|
||||
headers: Dict[str, str],
|
||||
name: str,
|
||||
timeout: int,
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{base_url}/api/v1/agents",
|
||||
headers=headers,
|
||||
params={"search": name, "limit": 100},
|
||||
timeout=timeout,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"[FAIL] 按名称查找 Agent 失败: {response.status_code}")
|
||||
print(response.text[:800])
|
||||
return None
|
||||
agents: List[Dict[str, Any]] = response.json() or []
|
||||
exact = [a for a in agents if (a.get("name") or "").strip() == name]
|
||||
pick = exact[0] if exact else (agents[0] if agents else None)
|
||||
if not pick:
|
||||
print(f"[FAIL] 未找到名为「{name}」的 Agent(search 无结果)")
|
||||
return None
|
||||
print(
|
||||
f"[OK] 使用 Agent: {pick.get('name')} (ID: {pick['id']}) "
|
||||
f"状态: {pick.get('status')}"
|
||||
)
|
||||
return str(pick["id"])
|
||||
except Exception as e:
|
||||
print(f"[FAIL] 查找 Agent 异常: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _find_first_published_agent(
|
||||
base_url: str,
|
||||
headers: Dict[str, str],
|
||||
timeout: int,
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{base_url}/api/v1/agents",
|
||||
headers=headers,
|
||||
params={"status": "published", "limit": 10},
|
||||
timeout=timeout,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"[FAIL] 获取 Agent 列表失败: {response.status_code}")
|
||||
print(response.text[:800])
|
||||
return None
|
||||
agents: List[Dict[str, Any]] = response.json() or []
|
||||
if not agents:
|
||||
print("[FAIL] 未找到可用的 Agent")
|
||||
print("请先创建并发布 Agent,或指定 agent_id / --agent-name / --homework")
|
||||
return None
|
||||
published_agents = [a for a in agents if a.get("status") == "published"]
|
||||
if published_agents:
|
||||
a = published_agents[0]
|
||||
print(f"[OK] 找到已发布的 Agent: {a['name']} (ID: {a['id']})")
|
||||
return str(a["id"])
|
||||
a = agents[0]
|
||||
print(
|
||||
f"[WARN] 使用 Agent: {a['name']} (ID: {a['id']}) - 状态: {a.get('status')}"
|
||||
)
|
||||
return str(a["id"])
|
||||
except Exception as e:
|
||||
print(f"[FAIL] 获取 Agent 列表异常: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def test_agent_execution(
|
||||
agent_id: Optional[str] = None,
|
||||
user_input: str = DEFAULT_ANDROID_PROMPT,
|
||||
*,
|
||||
base_url: str = DEFAULT_BASE_URL,
|
||||
username: str = "admin",
|
||||
password: str = "123456",
|
||||
agent_name: Optional[str] = None,
|
||||
request_timeout: int = 120,
|
||||
max_wait_time: int = 300,
|
||||
poll_interval: float = 2.0,
|
||||
) -> None:
|
||||
"""
|
||||
测试 Agent 执行
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID;为 None 时按 agent_name 或已发布列表解析
|
||||
user_input: 用户输入(写入 query / USER_INPUT)
|
||||
base_url: API 根地址
|
||||
agent_name: 按名称精确匹配查找(配合 search 参数)
|
||||
request_timeout: 单次 HTTP 超时(秒)
|
||||
max_wait_time: 轮询最长等待(秒)
|
||||
poll_interval: 轮询间隔(秒)
|
||||
"""
|
||||
base_url = base_url.rstrip("/")
|
||||
print_section("Agent工作流执行测试")
|
||||
print(f"API: {base_url}")
|
||||
|
||||
print_section("1. 用户登录")
|
||||
headers = _login(base_url, username, password, request_timeout)
|
||||
if not headers:
|
||||
return
|
||||
|
||||
if not agent_id:
|
||||
print_section("2. 查找可用的 Agent")
|
||||
if agent_name:
|
||||
agent_id = _find_agent_by_name(
|
||||
base_url, headers, agent_name, request_timeout
|
||||
)
|
||||
else:
|
||||
agent_id = _find_first_published_agent(
|
||||
base_url, headers, request_timeout
|
||||
)
|
||||
if not agent_id:
|
||||
return
|
||||
else:
|
||||
print_section("2. 使用指定的 Agent")
|
||||
print(f"Agent ID: {agent_id}")
|
||||
|
||||
print_section("3. 执行 Agent 工作流")
|
||||
print(f"用户输入: {user_input}")
|
||||
|
||||
input_data = {"query": user_input, "USER_INPUT": user_input}
|
||||
execution_data = {"agent_id": agent_id, "input_data": input_data}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{base_url}/api/v1/executions",
|
||||
headers=headers,
|
||||
json=execution_data,
|
||||
timeout=request_timeout,
|
||||
)
|
||||
if response.status_code != 201:
|
||||
print(f"[FAIL] 创建执行任务失败: {response.status_code}")
|
||||
print(f"响应: {response.text[:2000]}")
|
||||
return
|
||||
execution = response.json()
|
||||
execution_id = execution["id"]
|
||||
print(f"[OK] 执行任务已创建: {execution_id}")
|
||||
print(f"状态: {execution.get('status')}")
|
||||
except Exception as e:
|
||||
print(f"[FAIL] 创建执行任务异常: {e}")
|
||||
return
|
||||
|
||||
print_section("4. 等待执行完成")
|
||||
start_time = time.time()
|
||||
final_loop_status: Optional[str] = None
|
||||
|
||||
while True:
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time > max_wait_time:
|
||||
print(f"[FAIL] 执行超时(超过 {max_wait_time} 秒)")
|
||||
break
|
||||
try:
|
||||
status_response = requests.get(
|
||||
f"{base_url}/api/v1/executions/{execution_id}/status",
|
||||
headers=headers,
|
||||
timeout=request_timeout,
|
||||
)
|
||||
if status_response.status_code == 200:
|
||||
status = status_response.json()
|
||||
current_status = status.get("status")
|
||||
final_loop_status = current_status
|
||||
progress = status.get("progress", 0)
|
||||
print(
|
||||
f"[...] 执行中 状态={current_status} 进度={progress}%",
|
||||
end="\r",
|
||||
)
|
||||
if current_status == "completed":
|
||||
print("\n[OK] 执行完成")
|
||||
break
|
||||
if current_status == "failed":
|
||||
print("\n[FAIL] 执行失败")
|
||||
err = status.get("error") or status.get("error_message")
|
||||
if not err and status.get("failed_nodes"):
|
||||
fn = status["failed_nodes"][0]
|
||||
err = fn.get("error_message") or fn.get("error_type")
|
||||
print(f"错误信息: {err or '未知错误'}")
|
||||
break
|
||||
if current_status in ("cancelled", "awaiting_approval"):
|
||||
print(f"\n[WARN] 结束轮询: 状态={current_status}")
|
||||
break
|
||||
current_node = status.get("current_node")
|
||||
if current_node:
|
||||
nid = current_node.get("node_id")
|
||||
ntype = current_node.get("node_type")
|
||||
print(f"\n 当前节点: {nid} ({ntype})")
|
||||
time.sleep(poll_interval)
|
||||
except Exception as e:
|
||||
print(f"\n[FAIL] 获取执行状态异常: {e}")
|
||||
time.sleep(poll_interval)
|
||||
|
||||
print_section("5. 获取执行结果")
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{base_url}/api/v1/executions/{execution_id}",
|
||||
headers=headers,
|
||||
timeout=request_timeout,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
execution = response.json()
|
||||
status = execution.get("status")
|
||||
output_data = execution.get("output_data")
|
||||
execution_time = execution.get("execution_time")
|
||||
err_msg = execution.get("error_message")
|
||||
|
||||
print(f"执行状态: {status}")
|
||||
if err_msg and status != "completed":
|
||||
print(f"服务端错误信息: {err_msg[:2000]}")
|
||||
if execution_time is not None:
|
||||
print(f"执行时间: {execution_time}ms")
|
||||
|
||||
print("\n输出结果:")
|
||||
print("-" * 80)
|
||||
if output_data:
|
||||
if isinstance(output_data, dict):
|
||||
text_output = (
|
||||
output_data.get("result")
|
||||
or output_data.get("output")
|
||||
or output_data.get("text")
|
||||
or output_data.get("content")
|
||||
or json.dumps(output_data, ensure_ascii=False, indent=2)
|
||||
)
|
||||
print(text_output)
|
||||
else:
|
||||
print(output_data)
|
||||
else:
|
||||
print("(无输出数据)")
|
||||
print("-" * 80)
|
||||
|
||||
if execution.get("logs"):
|
||||
print("\n执行日志:")
|
||||
for log in execution.get("logs", []):
|
||||
print(f" [{log.get('timestamp')}] {log.get('message')}")
|
||||
else:
|
||||
print(f"[FAIL] 获取执行结果失败: {response.status_code}")
|
||||
print(f"响应: {response.text[:2000]}")
|
||||
except Exception as e:
|
||||
print(f"[FAIL] 获取执行结果异常: {e}")
|
||||
|
||||
print_section("测试完成")
|
||||
if final_loop_status and final_loop_status != "completed":
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Agent 工作流执行测试(input_data 与总结文档一致:query + USER_INPUT)"
|
||||
)
|
||||
p.add_argument("agent_id", nargs="?", default=None, help="Agent UUID(可选)")
|
||||
p.add_argument("user_input", nargs="?", default=None, help="用户消息(可选)")
|
||||
p.add_argument(
|
||||
"--homework",
|
||||
action="store_true",
|
||||
help=f"测试「{HOMEWORK_AGENT_NAME}」,默认发送「{HOMEWORK_DEFAULT_MESSAGE}」",
|
||||
)
|
||||
p.add_argument(
|
||||
"--homework2",
|
||||
action="store_true",
|
||||
help=f"测试「{HOMEWORK2_AGENT_NAME}」(快速档案+持久记忆);默认发送一段 4.27 样例作业",
|
||||
)
|
||||
p.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
default=None,
|
||||
metavar="TEXT",
|
||||
help="与 --homework / --homework2 联用的用户话术(推荐,避免位置参数被当成 agent_id)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--agent-name",
|
||||
default=None,
|
||||
help="按名称精确查找 Agent(未传 agent_id 时)",
|
||||
)
|
||||
p.add_argument("--base-url", default=DEFAULT_BASE_URL, help="API 根地址")
|
||||
p.add_argument("--username", default="admin")
|
||||
p.add_argument("--password", default="123456")
|
||||
p.add_argument("--request-timeout", type=int, default=120, help="单次 HTTP 超时秒数")
|
||||
p.add_argument("--max-wait", type=int, default=300, help="轮询最长等待秒数")
|
||||
p.add_argument("--poll-interval", type=float, default=2.0)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = _parse_args()
|
||||
name: Optional[str] = None
|
||||
uid: Optional[str] = args.agent_id
|
||||
msg: str
|
||||
|
||||
if args.homework and args.homework2:
|
||||
print("[WARN] 同时指定 --homework 与 --homework2,优先使用 --homework2")
|
||||
if (args.homework or args.homework2) and args.agent_name:
|
||||
print("[WARN] 同时指定 --homework/--homework2 与 --agent-name,将使用 --agent-name 查找")
|
||||
|
||||
def _msg_from_homework_flags(default_v1: str, default_v2: str) -> str:
|
||||
if args.message is not None:
|
||||
return args.message
|
||||
if args.user_input is not None:
|
||||
return args.user_input
|
||||
return default_v2 if args.homework2 else default_v1
|
||||
|
||||
if args.agent_name:
|
||||
name = args.agent_name
|
||||
if args.homework2:
|
||||
msg = _msg_from_homework_flags(HOMEWORK_DEFAULT_MESSAGE, HOMEWORK2_DEFAULT_MESSAGE)
|
||||
elif args.homework:
|
||||
msg = _msg_from_homework_flags(HOMEWORK_DEFAULT_MESSAGE, HOMEWORK2_DEFAULT_MESSAGE)
|
||||
else:
|
||||
msg = (
|
||||
args.user_input
|
||||
if args.user_input is not None
|
||||
else DEFAULT_ANDROID_PROMPT
|
||||
)
|
||||
elif args.homework2:
|
||||
name = HOMEWORK2_AGENT_NAME
|
||||
msg = _msg_from_homework_flags(HOMEWORK_DEFAULT_MESSAGE, HOMEWORK2_DEFAULT_MESSAGE)
|
||||
elif args.homework:
|
||||
name = HOMEWORK_AGENT_NAME
|
||||
msg = _msg_from_homework_flags(HOMEWORK_DEFAULT_MESSAGE, HOMEWORK2_DEFAULT_MESSAGE)
|
||||
else:
|
||||
msg = (
|
||||
args.user_input
|
||||
if args.user_input is not None
|
||||
else DEFAULT_ANDROID_PROMPT
|
||||
)
|
||||
|
||||
test_agent_execution(
|
||||
agent_id=uid,
|
||||
user_input=msg,
|
||||
base_url=args.base_url,
|
||||
username=args.username,
|
||||
password=args.password,
|
||||
agent_name=name if not uid else None,
|
||||
request_timeout=args.request_timeout,
|
||||
max_wait_time=args.max_wait,
|
||||
poll_interval=args.poll_interval,
|
||||
)
|
||||
69
scripts/testing/test_coding_agent_execution.py
Normal file
69
scripts/testing/test_coding_agent_execution.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
代码编程助手 — 工作流执行冒烟测试
|
||||
|
||||
与 test_agent_execution.py 相同调用链:POST /api/v1/executions,
|
||||
input_data 仅含 query、USER_INPUT(与《工作流调用测试总结》一致)。
|
||||
|
||||
用法示例:
|
||||
python test_coding_agent_execution.py
|
||||
python test_coding_agent_execution.py -m "你好"
|
||||
python test_coding_agent_execution.py <agent_id>
|
||||
python test_coding_agent_execution.py <agent_id> "写一个 hello world"
|
||||
python test_coding_agent_execution.py --base-url http://127.0.0.1:8037 --max-wait 420
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
from test_agent_execution import DEFAULT_BASE_URL, test_agent_execution
|
||||
|
||||
CODING_AGENT_NAME = "代码编程助手"
|
||||
DEFAULT_MESSAGE = "你好"
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description=f'测试「{CODING_AGENT_NAME}」工作流执行(默认用户话术「{DEFAULT_MESSAGE}」)'
|
||||
)
|
||||
p.add_argument("agent_id", nargs="?", default=None, help="Agent UUID(可选;不传则按名称查找)")
|
||||
p.add_argument("user_input", nargs="?", default=None, help="用户消息(可选;等价于省略时使用默认值)")
|
||||
p.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
default=None,
|
||||
metavar="TEXT",
|
||||
help=f'用户话术(优先于第二个位置参数;默认「{DEFAULT_MESSAGE}」)',
|
||||
)
|
||||
p.add_argument("--base-url", default=DEFAULT_BASE_URL, help="API 根地址")
|
||||
p.add_argument("--username", default="admin")
|
||||
p.add_argument("--password", default="123456")
|
||||
p.add_argument("--request-timeout", type=int, default=120, help="单次 HTTP 超时秒数")
|
||||
p.add_argument("--max-wait", type=int, default=420, help="轮询最长等待秒数(编程助手可能多轮 LLM)")
|
||||
p.add_argument("--poll-interval", type=float, default=2.0)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
msg = DEFAULT_MESSAGE
|
||||
if args.message is not None:
|
||||
msg = args.message
|
||||
elif args.user_input is not None:
|
||||
msg = args.user_input
|
||||
|
||||
test_agent_execution(
|
||||
agent_id=args.agent_id,
|
||||
user_input=msg,
|
||||
base_url=args.base_url,
|
||||
username=args.username,
|
||||
password=args.password,
|
||||
agent_name=None if args.agent_id else CODING_AGENT_NAME,
|
||||
request_timeout=args.request_timeout,
|
||||
max_wait_time=args.max_wait,
|
||||
poll_interval=args.poll_interval,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
135
scripts/testing/test_database_query_tool.py
Normal file
135
scripts/testing/test_database_query_tool.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试数据库查询工具
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.services.builtin_tools import database_query_tool, _validate_sql_query
|
||||
|
||||
|
||||
def test_sql_validation():
|
||||
"""测试SQL验证功能"""
|
||||
print("=" * 60)
|
||||
print("测试SQL验证功能")
|
||||
print("=" * 60)
|
||||
|
||||
test_cases = [
|
||||
("SELECT * FROM users", True, "正常SELECT查询"),
|
||||
("select * from users", True, "小写SELECT查询"),
|
||||
("INSERT INTO users VALUES (1, 'test')", False, "INSERT查询(应拒绝)"),
|
||||
("UPDATE users SET name='test'", False, "UPDATE查询(应拒绝)"),
|
||||
("DELETE FROM users", False, "DELETE查询(应拒绝)"),
|
||||
("DROP TABLE users", False, "DROP查询(应拒绝)"),
|
||||
("SELECT * FROM users; DROP TABLE users", False, "多语句查询(应拒绝)"),
|
||||
("SELECT * FROM users WHERE id = 1", True, "带WHERE的SELECT查询"),
|
||||
("SELECT u.id, u.name FROM users u", True, "带别名的SELECT查询"),
|
||||
]
|
||||
|
||||
for sql, expected, description in test_cases:
|
||||
is_safe, error_msg = _validate_sql_query(sql)
|
||||
status = "✅" if is_safe == expected else "❌"
|
||||
print(f"{status} {description}")
|
||||
print(f" SQL: {sql[:50]}...")
|
||||
if not is_safe:
|
||||
print(f" 错误: {error_msg}")
|
||||
print()
|
||||
|
||||
|
||||
async def test_database_query():
|
||||
"""测试数据库查询功能"""
|
||||
print("=" * 60)
|
||||
print("测试数据库查询功能")
|
||||
print("=" * 60)
|
||||
|
||||
# 测试1: 查询系统表(如果存在)
|
||||
print("\n1. 测试查询系统表(users表)")
|
||||
try:
|
||||
result = await database_query_tool(
|
||||
query="SELECT COUNT(*) as user_count FROM users LIMIT 1",
|
||||
timeout=10
|
||||
)
|
||||
data = json.loads(result)
|
||||
if data.get("success"):
|
||||
print(f" ✅ 查询成功")
|
||||
print(f" 结果: {json.dumps(data, ensure_ascii=False, indent=2)}")
|
||||
else:
|
||||
print(f" ❌ 查询失败: {data.get('error')}")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 查询异常: {str(e)}")
|
||||
|
||||
# 测试2: 测试SQL注入防护
|
||||
print("\n2. 测试SQL注入防护")
|
||||
try:
|
||||
result = await database_query_tool(
|
||||
query="INSERT INTO users (username) VALUES ('hacker')",
|
||||
timeout=10
|
||||
)
|
||||
data = json.loads(result)
|
||||
if not data.get("success") and "不允许" in data.get("error", ""):
|
||||
print(f" ✅ SQL注入防护生效")
|
||||
print(f" 错误信息: {data.get('error')}")
|
||||
else:
|
||||
print(f" ❌ SQL注入防护失效!")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 异常: {str(e)}")
|
||||
|
||||
# 测试3: 测试复杂查询
|
||||
print("\n3. 测试复杂SELECT查询")
|
||||
try:
|
||||
result = await database_query_tool(
|
||||
query="SELECT id, username, email FROM users LIMIT 5",
|
||||
timeout=10
|
||||
)
|
||||
data = json.loads(result)
|
||||
if data.get("success"):
|
||||
print(f" ✅ 查询成功")
|
||||
print(f" 返回行数: {data.get('row_count', 0)}")
|
||||
if data.get('data'):
|
||||
print(f" 示例数据: {json.dumps(data['data'][0] if data['data'] else {}, ensure_ascii=False, indent=2)}")
|
||||
else:
|
||||
print(f" ❌ 查询失败: {data.get('error')}")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 查询异常: {str(e)}")
|
||||
|
||||
# 测试4: 测试超时控制
|
||||
print("\n4. 测试超时控制(使用长时间查询)")
|
||||
try:
|
||||
result = await database_query_tool(
|
||||
query="SELECT SLEEP(5) as test",
|
||||
timeout=2
|
||||
)
|
||||
data = json.loads(result)
|
||||
if "超时" in data.get("error", ""):
|
||||
print(f" ✅ 超时控制生效")
|
||||
else:
|
||||
print(f" ⚠️ 超时控制未生效(可能数据库不支持SLEEP函数)")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 异常: {str(e)}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("\n" + "=" * 60)
|
||||
print("数据库查询工具测试")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
# 测试SQL验证
|
||||
test_sql_validation()
|
||||
|
||||
# 测试数据库查询
|
||||
print("\n")
|
||||
asyncio.run(test_database_query())
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
175
scripts/testing/test_memory_functionality.py
Normal file
175
scripts/testing/test_memory_functionality.py
Normal file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试记忆功能
|
||||
参考工作流调用测试总结.txt的测试方法
|
||||
"""
|
||||
import sys
|
||||
sys.path.insert(0, 'backend')
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import Agent
|
||||
from app.models.execution import Execution
|
||||
from app.models.execution_log import ExecutionLog
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def test_memory_functionality():
|
||||
"""测试记忆功能"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 获取智能聊天助手Agent
|
||||
agent = db.query(Agent).filter(
|
||||
Agent.name == '智能聊天助手(完整示例)'
|
||||
).first()
|
||||
|
||||
if not agent:
|
||||
print("❌ 未找到'智能聊天助手(完整示例)'Agent")
|
||||
return
|
||||
|
||||
print(f"✅ 找到Agent: {agent.name} (ID: {agent.id})")
|
||||
print("="*80)
|
||||
|
||||
# 获取最近的两次执行(应该对应两次对话)
|
||||
executions = db.query(Execution).filter(
|
||||
Execution.agent_id == agent.id
|
||||
).order_by(Execution.created_at.desc()).limit(2).all()
|
||||
|
||||
if len(executions) < 2:
|
||||
print(f"⚠️ 只找到 {len(executions)} 次执行,需要至少2次执行来测试记忆功能")
|
||||
print("请先进行两次对话测试")
|
||||
return
|
||||
|
||||
print(f"\n找到 {len(executions)} 次执行记录")
|
||||
print("="*80)
|
||||
|
||||
# 分析每次执行
|
||||
for i, exec_record in enumerate(reversed(executions), 1): # 按时间正序
|
||||
print(f"\n{'='*80}")
|
||||
print(f"执行 {i}: {exec_record.id}")
|
||||
print(f"输入: {exec_record.input_data}")
|
||||
print(f"时间: {exec_record.created_at}")
|
||||
print(f"状态: {exec_record.status}")
|
||||
|
||||
# 检查关键节点的数据流转
|
||||
nodes_to_check = [
|
||||
('cache-query', '查询记忆'),
|
||||
('transform-merge', '合并上下文'),
|
||||
('llm-question', '问题回答'),
|
||||
('cache-update', '更新记忆'),
|
||||
('llm-format', '格式化回复'),
|
||||
('end-1', '最终输出')
|
||||
]
|
||||
|
||||
for node_id, label in nodes_to_check:
|
||||
# 查找节点执行完成的日志
|
||||
log = db.query(ExecutionLog).filter(
|
||||
ExecutionLog.execution_id == exec_record.id,
|
||||
ExecutionLog.node_id == node_id,
|
||||
ExecutionLog.message.like(f'节点 {node_id}%执行完成')
|
||||
).first()
|
||||
|
||||
if not log:
|
||||
# 如果没有执行完成的日志,查找开始执行的日志
|
||||
log = db.query(ExecutionLog).filter(
|
||||
ExecutionLog.execution_id == exec_record.id,
|
||||
ExecutionLog.node_id == node_id,
|
||||
ExecutionLog.message.like(f'节点 {node_id}%开始执行')
|
||||
).first()
|
||||
|
||||
if log and log.data:
|
||||
data_key = 'output' if '执行完成' in log.message else 'input'
|
||||
data = log.data.get(data_key, {})
|
||||
|
||||
print(f"\n{label} ({node_id}):")
|
||||
|
||||
if isinstance(data, dict):
|
||||
print(f" keys: {list(data.keys())}")
|
||||
|
||||
# 检查memory字段
|
||||
if 'memory' in data:
|
||||
memory = data['memory']
|
||||
if isinstance(memory, dict):
|
||||
print(f" ✅ memory存在,keys: {list(memory.keys())}")
|
||||
if 'conversation_history' in memory:
|
||||
history = memory['conversation_history']
|
||||
if isinstance(history, list):
|
||||
print(f" ✅ conversation_history: {len(history)} 条")
|
||||
if history:
|
||||
print(f" 最新一条: {history[-1].get('content', '')[:50]}")
|
||||
else:
|
||||
print(f" ❌ conversation_history不是list: {type(history)}")
|
||||
# 检查conversation_history字段(可能在顶层)
|
||||
elif 'conversation_history' in data:
|
||||
history = data['conversation_history']
|
||||
if isinstance(history, list):
|
||||
print(f" ✅ conversation_history在顶层: {len(history)} 条")
|
||||
if history:
|
||||
print(f" 最新一条: {history[-1].get('content', '')[:50]}")
|
||||
|
||||
# 对于end节点,检查最终输出
|
||||
if node_id == 'end-1' and 'output' in data:
|
||||
output = data['output']
|
||||
if isinstance(output, str):
|
||||
print(f" ✅ 最终输出: {output[:200]}")
|
||||
# 检查是否包含名字
|
||||
if '老七' in output:
|
||||
print(f" ✅ 输出中包含名字'老七'")
|
||||
else:
|
||||
print(f" ❌ 输出中不包含名字'老七'")
|
||||
elif isinstance(data, str):
|
||||
print(f" 输出类型: str, 内容: {data[:200]}")
|
||||
if '老七' in data:
|
||||
print(f" ✅ 输出中包含名字'老七'")
|
||||
else:
|
||||
print(f" ❌ 输出中不包含名字'老七'")
|
||||
else:
|
||||
print(f"\n{label} ({node_id}): ❌ 未找到执行日志")
|
||||
|
||||
# 检查最终输出
|
||||
if exec_record.output_data:
|
||||
output_data = exec_record.output_data
|
||||
if isinstance(output_data, dict):
|
||||
result = output_data.get('result', '')
|
||||
if isinstance(result, str):
|
||||
print(f"\n最终结果: {result[:200]}")
|
||||
if '老七' in result:
|
||||
print(f"✅ 最终结果中包含名字'老七'")
|
||||
else:
|
||||
print(f"❌ 最终结果中不包含名字'老七'")
|
||||
|
||||
# 检查Redis中的记忆数据
|
||||
print(f"\n{'='*80}")
|
||||
print("检查Redis中的记忆数据:")
|
||||
try:
|
||||
from app.core.redis_client import get_redis_client
|
||||
redis_client = get_redis_client()
|
||||
if redis_client:
|
||||
keys = redis_client.keys('user_memory_*')
|
||||
if keys:
|
||||
for key in keys:
|
||||
value = redis_client.get(key)
|
||||
if value:
|
||||
try:
|
||||
memory_data = json.loads(value)
|
||||
if 'conversation_history' in memory_data:
|
||||
history = memory_data['conversation_history']
|
||||
print(f" ✅ {key}: {len(history)} 条对话记录")
|
||||
if history:
|
||||
print(f" 最新一条: {history[-1].get('content', '')[:50]}")
|
||||
except:
|
||||
print(f" ⚠️ {key}: 无法解析JSON")
|
||||
else:
|
||||
print(" ❌ Redis中没有找到记忆数据")
|
||||
else:
|
||||
print(" ⚠️ Redis客户端不可用")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 检查Redis失败: {str(e)}")
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print("测试完成")
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_memory_functionality()
|
||||
66
scripts/testing/test_output_variable_extraction.py
Normal file
66
scripts/testing/test_output_variable_extraction.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试output变量提取逻辑
|
||||
"""
|
||||
import sys
|
||||
sys.path.insert(0, 'backend')
|
||||
|
||||
from app.services.workflow_engine import WorkflowEngine
|
||||
|
||||
# 模拟llm-format节点的输入数据
|
||||
input_data = {
|
||||
'right': {
|
||||
'right': {
|
||||
'right': '是的,我记得!根据我们之前的对话,你告诉我你的名字叫"老七"。我会在本次对话中记住这个名字,以便更好地为你提供帮助。如果你希望我用其他称呼,也可以随时告诉我。',
|
||||
'query': '你还记得我的名字吗?'
|
||||
},
|
||||
'memory': {
|
||||
'conversation_history': [],
|
||||
'user_profile': {},
|
||||
'context': {}
|
||||
},
|
||||
'query': '你还记得我的名字吗?'
|
||||
},
|
||||
'memory': {
|
||||
'conversation_history': [],
|
||||
'user_profile': {},
|
||||
'context': {}
|
||||
},
|
||||
'query': '你还记得我的名字吗?'
|
||||
}
|
||||
|
||||
# 创建WorkflowEngine实例
|
||||
engine = WorkflowEngine("test", {"nodes": [], "edges": []})
|
||||
|
||||
# 测试_get_nested_value方法
|
||||
print("测试_get_nested_value方法:")
|
||||
value1 = engine._get_nested_value(input_data, 'output')
|
||||
print(f" _get_nested_value(input_data, 'output'): {value1}")
|
||||
|
||||
# 测试output变量提取逻辑
|
||||
print("\n测试output变量提取逻辑:")
|
||||
right_value = input_data.get('right')
|
||||
print(f" right_value类型: {type(right_value)}")
|
||||
print(f" right_value: {str(right_value)[:100]}")
|
||||
|
||||
if right_value is not None:
|
||||
if isinstance(right_value, str):
|
||||
value = right_value
|
||||
print(f" ✅ 从right字段(字符串)提取: {value[:100]}")
|
||||
elif isinstance(right_value, dict):
|
||||
current = right_value
|
||||
depth = 0
|
||||
while isinstance(current, dict) and depth < 10:
|
||||
if 'right' in current:
|
||||
current = current['right']
|
||||
depth += 1
|
||||
if isinstance(current, str):
|
||||
value = current
|
||||
print(f" ✅ 从right字段(嵌套{depth}层)提取: {value[:100]}")
|
||||
break
|
||||
else:
|
||||
break
|
||||
else:
|
||||
print(f" ❌ 无法提取字符串值")
|
||||
|
||||
print("\n测试完成")
|
||||
368
scripts/testing/test_tool_calling_visualization.py
Normal file
368
scripts/testing/test_tool_calling_visualization.py
Normal file
@@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试工具调用可视化功能
|
||||
创建一个简单的Agent,使用工具调用,然后查看执行详情
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
|
||||
BASE_URL = "http://localhost:8037"
|
||||
|
||||
def print_section(title):
|
||||
print("\n" + "=" * 80)
|
||||
print(f" {title}")
|
||||
print("=" * 80)
|
||||
|
||||
def print_info(message):
|
||||
print(f"ℹ️ {message}")
|
||||
|
||||
def print_success(message):
|
||||
print(f"✅ {message}")
|
||||
|
||||
def print_error(message):
|
||||
print(f"❌ {message}")
|
||||
|
||||
def login():
|
||||
"""用户登录"""
|
||||
print_section("1. 用户登录")
|
||||
login_data = {"username": "admin", "password": "123456"}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/api/v1/auth/login", data=login_data)
|
||||
if response.status_code != 200:
|
||||
print_error(f"登录失败: {response.status_code}")
|
||||
return None
|
||||
|
||||
token = response.json().get("access_token")
|
||||
if not token:
|
||||
print_error("登录失败: 未获取到token")
|
||||
return None
|
||||
|
||||
print_success(f"登录成功")
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
except Exception as e:
|
||||
print_error(f"登录异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def create_test_workflow(headers):
|
||||
"""创建测试工作流(使用工具调用)"""
|
||||
print_section("2. 创建测试工作流")
|
||||
|
||||
workflow_data = {
|
||||
"name": "工具调用可视化测试工作流",
|
||||
"description": "用于测试工具调用可视化功能",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "start",
|
||||
"position": {"x": 100, "y": 200},
|
||||
"data": {"label": "开始"}
|
||||
},
|
||||
{
|
||||
"id": "llm-with-tools",
|
||||
"type": "llm",
|
||||
"position": {"x": 400, "y": 200},
|
||||
"data": {
|
||||
"label": "工具调用测试",
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 1000,
|
||||
"enable_tools": True,
|
||||
"selected_tools": ["http_request", "datetime", "math_calculate"],
|
||||
"prompt": """用户请求:{{input.query}}
|
||||
|
||||
请根据用户需求,选择合适的工具执行任务。可以使用以下工具:
|
||||
- http_request: 发送HTTP请求
|
||||
- datetime: 获取当前时间
|
||||
- math_calculate: 执行数学计算
|
||||
|
||||
请分析用户需求,调用合适的工具,然后基于工具返回的结果生成回复。"""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "end",
|
||||
"type": "end",
|
||||
"position": {"x": 700, "y": 200},
|
||||
"data": {"label": "结束"}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e1",
|
||||
"source": "start",
|
||||
"target": "llm-with-tools",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left"
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"source": "llm-with-tools",
|
||||
"target": "end",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/v1/workflows",
|
||||
headers=headers,
|
||||
json=workflow_data
|
||||
)
|
||||
|
||||
if response.status_code not in [200, 201]:
|
||||
print_error(f"创建工作流失败: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return None
|
||||
|
||||
workflow = response.json()
|
||||
print_success(f"工作流创建成功: {workflow.get('id')}")
|
||||
return workflow
|
||||
except Exception as e:
|
||||
print_error(f"创建工作流异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def execute_workflow(headers, workflow_id, input_data):
|
||||
"""执行工作流"""
|
||||
print_section("3. 执行工作流")
|
||||
|
||||
execution_data = {
|
||||
"workflow_id": workflow_id,
|
||||
"input_data": input_data
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/v1/executions",
|
||||
headers=headers,
|
||||
json=execution_data
|
||||
)
|
||||
|
||||
if response.status_code not in [200, 201]:
|
||||
print_error(f"执行工作流失败: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return None
|
||||
|
||||
execution = response.json()
|
||||
execution_id = execution.get("id")
|
||||
print_success(f"执行已创建: {execution_id}")
|
||||
return execution_id
|
||||
except Exception as e:
|
||||
print_error(f"执行工作流异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def wait_for_completion(headers, execution_id, timeout=60):
|
||||
"""等待执行完成"""
|
||||
print_section("4. 等待执行完成")
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/v1/executions/{execution_id}",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print_error(f"获取执行状态失败: {response.status_code}")
|
||||
return None
|
||||
|
||||
execution = response.json()
|
||||
status = execution.get("status")
|
||||
|
||||
print_info(f"执行状态: {status}")
|
||||
|
||||
if status in ["completed", "failed"]:
|
||||
print_success(f"执行完成,状态: {status}")
|
||||
return execution
|
||||
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
print_error(f"获取执行状态异常: {str(e)}")
|
||||
return None
|
||||
|
||||
print_error("执行超时")
|
||||
return None
|
||||
|
||||
def get_execution_logs(headers, execution_id):
|
||||
"""获取执行日志"""
|
||||
print_section("5. 获取执行日志(包含工具调用信息)")
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/v1/execution-logs/executions/{execution_id}",
|
||||
headers=headers,
|
||||
params={"limit": 100}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print_error(f"获取执行日志失败: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return None
|
||||
|
||||
logs = response.json()
|
||||
print_success(f"获取到 {len(logs)} 条日志")
|
||||
|
||||
# 查找工具调用相关的日志
|
||||
tool_call_logs = []
|
||||
for log in logs:
|
||||
if not log:
|
||||
continue
|
||||
data = log.get("data") or {}
|
||||
if isinstance(data, str):
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except:
|
||||
data = {}
|
||||
if data.get("tool_name") or "工具" in log.get("message", ""):
|
||||
tool_call_logs.append(log)
|
||||
|
||||
if tool_call_logs:
|
||||
print_success(f"找到 {len(tool_call_logs)} 条工具调用日志")
|
||||
print("\n工具调用日志详情:")
|
||||
for i, log in enumerate(tool_call_logs, 1):
|
||||
print(f"\n{i}. {log.get('message')}")
|
||||
print(f" 时间: {log.get('timestamp')}")
|
||||
print(f" 节点: {log.get('node_id')}")
|
||||
data = log.get("data", {})
|
||||
if data.get("tool_name"):
|
||||
print(f" 工具名称: {data.get('tool_name')}")
|
||||
print(f" 状态: {data.get('status')}")
|
||||
if data.get("tool_args"):
|
||||
print(f" 参数: {json.dumps(data.get('tool_args'), ensure_ascii=False, indent=2)}")
|
||||
if data.get("duration"):
|
||||
print(f" 耗时: {data.get('duration')}ms")
|
||||
else:
|
||||
print_info("未找到工具调用日志")
|
||||
|
||||
return logs
|
||||
except Exception as e:
|
||||
print_error(f"获取执行日志异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_node_execution_data(headers, execution_id, node_id):
|
||||
"""获取节点执行数据"""
|
||||
print_section(f"6. 获取节点执行数据 (节点: {node_id})")
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/v1/execution-logs/executions/{execution_id}",
|
||||
headers=headers,
|
||||
params={"node_id": node_id, "limit": 100}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print_error(f"获取节点执行数据失败: {response.status_code}")
|
||||
return None
|
||||
|
||||
logs = response.json()
|
||||
print_success(f"获取到 {len(logs)} 条节点日志")
|
||||
|
||||
# 显示工具调用信息
|
||||
tool_calls = []
|
||||
for log in logs:
|
||||
if not log:
|
||||
continue
|
||||
data = log.get("data") or {}
|
||||
if isinstance(data, str):
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except:
|
||||
data = {}
|
||||
if data.get("tool_name"):
|
||||
tool_calls.append({
|
||||
"tool_name": data.get("tool_name"),
|
||||
"status": data.get("status"),
|
||||
"args": data.get("tool_args"),
|
||||
"result": data.get("tool_result"),
|
||||
"duration": data.get("duration"),
|
||||
"timestamp": log.get("timestamp")
|
||||
})
|
||||
|
||||
if tool_calls:
|
||||
print_success(f"找到 {len(tool_calls)} 个工具调用")
|
||||
for i, call in enumerate(tool_calls, 1):
|
||||
print(f"\n工具调用 {i}:")
|
||||
print(f" 工具: {call['tool_name']}")
|
||||
print(f" 状态: {call['status']}")
|
||||
print(f" 参数: {json.dumps(call['args'], ensure_ascii=False, indent=2) if call['args'] else '无'}")
|
||||
if call.get('result'):
|
||||
result_preview = call['result'][:200] if len(call['result']) > 200 else call['result']
|
||||
print(f" 结果预览: {result_preview}...")
|
||||
print(f" 耗时: {call.get('duration', 'N/A')}ms")
|
||||
print(f" 时间: {call['timestamp']}")
|
||||
else:
|
||||
print_info("该节点没有工具调用")
|
||||
|
||||
return logs
|
||||
except Exception as e:
|
||||
print_error(f"获取节点执行数据异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print_section("工具调用可视化功能测试")
|
||||
|
||||
# 1. 登录
|
||||
headers = login()
|
||||
if not headers:
|
||||
return
|
||||
|
||||
# 2. 创建测试工作流
|
||||
workflow = create_test_workflow(headers)
|
||||
if not workflow:
|
||||
return
|
||||
|
||||
workflow_id = workflow.get("id")
|
||||
|
||||
# 3. 执行工作流(使用不同的测试用例)
|
||||
test_cases = [
|
||||
{
|
||||
"name": "测试HTTP请求工具",
|
||||
"input": {"query": "请查询 https://api.github.com/users/octocat 的信息"}
|
||||
},
|
||||
{
|
||||
"name": "测试时间工具",
|
||||
"input": {"query": "现在是什么时间?"}
|
||||
},
|
||||
{
|
||||
"name": "测试数学计算工具",
|
||||
"input": {"query": "计算 123 * 456 的结果"}
|
||||
}
|
||||
]
|
||||
|
||||
for i, test_case in enumerate(test_cases, 1):
|
||||
print_section(f"测试用例 {i}: {test_case['name']}")
|
||||
|
||||
# 执行工作流
|
||||
execution_id = execute_workflow(headers, workflow_id, test_case["input"])
|
||||
if not execution_id:
|
||||
continue
|
||||
|
||||
# 等待完成
|
||||
execution = wait_for_completion(headers, execution_id)
|
||||
if not execution:
|
||||
continue
|
||||
|
||||
# 获取执行日志
|
||||
logs = get_execution_logs(headers, execution_id)
|
||||
|
||||
# 获取节点执行数据
|
||||
node_logs = get_node_execution_data(headers, execution_id, "llm-with-tools")
|
||||
|
||||
print_success(f"测试用例 {i} 完成")
|
||||
print("\n" + "-" * 80)
|
||||
|
||||
print_section("测试完成")
|
||||
print_success("所有测试用例执行完成!")
|
||||
print_info("请在前端查看执行详情,验证工具调用可视化功能:")
|
||||
print_info(f"1. 打开执行详情页面: http://localhost:8038/executions/{execution_id}")
|
||||
print_info("2. 点击节点查看节点执行详情")
|
||||
print_info("3. 检查工具调用可视化是否正确显示")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
169
scripts/testing/test_workflow_data_flow.py
Normal file
169
scripts/testing/test_workflow_data_flow.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
工作流数据流转测试脚本
|
||||
用于诊断"答非所问"问题
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.services.workflow_engine import WorkflowEngine
|
||||
from app.core.database import SessionLocal
|
||||
|
||||
|
||||
def print_section(title):
|
||||
"""打印分隔线"""
|
||||
print("\n" + "=" * 80)
|
||||
print(f" {title}")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
def print_data(label, data, indent=0):
|
||||
"""格式化打印数据"""
|
||||
prefix = " " * indent
|
||||
print(f"{prefix}{label}:")
|
||||
if isinstance(data, dict):
|
||||
print(f"{prefix} {json.dumps(data, ensure_ascii=False, indent=2)}")
|
||||
else:
|
||||
print(f"{prefix} {data}")
|
||||
|
||||
|
||||
async def test_workflow_data_flow():
|
||||
"""测试工作流数据流转"""
|
||||
print_section("工作流数据流转测试")
|
||||
|
||||
# 模拟一个简单的工作流
|
||||
workflow_data = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start-1",
|
||||
"type": "start",
|
||||
"position": {"x": 100, "y": 100},
|
||||
"data": {
|
||||
"label": "开始"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "llm-1",
|
||||
"type": "llm",
|
||||
"position": {"x": 300, "y": 100},
|
||||
"data": {
|
||||
"label": "LLM处理",
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"prompt": "请处理用户请求。",
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 1500
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "end-1",
|
||||
"type": "end",
|
||||
"position": {"x": 500, "y": 100},
|
||||
"data": {
|
||||
"label": "结束",
|
||||
"output_format": "text"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{"id": "e1", "source": "start-1", "target": "llm-1"},
|
||||
{"id": "e2", "source": "llm-1", "target": "end-1"}
|
||||
]
|
||||
}
|
||||
|
||||
# 模拟前端发送的输入数据
|
||||
input_data = {
|
||||
"query": "苹果英语怎么讲?",
|
||||
"USER_INPUT": "苹果英语怎么讲?"
|
||||
}
|
||||
|
||||
print_section("1. 初始输入数据")
|
||||
print_data("input_data", input_data)
|
||||
|
||||
# 创建引擎(不使用logger,避免数据库依赖)
|
||||
engine = WorkflowEngine("test-workflow", workflow_data)
|
||||
|
||||
# 重写get_node_input方法,添加详细日志
|
||||
original_get_node_input = engine.get_node_input
|
||||
|
||||
def logged_get_node_input(node_id, node_outputs, active_edges=None):
|
||||
print_section(f"获取节点输入: {node_id}")
|
||||
print_data("node_outputs", node_outputs)
|
||||
result = original_get_node_input(node_id, node_outputs, active_edges)
|
||||
print_data(f"返回的input_data (for {node_id})", result)
|
||||
return result
|
||||
|
||||
engine.get_node_input = logged_get_node_input
|
||||
|
||||
# 重写execute_node方法,添加详细日志
|
||||
original_execute_node = engine.execute_node
|
||||
|
||||
async def logged_execute_node(node, input_data):
|
||||
node_id = node.get('id')
|
||||
node_type = node.get('type')
|
||||
|
||||
print_section(f"执行节点: {node_id} ({node_type})")
|
||||
print_data("节点配置", node.get('data', {}))
|
||||
print_data("输入数据", input_data)
|
||||
|
||||
result = await original_execute_node(node, input_data)
|
||||
|
||||
print_data("执行结果", result)
|
||||
|
||||
# 如果是LLM节点,特别关注prompt和输出
|
||||
if node_type == 'llm':
|
||||
print_section(f"LLM节点详细分析: {node_id}")
|
||||
node_data = node.get('data', {})
|
||||
prompt = node_data.get('prompt', '')
|
||||
print_data("原始prompt", prompt)
|
||||
print_data("输入数据", input_data)
|
||||
|
||||
# 模拟prompt格式化逻辑
|
||||
if isinstance(input_data, dict):
|
||||
# 检查是否有嵌套的input字段
|
||||
nested_input = input_data.get('input')
|
||||
if isinstance(nested_input, dict):
|
||||
print("⚠️ 发现嵌套的input字段!")
|
||||
print_data("嵌套input内容", nested_input)
|
||||
# 尝试提取user_query
|
||||
user_query = None
|
||||
for key in ['query', 'input', 'text', 'message', 'content', 'user_input', 'USER_INPUT']:
|
||||
if key in nested_input:
|
||||
user_query = nested_input[key]
|
||||
print(f"✅ 从嵌套input中提取到user_query: {key} = {user_query}")
|
||||
break
|
||||
else:
|
||||
# 从顶层提取
|
||||
user_query = None
|
||||
for key in ['query', 'input', 'text', 'message', 'content', 'user_input', 'USER_INPUT']:
|
||||
if key in input_data:
|
||||
value = input_data[key]
|
||||
if isinstance(value, str):
|
||||
user_query = value
|
||||
print(f"✅ 从顶层提取到user_query: {key} = {user_query}")
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
engine.execute_node = logged_execute_node
|
||||
|
||||
# 执行工作流
|
||||
print_section("开始执行工作流")
|
||||
try:
|
||||
result = await engine.execute(input_data)
|
||||
print_section("工作流执行完成")
|
||||
print_data("最终结果", result)
|
||||
except Exception as e:
|
||||
print_section("执行出错")
|
||||
print(f"错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_workflow_data_flow())
|
||||
529
scripts/testing/test_workflow_tool.py
Normal file
529
scripts/testing/test_workflow_tool.py
Normal file
@@ -0,0 +1,529 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
工作流测试工具
|
||||
支持通过Agent名称和用户输入来测试工作流执行
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
# API基础URL
|
||||
BASE_URL = "http://localhost:8037"
|
||||
|
||||
def print_section(title):
|
||||
"""打印分隔线"""
|
||||
print("\n" + "=" * 80)
|
||||
print(f" {title}")
|
||||
print("=" * 80)
|
||||
|
||||
def print_info(message):
|
||||
"""打印信息"""
|
||||
print(f"ℹ️ {message}")
|
||||
|
||||
def print_success(message):
|
||||
"""打印成功信息"""
|
||||
print(f"✅ {message}")
|
||||
|
||||
def print_error(message):
|
||||
"""打印错误信息"""
|
||||
print(f"❌ {message}")
|
||||
|
||||
def print_warning(message):
|
||||
"""打印警告信息"""
|
||||
print(f"⚠️ {message}")
|
||||
|
||||
def login(username="admin", password="123456"):
|
||||
"""
|
||||
用户登录
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, token: str or None, headers: dict or None)
|
||||
"""
|
||||
print_section("1. 用户登录")
|
||||
login_data = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/api/v1/auth/login", data=login_data)
|
||||
if response.status_code != 200:
|
||||
print_error(f"登录失败: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return False, None, None
|
||||
|
||||
token = response.json().get("access_token")
|
||||
if not token:
|
||||
print_error("登录失败: 未获取到token")
|
||||
return False, None, None
|
||||
|
||||
print_success(f"登录成功 (用户: {username})")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
return True, token, headers
|
||||
except requests.exceptions.ConnectionError:
|
||||
print_error("无法连接到后端服务,请确保后端服务正在运行")
|
||||
print_info(f"后端服务地址: {BASE_URL}")
|
||||
return False, None, None
|
||||
except Exception as e:
|
||||
print_error(f"登录异常: {str(e)}")
|
||||
return False, None, None
|
||||
|
||||
def find_agent_by_name(agent_name, headers):
|
||||
"""
|
||||
通过名称查找Agent
|
||||
|
||||
Args:
|
||||
agent_name: Agent名称
|
||||
headers: 请求头(包含token)
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, agent: dict or None)
|
||||
"""
|
||||
print_section("2. 查找Agent")
|
||||
print_info(f"搜索Agent: {agent_name}")
|
||||
|
||||
try:
|
||||
# 搜索Agent
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/v1/agents",
|
||||
headers=headers,
|
||||
params={"search": agent_name, "limit": 100}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print_error(f"获取Agent列表失败: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return False, None
|
||||
|
||||
agents = response.json()
|
||||
|
||||
# 精确匹配名称
|
||||
exact_match = None
|
||||
for agent in agents:
|
||||
if agent.get("name") == agent_name:
|
||||
exact_match = agent
|
||||
break
|
||||
|
||||
if exact_match:
|
||||
agent_id = exact_match["id"]
|
||||
agent_status = exact_match.get("status", "unknown")
|
||||
print_success(f"找到Agent: {agent_name} (ID: {agent_id}, 状态: {agent_status})")
|
||||
|
||||
# 检查状态
|
||||
if agent_status not in ["published", "running"]:
|
||||
print_warning(f"Agent状态为 '{agent_status}',可能无法执行")
|
||||
print_info("只有 'published' 或 'running' 状态的Agent可以执行")
|
||||
|
||||
return True, exact_match
|
||||
|
||||
# 如果没有精确匹配,显示相似的结果
|
||||
if agents:
|
||||
print_warning(f"未找到名称为 '{agent_name}' 的Agent")
|
||||
print_info("找到以下相似的Agent:")
|
||||
for agent in agents[:5]: # 只显示前5个
|
||||
print(f" - {agent.get('name')} (ID: {agent.get('id')}, 状态: {agent.get('status')})")
|
||||
else:
|
||||
print_error(f"未找到任何Agent")
|
||||
print_info("请检查Agent名称是否正确,或先创建一个Agent")
|
||||
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"查找Agent异常: {str(e)}")
|
||||
return False, None
|
||||
|
||||
def execute_agent(agent_id, user_input, headers):
|
||||
"""
|
||||
执行Agent工作流
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
user_input: 用户输入内容
|
||||
headers: 请求头
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, execution_id: str or None)
|
||||
"""
|
||||
print_section("3. 执行Agent工作流")
|
||||
print_info(f"用户输入: {user_input}")
|
||||
|
||||
input_data = {
|
||||
"query": user_input,
|
||||
"USER_INPUT": user_input
|
||||
}
|
||||
|
||||
execution_data = {
|
||||
"agent_id": agent_id,
|
||||
"input_data": input_data
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/v1/executions",
|
||||
headers=headers,
|
||||
json=execution_data
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
print_error(f"创建执行任务失败: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return False, None
|
||||
|
||||
execution = response.json()
|
||||
execution_id = execution["id"]
|
||||
status = execution.get("status")
|
||||
print_success(f"执行任务已创建")
|
||||
print_info(f"执行ID: {execution_id}")
|
||||
print_info(f"状态: {status}")
|
||||
return True, execution_id
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"创建执行任务异常: {str(e)}")
|
||||
return False, None
|
||||
|
||||
def wait_for_completion(execution_id, headers, max_wait_time=300, poll_interval=2):
|
||||
"""
|
||||
等待执行完成
|
||||
|
||||
Args:
|
||||
execution_id: 执行ID
|
||||
headers: 请求头
|
||||
max_wait_time: 最大等待时间(秒)
|
||||
poll_interval: 轮询间隔(秒)
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, status: str or None)
|
||||
"""
|
||||
print_section("4. 等待执行完成")
|
||||
print_info(f"最大等待时间: {max_wait_time}秒")
|
||||
print_info(f"轮询间隔: {poll_interval}秒")
|
||||
|
||||
start_time = time.time()
|
||||
last_node = None
|
||||
|
||||
while True:
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time > max_wait_time:
|
||||
print_error(f"执行超时(超过{max_wait_time}秒)")
|
||||
return False, "timeout"
|
||||
|
||||
try:
|
||||
# 获取执行状态
|
||||
status_response = requests.get(
|
||||
f"{BASE_URL}/api/v1/executions/{execution_id}/status",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if status_response.status_code == 200:
|
||||
status = status_response.json()
|
||||
current_status = status.get("status")
|
||||
progress = status.get("progress", 0)
|
||||
current_node = status.get("current_node")
|
||||
|
||||
# 显示当前执行的节点
|
||||
if current_node:
|
||||
node_id = current_node.get("node_id", "unknown")
|
||||
node_name = current_node.get("node_name", "unknown")
|
||||
if node_id != last_node:
|
||||
print_info(f"当前节点: {node_id} ({node_name})")
|
||||
last_node = node_id
|
||||
|
||||
# 显示进度
|
||||
elapsed_str = f"{int(elapsed_time)}秒"
|
||||
print(f"⏳ 执行中... 状态: {current_status}, 进度: {progress}%, 耗时: {elapsed_str}", end="\r")
|
||||
|
||||
if current_status == "completed":
|
||||
print() # 换行
|
||||
print_success("执行完成!")
|
||||
return True, "completed"
|
||||
elif current_status == "failed":
|
||||
print() # 换行
|
||||
print_error("执行失败")
|
||||
error = status.get("error", "未知错误")
|
||||
print_error(f"错误信息: {error}")
|
||||
return False, "failed"
|
||||
|
||||
time.sleep(poll_interval)
|
||||
except KeyboardInterrupt:
|
||||
print() # 换行
|
||||
print_warning("用户中断执行")
|
||||
return False, "interrupted"
|
||||
except Exception as e:
|
||||
print_error(f"获取执行状态异常: {str(e)}")
|
||||
time.sleep(poll_interval)
|
||||
|
||||
def get_execution_result(execution_id, headers):
|
||||
"""
|
||||
获取执行结果
|
||||
|
||||
Args:
|
||||
execution_id: 执行ID
|
||||
headers: 请求头
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, result: dict or None)
|
||||
"""
|
||||
print_section("5. 获取执行结果")
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/v1/executions/{execution_id}",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print_error(f"获取执行结果失败: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return False, None
|
||||
|
||||
execution = response.json()
|
||||
status = execution.get("status")
|
||||
output_data = execution.get("output_data")
|
||||
execution_time = execution.get("execution_time")
|
||||
|
||||
print_info(f"执行状态: {status}")
|
||||
if execution_time:
|
||||
print_info(f"执行时间: {execution_time}ms ({execution_time/1000:.2f}秒)")
|
||||
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("输出结果:")
|
||||
print("=" * 80)
|
||||
|
||||
if output_data:
|
||||
if isinstance(output_data, dict):
|
||||
# 如果 result 字段是字符串,尝试解析它(类似JSON节点的parse操作)
|
||||
if "result" in output_data and isinstance(output_data["result"], str):
|
||||
try:
|
||||
# 尝试使用 ast.literal_eval 解析Python字典字符串
|
||||
import ast
|
||||
parsed_result = ast.literal_eval(output_data["result"])
|
||||
output_data = parsed_result
|
||||
except:
|
||||
# 如果解析失败,尝试作为JSON解析
|
||||
try:
|
||||
parsed_result = json.loads(output_data["result"])
|
||||
output_data = parsed_result
|
||||
except:
|
||||
pass
|
||||
|
||||
# 使用类似JSON节点的extract操作来提取文本
|
||||
def json_extract(data, path):
|
||||
"""类似JSON节点的extract操作,使用路径提取数据"""
|
||||
if not path or not isinstance(data, dict):
|
||||
return None
|
||||
# 支持 $.right.right.right 格式的路径
|
||||
path = path.replace('$.', '').replace('$', '')
|
||||
keys = path.split('.')
|
||||
result = data
|
||||
for key in keys:
|
||||
if isinstance(result, dict) and key in result:
|
||||
result = result[key]
|
||||
else:
|
||||
return None
|
||||
return result
|
||||
|
||||
# 尝试使用路径提取:递归查找 right 字段直到找到字符串
|
||||
def extract_text_by_path(data, depth=0, max_depth=10):
|
||||
"""递归提取嵌套在right字段中的文本"""
|
||||
if depth > max_depth:
|
||||
return None
|
||||
if isinstance(data, str):
|
||||
# 如果是字符串且不是JSON格式,返回它
|
||||
if len(data) > 10 and not data.strip().startswith('{') and not data.strip().startswith('['):
|
||||
return data
|
||||
return None
|
||||
if isinstance(data, dict):
|
||||
# 优先查找 right 字段
|
||||
if "right" in data:
|
||||
right_value = data["right"]
|
||||
# 如果 right 的值是字符串,直接返回
|
||||
if isinstance(right_value, str) and len(right_value) > 10:
|
||||
return right_value
|
||||
# 否则递归查找
|
||||
result = extract_text_by_path(right_value, depth + 1, max_depth)
|
||||
if result:
|
||||
return result
|
||||
# 查找其他常见的输出字段
|
||||
for key in ["output", "text", "content"]:
|
||||
if key in data:
|
||||
result = extract_text_by_path(data[key], depth + 1, max_depth)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
return None
|
||||
|
||||
# 优先检查 result 字段(JSON节点提取后的结果)
|
||||
if "result" in output_data and isinstance(output_data["result"], str):
|
||||
text_output = output_data["result"]
|
||||
else:
|
||||
# 先尝试使用路径提取(类似JSON节点的extract操作)
|
||||
# 尝试多个可能的路径
|
||||
paths_to_try = [
|
||||
"right.right.right", # 最常见的嵌套路径
|
||||
"right.right",
|
||||
"right",
|
||||
"output",
|
||||
"text",
|
||||
"content"
|
||||
]
|
||||
|
||||
text_output = None
|
||||
for path in paths_to_try:
|
||||
extracted = json_extract(output_data, f"$.{path}")
|
||||
if extracted and isinstance(extracted, str) and len(extracted) > 10:
|
||||
text_output = extracted
|
||||
break
|
||||
|
||||
# 如果路径提取失败,使用递归提取
|
||||
if not text_output:
|
||||
text_output = extract_text_by_path(output_data)
|
||||
|
||||
if text_output and isinstance(text_output, str):
|
||||
print(text_output)
|
||||
print()
|
||||
print_info(f"回答长度: {len(text_output)} 字符")
|
||||
else:
|
||||
# 如果无法提取,显示格式化的JSON
|
||||
print(json.dumps(output_data, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(output_data)
|
||||
else:
|
||||
print("(无输出数据)")
|
||||
|
||||
print("=" * 80)
|
||||
|
||||
return True, execution
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"获取执行结果异常: {str(e)}")
|
||||
return False, None
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="工作流测试工具 - 通过Agent名称和用户输入测试工作流执行",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例:
|
||||
# 使用默认参数(交互式输入)
|
||||
python3 test_workflow_tool.py
|
||||
|
||||
# 指定Agent名称和用户输入
|
||||
python3 test_workflow_tool.py -a "智能需求分析与解决方案生成器" -i "生成一个导出androidlog的脚本"
|
||||
|
||||
# 指定用户名和密码
|
||||
python3 test_workflow_tool.py -u admin -p 123456 -a "Agent名称" -i "用户输入"
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-a", "--agent-name",
|
||||
type=str,
|
||||
help="Agent名称(如果不指定,将交互式输入)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i", "--input",
|
||||
type=str,
|
||||
help="用户输入内容(如果不指定,将交互式输入)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-u", "--username",
|
||||
type=str,
|
||||
default="admin",
|
||||
help="登录用户名(默认: admin)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-p", "--password",
|
||||
type=str,
|
||||
default="123456",
|
||||
help="登录密码(默认: 123456)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--max-wait",
|
||||
type=int,
|
||||
default=300,
|
||||
help="最大等待时间(秒,默认: 300)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--poll-interval",
|
||||
type=float,
|
||||
default=2.0,
|
||||
help="轮询间隔(秒,默认: 2.0)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 打印标题
|
||||
print("=" * 80)
|
||||
print(" 工作流测试工具")
|
||||
print("=" * 80)
|
||||
|
||||
# 1. 登录
|
||||
success, token, headers = login(args.username, args.password)
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
||||
# 2. 获取Agent名称
|
||||
agent_name = args.agent_name
|
||||
if not agent_name:
|
||||
agent_name = input("\n请输入Agent名称: ").strip()
|
||||
if not agent_name:
|
||||
print_error("Agent名称不能为空")
|
||||
sys.exit(1)
|
||||
|
||||
# 3. 查找Agent
|
||||
success, agent = find_agent_by_name(agent_name, headers)
|
||||
if not success or not agent:
|
||||
sys.exit(1)
|
||||
|
||||
agent_id = agent["id"]
|
||||
|
||||
# 4. 获取用户输入
|
||||
user_input = args.input
|
||||
if not user_input:
|
||||
user_input = input("\n请输入用户输入内容: ").strip()
|
||||
if not user_input:
|
||||
print_error("用户输入不能为空")
|
||||
sys.exit(1)
|
||||
|
||||
# 5. 执行Agent
|
||||
success, execution_id = execute_agent(agent_id, user_input, headers)
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
||||
# 6. 等待执行完成
|
||||
success, status = wait_for_completion(
|
||||
execution_id,
|
||||
headers,
|
||||
max_wait_time=args.max_wait,
|
||||
poll_interval=args.poll_interval
|
||||
)
|
||||
|
||||
if not success:
|
||||
if status == "timeout":
|
||||
print_warning("执行超时,但可能仍在后台运行")
|
||||
print_info(f"执行ID: {execution_id}")
|
||||
print_info("可以通过API查询执行状态")
|
||||
sys.exit(1)
|
||||
|
||||
# 7. 获取执行结果
|
||||
success, result = get_execution_result(execution_id, headers)
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
||||
# 完成
|
||||
print_section("测试完成")
|
||||
print_success("工作流测试成功完成!")
|
||||
print_info(f"执行ID: {execution_id}")
|
||||
print_info(f"Agent: {agent_name}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
134
scripts/testing/test_zhini_kefu_6.py
Normal file
134
scripts/testing/test_zhini_kefu_6.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试「知你客服6号」Agent:登录 -> 创建执行 -> 轮询直到结束。
|
||||
|
||||
用法:
|
||||
python test_zhini_kefu_6.py
|
||||
python test_zhini_kefu_6.py --base-url http://127.0.0.1:8037
|
||||
set PLATFORM_BASE_URL=... && python test_zhini_kefu_6.py
|
||||
|
||||
依赖: requests(与项目其他测试脚本一致)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
# 知你客服6号(本地平台 Agent 管理中的名称对应 ID,若你环境不同请改此处或传 --agent-id)
|
||||
DEFAULT_AGENT_ID = "2acc84d5-814b-4d61-9703-94a4b117375f"
|
||||
DEFAULT_MESSAGE = "你好"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
parser = argparse.ArgumentParser(description="测试知你客服6号,发送一条用户消息并打印结果")
|
||||
parser.add_argument(
|
||||
"--base-url",
|
||||
default=os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037"),
|
||||
help="平台 API 根地址(默认 http://127.0.0.1:8037)",
|
||||
)
|
||||
parser.add_argument("--username", default=os.getenv("PLATFORM_USERNAME", "admin"))
|
||||
parser.add_argument("--password", default=os.getenv("PLATFORM_PASSWORD", "123456"))
|
||||
parser.add_argument("--agent-id", default=os.getenv("ZHINI_6_AGENT_ID", DEFAULT_AGENT_ID))
|
||||
parser.add_argument("--message", "-m", default=DEFAULT_MESSAGE, help="用户消息,默认:你好")
|
||||
parser.add_argument(
|
||||
"--user-id",
|
||||
default="script_test_zhini6",
|
||||
help="多轮记忆隔离用 user_id(可选)",
|
||||
)
|
||||
parser.add_argument("--timeout", type=int, default=180, help="轮询最长秒数")
|
||||
parser.add_argument("--poll", type=float, default=0.8, help="轮询间隔秒")
|
||||
args = parser.parse_args()
|
||||
|
||||
base = args.base_url.rstrip("/")
|
||||
|
||||
print("=" * 60)
|
||||
print("知你客服6号 执行测试")
|
||||
print(" base_url :", base)
|
||||
print(" agent_id :", args.agent_id)
|
||||
print(" message :", args.message)
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 登录
|
||||
r = requests.post(
|
||||
f"{base}/api/v1/auth/login",
|
||||
data={"username": args.username, "password": args.password},
|
||||
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
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
msg = args.message
|
||||
body = {
|
||||
"agent_id": args.agent_id,
|
||||
"input_data": {
|
||||
"query": msg,
|
||||
"USER_INPUT": msg,
|
||||
"user_id": args.user_id,
|
||||
},
|
||||
}
|
||||
|
||||
# 2. 创建执行
|
||||
r2 = requests.post(f"{base}/api/v1/executions", headers=headers, json=body, timeout=30)
|
||||
if r2.status_code != 201:
|
||||
print("创建执行失败:", r2.status_code, r2.text[:1000], file=sys.stderr)
|
||||
return 1
|
||||
|
||||
ex = r2.json()
|
||||
eid = ex["id"]
|
||||
print("已创建执行:", eid)
|
||||
print("初始状态:", ex.get("status"))
|
||||
|
||||
# 3. 轮询
|
||||
deadline = time.time() + args.timeout
|
||||
status = ex.get("status", "pending")
|
||||
while status in ("pending", "running") and time.time() < deadline:
|
||||
time.sleep(args.poll)
|
||||
rs = requests.get(f"{base}/api/v1/executions/{eid}", headers=headers, timeout=60)
|
||||
if rs.status_code != 200:
|
||||
print("查询执行失败:", rs.status_code, rs.text[:500], file=sys.stderr)
|
||||
return 1
|
||||
detail = rs.json()
|
||||
status = detail.get("status", status)
|
||||
print(" ...", status)
|
||||
|
||||
final = requests.get(f"{base}/api/v1/executions/{eid}", headers=headers, timeout=60).json()
|
||||
status = final.get("status")
|
||||
print()
|
||||
print("最终状态:", status)
|
||||
if final.get("error_message"):
|
||||
print("错误信息:", final["error_message"])
|
||||
od = final.get("output_data")
|
||||
if od is not None:
|
||||
print("output_data:")
|
||||
print(json.dumps(od, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print("output_data: null")
|
||||
|
||||
return 0 if status == "completed" else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
126
scripts/tools/check_execution_logs.py
Normal file
126
scripts/tools/check_execution_logs.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
查看执行日志的脚本
|
||||
用于诊断数据流转问题
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.execution import Execution
|
||||
from app.models.execution_log import ExecutionLog
|
||||
|
||||
|
||||
def format_json(data):
|
||||
"""格式化JSON数据"""
|
||||
if isinstance(data, dict):
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
return str(data)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# 获取最近的执行记录
|
||||
print("=" * 80)
|
||||
print("查找最近的Agent执行记录...")
|
||||
print("=" * 80)
|
||||
|
||||
execution = db.query(Execution).filter(
|
||||
Execution.agent_id.isnot(None)
|
||||
).order_by(Execution.created_at.desc()).first()
|
||||
|
||||
if not execution:
|
||||
print("❌ 没有找到执行记录")
|
||||
return
|
||||
|
||||
print(f"\n✅ 找到执行记录: {execution.id}")
|
||||
print(f" 状态: {execution.status}")
|
||||
print(f" 执行时间: {execution.execution_time}ms")
|
||||
print(f" 创建时间: {execution.created_at}")
|
||||
|
||||
# 显示输入数据
|
||||
print("\n" + "=" * 80)
|
||||
print("输入数据 (input_data):")
|
||||
print("=" * 80)
|
||||
if execution.input_data:
|
||||
print(format_json(execution.input_data))
|
||||
else:
|
||||
print("(空)")
|
||||
|
||||
# 显示输出数据
|
||||
print("\n" + "=" * 80)
|
||||
print("输出数据 (output_data):")
|
||||
print("=" * 80)
|
||||
if execution.output_data:
|
||||
print(format_json(execution.output_data))
|
||||
else:
|
||||
print("(空)")
|
||||
|
||||
# 获取执行日志
|
||||
print("\n" + "=" * 80)
|
||||
print("执行日志 (按时间顺序):")
|
||||
print("=" * 80)
|
||||
|
||||
logs = db.query(ExecutionLog).filter(
|
||||
ExecutionLog.execution_id == execution.id
|
||||
).order_by(ExecutionLog.timestamp.asc()).all()
|
||||
|
||||
if not logs:
|
||||
print("❌ 没有找到执行日志")
|
||||
return
|
||||
|
||||
for i, log in enumerate(logs, 1):
|
||||
print(f"\n[{i}] {log.timestamp.strftime('%Y-%m-%d %H:%M:%S')} [{log.level}]")
|
||||
print(f" 节点: {log.node_id or '(无)'} ({log.node_type or '(无)'})")
|
||||
print(f" 消息: {log.message}")
|
||||
|
||||
if log.data:
|
||||
print(f" 数据:")
|
||||
data_str = format_json(log.data)
|
||||
# 只显示前500个字符
|
||||
if len(data_str) > 500:
|
||||
print(data_str[:500] + "...")
|
||||
else:
|
||||
print(data_str)
|
||||
|
||||
if log.duration:
|
||||
print(f" 耗时: {log.duration}ms")
|
||||
|
||||
# 特别关注LLM节点的输入输出
|
||||
print("\n" + "=" * 80)
|
||||
print("LLM节点详细分析:")
|
||||
print("=" * 80)
|
||||
|
||||
llm_logs = [log for log in logs if log.node_type == 'llm']
|
||||
if llm_logs:
|
||||
for log in llm_logs:
|
||||
if log.message == "节点开始执行" and log.data:
|
||||
print(f"\n节点 {log.node_id} 的输入数据:")
|
||||
input_data = log.data.get('input', {})
|
||||
print(format_json(input_data))
|
||||
|
||||
if log.message == "节点执行完成" and log.data:
|
||||
print(f"\n节点 {log.node_id} 的输出数据:")
|
||||
output_data = log.data.get('output', {})
|
||||
print(format_json(output_data))
|
||||
else:
|
||||
print("❌ 没有找到LLM节点的日志")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
164
scripts/tools/check_switch_logs.py
Normal file
164
scripts/tools/check_switch_logs.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
查看Switch节点日志的专用脚本
|
||||
用于诊断Switch节点的分支过滤问题
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.execution import Execution
|
||||
from app.models.execution_log import ExecutionLog
|
||||
|
||||
|
||||
def format_json(data):
|
||||
"""格式化JSON数据"""
|
||||
if isinstance(data, dict):
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
return str(data)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# 获取最近的执行记录
|
||||
print("=" * 80)
|
||||
print("查找最近的Agent执行记录...")
|
||||
print("=" * 80)
|
||||
|
||||
execution = db.query(Execution).filter(
|
||||
Execution.agent_id.isnot(None)
|
||||
).order_by(Execution.created_at.desc()).first()
|
||||
|
||||
if not execution:
|
||||
print("❌ 没有找到执行记录")
|
||||
return
|
||||
|
||||
print(f"\n✅ 找到执行记录: {execution.id}")
|
||||
print(f" 状态: {execution.status}")
|
||||
print(f" 执行时间: {execution.execution_time}ms")
|
||||
print(f" 创建时间: {execution.created_at}")
|
||||
|
||||
# 获取执行日志
|
||||
print("\n" + "=" * 80)
|
||||
print("Switch节点相关日志:")
|
||||
print("=" * 80)
|
||||
|
||||
logs = db.query(ExecutionLog).filter(
|
||||
ExecutionLog.execution_id == execution.id
|
||||
).order_by(ExecutionLog.timestamp.asc()).all()
|
||||
|
||||
if not logs:
|
||||
print("❌ 没有找到执行日志")
|
||||
return
|
||||
|
||||
# 筛选Switch节点相关的日志
|
||||
switch_logs = []
|
||||
for log in logs:
|
||||
if log.node_type == 'switch' or 'Switch' in log.message or '[rjb] Switch' in log.message:
|
||||
switch_logs.append(log)
|
||||
|
||||
if not switch_logs:
|
||||
print("❌ 没有找到Switch节点相关的日志")
|
||||
print("\n所有日志节点类型:")
|
||||
node_types = set(log.node_type for log in logs if log.node_type)
|
||||
for nt in sorted(node_types):
|
||||
print(f" - {nt}")
|
||||
return
|
||||
|
||||
print(f"\n找到 {len(switch_logs)} 条Switch节点相关日志:\n")
|
||||
|
||||
for i, log in enumerate(switch_logs, 1):
|
||||
print(f"[{i}] {log.timestamp.strftime('%H:%M:%S.%f')[:-3]} [{log.level}]")
|
||||
print(f" 节点: {log.node_id or '(无)'} ({log.node_type or '(无)'})")
|
||||
print(f" 消息: {log.message}")
|
||||
|
||||
if log.data:
|
||||
print(f" 数据:")
|
||||
data_str = format_json(log.data)
|
||||
# 显示完整数据
|
||||
for line in data_str.split('\n'):
|
||||
print(f" {line}")
|
||||
|
||||
if log.duration:
|
||||
print(f" 耗时: {log.duration}ms")
|
||||
print()
|
||||
|
||||
# 特别分析Switch节点的匹配和过滤过程
|
||||
print("=" * 80)
|
||||
print("Switch节点执行流程分析:")
|
||||
print("=" * 80)
|
||||
|
||||
match_logs = [log for log in switch_logs if '匹配' in log.message]
|
||||
filter_logs = [log for log in switch_logs if '过滤' in log.message]
|
||||
|
||||
if match_logs:
|
||||
print("\n📊 匹配阶段:")
|
||||
for log in match_logs:
|
||||
if log.data:
|
||||
data = log.data
|
||||
print(f" 节点 {log.node_id}:")
|
||||
print(f" 字段: {data.get('field', 'N/A')}")
|
||||
print(f" 字段值: {data.get('field_value', 'N/A')}")
|
||||
print(f" 匹配的分支: {data.get('matched_case', 'N/A')}")
|
||||
print(f" 处理后的输入键: {data.get('processed_input_keys', 'N/A')}")
|
||||
|
||||
if filter_logs:
|
||||
print("\n🔍 过滤阶段:")
|
||||
for log in filter_logs:
|
||||
if log.data:
|
||||
data = log.data
|
||||
print(f" 节点 {log.node_id}:")
|
||||
print(f" 匹配的分支: {data.get('branch', 'N/A')}")
|
||||
print(f" 过滤前边数: {data.get('edges_before', 'N/A')}")
|
||||
print(f" 保留边数: {data.get('edges_kept', 'N/A')}")
|
||||
print(f" 移除边数: {data.get('edges_removed', 'N/A')}")
|
||||
|
||||
# 检查意图理解节点的输出
|
||||
print("\n" + "=" * 80)
|
||||
print("意图理解节点输出分析:")
|
||||
print("=" * 80)
|
||||
|
||||
intent_logs = [log for log in logs if log.node_id and 'intent' in log.node_id.lower()]
|
||||
if intent_logs:
|
||||
for log in intent_logs:
|
||||
if log.message == "节点执行完成" and log.data:
|
||||
print(f"\n节点 {log.node_id} 的输出:")
|
||||
output = log.data.get('output', {})
|
||||
print(format_json(output))
|
||||
else:
|
||||
print("❌ 没有找到意图理解节点的日志")
|
||||
|
||||
# 检查所有节点的输出(用于调试)
|
||||
print("\n" + "=" * 80)
|
||||
print("所有节点输出摘要:")
|
||||
print("=" * 80)
|
||||
|
||||
node_outputs = {}
|
||||
for log in logs:
|
||||
if log.message == "节点执行完成" and log.node_id:
|
||||
node_outputs[log.node_id] = log.data.get('output', {})
|
||||
|
||||
for node_id, output in node_outputs.items():
|
||||
if isinstance(output, str) and len(output) > 100:
|
||||
print(f"{node_id}: {output[:100]}...")
|
||||
else:
|
||||
print(f"{node_id}: {output}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
403
scripts/tools/daily_work_summary.py
Normal file
403
scripts/tools/daily_work_summary.py
Normal file
@@ -0,0 +1,403 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
===========================================
|
||||
📋 每日工作总结自动生成器
|
||||
功能:每天早上9点 → 生成今日工作总结 → 推送到指定渠道
|
||||
===========================================
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
import smtplib
|
||||
import requests
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from pathlib import Path
|
||||
|
||||
# ===========================================
|
||||
# 📝 第一部分:配置区(请按需修改)
|
||||
# ===========================================
|
||||
|
||||
class Config:
|
||||
"""所有配置集中管理,方便修改"""
|
||||
|
||||
# --- 数据来源方式 ---
|
||||
# mode = "manual" → 手动输入今日事项
|
||||
# mode = "file" → 从文件读取今日事项
|
||||
DATA_MODE = "manual"
|
||||
|
||||
# 当 DATA_MODE = "file" 时,读取此文件
|
||||
DATA_FILE = "today_work_items.txt"
|
||||
|
||||
# --- 推送方式(可多选)---
|
||||
# 支持:wecom(企业微信)、dingtalk(钉钉)、email(邮件)
|
||||
PUSH_CHANNELS = ["wecom"] # 可改为 ["dingtalk"] 或 ["email"]
|
||||
|
||||
# ----- 企业微信机器人 -----
|
||||
WECOM_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY_HERE"
|
||||
|
||||
# ----- 钉钉机器人 -----
|
||||
DINGTALK_WEBHOOK = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN_HERE"
|
||||
|
||||
# ----- 邮件配置 -----
|
||||
SMTP_SERVER = "smtp.qq.com"
|
||||
SMTP_PORT = 465
|
||||
SMTP_USER = "your_email@qq.com"
|
||||
SMTP_PASSWORD = "your_auth_code" # 邮件授权码,非密码
|
||||
MAIL_TO = ["boss@company.com", "yourself@company.com"]
|
||||
|
||||
# --- 总结模板 ---
|
||||
SUMMARY_TEMPLATE = """
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📅 今日工作总结 · {date}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
👤 姓名:{name}
|
||||
🏢 部门:{department}
|
||||
|
||||
📌 今日工作事项:
|
||||
{work_items}
|
||||
|
||||
📊 工作总结:
|
||||
{summary}
|
||||
|
||||
✅ 明日计划:
|
||||
{tomorrow_plan}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
⏰ 生成时间:{generate_time}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
"""
|
||||
|
||||
|
||||
# ===========================================
|
||||
# 🔧 第二部分:核心功能
|
||||
# ===========================================
|
||||
|
||||
class WorkSummaryGenerator:
|
||||
"""工作总结生成器"""
|
||||
|
||||
def __init__(self, name="张三", department="技术部"):
|
||||
self.name = name
|
||||
self.department = department
|
||||
self.today = datetime.date.today()
|
||||
self.tomorrow = self.today + datetime.timedelta(days=1)
|
||||
|
||||
def get_work_items_from_input(self):
|
||||
"""方式一:手动输入今日工作事项"""
|
||||
print("\n📝 请输入你今天的工作事项(每行一条,输入空行结束):")
|
||||
print("(例:完成了XX功能开发 / 修复了XXbug / 参加了XX会议)\n")
|
||||
items = []
|
||||
while True:
|
||||
line = input(" ➜ ").strip()
|
||||
if not line:
|
||||
break
|
||||
items.append(line)
|
||||
return items
|
||||
|
||||
def get_work_items_from_file(self, filepath):
|
||||
"""方式二:从文件读取今日工作事项"""
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
items = [line.strip() for line in f if line.strip()]
|
||||
return items
|
||||
except FileNotFoundError:
|
||||
print(f"⚠️ 文件 {filepath} 不存在,请先创建!")
|
||||
return []
|
||||
|
||||
def generate_summary(self, work_items):
|
||||
"""生成工作总结(基于规则+模板,你也可以接入AI API)"""
|
||||
if not work_items:
|
||||
return "今日暂无工作记录。"
|
||||
|
||||
# 统计信息
|
||||
total_items = len(work_items)
|
||||
|
||||
# 简单分类统计(关键词匹配)
|
||||
categories = {
|
||||
"开发": 0, "修复": 0, "会议": 0, "文档": 0,
|
||||
"测试": 0, "沟通": 0, "其他": 0
|
||||
}
|
||||
keywords_map = {
|
||||
"开发": ["开发", "编码", "编程", "实现", "搭建", "构建"],
|
||||
"修复": ["修复", "解决", "处理", "修改", "bug", "Bug"],
|
||||
"会议": ["会议", "评审", "讨论", "沟通会", "晨会", "周会"],
|
||||
"文档": ["文档", "文档", "方案", "设计", "PPT", "报告"],
|
||||
"测试": ["测试", "调试", "自测", "联调", "验证"],
|
||||
"沟通": ["沟通", "对齐", "同步", "协调", "对接"],
|
||||
}
|
||||
|
||||
for item in work_items:
|
||||
matched = False
|
||||
for cat, keywords in keywords_map.items():
|
||||
if any(kw in item for kw in keywords):
|
||||
categories[cat] += 1
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
categories["其他"] += 1
|
||||
|
||||
# 生成总结文本
|
||||
summary_parts = [f"今日共完成 {total_items} 项工作,具体如下:\n"]
|
||||
|
||||
# 分类汇总
|
||||
active_cats = {k: v for k, v in categories.items() if v > 0}
|
||||
if active_cats:
|
||||
summary_parts.append("📊 工作类型分布:")
|
||||
for cat, count in active_cats.items():
|
||||
bar = "█" * count
|
||||
summary_parts.append(f" {cat}:{bar} {count}项")
|
||||
summary_parts.append("")
|
||||
|
||||
# 逐项列出
|
||||
summary_parts.append("📋 详细工作记录:")
|
||||
for i, item in enumerate(work_items, 1):
|
||||
summary_parts.append(f" {i}. {item}")
|
||||
|
||||
# 评价与建议
|
||||
summary_parts.append(f"\n💡 今日小结:今日工作效率{'较高' if total_items >= 5 else '正常'},"
|
||||
f"建议明日继续保持{'/优化时间管理' if total_items < 3 else ''}。")
|
||||
|
||||
return "\n".join(summary_parts)
|
||||
|
||||
def generate_tomorrow_plan(self):
|
||||
"""生成明日计划(可扩展为从配置文件读取)"""
|
||||
plan_items = [
|
||||
"继续推进当前开发任务",
|
||||
"同步项目进度",
|
||||
"代码审查",
|
||||
]
|
||||
return "\n".join(f" {i+1}. {item}" for i, item in enumerate(plan_items))
|
||||
|
||||
def build_summary_text(self, work_items, summary):
|
||||
"""组装完整的总结文本"""
|
||||
tomorrow_plan = self.generate_tomorrow_plan()
|
||||
formatted_items = "\n".join(f" • {item}" for item in work_items)
|
||||
|
||||
now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
return Config.SUMMARY_TEMPLATE.format(
|
||||
date=self.today.strftime("%Y年%m月%d日"),
|
||||
name=self.name,
|
||||
department=self.department,
|
||||
work_items=formatted_items if formatted_items else " (暂无记录)",
|
||||
summary=summary,
|
||||
tomorrow_plan=tomorrow_plan,
|
||||
generate_time=now_str
|
||||
)
|
||||
|
||||
def run(self):
|
||||
"""主运行流程"""
|
||||
print(f"\n{'='*50}")
|
||||
print(f"📅 今日工作总结生成器")
|
||||
print(f"📆 {self.today.strftime('%Y年%m月%d日')}")
|
||||
print(f"{'='*50}\n")
|
||||
|
||||
# 1️⃣ 获取工作事项
|
||||
if Config.DATA_MODE == "file":
|
||||
items = self.get_work_items_from_file(Config.DATA_FILE)
|
||||
else:
|
||||
items = self.get_work_items_from_input()
|
||||
|
||||
if not items:
|
||||
print("⚠️ 没有输入任何工作事项,将生成空总结。")
|
||||
|
||||
# 2️⃣ 生成总结
|
||||
summary = self.generate_summary(items)
|
||||
final_text = self.build_summary_text(items, summary)
|
||||
|
||||
# 3️⃣ 本地保存
|
||||
self.save_to_file(final_text)
|
||||
|
||||
# 4️⃣ 推送
|
||||
pusher = MessagePusher()
|
||||
pusher.push(final_text)
|
||||
|
||||
return final_text
|
||||
|
||||
def save_to_file(self, content):
|
||||
"""保存到本地文件"""
|
||||
filename = f"工作总结_{self.today.strftime('%Y%m%d')}.md"
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print(f"\n✅ 已保存到本地文件:{os.path.abspath(filename)}")
|
||||
|
||||
|
||||
# ===========================================
|
||||
# 📨 第三部分:消息推送
|
||||
# ===========================================
|
||||
|
||||
class MessagePusher:
|
||||
"""消息推送器"""
|
||||
|
||||
def push(self, content):
|
||||
"""根据配置推送消息到各个渠道"""
|
||||
for channel in Config.PUSH_CHANNELS:
|
||||
if channel == "wecom":
|
||||
self.push_wecom(content)
|
||||
elif channel == "dingtalk":
|
||||
self.push_dingtalk(content)
|
||||
elif channel == "email":
|
||||
self.push_email(content)
|
||||
else:
|
||||
print(f"⚠️ 未知推送渠道: {channel}")
|
||||
|
||||
def push_wecom(self, content):
|
||||
"""推送到企业微信机器人"""
|
||||
url = Config.WECOM_WEBHOOK
|
||||
if "YOUR_KEY_HERE" in url:
|
||||
print("⚠️ 跳过企业微信推送:未配置Webhook地址")
|
||||
return
|
||||
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {"content": content}
|
||||
}
|
||||
try:
|
||||
resp = requests.post(url, json=data, timeout=10)
|
||||
result = resp.json()
|
||||
if result.get("errcode") == 0:
|
||||
print("✅ 企业微信推送成功!")
|
||||
else:
|
||||
print(f"⚠️ 企业微信推送失败: {result}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 企业微信推送异常: {e}")
|
||||
|
||||
def push_dingtalk(self, content):
|
||||
"""推送到钉钉机器人"""
|
||||
url = Config.DINGTALK_WEBHOOK
|
||||
if "YOUR_TOKEN_HERE" in url:
|
||||
print("⚠️ 跳过钉钉推送:未配置Webhook地址")
|
||||
return
|
||||
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": "今日工作总结",
|
||||
"text": content
|
||||
}
|
||||
}
|
||||
try:
|
||||
resp = requests.post(url, json=data, timeout=10)
|
||||
result = resp.json()
|
||||
if result.get("errcode") == 0:
|
||||
print("✅ 钉钉推送成功!")
|
||||
else:
|
||||
print(f"⚠️ 钉钉推送失败: {result}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 钉钉推送异常: {e}")
|
||||
|
||||
def push_email(self, content):
|
||||
"""通过邮件推送"""
|
||||
if "your_email" in Config.SMTP_USER:
|
||||
print("⚠️ 跳过邮件推送:未配置邮箱信息")
|
||||
return
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = Config.SMTP_USER
|
||||
msg["To"] = ", ".join(Config.MAIL_TO)
|
||||
msg["Subject"] = f"今日工作总结 - {datetime.date.today().strftime('%Y-%m-%d')}"
|
||||
|
||||
msg.attach(MIMEText(content, "plain", "utf-8"))
|
||||
|
||||
with smtplib.SMTP_SSL(Config.SMTP_SERVER, Config.SMTP_PORT) as server:
|
||||
server.login(Config.SMTP_USER, Config.SMTP_PASSWORD)
|
||||
server.sendmail(Config.SMTP_USER, Config.MAIL_TO, msg.as_string())
|
||||
|
||||
print("✅ 邮件推送成功!")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 邮件推送异常: {e}")
|
||||
|
||||
|
||||
# ===========================================
|
||||
# 🚀 第四部分:程序入口
|
||||
# ===========================================
|
||||
|
||||
def setup_windows_task():
|
||||
"""生成Windows任务计划程序导入脚本"""
|
||||
script_path = os.path.abspath(sys.argv[0])
|
||||
python_path = sys.executable
|
||||
|
||||
cmd = f'''@echo off
|
||||
echo ========================================
|
||||
echo 设置每日9点自动运行工作总结脚本
|
||||
echo ========================================
|
||||
|
||||
REM 请以管理员身份运行此.bat文件
|
||||
|
||||
schtasks /create /tn "每日工作总结" /tr "{python_path} {script_path}" /sc daily /st 09:00 /f
|
||||
|
||||
if %errorlevel%==0 (
|
||||
echo ✅ 定时任务创建成功!每天早上9:00自动运行。
|
||||
) else (
|
||||
echo ❌ 创建失败,请尝试以管理员身份运行。
|
||||
)
|
||||
|
||||
pause
|
||||
'''
|
||||
|
||||
bat_path = "setup_windows_task.bat"
|
||||
with open(bat_path, "w", encoding="utf-8") as f:
|
||||
f.write(cmd)
|
||||
print(f"\n📁 Windows定时任务脚本已生成:{os.path.abspath(bat_path)}")
|
||||
print(" → 右键点击该文件,选择「以管理员身份运行」即可设置定时任务!")
|
||||
|
||||
|
||||
def setup_linux_cron():
|
||||
"""输出Linux crontab配置说明"""
|
||||
script_path = os.path.abspath(sys.argv[0])
|
||||
python_path = sys.executable
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("🐧 Linux/macOS 定时任务设置(crontab)")
|
||||
print("="*50)
|
||||
print(f"在终端中执行以下命令添加定时任务:")
|
||||
print(f"\n crontab -e")
|
||||
print(f"\n然后添加一行:")
|
||||
print(f"\n 0 9 * * * {python_path} {script_path}")
|
||||
print(f"\n保存退出即可!📌")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
# 检查是否有命令行参数
|
||||
if len(sys.argv) > 1:
|
||||
arg = sys.argv[1].lower()
|
||||
if arg == "--setup-win":
|
||||
setup_windows_task()
|
||||
sys.exit(0)
|
||||
elif arg == "--setup-linux":
|
||||
setup_linux_cron()
|
||||
sys.exit(0)
|
||||
|
||||
print("""
|
||||
╔══════════════════════════════════════════╗
|
||||
║ 🍊 每日工作总结生成器 ║
|
||||
║ ║
|
||||
║ 用法: ║
|
||||
║ python daily_work_summary.py ║
|
||||
║ python daily_work_summary.py --setup-win ║
|
||||
║ python daily_work_summary.py --setup-linux ║
|
||||
╚══════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
# 运行主程序
|
||||
generator = WorkSummaryGenerator(name="张三", department="技术部")
|
||||
result = generator.run()
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("🎉 工作总结生成完成!")
|
||||
print("="*50)
|
||||
|
||||
# 生成本地定时任务设置脚本
|
||||
if sys.platform == "win32":
|
||||
setup_windows_task()
|
||||
else:
|
||||
setup_linux_cron()
|
||||
|
||||
print("\n💡 小提示:修改 config.py 中的配置可自定义推送到不同渠道~")
|
||||
108
scripts/tools/debug_switch_node.py
Normal file
108
scripts/tools/debug_switch_node.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
调试Switch节点的详细脚本
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.execution import Execution
|
||||
from app.models.agent import Agent
|
||||
|
||||
def main():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 获取最近的执行记录
|
||||
execution = db.query(Execution).filter(
|
||||
Execution.agent_id.isnot(None)
|
||||
).order_by(Execution.created_at.desc()).first()
|
||||
|
||||
if not execution:
|
||||
print("❌ 没有找到执行记录")
|
||||
return
|
||||
|
||||
print(f"执行ID: {execution.id}")
|
||||
print(f"状态: {execution.status}")
|
||||
print()
|
||||
|
||||
# 获取Agent配置
|
||||
agent = db.query(Agent).filter(Agent.id == execution.agent_id).first()
|
||||
if not agent:
|
||||
print("❌ 没有找到Agent")
|
||||
return
|
||||
|
||||
workflow_config = agent.workflow_config
|
||||
nodes = workflow_config.get('nodes', [])
|
||||
edges = workflow_config.get('edges', [])
|
||||
|
||||
# 找到Switch节点
|
||||
switch_node = None
|
||||
for node in nodes:
|
||||
if node.get('type') == 'switch':
|
||||
switch_node = node
|
||||
break
|
||||
|
||||
if not switch_node:
|
||||
print("❌ 没有找到Switch节点")
|
||||
return
|
||||
|
||||
print("=" * 80)
|
||||
print("Switch节点配置:")
|
||||
print("=" * 80)
|
||||
print(f"节点ID: {switch_node['id']}")
|
||||
print(f"字段: {switch_node['data'].get('field')}")
|
||||
print(f"Cases: {json.dumps(switch_node['data'].get('cases', {}), ensure_ascii=False, indent=2)}")
|
||||
print(f"Default: {switch_node['data'].get('default')}")
|
||||
print()
|
||||
|
||||
# 找到从Switch节点出发的边
|
||||
print("=" * 80)
|
||||
print("从Switch节点出发的边:")
|
||||
print("=" * 80)
|
||||
switch_edges = [e for e in edges if e.get('source') == switch_node['id']]
|
||||
for edge in switch_edges:
|
||||
print(f"边ID: {edge.get('id')}")
|
||||
print(f" sourceHandle: {edge.get('sourceHandle')}")
|
||||
print(f" target: {edge.get('target')}")
|
||||
print()
|
||||
|
||||
# 查看执行结果
|
||||
print("=" * 80)
|
||||
print("执行结果中的节点输出:")
|
||||
print("=" * 80)
|
||||
if execution.output_data and 'node_results' in execution.output_data:
|
||||
node_results = execution.output_data['node_results']
|
||||
if switch_node['id'] in node_results:
|
||||
switch_result = node_results[switch_node['id']]
|
||||
print(f"Switch节点输出: {json.dumps(switch_result, ensure_ascii=False, indent=2)}")
|
||||
else:
|
||||
print("❌ Switch节点没有输出结果")
|
||||
else:
|
||||
print("❌ 没有找到节点执行结果")
|
||||
|
||||
# 检查哪些分支节点执行了
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("执行了的分支节点:")
|
||||
print("=" * 80)
|
||||
if execution.output_data and 'node_results' in execution.output_data:
|
||||
node_results = execution.output_data['node_results']
|
||||
for edge in switch_edges:
|
||||
target_id = edge.get('target')
|
||||
if target_id in node_results:
|
||||
print(f"✅ {target_id} (sourceHandle: {edge.get('sourceHandle')})")
|
||||
else:
|
||||
print(f"❌ {target_id} (sourceHandle: {edge.get('sourceHandle')}) - 未执行")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
102
scripts/tools/publish_agent.py
Normal file
102
scripts/tools/publish_agent.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
发布Agent脚本
|
||||
"""
|
||||
import requests
|
||||
import sys
|
||||
|
||||
BASE_URL = "http://localhost:8037"
|
||||
|
||||
def login(username="admin", password="123456"):
|
||||
"""用户登录"""
|
||||
login_data = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/api/v1/auth/login", data=login_data)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ 登录失败: {response.status_code}")
|
||||
return None, None
|
||||
|
||||
token = response.json().get("access_token")
|
||||
if not token:
|
||||
print("❌ 登录失败: 未获取到token")
|
||||
return None, None
|
||||
|
||||
print(f"✅ 登录成功 (用户: {username})")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
return token, headers
|
||||
except Exception as e:
|
||||
print(f"❌ 登录异常: {str(e)}")
|
||||
return None, None
|
||||
|
||||
def deploy_agent(agent_id, headers):
|
||||
"""发布Agent"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/v1/agents/{agent_id}/deploy",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
agent = response.json()
|
||||
print(f"✅ Agent发布成功: {agent.get('name')} (状态: {agent.get('status')})")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 发布失败: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 发布异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def find_agent_by_name(agent_name, headers):
|
||||
"""通过名称查找Agent"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/v1/agents",
|
||||
headers=headers,
|
||||
params={"search": agent_name, "limit": 100}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"❌ 获取Agent列表失败: {response.status_code}")
|
||||
return None
|
||||
|
||||
agents = response.json()
|
||||
for agent in agents:
|
||||
if agent.get("name") == agent_name:
|
||||
return agent
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"❌ 查找Agent异常: {str(e)}")
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent_name = sys.argv[1] if len(sys.argv) > 1 else "知识库问答助手"
|
||||
|
||||
print(f"📝 发布Agent: {agent_name}\n")
|
||||
|
||||
# 登录
|
||||
token, headers = login()
|
||||
if not token:
|
||||
sys.exit(1)
|
||||
|
||||
# 查找Agent
|
||||
print(f"🔍 查找Agent: {agent_name}")
|
||||
agent = find_agent_by_name(agent_name, headers)
|
||||
if not agent:
|
||||
print(f"❌ 未找到Agent: {agent_name}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✅ 找到Agent: {agent.get('name')} (ID: {agent.get('id')}, 状态: {agent.get('status')})")
|
||||
|
||||
# 发布Agent
|
||||
print(f"\n🚀 发布Agent...")
|
||||
if deploy_agent(agent.get('id'), headers):
|
||||
print("\n✅ 完成!")
|
||||
else:
|
||||
sys.exit(1)
|
||||
279
scripts/tools/run_agent_test_cases.py
Normal file
279
scripts/tools/run_agent_test_cases.py
Normal file
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
读取 agent_test_cases.json(或 --cases 指定),批量执行 Agent 并做简单断言。
|
||||
规范见:(红头)agent测试用例文档.md
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
DEFAULT_CASES_FILE = "agent_test_cases.json"
|
||||
|
||||
|
||||
def _ensure_utf8_stdio() -> None:
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
for name in ("stdout", "stderr"):
|
||||
stream = getattr(sys, name, None)
|
||||
if stream is not None and hasattr(stream, "reconfigure"):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
_ensure_utf8_stdio()
|
||||
|
||||
|
||||
def _login(base_url: str, username: str, password: str, timeout: float) -> Optional[Dict[str, str]]:
|
||||
r = requests.post(
|
||||
f"{base_url}/api/v1/auth/login",
|
||||
data={"username": username, "password": password},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=timeout,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
print(f"[FAIL] 登录 {r.status_code}: {r.text[:500]}")
|
||||
return None
|
||||
token = r.json().get("access_token")
|
||||
if not token:
|
||||
print("[FAIL] 登录响应无 access_token")
|
||||
return None
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _resolve_agent_id(
|
||||
base_url: str,
|
||||
headers: Dict[str, str],
|
||||
agent: Dict[str, Any],
|
||||
timeout: float,
|
||||
) -> Optional[str]:
|
||||
if agent.get("id"):
|
||||
return str(agent["id"])
|
||||
name = agent.get("name")
|
||||
if not name:
|
||||
print("[FAIL] agent 需包含 id 或 name")
|
||||
return None
|
||||
r = requests.get(
|
||||
f"{base_url}/api/v1/agents",
|
||||
headers=headers,
|
||||
params={"search": name, "limit": 100},
|
||||
timeout=timeout,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
print(f"[FAIL] 查找 Agent {r.status_code}: {r.text[:500]}")
|
||||
return None
|
||||
agents: List[Dict[str, Any]] = r.json() or []
|
||||
exact = [a for a in agents if (a.get("name") or "").strip() == name]
|
||||
pick = exact[0] if exact else (agents[0] if agents else None)
|
||||
if not pick:
|
||||
print(f"[FAIL] 未找到 Agent: {name}")
|
||||
return None
|
||||
print(f"[OK] Agent: {pick.get('name')} ({pick['id']}) status={pick.get('status')}")
|
||||
return str(pick["id"])
|
||||
|
||||
|
||||
def _extract_output_text(output_data: Any) -> str:
|
||||
if output_data is None:
|
||||
return ""
|
||||
if isinstance(output_data, str):
|
||||
return output_data
|
||||
if isinstance(output_data, dict):
|
||||
for key in ("result", "output", "text", "content"):
|
||||
v = output_data.get(key)
|
||||
if v is not None:
|
||||
return v if isinstance(v, str) else str(v)
|
||||
return json.dumps(output_data, ensure_ascii=False)
|
||||
return str(output_data)
|
||||
|
||||
|
||||
def _poll_until_terminal(
|
||||
base_url: str,
|
||||
headers: Dict[str, str],
|
||||
execution_id: str,
|
||||
max_wait: float,
|
||||
poll_interval: float,
|
||||
timeout: float,
|
||||
) -> Tuple[str, Optional[Dict[str, Any]]]:
|
||||
deadline = time.time() + max_wait
|
||||
last_status = "unknown"
|
||||
while time.time() < deadline:
|
||||
sr = requests.get(
|
||||
f"{base_url}/api/v1/executions/{execution_id}/status",
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
if sr.status_code != 200:
|
||||
print(f"[WARN] status {sr.status_code}: {sr.text[:300]}")
|
||||
time.sleep(poll_interval)
|
||||
continue
|
||||
body = sr.json()
|
||||
last_status = str(body.get("status") or "")
|
||||
if last_status in ("completed", "failed", "cancelled", "awaiting_approval"):
|
||||
break
|
||||
time.sleep(poll_interval)
|
||||
dr = requests.get(
|
||||
f"{base_url}/api/v1/executions/{execution_id}",
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
if dr.status_code != 200:
|
||||
print(f"[FAIL] 获取执行详情 {dr.status_code}: {dr.text[:500]}")
|
||||
return last_status, None
|
||||
return last_status, dr.json()
|
||||
|
||||
|
||||
def _check_expect(text: str, status: str, detail: Optional[Dict[str, Any]], expect: Dict[str, Any]) -> List[str]:
|
||||
errors: List[str] = []
|
||||
want_status = expect.get("status", "completed")
|
||||
if status != want_status:
|
||||
errors.append(f"状态期望 {want_status!r},实际 {status!r}")
|
||||
if detail and status != "completed":
|
||||
em = detail.get("error_message")
|
||||
if em:
|
||||
errors.append(f"error_message: {em[:500]}")
|
||||
if not expect:
|
||||
return errors
|
||||
ci = bool(expect.get("case_insensitive"))
|
||||
hay = text if not ci else text.lower()
|
||||
for sub in expect.get("output_contains") or []:
|
||||
s = sub if not ci else sub.lower()
|
||||
if s not in hay:
|
||||
errors.append(f"输出应包含 {sub!r}")
|
||||
for sub in expect.get("output_not_contains") or []:
|
||||
s = sub if not ci else sub.lower()
|
||||
if s in hay:
|
||||
errors.append(f"输出不应包含 {sub!r}")
|
||||
return errors
|
||||
|
||||
|
||||
def _run_one_case(
|
||||
base_url: str,
|
||||
headers: Dict[str, str],
|
||||
defaults: Dict[str, Any],
|
||||
case: Dict[str, Any],
|
||||
) -> bool:
|
||||
cid = case.get("id", "(no-id)")
|
||||
title = case.get("name", "")
|
||||
print("\n" + "-" * 60)
|
||||
print(f"CASE {cid}" + (f" — {title}" if title else ""))
|
||||
|
||||
req_timeout = float(case.get("request_timeout_sec", defaults.get("request_timeout_sec", 120)))
|
||||
max_wait = float(case.get("max_wait_sec", defaults.get("max_wait_sec", 300)))
|
||||
poll_iv = float(case.get("poll_interval_sec", defaults.get("poll_interval_sec", 2)))
|
||||
|
||||
agent_id = _resolve_agent_id(base_url, headers, case.get("agent") or {}, req_timeout)
|
||||
if not agent_id:
|
||||
return False
|
||||
|
||||
message = case.get("message")
|
||||
if message is None:
|
||||
print("[FAIL] 缺少 message")
|
||||
return False
|
||||
|
||||
input_data: Dict[str, Any] = {"query": message, "USER_INPUT": message}
|
||||
extra = case.get("input_extra")
|
||||
if isinstance(extra, dict):
|
||||
input_data = {**extra, **input_data}
|
||||
|
||||
er = requests.post(
|
||||
f"{base_url}/api/v1/executions",
|
||||
headers=headers,
|
||||
json={"agent_id": agent_id, "input_data": input_data},
|
||||
timeout=req_timeout,
|
||||
)
|
||||
if er.status_code != 201:
|
||||
print(f"[FAIL] 创建执行 {er.status_code}: {er.text[:800]}")
|
||||
return False
|
||||
ex = er.json()
|
||||
eid = ex["id"]
|
||||
print(f"[OK] execution_id={eid}")
|
||||
|
||||
st, detail = _poll_until_terminal(base_url, headers, eid, max_wait, poll_iv, req_timeout)
|
||||
text = _extract_output_text((detail or {}).get("output_data"))
|
||||
expect = case.get("expect") or {}
|
||||
errs = _check_expect(text, st, detail, expect)
|
||||
if errs:
|
||||
for e in errs:
|
||||
print(f"[FAIL] {e}")
|
||||
if text:
|
||||
print("[OUTPUT_PREVIEW]")
|
||||
print(text[:2000] + ("…" if len(text) > 2000 else ""))
|
||||
return False
|
||||
print(f"[OK] 通过 status={st}")
|
||||
if text:
|
||||
print("[OUTPUT_PREVIEW]")
|
||||
print(text[:1200] + ("…" if len(text) > 1200 else ""))
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="批量运行 Agent 测试用例(JSON)")
|
||||
ap.add_argument(
|
||||
"--cases",
|
||||
default=os.environ.get("AGENT_TEST_CASES", DEFAULT_CASES_FILE),
|
||||
help=f"用例 JSON 路径(默认 {DEFAULT_CASES_FILE})",
|
||||
)
|
||||
ap.add_argument("--username", default=None)
|
||||
ap.add_argument("--password", default=None)
|
||||
ap.add_argument("--base-url", default=None, help="覆盖 defaults.base_url / API_BASE_URL")
|
||||
args = ap.parse_args()
|
||||
|
||||
path = args.cases
|
||||
if not os.path.isfile(path):
|
||||
print(f"[FAIL] 找不到用例文件: {path}")
|
||||
print("请先按 (红头)agent测试用例文档.md 创建 JSON,或复制示例为 agent_test_cases.json")
|
||||
return 2
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
spec = json.load(f)
|
||||
|
||||
defaults = spec.get("defaults") or {}
|
||||
base_url = (
|
||||
args.base_url
|
||||
or defaults.get("base_url")
|
||||
or os.environ.get("API_BASE_URL", "http://localhost:8037")
|
||||
)
|
||||
base_url = base_url.rstrip("/")
|
||||
|
||||
username = args.username or defaults.get("username", "admin")
|
||||
password = args.password or defaults.get("password", "123456")
|
||||
req_timeout = float(defaults.get("request_timeout_sec", 120))
|
||||
|
||||
print(f"API: {base_url}")
|
||||
print(f"用例文件: {path}")
|
||||
|
||||
headers = _login(base_url, username, password, req_timeout)
|
||||
if not headers:
|
||||
return 3
|
||||
|
||||
cases: List[Dict[str, Any]] = spec.get("cases") or []
|
||||
if not cases:
|
||||
print("[FAIL] cases 为空")
|
||||
return 4
|
||||
|
||||
ok, skip, fail = 0, 0, 0
|
||||
for case in cases:
|
||||
if case.get("enabled") is False:
|
||||
print(f"\n[SKIP] {case.get('id', '?')}")
|
||||
skip += 1
|
||||
continue
|
||||
if _run_one_case(base_url, headers, defaults, case):
|
||||
ok += 1
|
||||
else:
|
||||
fail += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"汇总: 通过 {ok} 失败 {fail} 跳过 {skip}")
|
||||
return 0 if fail == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
43
scripts/tools/开放端口脚本.sh
Normal file
43
scripts/tools/开放端口脚本.sh
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# 开放8037和8038端口的脚本
|
||||
|
||||
echo "正在检查并开放端口 8037 和 8038..."
|
||||
|
||||
# 方法1: 使用firewalld (CentOS/RHEL 7+)
|
||||
if command -v firewall-cmd &> /dev/null; then
|
||||
echo "检测到 firewalld,使用 firewalld 开放端口..."
|
||||
sudo firewall-cmd --permanent --add-port=8037/tcp
|
||||
sudo firewall-cmd --permanent --add-port=8038/tcp
|
||||
sudo firewall-cmd --reload
|
||||
echo "✅ firewalld 端口已开放"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 方法2: 使用ufw (Ubuntu/Debian)
|
||||
if command -v ufw &> /dev/null; then
|
||||
echo "检测到 ufw,使用 ufw 开放端口..."
|
||||
sudo ufw allow 8037/tcp
|
||||
sudo ufw allow 8038/tcp
|
||||
echo "✅ ufw 端口已开放"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 方法3: 使用iptables
|
||||
echo "使用 iptables 开放端口..."
|
||||
sudo iptables -I INPUT -p tcp --dport 8037 -j ACCEPT
|
||||
sudo iptables -I INPUT -p tcp --dport 8038 -j ACCEPT
|
||||
|
||||
# 保存iptables规则(根据系统不同)
|
||||
if [ -f /etc/redhat-release ]; then
|
||||
# CentOS/RHEL
|
||||
sudo service iptables save 2>/dev/null || sudo iptables-save > /etc/sysconfig/iptables
|
||||
elif [ -f /etc/debian_version ]; then
|
||||
# Debian/Ubuntu
|
||||
sudo iptables-save > /etc/iptables/rules.v4 2>/dev/null || echo "请手动保存iptables规则"
|
||||
fi
|
||||
|
||||
echo "✅ iptables 端口已开放"
|
||||
echo ""
|
||||
echo "⚠️ 注意:如果使用云服务器(如腾讯云、阿里云等),还需要在云控制台配置安全组规则:"
|
||||
echo " - 开放入站规则:TCP 8037"
|
||||
echo " - 开放入站规则:TCP 8038"
|
||||
63
scripts/tools/执行数据库迁移.sh
Normal file
63
scripts/tools/执行数据库迁移.sh
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 数据库迁移脚本
|
||||
# 用于创建模板市场相关的数据库表
|
||||
|
||||
echo "=========================================="
|
||||
echo "执行数据库迁移 - 模板市场"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 数据库连接信息(从config.py中获取)
|
||||
DB_HOST="gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com"
|
||||
DB_PORT="24936"
|
||||
DB_USER="root"
|
||||
DB_NAME="agent_db"
|
||||
SQL_FILE="backend/create_template_market_tables.sql"
|
||||
|
||||
# 检查SQL文件是否存在
|
||||
if [ ! -f "$SQL_FILE" ]; then
|
||||
echo "❌ SQL文件不存在: $SQL_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📄 SQL文件: $SQL_FILE"
|
||||
echo "🔗 数据库: $DB_NAME @ $DB_HOST:$DB_PORT"
|
||||
echo ""
|
||||
|
||||
# 检查mysql命令是否可用
|
||||
if ! command -v mysql &> /dev/null; then
|
||||
echo "⚠️ mysql命令不可用,请手动执行SQL脚本"
|
||||
echo ""
|
||||
echo "手动执行步骤:"
|
||||
echo "1. 连接到数据库:"
|
||||
echo " mysql -h $DB_HOST -P $DB_PORT -u $DB_USER -p $DB_NAME"
|
||||
echo ""
|
||||
echo "2. 执行SQL:"
|
||||
echo " source $(pwd)/$SQL_FILE;"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 提示输入密码
|
||||
echo "请输入数据库密码:"
|
||||
read -s DB_PASSWORD
|
||||
|
||||
echo ""
|
||||
echo "正在执行SQL脚本..."
|
||||
echo ""
|
||||
|
||||
# 执行SQL脚本
|
||||
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" < "$SQL_FILE"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo "✅ 数据库迁移完成!"
|
||||
echo ""
|
||||
echo "📊 验证表是否创建:"
|
||||
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "SHOW TABLES LIKE 'workflow_template%'; SHOW TABLES LIKE 'template_%';"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ 数据库迁移失败,请检查错误信息"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user