Files
aiagent/docs/虚拟公司白盒控制室详细设计.md
renjianbo 495eafcd78 feat: virtual company M2 control + tier-3/4 capabilities + per-dept session fix
- M2-a/b/c: stop/pause/resume/skip dept, plan/rework approval gates,
  budget & timeout circuit breakers, per-dept cancel and on-demand rework
- tier-3: batched event persistence (_EventBuffer), project/dept
  concurrency semaphores, company_ws event streaming
- tier-4: cross-dept deliverable handoff, CEO memory feedback loop,
  per-dept model override, one-click Markdown report export (RFC5987)
- fix: per-department independent SQLAlchemy session in parallel waves
  (shared Session race across asyncio tasks in company_orchestrator)
- fix: rename process_open_fds gauge to app_process_open_fds
  (collides with prometheus_client ProcessCollector on Linux)
- fix: dept role resolution ladder, real token usage & cost tracking,
  deliverable file capture (mtime scan of dept workspace)
- tests: test_company_orchestrator.py 22 cases with mocked LLM
- frontend: CompanyControlRoom, companyExecution store, EChart,
  dept model config dialog, ws proxy, build/typecheck decoupling
- docs: virtual company design/usage docs, M2 control plan,
  deepseek4pro handover docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-26 18:16:39 +08:00

22 KiB
Raw Blame History

虚拟公司「白盒控制室」详细设计(工程实现规格)

配套文档:虚拟公司白盒控制室实现方案.md(概览)。本文是可直接指导编码的详细规格。 分支:rjb_tiangong_debug | 编写日期:2026-07-11 关键源文件:

  • 编排 backend/app/services/company_orchestrator.py
  • 部门执行 backend/app/services/team_orchestrator.py
  • Agent 循环 backend/app/agent_runtime/core.py(run_stream: 764–1291)
  • API backend/app/api/companies.py(execute/stream: 387–408)
  • 模型 backend/app/models/company.py
  • 僵尸监管 backend/app/services/project_supervisor.py
  • 可复用 WS backend/app/api/websocket.py + app/websocket/manager.py
  • 前端主页面 frontend/src/views/CompanyBuilder.vue(routeStreamEvent: 1294–1546)

0. 术语

术语 含义
项目(project) 一次公司级执行,对应 CompanyProject 一行
部门(department) 复用 Team,通过 Team.parent_company_id 挂到公司
phase 部门内 TeamOrchestrator 的一个执行阶段(PM/架构/开发/QA...)
事件(event) 编排产生的一条 SSE/WS 消息
检查点(checkpoint) 执行循环中读取控制标志的位置
边界暂停 仅在部门/阶段边界暂停,不打断进行中的 LLM 调用

1. 现状深挖(数据源盘点)

1.1 execute_stream 事件全集(company_orchestrator.py:513–830)

type 关键 payload 源行
start message 542
company_start company_name, department_count, departments[] 551
ceo_plan_start message 560
ceo_plan_done plan(完整 CEO JSON: analysis/departments/risks/metrics), duration_ms 568
round_start round_index, max_rounds, rework_depts[] 622/635
dept_waiting department_name, waiting_for[] 661
dept_budget_exceeded department_name, max_tokens, tokens_used 688
dept_agent_event department_name, department_type, sub_type, data(整包 team 事件), last_thought, phase_progress, phase_name, tokens_used, budget_remaining, budget_percentage 710
dept_budget department_name, used, remaining, percentage 742
dept_done department_name, success, result{deliverable, files[], phase_count}, duration_ms, tokens_used 750
ceo_score_start message, round_index 767
ceo_score_done scores[{name,score,feedback,pass,improvement_areas}], overall_score, summary, next_steps, dept_pass_map, round_index 783
round_done round_index, scores, overall_score, rework_needed, passed_depts[], failed_depts[] 799
company_done success, summary, departments[], project_id, total_duration_ms, round_count, overall_score, review_scores 819
error message, phase 547/565

1.2 部门内最细粒度(dept_agent_event.data → TeamOrchestrator → AgentRuntime.run_stream)

agent_event.data 可透出(core.py 源行):

  • think: content, reasoning(思维链 reasoning_content), tool_names[], iteration — 944/1062
  • tool_call: name, input(完整入参), iteration — 1124
  • tool_result: name, result(截断 500 字), iteration — 1187
  • approval_required: approval_id, tool_name, args — 1136
  • final: content, reasoning, iterations_used, tool_calls_made, token_usage(真实 TokenBudget) — 1020

