feat(roundtable): 业务链条配置增加管理员权限,管理员可配所有链条
- 后端 store/路由:scope=all(仅管理员)列全量;get/update/delete 加 is_admin 旁路,管理员可操作任意 owner 链条且不转移归属;非管理员请求 all 退化为 mine - 前端列表页/会商弹窗:管理员多「全部链条」筛选 tab,canManage=isAdmin||isMine 放开发布/设置/删除 - 编辑器:管理员可见发布按钮;按 chainId 找不到本地列表时直接 getChain 拉取(修复管理员从「全部链条」进编辑器空白),并修掉自身无链条时加载卡死的边界 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
48744441ba
commit
f13a1bb7fe
6 files changed
+113
-45
No files matched your search
@@ -54,8 +54,13 @@ export interface ChainUpdatePayload {
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
/** 列表筛选范围:`mine` = 我的;`public` = 所有已发布的公共链条。 */
|
||||
export type ChainScope = "mine" | "public";
|
||||
/**
|
||||
* 列表筛选范围:
|
||||
* - `mine` = 我的;
|
||||
* - `public` = 所有已发布的公共链条;
|
||||
* - `all` = 所有账号的全部链条(**仅管理员**;后端对非管理员请求 `all` 退化为 `mine`)。
|
||||
*/
|
||||
export type ChainScope = "mine" | "public" | "all";
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/roundtable-chains`;
|
||||
|
||||
@@ -146,7 +151,7 @@ function fromUpdate(p: ChainUpdatePayload) {
|
||||
* - `"public"`:所有已发布的公共链条(任何账号可见 / 可选入研讨)。
|
||||
*/
|
||||
export async function listChains(scope: ChainScope = "mine"): Promise<BusinessChain[]> {
|
||||
const url = scope === "public" ? `${BASE()}?scope=public` : BASE();
|
||||
const url = scope === "mine" ? BASE() : `${BASE()}?scope=${scope}`;
|
||||
const res = await apiFetch(url, { method: "GET" });
|
||||
const data = await readJson<{ chains: ServerChain[] }>(res, "list chains");
|
||||
return (data.chains ?? []).map(toChain);
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { listAgents } from "@/core/agents";
|
||||
|
||||
import { listChains, setChainPublic, type ChainScope } from "../api/chains";
|
||||
@@ -71,7 +72,9 @@ export function BusinessChainListBody({
|
||||
const [chains, setChains] = useState<BusinessChain[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// 筛选:我的 / 公共(已发布)。
|
||||
// 管理员:额外多一个「全部」筛选,且可编辑 / 发布任意账号的链条。
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
// 筛选:我的 / 公共(已发布)/(管理员)全部。
|
||||
const [scope, setScope] = useState<ChainScope>("mine");
|
||||
const [publishingId, setPublishingId] = useState<string | null>(null);
|
||||
// 实时名称表(id 小写 → 最新 name):用于把席位的落库快照名刷新成最新(agent 改名同步)。
|
||||
@@ -147,9 +150,9 @@ export function BusinessChainListBody({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 筛选:我的 / 公共 */}
|
||||
{/* 筛选:我的 / 公共 /(管理员)全部 */}
|
||||
<div className="shrink-0 flex items-center gap-1 rounded-lg border border-border bg-muted/40 p-0.5 self-start">
|
||||
{(["mine", "public"] as const).map((s) => (
|
||||
{((isAdmin ? ["mine", "public", "all"] : ["mine", "public"]) as ChainScope[]).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
@@ -161,7 +164,7 @@ export function BusinessChainListBody({
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{s === "mine" ? "我的" : "公共"}
|
||||
{s === "mine" ? "我的" : s === "public" ? "公共" : "全部"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -195,6 +198,7 @@ export function BusinessChainListBody({
|
||||
<ChainRow
|
||||
key={chain.id}
|
||||
chain={chain}
|
||||
canManage={isAdmin || chain.isMine !== false}
|
||||
nameById={nameById}
|
||||
publishing={publishingId === chain.id}
|
||||
onSettings={() => onConfigure(chain.id)}
|
||||
@@ -245,18 +249,20 @@ export function BusinessChainListDialog({
|
||||
|
||||
function ChainRow({
|
||||
chain,
|
||||
canManage,
|
||||
nameById,
|
||||
publishing,
|
||||
onSettings,
|
||||
onTogglePublic,
|
||||
}: {
|
||||
chain: BusinessChain;
|
||||
/** 能否编辑 / 发布(本人链条 或 管理员)。 */
|
||||
canManage: boolean;
|
||||
nameById: Map<string, string>;
|
||||
publishing: boolean;
|
||||
onSettings: () => void;
|
||||
onTogglePublic: () => void;
|
||||
}) {
|
||||
const isMine = chain.isMine !== false;
|
||||
// 配置者用户名:后端返回 email,展示时取 @ 前的本地部分更像「用户名」。
|
||||
const ownerName = chain.ownerName ? chain.ownerName.split("@")[0] : "";
|
||||
return (
|
||||
@@ -274,8 +280,8 @@ function ChainRow({
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1.5">
|
||||
{/* 发布 / 取消发布:仅本人的链条可操作。 */}
|
||||
{isMine && (
|
||||
{/* 发布 / 取消发布:本人的链条或管理员可操作。 */}
|
||||
{canManage && (
|
||||
<Button
|
||||
variant={chain.isPublic ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
@@ -291,8 +297,8 @@ function ChainRow({
|
||||
{chain.isPublic ? "取消发布" : "发布"}
|
||||
</Button>
|
||||
)}
|
||||
{/* 设置(编辑):仅本人的链条;公共里别人的链条只读浏览。 */}
|
||||
{isMine && (
|
||||
{/* 设置(编辑):本人的链条或管理员;普通用户看别人的链条只读浏览。 */}
|
||||
{canManage && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -78,6 +78,7 @@ import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize
|
||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||
import { ModelNameSelect } from "@/components/workspace/model-name-select";
|
||||
import { listAgents } from "@/core/agents";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
|
||||
import {
|
||||
filterRecommendCandidates,
|
||||
@@ -86,6 +87,7 @@ import {
|
||||
import {
|
||||
createChain,
|
||||
deleteChain,
|
||||
getChain,
|
||||
listChains,
|
||||
optimizeChainPrompt,
|
||||
setChainPublic,
|
||||
@@ -171,6 +173,8 @@ export default function BusinessChainEditorPage() {
|
||||
// 当前编辑链条的发布状态 / 是否本人所有(发布按钮 + 只读保护用)。
|
||||
const [currentChainPublic, setCurrentChainPublic] = useState(false);
|
||||
const [currentChainMine, setCurrentChainMine] = useState(true);
|
||||
// 管理员:可编辑 / 发布 / 删除任意账号的链条(与「全部链条」筛选配套)。
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
@@ -330,10 +334,23 @@ export default function BusinessChainEditorPage() {
|
||||
if (appliedChainParamRef.current) return;
|
||||
const chainId = searchParams.get("chainId");
|
||||
if (!chainId) return;
|
||||
if (chains.length === 0) return; // 等 refreshChains 拉回列表
|
||||
const chain = chains.find((c) => c.id === chainId);
|
||||
if (chain) loadChain(chain);
|
||||
appliedChainParamRef.current = true; // 命中或链条已不存在,都不再重试
|
||||
appliedChainParamRef.current = true; // 只应用一次,之后不覆盖用户手动切换
|
||||
// 优先用「我的链条」列表里已拉到的对象(省一次请求);不在列表里(管理员从「全部链条」
|
||||
// 点设置 / 公共链条 / 自己没有任何链条)则直接按 id 拉取。后端对管理员放开任意链条、
|
||||
// 对所有人放开已发布公共链条;拉不到则静默保持空白新建态。
|
||||
const local = chains.find((c) => c.id === chainId);
|
||||
if (local) {
|
||||
loadChain(local);
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const chain = await getChain(chainId);
|
||||
if (chain) loadChain(chain);
|
||||
} catch {
|
||||
// 静默:取不到就保持当前(空白)状态。
|
||||
}
|
||||
})();
|
||||
}, [chains, searchParams, loadChain]);
|
||||
|
||||
// ─── 席位增删改(全部委托给 lib/chain-stages 纯函数)────────────────────
|
||||
@@ -987,8 +1004,8 @@ export default function BusinessChainEditorPage() {
|
||||
|
||||
{/* 右侧操作 */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{/* 发布 / 取消发布:仅已保存且本人所有的链条。发布后所有账号可在「公共」筛选查看 / 选用。 */}
|
||||
{currentChainId && currentChainMine && (
|
||||
{/* 发布 / 取消发布:已保存且本人所有(或管理员)的链条。发布后所有账号可在「公共」筛选查看 / 选用。 */}
|
||||
{currentChainId && (currentChainMine || isAdmin) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTogglePublish}
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useRouteNavigate } from "@/core/route-base";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||
@@ -83,6 +84,8 @@ export default function BusinessChainListPage() {
|
||||
const navigate = useRouteNavigate();
|
||||
// 整片脱敏:链条列表页展示文本(链条名/描述/席位名/标题)。
|
||||
const dzRef = useDesensitizeSubtree<HTMLDivElement>();
|
||||
// 管理员:额外多一个「全部链条」筛选,且可编辑 / 发布 / 删除任意账号的链条。
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
|
||||
// 进入编辑器:带 chainId = 打开并切到该链条;不带 = 空白新建。
|
||||
// 路径写 workspace base,`useRouteNavigate` 会按当前路由 base 改写。
|
||||
@@ -314,13 +317,14 @@ export default function BusinessChainListPage() {
|
||||
|
||||
{/* 筛选工具栏 */}
|
||||
<div className="mb-6 flex flex-col gap-4 rounded-2xl border border-zinc-100 bg-white p-4 shadow-sm dark:border-[#1A2F5A] dark:bg-[#122347] lg:flex-row lg:items-center lg:justify-between">
|
||||
{/* 我的 / 公共 */}
|
||||
{/* 我的 / 公共 /(管理员)全部 */}
|
||||
<div className="flex self-start rounded-xl bg-zinc-100 p-1 dark:bg-[#0C1836]">
|
||||
{(
|
||||
[
|
||||
["mine", "我的链条"],
|
||||
["public", "公共列表"],
|
||||
] as const
|
||||
...(isAdmin ? ([["all", "全部链条"]] as const) : []),
|
||||
] as [ChainScope, string][]
|
||||
).map(([key, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
@@ -439,6 +443,7 @@ export default function BusinessChainListPage() {
|
||||
<ChainCard
|
||||
key={chain.id}
|
||||
chain={chain}
|
||||
canManage={isAdmin || chain.isMine !== false}
|
||||
publishing={publishingId === chain.id}
|
||||
onSettings={() => goConfigure(chain.id)}
|
||||
onTogglePublic={() => handleTogglePublic(chain)}
|
||||
@@ -450,6 +455,7 @@ export default function BusinessChainListPage() {
|
||||
) : (
|
||||
<ChainTable
|
||||
chains={visibleChains}
|
||||
canManage={isAdmin}
|
||||
publishingId={publishingId}
|
||||
onSettings={(c) => goConfigure(c.id)}
|
||||
onTogglePublic={handleTogglePublic}
|
||||
@@ -620,18 +626,20 @@ function EmptyState({
|
||||
|
||||
function ChainCard({
|
||||
chain,
|
||||
canManage,
|
||||
publishing,
|
||||
onSettings,
|
||||
onTogglePublic,
|
||||
onDelete,
|
||||
}: {
|
||||
chain: BusinessChain;
|
||||
/** 能否编辑 / 发布 / 删除(本人链条 或 管理员)。 */
|
||||
canManage: boolean;
|
||||
publishing: boolean;
|
||||
onSettings: () => void;
|
||||
onTogglePublic: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const isMine = chain.isMine !== false;
|
||||
const ownerName = chain.ownerName ? chain.ownerName.split("@")[0] : "";
|
||||
|
||||
return (
|
||||
@@ -685,8 +693,8 @@ function ChainCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮:仅本人链条可发布 / 设置 / 删除(公共里别人的链条只读浏览)。 */}
|
||||
{isMine && (
|
||||
{/* 操作按钮:本人链条或管理员可发布 / 设置 / 删除(普通用户看别人的链条只读浏览)。 */}
|
||||
{canManage && (
|
||||
<div className="flex shrink-0 items-center gap-1 opacity-80 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
@@ -892,12 +900,15 @@ function dotClass(avatarType: string): string {
|
||||
|
||||
function ChainTable({
|
||||
chains,
|
||||
canManage,
|
||||
publishingId,
|
||||
onSettings,
|
||||
onTogglePublic,
|
||||
onDelete,
|
||||
}: {
|
||||
chains: BusinessChain[];
|
||||
/** 管理员:可管理任意链条(即使非本人)。 */
|
||||
canManage: boolean;
|
||||
publishingId: string | null;
|
||||
onSettings: (c: BusinessChain) => void;
|
||||
onTogglePublic: (c: BusinessChain) => void;
|
||||
@@ -919,7 +930,7 @@ function ChainTable({
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 text-xs text-zinc-800 dark:divide-[#1A2F5A] dark:text-[#C5CFE0]">
|
||||
{chains.map((chain) => {
|
||||
const isMine = chain.isMine !== false;
|
||||
const isMine = canManage || chain.isMine !== false;
|
||||
const ownerName = chain.ownerName ? chain.ownerName.split("@")[0] : "—";
|
||||
return (
|
||||
<tr
|
||||
|
||||
Reference in new issue
Block a user