298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
/**
|
|
* Backend-persisted business-chain storage for /page/roundtable/planning.
|
|
*
|
|
* A business chain is a named, reusable, *ordered* list of agents. Persisted
|
|
* server-side (MySQL) bound to the logged-in user, so it follows the account
|
|
* across devices/sessions. Backend: `/api/roundtable-chains` (see
|
|
* `app/gateway/routers/roundtable_chains.py`).
|
|
*
|
|
* snake_case ⇄ camelCase mapping is collapsed at this boundary (mirroring
|
|
* `api/drafts.ts`) so callers stay clean.
|
|
*/
|
|
|
|
import { apiFetch } from "@/core/api/fetch-client";
|
|
import { getBackendBaseURL } from "@/core/config";
|
|
import { OptType, writeYLog } from "@/core/log/y-log";
|
|
|
|
import type { BusinessChain, ChainSeat } from "@/roundtable-planning/lib/business-chain";
|
|
import type { SeatMode } from "@/roundtable-planning/lib/seat-mode";
|
|
|
|
/** Fields accepted when creating a chain. */
|
|
export interface ChainCreatePayload {
|
|
/** Optional stable id so the client can keep one id across create + updates. */
|
|
id?: string;
|
|
title: string;
|
|
description?: string;
|
|
seats: ChainSeat[];
|
|
/** Optional layered orchestration (agent_id groups). null/absent = linear. */
|
|
stages?: string[][] | null;
|
|
/** Optional per-stage goals aligned to `stages` (dag-only). null = none. */
|
|
stageGoals?: (string | null)[] | null;
|
|
/** Optional coordinator orchestration prompt (dag-only). null/"" = none. */
|
|
coordinatorPrompt?: string | null;
|
|
/** 默认席位执行模式(flash/thinking/pro/ultra);null = 未设。 */
|
|
seatMode?: SeatMode | null;
|
|
/** 研讨前并行取数开关(逐条 opt-in);缺省 false。 */
|
|
gatherFirst?: boolean;
|
|
}
|
|
|
|
/** Partial update — send only what changed. */
|
|
export interface ChainUpdatePayload {
|
|
title?: string;
|
|
description?: string;
|
|
seats?: ChainSeat[];
|
|
/** Send `null` to clear layering back to linear; omit to leave unchanged. */
|
|
stages?: string[][] | null;
|
|
/** Per-stage goals aligned to `stages`; send `null` to clear, omit to leave. */
|
|
stageGoals?: (string | null)[] | null;
|
|
/** 写给总控的编排提示;send `null`/"" to clear, omit to leave. */
|
|
coordinatorPrompt?: string | null;
|
|
/** 默认席位执行模式;send `null` to clear, omit to leave. */
|
|
seatMode?: SeatMode | null;
|
|
/** 研讨前并行取数开关;send true/false 改,omit 不改。 */
|
|
gatherFirst?: boolean;
|
|
/** 发布开关:true 发布为公共 / false 取消发布;omit 不改。仅 owner 生效。 */
|
|
isPublic?: boolean;
|
|
/** 启停开关:true 启用 / false 停用;omit 不改。仅 owner / 管理员生效。 */
|
|
enabled?: boolean;
|
|
}
|
|
|
|
/**
|
|
* 列表筛选范围:
|
|
* - `mine` = 我的;
|
|
* - `public` = 所有已发布的公共链条;
|
|
* - `all` = 所有账号的全部链条(**仅管理员**;后端对非管理员请求 `all` 退化为 `mine`)。
|
|
*/
|
|
export type ChainScope = "mine" | "public" | "all";
|
|
|
|
const BASE = () => `${getBackendBaseURL()}/api/roundtable-chains`;
|
|
|
|
async function readJson<T>(res: Response, ctx: string): Promise<T> {
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => "");
|
|
throw new Error(`${ctx} failed (${res.status}): ${text}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
// ── server ⇄ client field mapping ──────────────────────────────────────────
|
|
// Backend uses snake_case (created_at / updated_at); the rest of the frontend
|
|
// reads camelCase. `seats` is already camel-clean ({agent_id, name, description}).
|
|
|
|
interface ServerChain {
|
|
id: string;
|
|
title: string;
|
|
description: string | null;
|
|
seats: ChainSeat[];
|
|
stages?: string[][] | null;
|
|
stage_goals?: (string | null)[] | null;
|
|
coordinator_prompt?: string | null;
|
|
seat_mode?: string | null;
|
|
gather_first?: boolean;
|
|
is_public?: boolean;
|
|
enabled?: boolean;
|
|
is_mine?: boolean;
|
|
owner_name?: string | null;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
function toChain(s: ServerChain): BusinessChain {
|
|
return {
|
|
id: s.id,
|
|
title: s.title ?? "",
|
|
description: s.description ?? undefined,
|
|
seats: Array.isArray(s.seats) ? s.seats : [],
|
|
stages: Array.isArray(s.stages) ? s.stages : null,
|
|
stageGoals: Array.isArray(s.stage_goals) ? s.stage_goals : null,
|
|
coordinatorPrompt: s.coordinator_prompt ?? null,
|
|
seatMode: (s.seat_mode as SeatMode | null) ?? undefined,
|
|
gatherFirst: s.gather_first ?? false,
|
|
isPublic: s.is_public ?? false,
|
|
enabled: s.enabled ?? true,
|
|
// 后端缺省时按「我的」处理(向后兼容旧响应)。
|
|
isMine: s.is_mine ?? true,
|
|
ownerName: s.owner_name ?? undefined,
|
|
createdAt: s.created_at ?? undefined,
|
|
updatedAt: s.updated_at ?? undefined,
|
|
};
|
|
}
|
|
|
|
function fromCreate(p: ChainCreatePayload) {
|
|
return {
|
|
...(p.id ? { id: p.id } : {}),
|
|
title: p.title,
|
|
...(p.description !== undefined ? { description: p.description } : {}),
|
|
seats: p.seats,
|
|
...(p.stages !== undefined ? { stages: p.stages } : {}),
|
|
...(p.stageGoals !== undefined ? { stage_goals: p.stageGoals } : {}),
|
|
...(p.coordinatorPrompt !== undefined ? { coordinator_prompt: p.coordinatorPrompt } : {}),
|
|
...(p.seatMode !== undefined ? { seat_mode: p.seatMode } : {}),
|
|
...(p.gatherFirst !== undefined ? { gather_first: p.gatherFirst } : {}),
|
|
};
|
|
}
|
|
|
|
function fromUpdate(p: ChainUpdatePayload) {
|
|
const out: Record<string, unknown> = {};
|
|
if (p.title !== undefined) out.title = p.title;
|
|
if (p.description !== undefined) out.description = p.description;
|
|
if (p.seats !== undefined) out.seats = p.seats;
|
|
// Forward stages even when explicitly null (clears layering server-side).
|
|
if (p.stages !== undefined) out.stages = p.stages;
|
|
// Forward stage goals even when explicitly null (clears goals server-side).
|
|
if (p.stageGoals !== undefined) out.stage_goals = p.stageGoals;
|
|
if (p.coordinatorPrompt !== undefined) out.coordinator_prompt = p.coordinatorPrompt;
|
|
if (p.seatMode !== undefined) out.seat_mode = p.seatMode;
|
|
if (p.gatherFirst !== undefined) out.gather_first = p.gatherFirst;
|
|
if (p.isPublic !== undefined) out.is_public = p.isPublic;
|
|
if (p.enabled !== undefined) out.enabled = p.enabled;
|
|
return out;
|
|
}
|
|
|
|
// ── CRUD ────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* List chains, newest first. `scope`:
|
|
* - `"mine"`(默认):当前用户自己的链条;
|
|
* - `"public"`:所有已发布的公共链条(任何账号可见 / 可选入研讨)。
|
|
*/
|
|
export async function listChains(scope: ChainScope = "mine"): Promise<BusinessChain[]> {
|
|
const url = scope === "mine" ? BASE() : `${BASE()}?scope=${scope}`;
|
|
const res = await apiFetch(url, { method: "GET" });
|
|
const data = await readJson<{ chains: ServerChain[] }>(res, "list chains");
|
|
const chains = (data.chains ?? []).map(toChain);
|
|
writeYLog({
|
|
optType: OptType.search,
|
|
optDetail: "CM助手-全量-业务链条-查询",
|
|
url,
|
|
reqParam: { scope },
|
|
returnParam: { count: chains.length },
|
|
});
|
|
return chains;
|
|
}
|
|
|
|
/** 发布 / 取消发布一条链条(仅 owner 生效)。返回更新后的链条,404 返回 null。 */
|
|
export async function setChainPublic(id: string, isPublic: boolean): Promise<BusinessChain | null> {
|
|
return updateChain(id, { isPublic });
|
|
}
|
|
|
|
/** 启用 / 停用一条链条(仅 owner / 管理员生效)。返回更新后的链条,404 返回 null。 */
|
|
export async function setChainEnabled(id: string, enabled: boolean): Promise<BusinessChain | null> {
|
|
return updateChain(id, { enabled });
|
|
}
|
|
|
|
/** Fetch one chain. Returns null on 404. */
|
|
export async function getChain(id: string): Promise<BusinessChain | null> {
|
|
const res = await apiFetch(`${BASE()}/${encodeURIComponent(id)}`, { method: "GET" });
|
|
if (res.status === 404) return null;
|
|
const data = await readJson<ServerChain>(res, "get chain");
|
|
const chain = toChain(data);
|
|
writeYLog({
|
|
optType: OptType.view,
|
|
optDetail: "CM助手-全量-业务链条-查看",
|
|
url: `${BASE()}/${id}`,
|
|
reqParam: { id },
|
|
returnParam: { id: chain.id, title: chain.title },
|
|
});
|
|
return chain;
|
|
}
|
|
|
|
export async function createChain(payload: ChainCreatePayload): Promise<BusinessChain> {
|
|
const res = await apiFetch(BASE(), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(fromCreate(payload)),
|
|
});
|
|
const data = await readJson<ServerChain>(res, "create chain");
|
|
const chain = toChain(data);
|
|
writeYLog({
|
|
optType: OptType.create,
|
|
optDetail: "CM助手-全量-业务链条-新建",
|
|
url: BASE(),
|
|
reqParam: { title: payload.title, seats: payload.seats?.length ?? 0 },
|
|
returnParam: { id: chain.id, title: chain.title },
|
|
});
|
|
return chain;
|
|
}
|
|
|
|
export async function updateChain(id: string, patch: ChainUpdatePayload): Promise<BusinessChain | null> {
|
|
const res = await apiFetch(`${BASE()}/${encodeURIComponent(id)}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(fromUpdate(patch)),
|
|
});
|
|
if (res.status === 404) return null;
|
|
const data = await readJson<ServerChain>(res, "update chain");
|
|
const chain = toChain(data);
|
|
const enabledToggle = patch.enabled === true || patch.enabled === false;
|
|
writeYLog({
|
|
optType: enabledToggle
|
|
? patch.enabled
|
|
? OptType.enabled
|
|
: OptType.disabled
|
|
: patch.isPublic === true
|
|
? OptType.enabled
|
|
: patch.isPublic === false
|
|
? OptType.disabled
|
|
: OptType.edit,
|
|
optDetail: enabledToggle
|
|
? patch.enabled
|
|
? "CM助手-全量-业务链条-启用"
|
|
: "CM助手-全量-业务链条-停用"
|
|
: patch.isPublic === true
|
|
? "CM助手-全量-业务链条-发布"
|
|
: patch.isPublic === false
|
|
? "CM助手-全量-业务链条-取消发布"
|
|
: "CM助手-全量-业务链条-编辑",
|
|
url: `${BASE()}/${id}`,
|
|
reqParam: { id, ...patch },
|
|
returnParam: { id: chain.id, title: chain.title },
|
|
});
|
|
return chain;
|
|
}
|
|
|
|
export async function deleteChain(id: string): Promise<void> {
|
|
const res = await apiFetch(`${BASE()}/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
if (!res.ok && res.status !== 404) {
|
|
const text = await res.text().catch(() => "");
|
|
throw new Error(`delete chain failed (${res.status}): ${text}`);
|
|
}
|
|
writeYLog({
|
|
optType: OptType.delete,
|
|
optDetail: "CM助手-全量-业务链条-删除",
|
|
url: `${BASE()}/${id}`,
|
|
reqParam: { id },
|
|
});
|
|
}
|
|
|
|
// ── 提示词优化 ───────────────────────────────────────────────────────────────
|
|
|
|
export interface OptimizeChainPromptRequest {
|
|
/** 用户当前写的提示词原文(非空)。 */
|
|
promptText: string;
|
|
/** coordinator = 总控提示词;stage_goal = 某个 Stage 的阶段目标。 */
|
|
kind: "coordinator" | "stage_goal";
|
|
/** 链条结构上下文(标题 / 各 Stage 席位与目标),帮助模型贴着链条优化。 */
|
|
context?: string;
|
|
/** 可选模型覆盖,缺省由后端用 config models[0]。 */
|
|
modelName?: string;
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
/** 调后端单次 LLM 重写提示词,返回优化后的全文(失败抛错,错误信息可直接 toast)。 */
|
|
export async function optimizeChainPrompt(req: OptimizeChainPromptRequest): Promise<string> {
|
|
const res = await apiFetch(`${BASE()}/optimize-prompt`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
prompt_text: req.promptText,
|
|
kind: req.kind,
|
|
context: req.context ?? "",
|
|
...(req.modelName ? { model_name: req.modelName } : {}),
|
|
}),
|
|
signal: req.signal,
|
|
});
|
|
const data = await readJson<{ optimized: string }>(res, "optimize chain prompt");
|
|
return data.optimized ?? "";
|
|
}
|