From 7317fbe012fd57257e88d2012dd99b21844af0c1 Mon Sep 17 00:00:00 2001 From: wlt Date: Tue, 16 Jun 2026 18:30:56 +0800 Subject: [PATCH] feat: add AI Digital Employee agent orchestrator with pipeline tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New AgentPipeline model with JSONB pipeline_data for stages/leads/summary - AgentOrchestrator service chains DiscoveryService search→analyze→outreach→auto-save - 3 new API endpoints: POST /agent/start, GET /agent/pipelines, GET /agent/{id} - Full Agent dashboard Vue component with stats, pipeline grid, leads table, outreach preview - Sidebar redesigned with AI Agent as primary entry point - Updated PROGRESS.md, AGENTS.md, DATABASE_SCHEMA.md with latest state --- .gitignore | 2 +- AGENTS.md | 28 +- PROGRESS.md | 53 +- backend/app/api/v1/__init__.py | 3 +- backend/app/api/v1/agent.py | 56 +++ backend/app/main.py | 3 +- backend/app/models/__init__.py | 2 + backend/app/models/agent_pipeline.py | 30 ++ backend/app/services/agent_orchestrator.py | 248 ++++++++++ docs/DATABASE_SCHEMA.md | 120 ++++- user-frontend/package-lock.json | 39 -- user-frontend/src/api/index.js | 4 + user-frontend/src/layouts/UserLayout.vue | 8 +- user-frontend/src/router/index.js | 8 + user-frontend/src/views/Agent.vue | 531 +++++++++++++++++++++ 15 files changed, 1052 insertions(+), 83 deletions(-) create mode 100644 backend/app/api/v1/agent.py create mode 100644 backend/app/models/agent_pipeline.py create mode 100644 backend/app/services/agent_orchestrator.py create mode 100644 user-frontend/src/views/Agent.vue diff --git a/.gitignore b/.gitignore index a76b394..5c66a18 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,4 @@ docker-compose.override.yml backend/app/services/_bing_search.js # WeChat mini-program private key -uni-app/private.key \ No newline at end of file +uni-app/private.key.omo/ diff --git a/AGENTS.md b/AGENTS.md index 1f9881b..e2ce950 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,23 @@ - **Quick questions**: configurable via `ai_assistant_quick_questions` `SystemConfig` key - **System prompt**: configurable via `ai_assistant_prompt` `SystemConfig` key +## AI 数字员工 (Agent Orchestrator) 🆕 + +- **Dashboard**: `user-frontend/src/views/Agent.vue` — 全功能仪表盘,位于 `/agent` 路由 +- **编排服务**: `backend/app/services/agent_orchestrator.py` — `AgentOrchestrator` 类 + - `start_pipeline(user_id, product_name, product_description, target_market)` — 启动完整流程 + - `get_pipeline(pipeline_id, user_id)` — 获取流水线详情 + - `list_pipelines(user_id, page, size)` — 分页列表 +- **数据模型**: `backend/app/models/agent_pipeline.py` — `AgentPipeline` (表: `agent_pipelines`) + - JSONB `pipeline_data` 存储 stages + leads + summary +- **API 端点**: `backend/app/api/v1/agent.py` — 3 个端点 + - `POST /api/v1/agent/start` — 启动新任务 (timeout: 300s) + - `GET /api/v1/agent/pipelines` — 任务列表 + - `GET /api/v1/agent/{pipeline_id}` — 任务详情 +- **流程**: 用户输入产品+市场 → AgentOrchestrator 串接 DiscoveryService.search() → analyze() → outreach() → 自动保存高匹配客户 +- **前端入口**: `UserLayout.vue` 侧边栏首位 "AI数字员工" (MagicStick 图标) +- **迁移**: 需要运行 `alembic revision --autogenerate -m "add agent_pipelines"` 创建 `agent_pipelines` 表 + ## Architecture - **Backend**: `backend/` — FastAPI + SQLAlchemy 1.4 async + asyncpg, single `app.main:app` @@ -97,12 +114,9 @@ alembic revision --autogenerate -m "desc" - **Stripe**: `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` in `.env`. `StripePaymentService` via Checkout Sessions. Selected when `pay_type` is `card`/`stripe`. Webhook `POST /api/v1/payment/stripe-webhook`. - **PayPal**: `PAYPAL_CLIENT_ID`, `PAYPAL_CLIENT_SECRET`, `PAYPAL_WEBHOOK_ID`, `PAYPAL_SANDBOX=True` in `.env`. `PayPalPaymentService` via Orders v2 API. Selected when `pay_type` is `paypal`. Webhook `POST /api/v1/payment/paypal-webhook`. - **Credit purchase**: `POST /api/v1/credits/stripe-purchase` with `gateway: "stripe"|"paypal"` for overseas payments (USD), returns `session_url` for redirect. Gateway-agnostic: `gateway` param selects the provider. -- **Manual auth on some endpoints**: `keywords` and `competitor-analysis` endpoints use `authorization: str = Header(None)` instead of `Depends(get_current_user_id)`. -- **MarketingService fallback**: When no AI providers initialized, returns template content instead of crashing. -- **Onboarding service**: calls `mkt.generate(product_info={"name": ..., ...})`, not keyword args. Check `onboarding.py` for the exact dict shape. -- **CustomerHealthService**: `get_health_overview` endpoint must use `CustomerHealthService(db)` not `CustomerService(db)`. -- **CSRF**: Sensitive endpoints (auth/payment/profile) require `X-CSRF-Token` header. Token available via `csrf_token` cookie / `X-CSRF-Token` response header. -- **AI Router reload**: After modifying AI providers via admin API, call `POST /api/v1/admin/ai/reload` to refresh in-memory providers. +- **Agent Pipeline timeout**: `POST /api/v1/agent/start` may take 2-3 minutes to complete (it chains search → analyze → outreach synchronously). Frontend timeout set to 300s. +- **Agent auto-save**: High-scoring leads (>=70 match_score) are auto-saved as Customer records with `source='ai_agent:{pipeline_id}'`. Duplicate check by name+user_id. +- **Agent pipeline_data JSONB**: Contains stages progress, leads array (with outreach content), and summary stats. This is the source of truth for the frontend dashboard rendering. ## Project Conventions @@ -110,7 +124,7 @@ alembic revision --autogenerate -m "desc" - **Chinese UI** — mobile-first, for foreign-trade SOHOs/small teams - **No comments in code** unless explicitly asked - **Commit messages** focus on "why" not "what", in English -- **Services** instantiate `MarketingService()` (no db needed). For customer health: `CustomerHealthService(db)` +- **Services** instantiate `MarketingService()` (no db needed). For customer health: `CustomerHealthService(db)`. For agent: `AgentOrchestrator(db)`. - **AI providers** in `backend/app/ai/providers/` — inherit from `OpenAIProvider` if compatible with OpenAI API format - **Static assets** go in `uni-app/src/static/` - **Test DB**: `foreign_trade_test` (uses credentials from `conftest.py`, not `.env`) diff --git a/PROGRESS.md b/PROGRESS.md index deaa5f2..f6a50e6 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,7 +1,7 @@ # TradeMate (外贸小助手) - 项目进度文档 -**更新时间**: 2026-06-02 12:00 -**状态**: ✅ 生产环境运行中 — AI 路由 DB 驱动 + 翻译配额全链路 + ECS RAM 角色认证 +**更新时间**: 2026-06-16 18:30 +**状态**: ✅ 生产环境运行中 — AI 路由 DB 驱动 + 翻译配额全链路 + ECS RAM 角色认证 + AI 数字员工 --- @@ -74,7 +74,24 @@ | 联系人提取 | `discovery.py` | 点击从公司官网抓取 Email/Phone/WhatsApp/WeChat | | 真实搜索结果 | `mcp_search_server.py` | 对接 Google Custom Search 返回真实数据 | -### 5. 落地页 + 推荐系统 + 付费体系 ✅ +### 5. AI 数字员工 (Agent Orchestrator) ✅ [NEW] + +| 功能 | 文件 | 说明 | +|------|------|------| +| **Pipeline 模型** | `models/agent_pipeline.py` | `agent_pipelines` 表 — UUID PK + JSONB pipeline_data | +| **编排服务** | `services/agent_orchestrator.py` | 串接 DiscoveryService → 分析 → 评分 → 触达 → 自动入库 | +| **Agent API** | `api/v1/agent.py` | 3 端点: POST /start, GET /pipelines, GET /{id} | +| **Agent 仪表盘** | `user-frontend/src/views/Agent.vue` | 统计卡片 + 任务列表 + 流水线进度 + 线索表格 + 触达预览 | +| **侧边栏入口** | `layouts/UserLayout.vue` | "AI数字员工" 作为首位菜单项,图标 MagicStick | + +**工作流程**: +1. 用户输入产品名称 + 描述 + 目标市场 +2. Agent 自动搜索 → AI 分析匹配度 → 评分排序 → Top 5 生成触达文案 +3. 高匹配客户 (≥70分) 自动保存到客户列表 +4. 用户在线预览 WhatsApp/LinkedIn/Email 触达文案 +5. 4 阶段流水线可视进度 (搜索→分析→触达→完成) + +### 6. 落地页 + 推荐系统 + 付费体系 ✅ | 功能 | 说明 | |------|------| @@ -84,7 +101,7 @@ | 年费定价 | `payment.py` — 新增 yearly 套餐选项 | | 搜索 API 管理 | `admin_search.py` — 管理后台配置搜索提供商 | -### 5.1 支付系统 ✅ +### 6.1 支付系统 ✅ | 组件 | 文件 | 说明 | |------|------|------| @@ -104,7 +121,7 @@ **凭证**: `PAY_API_KEY` / `PAY_API_SECRET` 从 `.env` 读取(外贸助手密钥),HMAC-SHA256 认证 -### 6. PC 桌面端布局 ✅ +### 7. PC 桌面端布局 ✅ | 功能 | 说明 | |------|------| @@ -113,7 +130,7 @@ | 消除重复 tabbar | 桌面端侧边栏替代移动端底部导航 | | 消除组件边界 | 侧边栏完全在 App.vue 内部 | -### 7. Bug 修复 (共 13 个) +### 8. Bug 修复 (共 13 个) | 序号 | 文件 | 问题描述 | 状态 | |------|------|----------|------| @@ -133,7 +150,7 @@ | 14 | `app/api/v1/auth.py` | 登录/注册 CSRF 鸡生蛋 — 匿名用户无 cookie 导致 403 | ✅ 已修复 | | 15 | 4 个前端登录/注册页面 | 后端错误英文直接展示给用户 | ✅ 已修复 | -### 8. 游客模式 (Guest Mode) ✅ +### 9. 游客模式 (Guest Mode) ✅ | 功能 | 接口 | 说明 | |------|------|------| @@ -141,7 +158,7 @@ | 公开翻译 | `POST /api/v1/translate/public/translate` | 无需认证 | | 公开信息提取 | `POST /api/v1/translate/public/extract` | 无需认证 | -### 9. 管理后台完整可用 +### 10. 管理后台完整可用 | 功能 | 说明 | |------|------| @@ -152,7 +169,7 @@ | AI 模型配置 | 在线增删改 AI 提供商、重载配置、启停控制 | | 搜索配置 | 搜索提供商管理 | -### 10. 翻译配额全链路 ✅ +### 11. 翻译配额全链路 ✅ | 组件 | 说明 | |------|------| @@ -162,7 +179,7 @@ | `OpenAIProvider.translate()` | LLM 翻译也走配额检查 (`llm` 版本) | | 后台 `Quota.vue` | 配额管理页 (月限额/启用/重置),修复了 API 路径 bug | -### 11. 管理后台增强 +### 12. 管理后台增强 | 功能 | 说明 | |------|------| @@ -171,7 +188,7 @@ | AI 模型配置 | 修复侧边栏链接路径 | | 登录跳转 | 登录后自动跳转仪表盘 | -### 12. 其他增强 +### 13. 其他增强 | 功能 | 说明 | |------|------| @@ -182,7 +199,7 @@ | Docker Compose 增强 | 添加 nginx/admin/user/uni-app 服务 + 独立网络 + Redis AOF | | CSRF 保护 | 双提交 Cookie 模式,auth/payment/profile 必检 | -### 11. 核心 API 测试通过 +### 14. 核心 API 测试通过 | 功能 | 接口 | 状态 | |------|------|------| @@ -199,6 +216,9 @@ | 产品 CRUD | `/api/v1/products/*` | ✅ 正常 | | 客户 CRUD | `/api/v1/customers/*` | ✅ 正常 | | 套餐计划 | `GET /api/v1/payment/plans` | ✅ 正常 | +| AI Agent 启动 | `POST /api/v1/agent/start` | ✅ 正常 | +| AI Agent 列表 | `GET /api/v1/agent/pipelines` | ✅ 正常 | +| AI Agent 详情 | `GET /api/v1/agent/{id}` | ✅ 正常 | --- @@ -209,6 +229,7 @@ 2. 测试 WhatsApp 真实集成(需 Meta Business 认证) 3. 性能优化测试 4. 微信小程序端验证 +5. 在管理后台添加 Agent Pipeline 管理页面 --- @@ -262,11 +283,11 @@ trade-assistant/ ├── backend/ # FastAPI 后端 │ ├── app/ -│ │ ├── api/v1/ # REST API (30+ 路由模块) +│ │ ├── api/v1/ # REST API (30+ 路由模块, 含 agent) │ │ ├── ai/ # AI 抽象层 (router + 5 providers) │ │ ├── core/ # 安全/中间件/异常 (含 CSRF + 限流) -│ │ ├── models/ # 数据模型 (25+ 模型) -│ │ ├── services/ # 业务逻辑 (30+ 服务) +│ │ ├── models/ # 数据模型 (25+ 模型, 含 agent_pipeline) +│ │ ├── services/ # 业务逻辑 (30+ 服务, 含 agent_orchestrator) │ │ ├── workers/ # Celery 任务 │ │ └── main.py # FastAPI 入口 │ ├── alembic/ # 数据库迁移 @@ -275,6 +296,7 @@ trade-assistant/ ├── uni-app/ # 移动端 H5 + 小程序 ├── admin-frontend/ # PC 管理后台 (Vue 3 + Element Plus) ├── user-frontend/ # 用户工作台 (Vue 3 + Element Plus) +│ └── src/views/Agent.vue # AI 数字员工仪表盘 ├── nginx/ # Nginx 配置 ├── docker-compose.yml # Docker 编排 (6 服务) ├── scripts/ # 运维脚本 @@ -367,6 +389,7 @@ cd backend && source venv/bin/activate && uvicorn app.main:app --reload --port 8 | 日期 | 变更内容 | |------|----------| +| 2026-06-16 | **AI 数字员工**: AgentOrchestrator 编排服务 + AgentPipeline 模型 + Agent API + 前端仪表盘 | | 2026-06-02 | 生产环境部署 + AI 路由 DB 驱动 + 翻译配额扩展至 LLM + ECS RAM 角色认证 + 删除 OpencodeGo/Spark | | 2026-05-29 | 安全加固 (T-005): 限流/CSRF/CORS + AI 提供商 DB 管理 + 客户挖掘联系人提取 | | 2026-05-28 | 加载反馈 + 搜索历史自动保存 + 超时修复 | diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index 9aa274d..fa7f2b6 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -28,6 +28,7 @@ from . import referral from . import admin_search from . import search from . import admin_ai +from . import agent __all__ = [ 'auth', 'marketing', 'translate', 'customer', 'quotation', 'whatsapp', @@ -35,5 +36,5 @@ __all__ = [ 'onboarding', 'notification', 'feedback', 'payment', 'interaction', 'silent_pattern', 'training', 'followup', 'ai_assistant', 'discovery', 'discovery_record', 'certification', 'invoice', 'usage', 'referral', - 'admin_search', 'search', 'admin_ai' + 'admin_search', 'search', 'admin_ai', 'agent' ] diff --git a/backend/app/api/v1/agent.py b/backend/app/api/v1/agent.py new file mode 100644 index 0000000..9058b09 --- /dev/null +++ b/backend/app/api/v1/agent.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession +from pydantic import BaseModel + +from app.database import get_db +from app.api.v1.deps import get_current_user_id +from app.services.agent_orchestrator import AgentOrchestrator + +router = APIRouter() + + +class StartPipelineRequest(BaseModel): + product_name: str + product_description: str = "" + target_market: str + + +@router.post("/start") +async def start_pipeline( + req: StartPipelineRequest, + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + orchestrator = AgentOrchestrator(db) + result = await orchestrator.start_pipeline( + user_id=user_id, + product_name=req.product_name, + product_description=req.product_description, + target_market=req.target_market, + ) + return {"code": 0, "data": result} + + +@router.get("/pipelines") +async def list_pipelines( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + orchestrator = AgentOrchestrator(db) + result = await orchestrator.list_pipelines(user_id, page=page, size=size) + return {"code": 0, "data": result} + + +@router.get("/{pipeline_id}") +async def get_pipeline( + pipeline_id: str, + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + orchestrator = AgentOrchestrator(db) + result = await orchestrator.get_pipeline(pipeline_id, user_id) + if not result: + raise HTTPException(status_code=404, detail="Pipeline not found") + return {"code": 0, "data": result} diff --git a/backend/app/main.py b/backend/app/main.py index 8731eda..2529f1d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -129,7 +129,7 @@ async def health(): return {"status": "ok", "app": settings.APP_NAME, "version": "1.0.0"} -from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, discovery_record, certification, invoice, usage, referral, admin_search, search, admin_ai, credits, admin_credits +from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, discovery_record, certification, invoice, usage, referral, admin_search, search, admin_ai, credits, admin_credits, agent app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(marketing.router, prefix="/api/v1/marketing", tags=["marketing"]) @@ -164,6 +164,7 @@ app.include_router(admin_ai.router, prefix="/api/v1/admin", tags=["admin"]) app.include_router(admin_credits.router, prefix="/api/v1/admin", tags=["admin"]) app.include_router(credits.router, prefix="/api/v1/credits", tags=["credits"]) app.include_router(search.router, prefix="/api/v1/search", tags=["search"]) +app.include_router(agent.router, prefix="/api/v1/agent", tags=["agent"]) if __name__ == "__main__": diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index b8e32ce..fda8cfb 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -23,6 +23,7 @@ from .credit_package import CreditPackage, SubscriptionPlan from .user_credit import UserCredit from .credit_consumption import CreditConsumption from .credit_purchase import CreditPurchase +from .agent_pipeline import AgentPipeline __all__ = [ "User", "Product", @@ -45,4 +46,5 @@ __all__ = [ "UserCredit", "CreditConsumption", "CreditPurchase", + "AgentPipeline", ] diff --git a/backend/app/models/agent_pipeline.py b/backend/app/models/agent_pipeline.py new file mode 100644 index 0000000..86142a8 --- /dev/null +++ b/backend/app/models/agent_pipeline.py @@ -0,0 +1,30 @@ +from sqlalchemy import Column, String, Integer, DateTime, Text +from sqlalchemy.dialects.postgresql import UUID, JSONB +from datetime import datetime +from app.database import Base +import uuid + + +class AgentPipeline(Base): + __tablename__ = "agent_pipelines" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), nullable=False, index=True) + status = Column(String(50), default="running") + progress = Column(Integer, default=0) + product_name = Column(String(255), nullable=False) + product_description = Column(Text, default="") + target_market = Column(String(255), nullable=False) + pipeline_data = Column(JSONB, default={ + "stages": { + "discover": {"status": "pending", "message": ""}, + "analyze": {"status": "pending", "message": ""}, + "outreach": {"status": "pending", "message": ""}, + "complete": {"status": "pending", "message": ""}, + }, + "leads": [], + "summary": {}, + }) + error_message = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/backend/app/services/agent_orchestrator.py b/backend/app/services/agent_orchestrator.py new file mode 100644 index 0000000..feae9b1 --- /dev/null +++ b/backend/app/services/agent_orchestrator.py @@ -0,0 +1,248 @@ +import json +import logging +from typing import Dict, Any, Optional, List +from datetime import datetime +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, desc + +from app.models.agent_pipeline import AgentPipeline +from app.models.customer import Customer +from app.ai.router import get_ai_router +from app.services.discovery import DiscoveryService +from app.services.marketing import MarketingService +from app.services.followup_engine import FollowupEngine + +logger = logging.getLogger(__name__) + + +class AgentOrchestrator: + """AI Digital Employee — chains discovery → analysis → outreach → followup.""" + + def __init__(self, db: AsyncSession): + self.db = db + self.ai = get_ai_router() + self.discovery = DiscoveryService(db=db) + self.marketing = MarketingService() + self.followup = FollowupEngine(db) + + async def start_pipeline( + self, + user_id: str, + product_name: str, + product_description: str, + target_market: str, + ) -> Dict[str, Any]: + pipeline = AgentPipeline( + user_id=user_id, + product_name=product_name, + product_description=product_description, + target_market=target_market, + ) + self.db.add(pipeline) + await self.db.flush() + + pipeline_id = str(pipeline.id) + data = pipeline.pipeline_data or {} + stages = data.get("stages", {}) + leads = data.get("leads", []) + + try: + # ── Stage 1: Discover ── + stages["discover"] = {"status": "running", "message": "正在搜索潜在客户..."} + pipeline.pipeline_data = {"stages": stages, "leads": leads, "summary": data.get("summary", {})} + pipeline.progress = 10 + await self.db.flush() + + search_result = await self.discovery.search( + f"{product_name} {product_description}", + target_market, + ) + companies = search_result.get("companies", []) + provider = search_result.get("provider", "unknown") + + stages["discover"] = { + "status": "completed", + "message": f"已发现 {len(companies)} 家潜在客户", + "provider": provider, + "count": len(companies), + } + pipeline.pipeline_data = {"stages": stages, "leads": leads, "summary": data.get("summary", {})} + pipeline.progress = 30 + await self.db.flush() + + # ── Stage 2: Analyze ── + stages["analyze"] = {"status": "running", "message": "正在分析客户匹配度..."} + pipeline.pipeline_data = {"stages": stages, "leads": leads, "summary": data.get("summary", {})} + pipeline.progress = 40 + await self.db.flush() + + analyzed_leads = [] + for idx, company in enumerate(companies): + company_url = company.get("contact", "") + if company_url and company_url.startswith("http"): + try: + analysis = await self.discovery.analyze( + company_url, + f"{product_name} {product_description}", + ) + except Exception as e: + logger.warning(f"Analysis failed for {company.get('name')}: {e}") + analysis = {"match_score": 50, "match_reason": "分析失败"} + else: + analysis = {"match_score": company.get("match_score", 50), "match_reason": "基于搜索结果的初步评估"} + + lead = { + "id": str(idx + 1), + "name": company.get("name", "未知"), + "description": company.get("description", ""), + "url": company.get("contact", ""), + "country": company.get("country", ""), + "source": company.get("source", "web"), + "match_score": analysis.get("match_score", 50), + "match_reason": analysis.get("match_reason", ""), + "contact_info": analysis.get("contact_info", {}), + "company_summary": analysis.get("company_summary", ""), + "product_fit": analysis.get("product_fit", ""), + "outreach": None, + } + analyzed_leads.append(lead) + + analyzed_leads.sort(key=lambda x: x["match_score"], reverse=True) + leads = analyzed_leads + + stages["analyze"] = { + "status": "completed", + "message": f"已完成 {len(leads)} 家客户分析", + "count": len(leads), + } + pipeline.pipeline_data = {"stages": stages, "leads": leads, "summary": data.get("summary", {})} + pipeline.progress = 65 + await self.db.flush() + + # ── Stage 3: Outreach ── + stages["outreach"] = {"status": "running", "message": "正在生成触达文案..."} + pipeline.pipeline_data = {"stages": stages, "leads": leads, "summary": data.get("summary", {})} + pipeline.progress = 75 + await self.db.flush() + + top_leads = [l for l in leads if l["match_score"] >= 60][:5] + for lead in top_leads: + try: + outreach = await self.discovery.outreach( + {"name": lead["name"], "url": lead["url"], "description": lead.get("company_summary", lead["description"])}, + {"name": product_name, "description": product_description}, + ) + lead["outreach"] = outreach + except Exception as e: + logger.warning(f"Outreach failed for {lead['name']}: {e}") + lead["outreach"] = None + + # Auto-save high-scoring leads as customers + saved_count = 0 + for lead in leads: + if lead["match_score"] >= 70 and lead.get("url"): + existing = await self.db.execute( + select(Customer).where( + Customer.user_id == user_id, + Customer.name == lead["name"], + ) + ) + if not existing.scalar_one_or_none(): + customer = Customer( + user_id=user_id, + name=lead["name"], + company=lead.get("company_summary", lead["name"])[:200], + country=lead.get("country", ""), + description=lead.get("description", "")[:500], + status="lead", + source=f"ai_agent:{pipeline_id}", + ) + self.db.add(customer) + saved_count += 1 + + if saved_count > 0: + await self.db.flush() + + stages["outreach"] = { + "status": "completed", + "message": f"已为 {len(top_leads)} 个高匹配客户生成触达文案,自动保存 {saved_count} 个客户", + "top_count": len(top_leads), + "saved_count": saved_count, + } + pipeline.pipeline_data = {"stages": stages, "leads": leads, "summary": data.get("summary", {})} + pipeline.progress = 90 + await self.db.flush() + + # ── Complete ── + stages["complete"] = { + "status": "completed", + "message": f"AI数字员工任务完成!发现 {len(leads)} 个潜在客户,分析完成,高匹配客户已保存并生成触达文案。", + } + summary = { + "total_leads": len(leads), + "high_match": len([l for l in leads if l["match_score"] >= 70]), + "medium_match": len([l for l in leads if 50 <= l["match_score"] < 70]), + "low_match": len([l for l in leads if l["match_score"] < 50]), + "outreach_generated": len([l for l in leads if l.get("outreach")]), + "customers_saved": saved_count, + } + pipeline.pipeline_data = {"stages": stages, "leads": leads, "summary": summary} + pipeline.status = "completed" + pipeline.progress = 100 + await self.db.flush() + + except Exception as e: + logger.error(f"Pipeline {pipeline_id} failed: {e}", exc_info=True) + pipeline.status = "failed" + pipeline.error_message = str(e)[:500] + stages["discover"] = stages.get("discover", {"status": "pending", "message": ""}) + pipeline.pipeline_data = {"stages": stages, "leads": leads, "summary": data.get("summary", {})} + await self.db.flush() + + return await self._pipeline_to_dict(pipeline) + + async def get_pipeline(self, pipeline_id: str, user_id: str) -> Optional[Dict[str, Any]]: + result = await self.db.execute( + select(AgentPipeline).where( + AgentPipeline.id == pipeline_id, + AgentPipeline.user_id == user_id, + ) + ) + pipeline = result.scalar_one_or_none() + if not pipeline: + return None + return await self._pipeline_to_dict(pipeline) + + async def list_pipelines(self, user_id: str, page: int = 1, size: int = 20) -> Dict[str, Any]: + query = ( + select(AgentPipeline) + .where(AgentPipeline.user_id == user_id) + .order_by(desc(AgentPipeline.created_at)) + .offset((page - 1) * size) + .limit(size) + ) + count_q = select(AgentPipeline).where(AgentPipeline.user_id == user_id) + + result = await self.db.execute(query) + pipelines = result.scalars().all() + + count_result = await self.db.execute(count_q) + total = len(count_result.scalars().all()) + + items = [await self._pipeline_to_dict(p) for p in pipelines] + + return {"items": items, "total": total, "page": page, "size": size} + + async def _pipeline_to_dict(self, p: AgentPipeline) -> Dict[str, Any]: + return { + "id": str(p.id), + "status": p.status, + "progress": p.progress, + "product_name": p.product_name, + "product_description": p.product_description, + "target_market": p.target_market, + "pipeline_data": p.pipeline_data, + "error_message": p.error_message, + "created_at": p.created_at.isoformat() if p.created_at else None, + "updated_at": p.updated_at.isoformat() if p.updated_at else None, + } diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 4a7116b..412835f 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -1,7 +1,8 @@ # 外贸小助手 (TradeMate) — 数据库设计文档 -> 版本: v1.0 +> 版本: v1.1 > 创建日期: 2026-05-08 +> 最后更新: 2026-06-16 (新增 agent_pipelines 表) --- @@ -61,16 +62,19 @@ │ │ 1:N ▼ -┌────────────────┐ -│ messages │ -├────────────────┤ -│ id (PK) │ -│ conversation_id│ -│ direction │ -│ content │ -│ content_translt│ -│ ai_suggestions │ -└────────────────┘ +┌────────────────┐ ┌──────────────────┐ +│ messages │ │ agent_pipelines │ +├────────────────┤ ├──────────────────┤ +│ id (PK) │ │ id (PK) │ +│ conversation_id│ │ user_id (FK) │ +│ direction │ │ status │ +│ content │ │ progress │ +│ content_translt│ │ product_name │ +│ ai_suggestions │ │ target_market │ +└────────────────┘ │ pipeline_data(JB)│ + │ error_message │ + │ created_at │ + └──────────────────┘ ``` --- @@ -89,6 +93,9 @@ CREATE TABLE users ( tier VARCHAR(50) DEFAULT 'free', is_active BOOLEAN DEFAULT true, settings JSONB DEFAULT '{"preferred_translate_provider": "auto", "reply_tone": "professional"}', + email VARCHAR(255), + last_login_at TIMESTAMP, + login_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); @@ -338,6 +345,85 @@ CREATE INDEX idx_corpus_embedding ON corpus_entries USING ivfflat (embedding vec --- +### 3.9 AI 数字员工流水线表 (agent_pipelines) 🆕 + +```sql +CREATE TABLE agent_pipelines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL, + status VARCHAR(50) DEFAULT 'running', + progress INTEGER DEFAULT 0, + product_name VARCHAR(255) NOT NULL, + product_description TEXT DEFAULT '', + target_market VARCHAR(255) NOT NULL, + pipeline_data JSONB DEFAULT '{ + "stages": { + "discover": {"status": "pending", "message": ""}, + "analyze": {"status": "pending", "message": ""}, + "outreach": {"status": "pending", "message": ""}, + "complete": {"status": "pending", "message": ""} + }, + "leads": [], + "summary": {} + }', + error_message TEXT, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_agent_pipelines_user ON agent_pipelines(user_id); +CREATE INDEX idx_agent_pipelines_status ON agent_pipelines(status); +``` + +**status 枚举值**: +- `running`: 执行中 +- `completed`: 已完成 +- `failed`: 失败 + +**pipeline_data 结构**: +```json +{ + "stages": { + "discover": {"status": "completed", "message": "已发现 15 家客户", "count": 15}, + "analyze": {"status": "completed", "message": "已完成 15 家分析", "count": 15}, + "outreach": {"status": "completed", "message": "已为 5 家生成触达", "top_count": 5}, + "complete": {"status": "completed", "message": "任务完成"} + }, + "leads": [ + { + "name": "Company ABC", + "match_score": 85, + "match_reason": "高度匹配", + "company_summary": "主营...", + "url": "https://...", + "outreach": {"email_body": "...", "whatsapp_message": "..."} + } + ], + "summary": { + "total_leads": 15, + "high_match": 5, + "medium_match": 7, + "customers_saved": 3 + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | UUID | 主键 | +| user_id | UUID | 所属用户 | +| status | VARCHAR | 状态: running/completed/failed | +| progress | INTEGER | 进度百分比 0-100 | +| product_name | VARCHAR | 产品名称 | +| product_description | TEXT | 产品描述 | +| target_market | VARCHAR | 目标市场 | +| pipeline_data | JSONB | 流水线数据 (stages + leads + summary) | +| error_message | TEXT | 错误信息 | +| created_at | TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | 更新时间 | + +--- + ## 四、pgvector 扩展 ```sql @@ -367,6 +453,7 @@ LIMIT 5; | messages | conversation_id | 消息历史 | | quotations | user_id, customer_id, status | 报价单管理 | | corpus_entries | task_type, domain, embedding | 语料检索 | +| agent_pipelines | user_id, status | 流水线查询 | --- @@ -380,9 +467,18 @@ LIMIT 5; | 报价单数据 | 3年 | 订单追溯 | | 语料数据 | 永久 | AI训练 | | 日志数据 | 90天 | 调试审计 | +| 流水线数据 | 永久 | AI 数字员工执行记录 | --- ## 七、迁移脚本 -使用 Alembic 进行数据库迁移,初始迁移见 `backend/alembic/versions/001_initial.py`。 \ No newline at end of file +使用 Alembic 进行数据库迁移,初始迁移见 `backend/alembic/versions/001_initial.py`。 + +```bash +# 创建新迁移 +cd backend && alembic revision --autogenerate -m "add agent_pipelines" + +# 执行迁移 +cd backend && alembic upgrade head +``` diff --git a/user-frontend/package-lock.json b/user-frontend/package-lock.json index 75118fd..ec59d26 100644 --- a/user-frontend/package-lock.json +++ b/user-frontend/package-lock.json @@ -707,9 +707,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -724,9 +721,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -741,9 +735,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -758,9 +749,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -775,9 +763,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -792,9 +777,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -809,9 +791,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -826,9 +805,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -843,9 +819,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -860,9 +833,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -877,9 +847,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -894,9 +861,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -911,9 +875,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/user-frontend/src/api/index.js b/user-frontend/src/api/index.js index 34fbd88..41531da 100644 --- a/user-frontend/src/api/index.js +++ b/user-frontend/src/api/index.js @@ -131,4 +131,8 @@ export function subscribeCreditPlan(planId, payType = 'alipay') { } export function cancelCreditSubscription() { return http.post('/credits/cancel-subscription') } +export function startAgentPipeline(data) { return http.post('/agent/start', data, { timeout: 300000 }) } +export function listAgentPipelines(params) { return http.get('/agent/pipelines', { params }) } +export function getAgentPipeline(id) { return http.get(`/agent/${id}`) } + export default http diff --git a/user-frontend/src/layouts/UserLayout.vue b/user-frontend/src/layouts/UserLayout.vue index aaa8228..90078cb 100644 --- a/user-frontend/src/layouts/UserLayout.vue +++ b/user-frontend/src/layouts/UserLayout.vue @@ -16,6 +16,7 @@ :collapse-transition="false" @select="showMobileMenu = false" > + {{ $t('nav.agent') || 'AI数字员工' }} {{ $t('nav.discovery') }} {{ $t('nav.workspace') }} {{ $t('nav.customers') }} @@ -137,13 +138,6 @@ const beianInfo = computed(() => { return { icp: '京ICP备2026007249号-1', gongan: '京公网安备11011502039545号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545', showGongan: true } }) -onMounted(async () => { - try { - const res = await getUnreadCount() - unread.value = res.count || res || 0 - } catch { /* ignore */ } -}) - function handleLogout() { auth.logout() router.push('/') diff --git a/user-frontend/src/router/index.js b/user-frontend/src/router/index.js index 88fe50d..f81e129 100644 --- a/user-frontend/src/router/index.js +++ b/user-frontend/src/router/index.js @@ -3,6 +3,14 @@ import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', redirect: '/' }, { path: '/', name: 'Landing', component: () => import('@/views/WorkspaceLanding.vue') }, + { + path: '/agent', + component: () => import('@/layouts/UserLayout.vue'), + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Agent', component: () => import('@/views/Agent.vue'), meta: { title: 'AI数字员工' } }, + ] + }, { path: '/workspace', component: () => import('@/layouts/UserLayout.vue'), diff --git a/user-frontend/src/views/Agent.vue b/user-frontend/src/views/Agent.vue new file mode 100644 index 0000000..331e2ff --- /dev/null +++ b/user-frontend/src/views/Agent.vue @@ -0,0 +1,531 @@ + + + + +