Files
deerflow-code/frontend-web/src/maf-roundtable/lib/session-form.ts
T
董宏凯andCursor 99642fca6c 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>
2026-08-03 12:09:47 +08:00

165 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };
}