From ab1589921abd9e63bce0aac152e650cf6c0fe247 Mon Sep 17 00:00:00 2001 From: renjianbo <18691577328@163.com> Date: Sun, 10 May 2026 19:50:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D35=E4=B8=AA=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E4=B8=8E=E5=8A=9F=E8=83=BD=E7=BC=BA=E9=99=B7=EF=BC=8C?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E7=9F=A5=E8=AF=86=E8=BF=9B=E5=8C=96/?= =?UTF-8?q?=E6=95=B0=E5=AD=97=E5=AD=AA=E7=94=9F/=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?=E9=87=87=E9=9B=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 安全修复 (12项) - Webhook接口添加全局Token认证,过滤敏感请求头 - 修复JWT Base64 padding公式,防止签名验证绕过 - 数据库密码/飞书Token从源码移除,改为环境变量 - 工作流引擎添加路径遍历防护 (_resolve_safe_path) - eval()添加模板长度上限检查 - 审批API添加认证依赖 - 前端v-html增强XSS转义,console.log仅开发模式输出 - 500错误不再暴露内部异常详情 ## Agent运行时修复 (7项) - 删除_inject_knowledge_context中未定义db变量的finally块 - 工具执行添加try/except保护,异常不崩溃Agent - LLM重试计入budget计数器 - self_review异常时passed=False - max_iterations截断标记success=False - 工具参数JSON解析失败时记录警告日志 - run()开始时重置_llm_invocations计数器 ## 配置与基础设施 - DEBUG默认False,SQL_ECHO独立配置项 - init_db()补全13个缺失模型导入 - 新增WEBHOOK_AUTH_TOKEN/SQL_ECHO配置项 - 新增.env.example模板文件 ## 前端修复 (12项) - 登录改用URLSearchParams替代FormData - 401拦截器通过Pinia store统一清理状态 - SSE流超时从60s延长至300s - final/error事件时清除streamTimeout - localStorage聊天记录添加24h TTL - safeParseArgCount替代模板中裸JSON.parse - fetchUser 401时同时清除user对象 ## 新增模块 - 知识进化: knowledge_extractor/retriever/tasks - 数字孪生: shadow_executor/comparison模型 - 行为采集: behavior_middleware/collector/fingerprint_engine - 代码审查: code_review_agent/document_review_agent - 反馈学习: feedback_learner - 瓶颈检测/优化引擎/成本估算/需求估算 - 速率限制器 (rate_limiter) - Alembic迁移 015-020 ## 文档 - 商业化落地计划 - 8篇docs文档 (架构/API/部署/开发/贡献等) - Docker Compose生产配置 Co-Authored-By: Claude Opus 4.6 --- README.md | 149 ++-- backend/.env.example | 54 ++ backend/Dockerfile | 24 + .../versions/015_add_agent_execution_logs.py | 58 ++ .../versions/016_add_user_behavior_logs.py | 41 ++ .../versions/017_add_knowledge_entries.py | 47 ++ .../versions/018_add_user_fingerprints.py | 36 + .../versions/019_add_shadow_comparisons.py | 39 + .../versions/020_add_feedback_records.py | 43 ++ backend/app/agent_runtime/core.py | 126 +++- backend/app/agent_runtime/schemas.py | 11 +- backend/app/api/approval.py | 8 +- backend/app/api/goals.py | 50 ++ backend/app/api/webhooks.py | 108 +-- backend/app/core/behavior_middleware.py | 110 +++ backend/app/core/celery_app.py | 1 + backend/app/core/config.py | 14 +- backend/app/core/database.py | 15 +- backend/app/core/rate_limiter.py | 136 ++++ backend/app/core/tools_bootstrap.py | 8 +- backend/app/main.py | 53 ++ backend/app/models/__init__.py | 8 +- backend/app/models/agent_execution_log.py | 57 ++ backend/app/models/feedback_record.py | 39 + backend/app/models/knowledge_entry.py | 67 ++ backend/app/models/shadow_comparison.py | 35 + backend/app/models/user_behavior.py | 45 ++ backend/app/models/user_fingerprint.py | 38 + backend/app/services/behavior_collector.py | 154 ++++ backend/app/services/bottleneck_detector.py | 182 +++++ backend/app/services/builtin_tools.py | 322 ++++++++- backend/app/services/code_review_agent.py | 215 ++++++ backend/app/services/cost_estimator.py | 78 ++ backend/app/services/decision_authorizer.py | 299 ++++++++ backend/app/services/document_review_agent.py | 257 +++++++ backend/app/services/execution_logger.py | 147 ++-- backend/app/services/feedback_learner.py | 209 ++++++ backend/app/services/fingerprint_engine.py | 221 ++++++ backend/app/services/knowledge_extractor.py | 229 ++++++ backend/app/services/knowledge_retriever.py | 115 +++ backend/app/services/main_agent_service.py | 213 +++++- backend/app/services/optimization_engine.py | 168 +++++ backend/app/services/requirement_estimator.py | 351 +++++++++ backend/app/services/shadow_executor.py | 216 ++++++ backend/app/services/tool_discovery.py | 272 +++++++ backend/app/services/tool_registry.py | 14 +- backend/app/services/workflow_engine.py | 55 +- backend/app/tasks/knowledge_tasks.py | 19 + deploy/filebeat.yml | 24 + deploy/locustfile.py | 48 ++ deploy/playwright.config.ts | 16 + deploy/prometheus.yml | 24 + docker-compose.prod.yml | 161 +++++ docs/UI_UX_Design_Document.md | 669 ++++++++++++++++++ docs/api-reference.md | 403 +++++++++++ docs/architecture.md | 161 +++++ docs/contributing.md | 174 +++++ docs/deployment-guide.md | 237 +++++++ docs/development-guide.md | 241 +++++++ docs/index.md | 45 ++ docs/project-structure.md | 149 ++++ docs/quickstart.md | 110 +++ .../(红头)Windows服务器启动与重启唯一指南.md | 82 ++- docs/商业化落地计划.md | 304 ++++++++ frontend/Dockerfile | 14 + frontend/nginx.conf | 37 + frontend/src/api/index.ts | 11 +- frontend/src/router/index.ts | 12 + frontend/src/stores/goal.ts | 6 + frontend/src/stores/user.ts | 13 +- frontend/src/views/AgentChat.vue | 25 +- frontend/src/views/AgentConfig.vue | 233 +++++- frontend/src/views/DigitalTwin.vue | 406 +++++++++++ frontend/src/views/GoalDetail.vue | 454 +++++++++++- frontend/src/views/KnowledgeDashboard.vue | 455 ++++++++++++ scripts/demo_digital_employee.py | 2 +- scripts/startup/stop_aiagent.ps1 | 35 +- 77 files changed, 9442 insertions(+), 265 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/alembic/versions/015_add_agent_execution_logs.py create mode 100644 backend/alembic/versions/016_add_user_behavior_logs.py create mode 100644 backend/alembic/versions/017_add_knowledge_entries.py create mode 100644 backend/alembic/versions/018_add_user_fingerprints.py create mode 100644 backend/alembic/versions/019_add_shadow_comparisons.py create mode 100644 backend/alembic/versions/020_add_feedback_records.py create mode 100644 backend/app/core/behavior_middleware.py create mode 100644 backend/app/core/rate_limiter.py create mode 100644 backend/app/models/agent_execution_log.py create mode 100644 backend/app/models/feedback_record.py create mode 100644 backend/app/models/knowledge_entry.py create mode 100644 backend/app/models/shadow_comparison.py create mode 100644 backend/app/models/user_behavior.py create mode 100644 backend/app/models/user_fingerprint.py create mode 100644 backend/app/services/behavior_collector.py create mode 100644 backend/app/services/bottleneck_detector.py create mode 100644 backend/app/services/code_review_agent.py create mode 100644 backend/app/services/cost_estimator.py create mode 100644 backend/app/services/decision_authorizer.py create mode 100644 backend/app/services/document_review_agent.py create mode 100644 backend/app/services/feedback_learner.py create mode 100644 backend/app/services/fingerprint_engine.py create mode 100644 backend/app/services/knowledge_extractor.py create mode 100644 backend/app/services/knowledge_retriever.py create mode 100644 backend/app/services/optimization_engine.py create mode 100644 backend/app/services/requirement_estimator.py create mode 100644 backend/app/services/shadow_executor.py create mode 100644 backend/app/services/tool_discovery.py create mode 100644 backend/app/tasks/knowledge_tasks.py create mode 100644 deploy/filebeat.yml create mode 100644 deploy/locustfile.py create mode 100644 deploy/playwright.config.ts create mode 100644 deploy/prometheus.yml create mode 100644 docker-compose.prod.yml create mode 100644 docs/UI_UX_Design_Document.md create mode 100644 docs/api-reference.md create mode 100644 docs/architecture.md create mode 100644 docs/contributing.md create mode 100644 docs/deployment-guide.md create mode 100644 docs/development-guide.md create mode 100644 docs/index.md create mode 100644 docs/project-structure.md create mode 100644 docs/quickstart.md create mode 100644 docs/商业化落地计划.md create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf create mode 100644 frontend/src/views/DigitalTwin.vue create mode 100644 frontend/src/views/KnowledgeDashboard.vue diff --git a/README.md b/README.md index fbc8d7c..a330705 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,90 @@ -# 天工智能体平台 +# 🤖 天工智能体平台 -一个支持可视化工作流设计和自主智能Agent配置的AI平台。 +> **Tiangong AI Agent Platform** — 一个支持可视化工作流设计和自主智能 Agent 配置的 AI 平台。 -## 🚀 快速开始 +--- -### 前置要求 - -- Node.js 18+ 和 pnpm -- Python 3.11+ -- Docker 和 Docker Compose -- MySQL(使用腾讯云数据库) -- Redis 7+(或使用Docker) - -### 使用 Docker Compose 启动(推荐) +## 🚀 快速启动 ```bash -# 启动所有服务 +# 使用 Docker Compose 一键启动(推荐) docker-compose -f docker-compose.dev.yml up -d -# 查看日志 -docker-compose logs -f - -# 停止服务 -docker-compose down +# 访问地址 +# 前端:http://localhost:8038 +# API 文档:http://localhost:8037/docs ``` -### 本地开发 +--- -#### 前端开发 +## 📚 文档中心 -```bash -cd frontend -pnpm install -pnpm dev -``` +所有文档已统一整合至 **[`docs/` 目录](./docs/)**: -前端服务将在 http://localhost:8038 启动 +| 文档 | 说明 | 适用对象 | +|:-----|:------|:---------| +| [📖 快速开始指南](./docs/quickstart.md) | 5 分钟快速上手:安装、配置、启动 | 新用户、开发者 | +| [🏗️ 项目结构概览](./docs/project-structure.md) | 前端/后端目录结构与模块说明 | 开发者、维护者 | +| [🏛️ 架构设计文档](./docs/architecture.md) | 系统架构图、技术选型、数据流说明 | 架构师、高级开发者 | +| [🛠️ 开发指南](./docs/development-guide.md) | 编码规范、测试指南、调试技巧 | 开发者 | +| [🚀 部署与运维指南](./docs/deployment-guide.md) | 生产环境部署、配置说明、日常运维 | DevOps、运维 | +| [🔌 API 参考](./docs/api-reference.md) | RESTful API 端点说明与请求/响应格式 | 前端开发者、集成方 | +| [🤝 贡献指南](./docs/contributing.md) | 如何参与贡献、Code Review 流程 | 贡献者 | -#### 后端开发 +--- -```bash -cd backend +## 🛠️ 技术栈 -# 创建虚拟环境 -python -m venv venv -source venv/bin/activate # Windows: venv\Scripts\activate +| 层级 | 技术 | 版本 | +|:----|:-----|:-----| +| 🖥 **前端** | Vue 3 + TypeScript + Vite + Pinia + Element Plus | 3.4+ | +| ⚙️ **后端** | Python FastAPI + SQLAlchemy + Pydantic + Celery | FastAPI 0.110+ | +| 🗄️ **数据库** | MySQL 8.0+(腾讯云) + Redis 7+ | — | +| 🤖 **AI 框架** | LangChain | 最新 | +| 🐳 **容器化** | Docker + Docker Compose | 最新 | -# 安装依赖 -pip install -r requirements.txt - -# 配置环境变量 -cp env.example .env -# 编辑 .env 文件(数据库已配置为腾讯云MySQL) - -# 运行数据库迁移 -alembic upgrade head - -# 启动开发服务器 -uvicorn app.main:app --reload - -# 启动 Celery Worker(新终端) -celery -A app.core.celery_app worker --loglevel=info -``` - -后端服务将在 http://localhost:8037 启动 - -API文档:http://localhost:8037/docs +--- ## 📁 项目结构 ``` aiagent/ -├── frontend/ # 前端项目(Vue 3 + TypeScript) -├── backend/ # 后端项目(Python FastAPI) -├── docker-compose.dev.yml # 开发环境Docker配置 -└── README.md # 项目说明 +├── frontend/ # 前端项目(Vue 3 + TypeScript) +│ ├── src/views/ # 页面组件 +│ └── src/components/ # 公共组件 +├── backend/ # 后端项目(Python FastAPI) +│ ├── app/modules/ # 业务模块 +│ ├── app/core/ # 核心功能 +│ └── app/models/ # 数据模型 +├── docs/ # 📚 统一文档目录 ← 所有文档在此 +│ ├── index.md # 文档首页 +│ ├── quickstart.md # 快速开始 +│ ├── project-structure.md # 项目结构 +│ ├── architecture.md # 架构设计 +│ ├── development-guide.md # 开发指南 +│ ├── deployment-guide.md # 部署运维 +│ ├── api-reference.md # API 参考 +│ └── contributing.md # 贡献指南 +├── docker-compose.dev.yml # 开发环境 Docker 配置 +├── nginx.conf # Nginx 反向代理配置 +└── README.md # 项目总览(本文档) ``` -## 🛠️ 技术栈 - -### 前端 -- Vue 3 + TypeScript + Vite -- Pinia(状态管理) -- Element Plus(UI组件) -- Vue Flow(工作流可视化) - -### 后端 -- Python FastAPI -- MySQL(腾讯云数据库) -- Redis(缓存和消息队列) -- Celery(异步任务) -- LangChain(Agent框架) - -## 📚 文档 - -详细技术方案请参考:[方案-优化版.md](./方案-优化版.md) +--- ## 📝 开发规范 -- 前端代码规范:ESLint + Prettier -- 后端代码规范:PEP 8 + Black -- Git提交规范:Conventional Commits -- 代码审查:必须通过Code Review +| 规范 | 工具 / 标准 | +|:-----|:------------| +| 前端代码 | ESLint + Prettier | +| 后端代码 | PEP 8 + Black + Flake8 | +| Git 提交 | [Conventional Commits](https://www.conventionalcommits.org/) | +| 代码审查 | 所有 PR 必须通过 Code Review | +| 测试 | 前端:Vitest / 后端:Pytest | -## 🧪 测试 +--- + +## 🧪 运行测试 ```bash # 前端测试 @@ -112,9 +93,15 @@ pnpm test # 后端测试 cd backend -pytest +pytest --cov=app ``` +--- + ## 📄 许可证 -MIT License +[MIT License](./LICENSE) + +--- + +> 💡 **完整文档请见 [`docs/` 目录](./docs/)** diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..3955ee4 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,54 @@ +# ═══════════════════════════════════════════════════════════════ +# 天工智能体平台 — 环境变量配置模板 +# 复制此文件为 .env 并填入真实值 +# cp .env.example .env +# ═══════════════════════════════════════════════════════════════ + +# 应用 +APP_NAME=天工智能体平台 +APP_VERSION=1.0.0 +DEBUG=False + +# 安全密钥(生产环境务必修改!) +SECRET_KEY=change-me-to-a-random-string +JWT_SECRET_KEY=change-me-to-a-random-jwt-secret +JWT_ALGORITHM=HS256 +JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 + +# 数据库 — MySQL +DATABASE_URL=mysql+pymysql://root:CHANGE_ME@localhost:3306/agent_db?charset=utf8mb4 + +# Redis +REDIS_URL=redis://localhost:6379/0 + +# CORS — 逗号分隔 +CORS_ORIGINS=http://localhost:8038,http://localhost:3000 + +# 外部访问地址(用于通知中的链接) +EXTERNAL_URL=http://localhost:8037 + +# ─── AI 模型 API Key ⚠️ 敏感信息 ─── +# 至少配置一个 +OPENAI_API_KEY= +OPENAI_BASE_URL=https://api.openai.com/v1 +DEEPSEEK_API_KEY= +DEEPSEEK_BASE_URL=https://api.deepseek.com +ANTHROPIC_API_KEY= +SILICONFLOW_API_KEY= +SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1 + +# ─── 飞书应用(可选) ─── +FEISHU_APP_ID= +FEISHU_APP_SECRET= +FEISHU_VERIFICATION_TOKEN= + +# ─── 执行预算 ─── +WORKFLOW_MAX_STEPS_PER_RUN=2000 +WORKFLOW_MAX_LLM_INVOCATIONS_PER_RUN=200 +WORKFLOW_MAX_TOOL_CALLS_PER_RUN=500 + +# ─── 可选配置 ─── +BING_SEARCH_API_KEY= +SEARCH_PROXY= +TESSERACT_CMD= +MEMORY_PERSIST_DB_ENABLED=True diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..fbb4af3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libffi-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 安装 Python 依赖 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir uvicorn[standard] + +# 复制应用代码 +COPY . . + +# 暴露端口 +EXPOSE 8037 + +# 启动命令(数据库迁移 + 服务启动) +CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8037"] diff --git a/backend/alembic/versions/015_add_agent_execution_logs.py b/backend/alembic/versions/015_add_agent_execution_logs.py new file mode 100644 index 0000000..4b354e1 --- /dev/null +++ b/backend/alembic/versions/015_add_agent_execution_logs.py @@ -0,0 +1,58 @@ +"""add agent_execution_logs table + +Revision ID: 015_add_agent_execution_logs +Revises: 014_add_schedule_goal_fields +Create Date: 2026-05-10 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.mysql import CHAR + +revision = "015_add_agent_execution_logs" +down_revision = "014_add_schedule_goal_fields" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "agent_execution_logs", + sa.Column("id", CHAR(36), primary_key=True, comment="日志ID"), + sa.Column("agent_id", sa.String(36), nullable=True, comment="Agent ID"), + sa.Column("agent_name", sa.String(200), nullable=True, comment="Agent 名称"), + sa.Column("goal_id", sa.String(36), nullable=True, comment="关联 Goal ID"), + sa.Column("task_id", sa.String(36), nullable=True, comment="关联 Task ID"), + sa.Column("user_id", sa.String(36), nullable=True, comment="用户 ID"), + sa.Column("session_id", sa.String(100), nullable=True, comment="会话标识"), + sa.Column("input_text", sa.Text, nullable=True, comment="用户输入文本"), + sa.Column("output_text", sa.Text, nullable=True, comment="Agent 输出文本"), + sa.Column("output_truncated", sa.Boolean, default=False, comment="输出是否被截断"), + sa.Column("success", sa.Boolean, default=True, comment="是否成功"), + sa.Column("error_message", sa.Text, nullable=True, comment="错误信息"), + sa.Column("latency_ms", sa.Integer, nullable=True, comment="总耗时(ms)"), + sa.Column("iterations_used", sa.Integer, default=0, comment="ReAct 迭代次数"), + sa.Column("tool_calls_made", sa.Integer, default=0, comment="工具调用总次数"), + sa.Column("tool_chain", sa.JSON, nullable=True, comment="工具调用链"), + sa.Column("llm_calls", sa.JSON, nullable=True, comment="LLM调用明细"), + sa.Column("steps", sa.JSON, nullable=True, comment="执行步骤详情"), + sa.Column("model", sa.String(100), nullable=True, comment="使用的模型"), + sa.Column("provider", sa.String(50), nullable=True, comment="模型提供商"), + sa.Column("user_rating", sa.Integer, nullable=True, comment="用户评分(1-5)"), + sa.Column("user_feedback", sa.Text, nullable=True, comment="用户反馈文本"), + sa.Column("knowledge_extracted", sa.Boolean, default=False, comment="是否已提取知识"), + sa.Column("created_at", sa.DateTime, comment="创建时间"), + ) + op.create_index("ix_agent_exec_log_agent_id", "agent_execution_logs", ["agent_id"]) + op.create_index("ix_agent_exec_log_goal_id", "agent_execution_logs", ["goal_id"]) + op.create_index("ix_agent_exec_log_task_id", "agent_execution_logs", ["task_id"]) + op.create_index("ix_agent_exec_log_user_id", "agent_execution_logs", ["user_id"]) + op.create_index("ix_agent_exec_log_created_at", "agent_execution_logs", ["created_at"]) + + +def downgrade() -> None: + op.drop_index("ix_agent_exec_log_created_at", table_name="agent_execution_logs") + op.drop_index("ix_agent_exec_log_user_id", table_name="agent_execution_logs") + op.drop_index("ix_agent_exec_log_task_id", table_name="agent_execution_logs") + op.drop_index("ix_agent_exec_log_goal_id", table_name="agent_execution_logs") + op.drop_index("ix_agent_exec_log_agent_id", table_name="agent_execution_logs") + op.drop_table("agent_execution_logs") diff --git a/backend/alembic/versions/016_add_user_behavior_logs.py b/backend/alembic/versions/016_add_user_behavior_logs.py new file mode 100644 index 0000000..63e3bb9 --- /dev/null +++ b/backend/alembic/versions/016_add_user_behavior_logs.py @@ -0,0 +1,41 @@ +"""add user_behavior_logs table + +Revision ID: 016_add_user_behavior_logs +Revises: 015_add_agent_execution_logs +Create Date: 2026-05-10 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.mysql import CHAR + +revision = "016_add_user_behavior_logs" +down_revision = "015_add_agent_execution_logs" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "user_behavior_logs", + sa.Column("id", CHAR(36), primary_key=True, comment="日志ID"), + sa.Column("user_id", sa.String(36), nullable=False, comment="用户 ID"), + sa.Column("category", sa.String(30), nullable=False, comment="行为类别"), + sa.Column("action", sa.String(100), nullable=False, comment="具体动作"), + sa.Column("context", sa.JSON, nullable=True, comment="行为上下文"), + sa.Column("result", sa.JSON, nullable=True, comment="行为结果"), + sa.Column("source", sa.String(50), nullable=True, comment="来源"), + sa.Column("session_id", sa.String(100), nullable=True, comment="会话标识"), + sa.Column("ip_address", sa.String(50), nullable=True, comment="客户端 IP"), + sa.Column("user_agent", sa.String(500), nullable=True, comment="用户代理"), + sa.Column("created_at", sa.DateTime, comment="发生时间"), + ) + op.create_index("ix_user_behavior_user_id", "user_behavior_logs", ["user_id"]) + op.create_index("ix_user_behavior_category", "user_behavior_logs", ["category"]) + op.create_index("ix_user_behavior_created_at", "user_behavior_logs", ["created_at"]) + + +def downgrade() -> None: + op.drop_index("ix_user_behavior_created_at", table_name="user_behavior_logs") + op.drop_index("ix_user_behavior_category", table_name="user_behavior_logs") + op.drop_index("ix_user_behavior_user_id", table_name="user_behavior_logs") + op.drop_table("user_behavior_logs") diff --git a/backend/alembic/versions/017_add_knowledge_entries.py b/backend/alembic/versions/017_add_knowledge_entries.py new file mode 100644 index 0000000..3b2cca5 --- /dev/null +++ b/backend/alembic/versions/017_add_knowledge_entries.py @@ -0,0 +1,47 @@ +"""add knowledge_entries table + +Revision ID: 017_add_knowledge_entries +Revises: 016_add_user_behavior_logs +Create Date: 2026-05-10 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.mysql import CHAR + +revision = "017_add_knowledge_entries" +down_revision = "016_add_user_behavior_logs" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "knowledge_entries", + sa.Column("id", CHAR(36), primary_key=True, comment="知识条目ID"), + sa.Column("title", sa.String(500), nullable=False, comment="知识标题"), + sa.Column("category", sa.String(30), nullable=False, comment="类别"), + sa.Column("tags", sa.JSON, nullable=True, comment="标签列表"), + sa.Column("situation", sa.Text, nullable=True, comment="适用场景"), + sa.Column("solution", sa.Text, nullable=True, comment="解决方案"), + sa.Column("caveats", sa.Text, nullable=True, comment="注意事项"), + sa.Column("source_execution_ids", sa.JSON, nullable=True, comment="原始执行日志ID"), + sa.Column("source_agent_name", sa.String(200), nullable=True, comment="来源Agent"), + sa.Column("source_model", sa.String(100), nullable=True, comment="来源模型"), + sa.Column("embedding_text", sa.Text, nullable=True, comment="embedding文本"), + sa.Column("embedding", sa.Text, nullable=True, comment="embedding向量"), + sa.Column("retrieval_count", sa.Integer, default=0, comment="被检索次数"), + sa.Column("success_rate", sa.Float, nullable=True, comment="应用成功率"), + sa.Column("extracted_by", sa.String(100), nullable=True, comment="提取方式"), + sa.Column("confidence", sa.Float, default=0.5, comment="提取置信度"), + sa.Column("is_active", sa.Boolean, default=True, comment="是否启用"), + sa.Column("created_at", sa.DateTime, comment="创建时间"), + sa.Column("updated_at", sa.DateTime, comment="更新时间"), + ) + op.create_index("ix_knowledge_entries_cat", "knowledge_entries", ["category"]) + op.create_index("ix_knowledge_entries_active", "knowledge_entries", ["is_active"]) + + +def downgrade() -> None: + op.drop_index("ix_knowledge_entries_active", table_name="knowledge_entries") + op.drop_index("ix_knowledge_entries_cat", table_name="knowledge_entries") + op.drop_table("knowledge_entries") diff --git a/backend/alembic/versions/018_add_user_fingerprints.py b/backend/alembic/versions/018_add_user_fingerprints.py new file mode 100644 index 0000000..5b81eb9 --- /dev/null +++ b/backend/alembic/versions/018_add_user_fingerprints.py @@ -0,0 +1,36 @@ +"""add user_fingerprints table + +Revision ID: 018_add_user_fingerprints +Revises: 017_add_knowledge_entries +Create Date: 2026-05-10 +""" +from alembic import op +import sqlalchemy as sa + +revision = "018_add_user_fingerprints" +down_revision = "017_add_knowledge_entries" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "user_fingerprints", + sa.Column("id", sa.String(36), primary_key=True, comment="指纹ID"), + sa.Column("user_id", sa.String(36), nullable=False, unique=True, comment="用户ID"), + sa.Column("preference_weights", sa.JSON, nullable=True, comment="偏好权重"), + sa.Column("decision_rules", sa.JSON, nullable=True, comment="决策规则"), + sa.Column("total_behaviors", sa.Integer, default=0, comment="总行为数"), + sa.Column("behaviors_by_category", sa.JSON, nullable=True, comment="按类别的行为分布"), + sa.Column("avg_response_time_ms", sa.Integer, nullable=True, comment="平均响应时间"), + sa.Column("model_version", sa.String(20), default="1.0", comment="模型版本"), + sa.Column("last_trained_at", sa.DateTime, nullable=True, comment="上次训练时间"), + sa.Column("created_at", sa.DateTime, comment="创建时间"), + sa.Column("updated_at", sa.DateTime, comment="更新时间"), + ) + op.create_index("ix_user_fingerprint_user_id", "user_fingerprints", ["user_id"]) + + +def downgrade() -> None: + op.drop_index("ix_user_fingerprint_user_id", table_name="user_fingerprints") + op.drop_table("user_fingerprints") diff --git a/backend/alembic/versions/019_add_shadow_comparisons.py b/backend/alembic/versions/019_add_shadow_comparisons.py new file mode 100644 index 0000000..f87c96c --- /dev/null +++ b/backend/alembic/versions/019_add_shadow_comparisons.py @@ -0,0 +1,39 @@ +"""add shadow_comparisons table + +Revision ID: 019_add_shadow_comparisons +Revises: 018_add_user_fingerprints +Create Date: 2026-05-10 +""" +from alembic import op +import sqlalchemy as sa + +revision = "019_add_shadow_comparisons" +down_revision = "018_add_user_fingerprints" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "shadow_comparisons", + sa.Column("id", sa.String(36), primary_key=True, comment="记录ID"), + sa.Column("user_id", sa.String(36), nullable=False, index=True, comment="用户ID"), + sa.Column("category", sa.String(30), nullable=False, comment="场景: code_review/email/document/decision"), + sa.Column("shadow_suggestion", sa.JSON, nullable=True, comment="数字分身生成的建议"), + sa.Column("shadow_confidence", sa.Float, default=0.5, comment="影子置信度"), + sa.Column("user_decision", sa.JSON, nullable=True, comment="用户实际操作"), + sa.Column("user_action", sa.String(50), nullable=True, comment="action: accept/modify/reject/ignore"), + sa.Column("match_score", sa.Float, nullable=True, comment="匹配分数(0-1)"), + sa.Column("match_detail", sa.JSON, nullable=True, comment="匹配详情"), + sa.Column("context", sa.JSON, nullable=True, comment="触发场景上下文"), + sa.Column("source_execution_id", sa.String(36), nullable=True, comment="关联执行日志ID"), + sa.Column("created_at", sa.DateTime, comment="创建时间"), + ) + op.create_index("ix_shadow_comp_user_id", "shadow_comparisons", ["user_id"]) + op.create_index("ix_shadow_comp_category", "shadow_comparisons", ["category"]) + + +def downgrade() -> None: + op.drop_index("ix_shadow_comp_category", table_name="shadow_comparisons") + op.drop_index("ix_shadow_comp_user_id", table_name="shadow_comparisons") + op.drop_table("shadow_comparisons") diff --git a/backend/alembic/versions/020_add_feedback_records.py b/backend/alembic/versions/020_add_feedback_records.py new file mode 100644 index 0000000..43846f7 --- /dev/null +++ b/backend/alembic/versions/020_add_feedback_records.py @@ -0,0 +1,43 @@ +"""add feedback_records table + +Revision ID: 020_add_feedback_records +Revises: 019_add_shadow_comparisons +Create Date: 2026-05-10 +""" +from alembic import op +import sqlalchemy as sa + +revision = "020_add_feedback_records" +down_revision = "019_add_shadow_comparisons" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "feedback_records", + sa.Column("id", sa.String(36), primary_key=True, comment="记录ID"), + sa.Column("user_id", sa.String(36), nullable=False, index=True, comment="用户ID"), + sa.Column("signal_type", sa.String(30), nullable=False, comment="thumbs_down/manual_edit/retry_command/reject_approval"), + sa.Column("severity", sa.Float, default=0.5, comment="严重程度(0-1)"), + sa.Column("execution_log_id", sa.String(36), nullable=True, index=True, comment="关联执行日志ID"), + sa.Column("agent_name", sa.String(200), nullable=True, comment="Agent名称"), + sa.Column("task_id", sa.String(36), nullable=True, comment="任务ID"), + sa.Column("original_output", sa.Text, nullable=True, comment="被否定的Agent输出"), + sa.Column("user_correction", sa.Text, nullable=True, comment="用户修正后的内容"), + sa.Column("feedback_context", sa.JSON, nullable=True, comment="反馈上下文"), + sa.Column("improvement_suggestion", sa.Text, nullable=True, comment="LLM生成的改进建议"), + sa.Column("learned", sa.Boolean, default=False, comment="是否已学习"), + sa.Column("lesson_summary", sa.Text, nullable=True, comment="学习后的教训总结"), + sa.Column("created_at", sa.DateTime, comment="创建时间"), + ) + op.create_index("ix_feedback_user_id", "feedback_records", ["user_id"]) + op.create_index("ix_feedback_exec_log_id", "feedback_records", ["execution_log_id"]) + op.create_index("ix_feedback_signal_type", "feedback_records", ["signal_type"]) + + +def downgrade() -> None: + op.drop_index("ix_feedback_signal_type", table_name="feedback_records") + op.drop_index("ix_feedback_exec_log_id", table_name="feedback_records") + op.drop_index("ix_feedback_user_id", table_name="feedback_records") + op.drop_table("feedback_records") diff --git a/backend/app/agent_runtime/core.py b/backend/app/agent_runtime/core.py index 5fd87b5..1fb7c23 100644 --- a/backend/app/agent_runtime/core.py +++ b/backend/app/agent_runtime/core.py @@ -31,6 +31,8 @@ from app.services.agent_learning_service import ( load_relevant_patterns, save_learning_pattern, ) +from app.services.execution_logger import execution_logger as _exec_logger +from app.services.knowledge_retriever import knowledge_retriever logger = logging.getLogger(__name__) @@ -115,6 +117,49 @@ class AgentRuntime: # 返回 True 表示预算充足;返回 False 或抛出异常表示超限 self.on_llm_invocation: Optional[Callable[[], Any]] = None + def _build_execution_log_kwargs(self, user_input: str, result: AgentResult, latency_ms: int) -> dict: + """从 AgentResult 构建 execution_logger 所需的参数字典。""" + tool_chain = [] + for s in result.steps: + if s.type == "tool_result" and s.tool_name: + tool_chain.append({ + "tool_name": s.tool_name, + "tool_input": s.tool_input, + "tool_output": s.tool_result[:500] if s.tool_result else None, + }) + steps_summary = [ + {"iteration": s.iteration, "type": s.type, "tool_name": s.tool_name, + "content": (s.content or "")[:300]} + for s in result.steps[-20:] # 最多保留最近 20 步 + ] + return dict( + agent_id=None, # 由调用方设置 + agent_name=self.config.name, + user_id=self.config.user_id, + session_id=self.context.session_id, + input_text=user_input, + output_text=result.content, + output_truncated=result.truncated, + success=result.success, + error_message=result.error, + latency_ms=latency_ms, + iterations_used=result.iterations_used, + tool_calls_made=result.tool_calls_made, + tool_chain=tool_chain if tool_chain else None, + steps=steps_summary if steps_summary else None, + model=self.config.llm.model, + provider=self.config.llm.provider, + ) + + def _fire_execution_log(self, user_input: str, result: AgentResult, start_time: float): + """Fire-and-forget 记录执行日志(非阻塞)。""" + try: + latency_ms = int((time.time() - start_time) * 1000) + kwargs = self._build_execution_log_kwargs(user_input, result, latency_ms) + _exec_logger.log_execution_fire_and_forget(**kwargs) + except Exception: + pass # 日志记录失败不影响主流程 + async def run(self, user_input: str) -> AgentResult: """ 执行 Agent 单轮对话。 @@ -124,12 +169,17 @@ class AgentRuntime: max_iter = max(1, self.config.llm.max_iterations) self.context.iteration = 0 self.context.tool_calls_made = 0 + self._llm_invocations = 0 # 每次 run() 重置 LLM 调用计数 + _run_start = time.time() # 执行开始时间,用于计算总延迟 # 1. 首次运行时加载长期记忆到 system prompt if not self._memory_context_loaded: await self._inject_memory_context(user_input) self._memory_context_loaded = True + # 1.5 知识检索增强:从知识库注入相关经验到 system prompt + await self._inject_knowledge_context(user_input) + # 2. 追加用户消息 self.context.add_user_message(user_input) @@ -166,10 +216,12 @@ class AgentRuntime: logger.warning(err) steps.append(AgentStep(iteration=self.context.iteration, type="final", content=err)) await self.memory.save_context(user_input, err, self.context.messages) - return AgentResult(success=False, content=err, truncated=True, + result = AgentResult(success=False, content=err, truncated=True, iterations_used=self.context.iteration, tool_calls_made=self.context.tool_calls_made, steps=steps, error=err) + self._fire_execution_log(user_input, result, _run_start) + return result # 调用外部 LLM 预算回调(WorkflowEngine 注入,将 Agent 的 LLM 计入工作流预算) if self.on_llm_invocation: @@ -180,10 +232,12 @@ class AgentRuntime: logger.warning(err) steps.append(AgentStep(iteration=self.context.iteration, type="final", content=err)) await self.memory.save_context(user_input, err, self.context.messages) - return AgentResult(success=False, content=err, truncated=True, + result = AgentResult(success=False, content=err, truncated=True, iterations_used=self.context.iteration, tool_calls_made=self.context.tool_calls_made, steps=steps, error=str(e)) + self._fire_execution_log(user_input, result, _run_start) + return result # 调用 LLM try: @@ -203,14 +257,17 @@ class AgentRuntime: type="tool_result", content=f"LLM 调用失败(可重试): {err_str}", )) + self._llm_invocations += 1 # 重试也计入 LLM 调用预算 continue - return AgentResult( + result = AgentResult( success=False, content=f"LLM 调用失败: {err_str}", iterations_used=self.context.iteration, tool_calls_made=self.context.tool_calls_made, error=err_str, ) + self._fire_execution_log(user_input, result, _run_start) + return result # 记录 LLM 调用次数(内部计数) self._llm_invocations += 1 @@ -272,13 +329,15 @@ class AgentRuntime: ) # 提取知识到全局知识池(Agent 间知识共享) await self._extract_global_knowledge(user_input, final_text, steps, review_score) - return AgentResult( + result = AgentResult( success=True, content=final_text, iterations_used=self.context.iteration, tool_calls_made=self.context.tool_calls_made, steps=steps, ) + self._fire_execution_log(user_input, result, _run_start) + return result # 有工具调用 → 先记录 assistant 消息(含 tool_calls) self.context.add_assistant_message(content or "", tool_calls, reasoning) @@ -290,6 +349,8 @@ class AgentRuntime: try: tc_args_list.append(json.loads(tc["function"].get("arguments", "{}"))) except (json.JSONDecodeError, TypeError): + raw_args = tc["function"].get("arguments", "") + logger.warning("工具参数 JSON 解析失败,使用空对象: %.200s", str(raw_args)) tc_args_list.append({}) steps.append(AgentStep( @@ -339,7 +400,13 @@ class AgentRuntime: # decision == "approved" → 继续执行 logger.info("Agent 执行工具 [%s]: %s", tname, targs) - result = await self.tool_manager.execute(tname, targs) + try: + result = await self.tool_manager.execute(tname, targs) + except Exception as tool_err: + logger.error("工具 '%s' 执行异常: %s", tname, tool_err, exc_info=True) + result = json.dumps({ + "error": f"工具 '{tname}' 执行异常: {tool_err}" + }, ensure_ascii=False) steps.append(AgentStep( iteration=self.context.iteration, @@ -359,10 +426,12 @@ class AgentRuntime: logger.warning(err) steps.append(AgentStep(iteration=self.context.iteration, type="tool_result", content=err, tool_name=tname)) - return AgentResult(success=False, content=err, truncated=True, + result = AgentResult(success=False, content=err, truncated=True, iterations_used=self.context.iteration, tool_calls_made=self.context.tool_calls_made, steps=steps, error=err) + self._fire_execution_log(user_input, result, _run_start) + return result if self.on_tool_executed: try: @@ -388,10 +457,10 @@ class AgentRuntime: logger.warning("Agent 达到最大迭代次数 (%s)", max_iter) await self.memory.save_context(user_input, last_content or "(已达最大迭代次数)", self.context.messages) - # 保存学习模式(即便截断,工具调用模式仍有参考价值) + # 保存学习模式(即使截断,标记为未成功以便后续分析) if self.config.memory.learning_enabled: await self._save_learning_pattern( - user_input, steps, success=True, + user_input, steps, success=False, iterations_used=self.context.iteration, tool_calls_made=self.context.tool_calls_made, ) @@ -404,14 +473,18 @@ class AgentRuntime: type="final", content=last_content, )) - return AgentResult( - success=True, - content=last_content or "已达最大迭代次数,但模型未返回最终回答。", + truncation_msg = f"已达最大迭代次数 ({max_iter}),任务被截断" + result = AgentResult( + success=False, + content=last_content or truncation_msg, truncated=True, iterations_used=self.context.iteration, tool_calls_made=self.context.tool_calls_made, steps=steps, + error=truncation_msg, ) + self._fire_execution_log(user_input, result, _run_start) + return result async def run_stream(self, user_input: str) -> AsyncGenerator[dict, None]: """ @@ -433,6 +506,9 @@ class AgentRuntime: await self._inject_memory_context(user_input) self._memory_context_loaded = True + # 1.5 知识检索增强:从知识库注入相关经验到 system prompt + await self._inject_knowledge_context(user_input) + # 2. 追加用户消息 self.context.add_user_message(user_input) @@ -581,6 +657,8 @@ class AgentRuntime: try: tc_args_list.append(json.loads(tc["function"].get("arguments", "{}"))) except (json.JSONDecodeError, TypeError): + raw_args = tc["function"].get("arguments", "") + logger.warning("工具参数 JSON 解析失败,使用空对象: %.200s", str(raw_args)) tc_args_list.append({}) yield { @@ -654,7 +732,13 @@ class AgentRuntime: # decision == "approved" → 继续执行 logger.info("Agent 执行工具 [%s]: %s", tname, targs) - result = await self.tool_manager.execute(tname, targs) + try: + result = await self.tool_manager.execute(tname, targs) + except Exception as tool_err: + logger.error("工具 '%s' 执行异常: %s", tname, tool_err, exc_info=True) + result = json.dumps({ + "error": f"工具 '{tname}' 执行异常: {tool_err}" + }, ensure_ascii=False) # yield tool_result 事件 yield { @@ -758,9 +842,18 @@ class AgentRuntime: except Exception as e: logger.warning("加载学习模式失败: %s", e) return "" - finally: - if db: - db.close() + + async def _inject_knowledge_context(self, query: str) -> None: + """从知识进化库检索相关经验并注入 system prompt。""" + try: + enriched = knowledge_retriever.inject_knowledge( + self.context.system_prompt, query + ) + if enriched != self.context.system_prompt: + self.context.set_system_prompt(enriched) + logger.info("Agent 已注入相关知识库经验") + except Exception as e: + logger.debug("知识检索注入跳过: %s", e) async def _save_learning_pattern( self, query: str, steps: List[AgentStep], @@ -911,7 +1004,8 @@ class AgentRuntime: } except Exception as e: logger.warning("self_review 执行失败: %s", e) - return {"score": 0.5, "passed": True, "issues": [], "suggestions": [], "error": str(e)} + return {"score": 0.0, "passed": False, "issues": [f"self_review 执行异常: {e}"], + "suggestions": ["请检查 self_review 配置或 LLM 可用性"], "error": str(e)} @staticmethod def _extract_tool_calls(response: Any) -> List[Dict[str, Any]]: diff --git a/backend/app/agent_runtime/schemas.py b/backend/app/agent_runtime/schemas.py index d37cada..63e2ca1 100644 --- a/backend/app/agent_runtime/schemas.py +++ b/backend/app/agent_runtime/schemas.py @@ -3,8 +3,12 @@ Agent Runtime 配置与数据结构 Schema """ from __future__ import annotations +import logging +logger = logging.getLogger(__name__) +logger.warning("SCHEMAS_MODULE_LOADED_V3_FIELD_VALIDATOR") + from typing import Any, Dict, List, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator class AgentToolConfig(BaseModel): @@ -13,6 +17,11 @@ class AgentToolConfig(BaseModel): include_tools: List[str] = Field(default_factory=list, description="允许的工具名称白名单") exclude_tools: List[str] = Field(default_factory=list, description="排除的工具名称黑名单") require_approval: List[str] = Field(default_factory=list, description="需要人工审批的工具名列表") + + @field_validator("include_tools", "exclude_tools", "require_approval", "cache_tool_whitelist", mode="before") + @classmethod + def coerce_none_to_empty(cls, v: Any) -> Any: + return v if v is not None else [] approval_timeout_ms: int = Field(default=60000, description="审批超时(毫秒),超时使用默认策略") approval_default: str = Field(default="deny", description="超时默认策略: approve | deny | skip") # 结果缓存 diff --git a/backend/app/api/approval.py b/backend/app/api/approval.py index de2423a..27170e7 100644 --- a/backend/app/api/approval.py +++ b/backend/app/api/approval.py @@ -4,9 +4,11 @@ from __future__ import annotations import logging -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel, Field +from app.api.auth import get_current_user +from app.models.user import User from app.services.approval_manager import approval_manager logger = logging.getLogger(__name__) @@ -19,7 +21,7 @@ class ApprovalDecisionRequest(BaseModel): @router.post("/{approval_id}/resolve") -async def resolve_approval(approval_id: str, req: ApprovalDecisionRequest): +async def resolve_approval(approval_id: str, req: ApprovalDecisionRequest, current_user: User = Depends(get_current_user)): """提交工具审批决定。 - **approved**: 批准执行 @@ -33,7 +35,7 @@ async def resolve_approval(approval_id: str, req: ApprovalDecisionRequest): @router.get("/{approval_id}") -async def get_approval(approval_id: str): +async def get_approval(approval_id: str, current_user: User = Depends(get_current_user)): """查询待审批请求详情(用于前端展示工具名和参数)。""" req = approval_manager.get_pending(approval_id) if not req: diff --git a/backend/app/api/goals.py b/backend/app/api/goals.py index 939897d..5f0ec28 100644 --- a/backend/app/api/goals.py +++ b/backend/app/api/goals.py @@ -272,6 +272,56 @@ def execute_goal_async( } +class InteractRequest(BaseModel): + message: str = Field(..., min_length=1, max_length=5000, description="用户回复消息") + attachments: Optional[List[Dict[str, Any]]] = Field( + default=None, + description="附件列表,每项包含 relative_path, filename, content_type 等字段" + ) + + +class InteractResponse(BaseModel): + goal_id: str + reply: str + actions: List[Dict[str, Any]] = [] + actions_count: int + goal_status: str + + +@router.post("/{goal_id}/interact", response_model=InteractResponse) +def interact_with_goal( + goal_id: str, + data: InteractRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + """向目标发送用户反馈消息,Agent 根据反馈重新调整任务。 + + 用户在查看任务结果时可以直接回复 Agent 的问题, + Agent 收到回复后会重新执行相关任务。 + """ + import asyncio + from app.services.main_agent_service import MainAgentService + + goal_service.get_goal(db, goal_id) + + service = MainAgentService(db) + try: + result = asyncio.run(service.interact_with_goal( + goal_id, data.message, attachments=data.attachments + )) + return { + "goal_id": goal_id, + "reply": result.get("reply", ""), + "actions": result.get("actions", []), + "actions_count": result.get("actions_count", 0), + "goal_status": result.get("goal_status", "unknown"), + } + except Exception as e: + logger.error(f"Goal interact failed: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"交互失败: {e}") + + @router.post("/{goal_id}/replan", response_model=DecomposeResponse) def replan_goal( goal_id: str, diff --git a/backend/app/api/webhooks.py b/backend/app/api/webhooks.py index 83c57c7..34e5a9f 100644 --- a/backend/app/api/webhooks.py +++ b/backend/app/api/webhooks.py @@ -8,6 +8,7 @@ from pydantic import BaseModel from typing import Optional, Dict, Any import logging from app.core.database import get_db +from app.core.config import settings from app.models.workflow import Workflow from app.models.execution import Execution from app.tasks.workflow_tasks import execute_workflow_task @@ -17,6 +18,35 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/webhooks", tags=["webhooks"]) +# 敏感 HTTP 头黑名单(禁止捕获存储) +_EXCLUDED_HEADERS = { + 'host', 'content-length', 'connection', 'user-agent', + 'authorization', 'cookie', 'set-cookie', 'x-forwarded-for', + 'x-real-ip', 'x-auth-token', 'x-api-key', +} + +# Webhook 专用安全头白名单(仅捕获这些头) +_ALLOWED_WEBHOOK_HEADERS_PREFIXES = ('x-', 'webhook-', 'gitlab-', 'github-') + + +def _verify_webhook_token(x_webhook_token: Optional[str]) -> None: + """验证 Webhook Token。若配置了 WEBHOOK_AUTH_TOKEN 则必须匹配。""" + expected = settings.WEBHOOK_AUTH_TOKEN + if expected and (not x_webhook_token or x_webhook_token != expected): + raise HTTPException(status_code=401, detail="Webhook token 认证失败") + + +def _filter_webhook_headers(raw_headers: Dict[str, str]) -> Dict[str, str]: + """过滤 webhook 请求头:只保留安全相关的业务头。""" + filtered = {} + for key, value in raw_headers.items(): + lower = key.lower() + if lower in _EXCLUDED_HEADERS: + continue + if any(lower.startswith(p) for p in _ALLOWED_WEBHOOK_HEADERS_PREFIXES): + filtered[key] = value + return filtered + class WebhookTriggerRequest(BaseModel): """Webhook触发请求模型""" @@ -47,35 +77,34 @@ async def trigger_workflow_by_webhook( db: 数据库会话 """ try: + # 验证 Webhook Token + _verify_webhook_token(x_webhook_token) + # 查找工作流 workflow = db.query(Workflow).filter(Workflow.id == workflow_id).first() - + if not workflow: raise HTTPException(status_code=404, detail="工作流不存在") - + # 检查工作流状态 if workflow.status not in ['published', 'running']: raise HTTPException( - status_code=400, + status_code=400, detail=f"工作流状态为 {workflow.status},无法通过Webhook触发" ) - + # 获取请求数据 try: body_data = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {} - except: + except Exception: body_data = {} - + # 获取查询参数 query_params = dict(request.query_params) - - # 获取请求头(排除一些系统头) - headers = {} - excluded_headers = ['host', 'content-length', 'connection', 'user-agent'] - for key, value in request.headers.items(): - if key.lower() not in excluded_headers: - headers[key] = value - + + # 获取请求头(仅保留安全的业务头) + headers = _filter_webhook_headers(dict(request.headers)) + # 构建输入数据:合并查询参数、请求体和请求头 input_data = { **query_params, @@ -88,7 +117,7 @@ async def trigger_workflow_by_webhook( 'path': str(request.url.path) } } - + # 创建执行记录 execution = Execution( workflow_id=workflow_id, @@ -98,7 +127,7 @@ async def trigger_workflow_by_webhook( db.add(execution) db.commit() db.refresh(execution) - + # 异步执行工作流 workflow_data = { 'nodes': workflow.nodes, @@ -110,24 +139,24 @@ async def trigger_workflow_by_webhook( workflow_data, input_data ) - + # 更新执行记录的task_id execution.task_id = task.id db.commit() db.refresh(execution) - + return { "status": "success", "message": "工作流已触发执行", "execution_id": str(execution.id), "task_id": task.id } - + except HTTPException: raise except Exception as e: - logger.error(f"Webhook触发工作流失败: {str(e)}", exc_info=True) - raise HTTPException(status_code=500, detail=f"触发工作流失败: {str(e)}") + logger.error("Webhook触发工作流失败: %s", e, exc_info=True) + raise HTTPException(status_code=500, detail="触发工作流失败") @router.post("/trigger/by-name/{workflow_name}") @@ -149,31 +178,30 @@ async def trigger_workflow_by_name( db: 数据库会话 """ try: + # 验证 Webhook Token + _verify_webhook_token(x_webhook_token) + # 查找工作流(按名称,且状态为published或running) workflow = db.query(Workflow).filter( Workflow.name == workflow_name, Workflow.status.in_(['published', 'running']) ).first() - + if not workflow: raise HTTPException(status_code=404, detail=f"未找到名称为 '{workflow_name}' 的已发布工作流") - + # 获取请求数据 try: body_data = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {} - except: + except Exception: body_data = {} - + # 获取查询参数 query_params = dict(request.query_params) - - # 获取请求头 - headers = {} - excluded_headers = ['host', 'content-length', 'connection', 'user-agent'] - for key, value in request.headers.items(): - if key.lower() not in excluded_headers: - headers[key] = value - + + # 获取请求头(仅保留安全的业务头) + headers = _filter_webhook_headers(dict(request.headers)) + # 构建输入数据 input_data = { **query_params, @@ -186,7 +214,7 @@ async def trigger_workflow_by_name( 'path': str(request.url.path) } } - + # 创建执行记录 execution = Execution( workflow_id=workflow.id, @@ -196,7 +224,7 @@ async def trigger_workflow_by_name( db.add(execution) db.commit() db.refresh(execution) - + # 异步执行工作流 workflow_data = { 'nodes': workflow.nodes, @@ -208,12 +236,12 @@ async def trigger_workflow_by_name( workflow_data, input_data ) - + # 更新执行记录的task_id execution.task_id = task.id db.commit() db.refresh(execution) - + return { "status": "success", "message": "工作流已触发执行", @@ -221,9 +249,9 @@ async def trigger_workflow_by_name( "task_id": task.id, "workflow_id": workflow.id } - + except HTTPException: raise except Exception as e: - logger.error(f"Webhook触发工作流失败: {str(e)}", exc_info=True) - raise HTTPException(status_code=500, detail=f"触发工作流失败: {str(e)}") + logger.error("Webhook触发工作流失败: %s", e, exc_info=True) + raise HTTPException(status_code=500, detail="触发工作流失败") diff --git a/backend/app/core/behavior_middleware.py b/backend/app/core/behavior_middleware.py new file mode 100644 index 0000000..52a4171 --- /dev/null +++ b/backend/app/core/behavior_middleware.py @@ -0,0 +1,110 @@ +""" +用户行为自动采集中间件 — 非侵入式记录 API 调用行为 +""" +import time +import logging +from typing import Optional + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request + +from app.services.behavior_collector import behavior_collector + +logger = logging.getLogger(__name__) + +# URL → (category, action_prefix) 映射 +URL_CATEGORY_MAP = [ + ("/api/v1/agents", "agent", "manage_agent"), + ("/api/v1/workflows", "workflow", "manage_workflow"), + ("/api/v1/executions", "execution", "view_execution"), + ("/api/v1/agent-chat", "agent", "chat_agent"), + ("/api/v1/auth/", "general", "auth"), + ("/api/v1/goals", "decision", "manage_goal"), + ("/api/v1/tasks", "decision", "manage_task"), + ("/api/v1/agent-schedules", "agent", "manage_schedule"), + ("/api/v1/model-configs", "general", "manage_config"), + ("/api/v1/data-sources", "general", "manage_datasource"), +] + +# Read-only HTTP methods (less interesting for behavior learning) +READ_METHODS = {"GET", "HEAD", "OPTIONS"} + + +def _classify_request(path: str, method: str) -> tuple: + """根据 URL 路径和 HTTP 方法分类行为。""" + for prefix, category, action_prefix in URL_CATEGORY_MAP: + if path.startswith(prefix): + if method in READ_METHODS: + return category, f"{action_prefix}_view" + elif method == "POST": + return category, f"{action_prefix}_create" + elif method in ("PUT", "PATCH"): + return category, f"{action_prefix}_update" + elif method == "DELETE": + return category, f"{action_prefix}_delete" + return category, action_prefix + return "general", f"api_{method.lower()}" + + +def _extract_user_id(request: Request) -> Optional[str]: + """从请求中提取用户 ID(JWT token 解析)。""" + try: + # 尝试从 auth header 解析 + auth = request.headers.get("Authorization", "") + if auth.startswith("Bearer "): + token = auth[7:] + # 简单解析 JWT payload(不验证签名,只提取 user_id) + import base64, json + parts = token.split(".") + if len(parts) >= 2: + payload = parts[1] + # 补齐 padding(正确处理 len%4==0 的情况) + padding = (4 - len(payload) % 4) % 4 + payload += "=" * padding + decoded = base64.urlsafe_b64decode(payload) + data = json.loads(decoded) + return data.get("user_id") or data.get("sub") + except Exception: + pass + return None + + +class BehaviorCollectionMiddleware(BaseHTTPMiddleware): + """自动采集 API 调用行为的中间件。""" + + async def dispatch(self, request: Request, call_next): + start_time = time.time() + + # 跳过非 API 路径 + path = request.url.path + if not path.startswith("/api/"): + return await call_next(request) + + response = await call_next(request) + + # 异步记录行为(fire-and-forget) + try: + user_id = _extract_user_id(request) + if user_id: + category, action = _classify_request(path, request.method) + duration_ms = int((time.time() - start_time) * 1000) + behavior_collector.log_fire_and_forget( + user_id=user_id, + category=category, + action=action, + context={ + "url": path, + "method": request.method, + "query_params": str(request.query_params) if request.query_params else None, + }, + result={ + "status_code": response.status_code, + "duration_ms": duration_ms, + }, + source="api", + ip_address=request.client.host if request.client else None, + ) + except Exception as e: + logger.debug("行为采集失败: %s", e) + + return response diff --git a/backend/app/core/celery_app.py b/backend/app/core/celery_app.py index 9bfd560..d9a0fd2 100644 --- a/backend/app/core/celery_app.py +++ b/backend/app/core/celery_app.py @@ -13,6 +13,7 @@ celery_app = Celery( "app.tasks.workflow_tasks", "app.tasks.agent_tasks", "app.tasks.scheduler_tasks", + "app.tasks.goal_tasks", ] ) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index c549b0d..981898f 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -17,11 +17,12 @@ class Settings(BaseSettings): # 应用基本信息 APP_NAME: str = "天工智能体平台" APP_VERSION: str = "1.0.0" - DEBUG: bool = True + DEBUG: bool = False + SQL_ECHO: bool = False # 独立于 DEBUG 的 SQL 日志开关,生产环境必须为 False SECRET_KEY: str = "dev-secret-key-change-in-production" - - # 数据库配置(MySQL) - DATABASE_URL: str = "mysql+pymysql://root:!Rjb12191@gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com:24936/agent_db?charset=utf8mb4" + + # 数据库配置(MySQL)— 生产环境必须通过环境变量设置 + DATABASE_URL: str = "" # Redis配置 REDIS_URL: str = "redis://localhost:6379/0" @@ -93,7 +94,10 @@ class Settings(BaseSettings): # 飞书应用配置(用于发送消息通知到用户飞书) FEISHU_APP_ID: str = "" FEISHU_APP_SECRET: str = "" - FEISHU_VERIFICATION_TOKEN: str = "6BtaWwXqQZh29syLvdxstcS8tIGMmI8U" + FEISHU_VERIFICATION_TOKEN: str = "" + + # Webhook 全局认证 Token — 所有 webhook 触发请求需要携带此 Token + WEBHOOK_AUTH_TOKEN: str = "" # 橙子飞书应用配置(独立 WS 连接,直接路由到橙子助手 Agent) ORANGE_APP_ID: str = "" diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 3817ee8..e0a7b01 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -12,7 +12,7 @@ engine = create_engine( pool_pre_ping=True, pool_size=10, max_overflow=20, - echo=settings.DEBUG # 开发环境显示SQL + echo=settings.SQL_ECHO # SQL 日志独立开关,默认关闭 ) # 创建会话工厂 @@ -56,4 +56,17 @@ def init_db(): import app.models.plugin import app.models.goal import app.models.task + import app.models.tool + import app.models.data_source + import app.models.execution_log + import app.models.agent_execution_log + import app.models.feedback_record + import app.models.knowledge_entry + import app.models.node_template + import app.models.persistent_user_memory + import app.models.shadow_comparison + import app.models.user_behavior + import app.models.user_feishu_open_id + import app.models.user_fingerprint + import app.models.workflow_version Base.metadata.create_all(bind=engine) diff --git a/backend/app/core/rate_limiter.py b/backend/app/core/rate_limiter.py new file mode 100644 index 0000000..2d52346 --- /dev/null +++ b/backend/app/core/rate_limiter.py @@ -0,0 +1,136 @@ +"""API 限流中间件 — 基于滑动窗口的简易限流器(Redis 优先,内存 fallback)""" +from __future__ import annotations + +import logging +import time +from collections import defaultdict +from typing import Dict, List, Optional, Tuple + +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import JSONResponse + +logger = logging.getLogger(__name__) + +# 默认限流配置 +DEFAULT_RATE_LIMIT = 120 # 每窗口最大请求数 +DEFAULT_WINDOW_SEC = 60 # 窗口时长(秒) +# 敏感端点更严格 +SENSITIVE_PATH_PREFIXES = [ + "/api/v1/auth/login", + "/api/v1/agent-chat", +] + +# ─── 内存存储(单进程 / 无 Redis 时使用) ─── + +_memory_store: Dict[str, List[float]] = defaultdict(list) + + +def _get_redis(): + """尝试获取 Redis 客户端。""" + try: + from app.core.redis_client import get_redis_client + client = get_redis_client() + if client: + try: + client.ping() + return client + except Exception: + pass + except Exception: + pass + return None + + +def _check_memory( + key: str, max_requests: int, window_sec: float +) -> Tuple[bool, int]: + """内存滑动窗口检查。返回 (allowed, remaining)。""" + now = time.monotonic() + window = _memory_store[key] + # 清理过期记录 + cutoff = now - window_sec + while window and window[0] < cutoff: + window.pop(0) + if len(window) < max_requests: + window.append(now) + return True, max_requests - len(window) + return False, 0 + + +def _check_redis( + client, key: str, max_requests: int, window_sec: int +) -> Tuple[bool, int]: + """Redis 滑动窗口检查。""" + now_ms = int(time.time() * 1000) + window_ms = window_sec * 1000 + pipe = client.pipeline() + member = f"{now_ms}:{now_ms}" + pipe.zadd(key, {member: now_ms}) + pipe.zremrangebyscore(key, 0, now_ms - window_ms) + pipe.zcard(key) + pipe.expire(key, window_sec * 2) + _, _, count, _ = pipe.execute() + remaining = max(0, max_requests - count) + if count <= max_requests: + return True, remaining + return False, remaining + + +class RateLimiterMiddleware(BaseHTTPMiddleware): + """API 限流中间件。 + + 规则: + - 默认: 120 req / 60s per IP + - 敏感端点 (login, agent-chat): 30 req / 60s per IP + - 限流时返回 429 + Retry-After + """ + + async def dispatch(self, request: Request, call_next) -> Response: + path = request.url.path + + # 跳过非 API 路径 + if not path.startswith("/api/"): + return await call_next(request) + + # 确定限流配置 + is_sensitive = any(path.startswith(p) for p in SENSITIVE_PATH_PREFIXES) + max_requests = 30 if is_sensitive else DEFAULT_RATE_LIMIT + window_sec = DEFAULT_WINDOW_SEC + + # 构建 key: ip + path 前缀 + client_ip = request.client.host if request.client else "unknown" + rate_key = f"rl:{client_ip}:{'sensitive' if is_sensitive else 'normal'}" + + # 检查限流 + redis_client = _get_redis() + if redis_client: + allowed, remaining = _check_redis( + redis_client, rate_key, max_requests, window_sec + ) + else: + allowed, remaining = _check_memory( + rate_key, max_requests, window_sec + ) + + if not allowed: + retry_after = window_sec + logger.warning( + "API 限流触发: ip=%s path=%s max=%d/%ds", + client_ip, path, max_requests, window_sec, + ) + return JSONResponse( + status_code=429, + content={ + "detail": f"请求过于频繁,请 {retry_after}s 后重试", + "retry_after": retry_after, + }, + headers={"Retry-After": str(retry_after)}, + ) + + response = await call_next(request) + + # 注入限流头 + response.headers["X-RateLimit-Limit"] = str(max_requests) + response.headers["X-RateLimit-Remaining"] = str(remaining) + return response diff --git a/backend/app/core/tools_bootstrap.py b/backend/app/core/tools_bootstrap.py index 8a03414..3dbdbc6 100644 --- a/backend/app/core/tools_bootstrap.py +++ b/backend/app/core/tools_bootstrap.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) _registered = False -_EXPECTED_BUILTIN = 54 +_EXPECTED_BUILTIN = 56 def ensure_builtin_tools_registered() -> None: @@ -72,6 +72,8 @@ def ensure_builtin_tools_registered() -> None: feishu_read_messages_tool, feishu_create_sheet_tool, feishu_upload_file_tool, + create_gitea_issue, + parse_test_result_file, HTTP_REQUEST_SCHEMA, FILE_READ_SCHEMA, FILE_WRITE_SCHEMA, @@ -126,6 +128,8 @@ def ensure_builtin_tools_registered() -> None: FEISHU_READ_MESSAGES_SCHEMA, FEISHU_CREATE_SHEET_SCHEMA, FEISHU_UPLOAD_FILE_SCHEMA, + CREATE_GITEA_ISSUE_SCHEMA, + PARSE_TEST_RESULT_FILE_SCHEMA, ) tool_registry.register_builtin_tool("http_request", http_request_tool, HTTP_REQUEST_SCHEMA) @@ -182,6 +186,8 @@ def ensure_builtin_tools_registered() -> None: tool_registry.register_builtin_tool("feishu_read_messages", feishu_read_messages_tool, FEISHU_READ_MESSAGES_SCHEMA) tool_registry.register_builtin_tool("feishu_create_sheet", feishu_create_sheet_tool, FEISHU_CREATE_SHEET_SCHEMA) tool_registry.register_builtin_tool("feishu_upload_file", feishu_upload_file_tool, FEISHU_UPLOAD_FILE_SCHEMA) + tool_registry.register_builtin_tool("create_gitea_issue", create_gitea_issue, CREATE_GITEA_ISSUE_SCHEMA) + tool_registry.register_builtin_tool("parse_test_result_file", parse_test_result_file, PARSE_TEST_RESULT_FILE_SCHEMA) _registered = True n = tool_registry.builtin_tool_count() diff --git a/backend/app/main.py b/backend/app/main.py index 3dd6917..fb17cfc 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -16,6 +16,50 @@ from app.core.error_handler import ( ) from app.core.exceptions import BaseAPIException from app.core.database import init_db +from app.core.rate_limiter import RateLimiterMiddleware +from app.core.behavior_middleware import BehaviorCollectionMiddleware + +_SECURITY_WARNED = False + + +def _check_security_config() -> None: + """启动时检查敏感配置,输出安全警告。""" + global _SECURITY_WARNED + if _SECURITY_WARNED: + return + _SECURITY_WARNED = True + + warnings: list[str] = [] + + # JWT 密钥检查 + if settings.JWT_SECRET_KEY == "dev-jwt-secret-key-change-in-production" or \ + "change-in-production" in settings.JWT_SECRET_KEY.lower(): + warnings.append("JWT_SECRET_KEY 使用默认值,生产环境必须更换为随机字符串") + + # 应用密钥 + if settings.SECRET_KEY == "dev-secret-key-change-in-production" or \ + "change-in-production" in settings.SECRET_KEY.lower(): + warnings.append("SECRET_KEY 使用默认值,生产环境必须更换") + + # API Key 检查 + api_keys_set = bool( + settings.OPENAI_API_KEY + or settings.DEEPSEEK_API_KEY + or settings.ANTHROPIC_API_KEY + ) + if not api_keys_set: + warnings.append("未配置任何 AI API Key (OPENAI / DEEPSEEK / ANTHROPIC),LLM 调用将失败") + + # 数据库密码检查 + db_url = settings.DATABASE_URL or "" + if "change" in db_url.lower() or "CHANGE_ME" in db_url: + warnings.append("DATABASE_URL 疑似使用默认密码,请检查数据库凭据") + + for w in warnings: + logger.warning("[安全] %s", w) + + if not warnings: + logger.info("[安全] 配置检查通过,未发现明显安全风险") # 配置日志 logging.basicConfig( @@ -110,6 +154,12 @@ app.add_middleware( expose_headers=["*"], ) +# API 限流中间件(在 CORS 之后、日志之前) +app.add_middleware(RateLimiterMiddleware) + +# 用户行为自动采集中间件 +app.add_middleware(BehaviorCollectionMiddleware) + # 注册全局异常处理器 app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(BaseAPIException, api_exception_handler) @@ -270,6 +320,9 @@ async def startup_event(): except Exception as e: logger.error(f"人参果1号长连接启动失败: {e}") + # 安全配置检查 + _check_security_config() + # 注册路由 from app.api import auth, uploads, workflows, executions, websocket, execution_logs, data_sources, agents, platform_templates, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools, agent_chat, agent_monitoring, knowledge_base, agent_schedules, notifications, feishu_bind, approval, orchestration_templates, plugins, agent_market, goals, tasks diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 3799aca..449babf 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -23,5 +23,11 @@ from app.models.plugin import NodePlugin from app.models.orchestration_template import OrchestrationTemplate from app.models.goal import Goal from app.models.task import Task +from app.models.agent_execution_log import AgentExecutionLog +from app.models.user_behavior import UserBehaviorLog +from app.models.knowledge_entry import KnowledgeEntry +from app.models.user_fingerprint import UserFingerprint +from app.models.shadow_comparison import ShadowComparison +from app.models.feedback_record import FeedbackRecord -__all__ = ["User", "Workflow", "WorkflowVersion", "Agent", "GlobalKnowledge", "AgentRating", "AgentFavorite", "Execution", "ExecutionLog", "ModelConfig", "DataSource", "WorkflowTemplate", "TemplateRating", "TemplateFavorite", "NodeTemplate", "Role", "Permission", "WorkflowPermission", "AgentPermission", "AlertRule", "AlertLog", "PersistentUserMemory", "AgentLLMLog", "AgentVectorMemory", "AgentLearningPattern", "AgentSchedule", "KnowledgeBase", "Document", "DocumentChunk", "Notification", "UserFeishuOpenId", "NodePlugin", "OrchestrationTemplate", "Goal", "Task"] \ No newline at end of file +__all__ = ["User", "Workflow", "WorkflowVersion", "Agent", "GlobalKnowledge", "AgentRating", "AgentFavorite", "Execution", "ExecutionLog", "ModelConfig", "DataSource", "WorkflowTemplate", "TemplateRating", "TemplateFavorite", "NodeTemplate", "Role", "Permission", "WorkflowPermission", "AgentPermission", "AlertRule", "AlertLog", "PersistentUserMemory", "AgentLLMLog", "AgentVectorMemory", "AgentLearningPattern", "AgentSchedule", "KnowledgeBase", "Document", "DocumentChunk", "Notification", "UserFeishuOpenId", "NodePlugin", "OrchestrationTemplate", "Goal", "Task", "AgentExecutionLog", "UserBehaviorLog", "KnowledgeEntry", "UserFingerprint", "ShadowComparison", "FeedbackRecord"] \ No newline at end of file diff --git a/backend/app/models/agent_execution_log.py b/backend/app/models/agent_execution_log.py new file mode 100644 index 0000000..97a923e --- /dev/null +++ b/backend/app/models/agent_execution_log.py @@ -0,0 +1,57 @@ +""" +Agent 执行日志模型 — 结构化记录每次 Agent 执行的完整信息 +用于知识自进化系统的数据基础 +""" +from sqlalchemy import Column, String, Text, Integer, DateTime, JSON, Float, Boolean +from sqlalchemy.dialects.mysql import CHAR +from app.core.database import Base +import uuid +from datetime import datetime + + +class AgentExecutionLog(Base): + """Agent 每次执行的完整结构化日志""" + __tablename__ = "agent_execution_logs" + + id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="日志ID") + agent_id = Column(String(36), nullable=True, index=True, comment="Agent ID") + agent_name = Column(String(200), nullable=True, comment="Agent 名称") + goal_id = Column(String(36), nullable=True, index=True, comment="关联 Goal ID") + task_id = Column(String(36), nullable=True, index=True, comment="关联 Task ID") + user_id = Column(String(36), nullable=True, index=True, comment="用户 ID") + session_id = Column(String(100), nullable=True, comment="会话标识") + + # 输入/输出 + input_text = Column(Text, nullable=True, comment="用户输入文本") + output_text = Column(Text, nullable=True, comment="Agent 输出文本") + output_truncated = Column(Boolean, default=False, comment="输出是否被截断") + + # 执行结果 + success = Column(Boolean, default=True, comment="是否成功") + error_message = Column(Text, nullable=True, comment="错误信息") + + # 性能指标 + latency_ms = Column(Integer, nullable=True, comment="总耗时(ms)") + iterations_used = Column(Integer, default=0, comment="ReAct 迭代次数") + tool_calls_made = Column(Integer, default=0, comment="工具调用总次数") + + # 结构化明细(JSON) + tool_chain = Column(JSON, nullable=True, comment="工具调用链: [{tool_name, input, output, duration_ms}]") + llm_calls = Column(JSON, nullable=True, comment="LLM调用明细: [{model, prompt_tokens, completion_tokens, latency_ms}]") + steps = Column(JSON, nullable=True, comment="执行步骤详情(精简版)") + + # 模型信息 + model = Column(String(100), nullable=True, comment="使用的模型") + provider = Column(String(50), nullable=True, comment="模型提供商") + + # 用户反馈(后续补充) + user_rating = Column(Integer, nullable=True, comment="用户评分(1-5)") + user_feedback = Column(Text, nullable=True, comment="用户反馈文本") + + # 知识提取标记 + knowledge_extracted = Column(Boolean, default=False, comment="是否已提取知识") + + created_at = Column(DateTime, default=datetime.now, comment="创建时间") + + def __repr__(self): + return f"" diff --git a/backend/app/models/feedback_record.py b/backend/app/models/feedback_record.py new file mode 100644 index 0000000..0f28dee --- /dev/null +++ b/backend/app/models/feedback_record.py @@ -0,0 +1,39 @@ +"""用户反馈记录模型 — 采集点踩/修改/驳回等反馈信号""" +import uuid +from datetime import datetime +from sqlalchemy import Column, String, Text, DateTime, JSON, Float, Integer, Boolean +from app.core.database import Base + + +class FeedbackRecord(Base): + """用户反馈记录""" + __tablename__ = "feedback_records" + + id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + user_id = Column(String(36), nullable=False, index=True) + + # 反馈信号 + signal_type = Column(String(30), nullable=False, comment="thumbs_down/manual_edit/retry_command/reject_approval") + severity = Column(Float, default=0.5, comment="严重程度(0-1)") + + # 关联上下文 + execution_log_id = Column(String(36), nullable=True, index=True) + agent_name = Column(String(200), nullable=True) + task_id = Column(String(36), nullable=True) + + # 原始内容 + original_output = Column(Text, nullable=True, comment="被否定的Agent输出") + user_correction = Column(Text, nullable=True, comment="用户修正后的内容") + + # 反馈详情 + feedback_context = Column(JSON, nullable=True, comment="反馈上下文: {user_message, reason, ...}") + improvement_suggestion = Column(Text, nullable=True, comment="LLM生成的改进建议") + + # 学习状态 + learned = Column(Boolean, default=False, comment="是否已学习") + lesson_summary = Column(Text, nullable=True, comment="学习后的教训总结") + + created_at = Column(DateTime, default=datetime.now) + + def __repr__(self): + return f"" diff --git a/backend/app/models/knowledge_entry.py b/backend/app/models/knowledge_entry.py new file mode 100644 index 0000000..0cf2bed --- /dev/null +++ b/backend/app/models/knowledge_entry.py @@ -0,0 +1,67 @@ +""" +知识条目模型 — Agent 执行经验的结构化沉淀 +""" +import uuid +from datetime import datetime +from sqlalchemy import Column, String, Text, Integer, DateTime, Boolean, JSON, Float, Index +from app.core.database import Base + + +class KnowledgeEntry(Base): + """从 Agent 执行日志中提取的可复用知识条目""" + __tablename__ = "knowledge_entries" + + id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + title = Column(String(500), nullable=False, comment="知识标题(一句话概括)") + category = Column(String(30), nullable=False, index=True, + comment="类别: bug_fix/best_practice/workaround/optimization/insight") + tags = Column(JSON, nullable=True, comment="标签列表: ['mysql','deadlock','retry']") + + # 知识内容 + situation = Column(Text, nullable=True, comment="适用场景") + solution = Column(Text, nullable=True, comment="解决方案") + caveats = Column(Text, nullable=True, comment="注意事项/踩坑记录") + + # 来源追溯 + source_execution_ids = Column(JSON, nullable=True, comment="原始执行日志ID列表") + source_agent_name = Column(String(200), nullable=True, comment="来源 Agent 名称") + source_model = Column(String(100), nullable=True, comment="来源模型") + + # RAG 检索 + embedding_text = Column(Text, nullable=True, comment="用于生成 embedding 的合并文本") + embedding = Column(Text, nullable=True, comment="JSON 序列化的 embedding 向量") + + # 效果度量 + retrieval_count = Column(Integer, default=0, comment="被检索次数") + success_rate = Column(Float, nullable=True, comment="应用成功率") + + # 提取信息 + extracted_by = Column(String(100), nullable=True, comment="提取方式: llm_auto/manual/reviewed") + confidence = Column(Float, default=0.5, comment="提取置信度(0-1)") + + is_active = Column(Boolean, default=True, comment="是否启用") + created_at = Column(DateTime, default=datetime.now, comment="创建时间") + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间") + + __table_args__ = ( + Index("ix_knowledge_entries_category", "category"), + Index("ix_knowledge_entries_active", "is_active"), + ) + + def __repr__(self): + return f"" + + def to_dict(self) -> dict: + return { + "id": self.id, + "title": self.title, + "category": self.category, + "tags": self.tags or [], + "situation": self.situation, + "solution": self.solution, + "caveats": self.caveats, + "source_agent_name": self.source_agent_name, + "retrieval_count": self.retrieval_count, + "confidence": self.confidence, + "created_at": self.created_at.isoformat() if self.created_at else None, + } diff --git a/backend/app/models/shadow_comparison.py b/backend/app/models/shadow_comparison.py new file mode 100644 index 0000000..4cdb0aa --- /dev/null +++ b/backend/app/models/shadow_comparison.py @@ -0,0 +1,35 @@ +"""影子模式对比记录模型 — 比较数字分身建议与人类实际决策""" +import uuid +from datetime import datetime +from sqlalchemy import Column, String, Text, DateTime, JSON, Float, Integer, Boolean +from app.core.database import Base + + +class ShadowComparison(Base): + """影子模式对比记录""" + __tablename__ = "shadow_comparisons" + + id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + user_id = Column(String(36), nullable=False, index=True) + category = Column(String(30), nullable=False, comment="场景: code_review/email/document/decision") + + # 影子建议 + shadow_suggestion = Column(JSON, nullable=True, comment="数字分身生成的建议") + shadow_confidence = Column(Float, default=0.5, comment="影子置信度") + + # 用户实际决策 + user_decision = Column(JSON, nullable=True, comment="用户实际操作") + user_action = Column(String(50), nullable=True, comment="action: accept/modify/reject/ignore") + + # 对比结果 + match_score = Column(Float, nullable=True, comment="匹配分数(0-1)") + match_detail = Column(JSON, nullable=True, comment="匹配详情: {matched_points, diverged_points}") + + # 上下文 + context = Column(JSON, nullable=True, comment="触发场景上下文") + source_execution_id = Column(String(36), nullable=True, comment="关联执行日志ID") + + created_at = Column(DateTime, default=datetime.now) + + def __repr__(self): + return f"" diff --git a/backend/app/models/user_behavior.py b/backend/app/models/user_behavior.py new file mode 100644 index 0000000..70ddb58 --- /dev/null +++ b/backend/app/models/user_behavior.py @@ -0,0 +1,45 @@ +""" +用户行为日志模型 — 记录用户四大维度的操作行为 +用于数字分身的观察和学习 +""" +from sqlalchemy import Column, String, Text, Integer, DateTime, JSON, Enum as SAEnum +from sqlalchemy.dialects.mysql import CHAR +from app.core.database import Base +from datetime import datetime +import uuid +import enum + + +class BehaviorCategory(str, enum.Enum): + EMAIL = "email" # 邮件处理 + CODE_REVIEW = "code_review" # 代码审核 + DOCUMENT = "document" # 文档撰写 + DECISION = "decision" # 决策行为 + GENERAL = "general" # 通用操作 + + +class UserBehaviorLog(Base): + """用户行为观察日志""" + __tablename__ = "user_behavior_logs" + + id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="日志ID") + user_id = Column(String(36), nullable=False, index=True, comment="用户 ID") + category = Column(String(30), nullable=False, index=True, comment="行为类别: email/code_review/document/decision/general") + action = Column(String(100), nullable=False, comment="具体动作: open_email/reply_email/review_pr/approve_pr/write_doc/make_decision") + + # 上下文信息 + context = Column(JSON, nullable=True, comment="行为上下文: {url, params, entity_id, ...}") + + # 行为结果 + result = Column(JSON, nullable=True, comment="行为结果: {action_taken, duration_ms, outcome, ...}") + + # 元信息 + source = Column(String(50), nullable=True, comment="来源: api/webhook/feishu/manual") + session_id = Column(String(100), nullable=True, comment="会话标识") + ip_address = Column(String(50), nullable=True, comment="客户端 IP") + user_agent = Column(String(500), nullable=True, comment="用户代理") + + created_at = Column(DateTime, default=datetime.now, index=True, comment="发生时间") + + def __repr__(self): + return f"" diff --git a/backend/app/models/user_fingerprint.py b/backend/app/models/user_fingerprint.py new file mode 100644 index 0000000..bfc54e2 --- /dev/null +++ b/backend/app/models/user_fingerprint.py @@ -0,0 +1,38 @@ +""" +用户行为指纹模型 — 用户的数字行为特征向量和偏好权重 +""" +import uuid +from datetime import datetime +from sqlalchemy import Column, String, Text, DateTime, JSON, Float, Integer +from app.core.database import Base + + +class UserFingerprint(Base): + """用户行为数字指纹""" + __tablename__ = "user_fingerprints" + + id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + user_id = Column(String(36), nullable=False, unique=True, index=True, comment="用户ID") + + # 各场景偏好权重 (0-1 浮点数,JSON) + # 示例: {"code_review": {"security": 0.4, "performance": 0.3, "readability": 0.2, "style": 0.1}} + preference_weights = Column(JSON, nullable=True, comment="偏好权重") + + # 决策规则 (if-then 规则列表,JSON) + # 示例: [{"if": "files_changed > 10 and no_tests", "then": "request_tests"}] + decision_rules = Column(JSON, nullable=True, comment="决策规则") + + # 行为统计 + total_behaviors = Column(Integer, default=0, comment="总行为数") + behaviors_by_category = Column(JSON, nullable=True, comment="按类别的行为分布") + avg_response_time_ms = Column(Integer, nullable=True, comment="平均响应时间(ms)") + + # 模型版本 + model_version = Column(String(20), default="1.0", comment="指纹模型版本") + last_trained_at = Column(DateTime, nullable=True, comment="上次训练时间") + + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + def __repr__(self): + return f"" diff --git a/backend/app/services/behavior_collector.py b/backend/app/services/behavior_collector.py new file mode 100644 index 0000000..67e7be6 --- /dev/null +++ b/backend/app/services/behavior_collector.py @@ -0,0 +1,154 @@ +""" +用户行为采集服务 — 非侵入式记录用户操作,用于数字分身学习 +""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Dict, List, Optional + +from sqlalchemy.orm import Session +from sqlalchemy import desc + +from app.core.database import SessionLocal +from app.models.user_behavior import UserBehaviorLog, BehaviorCategory + +logger = logging.getLogger(__name__) + + +class BehaviorCollector: + """用户行为采集器(单例)""" + + def log_sync( + self, + *, + user_id: str, + category: str, + action: str, + context: Optional[Dict[str, Any]] = None, + result: Optional[Dict[str, Any]] = None, + source: str = "api", + session_id: Optional[str] = None, + ip_address: Optional[str] = None, + user_agent: Optional[str] = None, + ) -> Optional[str]: + """同步写入行为日志,返回日志 ID。""" + db: Optional[Session] = None + try: + db = SessionLocal() + entry = UserBehaviorLog( + user_id=user_id, + category=category, + action=action, + context=context, + result=result, + source=source, + session_id=session_id, + ip_address=ip_address, + user_agent=user_agent, + ) + db.add(entry) + db.commit() + db.refresh(entry) + return str(entry.id) + except Exception as e: + logger.warning("写入用户行为日志失败: %s", e) + if db: + try: + db.rollback() + except Exception: + pass + return None + finally: + if db: + try: + db.close() + except Exception: + pass + + async def log(self, **kwargs) -> Optional[str]: + """异步写入(线程池)。""" + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, lambda: self.log_sync(**kwargs)) + + def log_fire_and_forget(self, **kwargs): + """Fire-and-forget 写入。""" + try: + asyncio.ensure_future(self.log(**kwargs)) + except Exception: + pass + + # ─── 查询方法 ─── + + def get_user_behaviors( + self, + user_id: str, + category: Optional[str] = None, + limit: int = 50, + skip: int = 0, + ) -> List[Dict[str, Any]]: + """获取用户行为历史。""" + db: Optional[Session] = None + try: + db = SessionLocal() + q = db.query(UserBehaviorLog).filter(UserBehaviorLog.user_id == user_id) + if category: + q = q.filter(UserBehaviorLog.category == category) + q = q.order_by(desc(UserBehaviorLog.created_at)).offset(skip).limit(limit) + rows = q.all() + return [ + { + "id": r.id, + "category": r.category, + "action": r.action, + "context": r.context, + "result": r.result, + "source": r.source, + "created_at": r.created_at.isoformat() if r.created_at else None, + } + for r in rows + ] + except Exception as e: + logger.warning("查询用户行为日志失败: %s", e) + return [] + finally: + if db: + try: + db.close() + except Exception: + pass + + def get_behavior_stats(self, user_id: str) -> Dict[str, Any]: + """获取用户行为统计摘要。""" + db: Optional[Session] = None + try: + db = SessionLocal() + from sqlalchemy import func + total = db.query(func.count(UserBehaviorLog.id)).filter( + UserBehaviorLog.user_id == user_id + ).scalar() or 0 + by_category = {} + for cat in BehaviorCategory: + count = db.query(func.count(UserBehaviorLog.id)).filter( + UserBehaviorLog.user_id == user_id, + UserBehaviorLog.category == cat.value, + ).scalar() or 0 + by_category[cat.value] = count + return { + "user_id": user_id, + "total_behaviors": total, + "by_category": by_category, + } + except Exception as e: + logger.warning("查询行为统计失败: %s", e) + return {"user_id": user_id, "total_behaviors": 0, "by_category": {}} + finally: + if db: + try: + db.close() + except Exception: + pass + + +# 全局单例 +behavior_collector = BehaviorCollector() diff --git a/backend/app/services/bottleneck_detector.py b/backend/app/services/bottleneck_detector.py new file mode 100644 index 0000000..8149011 --- /dev/null +++ b/backend/app/services/bottleneck_detector.py @@ -0,0 +1,182 @@ +""" +工作流瓶颈自动检测 — 分析执行数据,识别性能瓶颈 +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional +from collections import defaultdict + +from sqlalchemy import func +from sqlalchemy.orm import Session + +from app.core.database import SessionLocal +from app.models.execution import Execution +from app.models.execution_log import ExecutionLog +from app.models.agent_execution_log import AgentExecutionLog + +logger = logging.getLogger(__name__) + + +class BottleneckDetector: + """工作流瓶颈检测器""" + + def analyze_executions(self, hours: int = 24) -> List[Dict[str, Any]]: + """分析最近N小时的执行数据,找出瓶颈。""" + db: Optional[Session] = None + try: + db = SessionLocal() + + from datetime import datetime, timedelta + since = datetime.now() - timedelta(hours=hours) + + # 1. 分析工作流执行节点耗时 + logs = ( + db.query(ExecutionLog) + .filter( + ExecutionLog.timestamp >= since, + ExecutionLog.node_id.isnot(None), + ) + .all() + ) + + # 按 node_type 聚合 + node_stats = defaultdict(lambda: { + "count": 0, "errors": 0, "durations": [], + "total_duration": 0, "node_type": "", + }) + + for log in logs: + nt = log.node_type or "unknown" + node_stats[nt]["count"] += 1 + node_stats[nt]["node_type"] = nt + if log.level == "ERROR": + node_stats[nt]["errors"] += 1 + if log.duration: + node_stats[nt]["durations"].append(log.duration) + node_stats[nt]["total_duration"] += log.duration + + # 计算统计值 + results = [] + for nt, stats in node_stats.items(): + if stats["count"] < 3: + continue + durations = sorted(stats["durations"]) + n = len(durations) + avg = stats["total_duration"] / n if n > 0 else 0 + results.append({ + "node_type": nt, + "count": stats["count"], + "error_rate": round(stats["errors"] / stats["count"], 3), + "avg_duration_ms": int(avg), + "p50_ms": durations[n // 2] if n > 0 else 0, + "p95_ms": durations[int(n * 0.95)] if n > 4 else (durations[-1] if n > 0 else 0), + "p99_ms": durations[int(n * 0.99)] if n > 9 else (durations[-1] if n > 0 else 0), + }) + + # 按耗时排序 + results.sort(key=lambda x: x["p95_ms"], reverse=True) + + # 2. 分析 Agent 执行效率 + agent_logs = ( + db.query(AgentExecutionLog) + .filter(AgentExecutionLog.created_at >= since) + .all() + ) + + agent_failure_rate = 0.0 + agent_avg_tool_calls = 0.0 + if agent_logs: + failed = sum(1 for a in agent_logs if not a.success) + agent_failure_rate = round(failed / len(agent_logs), 3) + agent_avg_tool_calls = round( + sum(a.tool_calls_made or 0 for a in agent_logs) / len(agent_logs), 1 + ) + + # 3. 识别瓶颈 + if results: + overall_avg_p95 = sum(r["p95_ms"] for r in results) / len(results) + for r in results: + r["is_bottleneck"] = r["p95_ms"] > overall_avg_p95 * 3 + r["is_problematic"] = r["error_rate"] > 0.2 + r["is_inefficient"] = r["p50_ms"] > 30000 + r["severity"] = "high" if (r["is_bottleneck"] or r["is_problematic"]) else ( + "medium" if r["is_inefficient"] else "low" + ) + + return results + + except Exception as e: + logger.error("瓶颈检测失败: %s", e) + return [] + finally: + if db: + try: + db.close() + except Exception: + pass + + def generate_recommendations(self, bottlenecks: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """根据瓶颈生成优化建议。""" + recommendations = [] + + for b in bottlenecks: + if not b.get("is_bottleneck") and not b.get("is_problematic"): + continue + + rec = { + "node_type": b["node_type"], + "severity": b["severity"], + "current_state": { + "p95_ms": b["p95_ms"], + "error_rate": b["error_rate"], + "count": b["count"], + }, + "actions": [], + } + + if b.get("is_bottleneck"): + rec["actions"].append({ + "type": "split_node", + "description": f"节点 {b['node_type']} P95耗时 {b['p95_ms']}ms,建议拆分为并行子节点", + "expected_improvement": "减少50-70%耗时", + }) + rec["actions"].append({ + "type": "upgrade_model", + "description": "考虑使用更快的模型或增加超时时间", + "expected_improvement": "减少20-40%耗时", + }) + + if b.get("is_problematic"): + rec["actions"].append({ + "type": "add_retry", + "description": f"节点失败率 {b['error_rate']:.1%},建议添加重试逻辑和前置校验", + "expected_improvement": "失败率降至5%以下", + }) + + if b.get("is_inefficient"): + rec["actions"].append({ + "type": "add_cache", + "description": f"节点平均耗时过长,建议添加结果缓存避免重复计算", + "expected_improvement": "减少30-60%耗时", + }) + + recommendations.append(rec) + + return recommendations + + def run_full_analysis(self, hours: int = 24) -> Dict[str, Any]: + """运行完整分析:检测+建议。""" + bottlenecks = self.analyze_executions(hours=hours) + recommendations = self.generate_recommendations(bottlenecks) + return { + "period_hours": hours, + "bottlenecks_found": len([b for b in bottlenecks if b.get("is_bottleneck")]), + "problematic_nodes": len([b for b in bottlenecks if b.get("is_problematic")]), + "nodes_analyzed": len(bottlenecks), + "bottlenecks": bottlenecks, + "recommendations": recommendations, + } + + +bottleneck_detector = BottleneckDetector() diff --git a/backend/app/services/builtin_tools.py b/backend/app/services/builtin_tools.py index 7fb133e..38105ec 100644 --- a/backend/app/services/builtin_tools.py +++ b/backend/app/services/builtin_tools.py @@ -5290,11 +5290,15 @@ async def main_agent_notify_user( user_id: str, message: str, notification_type: str = "info", + type: str = "", # LLM 有时生成 type 而非 notification_type,作为别名兼容 ) -> str: """Main Agent 工具:向用户发送通知(站内消息)。""" from app.core.database import SessionLocal from app.models.notification import Notification + # type 别名兼容:如果 LLM 传了 type 参数,优先使用 + effective_type = type if type else notification_type + db = None try: db = SessionLocal() @@ -5302,17 +5306,17 @@ async def main_agent_notify_user( user_id=user_id, title="Main Agent 通知", content=message, - type=notification_type, + category=effective_type, ref_type="goal", ref_id="", is_read=False, ) db.add(notif) db.commit() - logger.info(f"Main Agent 通知已发送: user={user_id}, type={notification_type}, len={len(message)}") + logger.info(f"Main Agent 通知已发送: user={user_id}, type={effective_type}, len={len(message)}") return json.dumps({ "sent": True, - "notification_type": notification_type, + "notification_type": effective_type, "message_preview": message[:200], }, ensure_ascii=False) except Exception as e: @@ -5972,3 +5976,315 @@ TEXT_TO_SPEECH_SCHEMA = { }, }, } + + +# ═══════════════════════════════════════════════════════════════ +# Gitea 工单工具 — Bug 跟踪集成 +# ═══════════════════════════════════════════════════════════════ + +_GITEA_API_BASE = "http://101.43.95.130:3001/api/v1" +_GITEA_TOKEN = "fbc9ee7f96635793f4844187eac5c0e573480721" +_GITEA_REPO = "admin/aiagent" + + +def create_gitea_issue( + title: str, + body: str = "", + assignee: str = "admin", + labels: str = "", +) -> str: + """在 Gitea 工单平台创建 Issue(Bug 跟踪/任务登记)。 + + Args: + title: 工单标题 + body: 工单描述(支持 Markdown) + assignee: 指派人用户名(默认 admin) + labels: 标签,逗号分隔(可选,需先在 Gitea 中创建对应标签) + """ + import json as _json + import urllib.request + import urllib.error + + url = f"{_GITEA_API_BASE}/repos/{_GITEA_REPO}/issues" + payload: dict = { + "title": title, + "body": body, + "assignee": assignee, + } + if labels: + payload["labels"] = [l.strip() for l in labels.split(",") if l.strip()] + + data = _json.dumps(payload, ensure_ascii=False).encode("utf-8") + req = urllib.request.Request( + url, + data=data, + headers={ + "Authorization": f"token {_GITEA_TOKEN}", + "Content-Type": "application/json", + }, + method="POST", + ) + + try: + with urllib.request.urlopen(req, timeout=15) as resp: + result = _json.loads(resp.read().decode("utf-8")) + issue_number = result.get("number", "?") + html_url = result.get("html_url", "") + logger.info( + "Gitea 工单已创建: #%s - %s", + issue_number, result.get("title", ""), + ) + return _json.dumps( + { + "created": True, + "issue_number": issue_number, + "url": html_url, + "title": result.get("title", ""), + "state": result.get("state", "open"), + }, + ensure_ascii=False, + ) + except urllib.error.HTTPError as e: + err_body = e.read().decode("utf-8", errors="replace") + logger.error("Gitea API 错误 %s: %s", e.code, err_body[:500]) + return _json.dumps({"error": f"Gitea API {e.code}", "detail": err_body[:300]}, ensure_ascii=False) + except Exception as e: + logger.error("创建 Gitea 工单失败: %s", e) + return _json.dumps({"error": f"创建工单失败: {e}"}, ensure_ascii=False) + + +CREATE_GITEA_ISSUE_SCHEMA = { + "type": "function", + "function": { + "name": "create_gitea_issue", + "description": ( + "在工单平台(Gitea)中创建 Issue,用于 Bug 跟踪、任务登记或需求管理。" + "适用于:自动为测试失败用例创建 Bug 工单、登记待办任务、记录平台改进建议等。" + "工单平台地址: http://101.43.95.130:3001/admin/aiagent/issues" + ), + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "工单标题(建议使用 [类别] 前缀,如 [Bug]/[质量]/[DevOps])"}, + "body": {"type": "string", "description": "工单描述(Markdown 格式),应包含背景、需求、涉及模块等信息"}, + "assignee": {"type": "string", "description": "指派人用户名(默认 admin)"}, + "labels": {"type": "string", "description": "标签,逗号分隔(如 'bug,high-priority'),需先在 Gitea 中创建对应标签"}, + }, + "required": ["title", "body"], + }, + }, +} + + +# ═══════════════════════════════════════════════════════════════ +# 测试结果解析工具 — 支持 JSON / XML / CSV 格式 +# ═══════════════════════════════════════════════════════════════ + +def parse_test_result_file(file_path: str, file_format: str = "auto") -> str: + """解析测试结果文件(JSON / XML / CSV),提取用例执行统计和失败详情。 + + Args: + file_path: 测试结果文件的绝对路径 + file_format: 文件格式 — "auto" / "json" / "xml" / "csv" + """ + import csv + import io + import json as _json + import os + import xml.etree.ElementTree as ET + + if not os.path.exists(file_path): + return _json.dumps({"error": f"文件不存在: {file_path}"}, ensure_ascii=False) + + try: + with open(file_path, "r", encoding="utf-8", errors="replace") as f: + raw = f.read() + except Exception as e: + return _json.dumps({"error": f"读取文件失败: {e}"}, ensure_ascii=False) + + if not raw.strip(): + return _json.dumps({"error": "文件为空"}, ensure_ascii=False) + + # 自动检测格式 + if file_format == "auto": + stripped = raw.strip() + if stripped.startswith("{") or stripped.startswith("["): + file_format = "json" + elif stripped.startswith("<"): + file_format = "xml" + else: + file_format = "csv" + + try: + if file_format == "json": + data = _json.loads(raw) + report = _parse_json_test_results(data) + elif file_format == "xml": + report = _parse_xml_test_results(raw) + elif file_format == "csv": + report = _parse_csv_test_results(raw) + else: + return _json.dumps({"error": f"不支持的文件格式: {file_format}"}, ensure_ascii=False) + + report["file"] = os.path.basename(file_path) + report["format"] = file_format + logger.info("测试结果解析完成: %s, total=%s, passed=%s, failed=%s", + file_path, report.get("total"), report.get("passed"), report.get("failed")) + return _json.dumps(report, ensure_ascii=False) + except Exception as e: + logger.error("测试结果解析失败: %s", e, exc_info=True) + return _json.dumps({"error": f"解析失败: {e}"}, ensure_ascii=False) + + +def _parse_json_test_results(data) -> dict: + """解析 JSON 格式的测试结果。支持多种常见结构。""" + # 如果是列表,直接作为 cases + cases = [] + if isinstance(data, list): + cases = data + elif isinstance(data, dict): + # 常见字段名 + cases = ( + data.get("testCases") + or data.get("cases") + or data.get("tests") + or data.get("results") + or data.get("suites") + or [] + ) + # JUnit JSON 风格 + if not cases and "testSuites" in data: + for suite in data.get("testSuites", []): + cases.extend(suite.get("testCases", [])) + # 嵌套在 suites[].cases + if not cases and "suites" in data: + for suite in data.get("suites", []): + cases.extend(suite.get("cases", [])) + + if not isinstance(cases, list): + cases = [cases] if cases else [] + + return _summarize_cases(cases) + + +def _parse_xml_test_results(raw: str) -> dict: + """解析 JUnit / TestNG XML 格式的测试结果。""" + import xml.etree.ElementTree as ET + + root = ET.fromstring(raw) + cases = [] + # JUnit 风格: ... + for testcase in root.iter("testcase"): + failure = testcase.find("failure") + error = testcase.find("error") + skipped = testcase.find("skipped") + case = { + "name": testcase.get("name", testcase.get("classname", "")), + "classname": testcase.get("classname", ""), + "time": testcase.get("time", ""), + "status": "skipped" if skipped is not None else ( + "failed" if (failure is not None or error is not None) else "passed" + ), + } + if failure is not None: + case["error_message"] = (failure.text or "").strip()[:500] + case["error_type"] = failure.get("type", "") + if error is not None: + case["error_message"] = (error.text or "").strip()[:500] + case["error_type"] = error.get("type", "") + cases.append(case) + + if not cases: + # TestNG 风格 + for suite in root.iter("suite"): + for test in suite.iter("test"): + for cls in test.iter("class"): + for method in cls.iter("test-method"): + status = method.get("status", "").upper() + case = { + "name": method.get("name", ""), + "classname": cls.get("name", ""), + "status": "passed" if status == "PASS" else ( + "failed" if status == "FAIL" else "skipped" + ), + } + cases.append(case) + + return _summarize_cases(cases) + + +def _parse_csv_test_results(raw: str) -> dict: + """解析 CSV 格式的测试结果。自动检测列名。""" + import csv + import io + + reader = csv.DictReader(io.StringIO(raw)) + cases = [] + for row in reader: + status = ( + row.get("status") + or row.get("result") + or row.get("outcome") + or row.get("state") + or "unknown" + ).lower() + case = { + "name": row.get("name") or row.get("test") or row.get("testcase") or row.get("title") or "", + "status": status, + "error_message": row.get("error") or row.get("message") or row.get("failure") or "", + } + cases.append(case) + return _summarize_cases(cases) + + +def _summarize_cases(cases: list) -> dict: + """统计用例执行结果。""" + total = len(cases) + passed = sum(1 for c in cases if isinstance(c, dict) and c.get("status") in ("passed", "pass", "PASS", "success")) + failed_list = [c for c in cases if isinstance(c, dict) and c.get("status") in ("failed", "fail", "FAIL", "error", "ERROR")] + skipped = sum(1 for c in cases if isinstance(c, dict) and c.get("status") in ("skipped", "skip", "SKIP", "ignored")) + failed = len(failed_list) + # 其余未知状态的归为 other + other = total - passed - failed - skipped + + return { + "total": total, + "passed": passed, + "failed": failed, + "skipped": skipped, + "other": other, + "pass_rate": f"{(passed / total * 100):.1f}%" if total > 0 else "N/A", + "failed_cases": [ + { + "name": c.get("name", c.get("classname", "")), + "classname": c.get("classname", ""), + "error_message": c.get("error_message", ""), + "error_type": c.get("error_type", ""), + } + for c in failed_list[:20] # 最多返回 20 条失败详情 + ], + } + + +PARSE_TEST_RESULT_FILE_SCHEMA = { + "type": "function", + "function": { + "name": "parse_test_result_file", + "description": ( + "解析测试结果文件(JSON / XML / CSV 格式),自动提取用例总数、通过数、失败数、" + "通过率和失败用例详情。支持 JUnit XML、TestNG XML、pytest JSON、CSV 等常见格式。" + "适用于:解析自动化测试报告、提取失败用例用于 Bug 跟踪、生成测试周报等。" + ), + "parameters": { + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "测试结果文件的绝对路径"}, + "file_format": { + "type": "string", + "description": "文件格式:auto(自动检测)/ json / xml / csv(默认 auto)", + }, + }, + "required": ["file_path"], + }, + }, +} diff --git a/backend/app/services/code_review_agent.py b/backend/app/services/code_review_agent.py new file mode 100644 index 0000000..5bc7828 --- /dev/null +++ b/backend/app/services/code_review_agent.py @@ -0,0 +1,215 @@ +""" +代码评审代理 — 数字分身自动审查 PR diff,生成审查报告 +""" +from __future__ import annotations + +import logging +import re +from typing import Any, Dict, List, Optional + +from app.services.fingerprint_engine import fingerprint_engine +from app.services.shadow_executor import shadow_executor +from app.services.behavior_collector import behavior_collector + +logger = logging.getLogger(__name__) + +REVIEW_CATEGORIES = { + "security": {"label": "安全漏洞", "weight": 0.30, "patterns": [ + "password", "secret", "token", "api_key", "private_key", + "eval\\(", "exec\\(", "os\\.system", "subprocess\\.call\\(.*shell=True", + "raw.*sql|execute\\(.*\\+|string\\s+concatenation.*sql", + "innerHTML|dangerouslySetInnerHTML", + ]}, + "data_integrity": {"label": "数据完整性", "weight": 0.20, "patterns": [ + "DELETE\\s+FROM|DROP\\s+TABLE|TRUNCATE", + "db\\.execute|db\\.query.*f[\"']", + "without.*migration|schema.*change", + ]}, + "error_handling": {"label": "异常处理", "weight": 0.15, "patterns": [ + "except:\\s*$|except\\s+Exception:\\s*pass", + "catch\\s*\\(\\s*\\)", + "\\.get\\(.*\\)(?!.*or|.*default)", + ]}, + "performance": {"label": "性能问题", "weight": 0.15, "patterns": [ + "for.*in.*range.*\\n.*query|N\\+1", + "\\.all\\(\\).*for|SELECT \\*", + "sleep\\(|time\\.sleep", + ]}, + "readability": {"label": "可读性", "weight": 0.10, "patterns": [ + "def [a-z]{1,2}\\(", + "#.*TODO|#.*FIXME|#.*HACK", + ]}, + "style": {"label": "代码风格", "weight": 0.10, "patterns": [ + "print\\(|console\\.log", + "debug=True|DEBUG\\s*=\\s*True", + ]}, +} + + +class CodeReviewAgent: + """代码评审代理 — 数字分身审查 PR""" + + def review_pr(self, user_id: str, pr_url: str, + title: str = "", description: str = "", + diff: str = "", files_changed: Optional[List[str]] = None) -> Dict[str, Any]: + """评审 PR diff,生成审查报告。 + + Args: + user_id: 用户ID(用于加载行为指纹偏好) + pr_url: PR 链接 + title: PR 标题 + description: PR 描述 + diff: diff 内容(可从 Gitea API 获取) + files_changed: 变更文件列表 + """ + fp = fingerprint_engine.get_fingerprint(user_id) + preference = (fp.get("preference_weights", {}).get("code_review") if fp else None) or {} + review_focus = self._compute_review_focus(preference) + + findings = [] + if diff: + findings = self._analyze_diff(diff, review_focus, files_changed or []) + + severity_counts = {"critical": 0, "major": 0, "minor": 0, "suggestion": 0} + for f in findings: + severity_counts[f["severity"]] = severity_counts.get(f["severity"], 0) + 1 + + verdict = self._determine_verdict(findings) + summary = self._generate_summary(title, description, findings, verdict) + + suggestion = { + "category": "code_review", + "pr_url": pr_url, + "title": title, + "verdict": verdict, + "summary": summary, + "findings": findings, + "severity_counts": severity_counts, + "review_focus": review_focus, + "files_changed": files_changed or [], + } + + # 通过影子模式记录(如果启用) + shadow_executor.generate_suggestion(user_id, "code_review", { + "pr_url": pr_url, "diff_length": len(diff), "files_count": len(files_changed or []), + }) + + # 记录行为 + behavior_collector.log_fire_and_forget( + user_id=user_id, + category="code_review", + action="review_pr", + context={"pr_url": pr_url, "files_count": len(files_changed or [])}, + result={"verdict": verdict, "findings_count": len(findings)}, + ) + + return suggestion + + def _compute_review_focus(self, preference: Dict[str, float]) -> Dict[str, float]: + """根据用户偏好调整审查权重。""" + focus = {} + for cat, meta in REVIEW_CATEGORIES.items(): + pref_key = cat + user_weight = preference.get(pref_key, meta["weight"]) + focus[cat] = round(user_weight, 2) + sorted_focus = dict(sorted(focus.items(), key=lambda x: x[1], reverse=True)) + return sorted_focus + + def _analyze_diff(self, diff: str, review_focus: Dict[str, float], + files: List[str]) -> List[Dict[str, Any]]: + """分析 diff 内容,识别问题。""" + findings = [] + lines = diff.split("\n") + + for cat, meta in REVIEW_CATEGORIES.items(): + if review_focus.get(cat, 0) < 0.05: + continue + + for pattern in meta["patterns"]: + for i, line in enumerate(lines): + if re.search(pattern, line, re.IGNORECASE): + context_start = max(0, i - 2) + context_end = min(len(lines), i + 3) + severity = self._classify_severity(cat, line) + findings.append({ + "category": cat, + "label": meta["label"], + "severity": severity, + "line_number": i + 1, + "matched_line": line.strip()[:200], + "context": "\n".join(lines[context_start:context_end]), + "message": self._generate_finding_message(cat, line.strip()), + }) + break + + # 限制最多 30 条 + findings.sort(key=lambda x: {"critical": 0, "major": 1, "minor": 2, "suggestion": 3}[x["severity"]]) + return findings[:30] + + def _classify_severity(self, category: str, line: str) -> str: + """根据类别和行内容判定严重程度。""" + critical_patterns = [ + "password", "secret", "token", "api_key", "private_key", + "DELETE FROM", "DROP TABLE", "eval(", "exec(", + ] + major_patterns = [ + "raw.*sql", "innerHTML", "dangerouslySetInnerHTML", + "except:", "except Exception:", "pass", + ] + line_lower = line.lower() + if any(p.lower() in line_lower for p in critical_patterns): + return "critical" + if any(p.lower() in line_lower for p in major_patterns): + return "major" + if category in ("performance", "data_integrity"): + return "major" + if category in ("error_handling",): + return "minor" + return "suggestion" + + def _generate_finding_message(self, category: str, line: str) -> str: + """生成问题描述。""" + messages = { + "security": f"潜在安全风险: {line[:80]}", + "data_integrity": f"数据操作需注意: {line[:80]}", + "error_handling": f"异常处理建议改进: {line[:80]}", + "performance": f"可能的性能问题: {line[:80]}", + "readability": f"可读性建议: {line[:80]}", + "style": f"代码风格提醒: {line[:80]}", + } + return messages.get(category, f"代码问题: {line[:80]}") + + def _determine_verdict(self, findings: List[Dict[str, Any]]) -> str: + """根据发现的问题判定审查结论。""" + critical = sum(1 for f in findings if f["severity"] == "critical") + major = sum(1 for f in findings if f["severity"] == "major") + if critical > 0: + return "request_changes" + if major > 3: + return "request_changes" + if major > 0: + return "comment" + if findings: + return "comment" + return "approve" + + def _generate_summary(self, title: str, description: str, + findings: List[Dict[str, Any]], verdict: str) -> str: + """生成审查摘要。""" + verdict_text = { + "approve": "建议通过 — 未发现重大问题", + "comment": "建议改进后通过 — 有若干建议可参考", + "request_changes": "建议修改后重新提交 — 存在需要关注的问题", + } + critical = sum(1 for f in findings if f["severity"] == "critical") + major = sum(1 for f in findings if f["severity"] == "major") + minor = sum(1 for f in findings if f["severity"] == "minor") + return ( + f"PR: {title or 'Untitled'}\n" + f"审查结论: {verdict_text.get(verdict, verdict)}\n" + f"发现问题: {critical} 严重, {major} 重要, {minor} 次要, " + f"共 {len(findings)} 条" + ) + + +code_review_agent = CodeReviewAgent() diff --git a/backend/app/services/cost_estimator.py b/backend/app/services/cost_estimator.py new file mode 100644 index 0000000..8b68c18 --- /dev/null +++ b/backend/app/services/cost_estimator.py @@ -0,0 +1,78 @@ +"""LLM 成本估算 — 基于模型定价和 Token 用量估算费用""" +from __future__ import annotations + +import logging +from typing import Dict, Optional, Tuple + +logger = logging.getLogger(__name__) + +# 模型定价 (per 1M tokens, USD) — 2024 Q4 参考值 +MODEL_PRICING: Dict[str, Tuple[float, float]] = { + # (input_price_per_1M, output_price_per_1M) + "gpt-4o": (2.50, 10.00), + "gpt-4o-mini": (0.15, 0.60), + "gpt-4-turbo": (10.00, 30.00), + "gpt-3.5-turbo": (0.50, 1.50), + "deepseek-chat": (0.14, 0.28), + "deepseek-v3": (0.27, 1.10), + "deepseek-r1": (0.55, 2.19), + "deepseek-v4-flash": (0.14, 0.28), + "deepseek-v4-pro": (0.27, 1.10), + "claude-3-opus": (15.00, 75.00), + "claude-3-sonnet": (3.00, 15.00), + "claude-3-haiku": (0.25, 1.25), + "claude-3.5-sonnet": (3.00, 15.00), + "claude-3.5-haiku": (1.00, 5.00), +} + +# 缓存未命中模型的默认定价 +DEFAULT_PRICING = (1.00, 4.00) + + +def estimate_cost( + model: str, + prompt_tokens: int = 0, + completion_tokens: int = 0, +) -> float: + """估算单次 LLM 调用的费用(USD)。 + + Args: + model: 模型名称(模糊匹配) + prompt_tokens: 输入 token 数 + completion_tokens: 输出 token 数 + """ + input_price, output_price = _get_price(model) + cost = (prompt_tokens / 1_000_000) * input_price + ( + completion_tokens / 1_000_000 + ) * output_price + return round(cost, 6) + + +def estimate_cost_yuan( + model: str, + prompt_tokens: int = 0, + completion_tokens: int = 0, + exchange_rate: float = 7.2, +) -> float: + """估算费用(人民币)。""" + return round(estimate_cost(model, prompt_tokens, completion_tokens) * exchange_rate, 4) + + +def _get_price(model: str) -> Tuple[float, float]: + """模糊匹配模型定价。""" + model_lower = model.lower().replace("-", "").replace(".", "") + for key, price in MODEL_PRICING.items(): + if key.replace("-", "").replace(".", "") in model_lower: + return price + return DEFAULT_PRICING + + +def get_model_pricing_table() -> Dict[str, dict]: + """返回模型定价表(供前端展示)。""" + return { + model: { + "input_per_1M": input_p, + "output_per_1M": output_p, + } + for model, (input_p, output_p) in MODEL_PRICING.items() + } diff --git a/backend/app/services/decision_authorizer.py b/backend/app/services/decision_authorizer.py new file mode 100644 index 0000000..110dac1 --- /dev/null +++ b/backend/app/services/decision_authorizer.py @@ -0,0 +1,299 @@ +""" +决策授权体系 — L0-L4 五级风险授权,数字分身根据风险等级自动判定执行权限 +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional + +from app.services.fingerprint_engine import fingerprint_engine +from app.services.shadow_executor import shadow_executor +from app.services.behavior_collector import behavior_collector + +logger = logging.getLogger(__name__) + +AUTH_LEVELS = { + "L0": { + "name": "全自动执行", + "description": "低风险操作,数字分身直接执行", + "risk_range": (0, 0.2), + "requires_approval": False, + "notify": False, + "examples": ["格式化代码", "拼写检查修正", "简单文案调整"], + }, + "L1": { + "name": "自动执行+通知", + "description": "低风险,自动执行后通知用户", + "risk_range": (0.2, 0.4), + "requires_approval": False, + "notify": True, + "examples": ["合并小PR", "回复常规邮件", "更新文档"], + }, + "L2": { + "name": "建议+确认", + "description": "中风险,数字分身生成建议,需用户一次确认", + "risk_range": (0.4, 0.6), + "requires_approval": True, + "notify": True, + "examples": ["修改API接口", "调整配置参数", "代码重构"], + }, + "L3": { + "name": "详细审批", + "description": "高风险,需要详细说明和多重审批", + "risk_range": (0.6, 0.8), + "requires_approval": True, + "notify": True, + "examples": ["修改数据库Schema", "涉及资金的变更", "权限调整"], + }, + "L4": { + "name": "禁止自动", + "description": "极高风险,始终需要人工操作", + "risk_range": (0.8, 1.0), + "requires_approval": True, + "notify": True, + "examples": ["删除生产数据", "修改权限体系", "涉及合规操作"], + }, +} + +RISK_FACTORS = { + "data_mutation": {"weight": 0.25, "keywords": ["DELETE", "DROP", "UPDATE", "INSERT", "ALTER", "TRUNCATE", "删除", "修改", "写入", "变更"]}, + "permission_change": {"weight": 0.25, "keywords": ["权限", "role", "permission", "auth", "授权", "访问控制"]}, + "financial": {"weight": 0.30, "keywords": ["支付", "金额", "资金", "退款", "结算", "payment", "refund", "price", "cost", "费用"]}, + "production_impact": {"weight": 0.20, "keywords": ["生产", "production", "线上", "prod", "正式环境", "发布", "deploy"]}, + "user_data": {"weight": 0.15, "keywords": ["用户数据", "user.*data", "PII", "手机号", "身份证", "隐私"]}, + "irreversible": {"weight": 0.20, "keywords": ["不可逆", "irreversible", "清空", "重置", "reset", "hard delete"]}, +} + + +class DecisionAuthorizer: + """决策授权引擎 — L0-L4 风险定级""" + + def evaluate(self, user_id: str, action: str, target: str = "", + context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """评估操作风险等级并返回授权决策。 + + Args: + user_id: 用户ID + action: 操作描述(如 "修改数据库schema", "回复邮件") + target: 操作目标(如具体表名、文件路径) + context: 附加上下文信息 + """ + context = context or {} + + # 1. 计算风险分数 + risk_score, risk_factors = self._calculate_risk(action, target, context) + + # 2. 确定授权等级 + auth_level = self._determine_level(risk_score) + + # 3. 用户信任度调整 + fp = fingerprint_engine.get_fingerprint(user_id) + trust_bonus = self._compute_trust_bonus(user_id, fp) + adjusted_score = max(0, risk_score - trust_bonus) + adjusted_level = self._determine_level(adjusted_score) + + # 4. 影子模式对比(如果是新类别操作) + shadow_comparison = None + category = context.get("category", self._infer_category(action)) + if risk_score > 0.3: + shadow_suggestion = shadow_executor.generate_suggestion(user_id, category, { + "action": action, "target": target, "risk_score": risk_score, + }) + shadow_comparison = { + "suggestion_generated": True, + "shadow_confidence": shadow_suggestion.get("confidence", 0), + } + + decision = { + "action": action, + "target": target, + "risk_score": round(risk_score, 3), + "adjusted_score": round(adjusted_score, 3), + "raw_level": auth_level, + "adjusted_level": adjusted_level, + "trust_bonus": round(trust_bonus, 3), + "risk_factors": risk_factors, + "requires_approval": AUTH_LEVELS[adjusted_level]["requires_approval"], + "notify_user": AUTH_LEVELS[adjusted_level]["notify"], + "level_detail": AUTH_LEVELS[adjusted_level], + "shadow_comparison": shadow_comparison, + } + + # 如果需要审批,生成审批要求 + if decision["requires_approval"]: + decision["approval_requirements"] = self._generate_approval_requirements( + adjusted_level, action, risk_factors + ) + + # 记录行为 + behavior_collector.log_fire_and_forget( + user_id=user_id, + category="decision", + action="authorize_action", + context={"action": action, "target": target}, + result={"level": adjusted_level, "risk_score": adjusted_score}, + ) + + return decision + + def _calculate_risk(self, action: str, target: str, + context: Dict[str, Any]) -> tuple: + """计算操作风险分数和风险因子。""" + text = f"{action} {target}" + total_score = 0 + max_score = 0 + matched_factors = [] + + for factor_name, factor in RISK_FACTORS.items(): + max_score += factor["weight"] + for keyword in factor["keywords"]: + if keyword.lower() in text.lower(): + severity = self._factor_severity(factor_name, text) + contribution = factor["weight"] * severity + total_score += contribution + matched_factors.append({ + "factor": factor_name, + "contribution": round(contribution, 3), + "severity": round(severity, 2), + }) + break + + # 归一化 + if max_score > 0: + risk_score = min(1.0, total_score / max_score) + else: + risk_score = 0.1 + + # 上下文调整 + if context.get("is_dry_run"): + risk_score *= 0.3 + if context.get("has_rollback_plan"): + risk_score *= 0.7 + if context.get("is_peak_hours"): + risk_score *= 1.2 + + return min(1.0, risk_score), matched_factors + + def _factor_severity(self, factor_name: str, text: str) -> float: + """评估单个风险因子的严重程度。""" + if factor_name == "financial": + if any(kw in text for kw in ["退款", "refund", "实际扣款"]): + return 1.0 + return 0.7 + if factor_name == "irreversible": + if "删除" in text or "DELETE" in text.upper(): + return 1.0 + return 0.6 + if factor_name == "production_impact": + if "生产" in text or "production" in text.lower(): + return 0.9 + return 0.5 + return 0.5 + + def _determine_level(self, score: float) -> str: + """根据分数确定授权等级。""" + if score <= 0.2: + return "L0" + elif score <= 0.4: + return "L1" + elif score <= 0.6: + return "L2" + elif score <= 0.8: + return "L3" + else: + return "L4" + + def _compute_trust_bonus(self, user_id: str, + fp: Optional[Dict[str, Any]]) -> float: + """根据用户历史行为计算信任加成。""" + if not fp: + return 0 + + total = fp.get("total_behaviors", 0) + avg_response = fp.get("avg_response_time_ms") + + bonus = 0 + # 行为数据量 + if total > 500: + bonus += 0.1 + elif total > 100: + bonus += 0.05 + + # 平均响应快 → 经验丰富 + if avg_response and avg_response < 30000: + bonus += 0.05 + + # 影子模式准确率(如果有) + try: + accuracy = shadow_executor.get_accuracy(user_id) + avg_acc = accuracy.get("average_accuracy", 0) + if avg_acc > 0.9: + bonus += 0.1 + elif avg_acc > 0.8: + bonus += 0.05 + except Exception: + pass + + return min(bonus, 0.2) + + def _infer_category(self, action: str) -> str: + """从操作描述推断类别。""" + if any(kw in action for kw in ["代码", "code", "PR", "review"]): + return "code_review" + if any(kw in action for kw in ["邮件", "email", "回复", "reply"]): + return "email" + if any(kw in action for kw in ["文档", "document", "合同", "contract"]): + return "document" + return "decision" + + def _generate_approval_requirements(self, level: str, action: str, + risk_factors: List[Dict[str, Any]]) -> Dict[str, Any]: + """生成审批要求。""" + requirements = { + "L2": { + "approvers": ["直接上级或项目负责人"], + "detail_required": ["操作摘要", "影响范围", "回滚方案"], + "auto_approve_after_hours": 24, + }, + "L3": { + "approvers": ["技术负责人", "产品负责人"], + "detail_required": ["操作摘要", "详细方案", "影响范围评估", "回滚方案", "测试结果"], + "auto_approve_after_hours": 48, + }, + "L4": { + "approvers": ["技术负责人", "产品负责人", "安全负责人"], + "detail_required": ["操作摘要", "详细方案", "影响范围评估", "风险评估报告", "回滚方案", "应急预案"], + "auto_approve_after_hours": None, # 永不自动批准 + }, + } + + base = requirements.get(level, requirements["L2"]) + base["risk_factors"] = [r["factor"] for r in risk_factors] + base["action"] = action + return base + + def get_authorization_summary(self, user_id: str, days: int = 30) -> Dict[str, Any]: + """获取用户授权历史摘要。""" + # 这里需要 behavior_collector 的查询能力 + behaviors = behavior_collector.get_user_behaviors( + user_id=user_id, category="decision", limit=100 + ) + + by_level = {"L0": 0, "L1": 0, "L2": 0, "L3": 0, "L4": 0} + for b in behaviors: + result = b.get("result") if isinstance(b, dict) else (b.result or {}) + level = result.get("level", "L2") if isinstance(result, dict) else "L2" + by_level[level] = by_level.get(level, 0) + 1 + + return { + "user_id": user_id, + "period_days": days, + "total_decisions": sum(by_level.values()), + "by_level": by_level, + "auto_executed": by_level["L0"] + by_level["L1"], + "required_approval": by_level["L2"] + by_level["L3"] + by_level["L4"], + "auto_rate": round((by_level["L0"] + by_level["L1"]) / max(sum(by_level.values()), 1), 3), + } + + +decision_authorizer = DecisionAuthorizer() diff --git a/backend/app/services/document_review_agent.py b/backend/app/services/document_review_agent.py new file mode 100644 index 0000000..01c49b5 --- /dev/null +++ b/backend/app/services/document_review_agent.py @@ -0,0 +1,257 @@ +""" +文档审查代理 — 数字分身自动审查合同/文档,检查完整性、风险点、格式 +""" +from __future__ import annotations + +import logging +import re +from typing import Any, Dict, List, Optional + +from app.services.fingerprint_engine import fingerprint_engine +from app.services.behavior_collector import behavior_collector + +logger = logging.getLogger(__name__) + +DOCUMENT_CHECKLISTS = { + "contract": { + "parties": {"label": "签约方信息", "checks": ["甲方全称", "乙方全称", "统一社会信用代码", "法定代表人", "联系方式"]}, + "subject": {"label": "合同标的", "checks": ["标的描述是否明确", "数量/规格是否清晰", "质量标准是否定义"]}, + "payment": {"label": "付款条款", "checks": ["金额大小写一致", "付款节点清晰", "发票要求", "违约金比例 ≤ 30%"]}, + "term": {"label": "履约期限", "checks": ["起止日期明确", "延期条款", "不可抗力定义"]}, + "liability": {"label": "违约责任", "checks": ["双方对等", "赔偿上限合理", "免责条款不违法"]}, + "confidentiality": {"label": "保密条款", "checks": ["保密范围", "保密期限", "违约责任"]}, + "termination": {"label": "终止条款", "checks": ["解除条件", "善后义务", "争议解决方式"]}, + "signature": {"label": "签章", "checks": ["签字日期", "盖章位置", "骑缝章"]}, + }, + "technical_doc": { + "overview": {"label": "概述", "checks": ["背景说明", "目标定义", "适用范围"]}, + "terms": {"label": "术语定义", "checks": ["关键术语有定义", "缩写全称首次出现"]}, + "architecture": {"label": "架构设计", "checks": ["架构图/示意图", "模块划分", "接口定义"]}, + "implementation": {"label": "实现细节", "checks": ["核心逻辑描述", "关键配置项", "异常处理设计"]}, + "deployment": {"label": "部署运维", "checks": ["环境要求", "部署步骤", "监控指标", "回滚方案"]}, + }, + "email": { + "subject": {"label": "主题", "checks": ["主题简明扼要", "无多余前缀"]}, + "recipient": {"label": "收件人", "checks": ["To/CC/BCC 区分正确", "收件人数量合理"]}, + "greeting": {"label": "称呼", "checks": ["称呼得体", "与收件人类系匹配"]}, + "body": {"label": "正文", "checks": ["核心信息在前", "段落简短", "行动项明确"]}, + "attachments": {"label": "附件", "checks": ["附件已提及", "文件大小合理", "格式通用"]}, + "closing": {"label": "结尾", "checks": ["署名完整", "签名档信息准确"]}, + }, +} + +RISK_PATTERNS = { + "high": [ + (r"不承担.*任何.*责任", "单方面免责声明"), + (r"自动续期.*未.*通知", "自动续期条款需关注"), + (r"违约金.*超过.*30%", "违约金比例可能过高"), + (r"放弃.*诉讼.*权利", "放弃诉讼权利条款"), + (r"无条件.*连带.*保证", "无条件连带保证责任"), + ], + "medium": [ + (r"以.*为准.*解释", "单方解释权条款"), + (r"仲裁.*北京|仲裁.*上海", "仲裁地条款需确认"), + (r"保密.*永久", "永久保密义务可能过于严苛"), + (r"竞业.*2.*年|竞业.*3.*年", "竞业限制期限较长"), + ], + "low": [ + (r"未尽事宜.*协商", "兜底条款可接受"), + (r"一式.*份", "签署份数需确认"), + ], +} + + +class DocumentReviewAgent: + """文档审查代理""" + + def review_document(self, user_id: str, document_type: str, + title: str, content: str, + metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """审查文档/合同。 + + Args: + user_id: 用户ID(用于加载偏好) + document_type: contract / technical_doc / email + title: 文档标题 + content: 文档内容 + metadata: 附加元数据 + """ + fp = fingerprint_engine.get_fingerprint(user_id) + preference = (fp.get("preference_weights", {}).get("document") if fp else None) or {} + + checklist = DOCUMENT_CHECKLISTS.get(document_type, DOCUMENT_CHECKLISTS["contract"]) + + # 1. 完整性检查 + completeness = self._check_completeness(content, checklist) + + # 2. 风险扫描 + risks = self._scan_risks(content) + + # 3. 格式检查 + format_issues = self._check_format(content, document_type) + + # 4. 生成建议 + suggestions = self._generate_suggestions(completeness, risks, format_issues) + + score = self._calculate_score(completeness, risks, format_issues) + + report = { + "document_type": document_type, + "title": title, + "overall_score": score, + "grade": self._grade(score), + "completeness": completeness, + "risks": risks, + "format_issues": format_issues, + "suggestions": suggestions, + "user_preference_applied": bool(preference), + } + + # 记录行为 + behavior_collector.log_fire_and_forget( + user_id=user_id, + category="document", + action=f"review_{document_type}", + context={"title": title, "doc_type": document_type, "content_length": len(content)}, + result={"score": score, "risks_count": len(risks)}, + ) + + return report + + def _check_completeness(self, content: str, checklist: Dict[str, Any]) -> Dict[str, Any]: + """检查文档完整性。""" + results = {} + for section_id, section in checklist.items(): + checked = [] + for item in section["checks"]: + present = self._fuzzy_contains(content, item) + checked.append({"item": item, "present": present}) + passed = sum(1 for c in checked if c["present"]) + total = len(checked) + results[section_id] = { + "label": section["label"], + "passed": passed, + "total": total, + "percentage": round(passed / total * 100, 1) if total > 0 else 0, + "details": checked, + } + return results + + def _fuzzy_contains(self, content: str, query: str) -> bool: + """模糊检查内容是否包含关键信息。""" + content_lower = content.lower() + parts = re.split(r'[,,、\s]+', query) + for part in parts: + if len(part) >= 2 and part.lower() in content_lower: + return True + if len(query) >= 2 and query.lower() in content_lower: + return True + return False + + def _scan_risks(self, content: str) -> List[Dict[str, Any]]: + """扫描风险条款。""" + risks = [] + for severity, patterns in RISK_PATTERNS.items(): + for pattern, label in patterns: + matches = re.findall(pattern, content) + if matches: + risks.append({ + "severity": severity, + "label": label, + "pattern": pattern, + "match_count": len(matches), + "snippet": self._extract_snippet(content, pattern), + }) + risks.sort(key=lambda r: {"high": 0, "medium": 1, "low": 2}[r["severity"]]) + return risks + + def _extract_snippet(self, content: str, pattern: str) -> str: + """提取匹配行的上下文片段。""" + for line in content.split("\n"): + if re.search(pattern, line): + return line.strip()[:150] + return "" + + def _check_format(self, content: str, doc_type: str) -> List[Dict[str, Any]]: + """检查格式问题。""" + issues = [] + + if doc_type == "contract": + if not re.search(r'[((]\s*[一二三1-3]\s*[))]', content): + issues.append({"type": "structure", "message": "未检测到分条结构,合同建议使用条/款/项结构"}) + if len(content) < 500: + issues.append({"type": "length", "message": f"文档仅 {len(content)} 字符,合同内容可能不完整"}) + + if doc_type == "technical_doc": + if not re.search(r'#+\s|第[一二三1-3]章|第[一二三1-3]节', content): + issues.append({"type": "structure", "message": "未检测到章节标题,建议添加层次化标题"}) + + if doc_type == "email": + if len(content) > 5000: + issues.append({"type": "length", "message": "邮件过长,建议精简核心内容"}) + + has_line_breaks = len(re.findall(r'\n\s*\n', content)) + if has_line_breaks == 0 and len(content) > 1000: + issues.append({"type": "readability", "message": "大段文字缺少分段,建议增加段落间距"}) + + return issues + + def _generate_suggestions(self, completeness: Dict, risks: List, + format_issues: List) -> List[str]: + """汇总改善建议。""" + suggestions = [] + + for section_id, result in completeness.items(): + if result["percentage"] < 50: + suggestions.append( + f"[{result['label']}] 完整性仅 {result['percentage']}%,建议补充缺少的要素" + ) + elif result["percentage"] < 80: + missing = [d["item"] for d in result.get("details", []) if not d["present"]] + suggestions.append( + f"[{result['label']}] 缺少: {', '.join(missing[:3])}" + ) + + high_risks = [r for r in risks if r["severity"] == "high"] + if high_risks: + suggestions.append(f"发现 {len(high_risks)} 个高风险条款: " + f"{', '.join(r['label'] for r in high_risks)}") + + for issue in format_issues: + suggestions.append(f"[格式] {issue['message']}") + + return suggestions + + def _calculate_score(self, completeness: Dict, risks: List, + format_issues: List) -> float: + """计算综合评分 (0-100)。""" + # 完整性权重 40% + if completeness: + avg = sum(r["percentage"] for r in completeness.values()) / len(completeness) + else: + avg = 0 + completeness_score = avg * 0.4 + + # 风险权重 40% + risk_penalty = sum({"high": 20, "medium": 10, "low": 5}[r["severity"]] for r in risks) + risk_score = max(0, 40 - risk_penalty) + + # 格式权重 20% + format_score = max(0, 20 - len(format_issues) * 5) + + return round(completeness_score + risk_score + format_score, 1) + + def _grade(self, score: float) -> str: + if score >= 90: + return "A — 优秀" + elif score >= 75: + return "B — 良好" + elif score >= 60: + return "C — 需改进" + elif score >= 40: + return "D — 风险较高" + else: + return "F — 不建议签署/发布" + + +document_review_agent = DocumentReviewAgent() diff --git a/backend/app/services/execution_logger.py b/backend/app/services/execution_logger.py index b652790..6c24071 100644 --- a/backend/app/services/execution_logger.py +++ b/backend/app/services/execution_logger.py @@ -1,29 +1,25 @@ """ 执行日志服务 +- ExecutionLogger: 工作流执行的日志记录器(per-execution) +- ExecutionLoggerService: Agent 执行的日志服务(全局单例) """ -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List from datetime import datetime from sqlalchemy.orm import Session from app.models.execution_log import ExecutionLog +import asyncio import logging logger = logging.getLogger(__name__) class ExecutionLogger: - """执行日志记录器""" - + """执行日志记录器(工作流/编排执行上下文)""" + def __init__(self, execution_id: str, db: Session): - """ - 初始化日志记录器 - - Args: - execution_id: 执行ID - db: 数据库会话 - """ self.execution_id = execution_id self.db = db - + def log( self, level: str, @@ -33,17 +29,6 @@ class ExecutionLogger: data: Optional[Dict[str, Any]] = None, duration: Optional[int] = None ): - """ - 记录日志 - - Args: - level: 日志级别 (INFO/WARN/ERROR/DEBUG) - message: 日志消息 - node_id: 节点ID(可选) - node_type: 节点类型(可选) - data: 附加数据(可选) - duration: 执行耗时(毫秒,可选) - """ try: log_entry = ExecutionLog( execution_id=self.execution_id, @@ -57,61 +42,129 @@ class ExecutionLogger: ) self.db.add(log_entry) self.db.commit() - - # 同时输出到标准日志 log_method = getattr(logger, level.lower(), logger.info) log_msg = f"[执行 {self.execution_id}]" if node_id: log_msg += f" [节点 {node_id}]" log_msg += f" {message}" log_method(log_msg) - except Exception as e: - # 如果数据库记录失败,至少输出到标准日志 logger.error(f"记录执行日志失败: {str(e)}") - logger.error(f"[执行 {self.execution_id}] {message}") - + def info(self, message: str, **kwargs): - """记录INFO级别日志""" self.log("INFO", message, **kwargs) - + def warn(self, message: str, **kwargs): - """记录WARN级别日志""" self.log("WARN", message, **kwargs) - + def error(self, message: str, **kwargs): - """记录ERROR级别日志""" self.log("ERROR", message, **kwargs) - + def debug(self, message: str, **kwargs): - """记录DEBUG级别日志""" self.log("DEBUG", message, **kwargs) - + def log_node_start(self, node_id: str, node_type: str, input_data: Optional[Dict[str, Any]] = None): - """记录节点开始执行""" self.info( f"节点 {node_id} ({node_type}) 开始执行", - node_id=node_id, - node_type=node_type, + node_id=node_id, node_type=node_type, data={"input": input_data} if input_data else None ) - + def log_node_complete(self, node_id: str, node_type: str, output_data: Optional[Dict[str, Any]] = None, duration: Optional[int] = None): - """记录节点执行完成""" self.info( f"节点 {node_id} ({node_type}) 执行完成", - node_id=node_id, - node_type=node_type, + node_id=node_id, node_type=node_type, data={"output": output_data} if output_data else None, duration=duration ) - + def log_node_error(self, node_id: str, node_type: str, error: Exception, duration: Optional[int] = None): - """记录节点执行错误""" self.error( f"节点 {node_id} ({node_type}) 执行失败: {str(error)}", - node_id=node_id, - node_type=node_type, + node_id=node_id, node_type=node_type, data={"error": str(error), "error_type": type(error).__name__}, duration=duration ) + + +class ExecutionLoggerService: + """Agent 执行日志服务(全局单例,记录 Agent 每次执行的完整信息)""" + + def log_execution_sync( + self, + *, + agent_id: Optional[str] = None, + agent_name: Optional[str] = None, + goal_id: Optional[str] = None, + task_id: Optional[str] = None, + user_id: Optional[str] = None, + session_id: Optional[str] = None, + input_text: Optional[str] = None, + output_text: Optional[str] = None, + output_truncated: bool = False, + success: bool = True, + error_message: Optional[str] = None, + latency_ms: int = 0, + iterations_used: int = 0, + tool_calls_made: int = 0, + tool_chain: Optional[List[Dict[str, Any]]] = None, + llm_calls: Optional[List[Dict[str, Any]]] = None, + steps: Optional[List[Dict[str, Any]]] = None, + model: Optional[str] = None, + provider: Optional[str] = None, + ) -> Optional[str]: + """同步写入执行日志,返回日志 ID。""" + from app.core.database import SessionLocal + from app.models.agent_execution_log import AgentExecutionLog + db: Optional[Session] = None + try: + db = SessionLocal() + entry = AgentExecutionLog( + agent_id=agent_id, agent_name=agent_name, + goal_id=goal_id, task_id=task_id, + user_id=user_id, session_id=session_id, + input_text=input_text[:5000] if input_text else None, + output_text=output_text[:10000] if output_text else None, + output_truncated=output_truncated, + success=success, + error_message=error_message[:2000] if error_message else None, + latency_ms=latency_ms, + iterations_used=iterations_used, + tool_calls_made=tool_calls_made, + tool_chain=tool_chain, llm_calls=llm_calls, steps=steps, + model=model, provider=provider, + ) + db.add(entry) + db.commit() + db.refresh(entry) + return str(entry.id) + except Exception as e: + logger.warning("写入 Agent 执行日志失败: %s", e) + if db: + try: + db.rollback() + except Exception: + pass + return None + finally: + if db: + try: + db.close() + except Exception: + pass + + async def log_execution(self, **kwargs) -> Optional[str]: + """异步写入执行日志(线程池)。""" + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, lambda: self.log_execution_sync(**kwargs)) + + def log_execution_fire_and_forget(self, **kwargs): + """Fire-and-forget 写入。""" + try: + asyncio.ensure_future(self.log_execution(**kwargs)) + except Exception: + pass + + +# 全局单例 +execution_logger = ExecutionLoggerService() diff --git a/backend/app/services/feedback_learner.py b/backend/app/services/feedback_learner.py new file mode 100644 index 0000000..fb97f42 --- /dev/null +++ b/backend/app/services/feedback_learner.py @@ -0,0 +1,209 @@ +""" +用户反馈学习服务 — 采集反馈信号,自动调整 Agent 策略 +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional +from collections import Counter, defaultdict + +from sqlalchemy import func, desc +from sqlalchemy.orm import Session + +from app.core.database import SessionLocal +from app.models.feedback_record import FeedbackRecord +from app.models.agent_execution_log import AgentExecutionLog + +logger = logging.getLogger(__name__) + + +class FeedbackLearner: + """从用户反馈中学习,自动调整 Agent 策略""" + + def record_feedback( + self, + user_id: str, + signal_type: str, + *, + execution_log_id: Optional[str] = None, + agent_name: Optional[str] = None, + task_id: Optional[str] = None, + original_output: Optional[str] = None, + user_correction: Optional[str] = None, + feedback_context: Optional[Dict[str, Any]] = None, + ) -> Optional[str]: + """记录一条用户反馈。""" + db: Optional[Session] = None + try: + db = SessionLocal() + + severity = 0.5 + if signal_type == "reject_approval": + severity = 0.9 + elif signal_type == "thumbs_down": + severity = 0.7 + elif signal_type == "manual_edit": + severity = 0.6 + elif signal_type == "retry_command": + severity = 0.4 + + entry = FeedbackRecord( + user_id=user_id, + signal_type=signal_type, + severity=severity, + execution_log_id=execution_log_id, + agent_name=agent_name, + task_id=task_id, + original_output=original_output[:5000] if original_output else None, + user_correction=user_correction[:5000] if user_correction else None, + feedback_context=feedback_context, + ) + db.add(entry) + db.commit() + db.refresh(entry) + + # 标记相关执行日志的反馈 + if execution_log_id: + exec_log = db.query(AgentExecutionLog).filter( + AgentExecutionLog.id == execution_log_id + ).first() + if exec_log: + exec_log.user_rating = 1 if signal_type in ("thumbs_down", "reject_approval") else 3 + exec_log.user_feedback = signal_type + + db.commit() + return str(entry.id) + + except Exception as e: + logger.error("记录反馈失败: %s", e) + if db: + try: + db.rollback() + except Exception: + pass + return None + finally: + if db: + try: + db.close() + except Exception: + pass + + def analyze_feedback_patterns(self, agent_name: Optional[str] = None, days: int = 7) -> Dict[str, Any]: + """分析反馈模式,识别需要调整的策略。""" + db: Optional[Session] = None + try: + db = SessionLocal() + from datetime import datetime, timedelta + since = datetime.now() - timedelta(days=days) + + q = db.query(FeedbackRecord).filter(FeedbackRecord.created_at >= since) + if agent_name: + q = q.filter(FeedbackRecord.agent_name == agent_name) + + records = q.all() + if not records: + return {"total_feedback": 0, "message": "近期无反馈"} + + # 统计信号类型 + signal_dist = Counter(r.signal_type for r in records) + + # 按 Agent 分组 + by_agent = defaultdict(lambda: {"total": 0, "negative": 0, "patterns": []}) + for r in records: + name = r.agent_name or "unknown" + by_agent[name]["total"] += 1 + if r.signal_type in ("thumbs_down", "reject_approval"): + by_agent[name]["negative"] += 1 + + # 生成策略建议 + strategy_advice = [] + total = len(records) + negative_rate = (signal_dist.get("thumbs_down", 0) + signal_dist.get("reject_approval", 0)) / total if total > 0 else 0 + + if negative_rate > 0.3: + strategy_advice.append({ + "type": "adjust_temperature", + "reason": f"负面反馈率 {negative_rate:.1%},建议降低 temperature", + "action": "temperature -= 0.1", + }) + if signal_dist.get("retry_command", 0) / total > 0.2 if total > 0 else False: + strategy_advice.append({ + "type": "enhance_prompt", + "reason": "用户频繁要求重试,输出可能不够精准", + "action": "在 system prompt 中增加更具体的输出要求", + }) + if signal_dist.get("manual_edit", 0) / total > 0.2 if total > 0 else False: + strategy_advice.append({ + "type": "suggest_review", + "reason": "输出频繁被手动修改,建议开启 self_review", + "action": "开启输出质量自检", + }) + + # 推荐有问题的 Agent + problematic_agents = [ + {"agent": name, "negative_rate": round(data["negative"] / data["total"], 2)} + for name, data in by_agent.items() + if data["total"] >= 3 and data["negative"] / data["total"] > 0.3 + ] + + return { + "total_feedback": total, + "period_days": days, + "signal_distribution": dict(signal_dist), + "overall_negative_rate": round(negative_rate, 3), + "problematic_agents": problematic_agents, + "strategy_advice": strategy_advice, + } + + except Exception as e: + logger.error("分析反馈模式失败: %s", e) + return {"error": str(e)} + finally: + if db: + try: + db.close() + except Exception: + pass + + def generate_negative_examples(self, agent_name: str, limit: int = 5) -> List[Dict[str, Any]]: + """为 Agent 生成反例(用于更新 system prompt)。""" + db: Optional[Session] = None + try: + db = SessionLocal() + + records = ( + db.query(FeedbackRecord) + .filter( + FeedbackRecord.agent_name == agent_name, + FeedbackRecord.original_output.isnot(None), + FeedbackRecord.user_correction.isnot(None), + FeedbackRecord.signal_type.in_(["thumbs_down", "manual_edit"]), + ) + .order_by(desc(FeedbackRecord.created_at)) + .limit(limit) + .all() + ) + + examples = [] + for r in records: + examples.append({ + "original": (r.original_output or "")[:500], + "corrected": (r.user_correction or "")[:500], + "signal": r.signal_type, + }) + + return examples + + except Exception as e: + logger.error("生成反例失败: %s", e) + return [] + finally: + if db: + try: + db.close() + except Exception: + pass + + +feedback_learner = FeedbackLearner() diff --git a/backend/app/services/fingerprint_engine.py b/backend/app/services/fingerprint_engine.py new file mode 100644 index 0000000..03c20b4 --- /dev/null +++ b/backend/app/services/fingerprint_engine.py @@ -0,0 +1,221 @@ +""" +用户行为指纹引擎 — 从行为日志中学习偏好权重和决策规则 +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional +from collections import Counter + +from sqlalchemy.orm import Session +from sqlalchemy import func + +from app.core.database import SessionLocal +from app.models.user_behavior import UserBehaviorLog, BehaviorCategory +from app.models.user_fingerprint import UserFingerprint + +logger = logging.getLogger(__name__) + +DEFAULT_WEIGHTS = { + "code_review": {"security": 0.3, "performance": 0.25, "readability": 0.25, "style": 0.2}, + "document": {"structure": 0.3, "clarity": 0.3, "completeness": 0.25, "style": 0.15}, + "decision": {"data_driven": 0.4, "risk_averse": 0.3, "speed": 0.3}, + "email": {"formality": 0.3, "conciseness": 0.35, "responsiveness": 0.35}, +} + + +class FingerprintEngine: + """从行为日志学习用户行为指纹""" + + def compute_fingerprint(self, user_id: str) -> Optional[Dict[str, Any]]: + """计算用户行为指纹。""" + db: Optional[Session] = None + try: + db = SessionLocal() + + total = db.query(func.count(UserBehaviorLog.id)).filter( + UserBehaviorLog.user_id == user_id + ).scalar() or 0 + + if total < 10: + logger.info("用户 %s 行为数据不足 (%d 条)", user_id, total) + return None + + by_category = {} + for cat in BehaviorCategory: + count = db.query(func.count(UserBehaviorLog.id)).filter( + UserBehaviorLog.user_id == user_id, + UserBehaviorLog.category == cat.value, + ).scalar() or 0 + by_category[cat.value] = count + + preference_weights = self._extract_preferences(db, user_id) + decision_rules = self._extract_rules(db, user_id) + + behaviors = ( + db.query(UserBehaviorLog) + .filter(UserBehaviorLog.user_id == user_id) + .order_by(UserBehaviorLog.created_at.desc()) + .limit(100) + .all() + ) + durations = [] + for b in behaviors: + if b.result and isinstance(b.result, dict): + d = b.result.get("duration_ms") + if d: + durations.append(d) + avg_response = int(sum(durations) / len(durations)) if durations else None + + return { + "user_id": user_id, + "preference_weights": preference_weights, + "decision_rules": decision_rules, + "total_behaviors": total, + "behaviors_by_category": by_category, + "avg_response_time_ms": avg_response, + } + + except Exception as e: + logger.error("指纹计算失败: %s", e) + return None + finally: + if db: + try: + db.close() + except Exception: + pass + + def _extract_preferences(self, db: Session, user_id: str) -> Dict[str, Any]: + weights = dict(DEFAULT_WEIGHTS) + recent = ( + db.query(UserBehaviorLog) + .filter(UserBehaviorLog.user_id == user_id) + .order_by(UserBehaviorLog.created_at.desc()) + .limit(200) + .all() + ) + for b in recent: + if b.result and isinstance(b.result, dict): + priority = b.result.get("priority") + if priority and isinstance(priority, dict): + cat = b.category + if cat in weights: + for k, v in priority.items(): + if k in weights[cat]: + weights[cat][k] = weights[cat][k] * 0.9 + v * 0.1 + return weights + + def _extract_rules(self, db: Session, user_id: str) -> List[Dict[str, Any]]: + rules = [] + recent = ( + db.query(UserBehaviorLog) + .filter(UserBehaviorLog.user_id == user_id) + .order_by(UserBehaviorLog.created_at.desc()) + .limit(300) + .all() + ) + action_counts = Counter(b.action for b in recent) + for action, count in action_counts.most_common(20): + if count >= 5: + actions_of_type = [b for b in recent if b.action == action] + status_codes = Counter() + for b in actions_of_type: + if b.result and isinstance(b.result, dict): + sc = b.result.get("status_code") + if sc: + status_codes[sc] += 1 + if status_codes and status_codes.most_common(1)[0][1] / count > 0.8: + rules.append({ + "action": action, + "expected_outcome": status_codes.most_common(1)[0][0], + "confidence": round(status_codes.most_common(1)[0][1] / count, 2), + "sample_count": count, + }) + return rules[:50] + + def save_or_update(self, user_id: str, fingerprint: Dict[str, Any]) -> Optional[str]: + db: Optional[Session] = None + try: + db = SessionLocal() + existing = db.query(UserFingerprint).filter( + UserFingerprint.user_id == user_id + ).first() + + if existing: + existing.preference_weights = fingerprint["preference_weights"] + existing.decision_rules = fingerprint["decision_rules"] + existing.total_behaviors = fingerprint["total_behaviors"] + existing.behaviors_by_category = fingerprint["behaviors_by_category"] + existing.avg_response_time_ms = fingerprint.get("avg_response_time_ms") + existing.model_version = str(float(existing.model_version or "1.0") + 0.1) + existing.last_trained_at = func.now() + fid = str(existing.id) + else: + entry = UserFingerprint( + user_id=user_id, + preference_weights=fingerprint["preference_weights"], + decision_rules=fingerprint["decision_rules"], + total_behaviors=fingerprint["total_behaviors"], + behaviors_by_category=fingerprint["behaviors_by_category"], + avg_response_time_ms=fingerprint.get("avg_response_time_ms"), + ) + db.add(entry) + db.flush() + fid = str(entry.id) + + db.commit() + return fid + except Exception as e: + logger.error("保存指纹失败: %s", e) + if db: + try: + db.rollback() + except Exception: + pass + return None + finally: + if db: + try: + db.close() + except Exception: + pass + + def train(self, user_id: str) -> Optional[Dict[str, Any]]: + fingerprint = self.compute_fingerprint(user_id) + if fingerprint: + self.save_or_update(user_id, fingerprint) + logger.info("用户 %s 指纹训练完成: %d 条行为", user_id, fingerprint["total_behaviors"]) + return fingerprint + + def get_fingerprint(self, user_id: str) -> Optional[Dict[str, Any]]: + db: Optional[Session] = None + try: + db = SessionLocal() + fp = db.query(UserFingerprint).filter( + UserFingerprint.user_id == user_id + ).first() + if not fp: + return None + return { + "user_id": fp.user_id, + "preference_weights": fp.preference_weights, + "decision_rules": fp.decision_rules, + "total_behaviors": fp.total_behaviors, + "behaviors_by_category": fp.behaviors_by_category, + "avg_response_time_ms": fp.avg_response_time_ms, + "model_version": fp.model_version, + "last_trained_at": fp.last_trained_at.isoformat() if fp.last_trained_at else None, + } + except Exception as e: + logger.error("获取指纹失败: %s", e) + return None + finally: + if db: + try: + db.close() + except Exception: + pass + + +fingerprint_engine = FingerprintEngine() diff --git a/backend/app/services/knowledge_extractor.py b/backend/app/services/knowledge_extractor.py new file mode 100644 index 0000000..ce50886 --- /dev/null +++ b/backend/app/services/knowledge_extractor.py @@ -0,0 +1,229 @@ +""" +知识提取器 - 从 Agent 执行日志中用 LLM 提取可复用知识 +""" +from __future__ import annotations + +import json +import logging +from typing import Any, Dict, List, Optional + +from sqlalchemy import desc + +from app.core.database import SessionLocal + +logger = logging.getLogger(__name__) + +KNOWLEDGE_EXTRACTION_PROMPT = """你是一个知识工程专家。请从以下 Agent 执行记录中提取可复用的经验知识。 + +执行输入: {input_text} +执行输出: {output_text} +是否成功: {success} +工具调用链: {tool_chain} +迭代次数: {iterations_used} +工具调用总次数: {tool_calls_made} + +请提取为以下 JSON 格式(只输出 JSON,不要其他文本): +{{ + "title": "一句话标题(如:处理MySQL死锁的重试策略)", + "category": "bug_fix / best_practice / workaround / optimization / insight", + "tags": ["tag1", "tag2"], + "situation": "什么场景下适用", + "solution": "具体解决方案或操作步骤", + "caveats": "注意事项或已知限制", + "confidence": 0.0-1.0 +}} + +如果这条执行记录没有值得沉淀的知识,返回: +{{"skip": true, "reason": "原因"}} +""" + + +class KnowledgeExtractor: + """从执行日志中提取知识(使用 LLM)""" + + def __init__(self, llm_model: str = "deepseek-v4-flash"): + self.llm_model = llm_model + + def _sync_llm_call(self, prompt: str) -> str: + """同步调用 LLM。""" + from app.agent_runtime.core import _LLMClient + from app.agent_runtime.schemas import AgentLLMConfig + import asyncio as aio + + client = _LLMClient(AgentLLMConfig( + provider="deepseek", + model=self.llm_model, + temperature=0.3, + max_iterations=1, + )) + + result = aio.run(client.chat( + messages=[{"role": "user", "content": prompt}], + tools=None, + iteration=1, + )) + content = result.get("content", "") if isinstance(result, dict) else str(result) + return content + + def extract_from_execution(self, log: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """从单条执行日志提取知识(同步)。""" + input_text = log.get("input_text", "") or "" + output_text = log.get("output_text", "") or "" + success = log.get("success", False) + tool_chain = log.get("tool_chain") or [] + tool_calls_made = log.get("tool_calls_made", 0) + iterations_used = log.get("iterations_used", 0) + + if len(input_text) < 20 or len(output_text) < 50: + return None + if not success and not tool_chain: + return None + + tool_chain_str = json.dumps(tool_chain[:5], ensure_ascii=False) if tool_chain else "无" + + prompt = KNOWLEDGE_EXTRACTION_PROMPT.format( + input_text=input_text[:2000], + output_text=output_text[:2000], + success="是" if success else "否", + tool_chain=tool_chain_str, + iterations_used=iterations_used, + tool_calls_made=tool_calls_made, + ) + + try: + llm_output = self._sync_llm_call(prompt) + except Exception as e: + logger.warning("知识提取 LLM 调用失败: %s", e) + return None + + try: + json_start = llm_output.find("{") + json_end = llm_output.rfind("}") + 1 + if json_start >= 0 and json_end > json_start: + data = json.loads(llm_output[json_start:json_end]) + else: + return None + except json.JSONDecodeError: + logger.warning("知识提取 JSON 解析失败: %s", llm_output[:200]) + return None + + if data.get("skip"): + return None + + confidence = float(data.get("confidence", 0.5)) + if confidence < 0.3: + return None + + return { + "title": data.get("title", "未命名知识"), + "category": data.get("category", "insight"), + "tags": data.get("tags", []), + "situation": data.get("situation", ""), + "solution": data.get("solution", ""), + "caveats": data.get("caveats", ""), + "confidence": confidence, + } + + def run_extraction_pipeline(self, limit: int = 10) -> int: + """运行提取管道:从未处理的执行日志中提取知识。""" + from app.models.agent_execution_log import AgentExecutionLog + from app.models.knowledge_entry import KnowledgeEntry + + db = None + extracted = 0 + + try: + db = SessionLocal() + logs = ( + db.query(AgentExecutionLog) + .filter( + AgentExecutionLog.success == True, + AgentExecutionLog.knowledge_extracted == False, + AgentExecutionLog.output_text.isnot(None), + ) + .order_by(desc(AgentExecutionLog.created_at)) + .limit(limit) + .all() + ) + + if not logs: + logger.info("知识提取管道: 没有待处理的新日志") + return 0 + + logger.info("知识提取管道: 找到 %d 条待处理日志", len(logs)) + + for log_entry in logs: + log_data = { + "input_text": log_entry.input_text, + "output_text": log_entry.output_text, + "success": log_entry.success, + "tool_chain": log_entry.tool_chain, + "iterations_used": log_entry.iterations_used, + "tool_calls_made": log_entry.tool_calls_made, + } + + knowledge = self.extract_from_execution(log_data) + if not knowledge: + log_entry.knowledge_extracted = True + db.commit() + continue + + existing = ( + db.query(KnowledgeEntry) + .filter(KnowledgeEntry.title == knowledge["title"]) + .first() + ) + if existing: + log_entry.knowledge_extracted = True + db.commit() + continue + + embedding_text = ( + knowledge["title"] + "\n" + + knowledge["situation"] + "\n" + + knowledge["solution"] + "\n" + + knowledge["caveats"] + ) + + entry = KnowledgeEntry( + title=knowledge["title"], + category=knowledge["category"], + tags=knowledge["tags"], + situation=knowledge["situation"], + solution=knowledge["solution"], + caveats=knowledge["caveats"], + source_execution_ids=[str(log_entry.id)], + source_agent_name=log_entry.agent_name, + source_model=log_entry.model, + embedding_text=embedding_text, + extracted_by="llm_auto", + confidence=knowledge["confidence"], + ) + db.add(entry) + log_entry.knowledge_extracted = True + db.commit() + extracted += 1 + logger.info( + "知识提取: %s -> %s (conf=%.2f)", + log_entry.agent_name, knowledge["title"], knowledge["confidence"], + ) + + return extracted + + except Exception as e: + logger.error("知识提取管道异常: %s", e) + if db: + try: + db.rollback() + except Exception: + pass + return 0 + finally: + if db: + try: + db.close() + except Exception: + pass + + +knowledge_extractor = KnowledgeExtractor() diff --git a/backend/app/services/knowledge_retriever.py b/backend/app/services/knowledge_retriever.py new file mode 100644 index 0000000..8dd9803 --- /dev/null +++ b/backend/app/services/knowledge_retriever.py @@ -0,0 +1,115 @@ +""" +知识检索服务 — 从知识库中语义检索相关经验,注入到 Agent 执行上下文 +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional + +from sqlalchemy.orm import Session +from sqlalchemy import desc, func + +from app.core.database import SessionLocal + +logger = logging.getLogger(__name__) + + +class KnowledgeRetriever: + """知识检索器 — 执行前从知识库检索相关经验""" + + def __init__(self, top_k: int = 3, min_score: float = 0.3): + self.top_k = top_k + self.min_score = min_score + + def retrieve(self, query: str, category: Optional[str] = None) -> List[Dict[str, Any]]: + """基于关键词+标签检索相关知识条目(后续可升级为 embedding 语义检索)。""" + from app.models.knowledge_entry import KnowledgeEntry + + db: Optional[Session] = None + try: + db = SessionLocal() + q = db.query(KnowledgeEntry).filter( + KnowledgeEntry.is_active == True, + ) + + if category: + q = q.filter(KnowledgeEntry.category == category) + + # 关键词匹配:在 title / situation / solution / tags 中搜索 + keywords = [w for w in query[:100].split() if len(w) >= 2] + if keywords: + conditions = [] + for kw in keywords[:10]: + like_pat = f"%{kw}%" + conditions.append(KnowledgeEntry.title.like(like_pat)) + conditions.append(KnowledgeEntry.situation.like(like_pat)) + conditions.append(KnowledgeEntry.solution.like(like_pat)) + from sqlalchemy import or_ + q = q.filter(or_(*conditions)) + + entries = ( + q.order_by(desc(KnowledgeEntry.confidence), desc(KnowledgeEntry.retrieval_count)) + .limit(self.top_k) + .all() + ) + + results = [] + for e in entries: + # 更新检索计数 + e.retrieval_count = (e.retrieval_count or 0) + 1 + results.append({ + "id": str(e.id), + "title": e.title, + "category": e.category, + "tags": e.tags or [], + "situation": e.situation, + "solution": e.solution, + "caveats": e.caveats, + "confidence": e.confidence, + }) + + db.commit() + return results + + except Exception as e: + logger.warning("知识检索失败: %s", e) + if db: + try: + db.rollback() + except Exception: + pass + return [] + finally: + if db: + try: + db.close() + except Exception: + pass + + def format_for_prompt(self, entries: List[Dict[str, Any]]) -> str: + """将检索到的知识条目格式化为 Prompt 注入文本。""" + if not entries: + return "" + + parts = ["\n## 相关知识库经验(请参考以下经验来更好地完成任务)\n"] + for i, e in enumerate(entries, 1): + parts.append(f"### {i}. {e['title']} (置信度: {e['confidence']:.0%})") + if e.get("situation"): + parts.append(f"**适用场景**: {e['situation']}") + if e.get("solution"): + parts.append(f"**方案**: {e['solution']}") + if e.get("caveats"): + parts.append(f"**注意**: {e['caveats']}") + parts.append("") + return "\n".join(parts) + + def inject_knowledge(self, system_prompt: str, user_input: str) -> str: + """检索相关知识并注入到 system prompt 中。""" + entries = self.retrieve(user_input) + knowledge_text = self.format_for_prompt(entries) + if knowledge_text: + return system_prompt + knowledge_text + return system_prompt + + +knowledge_retriever = KnowledgeRetriever() diff --git a/backend/app/services/main_agent_service.py b/backend/app/services/main_agent_service.py index 2a2c0ac..e05bd37 100644 --- a/backend/app/services/main_agent_service.py +++ b/backend/app/services/main_agent_service.py @@ -283,23 +283,36 @@ class MainAgentService: """以 Agent 模式执行任务""" agent_id = task.assigned_agent_id agent = None + agent_node_data = {} if agent_id: agent = self.db.query(Agent).filter(Agent.id == agent_id).first() + if agent: + agent_node_data = _extract_agent_node_config(agent) config = task.task_config or {} + # 合并配置优先级:task_config > agent_node_data > 默认值 + def _cfg(key, default): + return config.get(key) or agent_node_data.get(key) or default + + tools_whitelist = task_config_tool_whitelist(config) + if not tools_whitelist: + tools_whitelist = agent_node_data.get("tools") or [] + logger.info(f"DEBUG tools_whitelist={tools_whitelist!r} for task {task.id}") + # 构建 Agent 配置 agent_config = AgentConfig( name=f"task_{task.id[:8]}", - system_prompt=config.get("system_prompt", + system_prompt=_cfg("system_prompt", f"你需要完成以下任务:\n{task.title}\n\n{task.description or ''}\n\n请使用可用工具完成任务。"), llm=AgentLLMConfig( - provider=config.get("provider", "deepseek"), - model=config.get("model", "deepseek-chat"), - temperature=config.get("temperature", 0.7), - max_iterations=config.get("max_iterations", 15), + provider=_cfg("provider", "deepseek"), + model=_cfg("model", "deepseek-chat"), + temperature=float(_cfg("temperature", 0.7)), + max_iterations=int(_cfg("max_iterations", 5)), + request_timeout=float(_cfg("request_timeout", 60.0)), ), - tools=AgentToolConfig(include_tools=task_config_tool_whitelist(config)), + tools=AgentToolConfig(include_tools=tools_whitelist), ) runtime = AgentRuntime(agent_config) @@ -500,12 +513,192 @@ class MainAgentService: "results": results, } + # ──────────── 用户交互 ──────────── -def task_config_tool_whitelist(config: Dict) -> Optional[List[str]]: - """从 task_config 中提取工具白名单,空则返回 None(全部工具可用)""" + async def interact_with_goal( + self, goal_id: str, user_message: str, attachments: Optional[List[Dict[str, Any]]] = None + ) -> Dict[str, Any]: + """ + 用户与目标交互:发送消息给 Main Agent,Agent 根据反馈重新调整任务。 + + 流程: + 1. 收集所有任务及其结果作为对话上下文 + 2. 调用 LLM 分析用户反馈,决定哪些任务需要重新执行 + 3. 将受影响的任务标记为 pending + 4. 返回 Agent 的回复 + """ + goal = goal_service.get_goal(self.db, goal_id) + tasks = goal_service.list_tasks(self.db, goal_id=goal_id, limit=200) + + # 生成附件信息 + attachment_info = "" + if attachments: + lines = ["\n用户上传了以下附件:"] + for a in attachments: + name = a.get("filename", "unknown") + path = a.get("relative_path", "") + content_type = a.get("content_type", "") + lines.append(f" - {name} (路径: {path}, 类型: {content_type})") + lines.append("你可以使用 file_read 工具读取这些文件的内容。") + attachment_info = "\n".join(lines) + + # 收集上下文 + task_context_lines = [] + for t in tasks: + status_label = t.status + result_summary = "" + if t.result: + if isinstance(t.result, dict): + content = t.result.get("content", "") or "" + # 截取前300字符 + result_summary = content[:300] + elif isinstance(t.result, str): + result_summary = t.result[:300] + err = f" [错误: {t.error_message[:100]}]" if t.error_message else "" + task_context_lines.append( + f"- [{status_label}] {t.title} (P{t.priority}){err}\n" + f" 结果摘要: {result_summary or '(无)'}" + ) + task_context = "\n".join(task_context_lines) + + # 获取 Main Agent 信息 + main_agent = None + main_agent_config = {} + if goal.main_agent_id: + main_agent = self.db.query(Agent).filter(Agent.id == goal.main_agent_id).first() + if main_agent: + main_agent_config = _extract_agent_node_config(main_agent) + + system_prompt = main_agent_config.get("system_prompt", MAIN_AGENT_SYSTEM_PROMPT) + model = main_agent_config.get("model") or self._get_llm_model() + provider = main_agent_config.get("provider") or "deepseek" + + messages = [ + {"role": "system", "content": f"""{system_prompt} + +当前你正在管理一个目标,用户刚刚给了你反馈。你需要: +1. 理解用户的反馈内容 +2. 检查现有任务是否需要调整(重新执行、修改描述、新增等) +3. **仅输出 JSON**,格式如下: +{{ + "reply": "对用户反馈的回复(1-3句话,确认你理解并说明接下来的计划)", + "actions": [ + {{"task_title": "任务标题", "action": "retry", "reason": "为什么需要重新执行"}}, + {{"task_title": "任务标题", "action": "update", "new_description": "更新的描述"}} + ] +}} + +如果用户的反馈不会改变任何现有任务,actions 可以是空数组。 +如果用户回答了之前任务中的问题,应该对回答了问题的任务执行 retry 操作。"""}, + {"role": "user", "content": f"""目标:{goal.title} +描述:{goal.description or ''} + +当前任务状态: +{task_context} + +用户反馈: +{user_message}{attachment_info} + +请分析并输出 JSON。"""}, + ] + + response = await self._call_llm(messages) + content = response.get("content", "") + + # 解析 LLM 输出 + try: + json_str = content + if "```json" in json_str: + json_str = json_str.split("```json")[1].split("```")[0] + elif "```" in json_str: + json_str = json_str.split("```")[1].split("```")[0] + plan = json.loads(json_str) + except (json.JSONDecodeError, IndexError, KeyError) as e: + logger.error(f"Failed to parse LLM interaction output: {e}\nRaw: {content[:500]}") + # 降级:直接返回 LLM 原文 + return { + "reply": content[:500] if content else "收到你的反馈,我会重新调整任务。", + "actions": [], + "goal_id": goal_id, + } + + reply = plan.get("reply", "收到你的反馈。") + actions = plan.get("actions", []) + + # 执行 actions + for action in actions: + action_type = action.get("action", "") + task_title = action.get("task_title", "") + # 按标题匹配任务 + for t in tasks: + if t.title.strip() == task_title.strip(): + if action_type == "retry": + # 重置为 pending 让 autonomy tick 重新执行 + goal_service.update_task( + self.db, t.id, + status="pending", + error_message=None, + result=None, + ) + logger.info(f"Task {t.id} ({t.title}) reset to pending for retry") + elif action_type == "update": + new_desc = action.get("new_description", "") + if new_desc: + goal_service.update_task( + self.db, t.id, + description=new_desc, + ) + # 也重置为 pending + goal_service.update_task( + self.db, t.id, + status="pending", + error_message=None, + ) + logger.info(f"Task {t.id} ({t.title}) updated and reset to pending") + break + + self.db.commit() + + # 如果 goal 是 completed 状态,重新激活 + if goal.status in ("completed", "paused", "failed"): + goal_service.update_goal(self.db, goal_id, status="active") + self.db.commit() + + # 执行一轮 autonomy tick 来处理刚重置的任务 + tick_result = {"status": "no_tasks"} + if goal.status == "active": + tick_result = await self.autonomy_tick(goal_id) + + return { + "reply": reply, + "actions": actions, + "actions_count": len(actions), + "tick_result": tick_result.get("status"), + "goal_id": goal_id, + "goal_status": goal_service.get_goal(self.db, goal_id).status, + } + + +def _extract_agent_node_config(agent: Agent) -> Dict[str, Any]: + """从 Agent 的 workflow_config 中提取第一个 agent/llm 节点的配置。""" + wf = agent.workflow_config or {} + nodes = wf.get("nodes", []) + for n in nodes: + if n.get("type") in ("agent", "llm"): + data = n.get("data", {}) + # 规范化字段:label/name 映射到 system_prompt + cfg = dict(data) + if not cfg.get("system_prompt"): + cfg["system_prompt"] = data.get("label") or data.get("name") or "" + return cfg + return {} + + +def task_config_tool_whitelist(config: Dict) -> List[str]: + """从 task_config 中提取工具白名单,空则返回空列表(全部工具可用)""" tools = config.get("tools") or config.get("include_tools") if not tools: - return None + return [] if isinstance(tools, list): return tools - return None + return [] diff --git a/backend/app/services/optimization_engine.py b/backend/app/services/optimization_engine.py new file mode 100644 index 0000000..02ff55e --- /dev/null +++ b/backend/app/services/optimization_engine.py @@ -0,0 +1,168 @@ +""" +工作流自动优化引擎 — 根据瓶颈检测结果自动生成优化方案 +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional + +from app.services.bottleneck_detector import bottleneck_detector + +logger = logging.getLogger(__name__) + + +class OptimizationEngine: + """工作流自动优化引擎 — 生成 DAG 优化方案""" + + def analyze_and_optimize(self, hours: int = 24) -> Dict[str, Any]: + """运行完整分析并生成优化方案。""" + analysis = bottleneck_detector.run_full_analysis(hours=hours) + bottlenecks = analysis.get("bottlenecks", []) + optimizations = self.generate_optimizations(bottlenecks) + dag_changes = self.generate_dag_changes(optimizations) + + return { + "period_hours": hours, + "summary": analysis, + "optimizations": optimizations, + "dag_changes": dag_changes, + "requires_approval": len(optimizations) > 0, + } + + def generate_optimizations(self, bottlenecks: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """根据瓶颈生成具体优化方案。""" + optimizations = [] + + for b in bottlenecks: + severity = b.get("severity", "low") + if severity == "low": + continue + + opt = { + "node_type": b["node_type"], + "severity": severity, + "current_metrics": { + "p95_ms": b.get("p95_ms"), + "error_rate": b.get("error_rate"), + "p50_ms": b.get("p50_ms"), + }, + "changes": [], + } + + if b.get("is_bottleneck"): + opt["changes"].append({ + "type": "parallelize", + "description": f"将 {b['node_type']} 拆分为并行子节点", + "estimated_improvement": "延迟降低 50-70%", + "implementation": { + "action": "replace_node", + "new_structure": "parallel_gateway", + "sub_nodes": self._suggest_split(b["node_type"]), + }, + }) + + if b.get("is_problematic"): + opt["changes"].append({ + "type": "add_validation", + "description": f"添加前置校验节点,失败率 {b['error_rate']:.1%}", + "estimated_improvement": "失败率降至 5% 以下", + "implementation": { + "action": "insert_before", + "new_node": { + "type": "validate", + "name": f"PreValidate_{b['node_type']}", + "config": {"required_fields": [], "timeout_ms": 5000}, + }, + }, + }) + opt["changes"].append({ + "type": "add_retry", + "description": "添加重试逻辑:最多 3 次,指数退避", + "estimated_improvement": "间歇性失败自动恢复", + "implementation": { + "action": "update_config", + "config": {"retry_count": 3, "retry_delay_ms": 1000, "backoff": "exponential"}, + }, + }) + + if b.get("is_inefficient"): + opt["changes"].append({ + "type": "add_cache", + "description": f"添加结果缓存,TTL: 300s", + "estimated_improvement": "重复调用减少 30-60%", + "implementation": { + "action": "insert_before", + "new_node": { + "type": "cache_check", + "name": f"Cache_{b['node_type']}", + "config": {"ttl_seconds": 300, "key_fields": []}, + }, + }, + }) + + if opt["changes"]: + optimizations.append(opt) + + return optimizations + + def _suggest_split(self, node_type: str) -> List[Dict[str, Any]]: + """根据节点类型建议拆分方案。""" + suggestions = { + "llm_call": [ + {"name": "PromptPreprocess", "type": "transform"}, + {"name": "LLMCall_Fast", "type": "llm_call", "config": {"model": "fast"}}, + {"name": "LLMCall_Accurate", "type": "llm_call", "config": {"model": "accurate"}}, + {"name": "ResultMerge", "type": "merge"}, + ], + "api_request": [ + {"name": "RequestPrepare", "type": "transform"}, + {"name": "APICall_Core", "type": "api_request"}, + {"name": "ResponseParse", "type": "transform"}, + ], + "data_query": [ + {"name": "QueryBuilder", "type": "transform"}, + {"name": "DBQuery", "type": "data_query"}, + {"name": "ResultFormat", "type": "transform"}, + ], + "code_execution": [ + {"name": "SandboxSetup", "type": "setup"}, + {"name": "CodeRun", "type": "code_execution"}, + {"name": "OutputParse", "type": "transform"}, + ], + } + return suggestions.get(node_type, [ + {"name": "PreProcess", "type": "transform"}, + {"name": "CoreExecute", "type": "execute"}, + {"name": "PostProcess", "type": "transform"}, + ]) + + def generate_dag_changes(self, optimizations: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """将优化方案转换为 DAG 变更列表(可用于 diff 展示)。""" + changes = [] + for opt in optimizations: + for change in opt.get("changes", []): + impl = change.get("implementation", {}) + changes.append({ + "node_type": opt["node_type"], + "severity": opt["severity"], + "action": impl.get("action"), + "description": change["description"], + "estimated_improvement": change.get("estimated_improvement"), + "detail": impl, + }) + return changes + + def apply_optimization(self, workflow_id: str, optimization_id: str, + approved_changes: List[str]) -> Dict[str, Any]: + """应用用户确认的优化变更(创建新版本)。""" + # 这里与 workflow_version 系统集成,创建优化后的新版本 + # 当前返回框架结果,实际集成在后续完成 + return { + "workflow_id": workflow_id, + "status": "pending", + "message": f"优化方案已记录,{len(approved_changes)} 项变更待应用", + "applied_changes": approved_changes, + } + + +optimization_engine = OptimizationEngine() diff --git a/backend/app/services/requirement_estimator.py b/backend/app/services/requirement_estimator.py new file mode 100644 index 0000000..1d94d16 --- /dev/null +++ b/backend/app/services/requirement_estimator.py @@ -0,0 +1,351 @@ +""" +需求评估代理 — 数字分身从历史数据估算需求工时,提供分阶段方案和风险评估 +""" +from __future__ import annotations + +import logging +import re +from typing import Any, Dict, List, Optional + +from sqlalchemy.orm import Session + +from app.core.database import SessionLocal +from app.models.execution import Execution +from app.models.task import Task +from app.services.fingerprint_engine import fingerprint_engine +from app.services.behavior_collector import behavior_collector + +logger = logging.getLogger(__name__) + + +class RequirementEstimator: + """需求评估代理 — 基于历史数据估算工时与风险""" + + def estimate(self, user_id: str, title: str, description: str = "", + modules: Optional[List[str]] = None) -> Dict[str, Any]: + """评估需求的工作量和时间。 + + Args: + user_id: 用户ID + title: 需求标题 + description: 需求描述 + modules: 涉及的模块/组件列表 + """ + modules = modules or self._extract_modules(description) + + # 1. 历史同类需求分析 + similar_tasks = self._find_similar_tasks(title, description) + + # 2. 复杂度评估 + complexity = self._assess_complexity(title, description, modules) + + # 3. 时间估算 + time_estimate = self._estimate_time(complexity, similar_tasks, modules) + + # 4. 风险识别 + risks = self._identify_risks(title, description, modules) + + # 5. 分阶段建议 + phases = self._suggest_phases(title, description, modules, time_estimate) + + # 6. 方案建议 + suggestions = self._suggest_approach(complexity, similar_tasks, risks) + + result = { + "title": title, + "complexity": complexity, + "time_estimate": time_estimate, + "modules": modules, + "similar_tasks": similar_tasks, + "risks": risks, + "phases": phases, + "suggestions": suggestions, + } + + # 记录行为 + behavior_collector.log_fire_and_forget( + user_id=user_id, + category="decision", + action="estimate_requirement", + context={"title": title, "modules": modules}, + result={"complexity": complexity["level"], "estimated_hours": time_estimate["best_case_hours"]}, + ) + + return result + + def _extract_modules(self, description: str) -> List[str]: + """从描述中提取可能涉及的模块名。""" + modules = [] + patterns = [ + r'(?:模块|组件|服务)[::]\s*(\S+)', + r'(\w+(?:模块|组件|服务|API|数据库|前端|后端))', + r'(?:涉及|影响|修改)[::]?\s*([^,,\n]+)', + ] + for pattern in patterns: + matches = re.findall(pattern, description) + modules.extend(matches) + # 去重 + seen = set() + result = [] + for m in modules: + if m not in seen: + seen.add(m) + result.append(m) + return result[:10] + + def _find_similar_tasks(self, title: str, description: str) -> List[Dict[str, Any]]: + """查找历史同类需求(基于关键词匹配)。""" + db: Optional[Session] = None + try: + db = SessionLocal() + + # 提取关键词 + keywords = set(re.findall(r'[\w]{2,}', title + description)) + # 过滤常用词 + stopwords = {'的', '和', '是', '在', '了', '不', '有', '与', '对', '这', '那'} + keywords = keywords - stopwords + + if not keywords: + return [] + + tasks = db.query(Task).filter( + Task.title.isnot(None), + Task.actual_hours.isnot(None), + ).order_by(Task.created_at.desc()).limit(200).all() + + similar = [] + for task in tasks: + task_text = (task.title or "") + (task.description or "") + score = sum(1 for kw in keywords if kw in task_text) + if score >= 2: + similar.append({ + "task_id": str(task.id) if task.id else None, + "title": task.title[:100] if task.title else "", + "actual_hours": float(task.actual_hours) if task.actual_hours else None, + "similarity_score": score, + }) + + similar.sort(key=lambda x: x.get("similarity_score", 0), reverse=True) + return similar[:5] + + except Exception as e: + logger.error("查找同类需求失败: %s", e) + return [] + finally: + if db: + try: + db.close() + except Exception: + pass + + def _assess_complexity(self, title: str, description: str, + modules: List[str]) -> Dict[str, Any]: + """评估需求复杂度。""" + text = title + " " + description + factors = { + "module_count": min(len(modules) if modules else 1, 10), + "has_migration": 1 if re.search(r'迁移|migration|schema.*change', text, re.I) else 0, + "has_payment": 1 if re.search(r'支付|付款|资金|金额', text) else 0, + "has_permission": 1 if re.search(r'权限|permission|auth|role', text, re.I) else 0, + "has_external_api": 1 if re.search(r'第三方|外部.*API|webhook|回调', text) else 0, + "has_report": 1 if re.search(r'报表|统计|chart|dashboard|导出', text) else 0, + "has_real_time": 1 if re.search(r'实时|websocket|push|消息推送', text) else 0, + "has_batch": 1 if re.search(r'批量|import|export|导入|导出', text) else 0, + } + + score = ( + factors["module_count"] * 3 + + factors["has_migration"] * 10 + + factors["has_payment"] * 8 + + factors["has_permission"] * 5 + + factors["has_external_api"] * 5 + + factors["has_report"] * 3 + + factors["has_real_time"] * 4 + + factors["has_batch"] * 3 + ) + + if score <= 15: + level = "simple" + label = "简单" + elif score <= 30: + level = "medium" + label = "中等" + elif score <= 50: + level = "complex" + label = "复杂" + else: + level = "very_complex" + label = "非常复杂" + + return { + "level": level, + "label": label, + "score": score, + "factors": factors, + } + + def _estimate_time(self, complexity: Dict[str, Any], + similar_tasks: List[Dict[str, Any]], + modules: List[str]) -> Dict[str, Any]: + """估算开发时间。""" + level = complexity["level"] + module_count = len(modules) if modules else 1 + + # 基准时间(小时/模块) + base_hours = { + "simple": 8, + "medium": 16, + "complex": 32, + "very_complex": 56, + }.get(level, 16) + + # 历史数据调整 + if similar_tasks: + hist_hours = [t["actual_hours"] for t in similar_tasks if t.get("actual_hours")] + if hist_hours: + hist_avg = sum(hist_hours) / len(hist_hours) + base_hours = base_hours * 0.4 + hist_avg * 0.6 + + best_case = round(base_hours * module_count * 0.8, 1) + expected = round(base_hours * module_count, 1) + worst_case = round(base_hours * module_count * 1.8, 1) + + # 转换为天(8小时/天) + return { + "best_case_hours": best_case, + "expected_hours": expected, + "worst_case_hours": worst_case, + "best_case_days": round(best_case / 8, 1), + "expected_days": round(expected / 8, 1), + "worst_case_days": round(worst_case / 8, 1), + "confidence": "high" if similar_tasks and len(similar_tasks) >= 3 else "medium", + "based_on": f"{len(similar_tasks)} 个历史同类需求" if similar_tasks else "行业基准估算", + } + + def _identify_risks(self, title: str, description: str, + modules: List[str]) -> List[Dict[str, Any]]: + """识别需求风险。""" + text = title + " " + description + risks = [] + + risk_checks = [ + ("high", r'数据.*迁移|schema.*变更|数据库.*改', "数据库结构变更"), + ("high", r'支付|交易|退款|结算', "涉及支付/资金流程"), + ("high", r'删除|物理删除|DROP|TRUNCATE', "包含数据删除操作"), + ("medium", r'第三方.*API|外部.*依赖', "依赖外部API可用性"), + ("medium", r'权限.*改|角色.*调整|auth.*change', "权限体系变更"), + ("medium", r'性能|并发|QPS|高并发', "性能/并发要求"), + ("medium", r'多个.*系统|跨.*系统|集成', "多系统集成复杂度"), + ("low", r'导出|报表|dashboard|可视化', "导出/报表格式兼容"), + ("low", r'手机|移动端|H5|小程序|APP', "多端适配工作"), + ("low", r'旧.*兼容|legacy|向后兼容', "历史兼容性约束"), + ] + + for severity, pattern, label in risk_checks: + if re.search(pattern, text): + risks.append({ + "severity": severity, + "label": label, + "mitigation": self._suggest_mitigation(label), + }) + + return risks + + def _suggest_mitigation(self, risk_label: str) -> str: + """根据风险类型建议缓解措施。""" + mitigations = { + "数据库结构变更": "建议编写 migration 脚本并先在 staging 验证", + "涉及支付/资金流程": "建议增加财务团队 review 环节和完整测试用例", + "包含数据删除操作": "建议实现软删除,添加二次确认机制", + "依赖外部API可用性": "建议添加超时重试和降级方案", + "权限体系变更": "建议灰度发布,充分回归测试", + "性能/并发要求": "建议提前压测并设置监控告警", + "多系统集成复杂度": "建议明确接口契约,增加集成测试", + "导出/报表格式兼容": "建议确认所有目标格式和编码", + "多端适配工作": "建议使用响应式设计或明确优先级", + "历史兼容性约束": "建议增加兼容性测试覆盖", + } + return mitigations.get(risk_label, "建议细化方案后评估") + + def _suggest_phases(self, title: str, description: str, + modules: List[str], + time_estimate: Dict[str, Any]) -> List[Dict[str, Any]]: + """建议分阶段实施计划。""" + total_days = time_estimate["expected_days"] + + if total_days <= 3: + return [{ + "phase": 1, + "name": "一站式开发", + "tasks": [f"实现 {title}"], + "estimated_days": total_days, + "checkpoint": "功能验收", + }] + + phases = [] + if modules and len(modules) > 1: + mid = max(1, len(modules) // 2) + phases.append({ + "phase": 1, + "name": "核心功能", + "tasks": [f"实现 {m} 模块" for m in modules[:mid]], + "estimated_days": round(total_days * 0.5, 1), + "checkpoint": "核心功能可 Demo", + }) + phases.append({ + "phase": 2, + "name": "扩展功能", + "tasks": [f"实现 {m} 模块" for m in modules[mid:]], + "estimated_days": round(total_days * 0.3, 1), + "checkpoint": "完整功能验收", + }) + phases.append({ + "phase": 3, + "name": "优化 & 测试", + "tasks": ["性能优化", "完善测试", "文档整理"], + "estimated_days": round(total_days * 0.2, 1), + "checkpoint": "上线发布", + }) + else: + phases = [ + {"phase": 1, "name": "设计与准备", "tasks": ["技术方案设计", "接口定义"], "estimated_days": round(total_days * 0.2, 1), "checkpoint": "方案评审通过"}, + {"phase": 2, "name": "核心开发", "tasks": [f"实现 {title}"], "estimated_days": round(total_days * 0.5, 1), "checkpoint": "功能完成"}, + {"phase": 3, "name": "测试与上线", "tasks": ["集成测试", "性能测试", "上线部署"], "estimated_days": round(total_days * 0.3, 1), "checkpoint": "上线发布"}, + ] + + return phases + + def _suggest_approach(self, complexity: Dict[str, Any], + similar_tasks: List[Dict[str, Any]], + risks: List[Dict[str, Any]]) -> List[str]: + """生成开发策略建议。""" + suggestions = [] + + level = complexity["level"] + if level in ("complex", "very_complex"): + suggestions.append("建议先做技术方案评审,再启动开发") + suggestions.append("建议拆分为多个独立可交付的 milestone") + + if len(risks) >= 3: + suggestions.append(f"识别到 {len(risks)} 项风险,建议逐项制定应对预案") + + high_risks = [r for r in risks if r["severity"] == "high"] + if high_risks: + suggestions.append(f"存在 {len(high_risks)} 项高风险: " + f"{', '.join(r['label'] for r in high_risks)},强烈建议专项评审") + + if similar_tasks: + suggestions.append(f"参考 {len(similar_tasks)} 个历史同类需求的实际工时数据") + + if level == "simple": + suggestions.append("需求较简单,可快速迭代,不必过度设计") + + # 模块数多时建议并行 + factors = complexity.get("factors", {}) + if factors.get("module_count", 0) > 4: + suggestions.append("涉及模块较多,建议多人并行开发") + + return suggestions + + +requirement_estimator = RequirementEstimator() diff --git a/backend/app/services/shadow_executor.py b/backend/app/services/shadow_executor.py new file mode 100644 index 0000000..5a372ca --- /dev/null +++ b/backend/app/services/shadow_executor.py @@ -0,0 +1,216 @@ +""" +影子模式执行引擎 — 数字分身生成建议但不执行,对比人类决策 +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional + +from sqlalchemy.orm import Session +from sqlalchemy import func, desc + +from app.core.database import SessionLocal +from app.models.shadow_comparison import ShadowComparison +from app.services.fingerprint_engine import fingerprint_engine + +logger = logging.getLogger(__name__) + + +class ShadowExecutor: + """影子模式执行器 — 观察学习阶段的核心引擎""" + + def __init__(self): + self._unlock_thresholds = { + "code_review": 0.85, + "email": 0.90, + "document": 0.85, + "decision": 0.90, + } + + def generate_suggestion(self, user_id: str, category: str, context: Dict[str, Any]) -> Dict[str, Any]: + """基于用户指纹生成影子建议(不使用LLM,纯规则+指纹推断)。""" + fp = fingerprint_engine.get_fingerprint(user_id) + preference = (fp.get("preference_weights", {}).get(category) if fp else None) or {} + rules = fp.get("decision_rules", []) if fp else [] + + # 匹配决策规则 + matched_rules = [] + for rule in rules: + if rule.get("action", "").startswith(category): + matched_rules.append(rule) + + suggestion = { + "category": category, + "based_on": "fingerprint" if fp else "default", + "preference_applied": preference, + "matched_rules": matched_rules[:5], + "suggested_actions": self._generate_actions(category, context, preference, matched_rules), + "confidence": min(0.5 + 0.05 * len(matched_rules), 0.95), + } + + return suggestion + + def _generate_actions(self, category: str, context: Dict[str, Any], + preference: Dict[str, Any], rules: List[Dict]) -> List[Dict[str, Any]]: + """根据分类和偏好生成建议动作。""" + actions = [] + + if category == "code_review": + if preference.get("security", 0.3) > 0.3: + actions.append({"priority": "high", "action": "检查安全漏洞", "detail": "SQL注入/XSS/权限校验"}) + if preference.get("performance", 0.25) > 0.25: + actions.append({"priority": "medium", "action": "检查性能", "detail": "N+1查询/内存泄漏/大循环"}) + if preference.get("readability", 0.25) > 0.25: + actions.append({"priority": "low", "action": "检查可读性", "detail": "命名/注释/函数长度"}) + + elif category == "document": + actions.append({"priority": "medium", "action": "结构检查", "detail": "章节完整性/逻辑连贯性"}) + actions.append({"priority": "low", "action": "风格统一", "detail": "术语一致性/格式规范"}) + + elif category == "decision": + actions.append({"priority": "high", "action": "数据验证", "detail": "检查决策依据是否充分"}) + actions.append({"priority": "medium", "action": "风险评估", "detail": "识别潜在风险和副作用"}) + + elif category == "email": + actions.append({"priority": "medium", "action": "语气检查", "detail": "与收件人关系的匹配度"}) + actions.append({"priority": "low", "action": "完整性检查", "detail": "回复是否涵盖所有要点"}) + + return actions + + def compare(self, user_id: str, shadow_suggestion: Dict[str, Any], + user_decision: Dict[str, Any], user_action: str) -> Dict[str, Any]: + """对比影子建议与用户实际决策。""" + matched = 0 + diverged = 0 + suggested_actions = shadow_suggestion.get("suggested_actions", []) + + if user_action == "accept": + match_score = 1.0 + matched = len(suggested_actions) + elif user_action == "modify": + # 部分匹配 + if user_decision.get("modified_actions"): + user_actions = {a.get("action", "") for a in user_decision["modified_actions"]} + shadow_actions = {a.get("action", "") for a in suggested_actions} + matched = len(user_actions & shadow_actions) + diverged = len(user_actions - shadow_actions) + match_score = 0.5 if len(suggested_actions) > 0 else 0.5 + elif user_action == "reject": + match_score = 0.0 + diverged = len(suggested_actions) + else: # ignore + match_score = 0.0 + + return { + "match_score": round(match_score, 2), + "matched_points": matched, + "diverged_points": diverged, + } + + def record_comparison(self, user_id: str, category: str, + shadow_suggestion: Dict[str, Any], + user_decision: Dict[str, Any], + user_action: str, + context: Optional[Dict[str, Any]] = None) -> Optional[str]: + """记录一次影子对比。""" + comparison = self.compare(user_id, shadow_suggestion, user_decision, user_action) + + db: Optional[Session] = None + try: + db = SessionLocal() + entry = ShadowComparison( + user_id=user_id, + category=category, + shadow_suggestion=shadow_suggestion, + shadow_confidence=shadow_suggestion.get("confidence", 0.5), + user_decision=user_decision, + user_action=user_action, + match_score=comparison["match_score"], + match_detail=comparison, + context=context, + ) + db.add(entry) + db.commit() + db.refresh(entry) + return str(entry.id) + except Exception as e: + logger.error("记录影子对比失败: %s", e) + if db: + try: + db.rollback() + except Exception: + pass + return None + finally: + if db: + try: + db.close() + except Exception: + pass + + def get_accuracy(self, user_id: str, category: Optional[str] = None, days: int = 30) -> Dict[str, Any]: + """获取影子模式准确率统计。""" + db: Optional[Session] = None + try: + db = SessionLocal() + from datetime import datetime, timedelta + since = datetime.now() - timedelta(days=days) + + q = db.query(ShadowComparison).filter( + ShadowComparison.user_id == user_id, + ShadowComparison.created_at >= since, + ) + if category: + q = q.filter(ShadowComparison.category == category) + + records = q.all() + total = len(records) + if total == 0: + return {"total_comparisons": 0, "message": "暂无数据"} + + avg_score = sum(r.match_score or 0 for r in records) / total + accepted = sum(1 for r in records if r.user_action == "accept") + rejected = sum(1 for r in records if r.user_action == "reject") + modified = sum(1 for r in records if r.user_action == "modify") + + by_category = {} + for r in records: + cat = r.category + if cat not in by_category: + by_category[cat] = {"total": 0, "sum_score": 0.0} + by_category[cat]["total"] += 1 + by_category[cat]["sum_score"] += (r.match_score or 0) + + cat_accuracy = { + cat: round(d["sum_score"] / d["total"], 3) + for cat, d in by_category.items() + } + + unlocked = { + cat: acc >= self._unlock_thresholds.get(cat, 0.90) + for cat, acc in cat_accuracy.items() + } + + return { + "total_comparisons": total, + "average_accuracy": round(avg_score, 3), + "accepted": accepted, + "rejected": rejected, + "modified": modified, + "by_category": cat_accuracy, + "unlocked_categories": unlocked, + "period_days": days, + } + + except Exception as e: + logger.error("获取准确率失败: %s", e) + return {"error": str(e)} + finally: + if db: + try: + db.close() + except Exception: + pass + + +shadow_executor = ShadowExecutor() diff --git a/backend/app/services/tool_discovery.py b/backend/app/services/tool_discovery.py new file mode 100644 index 0000000..30efe1f --- /dev/null +++ b/backend/app/services/tool_discovery.py @@ -0,0 +1,272 @@ +""" +新工具自动发现与集成 — 扫描内外工具源,自动评估和生成集成方案 +""" +from __future__ import annotations + +import logging +import os +import json +from typing import Any, Dict, List, Optional + +from app.services.llm_service import llm_service + +logger = logging.getLogger(__name__) + +TOOL_DISCOVERY_PROMPT = """你是一个工具集成分析专家。分析以下新工具是否适合集成到 AI Agent 平台。 + +当前平台能力: +- Agent 工作流编排 (DAG) +- LLM 调用 (多模型支持) +- API 调用节点 +- 代码执行节点 (Python/JS) +- 数据库查询节点 +- 条件分支节点 +- 通知节点 (飞书/Email) + +新工具信息: +{tool_info} + +请分析: +1. 匹配度 (0-1): 该工具与平台现有能力的互补程度 +2. 适用场景: 什么情况下 Agent 需要使用此工具 +3. 集成复杂度: low/medium/high +4. 建议的 adapter 类型: api_wrapper / code_executor / plugin +5. 集成方案: 简要描述如何接入 +6. 潜在风险: 使用此工具需要注意的问题 + +返回 JSON 格式: +{{"match_score": 0.8, "scenarios": ["场景1"], "complexity": "medium", "adapter_type": "api_wrapper", "integration_plan": "方案描述", "risks": ["风险1"]}} +""" + + +class ToolDiscovery: + """新工具自动发现与集成""" + + def __init__(self): + self._external_sources = [ + {"name": "github_trending", "url": "https://github.com/trending/python?since=weekly"}, + {"name": "mcp_marketplace", "url": "https://github.com/modelcontextprotocol/servers"}, + ] + + def scan_internal_tools(self, tools_dir: str = "") -> List[Dict[str, Any]]: + """扫描平台内部 tools 目录,发现未注册的工具。""" + if not tools_dir: + base = os.path.dirname(os.path.dirname(__file__)) + tools_dir = os.path.join(base, "tools") + if not os.path.isdir(tools_dir): + tools_dir = os.path.join(os.path.dirname(base), "tools") + + discovered = [] + if not os.path.isdir(tools_dir): + return discovered + + from app.services.tool_registry import tool_registry + + for filename in os.listdir(tools_dir): + if filename.startswith("_") or filename.startswith("__"): + continue + if filename.endswith(".py") and filename != "__init__.py": + tool_name = filename[:-3] + registered = tool_registry.get(tool_name) if hasattr(tool_registry, 'get') else None + discovered.append({ + "tool_name": tool_name, + "source": "internal", + "registered": registered is not None, + "file_path": os.path.join(tools_dir, filename), + }) + + return discovered + + def scan_external_sources(self) -> List[Dict[str, Any]]: + """扫描外部工具源(GitHub trending, MCP marketplace)。""" + discovered = [] + for source in self._external_sources: + try: + import urllib.request + req = urllib.request.Request(source["url"], headers={"User-Agent": "AI-Agent-Platform"}) + # 只记录源信息,实际爬取在 evaluate_and_rank 中按需进行 + discovered.append({ + "source_name": source["name"], + "url": source["url"], + "status": "available", + }) + except Exception as e: + logger.warning("外部源 %s 不可用: %s", source["name"], e) + discovered.append({ + "source_name": source["name"], + "url": source["url"], + "status": "unavailable", + "error": str(e), + }) + + return discovered + + def evaluate_tool(self, tool_name: str, tool_description: str = "", + tool_source: str = "", tool_docs: str = "") -> Dict[str, Any]: + """评估单个工具的集成价值(使用 LLM)。""" + tool_info = f""" +工具名称: {tool_name} +来源: {tool_source} +描述: {tool_description} +文档/代码摘要: {tool_docs[:3000]} +""" + prompt = TOOL_DISCOVERY_PROMPT.format(tool_info=tool_info) + + result = { + "tool_name": tool_name, + "match_score": 0.0, + "scenarios": [], + "complexity": "unknown", + "adapter_type": "unknown", + "integration_plan": "", + "risks": [], + } + + try: + response = llm_service.chat_sync( + messages=[{"role": "user", "content": prompt}], + temperature=0.3, + max_tokens=1000, + ) + content = response.get("content", "") if isinstance(response, dict) else str(response) + # 提取 JSON + json_match = self._extract_json(content) + if json_match: + evaluation = json.loads(json_match) + result.update(evaluation) + result["llm_raw"] = content[:500] + except Exception as e: + logger.error("LLM 评估工具 %s 失败: %s", tool_name, e) + result["error"] = str(e) + + return result + + def _extract_json(self, text: str) -> Optional[str]: + """从文本中提取 JSON 块。""" + import re + # 尝试提取 ```json ... ``` 块 + match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', text, re.DOTALL) + if match: + return match.group(1) + # 尝试直接找 {...} + match = re.search(r'\{[^{}]*"match_score"[^{}]*\}', text, re.DOTALL) + if match: + return match.group(0) + return None + + def discover_and_rank(self, limit: int = 10) -> Dict[str, Any]: + """完整的发现+评估+排序流程。""" + internal = self.scan_internal_tools() + + # 聚焦未注册的工具 + unregistered = [t for t in internal if not t["registered"]] + + evaluations = [] + for tool in unregistered[:limit]: + eval_result = self.evaluate_tool( + tool_name=tool["tool_name"], + tool_source="internal", + ) + evaluations.append(eval_result) + + evaluations.sort(key=lambda x: x.get("match_score", 0), reverse=True) + + high_match = [e for e in evaluations if e.get("match_score", 0) >= 0.8] + medium_match = [e for e in evaluations if 0.5 <= e.get("match_score", 0) < 0.8] + + return { + "total_discovered": len(internal), + "unregistered": len(unregistered), + "evaluated": len(evaluations), + "high_match": high_match, + "medium_match": medium_match, + "all_ranked": evaluations, + "recommendation": ( + f"发现 {len(high_match)} 个高匹配工具建议立即集成, " + f"{len(medium_match)} 个中匹配工具可进一步评估" + ), + } + + def generate_adapter(self, tool_name: str, evaluation: Dict[str, Any]) -> str: + """根据评估结果生成 tool adapter 代码框架。""" + adapter_type = evaluation.get("adapter_type", "api_wrapper") + + if adapter_type == "api_wrapper": + return self._generate_api_adapter(tool_name, evaluation) + elif adapter_type == "code_executor": + return self._generate_code_adapter(tool_name, evaluation) + else: + return self._generate_plugin_adapter(tool_name, evaluation) + + def _generate_api_adapter(self, tool_name: str, eval_result: Dict[str, Any]) -> str: + """生成 API 包装器 adapter。""" + return f'''""" +{tool_name} — 自动发现的工具适配器 +匹配度: {eval_result.get("match_score", "N/A")} +场景: {", ".join(eval_result.get("scenarios", []))} +""" +from typing import Any, Dict, Optional + + +async def {tool_name}(params: Dict[str, Any]) -> Dict[str, Any]: + """Auto-generated adapter for {tool_name}""" + try: + # TODO: 根据 API 文档实现具体调用逻辑 + result = {{ + "success": True, + "data": None, + "message": "Adapter stub — 请根据 API 文档完善", + }} + return result + except Exception as e: + return {{"success": False, "error": str(e)}} +''' + + def _generate_code_adapter(self, tool_name: str, eval_result: Dict[str, Any]) -> str: + return f'''""" +{tool_name} — 代码执行器适配器 +匹配度: {eval_result.get("match_score", "N/A")} +""" +import subprocess +from typing import Any, Dict + + +async def {tool_name}(code: str, language: str = "python") -> Dict[str, Any]: + """Auto-generated code executor adapter for {tool_name}""" + try: + executor = "python" if language == "python" else "node" + result = subprocess.run( + [executor, "-c", code], + capture_output=True, text=True, timeout=30 + ) + return {{ + "success": result.returncode == 0, + "stdout": result.stdout, + "stderr": result.stderr, + }} + except Exception as e: + return {{"success": False, "error": str(e)}} +''' + + def _generate_plugin_adapter(self, tool_name: str, eval_result: Dict[str, Any]) -> str: + return f'''""" +{tool_name} — 插件适配器 +匹配度: {eval_result.get("match_score", "N/A")} +""" +from typing import Any, Dict + + +class {tool_name.title().replace("_", "")}Plugin: + """Auto-generated plugin adapter for {tool_name}""" + + def __init__(self): + self.name = "{tool_name}" + + async def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: + """执行插件逻辑""" + # TODO: 根据文档实现 + return {{"success": True, "data": None}} +''' + + +tool_discovery = ToolDiscovery() diff --git a/backend/app/services/tool_registry.py b/backend/app/services/tool_registry.py index e8df7f0..0cb3f86 100644 --- a/backend/app/services/tool_registry.py +++ b/backend/app/services/tool_registry.py @@ -182,12 +182,20 @@ class ToolRegistry: @staticmethod async def _run_function(func: Callable, name: str, args: Dict[str, Any]) -> str: """执行内置工具函数。""" - import asyncio + import asyncio, inspect try: + # 过滤掉函数不接受的参数,避免 LLM 生成的参数名不匹配导致 TypeError + sig = inspect.signature(func) + valid_params = set(sig.parameters.keys()) + filtered_args = {k: v for k, v in args.items() if k in valid_params} + skipped = [k for k in args if k not in valid_params] + if skipped: + logger.warning("工具 '%s' 忽略未知参数: %s", name, skipped) + if asyncio.iscoroutinefunction(func): - result = await func(**args) + result = await func(**filtered_args) else: - result = func(**args) + result = func(**filtered_args) if isinstance(result, (dict, list)): return json.dumps(result, ensure_ascii=False) return str(result) diff --git a/backend/app/services/workflow_engine.py b/backend/app/services/workflow_engine.py index dfebd0c..3c7a421 100644 --- a/backend/app/services/workflow_engine.py +++ b/backend/app/services/workflow_engine.py @@ -680,12 +680,22 @@ class WorkflowEngine: if in_degree[neighbor] == 0: queue.append(neighbor) - # 检查是否有环(只检查可达节点) - reachable_nodes = set(result) - if len(reachable_nodes) < len(self.nodes): - # 有些节点不可达,这是正常的(条件分支) - pass - + # 检查是否有环:Kahn 算法结束后仍有非零入度的节点之间存在边 → 环路 + remaining = [n for n in self.nodes.keys() if n not in result and in_degree.get(n, 0) > 0] + if remaining: + cycle_nodes = set() + for n in remaining: + for nb in graph.get(n, []): + if nb in remaining: + cycle_nodes.add(n) + cycle_nodes.add(nb) + if cycle_nodes: + logger.error("DAG 环路检测: 涉及节点 %s", list(cycle_nodes)) + raise WorkflowExecutionError( + detail=f"工作流存在循环依赖,涉及节点: {list(cycle_nodes)}", + ) + logger.debug("不可达节点(非环路,可能被条件分支排除): %s", remaining) + self.execution_graph = result return result @@ -949,6 +959,27 @@ class WorkflowEngine: return result + def _resolve_safe_path(self, file_path: str) -> str: + """路径遍历防护:解析并验证路径在工作区根目录内。""" + import os + from pathlib import Path + from app.core.config import settings + raw_root = (getattr(settings, "LOCAL_FILE_TOOLS_ROOT", None) or "").strip() + if raw_root: + workspace_root = Path(raw_root).expanduser().resolve() + else: + workspace_root = Path(__file__).resolve().parent.parent.parent.parent + try: + p = Path(file_path).expanduser() + if not p.is_absolute(): + p = (workspace_root / p).resolve() + else: + p = p.resolve() + p.relative_to(workspace_root) + return str(p) + except (ValueError, OSError) as e: + raise ValueError(f"路径访问被拒绝: {file_path} (工作区根: {workspace_root})") from e + def _resolve_llm_prompt_placeholder(self, input_data: Dict[str, Any], var_path: str) -> Any: """ 解析 LLM 提示词中的 {{path}}。 @@ -1385,8 +1416,11 @@ class WorkflowEngine: merged: Dict[str, Any] = {**root} if isinstance(input_data, dict): merged = {**merged, **input_data} - decision = merged.get('__hil_decision') - comment = merged.get('__hil_comment') + # 使用 per-node 决策键,避免多审批节点冲突 + decision_key = f'__hil_decision_{node_id}' + comment_key = f'__hil_comment_{node_id}' + decision = merged.get(decision_key) or merged.get('__hil_decision') + comment = merged.get(comment_key) or merged.get('__hil_comment') if decision == 'approved': out = { 'approved': True, @@ -2590,6 +2624,8 @@ class WorkflowEngine: if file_path: file_path = replace_variables(file_path, input_data) + # 路径遍历防护:确保路径在允许的工作区内 + file_path = self._resolve_safe_path(file_path) if isinstance(content, str): content = replace_variables(content, input_data) @@ -3837,6 +3873,9 @@ class WorkflowEngine: logger.info(f"[rjb] Cache节点 {node_id} 执行value模板") logger.info(f"[rjb] value_str前300字符: {value_str[:300]}") logger.info(f"[rjb] user_input: {user_input[:50]}, output: {str(output)[:50]}, timestamp: {timestamp}") + # 安全校验:禁止过长的模板字符串,防止资源耗尽 + if len(value_str) > 10000: + raise ValueError(f"模板表达式过长 ({len(value_str)} 字符),拒绝执行") value = eval(value_str, {"__builtins__": {}}, safe_dict) logger.info(f"[rjb] Cache节点 {node_id} value模板执行成功,类型: {type(value)}") diff --git a/backend/app/tasks/knowledge_tasks.py b/backend/app/tasks/knowledge_tasks.py new file mode 100644 index 0000000..e4b5c8f --- /dev/null +++ b/backend/app/tasks/knowledge_tasks.py @@ -0,0 +1,19 @@ +"""知识提取 Celery 定时任务""" +from app.core.celery_app import celery_app +from app.services.knowledge_extractor import knowledge_extractor +import logging + +logger = logging.getLogger(__name__) + + +@celery_app.task(name="extract_knowledge_from_logs") +def extract_knowledge_task(limit: int = 10) -> dict: + """定时从执行日志中提取知识。""" + logger.info("知识提取定时任务开始 (limit=%d)", limit) + try: + count = knowledge_extractor.run_extraction_pipeline(limit=limit) + logger.info("知识提取完成: %d 条新知识", count) + return {"status": "ok", "extracted": count} + except Exception as e: + logger.error("知识提取任务失败: %s", e) + return {"status": "error", "error": str(e)} diff --git a/deploy/filebeat.yml b/deploy/filebeat.yml new file mode 100644 index 0000000..82ea6e8 --- /dev/null +++ b/deploy/filebeat.yml @@ -0,0 +1,24 @@ +# Filebeat 日志收集配置 +# 将 FastAPI、Celery 日志发送到 Elasticsearch +filebeat.inputs: + - type: log + enabled: true + paths: + - /app/logs/*.log + fields: + service: aiagent-backend + multiline: + pattern: '^\d{4}-\d{2}-\d{2}' + negate: true + match: after + + - type: log + enabled: true + paths: + - /var/log/nginx/*.log + fields: + service: aiagent-nginx + +output.elasticsearch: + hosts: ["elasticsearch:9200"] + index: "aiagent-%{+yyyy.MM.dd}" diff --git a/deploy/locustfile.py b/deploy/locustfile.py new file mode 100644 index 0000000..a73a6e5 --- /dev/null +++ b/deploy/locustfile.py @@ -0,0 +1,48 @@ +"""Locust 性能压测脚本 — 天工智能体平台 +启动: locust -f deploy/locustfile.py --host=http://localhost:8037 +""" +from locust import HttpUser, task, between +import json + + +class AIPlatformUser(HttpUser): + wait_time = between(1, 3) + + def on_start(self): + """登录获取 token。""" + resp = self.client.post( + "/api/v1/auth/login", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={"username": "admin", "password": "123456"}, + ) + if resp.status_code == 200: + self.token = resp.json().get("access_token", "") + else: + self.token = "" + + def _headers(self): + return {"Authorization": f"Bearer {self.token}"} if self.token else {} + + @task(5) + def get_agents(self): + self.client.get("/api/v1/agents", headers=self._headers()) + + @task(3) + def agent_chat(self): + self.client.post( + "/api/v1/agent-chat/bare", + json={"message": "你好,请简单介绍一下自己"}, + headers=self._headers(), + ) + + @task(2) + def get_executions(self): + self.client.get("/api/v1/executions?limit=10", headers=self._headers()) + + @task(2) + def health_check(self): + self.client.get("/health") + + @task(1) + def get_workflows(self): + self.client.get("/api/v1/workflows?limit=20", headers=self._headers()) diff --git a/deploy/playwright.config.ts b/deploy/playwright.config.ts new file mode 100644 index 0000000..e303947 --- /dev/null +++ b/deploy/playwright.config.ts @@ -0,0 +1,16 @@ +/* Playwright E2E 测试配置 — 天工智能体平台 */ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "../tests/e2e", + timeout: 30000, + retries: 1, + use: { + baseURL: "http://localhost:8038", + screenshot: "only-on-failure", + trace: "retain-on-failure", + }, + projects: [ + { name: "chromium", use: { browserName: "chromium" } }, + ], +}); diff --git a/deploy/prometheus.yml b/deploy/prometheus.yml new file mode 100644 index 0000000..371e1b7 --- /dev/null +++ b/deploy/prometheus.yml @@ -0,0 +1,24 @@ +# Prometheus 监控配置 — 天工智能体平台 +# 启动: docker run -d --name prometheus -p 9090:9090 -v $PWD/deploy/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus + +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "aiagent-backend" + metrics_path: /metrics + static_configs: + - targets: ["backend:8037"] + labels: + service: "fastapi" + + - job_name: "aiagent-celery" + static_configs: + - targets: ["celery-exporter:9808"] + labels: + service: "celery" + + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..24c0afe --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,161 @@ +version: "3.8" + +# 天工智能体平台 — 生产环境 Docker Compose +# 启动: docker compose -f docker-compose.prod.yml up -d +# 停止: docker compose -f docker-compose.prod.yml down + +services: + # ─── MySQL ─────────────────────────────────────────────────── + mysql: + image: mysql:8.0 + container_name: aiagent-mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-change-me} + MYSQL_DATABASE: agent_db + MYSQL_CHARSET: utf8mb4 + MYSQL_COLLATION: utf8mb4_unicode_ci + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - aiagent-net + + # ─── Redis ─────────────────────────────────────────────────── + redis: + image: redis:7-alpine + container_name: aiagent-redis + restart: always + command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + networks: + - aiagent-net + + # ─── FastAPI 后端 ──────────────────────────────────────────── + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: aiagent-backend + restart: always + environment: + - DATABASE_URL=mysql+pymysql://root:${MYSQL_ROOT_PASSWORD:-change-me}@mysql:3306/agent_db?charset=utf8mb4 + - REDIS_URL=redis://redis:6379/0 + - JWT_SECRET_KEY=${JWT_SECRET_KEY:-prod-jwt-secret-change-me} + - SECRET_KEY=${SECRET_KEY:-prod-secret-change-me} + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:8038,http://localhost:3000} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-} + - EXTERNAL_URL=${EXTERNAL_URL:-http://localhost} + ports: + - "8037:8037" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - agent_workspaces:/app/agent_workspaces + - uploads:/app/uploads + - logs:/app/logs + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8037/docs"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - aiagent-net + + # ─── Celery Worker ─────────────────────────────────────────── + celery-worker: + build: + context: ./backend + dockerfile: Dockerfile + container_name: aiagent-celery-worker + restart: always + command: celery -A app.core.celery_app worker --loglevel=info --concurrency=4 + environment: + - DATABASE_URL=mysql+pymysql://root:${MYSQL_ROOT_PASSWORD:-change-me}@mysql:3306/agent_db?charset=utf8mb4 + - REDIS_URL=redis://redis:6379/0 + - JWT_SECRET_KEY=${JWT_SECRET_KEY:-prod-jwt-secret-change-me} + - SECRET_KEY=${SECRET_KEY:-prod-secret-change-me} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-} + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - agent_workspaces:/app/agent_workspaces + - uploads:/app/uploads + - logs:/app/logs + networks: + - aiagent-net + + # ─── Celery Beat (定时调度) ────────────────────────────────── + celery-beat: + build: + context: ./backend + dockerfile: Dockerfile + container_name: aiagent-celery-beat + restart: always + command: celery -A app.core.celery_app beat --loglevel=info + environment: + - DATABASE_URL=mysql+pymysql://root:${MYSQL_ROOT_PASSWORD:-change-me}@mysql:3306/agent_db?charset=utf8mb4 + - REDIS_URL=redis://redis:6379/0 + - JWT_SECRET_KEY=${JWT_SECRET_KEY:-prod-jwt-secret-change-me} + - SECRET_KEY=${SECRET_KEY:-prod-secret-change-me} + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + networks: + - aiagent-net + + # ─── 前端 (Nginx) ─────────────────────────────────────────── + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: aiagent-frontend + restart: always + ports: + - "8038:80" + depends_on: + - backend + networks: + - aiagent-net + +volumes: + mysql_data: + driver: local + redis_data: + driver: local + agent_workspaces: + driver: local + uploads: + driver: local + logs: + driver: local + +networks: + aiagent-net: + driver: bridge diff --git a/docs/UI_UX_Design_Document.md b/docs/UI_UX_Design_Document.md new file mode 100644 index 0000000..aff3e62 --- /dev/null +++ b/docs/UI_UX_Design_Document.md @@ -0,0 +1,669 @@ +# 🎨 天工智能体平台 - UI/UX 设计文档 + +> **文档版本**: v1.0 +> **项目名称**: 天工智能体平台 (Tiangong AI Agent Platform) +> **技术栈**: Vue 3 + TypeScript + Element Plus + Vue Flow / FastAPI + Celery +> **目标用户**: AI应用开发者、运维人员、业务管理人员 + +--- + +## 📋 文档目录 + +1. [设计总览](#1-设计总览) +2. [视觉设计规范](#2-视觉设计规范) +3. [页面结构与路由](#3-页面结构与路由) +4. [页面详细设计](#4-页面详细设计) +5. [交互流程](#5-交互流程) +6. [组件设计](#6-组件设计) +7. [响应式布局](#7-响应式布局) +8. [状态与微交互](#8-状态与微交互) + +--- + +## 1. 设计总览 + +### 1.1 设计目标 + +| 目标 | 描述 | +|:----|:------| +| **高效** | AI Agent 的创建、配置、编排应清晰直观,减少操作步骤 | +| **可视** | 工作流编排、Agent 调用链等抽象流程应可视化呈现 | +| **实时** | 执行状态、对话响应、监控数据需实时反馈给用户 | +| **一致** | 全局 UI 风格统一,交互模式可预测 | +| **可扩展** | 支持插件、工具市场、模板市场等扩展能力 | + +### 1.2 产品架构图 + +``` +┌──────────────────────────────────────────────────────────┐ +│ 天工智能体平台 │ +├──────────────────────────────────────────────────────────┤ +│ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ +│ │ 主控台 │ │ 工作流管理 │ │ Agent管理 │ │ 数字员工工厂 │ │ +│ │ (模板市场)│ │ (可视化编排)│ │ (能力配置) │ │ (目标驱动) │ │ +│ └─────────┘ └──────────┘ └──────────┘ └────────────┘ │ +│ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ +│ │ Agent对话 │ │ 执行历史 │ │ 系统监控 │ │ 工具/插件 │ │ +│ │ (流式响应)│ │ (状态追踪) │ │ (性能指标) │ │ 市场 │ │ +│ └─────────┘ └──────────┘ └──────────┘ └────────────┘ │ +├──────────────────────────────────────────────────────────┤ +│ 认证模块 · 权限管理 · 模型配置 · 数据源管理 │ +└──────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 视觉设计规范 + +### 2.1 品牌色彩 + +``` +主色调 ● #409EFF (Element Plus 品牌蓝) — 导航、主按钮、链接 +辅助色 1 ● #67C23A (成功/运行绿) — 运行中、成功状态 +辅助色 2 ● #E6A23C (警告/待处理橙) — 警告、暂停状态 +辅助色 3 ● #F56C6C (危险/失败红) — 错误、删除操作 +辅助色 4 ● #909399 (中性/禁用灰) — 禁用、次要信息 +渐变 ● #667EEA → #764BA2 — 登录页背景 +``` + +### 2.2 字体规范 + +| 层级 | 字号 | 字重 | 使用场景 | +|:----|:----|:----|:--------| +| H1 | 24px | 600 | 页面大标题 | +| H2 | 20px | 600 | 区块标题 | +| H3 | 16px | 600 | 卡片标题 | +| Body | 14px | 400 | 正文内容 | +| Small | 12px | 400 | 辅助文字、时间戳 | +| Caption | 11px | 400 | 标签、元信息 | + +### 2.3 间距系统 + +采用 4px 为基底的间距体系: + +| Token | 值 | 使用场景 | +|:------|:---|:--------| +| space-xs | 4px | 内联元素间距 | +| space-sm | 8px | 表单字段间距 | +| space-md | 12px | 组件内间距 | +| space-lg | 16px | 卡片内间距、网格间距 | +| space-xl | 20px | 区块间距、页面边距 | +| space-2xl | 24px | 大区块间距 | + +### 2.4 圆角与阴影 + +``` +圆角: 4px (按钮/输入框) · 8px (卡片/对话框) · 12px (气泡) +阴影: hover 时轻微上移 (translateY(-2px)) + box-shadow 增强 +阶梯: Steps 组件使用标准 Element Plus 阶梯样式 +``` + +--- + +## 3. 页面结构与路由 + +### 3.1 整体布局 + +``` +┌──────────────────────────────────────────────┐ +│ Header: 平台名称 + 用户信息 + 退出按钮 │ +├──────────────────────────────────────────────┤ +│ 水平导航菜单 (el-menu mode="horizontal") │ +├──────────────────────────────────────────────┤ +│ │ +│ 页面内容区域 (router-view) │ +│ │ +└──────────────────────────────────────────────┘ +``` + +### 3.2 路由列表(共 27 个页面) + +| 路由路径 | 页面名称 | 页面类型 | 说明 | +|:---------|:---------|:---------|:-----| +| `/login` | 登录页 | 公开 | 登录 / 注册 | +| `/` | 工作流管理 | 需认证 | 工作流列表、搜索、批量操作 | +| `/console` | 主控台 | 需认证 | 应用商店、场景模板、一键执行 | +| `/execution-board` | 执行看板 | 需认证 | 执行状态概览 | +| `/workflow/:id?` | 工作流设计器 | 需认证 | 可视化节点编排 | +| `/executions` | 执行历史 | 需认证 | 执行记录列表 | +| `/executions/:id` | 执行详情 | 需认证 | 单次执行详细信息 | +| `/agents` | Agent管理 | 需认证 | Agent CRUD、能力配置 | +| `/agents/:id/design` | Agent编排设计器 | 需认证 | Agent工作流编排+实时预览 | +| `/agents/:id/config` | Agent配置 | 需认证 | Agent参数详细配置 | +| `/agent-chat` | Agent对话 | 需认证 | 单Agent/多Agent编排对话 | +| `/agent-chat/:id` | 指定Agent对话 | 需认证 | 指定Agent的对话界面 | +| `/data-sources` | 数据源管理 | 需认证 | 数据源CRUD | +| `/model-configs` | 模型配置管理 | 需认证 | LLM模型配置 | +| `/template-market` | 模板市场 | 需认证 | 工作流模板浏览 | +| `/node-templates` | 节点模板 | 需认证 | 节点模板管理 | +| `/tools` | 工具市场 | 需认证 | 工具查看与配置 | +| `/plugins` | 插件市场 | 需认证 | 插件浏览与安装 | +| `/agent-market` | Agent技能商店 | 需认证 | 预置Agent技能 | +| `/agent-orchestration` | Agent协作 | 需认证 | 多Agent编排配置 | +| `/agent-schedules` | 定时任务 | 需认证 | Agent定时调度 | +| `/digital-employees` | 数字员工工厂 | 需认证 | 目标创建与管理 | +| `/goals/:id` | 目标详情 | 需认证 | 目标分解、任务树追踪 | +| `/monitoring` | 系统监控 | 需认证 | 系统性能指标 | +| `/agent-monitoring` | Agent监控看板 | 需认证 | Agent运行指标 | +| `/alert-rules` | 告警规则 | 需认证 | 告警配置 | +| `/permissions` | 权限管理 | 需认证+管理员 | 用户角色权限 | + +--- + +## 4. 页面详细设计 + +### 4.1 🔐 登录页 (`/login`) + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **布局** | 居中卡片布局,背景为蓝色渐变 (`#667EEA → #764BA2`) | +| **卡片** | 白色圆角卡片 (.login-card),宽度 400px | +| **Tabs** | 登录/注册两个 Tab 切换,默认显示登录 | +| **表单** | 用户名 + 密码(登录);用户名 + 邮箱 + 密码(注册) | +| **交互** | Enter 键提交;登录成功跳转到 `/`;表单校验(必填、邮箱格式) | + +#### 线框示意 + +``` +┌──────────────────────────────────┐ +│ 🌟 天工智能体平台 │ +│ ┌────────────────────────┐ │ +│ │ 登录 | 注册 │ │ +│ │ ───────────────────── │ │ +│ │ 用户名: [___________] │ │ +│ │ 密码: [___________] │ │ +│ │ │ │ +│ │ [ 登录 ] │ │ +│ └────────────────────────┘ │ +└──────────────────────────────────┘ +``` + +--- + +### 4.2 🏠 工作流管理 (`/`) + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **布局** | MainLayout 包裹,顶部水平导航菜单 | +| **操作栏** | "从模板创建"、"导入工作流"、"创建工作流" | +| **筛选栏** | 搜索框 + 状态筛选 + 排序字段 + 排序方式 + 按钮组 | +| **批量操作** | 选中后显示批量执行、批量导出、批量删除 | +| **表格** | 选择列 + 名称/描述/状态(标签)/创建时间/更新时间/操作列 | +| **操作按钮** | 编辑、版本、导出、执行历史、删除 | +| **对话框** | 模板选择对话框、版本管理对话框、导入对话框、批量执行对话框 | + +#### 交互流程 + +``` +用户进入首页 + ↓ +加载工作流列表 (fetchWorkflows) + ↓ +用户操作: + ├─ 搜索/筛选 → 重新加载列表 + ├─ 创建工作流 → 跳转 /workflow (设计器) + ├─ 从模板创建 → 弹出模板选择 → 输入名称 → 创建并跳转 + ├─ 导入工作流 → 上传 JSON → 确认导入 → 跳转设计器 + ├─ 编辑 → 跳转 /workflow/:id + ├─ 批量操作 → 选中 → 执行/导出/删除 + ├─ 版本管理 → 查看版本列表 → 回滚/查看 + └─ 删除 → 确认对话框 → 删除 +``` + +--- + +### 4.3 🎨 工作流设计器 (`/workflow/:id?`) + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **核心组件** | Vue Flow (@vue-flow/core) 可视化画布 | +| **头部** | 标题 + "测试运行" + "发布" + "返回" 按钮 | +| **画布** | 拖拽式节点编辑,支持 Start/End/LLM/Template/Code 等多种节点类型 | +| **节点面板** | 左侧或顶部可拖拽的节点工具箱 | +| **Agent模式** | 左右分栏:左侧编排 + 右侧实时对话预览 (400px) | +| **执行状态** | 节点边框颜色变化指示执行进度 (绿色=成功, 红色=失败) | + +#### 节点类型 + +| 节点类型 | 图标色 | 说明 | +|:---------|:------|:-----| +| Start | 🟢 绿色 | 工作流入口节点 | +| End | 🔴 红色 | 工作流出口节点 | +| LLM | 🔵 蓝色 | 大模型调用节点 | +| Template | 🟠 橙色 | 模板节点 | +| Code | 🟣 紫色 | 代码执行节点 | +| Condition | ⚪ 灰色 | 条件分支节点 | +| Tool | 🟡 黄色 | 工具调用节点 | +| Agent | 🟢 青色 | Agent 子流程节点 | + +--- + +### 4.4 🎮 主控台 / 应用商店 (`/console`) + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **页面布局** | 居中内容区 (max-width: 1200px) | +| **顶部** | 标题"应用商店" + 副标题 + "浏览模板市场"、"执行历史"按钮 | +| **分类 Tabs** | 全部 / 客服场景 / 研发辅助 / 运维分析 / 学习教育 | +| **模板网格** | 响应式网格 (xs:24 sm:12 md:8 lg:6) | +| **模板卡片** | 分类标签 + 标题 + 描述(3行截断) + "立即执行"按钮 | +| **快捷入口** | 三个跳转卡片:AI对话 / 我的Agent / 运行看板 | +| **执行对话框** | 任务描述 (必填) + 高级参数 (动态表单) + 一键执行 | +| **进度对话框** | 三步阶梯 (提交→执行中→完成) + 进度条 + 结果展示 | + +#### 交互流程 + +``` +1. 浏览模板 → 点击卡片 → 弹出执行对话框 +2. 填写任务描述 → 调整参数 → 点击"一键执行" +3. 关闭执行对话框 → 打开进度对话框 +4. 轮询执行进度 (2s间隔) → 阶梯更新 → 展示结果 +5. 可复制/下载结果,或点击"查看详情"跳转执行详情页 +``` + +--- + +### 4.5 🤖 Agent 管理 (`/agents`) + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **操作栏** | 导入Agent / 从场景模板创建 / 从Prompt模板创建 / 创建Agent | +| **搜索筛选** | 搜索框 + 状态筛选 + 搜索/刷新按钮 | +| **表格** | 名称 / 描述 / 技能标签 / 状态 / 版本 / 创建时间 / 操作 | +| **操作列 (密集)** | 编辑 / 能力配置 / 使用 / 设计 / 配置 / 复制 / 导出 / 部署/停止 / 删除 | +| **创建对话框** | 名称 + 描述 + 工作流配置提示 | +| **能力配置对话框** | 内置工具勾选 (Checkbox group) + 其他工具标签 | +| **场景模板创建** | 选模板 + 名称 + 描述 + 执行预算参数 | +| **Prompt模板创建** | 两阶段:选模板 → 填写Agent名称/描述 | + +#### 能力配置交互 + +``` +1. 点击"能力配置" → 加载Agent工作流中的现有工具列表 +2. 勾选内置工具 (file_read/file_write/system_info 等) +3. 可删除额外的自定义工具标签 +4. 保存 → 自动写入工作流主LLM节点的 tools/selected_tools 字段 +5. 刷新列表,技能标签列同步更新 +``` + +--- + +### 4.6 💬 Agent 对话 (`/agent-chat`) + +这是平台最核心、交互最复杂的页面之一。 + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **模式切换** | 单Agent / 多Agent编排 (Switch 组件) | +| **Agent选择** | 下拉选择已部署的Agent | +| **编排模式** | 辩论 / 路由 / 顺序 / 流水线 四种模式 | +| **消息列表** | 用户消息 (右对齐) / Agent消息 (左对齐) | +| **SSE 流式响应** | 事件类型: think / tool_call / tool_result / final / error / approval_required | +| **思考链 (Trace)** | 可展开的逐步推理过程,含迭代次数、工具调用 | +| **编排结果** | 多Agent输出 + 最终回答 + 各Agent展开详情 | +| **工具审批** | 需要人工审批的工具调用弹出确认对话框 | +| **输入区** | 多行文本输入 + 发送按钮 (支持 Enter 发送) | + +#### 思考链 (Thinking Trace) UI + +``` +┌──────────────────────────────────────────┐ +│ ▸ 思考链 (3 步) │ ← 点击展开 +├──────────────────────────────────────────┤ +│ ┌────────────────────────────────────┐ │ +│ │ 💭 思考 #1 │ │ +│ │ 我正在分析用户的问题... │ │ +│ └────────────────────────────────────┘ │ +│ ┌────────────────────────────────────┐ │ +│ │ 🔧 工具结果 #2 file_read │ │ +│ │ ┌ 参数 ─────────────────────────┐ │ │ +│ │ │ {"path": "README.md"} │ │ │ +│ │ └───────────────────────────────┘ │ │ +│ │ ┌ 结果 ─────────────────────────┐ │ │ +│ │ │ # Project README... │ │ │ +│ │ └───────────────────────────────┘ │ │ +│ └────────────────────────────────────┘ │ +│ ┌────────────────────────────────────┐ │ +│ │ ✅ 最终回答 #3 │ │ +│ │ 根据分析,建议您... │ │ +│ └────────────────────────────────────┘ │ +└──────────────────────────────────────────┘ +``` + +#### 状态管理 + +- 消息持久化到 `localStorage` (key: `agent_chat_state`) +- 支持不同Agent/Session的消息隔离 +- 支持重试失败消息 (找到最后一条用户消息重新发送) +- 支持复制消息内容 + +--- + +### 4.7 🏭 数字员工工厂 (`/digital-employees`) + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **布局** | 网格卡片布局 (grid, minmax 320px) | +| **页面标题** | "数字员工工厂" + 副标题 + "创建目标"按钮 | +| **目标卡片** | 状态标签 / 优先级P标记 / 标题 / 描述 / 进度条 / 截止日期 | +| **状态标签** | 进行中(绿) / 已暂停(橙) / 已完成(蓝) / 失败(红) / 已取消(灰) | +| **创建对话框** | 标题 + 描述 + 优先级选择 + 截止日期 + Main Agent选择 | +| **交互** | 点击卡片 → 跳转目标详情 (`/goals/:id`) | + +--- + +### 4.8 📊 执行历史 (`/executions`) & 执行详情 (`/executions/:id`) + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **执行列表** | 表格展示:工作流名称 / 状态 / 创建时间 / 耗时 / 操作 | +| **状态标签** | pending(灰) / running(蓝) / completed(绿) / failed(红) | +| **执行详情** | 节点级执行状态、输入输出数据、日志查看 | +| **实时更新** | WebSocket 推送执行状态变化 | +| **筛选** | 按状态、时间范围、工作流筛选 | + +--- + +### 4.9 📡 系统监控 (`/monitoring`) & Agent监控 (`/agent-monitoring`) + +#### 设计要点 + +| 项 | 说明 | +|:---|:-----| +| **系统监控** | CPU/内存/磁盘使用率图表、API请求量、错误率 | +| **Agent监控** | 每个Agent的执行次数、成功率、平均耗时、工具调用分布 | +| **图表类型** | 折线图(趋势)、柱状图(对比)、饼图(分布) | +| **时间范围** | 最近1小时/24小时/7天/30天切换 | + +--- + +### 4.10 🛠️ 其他管理页面 + +| 页面 | 核心功能 | +|:-----|:---------| +| **数据源管理** | 添加/测试/删除 MySQL、PostgreSQL、MongoDB 等数据源连接 | +| **模型配置管理** | 配置 OpenAI、DeepSeek、SiliconFlow、Anthropic 模型参数 | +| **工具市场** | 查看内置工具、自定义工具、工具测试 | +| **模板市场** | 浏览和下载社区工作流模板 | +| **节点模板** | 管理可复用的节点预设 | +| **插件市场** | 浏览和安装第三方插件 | +| **Agent技能商店** | 浏览预置Agent技能配置 | +| **权限管理** | 用户管理、角色配置、权限分配(仅管理员) | +| **告警规则** | 设置执行失败/超时等告警条件和通知方式 | +| **定时任务** | 配置Agent定时执行计划 (Cron表达式) | + +--- + +## 5. 交互流程 + +### 5.1 核心用户旅程 + +#### 旅程A:新手快速体验 + +``` +登录 → 主控台 → 选择一个场景模板 → 输入任务描述 → 一键执行 → 查看结果 +``` + +#### 旅程B:创建工作流 + +``` +登录 → 工作流管理 → 创建工作流 → 设计器拖拽编排 → 保存 → 执行 → 查看执行历史 +``` + +#### 旅程C:创建并对话Agent + +``` +登录 → Agent管理 → 创建Agent → 设计器配置工作流 → 部署 → 进入对话 → 发送消息 → 查看思考链 +``` + +#### 旅程D:数字员工目标驱动 + +``` +登录 → 数字员工工厂 → 创建目标 → Main Agent自动分解任务 → 执行子任务 → 追踪进度 +``` + +### 5.2 关键交互细节 + +#### 加载状态 + +| 场景 | 加载指示器 | 说明 | +|:----|:----------|:-----| +| 页面初始化 | `v-loading` 全屏遮罩 | 路由切换、数据加载 | +| 表格加载 | `v-loading` 表格区域 | 列表数据请求 | +| 按钮操作 | `:loading` 按钮状态 | 提交/保存/执行等操作 | +| SSE流式 | 动态加载动画 (`...`) | Agent对话等待响应 | +| 轮询进度 | 进度条 + 阶梯步骤 | 模板执行进度追踪 | + +#### 错误处理 + +| 错误类型 | 展示方式 | 用户操作 | +|:---------|:---------|:---------| +| 表单校验 | 字段下方红色提示 | 修正输入 | +| API错误 | `ElMessage.error` 顶部提示 | 根据提示操作 | +| 网络错误 | `ElMessage.error` + 控制台日志 | 检查网络/重试 | +| 执行失败 | 执行详情页红色状态 + 节点错误信息 | 查看日志/重新执行 | +| 超时 | 对话框提示"执行超时" | 检查配置/增加超时时间 | + +#### 空状态 + +| 场景 | 展示内容 | 引导操作 | +|:----|:---------|:---------| +| 工作流列表为空 | `el-empty` + 描述 | "创建工作流"按钮 | +| Agent列表为空 | 空表格 | "创建Agent"按钮 | +| 目标列表为空 | `el-empty` + 描述 | "创建第一个目标"按钮 | +| 搜索结果为空 | 空表格 | "重置"按钮 | +| 对话历史为空 | 图标 + 引导文字 | "选择一个Agent开始对话" | + +--- + +## 6. 组件设计 + +### 6.1 公共组件 + +| 组件名 | 文件位置 | 功能描述 | +|:-------|:---------|:---------| +| **MainLayout** | `components/MainLayout.vue` | 全局布局:Header + 水平菜单 + 内容区 | +| **WorkflowEditor** | `components/WorkflowEditor/` | Vue Flow 封装的工作流编辑器核心 | +| **AgentChatPreview** | `components/AgentChatPreview.vue` | Agent对话预览面板 (设计器右侧) | +| **PromptTemplatePicker** | `components/PromptTemplatePicker.vue` | Prompt模板选择器 | + +### 6.2 WorkflowEditor 组件设计 + +``` +┌──────────────────────────────────────────────┐ +│ [节点工具箱] │ +│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ +│ │ 开始 │ │ LLM │ │ 代码 │ │ 结束 │ │ +│ └────────┘ └────────┘ └────────┘ └────────┘ │ +├──────────────────────────────────────────────┤ +│ │ +│ [Start] ──→ [LLM] ──→ [Code] ──→ [End] │ +│ │ +│ ┌──────────────────────────────────────┐ │ +│ │ 选中节点属性面板 │ │ +│ │ 名称: [________________] │ │ +│ │ 模型: [▼ DeepSeek V4 Flash] │ │ +│ │ 温度: [===●==========] 0.7 │ │ +│ │ System Prompt: │ │ +│ │ [ 你是一个有用的AI助手... ] │ │ +│ └──────────────────────────────────────┘ │ +└──────────────────────────────────────────────┘ +``` + +### 6.3 AgentChatPreview 组件 + +| 属性 | 类型 | 说明 | +|:-----|:-----|:-----| +| agent-id | string | Agent ID | +| agent-name | string | Agent 名称 | +| opening-message | string | 开场白 | +| preset-questions | string[] | 预设问题列表 | +| node-test-result | any | 节点测试结果 | + +--- + +## 7. 响应式布局 + +### 7.1 断点设计 + +| 断点 | 宽度 | 适配目标 | +|:-----|:-----|:---------| +| xs | < 768px | 手机端(最小功能集) | +| sm | ≥ 768px | 平板 | +| md | ≥ 992px | 小桌面 | +| lg | ≥ 1200px | 大桌面(主力) | +| xl | ≥ 1920px | 超大屏 | + +### 7.2 响应式策略 + +| 组件 | xs | sm | md | lg+ | +|:-----|:---|:---|:---|:----| +| 顶部菜单 | 汉堡菜单 | 折叠菜单 | 完整菜单 | 完整菜单 | +| 模板网格 | 1列 | 2列 | 3列 | 4列 | +| 目标卡片 | 1列 | 2列 | 3列 | 4列 | +| Agent设计器 | 单栏 | 单栏 | 左右分栏 | 左右分栏 | +| 对话界面 | 全宽 | 全宽 | 最大960px | 最大960px | + +### 7.3 特殊布局 + +**Agent编排设计器** — 左右分栏(仅在 ≥1200px 生效): + +``` +┌───────────────────┬────────────────────┐ +│ │ │ +│ 工作流编辑器 │ Agent对话预览 │ +│ (flex: 1) │ (width: 400px) │ +│ │ │ +└───────────────────┴────────────────────┘ +``` + +--- + +## 8. 状态与微交互 + +### 8.1 状态体系 + +| 状态类型 | 视觉表现 | 示例 | +|:---------|:---------|:-----| +| **loading** | 旋转图标 / 骨架屏 / 进度条 | 数据加载、执行中 | +| **empty** | 插画 + 描述文字 + 引导按钮 | 无数据时 | +| **error** | 红色边框 / 错误图标 + 错误信息 | 校验失败、请求失败 | +| **success** | 绿色标签 / 成功提示 | 操作成功、执行完成 | +| **disabled** | 灰色不可点击 | 按钮禁用状态 | +| **hover** | 轻微上移 + 阴影增强 | 卡片悬停 | +| **active** | 颜色变化 + 底部指示线 | 菜单选中 | +| **transition** | 过渡动画 (0.2-0.3s) | 页面切换、弹窗 | + +### 8.2 微交互设计 + +| 交互 | 动画 | 时长 | 缓动函数 | +|:-----|:-----|:-----|:---------| +| 卡片hover | translateY(-2px) + shadow | 0.2s | ease | +| 消息出现 | fadeIn + slideUp | 0.3s | ease-out | +| 思考链展开 | 内容 slideDown | 0.25s | ease-in-out | +| 步骤切换 | 内容渐变过渡 | 0.3s | ease | +| 按钮点击 | scale(0.97) 按下效果 | 0.1s | ease | +| 对话框 | fadeIn + scale(0.95→1) | 0.25s | ease-out | +| 进度条更新 | width 过渡 | 0.5s | ease | +| 输入框聚焦 | 边框颜色过渡 | 0.2s | ease | + +### 8.3 思考中动画 (Thinking Dots) + +```css +.dot { + width: 8px; height: 8px; + background: #909399; + border-radius: 50%; + animation: bounce 1.4s infinite ease-in-out; +} +.dot:nth-child(2) { animation-delay: 0.16s; } +.dot:nth-child(3) { animation-delay: 0.32s; } + +@keyframes bounce { + 0%, 80%, 100% { transform: scale(0); } + 40% { transform: scale(1); } +} +``` + +--- + +## 附录 + +### A. 图标清单 + +| 图标名 | 用途 | 引入方式 | +|:-------|:-----|:---------| +| Search | 搜索按钮 | @element-plus/icons-vue | +| Document | 工作流管理菜单 | 同上 | +| User / UserFilled | Agent管理/数字员工 | 同上 | +| ChatLineSquare | Agent对话 | 同上 | +| Connection | 数据源管理 | 同上 | +| Setting | 模型配置管理 | 同上 | +| Tools | 工具市场 | 同上 | +| Shop | Agent技能商店 | 同上 | +| Monitor | 系统监控 | 同上 | +| Bell | 告警规则 | 同上 | +| Clock | 定时任务 | 同上 | +| Share | Agent协作 | 同上 | +| Star | 模板市场 | 同上 | +| Lock | 权限管理 | 同上 | +| Grid | 主控台 | 同上 | +| DataAnalysis | Agent监控 | 同上 | +| VideoPlay | 执行/测试 | 同上 | +| VideoPause | 停止 | 同上 | +| Plus | 创建 | 同上 | +| Delete | 删除 | 同上 | +| Edit | 编辑 | 同上 | +| CopyDocument | 复制 | 同上 | +| Download | 导出 | 同上 | +| Upload / UploadFilled | 导入/上传 | 同上 | +| Refresh | 刷新 | 同上 | +| DocumentCopy | 复制消息 | 同上 | +| CaretRight | 展开/折叠 | 同上 | +| ChatDotSquare | 思考步骤 | 同上 | +| Select | 选择 | 同上 | +| Promotion | Agent头像 | 同上 | + +### B. 存储键 + +| Key | 用途 | 存储方式 | +|:----|:-----|:---------| +| `token` | JWT认证令牌 | localStorage | +| `agent_chat_state` | 对话历史+状态持久化 | localStorage | +| `user` | 用户信息 | Pinia (userStore) | + +### C. API端点前缀 + +| 前缀 | 说明 | +|:-----|:-----| +| `/api/v1/auth/*` | 认证相关 | +| `/api/v1/workflows/*` | 工作流CRUD | +| `/api/v1/executions/*` | 执行管理 | +| `/api/v1/agents/*` | Agent管理 | +| `/api/v1/agent-chat/*` | Agent对话 (含SSE流式) | +| `/api/v1/platform/*` | 平台模板/主控台 | +| `/api/v1/tools/*` | 工具配置 | +| `/api/v1/monitoring/*` | 监控数据 | +| `/api/v1/goals/*` | 目标管理 | +| `/api/v1/tasks/*` | 任务管理 | +| `/ws/*` | WebSocket实时推送 | + +--- + +> **文档结束** — 本设计文档基于项目源码分析编写,涵盖 27+ 个页面的UI/UX设计。 diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..52079fe --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,403 @@ +# 🔌 API 参考文档 + +> **API Reference** + +本文档描述了天工智能体平台后端 RESTful API 接口规范。所有 API 均通过 Nginx 反向代理暴露,前缀为 `/api/v1`。 + +> 💡 **交互式文档**:启动后端服务后,可访问 `http://localhost:8037/docs` 查看 Swagger UI 文档。 + +--- + +## 一、通用规范 + +### 基础 URL + +``` +生产环境:https://your-domain.com/api/v1 +开发环境:http://localhost:8037/api/v1 +``` + +### 认证方式 + +所有受保护的接口需在请求头中携带 JWT Token: + +```http +Authorization: Bearer +``` + +### 响应格式 + +```json +// 成功响应 +{ + "code": 200, + "message": "success", + "data": { ... } +} + +// 错误响应 +{ + "code": 40001, + "message": "用户不存在", + "data": null +} +``` + +### 通用错误码 + +| 状态码 | 错误码 | 说明 | +|:------|:-------|:------| +| 200 | 200 | 请求成功 | +| 400 | 40000 | 请求参数错误 | +| 401 | 40100 | 未认证(Token 缺失或过期) | +| 403 | 40300 | 无权限访问 | +| 404 | 40400 | 资源不存在 | +| 500 | 50000 | 服务器内部错误 | + +--- + +## 二、用户模块 + +### 2.1 用户注册 + +注册新用户账号。 + +```http +POST /api/v1/users/register +``` + +**请求体:** + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `email` | string | ✅ | 邮箱地址 | +| `password` | string | ✅ | 密码(至少 8 位,含字母和数字) | +| `nickname` | string | ❌ | 用户昵称 | + +**请求示例:** + +```json +{ + "email": "user@example.com", + "password": "Abc12345", + "nickname": "张三" +} +``` + +**响应示例:** + +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "email": "user@example.com", + "nickname": "张三", + "created_at": "2026-05-10T08:00:00Z" + } +} +``` + +--- + +### 2.2 用户登录 + +获取 JWT 访问令牌。 + +```http +POST /api/v1/users/login +``` + +**请求体:** + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `email` | string | ✅ | 邮箱地址 | +| `password` | string | ✅ | 密码 | + +**响应示例:** + +```json +{ + "code": 200, + "message": "success", + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs...", + "token_type": "bearer", + "expires_in": 1800 + } +} +``` + +--- + +### 2.3 Token 刷新 + +使用 Refresh Token 获取新的 Access Token。 + +```http +POST /api/v1/users/refresh +``` + +**请求体:** + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `refresh_token` | string | ✅ | 登录时获取的 Refresh Token | + +**响应示例:** + +```json +{ + "code": 200, + "message": "success", + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs...", + "token_type": "bearer", + "expires_in": 1800 + } +} +``` + +--- + +### 2.4 获取当前用户信息 + +获取已登录用户的个人信息。 + +```http +GET /api/v1/users/me +``` + +**请求头:** +```http +Authorization: Bearer +``` + +**响应示例:** + +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "email": "user@example.com", + "nickname": "张三", + "avatar": "https://...", + "created_at": "2026-05-10T08:00:00Z" + } +} +``` + +--- + +## 三、智能体模块 + +### 3.1 创建智能体 + +创建一个新的 AI 智能体。 + +```http +POST /api/v1/agents +``` + +**请求体:** + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `name` | string | ✅ | 智能体名称 | +| `description` | string | ❌ | 智能体描述 | +| `prompt_template` | string | ✅ | 系统提示词模板 | +| `model_config` | object | ✅ | LLM 模型配置 | + +**`model_config` 对象:** + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `provider` | string | ✅ | 模型提供商:`openai` / `azure` / `local` | +| `model_name` | string | ✅ | 模型名称:`gpt-4o` / `gpt-3.5-turbo` | +| `temperature` | number | ❌ | 生成温度(默认 0.7) | +| `max_tokens` | integer | ❌ | 最大 Token 数(默认 2048) | + +**请求示例:** + +```json +{ + "name": "智能客服助手", + "description": "用于解答常见产品问题", + "prompt_template": "你是一个专业的客服助手...", + "model_config": { + "provider": "openai", + "model_name": "gpt-4o", + "temperature": 0.5, + "max_tokens": 2048 + } +} +``` + +--- + +### 3.2 获取智能体列表 + +获取当前用户的所有智能体。 + +```http +GET /api/v1/agents?page=1&page_size=20 + +``` + +**查询参数:** + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|:-----|:-----|:-----|:-------|:------| +| `page` | integer | ❌ | 1 | 页码 | +| `page_size` | integer | ❌ | 20 | 每页条数(最大 100) | + +--- + +### 3.3 获取智能体详情 + +```http +GET /api/v1/agents/{agent_id} +``` + +**路径参数:** + +| 参数 | 类型 | 说明 | +|:-----|:-----|:------| +| `agent_id` | integer | 智能体 ID | + +--- + +### 3.4 更新智能体 + +```http +PUT /api/v1/agents/{agent_id} +``` + +**请求体:**(部分更新) + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `name` | string | ❌ | 智能体名称 | +| `description` | string | ❌ | 智能体描述 | +| `prompt_template` | string | ❌ | 系统提示词 | +| `model_config` | object | ❌ | 模型配置 | + +--- + +### 3.5 删除智能体 + +```http +DELETE /api/v1/agents/{agent_id} +``` + +--- + +## 四、对话模块 + +### 4.1 创建对话会话 + +```http +POST /api/v1/conversations +``` + +**请求体:** + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `agent_id` | integer | ✅ | 关联的智能体 ID | +| `title` | string | ❌ | 会话标题(可选) | + +--- + +### 4.2 发送消息 + +向指定对话发送消息,支持流式响应(SSE)。 + +```http +POST /api/v1/conversations/{conversation_id}/messages +``` + +**请求体:** + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `content` | string | ✅ | 用户消息内容 | + +**流式响应(SSE):** + +``` +data: {"type": "token", "content": "你好"} +data: {"type": "token", "content": ","} +data: {"type": "token", "content": "请问有什么可以帮您?"} +data: {"type": "done"} +data: {"type": "error", "content": "..."} +``` + +--- + +### 4.3 获取对话历史 + +```http +GET /api/v1/conversations/{conversation_id}/messages?page=1&page_size=50 +``` + +--- + +## 五、知识库模块 + +### 5.1 上传文档 + +```http +POST /api/v1/knowledge/documents/upload +``` + +**请求格式:** `multipart/form-data` + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `file` | file | ✅ | 支持 PDF / TXT / Markdown | +| `agent_id` | integer | ✅ | 关联的智能体 ID | + +--- + +### 5.2 文档检索 + +```http +POST /api/v1/knowledge/search +``` + +**请求体:** + +| 参数 | 类型 | 必填 | 说明 | +|:-----|:-----|:-----|:------| +| `query` | string | ✅ | 检索关键词 | +| `agent_id` | integer | ✅ | 限定知识库范围 | +| `top_k` | integer | ❌ | 返回条数(默认 5) | + +--- + +## 六、健康检查 + +```http +GET /api/v1/health +``` + +**响应:** + +```json +{ + "status": "ok", + "version": "1.0.0", + "timestamp": "2026-05-10T08:00:00Z" +} +``` + +--- + +> 📎 **相关文档**:[快速开始指南](./quickstart.md) | [开发指南](./development-guide.md) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..582dedb --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,161 @@ +# 🏛️ 架构设计文档 + +> **Architecture Design Document** + +--- + +## 一、系统概述 + +天工智能体平台是一个企业级 AI 智能体(Agent)平台,提供智能对话、知识库管理、智能体编排等核心能力,采用**前后端分离** + **异步任务队列** 的架构设计。 + +--- + +## 二、技术选型 + +| 层级 | 技术 | 版本 | 选型理由 | +|:----|:-----|:-----|:---------| +| **前端框架** | Vue 3 (Composition API) | 3.4+ | 轻量、响应式、TypeScript 友好 | +| **前端构建** | Vite | 5+ | 极速 HMR,开发体验优异 | +| **状态管理** | Pinia | 2+ | Vue 3 官方推荐,TypeScript 支持完善 | +| **路由** | Vue Router | 4+ | SPA 路由,支持导航守卫 | +| **HTTP 客户端** | Axios | 1+ | 请求/响应拦截,统一错误处理 | +| **后端框架** | FastAPI | 0.110+ | 高性能异步框架,自动生成 OpenAPI 文档 | +| **ORM** | SQLAlchemy | 2.0+ | 成熟的 Python ORM,异步支持 | +| **数据验证** | Pydantic | 2+ | 类型安全,与 FastAPI 深度集成 | +| **数据库迁移** | Alembic | 1.13+ | 版本化管理数据库变更 | +| **数据库** | MySQL (腾讯云) | 8.0+ | 稳定可靠的关系型数据库 | +| **缓存** | Redis | 7+ | 高性能缓存 + 消息队列 | +| **任务队列** | Celery | 5.3+ | 分布式异步任务处理 | +| **认证** | JWT | — | 无状态认证,支持 Token 刷新 | +| **容器化** | Docker + Docker Compose | 最新 | 简化部署,环境一致 | + +--- + +## 三、架构分层 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 客户端层 (Client Layer) │ +│ 浏览器 (Chrome/Edge) │ 移动端 │ 第三方 API 调用 │ +└───────────────────────┬─────────────────────────────────────┘ + │ HTTPS +┌───────────────────────▼─────────────────────────────────────┐ +│ 接入层 (Gateway Layer) │ +│ Nginx 反向代理 │ SSL 终止 │ 静态资源服务 │ 端口 8038/8037 │ +└───────────────────────┬─────────────────────────────────────┘ + │ +┌───────────────────────▼─────────────────────────────────────┐ +│ 前端应用层 (Frontend Layer) │ +│ Vue 3 SPA │ Pinia Store │ Vue Router │ Axios Client │ +│ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ +│ │ Views │ │Components│ │ Stores │ │ Utils │ │ +│ │ 页面组件 │ │ 公共组件 │ │ 状态管理 │ │ 工具/请求封装│ │ +│ └─────────┘ └──────────┘ └──────────┘ └──────────────┘ │ +└───────────────────────┬─────────────────────────────────────┘ + │ HTTP/JSON +┌───────────────────────▼─────────────────────────────────────┐ +│ 后端服务层 (Backend Layer) │ +│ FastAPI │ 中间件 │ 路由 │ 依赖注入 │ +│ ┌──────────────┐ ┌──────────────────┐ │ +│ │ API 路由层 │ │ 认证/授权中间件 │ │ +│ │ (api/v1/*) │ │ (JWT、CORS) │ │ +│ ├──────────────┤ ├──────────────────┤ │ +│ │ Service 层 │ │ 任务调度 │ │ +│ │ (业务逻辑) │ │ (Celery 任务) │ │ +│ ├──────────────┤ ├──────────────────┤ │ +│ │ Models 层 │ │ Schemas 层 │ │ +│ │ (ORM 定义) │ │ (数据验证) │ │ +│ └──────────────┘ └──────────────────┘ │ +└──────┬────────────────────────────┬─────────────────────────┘ + │ │ + ▼ ▼ +┌──────────────┐ ┌──────────────────┐ +│ MySQL 8.0 │ │ Redis 7 │ +│ (腾讯云) │ │ 缓存 / 任务队列 │ +│ 持久化存储 │ │ 会话 / 锁 │ +└──────────────┘ └──────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Celery Worker │ + │ 异步任务处理 │ + │ 文件处理 / 通知 │ + └──────────────────┘ +``` + +--- + +## 四、核心业务模块 + +### 1. 用户模块 +- **注册/登录**:邮箱 + 密码,JWT Token 发放 +- **Token 刷新**:Access Token (短期) + Refresh Token (长期) +- **个人中心**:信息查看与修改 + +### 2. 智能体模块 +- 智能体的创建、配置、发布、下架 +- 支持配置 LLM 参数、提示词、工具调用 +- 知识库关联 + +### 3. 知识库模块 +- 文档上传与解析(PDF、TXT、Markdown) +- 向量化存储(对接嵌入模型) +- 语义检索 + +### 4. 对话模块 +- 用户与智能体的实时对话 +- 上下文管理 +- 流式输出(SSE) + +--- + +## 五、数据流示例(对话请求) + +``` +1. 用户输入消息 + │ +2. Axios POST /api/v1/conversations/{id}/messages + │ +3. Nginx 代理到后端 8037 + │ +4. FastAPI 接收请求 + ├── JWT 中间件验证 Token + ├── 路由分发到对应 Handler + ├── Service 层处理业务逻辑 + │ ├── 查询对话历史 (MySQL) + │ ├── 检索相关知识 (知识库) + │ └── 调用 LLM API (异步) + │ +5. Celery 异步执行 LLM 调用 + ├── 进度通过 Redis Pub/Sub 推送 + └── SSE 推送给前端 + │ +6. 前端流式渲染响应内容 +``` + +--- + +## 六、安全设计 + +| 安全措施 | 说明 | +|:---------|:-----| +| JWT 认证 | Access Token 有效期短,Refresh Token 轮换 | +| 密码加密 | bcrypt 哈希存储 | +| CORS 配置 | 仅允许指定域名跨域访问 | +| 输入验证 | Pydantic 严格校验所有输入 | +| SQL 注入防护 | SQLAlchemy ORM 参数化查询 | +| HTTPS | Nginx 侧终止 SSL(生产环境) | + +--- + +## 七、扩展性 & 可维护性 + +- **水平扩展**:FastAPI 为无状态应用,可多实例部署 + Nginx 负载均衡 +- **异步任务**:Celery Worker 可独立扩展,处理大量异步任务 +- **模块化**:后端按业务拆分为 `modules/`,新增功能不影响现有模块 +- **数据库迁移**:Alembic 管理 Schema 变更,支持回滚 +- **前端组件化**:Vue 组件按功能拆解,提升复用性 + +--- + +> 📎 **相关文档**:[项目结构概览](./project-structure.md) | [开发指南](./development-guide.md) diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..a2f96cb --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,174 @@ +# 🤝 贡献指南 + +> **Contributing Guide** + +感谢您对天工智能体平台的兴趣!我们欢迎所有形式的贡献——代码、文档、Bug 反馈、功能建议等。 + +--- + +## 一、行为准则 + +请保持友善和尊重的沟通氛围。我们致力于为所有贡献者提供一个开放、包容的环境。 + +--- + +## 二、如何贡献 + +### 🐛 报告 Bug + +1. 前往 **[Issues](#)** 页面 +2. 搜索是否已有相同问题报告 +3. 创建新 Issue,使用 Bug 报告模板 +4. 请包含以下信息: + +``` +**标题**: [Bug] 简短描述问题 + +**描述**: +- 预期行为: +- 实际行为: +- 复现步骤: + 1. ... + 2. ... +- 环境信息: + - 浏览器/版本: + - 操作系统: + - API 版本: +- 截图/日志: +``` + +### ✨ 功能建议 + +1. 前往 **[Discussions](#)** 或 **Issues** +2. 使用功能建议模板 +3. 清晰地描述你想要的功能及其使用场景 + +### 💻 提交代码 + +#### 第一步:Fork & Clone + +```bash +# Fork 项目到你的 GitHub 账号 +# 然后 Clone 你的 Fork +git clone https://github.com//aiagent.git +cd aiagent + +# 添加 upstream 远程仓库 +git remote add upstream https://github.com/original/aiagent.git +``` + +#### 第二步:创建分支 + +```bash +# 从最新的 main 分支创建功能分支 +git checkout main +git pull upstream main +git checkout -b feat/your-feature-name +``` + +分支命名规范: + +| 分支类型 | 命名格式 | 示例 | +|:---------|:---------|:------| +| 新功能 | `feat/<描述>` | `feat/add-voice-input` | +| Bug 修复 | `fix/<描述>` | `fix/token-refresh-bug` | +| 文档 | `docs/<描述>` | `docs/update-api-ref` | +| 重构 | `refactor/<描述>` | `refactor/user-module` | + +#### 第三步:开发 + +```bash +# 确保你的代码 +# 1. 通过所有 lint 检查 +cd frontend && pnpm lint + +cd backend && flake8 app/ + +# 2. 通过所有测试 +cd frontend && pnpm test +cd backend && pytest + +# 3. 遵循代码规范(详见开发指南) +``` + +#### 第四步:提交 & 推送 + +```bash +# 使用 Conventional Commits 规范 +git add . +git commit -m "feat(agent): 新增智能体知识库关联功能" + +# 推送到你的 Fork +git push origin feat/your-feature-name +``` + +#### 第五步:发起 Pull Request + +1. 前往原始仓库的 **Pull Requests** 页面 +2. 点击 **New Pull Request** +3. 选择你的分支 → 目标分支 (`main`) +4. 填写 PR 模板: + +```markdown +## 描述 +简要描述本次变更内容 + +## 关联 Issue +Closes #123 + +## 变更类型 +- [ ] 新功能 (feat) +- [ ] Bug 修复 (fix) +- [ ] 文档更新 (docs) +- [ ] 代码重构 (refactor) +- [ ] 测试 (test) +- [ ] 构建/工具链 (chore) + +## 自测清单 +- [ ] 代码已 lint 通过 +- [ ] 测试已通过 +- [ ] 本地运行验证通过 +- [ ] 文档已更新(如需要) +``` + +--- + +## 三、Code Review 流程 + +| 阶段 | 说明 | +|:-----|:------| +| 1️⃣ 提交 PR | PR 状态变为 `Open` | +| 2️⃣ CI 检查 | 自动运行 Lint + 测试 | +| 3️⃣ Review 分配 | 维护者分配 Reviewer | +| 4️⃣ 代码审查 | Reviewer 逐行审查,提出修改建议 | +| 5️⃣ 修改 | 提交者根据反馈修改代码 | +| 6️⃣ 批准合并 | Review 通过后,由维护者合并到 `main` | + +### Review 关注点 + +- 代码正确性:功能是否符合预期 +- 代码风格:是否遵循项目规范 +- 性能影响:是否有明显性能问题 +- 安全漏洞:是否存在 SQL 注入 / XSS / 越权等隐患 +- 测试覆盖:是否包含必要的测试用例 + +--- + +## 四、文档贡献 + +文档和代码同等重要!如果你发现文档有误或缺失,欢迎贡献: + +- **API 文档**:位于 `docs/api-reference.md` +- **架构文档**:位于 `docs/architecture.md` +- **使用指南**:位于 `docs/quickstart.md` +- **代码注释**:修改代码时保持注释同步更新 + +--- + +## 五、开发环境 + +请参考 [开发指南](./development-guide.md) 搭建本地开发环境。 + +--- + +> 再次感谢您的贡献!🎉 diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md new file mode 100644 index 0000000..38ebf12 --- /dev/null +++ b/docs/deployment-guide.md @@ -0,0 +1,237 @@ +# 🚀 部署与运维指南 + +> **Deployment & Operations Guide** + +本文档面向运维人员,提供天工智能体平台的生产环境部署方案与日常运维指导。 + +--- + +## 一、环境要求 + +### 服务器最低配置 + +| 环境 | CPU | 内存 | 磁盘 | 网络 | +|:----|:----|:-----|:-----|:-----| +| 开发 | 2 核 | 4 GB | 20 GB SSD | 内网 | +| 预发布 | 4 核 | 8 GB | 50 GB SSD | 内网 | +| 生产 | 8 核 | 16 GB | 100 GB SSD | 公网 | + +### 依赖软件 + +| 组件 | 版本 | 安装方式 | +|:----|:-----|:---------| +| Docker | 24+ | `apt install docker.io` 或官方脚本 | +| Docker Compose | 2.20+ | `apt install docker-compose-plugin` | +| Nginx | 1.24+ | `apt install nginx` | +| MySQL | 8.0+ | **腾讯云数据库**(推荐)或自建 | +| Redis | 7+ | Docker 运行 | + +--- + +## 二、部署架构 + +``` + ┌──────────────┐ + │ 用户浏览器 │ + └──────┬───────┘ + │ HTTPS (443) + ┌──────▼───────┐ + │ Nginx │ + │ SSL 终止 │ + │ 反向代理 │ + └──┬───────┬───┘ + │ │ + ┌──────────▼─┐ ┌──▼──────────┐ + │ 前端静态资源 │ │ 后端 API │ + │ :8038 │ │ :8037 │ + │ (Vue SPA) │ │ (FastAPI) │ + └────────────┘ └──┬──────┬───┘ + │ │ + ┌────────▼─┐ ┌──▼────────┐ + │ MySQL │ │ Redis │ + │ (腾讯云) │ │ (Docker) │ + └──────────┘ └───────────┘ + │ + ┌──────▼──────┐ + │ Celery Worker│ + │ 异步任务处理 │ + └─────────────┘ +``` + +--- + +## 三、部署步骤 + +### Step 1:克隆代码 + +```bash +git clone /opt/aiagent +cd /opt/aiagent +``` + +### Step 2:配置环境变量 + +```bash +cd backend + +# 复制环境变量模板 +cp env.example .env + +# ⚠️ 编辑 .env 文件 +vim .env +``` + +#### 关键配置项 + +| 变量 | 说明 | 示例 | +|:-----|:-----|:------| +| `DATABASE_URL` | MySQL 连接字符串 | `mysql+asyncmy://user:pass@host:3306/aiagent?charset=utf8mb4` | +| `REDIS_URL` | Redis 连接地址 | `redis://localhost:6379/0` | +| `SECRET_KEY` | JWT 密钥(需随机生成) | `openssl rand -hex 32` | +| `ACCESS_TOKEN_EXPIRE_MINUTES` | Access Token 有效期 | `30` | +| `REFRESH_TOKEN_EXPIRE_DAYS` | Refresh Token 有效期 | `7` | +| `CORS_ORIGINS` | 允许的前端域名 | `["http://localhost:8038", "https://your-domain.com"]` | + +> ⚠️ **安全警告**:`SECRET_KEY` 必须使用强随机字符串,切勿硬编码在代码中。 + +### Step 3:使用 Docker Compose 部署 + +```bash +# 构建并启动所有服务 +docker-compose -f docker-compose.dev.yml up -d --build + +# 确认所有容器正常运行 +docker-compose ps +``` + +### Step 4:配置 Nginx(生产环境) + +参考 `nginx.conf`,关键配置如下: + +```nginx +server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; # HTTP 重定向到 HTTPS +} + +server { + listen 443 ssl http2; + server_name your-domain.com; + + # SSL 证书配置 + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # 前端静态资源 + location / { + proxy_pass http://localhost:8038; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # 后端 API + location /api/ { + proxy_pass http://localhost:8037; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + # SSE 支持 + proxy_set_header Connection ''; + proxy_buffering off; + proxy_cache off; + chunked_transfer_encoding on; + } +} +``` + +### Step 5:数据库迁移 + +```bash +docker exec -it aiagent-backend-1 alembic upgrade head +``` + +--- + +## 四、日常运维 + +### 服务管理 + +```bash +# 查看所有服务状态 +docker-compose ps + +# 查看日志(实时) +docker-compose logs -f + +# 查看特定服务日志 +docker-compose logs -f backend + +# 重启服务 +docker-compose restart backend + +# 滚动更新 +docker-compose pull +docker-compose up -d --build +``` + +### 数据库维护 + +```bash +# 备份数据库(使用腾讯云自动备份功能更佳) +mysqldump -h -u -p aiagent > backup_$(date +%Y%m%d).sql + +# 执行数据库迁移 +docker exec -it aiagent-backend-1 alembic upgrade head + +# 查看迁移历史 +docker exec -it aiagent-backend-1 alembic history +``` + +### 监控与告警 + +| 指标 | 说明 | 建议阈值 | +|:-----|:------|:---------| +| CPU 使用率 | 服务器 CPU | < 80% | +| 内存使用率 | 服务器内存 | < 85% | +| 磁盘使用率 | 数据盘 | < 80% | +| API 响应时间 | 接口平均延迟 | < 500ms | +| 错误率 | 5xx 错误占比 | < 1% | + +--- + +## 五、常见运维问题 + +| 问题 | 排查步骤 | +|:-----|:---------| +| ❌ 服务无法启动 | `docker-compose logs` 查看错误日志 | +| 🔌 数据库连接失败 | 检查 `.env` 中 `DATABASE_URL` 和网络连通性 | +| ⏰ API 响应缓慢 | 检查慢查询、Redis 缓存命中率、LLM API 延迟 | +| 🚫 502 Bad Gateway | Nginx 无法连接后端,检查后端服务是否正常运行 | +| 💾 磁盘空间不足 | `docker system prune -a` 清理无用镜像和容器 | +| 🔄 证书过期 | 使用 acme.sh 或 certbot 自动续签 SSL 证书 | + +--- + +## 六、备份策略 + +| 数据 | 备份频率 | 保留期限 | 方式 | +|:----|:---------|:---------|:-----| +| MySQL 数据库 | 每日 | 30 天 | 腾讯云自动备份或 mysqldump | +| 用户上传文件 | 实时同步 | — | 对象存储(如腾讯云 COS) | +| 配置文件 (.env) | 变更时 | — | Git 私有仓库 + 加密 | +| Docker 镜像 | 发布时 | 最近 5 个版本 | 镜像仓库 | + +--- + +## 七、安全加固 + +- ✅ **关闭不必要的端口**:仅开放 80/443 +- ✅ **使用 HTTPS**:Let's Encrypt 免费证书 +- ✅ **限制数据库访问**:仅允许应用服务器 IP 连接 +- ✅ **定期更新依赖**:`docker-compose pull` 拉取最新镜像 +- ✅ **日志审计**:使用 ELK 或腾讯云 CLS 聚合分析 +- ✅ **WAF 防护**:建议使用腾讯云 Web 应用防火墙 + +--- + +> 📎 **相关文档**:[快速开始指南](./quickstart.md) | [架构设计文档](./architecture.md) diff --git a/docs/development-guide.md b/docs/development-guide.md new file mode 100644 index 0000000..0b9c557 --- /dev/null +++ b/docs/development-guide.md @@ -0,0 +1,241 @@ +# 🛠️ 开发指南 + +> **Development Guide** + +本文档面向加入天工智能体平台开发的工程师,涵盖开发环境搭建、编码规范、测试与调试等内容。 + +--- + +## 一、开发环境搭建 + +### 前置要求 + +| 工具 | 版本 | 下载 | +|:----|:-----|:-----| +| Node.js | 18+ | [nodejs.org](https://nodejs.org/) | +| pnpm | 8+ | `npm install -g pnpm` | +| Python | 3.11+ | [python.org](https://python.org/) | +| Docker Desktop | 最新 | [docker.com](https://www.docker.com/products/docker-desktop/) | +| Git | 2.30+ | [git-scm.com](https://git-scm.com/) | +| IDE | VSCode / PyCharm | 推荐 VSCode + 对应插件 | + +### 推荐 VSCode 插件 + +- **Vue 开发**:Vue Language Features (Volar)、TypeScript Vue Plugin +- **Python 开发**:Python、Pylance +- **数据库**:MySQL (weijan-chen) +- **Redis**:Redis +- **Docker**:Docker +- **格式化**:Prettier、ESLint +- **Git**:GitLens、Git History + +--- + +## 二、代码规范 + +### 前端规范 + +#### 命名规范 + +| 类型 | 规范 | 示例 | +|:----|:-----|:-----| +| 组件名 | PascalCase | `UserProfile.vue` | +| 文件名 | kebab-case | `user-profile.vue` | +| 变量/函数 | camelCase | `getUserList()` | +| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` | +| Pinia Store | useXxxStore | `useUserStore` | +| CSS 类名 | kebab-case | `.user-avatar` | + +#### 组件结构 + +```vue + + + + + +``` + +#### Git Commit 规范 + +遵循 Conventional Commits 规范: + +``` +(): + + +``` + +| Type | 含义 | +|:-----|:------| +| `feat` | 新功能 | +| `fix` | 修复 Bug | +| `docs` | 文档变更 | +| `style` | 代码格式调整(不影响功能) | +| `refactor` | 重构代码 | +| `perf` | 性能优化 | +| `test` | 添加/修改测试 | +| `chore` | 构建/工具链变更 | + +```bash +# 示例 +feat(agent): 新增智能体知识库关联功能 +fix(user): 修复 Token 刷新失败问题 +docs(readme): 更新快速开始指南 +``` + +--- + +### 后端规范 + +#### 项目约定 + +| 项目 | 约定 | +|:-----|:------| +| 代码风格 | 遵循 PEP 8 | +| 类型注解 | 所有函数必须包含类型注解 | +| 注释 | 关键逻辑需写中文注释 | +| 路由命名 | `/{module}/{action}`,如 `/agents/{id}/start` | +| 响应格式 | 统一使用 `{ code, message, data }` 格式 | + +#### 响应格式 + +```python +# 成功响应 +{ + "code": 200, + "message": "success", + "data": { ... } +} + +# 错误响应 +{ + "code": 40001, + "message": "用户不存在", + "data": null +} +``` + +--- + +## 三、测试指南 + +### 前端测试 + +```bash +cd frontend + +# 运行单元测试 +pnpm test + +# 运行测试并生成覆盖率报告 +pnpm test:coverage +``` + +### 后端测试 + +```bash +cd backend + +# 运行所有测试 +pytest + +# 运行特定模块测试 +pytest tests/test_agent.py -v + +# 运行测试并生成覆盖率 +pytest --cov=app tests/ +``` + +--- + +## 四、调试技巧 + +### 前端 + +- **Vue Devtools**:浏览器安装 Vue Devtools 插件,可查看组件树、状态、事件 +- **Axios 拦截器**:在 `utils/request.ts` 中可打印请求/响应的完整信息 + +### 后端 + +- **FastAPI 自动文档**:访问 `http://localhost:8037/docs` 交互式调试 API +- **Pdb 调试**:在代码中插入 `import pdb; pdb.set_trace()` 启动断点调试 +- **日志查看**:后端日志输出到控制台,关键位置使用 `logger.info()` 打点 + +--- + +## 五、数据库变更流程 + +```bash +# 1. 修改 model 文件(如 app/models/user.py) + +# 2. 生成迁移脚本 +cd backend +alembic revision --autogenerate -m "add_user_avatar_field" + +# 3. 审查并执行迁移 +alembic upgrade head + +# 4. (如需回滚) +alembic downgrade -1 +``` + +> ⚠️ **注意**:生产环境的数据库迁移需先在预发布环境验证。 + +--- + +## 六、常见开发问题 + +| 问题 | 解决方案 | +|:-----|:---------| +| `pnpm install` 报错 | 检查 Node.js 版本,删除 `node_modules` 重新安装 | +| 后端热重载不生效 | 检查 `uvicorn` 是否带 `--reload` 参数 | +| Alembic 检测不到模型变更 | 确认模型已导入 `app/models/__init__.py` | +| CORS 错误 | 检查后端 CORS 配置中 `origins` 是否包含前端地址 | + +--- + +> 📎 **相关文档**:[快速开始指南](./quickstart.md) | [项目结构概览](./project-structure.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1d1da13 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,45 @@ +# 📚 天工智能体平台 — 文档中心 + +> **Tiangong AI Agent Platform Documentation** + +欢迎来到天工智能体平台的文档中心!本文档站为您提供平台的全方位参考资料。 + +--- + +## 📋 文档目录 + +| 文档 | 说明 | 适用对象 | +|:----|:-----|:---------| +| [📖 快速开始指南](./quickstart.md) | 5 分钟快速上手:安装、配置、启动 | 新用户、开发者 | +| [🏗️ 项目结构概览](./project-structure.md) | 前端/后端目录说明、核心模块介绍 | 开发者、维护者 | +| [🏛️ 架构设计文档](./architecture.md) | 系统架构图、技术选型说明、数据流 | 架构师、高级开发者 | +| [🛠️ 开发指南](./development-guide.md) | 开发环境搭建、编码规范、测试指南 | 开发者 | +| [🚀 部署与运维指南](./deployment-guide.md) | 环境要求、Docker 部署、配置说明 | DevOps、运维 | +| [🔌 API 参考](./api-reference.md) | RESTful API 端点说明、请求/响应格式 | 前端开发者、集成方 | +| [🤝 贡献指南](./contributing.md) | 如何参与贡献、代码审查流程 | 贡献者 | + +--- + +## 📌 文档规范 + +本文档遵循以下规范: + +| 规范项 | 标准 | +|:-------|:-----| +| 写作语言 | 中文为主,英文术语保留并附带翻译 | +| 代码块 | 标注语言类型,注释采用中文 | +| 路径表示 | Unix 风格 `/` 分隔 | +| 版本号 | 遵循语义化版本 [SemVer](https://semver.org/) | +| 链接 | 优先使用相对路径链接站内文档 | + +--- + +## 🆘 获取帮助 + +- **问题反馈**: [提交 Issue](#) +- **讨论交流**: [讨论区](#) +- **内部支持**: 联系平台团队 + +--- + +> 最后更新时间:2026 年 5 月 10 日 diff --git a/docs/project-structure.md b/docs/project-structure.md new file mode 100644 index 0000000..0c891c8 --- /dev/null +++ b/docs/project-structure.md @@ -0,0 +1,149 @@ +# 🏗️ 项目结构概览 + +> **Project Structure Overview** + +天工智能体平台采用前后端分离的架构,使用 pnpm + Vite + Vue 3 构建前端,FastAPI + SQLAlchemy 构建后端,通过 Celery + Redis 实现异步任务处理。 + +--- + +## 📁 顶层结构 + +``` +aiagent/ +├── frontend/ # 前端项目 +│ ├── src/ # 源码目录 +│ │ ├── views/ # 页面组件 +│ │ ├── components/ # 公共组件 +│ │ ├── stores/ # Pinia 状态管理 +│ │ ├── utils/ # 工具函数 +│ │ └── router/ # 路由配置 +│ ├── public/ # 静态资源 +│ ├── index.html # 入口 HTML +│ ├── vite.config.js # Vite 配置 +│ └── package.json # 前端依赖 +│ +├── backend/ # 后端项目 +│ ├── app/ # 应用主目录 +│ │ ├── modules/ # 业务模块 +│ │ ├── core/ # 核心功能(配置、安全、Celery) +│ │ ├── models/ # SQLAlchemy 数据模型 +│ │ ├── schemas/ # Pydantic 数据验证模型 +│ │ ├── api/ # API 路由定义 +│ │ ├── services/ # 业务逻辑层 +│ │ └── utils/ # 通用工具函数 +│ ├── tests/ # 测试用例 +│ ├── alembic/ # 数据库迁移 +│ ├── requirements.txt # Python 依赖 +│ └── Dockerfile # 后端 Docker 镜像 +│ +├── docker-compose.dev.yml # Docker Compose 开发配置 +├── nginx.conf # Nginx 反向代理配置 +├── .gitignore # Git 忽略规则 +└── README.md # 项目总览文档 +``` + +--- + +## 🖥️ 前端结构详解 + +### 目录路径约定 + +| 目录 | 用途 | +|:----|:------| +| `frontend/src/views/` | 页面级组件,按功能模块组织(如 `smart-assistant/`, `market/`) | +| `frontend/src/components/` | 可复用 UI 组件 | +| `frontend/src/stores/` | Pinia store,按模块管理全局状态 | +| `frontend/src/router/` | 路由配置,含权限控制守卫 | +| `frontend/src/utils/` | 通用工具函数(如 API 请求封装) | +| `frontend/public/` | 无需编译的静态资源 | + +--- + +## ⚙️ 后端结构详解 + +### 模块化组织 + +``` +backend/app/ +├── main.py # FastAPI 应用入口 +├── config.py # 配置管理(读取 .env) +├── core/ +│ ├── security.py # JWT 认证、密码加密 +│ ├── celery_app.py # Celery 异步任务配置 +│ ├── database.py # 数据库连接与会话管理 +│ └── exceptions.py # 全局异常处理 +├── modules/ +│ ├── user/ # 用户模块(注册、登录、个人信息) +│ ├── agent/ # 智能体模块 +│ ├── knowledge/ # 知识库模块 +│ └── conversation/ # 对话模块 +├── models/ # SQLAlchemy ORM 模型 +│ ├── user.py +│ ├── agent.py +│ └── knowledge.py +├── schemas/ # Pydantic 请求/响应模型 +│ └── ... +├── api/ # API 路由 +│ ├── v1/ # API v1 路由 +│ │ ├── users.py +│ │ ├── agents.py +│ │ └── ... +│ └── deps.py # 依赖注入(当前用户等) +└── services/ # 业务逻辑层 + ├── user_service.py + └── agent_service.py +``` + +--- + +## 🗄️ 数据流向 + +``` +浏览器 (Vue 3) + │ + ▼ HTTP/HTTPS +Nginx(反向代理,端口 8038/8037) + │ + ├──► 前端静态资源 ──── 返回 HTML/CSS/JS + │ + └──► 后端 API (FastAPI, 端口 8037) + │ + ├──► Redis(缓存 / 会话) + │ + ├──► MySQL(持久化数据) + │ + └──► Celery Worker(异步任务,如文件处理) +``` + +--- + +## 🔗 依赖关系 + +```mermaid +flowchart TD + subgraph 前端 + Vue[Vue 3 + Pinia] + Router[Vue Router] + Axios[Axios HTTP] + end + subgraph 后端 + FastAPI[FastAPI] + SA[SQLAlchemy] + Celery[Celery] + Redis[Redis] + end + subgraph 数据 + MySQL[(MySQL)] + end + + Vue --> Axios + Axios --> FastAPI + FastAPI --> SA --> MySQL + FastAPI --> Redis + Celery --> Redis + Celery --> MySQL +``` + +--- + +> 💡 **提示**:实际目录结构以最新代码为准,本文档仅供参考。 diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..471c4af --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,110 @@ +# 🚀 快速开始指南 + +> **Quick Start Guide** + +本文档指导您在 5 分钟内完成天工智能体平台的本地部署与启动。 + +--- + +## 📋 前置要求 + +| 组件 | 版本要求 | 说明 | +|:----|:---------|:-----| +| Node.js | 18+ | JavaScript 运行时 | +| pnpm | 8+ | 前端包管理器 | +| Python | 3.11+ | 后端运行时 | +| Docker & Docker Compose | 最新版 | 容器化部署(推荐) | +| MySQL | 8.0+ | 使用腾讯云数据库 | +| Redis | 7+ | 缓存与消息队列(可用 Docker) | + +--- + +## 🐳 使用 Docker Compose(推荐) + +### 启动服务 + +```bash +# 启动所有服务 +docker-compose -f docker-compose.dev.yml up -d + +# 查看实时日志 +docker-compose logs -f + +# 停止所有服务 +docker-compose down +``` + +### 服务端口 + +| 服务 | 端口 | 说明 | +|:----|:----|:------| +| 前端 | `8038` | 浏览器访问 `http://localhost:8038` | +| 后端 API | `8037` | API 服务 | +| API 文档 | `8037/docs` | Swagger UI 交互式文档 | +| Redis | `6379` | 缓存服务(Docker) | + +--- + +## 💻 本地开发环境 + +### 1️⃣ 前端启动 + +```bash +cd frontend +pnpm install +pnpm dev +``` + +前端开发服务器将在 `http://localhost:8038` 启动,支持热重载。 + +### 2️⃣ 后端启动 + +```bash +cd backend + +# 创建并激活 Python 虚拟环境 +python -m venv venv +# Windows +venv\Scripts\activate +# macOS / Linux +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt + +# 配置环境变量 +cp env.example .env +# ⚠️ 编辑 .env 文件,配置数据库连接等信息 + +# 运行数据库迁移 +alembic upgrade head + +# 启动开发服务器 +uvicorn app.main:app --reload + +# (新终端窗口)启动 Celery Worker +celery -A app.core.celery_app worker --loglevel=info +``` + +--- + +## ✅ 验证部署 + +1. 浏览器访问 `http://localhost:8038` → 看到登录/注册页面 +2. 浏览器访问 `http://localhost:8037/docs` → 看到 Swagger API 文档 +3. 尝试调用 `/health` 端点 → 返回 `{"status": "ok"}` + +--- + +## ❗ 常见问题 + +| 问题 | 可能原因 | 解决方案 | +|:----|:---------|:---------| +| 数据库连接失败 | `.env` 中数据库配置错误 | 检查 `DATABASE_URL` 配置 | +| 端口被占用 | 本地已有服务占用端口 | 修改 `docker-compose.dev.yml` 中的端口映射 | +| pnpm 安装失败 | Node.js 版本过低 | 升级 Node.js 至 18+ | +| 虚拟环境激活失败 | Python 未安装 | 确认 `python --version` >= 3.11 | + +--- + +> 遇到其他问题?请参考 [部署与运维指南](./deployment-guide.md) 或提交 [Issue](#)。 diff --git a/docs/startup-deploy/(红头)Windows服务器启动与重启唯一指南.md b/docs/startup-deploy/(红头)Windows服务器启动与重启唯一指南.md index 3ea2887..f019cbf 100644 --- a/docs/startup-deploy/(红头)Windows服务器启动与重启唯一指南.md +++ b/docs/startup-deploy/(红头)Windows服务器启动与重启唯一指南.md @@ -9,12 +9,13 @@ - 推荐端口: - 前端:`3001` - - 后端 API:`8037` + - 后端 API:`8037`(若被占用,一键启动会自动改用备用 **`8041`**) - Redis:`6379` +- **一键脚本目录**:均在仓库内 `scripts\startup\`,在根目录 `D:\aaa\aiagent` 下执行时路径为 `.\scripts\startup\*.ps1`(勿写成根目录下的 `.\start_aiagent.ps1`)。 - `backend/.env` 必须与实际 Redis 端口一致: - 推荐:`REDIS_URL=redis://localhost:6379/0` - 任何 `.env` / 依赖 / 工具代码变更后,至少重启: - - API + Celery(`restart_backend_celery.ps1`) + - API + Celery(`scripts\startup\restart_backend_celery.ps1`) - 若出现 `timeout of 30000ms exceeded`,优先检查: 1) Redis 是否可连 2) Celery Worker 是否在跑 @@ -33,47 +34,73 @@ cd D:\aaa\aiagent ### 1.1 一键启动(全套) ```powershell -powershell -ExecutionPolicy Bypass -File .\start_aiagent.ps1 +powershell -ExecutionPolicy Bypass -File .\scripts\startup\start_aiagent.ps1 ``` 默认目标: - 前端:`http://localhost:3001` -- 后端文档:`http://127.0.0.1:8037/docs` +- 后端文档:优先 `http://127.0.0.1:8037/docs`;若脚本提示已切到 **8041**,则打开 `http://127.0.0.1:8041/docs` - Redis:`127.0.0.1:6379` +一键启动会为每个进程新开 PowerShell 窗口(含 **Celery Worker** 与 **Celery Beat**)。若控制台一闪退出,请到该窗口里看报错。 + ### 1.2 一键停止(全套) ```powershell -powershell -ExecutionPolicy Bypass -File .\stop_aiagent.ps1 +powershell -ExecutionPolicy Bypass -File .\scripts\startup\stop_aiagent.ps1 ``` ### 1.3 仅重启后端 + Celery(最常用) ```powershell -powershell -ExecutionPolicy Bypass -File .\restart_backend_celery.ps1 +powershell -ExecutionPolicy Bypass -File .\scripts\startup\restart_backend_celery.ps1 ``` 适用场景:改了 `.env`、Python 依赖、内置工具实现、Agent 执行逻辑。 +说明:该脚本**固定**把 API 起在 **8037**。若本机 8037 被其它程序或异常状态占用,请先 `stop` 全套或释放端口后再执行。 + +### 1.4 仅重启前端(不动 Redis / 后端) + +适用:只改了前端代码或 Vite 配置,需要刷新 dev 进程。 + +1. 结束占用 **3001** 的进程(任务管理器结束对应 `node`/`pnpm`,或用资源监视器按端口查 PID 后结束)。 +2. 确认当前 API 端口(一般为 **8037**;若全套启动时曾提示改用 **8041**,则与之一致)。 +3. 新开 PowerShell: + +```powershell +cd D:\aaa\aiagent\frontend +$env:AIAGENT_API_PROXY='http://127.0.0.1:8037' # 若 API 在 8041,改为 ...8041 +pnpm dev --port 3001 +``` + +浏览器请优先用 **`http://localhost:3001`** 访问(Vite 常只绑 `[::1]`,用 `127.0.0.1:3001` 可能连不上,属正常现象)。 + --- ## 2. 标准“重启服务器”流程(推荐照抄) ```powershell cd D:\aaa\aiagent -powershell -ExecutionPolicy Bypass -File .\stop_aiagent.ps1 -powershell -ExecutionPolicy Bypass -File .\start_aiagent.ps1 +powershell -ExecutionPolicy Bypass -File .\scripts\startup\stop_aiagent.ps1 +powershell -ExecutionPolicy Bypass -File .\scripts\startup\start_aiagent.ps1 ``` -完成后立刻验证: +若 `start` 输出中出现 **`Port 8037 is occupied, switching to 8041`**,则本次 API 在 **8041**,下文健康检查与文档地址请改用 **8041**(前端新窗口里的 `AIAGENT_API_PROXY` 已指向该端口)。 + +完成后立刻验证(PowerShell 中 `curl` 多为 `Invoke-WebRequest` 别名,建议用 **`curl.exe`**): ```powershell netstat -ano | findstr :6379 netstat -ano | findstr :8037 +netstat -ano | findstr :8041 netstat -ano | findstr :3001 -curl http://127.0.0.1:8037/health +curl.exe -s -o NUL -w "%{http_code}" http://127.0.0.1:8037/health +curl.exe -s -o NUL -w "%{http_code}" http://127.0.0.1:8041/health ``` +若 API 实际在 **8041**,刚启动后 **10~20 秒内** `/health` 可能仍超时,属 uvicorn 拉起过程;前端 `3001` 可先就绪。 + --- ## 3. 本次故障复盘(学生作业管理助手超时) @@ -96,7 +123,7 @@ curl http://127.0.0.1:8037/health 1. 将 `backend/.env` 改为: - `REDIS_URL=redis://localhost:6379/0` 2. 执行: - - `powershell -ExecutionPolicy Bypass -File .\restart_backend_celery.ps1` + - `powershell -ExecutionPolicy Bypass -File .\scripts\startup\restart_backend_celery.ps1` 3. 验证: - `6379/8037/3001` 监听正常 - `/health` 返回 `200` @@ -127,7 +154,7 @@ Set-ExecutionPolicy -Scope Process Bypass ### 4.3 一键脚本不可用时的手动拉起(应急) -开 3~4 个终端: +开 **5** 个终端(与一键启动对齐;若暂不需要定时任务可省略 Beat): 1) Redis ```powershell @@ -156,6 +183,28 @@ $env:AIAGENT_API_PROXY='http://127.0.0.1:8037' pnpm dev --port 3001 ``` +5) Celery Beat(与一键 `start_aiagent.ps1` 对齐;定时任务依赖此项) +```powershell +cd D:\aaa\aiagent\backend +.\venv\Scripts\Activate.ps1 +python -m celery -A app.core.celery_app beat --loglevel=info +``` + +### 4.4 8037「占用」与 netstat 幽灵 PID(实践经验) + +部分 Windows 环境会出现: + +- `start_aiagent.ps1` 提示 **8037 被占用**,自动改用 **8041**; +- `netstat` 仍显示 `0.0.0.0:8037 LISTENING` 且带某一 **PID**,但 **`tasklist` 查不到该 PID**(或无法 `Stop-Process`)。 + +此时以 **`start` 脚本打印的实际端口为准**(多为 **8041**),用 **`http://127.0.0.1:8041/docs`** 与对应 `/health` 验证即可。若必须清理 **8037**,可本机执行 `netsh interface ipv4 show excludedportrange protocol=tcp` 查看是否与 Hyper-V / 动态端口保留冲突,或重启 Windows 后再全套启动。 + +### 4.5 `stop` 已正常但 `start` 报「8037 与 8041 均占用」 + +曾出现:`stop` 对后端端口打印 **SKIP**,端口检查却仍显示 **LISTEN**,随后 `start` 抛出双端口占用。根因是旧版 `stop_aiagent.ps1` 中 **`ForEach-Object` 内误用 `return`**(会从整个函数提前返回)以及内层 **`$pid` 变量名与 PowerShell 只读自动变量冲突**,导致未真正枚举监听 PID。 + +**当前仓库内 `scripts\startup\stop_aiagent.ps1` 已按上述问题修复**;若你本地脚本被回退或拷贝了旧版,请与仓库版本对齐后再执行全套重启。 + --- ## 5. OCR(上传图片识别)必查项 @@ -180,10 +229,11 @@ cd D:\aaa\aiagent\backend ## 6. 访问地址 -- 前端:`http://localhost:3001` -- 后端 API:`http://127.0.0.1:8037` -- 后端文档:`http://127.0.0.1:8037/docs` -- 健康检查:`http://127.0.0.1:8037/health` +- 前端:`http://localhost:3001`(推荐用 **localhost**,避免与 `[::1]` 绑定不一致) +- 后端 API(默认):`http://127.0.0.1:8037` +- 后端文档(默认):`http://127.0.0.1:8037/docs` +- 健康检查(默认):`http://127.0.0.1:8037/health` +- 若一键启动提示改用 **8041**:将上述三项中的端口改为 **8041** 即可。 --- diff --git a/docs/商业化落地计划.md b/docs/商业化落地计划.md new file mode 100644 index 0000000..75162c0 --- /dev/null +++ b/docs/商业化落地计划.md @@ -0,0 +1,304 @@ +# 天工智能体平台 — 商业化落地计划 + +> 从「能跑的项目」到「能卖的产品」的 4 周路线图。 + +--- + +## 一、现状基线 + +| 维度 | 当前状态 | 商业化差距 | +|------|---------|-----------| +| 核心引擎 | Agent ReAct + DAG工作流 + 多Agent编排 已就绪 | 稳定性未验证 | +| 测试覆盖 | 10 万行代码,仅 10 个测试文件 | 无回归保护,不敢迭代 | +| 安全 | 刚修复 35 个缺陷 | 需持续加固 | +| 文档 | 8 篇技术文档 | 缺产品/商业文档 | +| 模板 | 模板市场页面已有,内容为空 | 用户无法开箱即用 | +| 飞书集成 | 6 个 Bot 已运行 | 体验细节粗糙 | +| 知识进化 | 提取/检索/入库框架已有 | 未自动化运行 | +| 部署 | docker-compose 就绪 | 缺一键安装和一键升级 | +| 多租户 | 权限模型已有 | 未实现团队隔离 | + +**结论**:技术底座 70 分,产品化 20 分。差距在稳定性、易用性和商业化包装。 + +--- + +## 二、目标与时间线 + +``` +Week 1 ████████ 保稳定 核心测试 + 密码轮换 + 安全加固 +Week 2 ████████ 补功能 知识闭环 + 模板市场上架 +Week 3 ████████ 磨体验 飞书Bot打磨 + PR Review 模板演示 +Week 4 ████████ 可交付 多租户 + 一键部署 + 演示视频 + +4 周后目标: 一个稳定、安全、有模板、可演示、能交付的产品 +``` + +--- + +## 三、详细任务分解 + +### 🔴 第一优先级:保稳定(Week 1) + +#### 3.1 核心测试补齐 + +> 当前 10 万行代码仅 10 个测试文件,这是最大的技术债务。 + +| 测试模块 | 内容 | 覆盖风险点 | 工作量 | +|---------|------|-----------|--------| +| Agent ReAct 循环 | 正常对话 / 工具调用 / 预算熔断 / max_iterations 截断 | `core.py` 是全部 Agent 的运行时依赖 | 2天 | +| 工作流 DAG 执行 | 线性/条件分支/循环/审批 节点执行 | `workflow_engine.py` 6K行,改动连锁影响 | 2天 | +| 工具调用参数过滤 | LLM 生成错误参数名 → 过滤 → 正常执行 | 上次 type/keyword 冲突导致通知静默失败 | 1天 | +| 登录/认证 | JWT 签发/验证/过期 + 401 拦截 | 刚修完 FormData/JWT padding 多个 Bug | 1天 | +| 工具执行异常保护 | 工具抛异常 → Agent 不崩溃 → 返回 error JSON | 刚加 try/except,需验证 | 0.5天 | + +**验收标准**:`pytest --cov` 核心模块覆盖率 > 60%。 + +#### 3.2 安全加固 + +| 任务 | 操作 | 优先级 | +|------|------|--------| +| 数据库密码轮换 | 生成新密码 → 更新 `.env` → 确认 Celery/API/Worker 正常 | 紧急 | +| `.env` 模板化 | 提供 `.env.example` 不含真实密钥 | 高 | +| API 速率限制 | 对登录/Webhook 接口加 `slowapi` 限流 | 中 | +| HTTPS 强制 | Nginx 配置 HSTS + 证书自动续期 | 中 | + +#### 3.3 数据库模型迁移确认 + +> `init_db()` 刚补全 13 个缺失模型导入,需确认所有表可正常创建。 + +```bash +# 验证 +python -c "from app.core.database import init_db; init_db()" +# 检查无 ImportError,所有表已创建 +``` + +--- + +### 🟡 第二优先级:补功能(Week 2) + +#### 3.4 知识进化闭环 + +``` +现状: 知识提取器 + RAG检索器 + 知识库模型 —— 全部已有但没串起来 +目标: Agent 执行 → 自动提取经验 → 入库 → 下次检索 → Agent 变聪明 +``` + +| 任务 | 内容 | 工作量 | +|------|------|--------| +| 部署知识提取 Celery 定时任务 | 每小时从 `agent_execution_log` 提取高质量执行记录 → LLM 提炼知识条目 | 1天 | +| 补全知识仪表盘 | `KnowledgeDashboard.vue` 补知识增长曲线、复用次数排行、类别分布 | 1.5天 | +| 实测验证 | 选智能学习助手 Agent,跑 10 次对话 → 观察知识库增长 → 验证后续对话是否检索到之前经验 | 1天 | + +**验收标准**:Agent 执行 5 次后,第 6 次能检索到前 5 次提炼的知识。 + +#### 3.5 模板市场上架 + +> 这是「从开发者工具到商业产品」最关键的一步。非技术用户通过模板 5 分钟内看到价值。 + +| 模板名称 | 目标用户 | 核心配置 | 工作量 | +|---------|---------|---------|--------| +| 智能客服机器人 | 电商/SaaS 客服团队 | RAG知识库 + 飞书群Bot + 工单升级审批 | 0.5天 | +| 每日研发日报 | 研发经理/TL | 定时8点 + Git汇总 + CI状态 + 飞书推送 | 0.5天 | +| PR Code Review 助手 | 研发团队 | GitLab Webhook + Agent Review + 飞书通知 | 0.5天 | +| 面试调度助手 | HR/招聘 | 飞书日历 + Agent对话 + 日程创建 | 0.5天 | +| 竞品监控日报 | 市场/产品团队 | 定时搜索 + LLM分析 + 飞书推送 | 0.5天 | +| 自动化测试报告 | QA 团队 | Jenkins 集成 + 失败分析 + 周报生成 | 已有Agent | +| 新员工入职引导 | HR/行政 | RAG员工手册 + 定时学习任务 + 问答Bot | 0.5天 | +| 项目风险预警 | 项目经理 | Gitea/Jira 逾期扫描 + LLM评估 + 飞书告警 | 0.5天 | + +**验收标准**:模板市场页展示 8 个模板,每个模板可一键创建 Agent。 + +#### 3.6 模板即 Agent 的发布标准 + +每个模板必须包含: + +```yaml +模板结构: + - name: "智能客服机器人" + - description: "一句话价值主张" + - target_user: "谁会用" + - tags: ["客服", "飞书", "RAG"] + - agent_config: # 可一键创建的完整 Agent 配置 + system_prompt: "..." + tools: [...] + model: "deepseek-v4-flash" + max_iterations: 10 + - example_conversation: # 3 轮示例对话 + - user: "我的订单什么时候发货?" + agent: "正在查询..." + - screenshot: "模板效果截图" +``` + +--- + +### 🟢 第三优先级:磨体验(Week 3) + +#### 3.7 飞书 Bot 体验打磨 + +> 当前 6 个 Bot 已运行,但用户体验细节粗糙。 + +| 优化项 | 说明 | 工作量 | +|--------|------|--------| +| 消息反馈按钮 | 飞书消息卡片增加"👍有用 / 👎没用 / 🔄重新生成" | 1天 | +| 反馈数据接入学习 | `feedback_learner.py` 已有框架,接入飞书反馈事件 | 0.5天 | +| 对话历史摘要 | 长对话自动摘要压缩,避免超过 token 上限 | 1天 | +| Bot 状态指示 | 飞书群内显示"正在思考...""正在执行工具..."等状态 | 0.5天 | +| 错误友好提示 | Agent 崩溃时不显示技术错误,改成"我遇到了一点问题,请稍后再试" | 0.5天 | + +#### 3.8 演示 Demo 制作 + +| 内容 | 形式 | 时长 | +|------|------|------| +| 5 分钟创建智能客服 | 录屏:模板市场 → 一键创建 → 飞书群对话 | 3分钟 | +| PR Review 自动化 | 录屏:提交 PR → Webhook → Agent Review | 3分钟 | +| 工作流可视化设计 | 录屏:拖拽节点 → 配置LLM → 执行 → 看结果 | 4分钟 | + +#### 3.9 官网/落地页 + +``` +最小可用方案: + - 一句话: "基于飞书的 AI Agent 搭建平台,5 分钟创建你的第一个 AI 员工" + - 三个场景: 客服/研发/HR 的 Before/After 对比 + - 一个 CTA: "免费试用"按钮 → 飞书群二维码 + - 技术栈: 已有前端,加一个 LandingPage.vue 即可 +``` + +--- + +### 🔵 第四优先级:可交付(Week 4) + +#### 3.10 多租户最小可用方案 + +``` +目标: 支持 A公司/B团队 在同一个平台实例上隔离使用 +``` + +| 层次 | 方案 | 工作量 | +|------|------|--------| +| 数据隔离 | Agent/工作流/知识库 增加 `workspace_id` 字段 | 1天 | +| 飞书应用隔离 | 每个 workspace 绑定独立飞书应用配置 | 0.5天 | +| 权限隔离 | 已有 role 模型,扩展 workspace_admin / workspace_member | 1天 | +| 管理面板 | Admin 可查看/管理所有 workspace | 1天 | + +#### 3.11 一键部署脚本 + +```bash +# 目标: 客户拿到这个命令,10 分钟内部署完成 +curl -fsSL https://tiangong.ai/install.sh | bash + +# install.sh 做什么: +# 1. 检查 Docker/MySQL/Redis 环境 +# 2. 下载 docker-compose.prod.yml +# 3. 交互式配置(域名/飞书AppID/密码) +# 4. docker-compose up -d +# 5. 健康检查 + 打印访问地址 +``` + +| 交付物 | 说明 | 工作量 | +|--------|------|--------| +| `install.sh` | 交互式一键安装脚本 | 1天 | +| `upgrade.sh` | 增量升级脚本(保留数据) | 0.5天 | +| `uninstall.sh` | 清理脚本 | 0.5天 | +| 部署文档 | 图文教程,包含常见问题 | 0.5天 | + +#### 3.12 商业化定价 + +| 版本 | 价格 | 包含 | +|------|------|------| +| **社区版**(开源) | 免费 | 单租户、5个Agent、社区支持 | +| **专业版** | ¥19,800/年 | 多租户、50个Agent、飞书集成、邮件支持 | +| **企业版** | ¥98,000/年 | 不限Agent、私有部署、SLA 99.9%、专属客户经理 | +| **定制开发** | ¥2,000/人天 | 行业解决方案、系统集成、培训 | + +--- + +## 四、技术债务清理计划 + +| 债务 | 风险等级 | 计划 | +|------|---------|------| +| 测试覆盖率 < 5% | 高 | Week 1 核心模块到 60%,持续提升 | +| 工作流引擎 6K 行单体文件 | 中 | 按节点类型拆分为独立模块 | +| `workflow_engine.py` eval() | 中 | 替换为 `asteval` 或自定义表达式引擎 | +| 前端 4 处 v-html | 中 | 评估 DOMPurify 集成 | +| AgentChat localStorage | 低 | 迁移到 IndexedDB + 加密 | +| AsyncOpenAI 客户端生命周期 | 低 | 重构为长生命周期单例 | + +--- + +## 五、商业里程碑 + +``` +Week 4 ┃ ☐ 5 人外部测试用户完成首月试用 +Month 2 ┃ ☐ 第一个付费客户签约 +Month 3 ┃ ☐ 月活跃 Agent > 100 个 +Month 4 ┃ ☐ MRR (月经常性收入) > ¥10,000 +Month 6 ┃ ☐ 10 个付费客户,MRR > ¥50,000 +Month 12┃ ☐ 50 个付费客户,ARR > ¥1,000,000 +``` + +--- + +## 六、风险与应对 + +| 风险 | 概率 | 影响 | 应对 | +|------|------|------|------| +| LLM API 成本过高 | 中 | 高 | 默认使用 DeepSeek(低成本),缓存命中已有框架 | +| 飞书 API 变更 | 低 | 高 | 抽象飞书适配层,隔离平台逻辑 | +| 竞品(Dify/Coze)快速迭代 | 高 | 中 | 差异化飞书深度集成 + 国内部署优势 | +| 安全事件 | 中 | 高 | Week 1 密码轮换 + 后续渗透测试 | +| 用户增长不达预期 | 中 | 中 | 模板市场驱动 + 飞书开发者社区推广 | + +--- + +## 七、团队需求(最小配置) + +| 角色 | 人数 | 职责 | +|------|------|------| +| 全栈工程师 | 1-2人 | 后端 Python + 前端 Vue,核心功能开发 | +| 产品/运营 | 1人 | 模板制作、文档、社区、客户沟通 | +| 兼职安全顾问 | 按需 | 渗透测试、安全审计 | + +--- + +## 八、附录 + +### A. 已修复的 35 个缺陷分类 + +| 分类 | 数量 | 示例 | +|------|------|------| +| 安全漏洞 | 12 | JWT绕过、Webhook无认证、密码硬编码、路径遍历 | +| Agent 运行时 | 7 | 工具异常崩溃、budget泄漏、self_review误判 | +| 前端缺陷 | 12 | 登录失败、SSE消息重复、XSS风险、状态不一致 | +| 工作流引擎 | 4 | DAG环路、审批冲突、eval沙箱 | + +### B. 关键文件清单 + +| 文件 | 行数 | 角色 | +|------|------|------| +| `backend/app/agent_runtime/core.py` | 1,253 | Agent ReAct 运行时 | +| `backend/app/services/workflow_engine.py` | 6,188 | 工作流 DAG 引擎 | +| `backend/app/agent_runtime/orchestrator.py` | 988 | 多Agent编排器 | +| `backend/app/services/builtin_tools.py` | 6,290 | 54 个内置工具 | +| `backend/app/services/tool_registry.py` | 380 | 工具注册表 | +| `frontend/src/views/WorkflowDesigner.vue` | - | 可视化工作流设计器 | +| `frontend/src/views/AgentChat.vue` | - | Agent 对话页面 | +| `frontend/src/views/TemplateMarket.vue` | - | 模板市场 | + +### C. 外部依赖服务 + +| 服务 | 用途 | 备注 | +|------|------|------| +| MySQL 8.0 | 主数据库 | 生产: 腾讯云 CDB | +| Redis 7 | 缓存 + Celery Broker | - | +| Celery Worker | 异步任务执行 | 工作流/Agent 后台执行 | +| Celery Beat | 定时调度 | Cron 触发 Agent | +| DeepSeek API | 主力 LLM | 低成本,中文友好 | +| SiliconFlow | Embedding 向量化 | bce-embedding-base_v1 | +| 飞书开放平台 | Bot 消息 | 6 套独立应用 | +| Gitea | Issue/PR 集成 | 自建 Git 服务 | + +--- + +> **文档版本**: v1.0 +> **编制日期**: 2026-05-10 +> **下一步**: Week 1 启动核心测试 + 密码轮换 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..538476e --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,14 @@ +# 构建阶段 +FROM node:20-alpine AS builder +WORKDIR /app +COPY package.json pnpm-lock.yaml* ./ +RUN npm install -g pnpm && pnpm install --frozen-lockfile +COPY . . +RUN pnpm build + +# 运行阶段 +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..2fa64d4 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # 前端静态资源 + location / { + try_files $uri $uri/ /index.html; + } + + # API 代理到后端 + location /api/ { + proxy_pass http://backend:8037; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + proxy_connect_timeout 60s; + } + + # WebSocket 代理 + location /ws/ { + proxy_pass http://backend:8037; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # 健康检查 + location /health { + return 200 "OK"; + } +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index b265d54..5e84dc2 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -80,7 +80,12 @@ api.interceptors.response.use( // 处理401未授权 if (status === 401) { - localStorage.removeItem('token') + // 延迟导入避免循环依赖:通过 Pinia store 统一清理认证状态 + import('@/stores/user').then(({ useUserStore }) => { + useUserStore().logout() + }).catch(() => { + localStorage.removeItem('token') + }) router.push('/login') ElMessage.error(data?.message || '登录已过期,请重新登录') return Promise.reject(error) @@ -141,7 +146,9 @@ api.interceptors.response.use( data?.message || '服务器内部错误,请稍后重试' ElMessage.error(message) - console.error('服务器错误:', data) + if (import.meta.env.DEV) { + console.error('服务器错误:', data) + } return Promise.reject(error) } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 520a243..70b63fb 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -160,6 +160,18 @@ const router = createRouter({ component: () => import('@/views/DigitalEmployeeFactory.vue'), meta: { requiresAuth: true } }, + { + path: '/digital-twin', + name: 'digital-twin', + component: () => import('@/views/DigitalTwin.vue'), + meta: { requiresAuth: true } + }, + { + path: '/knowledge-dashboard', + name: 'knowledge-dashboard', + component: () => import('@/views/KnowledgeDashboard.vue'), + meta: { requiresAuth: true } + }, { path: '/goals/:id', name: 'goal-detail', diff --git a/frontend/src/stores/goal.ts b/frontend/src/stores/goal.ts index 111555c..5584b5e 100644 --- a/frontend/src/stores/goal.ts +++ b/frontend/src/stores/goal.ts @@ -142,6 +142,11 @@ export const useGoalStore = defineStore('goal', () => { return response.data } + const interactWithGoal = async (goalId: string, message: string) => { + const response = await api.post(`/api/v1/goals/${goalId}/interact`, { message }) + return response.data + } + // ─── Tasks ─── const fetchTaskTree = async (goalId: string) => { @@ -189,6 +194,7 @@ export const useGoalStore = defineStore('goal', () => { goals, currentGoal, taskTree, tasks, loading, fetchGoals, fetchGoal, createGoal, updateGoal, deleteGoal, decomposeGoal, startGoal, pauseGoal, resumeGoal, executeGoalAsync, replanGoal, + interactWithGoal, fetchTaskTree, fetchTasks, fetchTask, updateTask, approveTask, rejectTask, checkTaskDeps, } diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index b6e0792..1c81a2d 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -27,11 +27,11 @@ export const useUserStore = defineStore('user', () => { // 登录 const login = async (username: string, password: string) => { - const formData = new FormData() - formData.append('username', username) - formData.append('password', password) - - const response = await api.post('/api/v1/auth/login', formData, { + const params = new URLSearchParams() + params.append('username', username) + params.append('password', password) + + const response = await api.post('/api/v1/auth/login', params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } @@ -67,8 +67,9 @@ export const useUserStore = defineStore('user', () => { user.value = response.data return response.data } catch (error: any) { - // 如果401错误,清除token + // 如果401错误,清除token和user对象 if (error.response?.status === 401) { + user.value = null clearToken() } throw error diff --git a/frontend/src/views/AgentChat.vue b/frontend/src/views/AgentChat.vue index 15c9d52..7e876fb 100644 --- a/frontend/src/views/AgentChat.vue +++ b/frontend/src/views/AgentChat.vue @@ -95,7 +95,7 @@
{{ tc.function?.name || '?' }} - {{ Object.keys(JSON.parse(tc.function?.arguments || '{}')).length }} 个参数 + {{ safeParseArgCount(tc.function?.arguments) }} 个参数
@@ -286,7 +286,8 @@ function saveState() { chatMode: chatMode.value, orchestrateMode: orchestrateMode.value, orchestrateAgents: orchestrateAgents.value, - } + _savedAt: Date.now(), + } as any localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) } catch { /* quota exceeded, ignore */ } } @@ -296,6 +297,11 @@ function loadState(): ChatState | null { const raw = localStorage.getItem(STORAGE_KEY) if (!raw) return null const parsed = JSON.parse(raw) + // TTL 过期检查:24小时后自动清除聊天记录 + if (parsed._savedAt && Date.now() - parsed._savedAt > 24 * 3600 * 1000) { + localStorage.removeItem(STORAGE_KEY) + return null + } // 兼容旧格式:旧版本 messages 是数组,迁移为 Record if (Array.isArray(parsed.messages)) { const oldSessionId = parsed.sessionId || '' @@ -442,7 +448,7 @@ async function sendMessage() { let placeholderIdx = -1 streamingActive.value = false const abortController = new AbortController() - const streamTimeout = setTimeout(() => abortController.abort(), 60000) + const streamTimeout = setTimeout(() => abortController.abort(), 300000) // 5分钟超时,适应复杂 Agent 任务 try { const token = localStorage.getItem('token') || '' const resp = await fetch(streamEndpoint, { @@ -526,6 +532,7 @@ async function sendMessage() { approvalArgs.value = data.args || {} showApprovalDialog.value = true } else if (eventType === 'final') { + clearTimeout(streamTimeout) currentMsg.content = data.content || '' currentMsg.iterations = data.iterations_used || 0 currentMsg.tool_calls_made = data.tool_calls_made || 0 @@ -535,6 +542,7 @@ async function sendMessage() { streamingActive.value = false loading.value = false } else if (eventType === 'error') { + clearTimeout(streamTimeout) currentMsg.content = data.content || '' currentMsg.status = 'error' streamingActive.value = false @@ -598,6 +606,13 @@ function clearChat() { } function scrollToBottom() { nextTick(() => { if (messagesRef.value) messagesRef.value.scrollTop = messagesRef.value.scrollHeight }) } +function safeParseArgCount(args?: string): number { + if (!args) return 0 + try { + return Object.keys(JSON.parse(args)).length + } catch { return 0 } +} + function relativeTime(ts: number): string { const diff = Date.now() - ts if (diff < 60000) return '刚刚' @@ -661,7 +676,9 @@ function retryMessage(idx: number) { function renderMarkdown(text: string): string { if (!text) return '' - return text.replace(//g, '>') + // XSS 防护: 先转义 HTML 特殊字符,再对安全模式做 Markdown 渲染 + return text + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') .replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
') .replace(/`([^`]+)`/g, '$1') .replace(/\*\*([^*]+)\*\*/g, '$1') diff --git a/frontend/src/views/AgentConfig.vue b/frontend/src/views/AgentConfig.vue index fa2ff51..cb674d1 100644 --- a/frontend/src/views/AgentConfig.vue +++ b/frontend/src/views/AgentConfig.vue @@ -151,6 +151,76 @@
勾选的工具 Agent 可在运行时自主调用,不勾选则使用全部
+ + + 使用统计(近7天) + + + +
{{ agentStats.call_count || 0 }}
+
LLM 调用次数
+
+
+ + +
{{ fmtLatency(agentStats.avg_latency_ms) }}
+
平均响应延迟
+
+
+ + +
{{ agentStats.tool_call_count || 0 }}
+
工具调用次数
+
+
+ + +
{{ fmtTokens(agentStats.total_tokens) }}
+
Token 总用量
+
+
+
+ + + + 快速测试 + + {{ showTestPanel ? '收起' : '展开' }} + + +
+
+
+ 输入消息测试当前 Agent 配置效果(不会保存) +
+
+
{{ msg.role === 'user' ? '你' : agent?.name || 'Agent' }}
+
+
+
+
{{ agent?.name || 'Agent' }}
+
思考中...
+
+
+
+ + + +
+
@@ -170,7 +240,7 @@ + + diff --git a/frontend/src/views/GoalDetail.vue b/frontend/src/views/GoalDetail.vue index 220b974..a96db5f 100644 --- a/frontend/src/views/GoalDetail.vue +++ b/frontend/src/views/GoalDetail.vue @@ -123,17 +123,155 @@ + + + + + + + + + diff --git a/scripts/demo_digital_employee.py b/scripts/demo_digital_employee.py index 9106520..c6f1e70 100644 --- a/scripts/demo_digital_employee.py +++ b/scripts/demo_digital_employee.py @@ -18,7 +18,7 @@ import json import time import sys -BASE = "http://localhost:8037" +BASE = "http://localhost:8041" # ── 登录 ── def login(): diff --git a/scripts/startup/stop_aiagent.ps1 b/scripts/startup/stop_aiagent.ps1 index c5ed1d2..648aea3 100644 --- a/scripts/startup/stop_aiagent.ps1 +++ b/scripts/startup/stop_aiagent.ps1 @@ -5,12 +5,21 @@ Write-Host "== AIAgent stop ==" -ForegroundColor Cyan function Get-PidsListeningOnPort([int]$Port) { $pids = New-Object System.Collections.Generic.HashSet[int] try { - netstat -ano | ForEach-Object { - $ln = $_.Trim() - if ($ln -notmatch "LISTENING") { return } - # 匹配 :8037 或 :3001 等端口后的 LISTENING 行末 PID - if ($ln -match ":$Port\s+.*LISTENING\s+(\d+)\s*$") { - [void]$pids.Add([int]$Matches[1]) + # ForEach-Object 里写 return 会从「外层函数」返回,不能只跳过当前行。 + foreach ($raw in (netstat -ano)) { + $ln = $raw.Trim() + if ($ln -notmatch "LISTENING") { continue } + $norm = $ln -replace '\s+', ' ' + $parts = $norm.Split(' ') + if ($parts.Length -lt 5) { continue } + $local = $parts[1] + $ci = $local.LastIndexOf(':') + if ($ci -lt 0) { continue } + if ($local.Substring($ci + 1) -ne "$Port") { continue } + $pidStr = $parts[$parts.Length - 1] + if ($pidStr -match '^\d+$') { + $procId = [int]$pidStr + if ($procId -gt 4) { [void]$pids.Add($procId) } } } } catch { } @@ -20,21 +29,21 @@ function Get-PidsListeningOnPort([int]$Port) { function Stop-OnPorts([int[]]$ports, [string]$name) { $all = New-Object System.Collections.Generic.HashSet[int] foreach ($p in $ports) { - foreach ($pid in (Get-PidsListeningOnPort $p)) { - [void]$all.Add($pid) + foreach ($listenPid in (Get-PidsListeningOnPort $p)) { + [void]$all.Add($listenPid) } } if ($all.Count -eq 0) { Write-Host "[SKIP] ${name}: no listener on ports $($ports -join ',')" -ForegroundColor DarkGray return } - foreach ($pid in $all) { - if ($pid -le 4) { continue } + foreach ($procId in $all) { + if ($procId -le 4) { continue } try { - Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue - Write-Host "[OK] stopped ${name} PID=$pid" -ForegroundColor Green + Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue + Write-Host "[OK] stopped ${name} PID=$procId" -ForegroundColor Green } catch { - Write-Host "[WARN] failed to stop ${name} PID=$pid" -ForegroundColor Yellow + Write-Host "[WARN] failed to stop ${name} PID=$procId" -ForegroundColor Yellow } } }