feat(maf): conversation-driven MAF roundtable (R1/R2) + server-owned stage-3 dispatch

Backend:
- Conversation API at /api/maf-roundtable/conversations: create/list/detail,
  paginated messages, atomic send+run start, owner-only isolation,
  admin read-only diagnostic access
- SSE real-time event stream (GET /conversations/{id}/stream) backed by
  ConversationEventHub + conversation_events store; multi-worker deployment doc
- Deterministic server-owned stage-3 dispatch contracts (dispatch_contracts.py):
  routing resolved from trusted MAF deployment config instead of manager-chosen
  agent ids; config.yaml keeps stage-3 routing deployment-owned
  (default_manager_agent_id / seat ids / business_routes / max_seats_per_run)
- Conversation persistence (conversations/messages stores) + v2 migration script
- Tests: conversations, conversation stream, migration, phase3 conversation dispatch

Frontend:
- Vitest test infra for MAF (vitest.maf.config.ts + test:maf script) with
  component tests for ApprovalCard/RunHeader/SessionsPage and use-maf-events
- Session form lib, flow-graph dialog / run-detail / sessions page updates,
  routes and nav tweaks
- Docs: R0 legacy interaction baseline mapping, updated implementation plan

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-03 12:09:47 +08:00
1 parent df408831e5
commit 99642fca6c
46 files changed
+6358 -1306

No files matched your search

@@ -0,0 +1,164 @@
/**
* MAF 会商创建表单的纯逻辑层。
*
* 把「从 health 默认值初始化表单」「校验最大轮数」「经理互斥过滤席位」「构建只含改写项的
* 请求体」等逻辑抽成纯函数,作为组件与测试的共同事实源。组件不得在内部复制这些逻辑,
* 测试必须 import 这里的真实函数,避免「实现坏掉、测试仍通过」。
*/
export interface HealthDefaults {
default_mode?: string;
default_write_policy?: string;
default_max_rounds?: number;
}
export interface SessionFormState {
title: string;
taskText: string;
mode: string;
managerAgentId: string;
seatAgentIds: string[];
modelName: string;
reasoningEffort: string;
/** 文本输入,空串表示「使用默认」。 */
maxRounds: string;
writePolicy: string;
showAdvanced: boolean;
}
/**
* 根据 health 返回的后端默认值创建一份干净的初始表单。
* - mode 默认取 health.default_mode(兜底 structured);
* - writePolicy 默认取 health.default_write_policy(空串表示使用默认);
* - maxRounds 默认取 health.default_max_rounds 的字符串(空串表示使用默认)。
*
* 注意:mode 即便来自 health 也只用作 UI 初始展示;提交时是否真的传回后端,
* 由 buildSessionOverrides 按「是否被改写」决定(见下)。
*/
export function createInitialSessionForm(
health: HealthDefaults | null | undefined,
): SessionFormState {
return {
title: "",
taskText: "",
mode: health?.default_mode ?? "structured",
managerAgentId: "",
seatAgentIds: [],
modelName: "",
reasoningEffort: "",
maxRounds:
typeof health?.default_max_rounds === "number"
? String(health.default_max_rounds)
: "",
writePolicy: health?.default_write_policy ?? "",
showAdvanced: false,
};
}
export interface MaxRoundsValidation {
valid: boolean;
value?: number;
error?: string;
}
/**
* 校验最大轮数输入。空串视为有效(= 使用后端默认)。必须是 1–64 的整数。
*/
export function validateMaxRounds(input: string): MaxRoundsValidation {
const trimmed = input.trim();
if (!trimmed) return { valid: true };
const n = Number(trimmed);
if (!Number.isInteger(n) || n < 1 || n > 64) {
return { valid: false, error: "最大轮数必须为 1–64 的整数" };
}
return { valid: true, value: n };
}
/**
* 经理与席位互斥(后端参与者唯一约束):切换经理时,从已选席位中移除该 agent。
*/
export function filterSeatsForManager(seats: string[], managerId: string): string[] {
if (!managerId) return seats;
return seats.filter((id) => id !== managerId);
}
/**
* 切换某个席位的选择(toggle)。若该 agent 已是经理,则拒绝(返回原数组)。
*/
export function toggleSeat(
seats: string[],
agentId: string,
managerAgentId: string,
): string[] {
if (agentId === managerAgentId) return seats;
return seats.includes(agentId)
? seats.filter((id) => id !== agentId)
: [...seats, agentId];
}
/**
* 展示一行只读的后端默认摘要(高级项折叠时用)。值只来自 health,绝不硬编码。
*/
export function defaultsSummaryLine(health: HealthDefaults | null | undefined): string {
if (!health) return "正在读取后端默认配置…";
return `当前后端默认:${health.default_mode} | ${health.default_write_policy} | 最大 ${health.default_max_rounds} 轮`;
}
export interface SessionOverrides {
mode?: "structured" | "group_chat";
writePolicy?: "auto" | "confirm_high_risk" | "confirm_all_writes";
maxRounds?: number;
}
/**
* 只产出「用户相对后端默认确实改写了」的字段。
*
* 设计:用户没改写 → 不传 → 后端决定(符合「最好不传对应字段」)。
* 模式必须是 structured/group_chat 之一才算有效改写;写入策略必须是合法枚举值;
* 最大轮数走 validateMaxRounds 校验。
*/
export function buildSessionOverrides(
form: Pick<SessionFormState, "mode" | "writePolicy" | "maxRounds">,
defaults: HealthDefaults | null | undefined,
): { overrides: SessionOverrides; maxRoundsError?: string } {
const result: SessionOverrides = {};
let maxRoundsError: string | undefined;
// 模式:仅当与后端默认不同且合法时传
if (form.mode !== (defaults?.default_mode ?? "structured")) {
if (form.mode === "structured" || form.mode === "group_chat") {
result.mode = form.mode;
}
}
// 写入策略:仅当与后端默认不同且合法时传
const policyDefault = defaults?.default_write_policy ?? "";
if (form.writePolicy !== policyDefault) {
if (
form.writePolicy === "auto" ||
form.writePolicy === "confirm_high_risk" ||
form.writePolicy === "confirm_all_writes"
) {
result.writePolicy = form.writePolicy;
}
// 空串 = 使用默认,不传
}
// 最大轮数:空串不传;非空走校验
const roundsValidation = validateMaxRounds(form.maxRounds);
if (form.maxRounds.trim()) {
if (!roundsValidation.valid) {
maxRoundsError = roundsValidation.error;
} else if (roundsValidation.value !== undefined) {
const defaultRounds =
typeof defaults?.default_max_rounds === "number"
? defaults.default_max_rounds
: null;
if (defaultRounds === null || roundsValidation.value !== defaultRounds) {
result.maxRounds = roundsValidation.value;
}
}
}
return { overrides: result, maxRoundsError };
}