TeamOrchestrator.execute_stream 事件:plan_start/plan/plan_done、architect_start/done、parallel_batch_start/phase_start/parallel_batch_end、agent_event、phase_done(output,success,iterations,tool_calls,files[])、qa_start/qa_done(review,score,issues)、fix_phase_start/done、final、error。

1.3 数据丢失点 / bug(前端或公司层丢弃)

  1. tool_call.input / tool_result.result 原样在 data 里但前端只显示工具名+耗时(CB:1409/1412)。
  2. last_thought 截断 2000、thinkHistory 仅最近 10 条(CB:1403)。
  3. 产出文件 dept_done.result.files 前端无文件树/预览/下载。
  4. token 是事件计数近似(company_orchestrator.py:712 current_used = ... + 1),非真实;真实值在 phase final.token_usage,公司层未上抬。
  5. dept_done.duration_ms 计时起点不稳(726 dept_timings.get(name, now))。
  6. CEO 规划/评分是 await runtime.run() 非流式(224/926),过程不可见。
  7. updated_at 有 onupdate 但执行中无 UPDATE → 运行期不刷新 → supervisor 15 分钟误杀(project_supervisor.py:41)。

1.4 控制缺口

  • 无 execution_id / 无 Celery / 无任务句柄;部门 asyncio.gather 跑在请求协程内。
  • 全库无 company 的 stop/pause/cancel/resume。
  • AgentRuntime 循环无取消检查点(只有 max_iterations 硬停 + 工具审批阻塞)。
  • company_projects 无 pause_state 字段(workflow 的 pause_state 是另一套,未接入)。
  • 断开 SSE 只停消费,后台继续 → 僵尸项目。

2. 目标架构

2.1 时序:启动 → 观测 → 控制

Client                API                     Runner(bg task)         Redis           DB
  │  POST execute/async │                          │                   │              │
  │────────────────────▶│ create CompanyProject    │                   │              │
  │                     │ status=queued ───────────┼───────────────────┼──────────────▶ insert
  │                     │ create_task(runner)       │                   │              │
  │◀── {project_id} ────│                          │                   │              │
  │                     │                          │ execute_stream()  │              │
  │  WS connect(pid)    │                          │  each event:      │              │
  │────────────────────▶│ replay events by seq ◀───┼───────────────────┼────────────── select
  │◀── replay... ───────│                          │  publish ─────────▶ pub/sub       │
  │◀── live event ──────┼──────────────────────────┼─────subscribe─────│              │
  │                     │                          │  persist ─────────┼──────────────▶ insert event
  │                     │                          │  heartbeat ───────┼──────────────▶ update updated_at
  │  POST stop          │ set control flag ────────┼───────────────────▶ key           │
  │                     │                          │ checkpoint reads ◀│              │
  │                     │                          │ break → status=stopped ───────────▶ update
  │◀── control_ack(WS) ─│                          │                   │              │

2.2 控制信号语义

action 触发点读到后行为
stop 检查点 break 出所有循环 → 落 stopped
pause 检查点 while paused: await sleep(0.5) 轮询,直到 resume/stop
resume 清 pause 标志
skip{dept} 从当前波 ready 列表移除该部门,标记 skipped
rework{depts[]} 覆盖下一轮 rework 列表(即使本轮 pass)

3. 后端详细设计

3.1 新表 company_project_events

DDL(utf8mb4):

CREATE TABLE company_project_events (
  id CHAR(36) PRIMARY KEY,
  project_id CHAR(36) NOT NULL,
  seq INT NOT NULL,                 -- 项目内自增序号,从 1
  type VARCHAR(50) NOT NULL,
  payload JSON,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  KEY idx_cpe_project_seq (project_id, seq),
  CONSTRAINT fk_cpe_project FOREIGN KEY (project_id)
    REFERENCES company_projects(id) ON DELETE CASCADE
) DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;

模型 backend/app/models/company_project_event.py:

class CompanyProjectEvent(Base):
    __tablename__ = "company_project_events"
    id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    project_id = Column(CHAR(36), ForeignKey("company_projects.id", ondelete="CASCADE"), nullable=False)
    seq = Column(Integer, nullable=False)
    type = Column(String(50), nullable=False)
    payload = Column(JSON)
    created_at = Column(DateTime, default=func.now())
    __table_args__ = (Index("idx_cpe_project_seq", "project_id", "seq"),)

company_projects 状态机扩展:queued/planning/in_progress/paused/stopping/stopped/completed/failed(+ supervisor 的 zombie)。

3.2 迁移安全流程(务必按序)

