diff --git a/backend/app/api/companies.py b/backend/app/api/companies.py index b679b7e..78ec115 100644 --- a/backend/app/api/companies.py +++ b/backend/app/api/companies.py @@ -60,6 +60,9 @@ class DepartmentAdd(BaseModel): class ExecuteRequest(BaseModel): project_description: str = Field(..., min_length=1, description="公司级项目目标") + ceo_plan: Optional[Dict[str, Any]] = Field( + None, description="已确认的 CEO 计划;提供时跳过二次规划,直接据此执行" + ) # ─── Endpoints ─── @@ -365,6 +368,24 @@ def remove_department( # ─── 项目执行 ─── +@router.post("/{company_id}/plan") +async def plan_company( + company_id: str, + body: ExecuteRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """仅生成 CEO 计划供预览/确认,不派发部门、不落库。""" + company = db.query(Company).filter(Company.id == company_id).first() + if not company: + raise HTTPException(status_code=404, detail="公司不存在") + orchestrator = CompanyOrchestrator(db, company_id, current_user.id) + try: + return await orchestrator.preview_plan(body.project_description) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + @router.post("/{company_id}/execute") async def execute_company( company_id: str, @@ -378,7 +399,7 @@ async def execute_company( raise HTTPException(status_code=404, detail="公司不存在") orchestrator = CompanyOrchestrator(db, company_id, current_user.id) try: - result = await orchestrator.execute(body.project_description) + result = await orchestrator.execute(body.project_description, ceo_plan=body.ceo_plan) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -400,7 +421,7 @@ async def execute_company_stream( async def event_stream(): try: - async for event in orchestrator.execute_stream(body.project_description): + async for event in orchestrator.execute_stream(body.project_description, ceo_plan=body.ceo_plan): yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n" except Exception as e: yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n" diff --git a/backend/app/services/company_orchestrator.py b/backend/app/services/company_orchestrator.py index ea9128b..f4be796 100644 --- a/backend/app/services/company_orchestrator.py +++ b/backend/app/services/company_orchestrator.py @@ -383,10 +383,27 @@ class CompanyOrchestrator: "_ts": __import__("time").time(), }) + async def preview_plan(self, project_description: str) -> Dict[str, Any]: + """仅运行 CEO 规划并返回计划,不派发部门、不落库(无状态预览)。 + + 供「执行前预览+确认」使用:前端拿到计划后可微调,再携带该计划正式执行。 + """ + company = self._load_company() + departments = self._load_departments() + if not departments: + raise ValueError("公司没有部门,请先添加部门") + ceo_plan = await self._run_ceo_planning(company, departments, project_description) + return { + "company_name": company.name, + "department_names": [d.name for d in departments], + "ceo_plan": ceo_plan, + } + async def execute( self, project_description: str, auto_approve_files: bool = True, + ceo_plan: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """执行公司级项目(非流式)。 @@ -418,9 +435,12 @@ class CompanyOrchestrator: self.db.add(project) self.db.commit() - # Phase 1: CEO Planning - logger.info("公司编排 [%s]: Phase 1 — CEO 规划开始", self.company_id) - ceo_plan = await self._run_ceo_planning(company, departments, project_description) + # Phase 1: CEO Planning(若已传入已确认的计划则跳过,直接采用) + if ceo_plan is None: + logger.info("公司编排 [%s]: Phase 1 — CEO 规划开始", self.company_id) + ceo_plan = await self._run_ceo_planning(company, departments, project_description) + else: + logger.info("公司编排 [%s]: Phase 1 — 采用已确认的 CEO 计划(跳过规划)", self.company_id) project.ceo_plan = ceo_plan project.status = "in_progress" self.db.commit() @@ -515,6 +535,7 @@ class CompanyOrchestrator: project_description: str, auto_approve_files: bool = True, max_rounds: int = 3, + ceo_plan: Optional[Dict[str, Any]] = None, ) -> AsyncGenerator[Dict[str, Any], None]: """流式执行公司级项目,SSE 事件实时推送。支持多轮迭代+打分+预算。 @@ -556,14 +577,17 @@ class CompanyOrchestrator: "_ts": _time.time(), } - # Phase 1: CEO Planning - yield {"type": "ceo_plan_start", "message": "CEO is analyzing the objective...", "_ts": _time.time()} + # Phase 1: CEO Planning(若已传入已确认的计划则跳过,直接采用) t_ceo_start = _time.time() - try: - ceo_plan = await self._run_ceo_planning(company, departments, project_description) - except Exception as e: - yield {"type": "error", "message": f"CEO planning failed: {e}", "phase": "ceo_planning", "_ts": _time.time()} - return + if ceo_plan is None: + yield {"type": "ceo_plan_start", "message": "CEO is analyzing the objective...", "_ts": _time.time()} + try: + ceo_plan = await self._run_ceo_planning(company, departments, project_description) + except Exception as e: + yield {"type": "error", "message": f"CEO planning failed: {e}", "phase": "ceo_planning", "_ts": _time.time()} + return + else: + yield {"type": "ceo_plan_start", "message": "Using approved plan...", "approved": True, "_ts": _time.time()} yield {"type": "ceo_plan_done", "plan": ceo_plan, "_ts": _time.time(), "duration_ms": int((_time.time() - t_ceo_start) * 1000)} diff --git a/backend/app/services/company_presets.py b/backend/app/services/company_presets.py index 11008b7..048b163 100644 --- a/backend/app/services/company_presets.py +++ b/backend/app/services/company_presets.py @@ -922,7 +922,7 @@ COMPANY_PRESETS: Dict[str, Dict[str, Any]] = { "group": "战略层", "description": "商业分析、融资BP、合作评估、赛道研判", "system_prompt": CEO_TECH_PROMPT, - "tools": ["web_search", "text_analyze", "task_plan", "http_request", "file_read"], + "tools": ["web_search", "text_analyze", "task_plan", "http_request", "file_read", "file_write"], "temperature": 0.5, "model": "deepseek-v4-pro", "max_iterations": 15, @@ -933,7 +933,7 @@ COMPANY_PRESETS: Dict[str, Dict[str, Any]] = { "group": "战略层", "description": "架构评审、技术选型、技术债评估、专利分析", "system_prompt": CTO_TECH_PROMPT, - "tools": ["web_search", "file_read", "grep_search", "text_analyze", "task_plan"], + "tools": ["web_search", "file_read", "grep_search", "text_analyze", "task_plan", "file_write"], "temperature": 0.3, "model": "deepseek-v4-pro", "max_iterations": 12, diff --git a/docs/腾讯云Linux部署指南.md b/docs/腾讯云Linux部署指南.md index b3ed53b..547f92a 100644 --- a/docs/腾讯云Linux部署指南.md +++ b/docs/腾讯云Linux部署指南.md @@ -16,6 +16,14 @@ 所有服务通过 Docker Compose 编排,统一在 `aiagent-net` 桥接网络中通信。 +> **重要:本项目在 101.43.95.130 的实际生产部署与本指南默认假设有三处差异,升级/运维前务必先看:** +> +> 1. **数据库是外部腾讯云 CloudDB(TDSQL-C),不是容器 MySQL。** `docker-compose.prod.yml` 里没有 mysql 服务,也就没有 `aiagent-mysql` 容器。凡是 `docker exec aiagent-mysql ...` 的命令都要改成直连外部实例(见第六、七、九节的「外部数据库」写法)。 +> 2. **服务器在国内,`docker compose build` 常卡在 apt-get / PyPI(files.pythonhosted.org)超时。** 构建或装包需走国内镜像源(见第四节)。 +> 3. **升级不要盲目 `alembic upgrade`。** 公司模块迁移 `d684c9b30c1c` 是 autogenerate 的破坏性迁移(会 drop 掉 tools/api_keys 等已有数据的表),且线上库对应表已存在。正确做法见第六节「增量升级」。 +> +> 实际部署目录为 `/home/renjianbo/aiagent`(本指南示例用 `/opt/aiagent`,按需替换)。 + --- ## 二、服务器要求 @@ -173,6 +181,21 @@ docker compose -f docker-compose.prod.yml build --pull docker compose -f docker-compose.prod.yml up -d ``` +> **国内服务器注意(重要)**:在腾讯云等国内服务器上,`docker compose build` 经常卡在 `apt-get update` 或 `files.pythonhosted.org` 超时。处理办法: +> +> - **给 Docker 配镜像加速**(构建时拉基础镜像):编辑 `/etc/docker/daemon.json` 加入 +> ```json +> { "registry-mirrors": ["https://mirror.ccs.tencentyun.com"] } +> ``` +> 然后 `sudo systemctl restart docker`(`mirror.ccs.tencentyun.com` 为腾讯云内网加速地址)。 +> - **给 pip 换国内源**:在 `backend/Dockerfile` 的 pip 安装步骤加 +> `-i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com`。 +> - **临时给已运行容器补一个 Python 包**(不想重建镜像时): +> ```bash +> docker exec <容器名> pip install -i https://mirrors.aliyun.com/pypi/simple/ \ +> --trusted-host mirrors.aliyun.com <包名>==<版本> +> ``` + ### 4. 验证部署 ```bash @@ -206,6 +229,8 @@ aiagent-kibana Up aiagent-filebeat Up ``` +> **使用外部腾讯云数据库时**:没有 `aiagent-mysql` 容器(少一个)。核心服务确认 `aiagent-backend`、`aiagent-redis`、`aiagent-frontend`、两个 celery 均为 `Up`,且 `curl -s -o /dev/null -w '%{http_code}' http://localhost:8037/docs` 返回 `200` 即为正常。 + --- ## 五、访问地址 @@ -260,6 +285,14 @@ docker compose -f docker-compose.prod.yml up -d ### 增量升级 +> **升级前必读(本项目实测坑)**: +> 1. **构建可能失败**:`docker compose build --pull` 在国内服务器常因 apt/PyPI 超时卡死,先按第四节配好国内镜像;急用时可用「docker cp 热更新 + 补依赖」绕过(见下方备用步骤 2B/3)。 +> 2. **迁移要小心**:公司模块迁移 `d684c9b30c1c_add_company_tables` 是破坏性的(会 `drop_table` tools/api_keys/fcm_tokens 等)。若线上库里 companies/company_projects 等表已存在,**不要执行 `alembic upgrade`**,而是标记版本: +> ```bash +> docker exec aiagent-backend sh -c "cd /app && alembic stamp d684c9b30c1c" +> ``` +> 3. **新依赖**:新代码引入了 `APScheduler==3.10.4`(公司调度用)。旧镜像若没有,backend 会 `ModuleNotFoundError: No module named 'apscheduler'` 崩溃,需按第四节临时装包(backend + 两个 celery 都要装)。 + 项目提供了 `upgrade.sh` 脚本实现零停机升级: ```bash @@ -274,31 +307,66 @@ bash upgrade.sh 5. 清理旧镜像,释放磁盘空间 6. 验证后端健康状态 -手动升级步骤(不使用脚本): +手动升级步骤(国内服务器 / 外部数据库实测流程): ```bash -# 拉取最新代码 +cd /home/renjianbo/aiagent # 或 /opt/aiagent + +# 1. 拉取最新代码 git pull -# 重新构建并启动 +# 2A. 常规:重建镜像并重启(网络好、已配国内镜像时) docker compose -f docker-compose.prod.yml build --pull docker compose -f docker-compose.prod.yml up -d + +# 2B. 备用:build 卡死时,热更新已运行容器的代码(不重建镜像) +docker cp backend/app aiagent-backend:/app/ +docker cp backend/alembic aiagent-backend:/app/ +docker cp backend/app aiagent-celery-worker:/app/ +docker cp backend/app aiagent-celery-beat:/app/ + +# 3. 有新 Python 依赖时,用国内源补装(三个容器都装) +for c in aiagent-backend aiagent-celery-worker aiagent-celery-beat; do + docker exec $c pip install -i https://mirrors.aliyun.com/pypi/simple/ \ + --trusted-host mirrors.aliyun.com -r /app/requirements.txt +done + +# 4. 迁移:先看当前版本,破坏性迁移用 stamp(勿盲目 upgrade,见上方警告) +docker exec aiagent-backend sh -c "cd /app && alembic current" + +# 5. 重启后端使代码生效并验证 +docker compose -f docker-compose.prod.yml restart backend +curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8037/docs # 期望 200 ``` +> 走 2B 热更新的改动只在容器可写层,`docker compose up`/重建容器会丢失;条件允许时仍应配好国内镜像走 2A 正式重建。 + ### 数据库备份 +**A. 容器内 MySQL(本指南默认):** ```bash -# 备份到本地文件 docker exec aiagent-mysql mysqldump -u root -p<密码> agent_db > agent_db_backup.sql +``` -# 定时备份(加入 crontab,每天凌晨 2 点) -# 0 2 * * * cd /opt/aiagent && docker exec aiagent-mysql mysqldump -u root -p<密码> agent_db > backups/agent_db_$(date +\%Y\%m\%d).sql +**B. 外部腾讯云 CloudDB(本项目实际用法,无 aiagent-mysql 容器):** 直连外部实例: +```bash +mysqldump -h gz-cynosdbmysql-grp-xxx.sql.tencentcdb.com -P 24936 \ + -u root -p<密码> agent_db > agent_db_backup.sql +# 若本机没装 mysql 客户端,可借用 mysql 镜像执行: +# docker run --rm mysql:8.0 mysqldump -h <外部地址> -P 24936 -uroot -p<密码> agent_db > agent_db_backup.sql + +# 定时备份(crontab,每天凌晨 2 点) +# 0 2 * * * mysqldump -h <外部地址> -P 24936 -uroot -p<密码> agent_db > /home/renjianbo/aiagent/backups/agent_db_$(date +\%Y\%m\%d).sql ``` ### 数据库恢复 ```bash +# 容器内 MySQL docker exec -i aiagent-mysql mysql -u root -p<密码> agent_db < agent_db_backup.sql + +# 外部腾讯云 CloudDB +mysql -h gz-cynosdbmysql-grp-xxx.sql.tencentcdb.com -P 24936 -u root -p<密码> agent_db < agent_db_backup.sql ``` --- @@ -392,17 +460,21 @@ free -h ### 数据库连接失败 +**容器内 MySQL:** ```bash -# 确认 MySQL 容器是否健康 docker exec aiagent-mysql mysqladmin ping -h localhost - -# 检查后端能否连接 MySQL -docker exec aiagent-backend sh -c "curl -s mysql:3306" || echo "无法连接" - -# 查看 MySQL 日志 docker logs aiagent-mysql --tail 30 ``` +**外部腾讯云 CloudDB(本项目实际用法):** 从 backend 容器内验证能否连通外部库: +```bash +docker exec aiagent-backend python -c "import os,re,pymysql; \ +u=os.environ['DATABASE_URL']; m=re.match(r'.*://([^:]+):([^@]+)@([^:]+):(\d+)/([^?]+)',u); \ +pymysql.connect(host=m.group(3),user=m.group(1),password=m.group(2),port=int(m.group(4)),database=m.group(5)); \ +print('DB OK')" +``` +排查点:`.env` 的 `DATABASE_URL` 是否正确、腾讯云 CloudDB 安全组是否放行了本服务器的出口 IP、账号密码是否有效、库 `agent_db` 是否存在。 + ### DeepSeek API 返回 401 确认 `.env` 中 `DEEPSEEK_API_KEY` 已正确配置,密钥未过期。 diff --git a/frontend/package.json b/frontend/package.json index b11eee1..084b5f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@vue-flow/core": "^1.30.0", "@vue-flow/minimap": "^1.1.0", "axios": "^1.6.2", + "echarts": "^6.1.0", "element-plus": "^2.4.4", "marked": "^18.0.5", "monaco-editor": "^0.44.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 81c287c..79eaff1 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: axios: specifier: ^1.6.2 version: 1.14.0 + echarts: + specifier: ^6.1.0 + version: 6.1.0 element-plus: specifier: ^2.4.4 version: 2.13.6(typescript@5.3.3)(vue@3.5.32(typescript@5.3.3)) @@ -838,6 +841,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + echarts@6.1.0: + resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==} + element-plus@2.13.6: resolution: {integrity: sha512-XHgwXr8Fjz6i+6BaqFhAbae/dJbG7bBAAlHrY3pWL7dpj+JcqcOyKYt4Oy5KP86FQwS1k4uIZDjCx2FyUR5lDg==} peerDependencies: @@ -1400,6 +1406,9 @@ packages: peerDependencies: typescript: '>=4.2.0' + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1547,6 +1556,9 @@ packages: yup@1.7.1: resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + zrender@6.1.0: + resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==} + snapshots: '@babel/helper-string-parser@7.27.1': {} @@ -2233,6 +2245,11 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + echarts@6.1.0: + dependencies: + tslib: 2.3.0 + zrender: 6.1.0 + element-plus@2.13.6(typescript@5.3.3)(vue@3.5.32(typescript@5.3.3)): dependencies: '@ctrl/tinycolor': 4.2.0 @@ -2841,6 +2858,8 @@ snapshots: dependencies: typescript: 5.3.3 + tslib@2.3.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -2944,3 +2963,7 @@ snapshots: tiny-case: 1.0.3 toposort: 2.0.2 type-fest: 2.19.0 + + zrender@6.1.0: + dependencies: + tslib: 2.3.0 diff --git a/frontend/src/api/companies.ts b/frontend/src/api/companies.ts index 31ed7a1..3b4767f 100644 --- a/frontend/src/api/companies.ts +++ b/frontend/src/api/companies.ts @@ -76,6 +76,16 @@ export async function executeCompany(companyId: string, projectDescription: stri return resp.data } +export async function previewCompanyPlan(companyId: string, projectDescription: string): Promise { + // CEO 规划是一次 LLM 调用,通常几十秒,需覆盖 axios 默认 30s 超时 + const resp = await api.post( + `/api/v1/companies/${companyId}/plan`, + { project_description: projectDescription }, + { timeout: 300000 }, + ) + return resp.data +} + export async function addDepartment(companyId: string, teamId: string): Promise { const resp = await api.post(`/api/v1/companies/${companyId}/departments`, { team_id: teamId }) return resp.data diff --git a/frontend/src/views/CompanyBuilder.vue b/frontend/src/views/CompanyBuilder.vue index 257a16e..5b49f82 100644 --- a/frontend/src/views/CompanyBuilder.vue +++ b/frontend/src/views/CompanyBuilder.vue @@ -175,33 +175,65 @@
- CEO 启动执行 + 启动执行(实时白盒) - 流式执行 + 停止 + + + 实时监控 + + + 后台执行
- -
- - + +
+ - -
+ +
+ + + +
@@ -342,6 +374,14 @@
+ + + @@ -427,7 +467,21 @@ :key="d.department_name" :title="`${d.department_name} — ${d.success ? '✓' : '✗'}`" > -
{{ JSON.stringify(d.result || d, null, 2) }}
+
+
+
📎 产物文件 ({{ deptFiles(d).length }})
+
+ + {{ fileBaseName(f) }} + {{ f }} +
+
+ + + +
{{ JSON.stringify(d.result || d, null, 2) }}
+
+
@@ -464,7 +518,7 @@
- +
-
-

