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>
165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
/**
|
||
* 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 };
|
||
}
|