cd backend
./venv/Scripts/python.exe -m alembic current   # 记录当前真实版本
./venv/Scripts/python.exe -m alembic heads      # 应为 d684c9b30c1c(含删表,勿跑)
# 1) 用 introspect 确认表结构已是最新(chat_messages/billing/companies 均存在)
# 2) 对齐版本,不执行任何 DDL:
./venv/Scripts/python.exe -m alembic stamp head
# 3) 新建迁移(只 create company_project_events + add index):
./venv/Scripts/python.exe -m alembic revision -m "026 add company_project_events"
#    手写 upgrade(): op.create_table(...); downgrade(): op.drop_table(...)
#    ⚠️ 不要用 --autogenerate(会再次生成删表 diff)
./venv/Scripts/python.exe -m alembic upgrade head

若 stamp head 会把 d684c9b30c1c 标为已应用——因表结构确已存在,这是正确的对齐,不会执行其删表。

3.3 Redis 规范(复用 app/core/redis_client.py)

Key/Channel 类型 值 TTL
company:events:{pid} pub/sub channel 事件 JSON —
company:control:{pid} string {"action":"stop|pause|run|skip|rework","dept":...,"ts":...} 24h
company:status:{pid} string 最新状态快照 JSON 24h

3.4 归一化事件信封(落库 + 推送统一格式)

{
  "seq": 128,
  "pid": "uuid",
  "type": "dept_agent_event",
  "ts": 1720684800.12,
  "payload": { ... 原事件字段 ... }
}

落库时 type/payload 拆列存;WS 推送发完整信封。

3.5 接口契约

启动(异步)

POST /api/v1/companies/{company_id}/execute/async
body: { project_description: str, max_rounds?: int=3, budget?: {...} }
200:  { project_id: str, status: "queued", ws_url: "/api/v1/ws/company-projects/{pid}" }
403:  非归属  404: 公司不存在

事件回放(WS 不可用时的 HTTP 兜底)

GET /api/v1/companies/{company_id}/projects/{pid}/events?since_seq=0&limit=500
200: { events: [信封...], last_seq: int, status: str, finished: bool }

控制

POST /api/v1/companies/{company_id}/projects/{pid}/stop      → { ok, status:"stopping" }
POST .../pause                                                → { ok, status:"paused" }
POST .../resume                                               → { ok, status:"in_progress" }
POST .../skip     body:{ department_name }                    → { ok }
POST .../rework   body:{ department_names: [] }               → { ok }
所有:403 非归属 / 404 项目不存在 / 409 项目已终态

3.6 后台执行包装器(新 company_runner.py 或并入 orchestrator)

async def run_company_project(project_id, company_id, user_id, desc, max_rounds):
    db = SessionLocal()
    seq = _next_seq(db, project_id)          # 断点续跑安全
    orch = CompanyOrchestrator(db, company_id, user_id)
    orch.control = ControlChannel(project_id)  # 见 3.7
    try:
        _set_status(db, project_id, "in_progress")
        async for evt in orch.execute_stream(desc, max_rounds=max_rounds):
            env = _envelope(project_id, seq, evt); seq += 1
            _persist(db, env)                 # insert company_project_events
            _publish(env)                     # redis publish
            _heartbeat(db, project_id)        # 每 N 事件或每 5s update updated_at
        _finalize_status(db, project_id)      # completed/failed by last event
    except asyncio.CancelledError:
        _set_status(db, project_id, "stopped"); _publish_terminal(project_id, "stopped"); raise
    except Exception as e:
        _set_status(db, project_id, "failed"); _publish_terminal(project_id, "failed", str(e))
    finally:
        db.close()

3.7 控制通道 + 检查点

class ControlChannel:
    def __init__(self, pid): self.pid = pid; self.r = get_redis_client()
    def read(self) -> dict: raw = self.r.get(f"company:control:{self.pid}"); return json.loads(raw) if raw else {}
    async def gate(self):                       # 在检查点调用
        ctl = self.read()
        if ctl.get("action") == "stop": raise asyncio.CancelledError()
        while ctl.get("action") == "pause":
            await asyncio.sleep(0.5); ctl = self.read()
    def skipped(self) -> set: ...
    def forced_rework(self) -> list: ...

插桩位置(company_orchestrator.py,均在边界):

  • :615 轮次循环头:await self.control.gate();forced = self.control.forced_rework() 合并进 rework。
  • :650 部门 wave 循环头:await self.control.gate();ready = [x for x in ready if x[0].name not in self.control.skipped()]。
  • _run_department_stream 每个 dept 事件 yield 前:await self.control.gate()(阶段边界级)。
  • (M4)core.py run_stream ReAct 循环(~:893)每轮 if control.read().action=="stop": break——需把 control 传入 runtime,一期不做。