部门效率

- - - - - - - - +
+

部门表现

+ +
+
+

部门协作关系

+ +
+
+

失败归因 ({{ insights.failure_reasons.length }})

+ +
+ {{ f.project }} + {{ f.department }} · {{ f.score }}分 +
+
{{ f.feedback }}
+

改进建议

@@ -607,6 +668,55 @@
+ + + +
+ 一键扫描 + 刷新 + 全部公司 + 扫描于 {{ formatDate(supervisorScanAt) }} +
+
提示:长时间流式任务不会刷新"更新时间",可能被误判为风险/僵尸,可扫描后再核对。
+ +
+
进行中 · 风险 ({{ filteredInProgress.length }})
+
+ +
+ {{ p.name }} + {{ p.is_at_risk ? '高风险' : '进行中' }} +
+
空闲 {{ p.idle_minutes }} 分钟
+
+ 距判定僵尸 + + {{ Math.round(p.will_become_zombie_in_minutes || 0) }} 分钟 +
+
+
+ +
+ +
+
僵尸项目 ({{ filteredZombies.length }})
+
+ +
+ {{ z.name }} + 恢复 +
+
空闲 {{ z.idle_minutes }} 分钟 · 最后更新 {{ formatDate(z.last_updated) }}
+
+
+ +
+
@@ -771,22 +881,91 @@
+ + +
+
+ {{ planCompanyName }} + {{ (planDraft.departments || []).length }} 个部门 + 可微调下方「战略分析 / 各部门目标 / 交付物」后再执行;部门与依赖关系保持 CEO 原样。 +
+ +
+
战略分析(analysis)
+ +
+ +
+
+
+
总体时间线
+
{{ planDraft.overall_timeline }}
+
+
+
成功指标
+
    +
  • {{ m }}
  • +
+
+
+
风险
+
    +
  • {{ r }}
  • +
+
+
+
+ +
部门任务拆分
+
+
+ {{ dept.department_name || dept.department_type }} + {{ dept.department_type }} + 优先级 {{ dept.priority }} + + 依赖:{{ dep }} + +
+
+
目标(goal)
+ +
+
+
交付物(每行一项)
+ +
+
+
关键问题
+
    +
  • {{ q }}
  • +
+
+
+
+ +