Files
deerflow-code/frontend-web/src/strategy-components/api/position-roles.ts
T
2026-07-29 21:53:32 +08:00

182 lines
5.3 KiB
TypeScript

/** 岗位角色配置 API:岗位会商工作视角,不等同于组织岗位/RBAC。 */
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
import { OptType, writeYLog } from "@/core/log/y-log";
export type PositionRoleType = "standard" | "intent";
export interface PositionRole {
id: string;
name: string;
description: string;
roleType: PositionRoleType;
enabled: boolean;
isBuiltin: boolean;
sortOrder: number;
updatedBy?: string | null;
updatedAt?: string | null;
/** True only when the API is unavailable and the UI is showing built-ins. */
isFallback?: boolean;
}
interface ServerPositionRole {
id: string;
name: string;
description: string;
role_type: PositionRoleType;
enabled: boolean;
is_builtin: boolean;
sort_order: number;
updated_by?: string | null;
updated_at?: string | null;
}
const BASE = () => `${getBackendBaseURL()}/api/position-roles`;
export const POSITION_ROLES_QUERY_KEY = ["position-roles"] as const;
/**
* Safe first-run directory for the position-roundtable workbench.
*
* The backend persists exactly the same values in ``position_roles``. Keeping
* this client copy means an old/offline gateway cannot turn the administrator
* page or the roundtable selector into an empty screen during deployment.
*/
export const DEFAULT_POSITION_ROLES: readonly PositionRole[] = [
{
id: "intelligence",
name: "测试分析岗",
description: "识别任务意图、补齐约束并确认业务链条",
roleType: "intent",
enabled: true,
isBuiltin: true,
sortOrder: 0,
},
{
id: "planning",
name: "组织测试岗",
description: "承接上游信息并进行组织测试",
roleType: "standard",
enabled: true,
isBuiltin: true,
sortOrder: 1,
},
{
id: "solution",
name: "方案设计岗",
description: "承接上游结果并形成方案产物",
roleType: "standard",
enabled: true,
isBuiltin: true,
sortOrder: 2,
},
{
id: "evaluation",
name: "综合测试岗",
description: "测试链条产物并提出综合建议",
roleType: "standard",
enabled: true,
isBuiltin: true,
sortOrder: 3,
},
];
function defaultRoles(): PositionRole[] {
return DEFAULT_POSITION_ROLES.map((role) => ({ ...role, isFallback: true }));
}
function fromServer(row: ServerPositionRole): PositionRole {
return {
id: row.id,
name: row.name ?? "",
description: row.description ?? "",
roleType: row.role_type ?? "standard",
enabled: row.enabled ?? true,
isBuiltin: row.is_builtin ?? false,
sortOrder: row.sort_order ?? 0,
updatedBy: row.updated_by ?? null,
updatedAt: row.updated_at ?? null,
};
}
function toServer(row: PositionRole): ServerPositionRole {
return {
id: row.id,
name: row.name,
description: row.description,
role_type: row.roleType,
enabled: row.enabled,
is_builtin: row.isBuiltin,
sort_order: row.sortOrder,
};
}
async function readJson<T>(response: Response, context: string): Promise<T> {
if (!response.ok) {
const payload = await response.json().catch(() => null as unknown);
const detail =
payload && typeof payload === "object" && "detail" in payload
? String((payload as { detail: unknown }).detail)
: `${context} failed (${response.status})`;
throw new Error(detail);
}
return (await response.json()) as T;
}
export async function listPositionRoles(): Promise<PositionRole[]> {
try {
const response = await apiFetch(BASE(), { method: "GET" });
const payload = await readJson<{ roles: ServerPositionRole[] }>(response, "list position roles");
const roles = (payload.roles ?? []).map(fromServer);
// An empty directory is never valid: it means an old gateway is running
// or the first-run seed has not completed yet. Keep the page usable.
return roles.length > 0 ? roles : defaultRoles();
} catch {
// Do not leave administrators with an empty configuration screen when an
// older deployment has not received the new router/migration yet.
return defaultRoles();
}
}
export async function replacePositionRoles(roles: PositionRole[]): Promise<PositionRole[]> {
const response = await apiFetch(BASE(), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roles: roles.map(toServer) }),
});
const payload = await readJson<{ roles: ServerPositionRole[] }>(response, "save position roles");
const saved = (payload.roles ?? []).map(fromServer);
writeYLog({
optType: OptType.edit,
optDetail: "CM助手-岗位角色配置-保存",
url: "/api/position-roles",
reqParam: { roles },
returnParam: saved,
});
return saved;
}
export function usePositionRoles(options?: { enabled?: boolean }) {
return useQuery({
queryKey: POSITION_ROLES_QUERY_KEY,
queryFn: listPositionRoles,
staleTime: 30_000,
refetchOnWindowFocus: false,
enabled: options?.enabled ?? true,
});
}
export function usePositionRoleActions() {
const queryClient = useQueryClient();
return {
save: useMutation({
mutationFn: replacePositionRoles,
onSuccess: (roles) => {
queryClient.setQueryData(POSITION_ROLES_QUERY_KEY, roles);
},
}),
};
}