3.8 WS 端点 backend/app/api/company_ws.py

WS /api/v1/ws/company-projects/{project_id}?token=JWT
  • 鉴权:仿 websocket.py:_validate_ws_token,改为校验 project→company.workspace_id→membership。
  • 连接后:
    1. 读 since_seq(客户端可在 query 带);从 DB 查 seq > since_seq 事件按序 send_json(回放)。
    2. redis.pubsub().subscribe("company:events:{pid}"),转发增量。
    3. 收到客户端 {type:"control", action} 可代理调用控制(或前端直接走 HTTP,二选一,推荐 HTTP)。
    4. 项目终态事件后 send final → close。
  • 复用 websocket_manager(app/websocket/manager.py)做连接登记,支持多观察者(同一项目多个前端)。

3.9 数据质量修复

  • 心跳:_heartbeat 每 5s / 每 20 事件 project.updated_at = func.now(); db.commit()。
  • 真实 token:_run_department_stream 捕获 phase final.token_usage,累加为 dept_real_tokens,在 dept_done/dept_budget 用真实值;保留旧近似字段兼容。
  • duration:在 dept_start 时 dept_timings[name]=now,dept_done 用差值。
  • supervisor:阈值改可配 + 依赖心跳后不再误杀(project_supervisor.py:41 阈值保持 900s 即可)。

4. 前端详细设计

4.1 路由(router/index.ts)

{ path: '/companies/:companyId/projects/:projectId/monitor',
  name: 'company-control-room',
  component: () => import('@/views/CompanyControlRoom.vue'),
  meta: { requiresAuth: true } }

4.2 Pinia store stores/companyExecution.ts

interface DeptPanel { name; type; status; phase; phaseProgress; agents: AgentTrace[]; tokens; durationMs; files: string[]; log: LogItem[] }
interface AgentTrace { name; iterations: Iter[] }
interface Iter { think; reasoning; toolCalls: {name;input;result;durationMs}[]; final? }
interface State {
  projectId; status; connected;
  ceoPlan; ceoScores; rounds: {index;reworkDepts;overallScore}[]; currentRound;
  depts: Record<string, DeptPanel>; timeline: TL[]; rawEvents: Envelope[];
  tokensTotal; controlBusy;
}
actions: connect(pid), disconnect(), applyEvent(env), stop(), pause(), resume(), skip(dept), rework(depts)
getters: deptList, dag(nodes/edges from ceoPlan.departments+dependencies), runningDepts
  • applyEvent 迁移并扩展 CompanyBuilder.routeStreamEvent(CB:1294),补充解析 tool_call.input/tool_result.result 与 final.token_usage。

4.3 WS 客户端(带重连 + 回放)

connect(pid) {
  ws = new WebSocket(`${wsBase}/ws/company-projects/${pid}?token=${token}&since_seq=${lastSeq}`)
  ws.onmessage = e => applyEvent(JSON.parse(e.data))
  ws.onclose = () => backoffReconnect()          // 断线指数退避重连,带 since_seq 续传
}
// 降级:WS 建连失败 → GET .../events 轮询 since_seq

4.4 组件树(CompanyControlRoom.vue)

CompanyControlRoom
├─ ControlBar        状态徽标 + 停止/暂停/恢复 + 轮次 el-steps
├─ split-left
│  └─ CompanyDag     @vue-flow 组织+依赖图,节点实时染色 + 右键跳过/返工
├─ split-center
│  └─ DeptPanelList
│     └─ DeptPanelCard (可展开)
│        └─ AgentTraceView  phase→agent→iteration(think/act/observe + 工具入参出参折叠)
└─ split-right (el-tabs)
   ├─ FilesTab       产出文件树 + 预览
   ├─ StatsTab       真实token/阶段耗时/工具次数(可选 echarts,或纯表格)
   ├─ RawEventsTab   复用 SystemLogs 筛选范式的事件流查看器
   └─ CeoTab         CEO 规划全文 + 评分卡

4.5 事件 → UI 映射(扩展版,新增粗体项)

事件 更新
company_start 初始化 dag 节点
ceo_plan_done CeoTab 全文 analysis + dag 依赖边
dept_start/waiting dag 节点状态、DeptPanelCard
dept_agent_event(think) AgentTraceView 追加 iteration.think + reasoning
dept_agent_event(tool_call) iteration.toolCalls 追加 {name,input}
dept_agent_event(tool_result) 对应 toolCall 补 result+durationMs
dept_agent_event(final) iteration.final + 真实 token_usage
dept_done 文件树、真实 token、耗时、节点终态
ceo_score_done 评分卡
round_start/done 轮次 el-steps
company_done / terminal ControlBar 终态、停止轮询
(所有) RawEventsTab append

