- 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>
22 KiB
22 KiB
虚拟公司「白盒控制室」详细设计(工程实现规格)
配套文档:
虚拟公司白盒控制室实现方案.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/1062tool_call: name, input(完整入参), iteration — 1124tool_result: name, result(截断 500 字), iteration — 1187approval_required: approval_id, tool_name, args — 1136final: 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(前端或公司层丢弃)
tool_call.input/tool_result.result原样在data里但前端只显示工具名+耗时(CB:1409/1412)。last_thought截断 2000、thinkHistory仅最近 10 条(CB:1403)。- 产出文件
dept_done.result.files前端无文件树/预览/下载。 - token 是事件计数近似(company_orchestrator.py:712
current_used = ... + 1),非真实;真实值在 phasefinal.token_usage,公司层未上抬。 dept_done.duration_ms计时起点不稳(726dept_timings.get(name, now))。- CEO 规划/评分是
await runtime.run()非流式(224/926),过程不可见。 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.pyrun_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。 - 连接后:
- 读
since_seq(客户端可在 query 带);从 DB 查seq > since_seq事件按序send_json(回放)。 redis.pubsub().subscribe("company:events:{pid}"),转发增量。- 收到客户端
{type:"control", action}可代理调用控制(或前端直接走 HTTP,二选一,推荐 HTTP)。 - 项目终态事件后
sendfinal → close。
- 读
- 复用
websocket_manager(app/websocket/manager.py)做连接登记,支持多观察者(同一项目多个前端)。
3.9 数据质量修复
- 心跳:
_heartbeat每 5s / 每 20 事件project.updated_at = func.now(); db.commit()。 - 真实 token:
_run_department_stream捕获 phasefinal.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 inmodels/__init__.py - alembic 迁移(手写,非 autogenerate)+ 按 3.2 流程 stamp→upgrade
company_runner.py:run_company_project(含 persist/publish/heartbeat),暂不接 controlcompanies.py:POST /execute/async、GET /projects/{pid}/eventscompany_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(手动,按红头指南启动)
manage.ps1 restart全绿,登录 admin/123456。- 建/选一个公司 → 启动项目 → 控制室实时看到 CEO 全文、部门 ReAct 逐迭代、工具入参出参、产出文件、真实 token。
- 运行中刷新页面 → 按 seq 回放 + 继续实时(可恢复)。
- 暂停→部门边界卡住;恢复→继续;停止→落 stopped,
manage.ps1 status无残留、supervisor 不误判。 - 跳过某部门 / 手动返工 → DAG 与结果生效。
- 回归:旧
/execute/stream+ CompanyBuilder 原视图仍可用。
7. 灰度 / 回滚
- 特性开关:
company.config.control_room_enabled或全局 envCOMPANY_CONTROL_ROOM=1;关闭时前端隐藏入口、后端仅保留旧/execute/stream。 - 兼容:旧接口与旧内联视图全程保留,新链路独立,可随时回退。
- 回滚:down 迁移仅 drop
company_project_events(不动其他表);删新增路由注册即可。
8. 待确认 / 开放问题
- 多 uvicorn worker 部署?若是,后台 asyncio 任务方案需评估是否迁 Celery(获得 revoke + 跨进程句柄)。当前若单 worker,asyncio + Redis flag 足够。
- 事件落库量:一个大项目可能上千事件,是否需要
payload大小上限/截断策略与定期归档。 - 文件产出预览:是否需要在线读取
./company_projects/...文件内容接口(涉及路径安全校验)。 - 是否要"多观察者广播"(同一项目多人同时看控制室)——WS manager 已支持,需确认权限粒度。
- token 计费口径:真实 token 是否要写回 usage_records 做计量。