4.6 API 封装(api/companies.ts 新增)

executeAsync(companyId, body): {project_id, ws_url}
getProjectEvents(companyId, pid, sinceSeq): {events,last_seq,status,finished}
stopProject / pauseProject / resumeProject(companyId, pid)
skipDepartment(companyId, pid, name) / reworkDepartments(companyId, pid, names[])
companyWsUrl(pid, sinceSeq)

5. 任务分解(按里程碑,文件级 checklist)

M1 可观测白盒(无控制)

  • 模型 company_project_event.py + register in models/__init__.py
  • alembic 迁移(手写,非 autogenerate)+ 按 3.2 流程 stamp→upgrade
  • company_runner.py:run_company_project(含 persist/publish/heartbeat),暂不接 control
  • companies.py:POST /execute/async、GET /projects/{pid}/events
  • company_ws.py:WS 端点 + 回放 + pub/sub;main.py 注册路由
  • 数据质量:真实 token 透传、duration、updated_at 心跳
  • 前端:route、stores/companyExecution.ts、CompanyControlRoom.vue、WS 客户端、api 封装
  • CompanyBuilder.vue:加"进入控制室"入口,流式执行改走 async

M2 停止 + 边界暂停/恢复

  • ControlChannel + 检查点插桩(615/650/_run_department_stream)
  • companies.py:stop/pause/resume 端点 + Redis flag
  • runner:except CancelledError 落 stopped
  • 前端 ControlBar 按钮 + store actions
  • 后台任务句柄:进程内 dict[pid]=task(用于本 worker 内 join/清理)

M3 跳过部门 / 手动返工

  • skip/rework 端点 + ControlChannel.skipped()/forced_rework()
  • 编排循环消费 skip/forced_rework
  • DAG 节点右键操作 + store actions

M4(增强,改 core.py)

  • control 传入 AgentRuntime;run_stream ReAct 循环加 stop 检查点
  • 快照-重入式真暂停(给 company_projects 加 pause_state,复用 workflow 范式)

6. 测试计划

单元

  • ControlChannel.gate():stop→抛 CancelledError;pause→阻塞至 resume。
  • _next_seq/_envelope/_persist 序号连续、幂等。
  • 迁移 upgrade/downgrade 可逆;downgrade 不误删他表。

集成(pytest + 真实/临时库)

  • POST /execute/async 返回 project_id 且后台任务落库事件。
  • GET /projects/{pid}/events?since_seq= 有序、可分页续传。
  • stop:运行中 stop → 数秒内状态 stopped、无新事件。
  • pause/resume:pause 后 event seq 停增,resume 后继续。
  • 权限:跨用户访问他人项目/控制 → 403。

E2E(手动,按红头指南启动)

  1. manage.ps1 restart 全绿,登录 admin/123456。
  2. 建/选一个公司 → 启动项目 → 控制室实时看到 CEO 全文、部门 ReAct 逐迭代、工具入参出参、产出文件、真实 token。
  3. 运行中刷新页面 → 按 seq 回放 + 继续实时(可恢复)。
  4. 暂停→部门边界卡住;恢复→继续;停止→落 stopped,manage.ps1 status 无残留、supervisor 不误判。
  5. 跳过某部门 / 手动返工 → DAG 与结果生效。
  6. 回归:旧 /execute/stream + CompanyBuilder 原视图仍可用。

7. 灰度 / 回滚

  • 特性开关:company.config.control_room_enabled 或全局 env COMPANY_CONTROL_ROOM=1;关闭时前端隐藏入口、后端仅保留旧 /execute/stream。
  • 兼容:旧接口与旧内联视图全程保留,新链路独立,可随时回退。
  • 回滚:down 迁移仅 drop company_project_events(不动其他表);删新增路由注册即可。

8. 待确认 / 开放问题

  1. 多 uvicorn worker 部署?若是,后台 asyncio 任务方案需评估是否迁 Celery(获得 revoke + 跨进程句柄)。当前若单 worker,asyncio + Redis flag 足够。
  2. 事件落库量:一个大项目可能上千事件,是否需要 payload 大小上限/截断策略与定期归档。
  3. 文件产出预览:是否需要在线读取 ./company_projects/... 文件内容接口(涉及路径安全校验)。
  4. 是否要"多观察者广播"(同一项目多人同时看控制室)——WS manager 已支持,需确认权限粒度。
  5. token 计费口径:真实 token 是否要写回 usage_records 做计量。