This commit is contained in:
fengxiangdebaozi committed 2026-08-25 17:36:14 +08:00
commit 7cc6aca398
14 files changed
+529 -119

No files matched your search

@@ -366,10 +366,15 @@ export function ArtifactFileDetail({
!useWriteFileStreaming &&
!isSkillFile &&
!isMock;
// 「AI 写作」md 展示开关:只要是可编辑的 markdown 就提供「AI 写作」视图(在 源码/预览 旁多一个
// 切换项),用户可随时切过去体验划词「AI 改写」。默认仍是「预览」,避免编辑器在内容加载期间
// 挂载导致白屏 + 避免复杂 md(表格/公式/HTML)误入有损编辑。
const canUseAiEditor = canEditArtifact;
// 「AI 写作」md 展示开关:在 源码/预览 旁多一个切换项,支持划词「AI 改写」。
// 深度研究等 write-file 虚拟预览没有 artifacts PATCH,但仍应提供该视图(本地 draft);
// 流式写入中不挂载,避免编辑器在内容加载期间白屏。
const canUseAiEditor =
language === "markdown" &&
!isSkillFile &&
!isMock &&
!thread.isLoading &&
(canEditArtifact || useWriteFileStreaming);
const [draftContent, setDraftContent] = useState("");
const [hasLocalEdits, setHasLocalEdits] = useState(false);
const [documentRewriteDialogOpen, setDocumentRewriteDialogOpen] = useState(false);
@@ -393,7 +398,9 @@ export function ArtifactFileDetail({
const maxGeneratedImages = imageCapabilities.data?.imageGeneration?.maxImages ?? 4;
const displayContent = documentRewriteActive
? documentRewrite.run.draft
: (canEditArtifact ? draftContent : (content ?? ""));
: (canEditArtifact || (canUseAiEditor && hasLocalEdits)
? draftContent
: (content ?? ""));
const artifactContentPending =
isCodeFile &&
!useWriteFileStreaming &&
@@ -443,17 +450,26 @@ export function ArtifactFileDetail({
setViewMode("code");
}, [documentRewriteActive]);
useEffect(() => {
if (viewMode !== "edit" || canUseAiEditor) return;
setViewMode(isSupportPreview ? "preview" : "code");
}, [canUseAiEditor, isSupportPreview, viewMode]);
useEffect(() => {
setDraftContent(content ?? "");
setHasLocalEdits(false);
}, [filepathFromProps]);
useEffect(() => {
if (!canEditArtifact || hasLocalEdits || documentRewriteActive) {
if (
(!canEditArtifact && !canUseAiEditor) ||
hasLocalEdits ||
documentRewriteActive
) {
return;
}
setDraftContent(content ?? "");
}, [canEditArtifact, content, documentRewriteActive, hasLocalEdits]);
}, [canEditArtifact, canUseAiEditor, content, documentRewriteActive, hasLocalEdits]);
useEffect(() => {
if (
@@ -1709,8 +1725,9 @@ export function ArtifactFileDetail({
)}
{viewMode === "edit" && canUseAiEditor && !documentRewriteActive && (
<MarkdownAiEditor
// 编辑用原始内容(与源码模式一致,不脱敏);保存仍走头部「保存 Markdown」按钮。
markdown={displayContent ?? ""}
// 编辑用原始内容(与源码模式一致,不脱敏);有 artifacts 保存能力时走头部「保存 Markdown」。
// write-file 虚拟预览(如深度研究)仅本地 draft,切换预览/源码仍可见本次改动。
markdown={(hasLocalEdits || canEditArtifact ? draftContent : content) ?? ""}
onChange={(value) => {
setDraftContent(value);
setHasLocalEdits(true);
@@ -55,15 +55,18 @@ export interface ChainUpdatePayload {
isPublic?: boolean;
/** 启停开关:true 启用 / false 停用;omit 不改。仅 owner / 管理员生效。 */
enabled?: boolean;
/** 「待审核」开关:true 提交审核 / false 清除(管理员发布时随 isPublic=true 一起提交);omit 不改。 */
pendingReview?: boolean;
}
/**
* 列表筛选范围:
* - `mine` = 我的;
* - `public` = 所有已发布的公共链条;
* - `pending` = 所有待审核(pendingReview=true)的链条(**仅管理员**;后端对非管理员退化为 `mine`);
* - `all` = 所有账号的全部链条(**仅管理员**;后端对非管理员请求 `all` 退化为 `mine`)。
*/
export type ChainScope = "mine" | "public" | "all";
export type ChainScope = "mine" | "public" | "pending" | "all";
const BASE = () => `${getBackendBaseURL()}/api/roundtable-chains`;
@@ -91,6 +94,7 @@ interface ServerChain {
gather_first?: boolean;
is_public?: boolean;
enabled?: boolean;
pending_review?: boolean;
is_mine?: boolean;
owner_name?: string | null;
created_at: string | null;
@@ -110,6 +114,8 @@ function toChain(s: ServerChain): BusinessChain {
gatherFirst: s.gather_first ?? false,
isPublic: s.is_public ?? false,
enabled: s.enabled ?? true,
// 「待审核」标记:缺省 false(旧响应兼容)。
pendingReview: s.pending_review ?? false,
// 后端缺省时按「我的」处理(向后兼容旧响应)。
isMine: s.is_mine ?? true,
ownerName: s.owner_name ?? undefined,
@@ -146,6 +152,7 @@ function fromUpdate(p: ChainUpdatePayload) {
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;
if (p.pendingReview !== undefined) out.pending_review = p.pendingReview;
return out;
}
@@ -181,6 +188,16 @@ export async function setChainEnabled(id: string, enabled: boolean): Promise<Bus
return updateChain(id, { enabled });
}
/** 提交审核(owner 在编辑器发起):进入「待审核」,等待管理员发布。返回更新后的链条,404 返回 null。 */
export async function submitChainForReview(id: string): Promise<BusinessChain | null> {
return updateChain(id, { pendingReview: true });
}
/** 管理员发布「待审核」链条:正式发布为公共并清除待审核标记。返回更新后的链条,404 返回 null。 */
export async function approveChainReview(id: string): Promise<BusinessChain | null> {
return updateChain(id, { isPublic: true, pendingReview: false });
}
/** 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" });
@@ -36,7 +36,7 @@ import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize
import { getStoredAuth } from "@/core/auth";
import { listAgents } from "@/core/agents";
import { listChains, setChainEnabled, setChainPublic, type ChainScope } from "../api/chains";
import { listChains, setChainEnabled, setChainPublic, submitChainForReview, updateChain, type ChainScope } from "../api/chains";
import { chainTagClasses, isChainEnabled, type BusinessChain } from "../lib/business-chain";
import { getAvatarTypeFor } from "../lib/roundtable-constants";
@@ -84,6 +84,8 @@ export function BusinessChainListBody({
const [enabledFilter, setEnabledFilter] = useState<EnabledFilter>("all");
const [publishingId, setPublishingId] = useState<string | null>(null);
const [togglingId, setTogglingId] = useState<string | null>(null);
// 发布审核选择弹窗的目标链条(null = 关闭)。草稿点「发布」先选 确认审核 / 直接发布。
const [reviewTarget, setReviewTarget] = useState<BusinessChain | null>(null);
// 实时名称表(id 小写 → 最新 name):用于把席位的落库快照名刷新成最新(agent 改名同步)。
const [nameById, setNameById] = useState<Map<string, string>>(new Map());
@@ -140,25 +142,89 @@ export function BusinessChainListBody({
};
}, [active, scope]);
// 发布入口:已发布 → 直接取消发布(旧行为);审核中 → 撤回审核;草稿 → 弹窗选
// 「确认审核 / 直接发布」(与链条编辑页一致)。
const handleTogglePublic = useCallback(
async (chain: BusinessChain) => {
setPublishingId(chain.id);
try {
const updated = await setChainPublic(chain.id, !chain.isPublic);
if (updated) {
setChains((prev) => prev.map((c) => (c.id === updated.id ? updated : c)));
} else {
await refresh(scope);
// 审核中 → 撤回审核,回到草稿。
if (!chain.isPublic && chain.pendingReview) {
if (!window.confirm(`「${chain.title || "未命名链条"}」正在审核中,确认撤回审核?`)) return;
setPublishingId(chain.id);
try {
const updated = await updateChain(chain.id, { pendingReview: false });
if (updated) {
setChains((prev) => prev.map((c) => (c.id === updated.id ? updated : c)));
} else {
await refresh(scope);
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setPublishingId(null);
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setPublishingId(null);
return;
}
// 已发布 → 直接取消发布。
if (chain.isPublic) {
setPublishingId(chain.id);
try {
const updated = await setChainPublic(chain.id, false);
if (updated) {
setChains((prev) => prev.map((c) => (c.id === updated.id ? updated : c)));
} else {
await refresh(scope);
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setPublishingId(null);
}
return;
}
// 草稿 → 弹窗选择。
setReviewTarget(chain);
},
[refresh, scope],
);
// 弹窗「确认审核」:提交进入待审核,等待管理员发布。
const handleSubmitReview = useCallback(async () => {
const chain = reviewTarget;
if (!chain) return;
setPublishingId(chain.id);
try {
const updated = await submitChainForReview(chain.id);
if (updated) {
setChains((prev) => prev.map((c) => (c.id === updated.id ? updated : c)));
}
setReviewTarget(null);
if (!updated) await refresh(scope);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setPublishingId(null);
}
}, [reviewTarget, refresh, scope]);
// 弹窗「直接发布」:立即对所有账号可见(旧行为)。
const handleDirectPublish = useCallback(async () => {
const chain = reviewTarget;
if (!chain) return;
setPublishingId(chain.id);
try {
const updated = await setChainPublic(chain.id, true);
if (updated) {
setChains((prev) => prev.map((c) => (c.id === updated.id ? updated : c)));
}
setReviewTarget(null);
if (!updated) await refresh(scope);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setPublishingId(null);
}
}, [reviewTarget, refresh, scope]);
const handleToggleEnabled = useCallback(
async (chain: BusinessChain) => {
setTogglingId(chain.id);
@@ -273,6 +339,39 @@ export function BusinessChainListBody({
</div>
)}
</div>
{/* 发布审核选择弹窗:确认审核 → 待审核(管理员发布);直接发布 → 立即公共可见。 */}
<Dialog open={reviewTarget !== null} onOpenChange={(o) => !o && setReviewTarget(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<GlobeIcon className="size-4 text-primary" />
发布业务链条
</DialogTitle>
<DialogDescription>
是否需要审核?确认审核后「{reviewTarget?.title || "未命名链条"}」将进入「待审核」,由管理员在业务链条列表中发布;直接发布则立即对所有账号可见 / 选用。
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setReviewTarget(null)}>
取消
</Button>
<Button
variant="secondary"
onClick={handleDirectPublish}
disabled={reviewTarget ? publishingId === reviewTarget.id : true}
>
直接发布
</Button>
<Button
onClick={handleSubmitReview}
disabled={reviewTarget ? publishingId === reviewTarget.id : true}
>
确认审核
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
@@ -355,6 +454,11 @@ function ChainRow({
公共
</span>
)}
{chain.pendingReview && (
<span className="shrink-0 rounded-full bg-amber-400/10 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-300">
待审核
</span>
)}
{!enabled && (
<span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
已停用
@@ -375,13 +479,20 @@ function ChainRow({
</span>
</label>
)}
{/* 发布 / 取消发布:本人的链条或管理员可操作。 */}
{/* 发布 / 取消发布 / 审核中撤回:本人的链条或管理员可操作。 */}
{canManage && (
<Button
variant={chain.isPublic ? "secondary" : "outline"}
size="sm"
onClick={onTogglePublic}
disabled={publishing}
title={
chain.isPublic
? "取消发布,收回为私有"
: chain.pendingReview
? "审核中,点击撤回审核"
: "发布:可选提交审核(管理员发布)或直接发布"
}
className="h-8 gap-1.5 px-3 text-sm"
>
{publishing ? (
@@ -389,7 +500,7 @@ function ChainRow({
) : (
<GlobeIcon className="size-4" />
)}
{chain.isPublic ? "取消发布" : "发布"}
{chain.isPublic ? "取消发布" : chain.pendingReview ? "审核中" : "发布"}
</Button>
)}
{/* 设置(编辑):本人的链条或管理员;普通用户看别人的链条只读浏览。 */}
@@ -100,6 +100,12 @@ export interface BusinessChain {
gatherFirst?: boolean;
/** 是否已发布为公共(所有账号可在「公共」筛选查看 / 选入研讨)。 */
isPublic?: boolean;
/**
* 「待审核」标记:true = 已在编辑器点「发布 → 确认审核」提交审核,等待管理员在
* 业务链条列表「待审核」筛选里发布;管理员发布时清回 false 并置 isPublic=true。
* 缺省 / false = 未在审核中(草稿或已发布)。
*/
pendingReview?: boolean;
/**
* 启停状态。缺省 / undefined = 启用(存量链条无该字段时按启用处理)。
* 停用后管理弹窗仍可见,选链弹窗 / 公共入口不再选用。
@@ -66,6 +66,15 @@ import {
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -100,6 +109,7 @@ import {
listChains,
optimizeChainPrompt,
setChainPublic,
submitChainForReview,
updateChain,
} from "@/roundtable-planning/api/chains";
import {
@@ -217,6 +227,10 @@ export default function BusinessChainEditorPage() {
const [currentChainId, setCurrentChainId] = useState<string | null>(null);
// 当前编辑链条的发布状态 / 是否本人所有(发布按钮 + 只读保护用)。
const [currentChainPublic, setCurrentChainPublic] = useState(false);
// 「待审核」状态:true = 已提交审核、等待管理员发布(发布按钮变「审核中」)。
const [currentChainPending, setCurrentChainPending] = useState(false);
// 发布审核确认弹窗(发布前选择「确认审核」或「直接发布」)。
const [reviewDialogOpen, setReviewDialogOpen] = useState(false);
const [currentChainMine, setCurrentChainMine] = useState(true);
// 管理员:可编辑 / 发布 / 删除任意账号的链条(与「全部链条」筛选配套)。
const isAdmin = getStoredAuth()?.system_role === "admin";
@@ -349,6 +363,7 @@ export default function BusinessChainEditorPage() {
const loadChain = useCallback((chain: BusinessChain) => {
setCurrentChainId(chain.id);
setCurrentChainPublic(chain.isPublic ?? false);
setCurrentChainPending(chain.pendingReview ?? false);
setCurrentChainMine(chain.isMine !== false);
setTitle(chain.title);
setDescription(chain.description ?? "");
@@ -362,6 +377,7 @@ export default function BusinessChainEditorPage() {
const startNewChain = useCallback(() => {
setCurrentChainId(null);
setCurrentChainPublic(false);
setCurrentChainPending(false);
setCurrentChainMine(true);
setTitle("");
setDescription("");
@@ -476,19 +492,6 @@ export default function BusinessChainEditorPage() {
);
}, []);
const setSeatJobResponsibilitiesCb = useCallback((agentId: string, jobResponsibilities: string) => {
setStageGroups((prev) =>
prev.map((group) => ({
...group,
seats: group.seats.map((seat) =>
seat.agent_id === agentId
? { ...seat, job_responsibilities: jobResponsibilities }
: seat,
),
})),
);
}, []);
// ── 提示词 AI 优化(总控提示词 / 各 Stage 阶段目标)────────────────────────
const [optimizingCoordinator, setOptimizingCoordinator] = useState(false);
const [goalOptimizingIdx, setGoalOptimizingIdx] = useState<number | null>(null);
@@ -694,7 +697,7 @@ export default function BusinessChainEditorPage() {
seat_mode: s.seat_mode ?? null,
// 岗位协同专用岗位归属:普通圆桌忽略,但保存时必须原样保留。
position_id: s.position_id ?? null,
// 业务链席位岗位职责:会商智能体配置面板只读展示。
// 业务链席位岗位职责:编辑器已不提供输入,保存时原样保留存量值(会商智能体配置面板只读展示)。
job_responsibilities: s.job_responsibilities?.trim() || null,
};
});
@@ -777,31 +780,104 @@ export default function BusinessChainEditorPage() {
}
}, [currentChainId, title, startNewChain, refreshChains]);
// 发布 / 取消发布:把当前链条设为公共(所有账号可见 / 选用)或收回。
// 发布入口:已发布 → 收回;审核中 → 撤回审核;未发布 → 弹窗选择「确认审核 / 直接发布」。
const handleTogglePublish = useCallback(async () => {
if (!currentChainId) {
toast.warning("请先保存链条再发布");
return;
}
const next = !currentChainPublic;
// 已发布 → 收回为私有(旧行为不变)。
if (currentChainPublic) {
setPublishing(true);
try {
const updated = await setChainPublic(currentChainId, false);
if (!updated) {
toast.error("链条已不存在,可能被删除");
startNewChain();
await refreshChains();
return;
}
setCurrentChainPublic(updated.isPublic ?? false);
toast.success("已取消发布");
await refreshChains();
} catch (err) {
toast.error(`取消发布失败:${err instanceof Error ? err.message : String(err)}`);
} finally {
setPublishing(false);
}
return;
}
// 审核中 → 撤回审核,回到未发布草稿。
if (currentChainPending) {
if (!window.confirm("该链条正在审核中,确认撤回审核?")) return;
setPublishing(true);
try {
const updated = await updateChain(currentChainId, { pendingReview: false });
if (!updated) {
toast.error("链条已不存在,可能被删除");
startNewChain();
await refreshChains();
return;
}
setCurrentChainPending(updated.pendingReview ?? false);
toast.success("已撤回审核");
await refreshChains();
} catch (err) {
toast.error(`撤回审核失败:${err instanceof Error ? err.message : String(err)}`);
} finally {
setPublishing(false);
}
return;
}
// 未发布 → 弹窗选择审核 / 直接发布。
setReviewDialogOpen(true);
}, [currentChainId, currentChainPublic, currentChainPending, startNewChain, refreshChains]);
// 弹窗「确认审核」:提交进入待审核,等待管理员在业务链条列表「待审核」筛选里发布。
const handleSubmitReview = useCallback(async () => {
if (!currentChainId) return;
setPublishing(true);
try {
const updated = await setChainPublic(currentChainId, next);
const updated = await submitChainForReview(currentChainId);
if (!updated) {
toast.error("链条已不存在,可能被删除");
startNewChain();
await refreshChains();
return;
}
setCurrentChainPublic(updated.isPublic ?? next);
toast.success(next ? "已发布为公共,所有账号都能查看 / 选用" : "已取消发布");
setCurrentChainPending(updated.pendingReview ?? true);
setReviewDialogOpen(false);
toast.success("已提交审核,等待管理员在业务链条列表发布");
await refreshChains();
} catch (err) {
toast.error(`${next ? "发布" : "取消发布"}失败:${err instanceof Error ? err.message : String(err)}`);
toast.error(`提交审核失败:${err instanceof Error ? err.message : String(err)}`);
} finally {
setPublishing(false);
}
}, [currentChainId, currentChainPublic, startNewChain, refreshChains]);
}, [currentChainId, startNewChain, refreshChains]);
// 弹窗「直接发布」:保持旧发布行为,立即对所有账号可见。
const handleDirectPublish = useCallback(async () => {
if (!currentChainId) return;
setPublishing(true);
try {
const updated = await setChainPublic(currentChainId, true);
if (!updated) {
toast.error("链条已不存在,可能被删除");
startNewChain();
await refreshChains();
return;
}
setCurrentChainPublic(updated.isPublic ?? true);
setReviewDialogOpen(false);
toast.success("已发布为公共,所有账号都能查看 / 选用");
await refreshChains();
} catch (err) {
toast.error(`发布失败:${err instanceof Error ? err.message : String(err)}`);
} finally {
setPublishing(false);
}
}, [currentChainId, startNewChain, refreshChains]);
// ─── Render ──────────────────────────────────────────────────────────
const triggerLabel = currentChainId ? title || "未命名链条" : "+ 新建链条…";
@@ -1099,18 +1175,26 @@ export default function BusinessChainEditorPage() {
{/* 右侧操作 */}
<div className="ml-auto flex items-center gap-2">
{/* 发布 / 取消发布:已保存且本人所有(或管理员)的链条。发布后所有账号可在「公共」筛选查看 / 选用。 */}
{/* 发布:未发布 → 弹窗选「确认审核 / 直接发布」;审核中 → 撤回;已发布 → 收回。 */}
{currentChainId && (currentChainMine || isAdmin) && (
<button
type="button"
onClick={handleTogglePublish}
disabled={publishing}
title={currentChainPublic ? "已发布为公共,点击收回为私有" : "发布为公共,所有账号都能查看 / 选用"}
title={
currentChainPublic
? "已发布为公共,点击收回为私有"
: currentChainPending
? "审核中,点击撤回审核"
: "发布为公共:可选提交审核(管理员发布)或直接发布"
}
className={cn(
"flex items-center gap-1.5 border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60",
currentChainPublic
? "border-emerald-400/50 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/15 dark:text-emerald-400"
: "border-border bg-card text-muted-foreground hover:bg-muted hover:text-foreground",
: currentChainPending
? "border-amber-400/50 bg-amber-400/10 text-amber-600 hover:bg-amber-400/15 dark:text-amber-300"
: "border-border bg-card text-muted-foreground hover:bg-muted hover:text-foreground",
R_CTRL,
)}
>
@@ -1119,7 +1203,7 @@ export default function BusinessChainEditorPage() {
) : (
<Globe className="size-4" />
)}
{currentChainPublic ? "已发布" : "发布"}
{currentChainPublic ? "已发布" : currentChainPending ? "审核中" : "发布"}
</button>
)}
<button
@@ -1299,7 +1383,6 @@ export default function BusinessChainEditorPage() {
positionConfigMode={positionConfigMode}
positionOptions={positionOptions}
onSeatPositionChange={setSeatPositionCb}
onSeatJobResponsibilitiesChange={setSeatJobResponsibilitiesCb}
onStageGoalChange={setGoal}
goalOptimizingIdx={goalOptimizingIdx}
onOptimizeGoal={handleOptimizeStageGoal}
@@ -1354,6 +1437,33 @@ export default function BusinessChainEditorPage() {
}}
/>
{/* 发布审核确认弹窗:确认审核 → 进入「待审核」由管理员发布;直接发布 → 立即公共可见。 */}
<Dialog open={reviewDialogOpen} onOpenChange={setReviewDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Globe className="size-4 text-primary" />
发布业务链条
</DialogTitle>
<DialogDescription>
是否需要审核?确认审核后该链条将进入「待审核」,由管理员在业务链条列表中发布;
直接发布则立即对所有账号可见 / 选用。
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setReviewDialogOpen(false)}>
取消
</Button>
<Button variant="secondary" onClick={handleDirectPublish} disabled={publishing}>
直接发布
</Button>
<Button onClick={handleSubmitReview} disabled={publishing}>
确认审核
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 页脚(步骤一):分层编排说明。步骤切换走顶部步骤导航。 */}
{step === 1 && (
<footer className="shrink-0 select-none border-t border-border bg-card px-6 py-3 text-center text-[11px] text-muted-foreground">
@@ -1484,7 +1594,6 @@ function StagesColumn({
positionConfigMode,
positionOptions,
onSeatPositionChange,
onSeatJobResponsibilitiesChange,
onStageGoalChange,
goalOptimizingIdx,
onOptimizeGoal,
@@ -1509,7 +1618,6 @@ function StagesColumn({
/** Live directory from the administrator's position-role configuration. */
positionOptions: readonly PositionOption[];
onSeatPositionChange: (agentId: string, positionId: PositionId | null) => void;
onSeatJobResponsibilitiesChange: (agentId: string, jobResponsibilities: string) => void;
onStageGoalChange: (stageIdx: number, goal: string) => void;
/** 正在 AI 优化阶段目标的 stage 下标(null = 没有进行中的优化)。 */
goalOptimizingIdx: number | null;
@@ -1619,7 +1727,6 @@ function StagesColumn({
positionConfigMode={positionConfigMode}
positionOptions={positionOptions}
onSeatPositionChange={onSeatPositionChange}
onSeatJobResponsibilitiesChange={onSeatJobResponsibilitiesChange}
onGoalChange={(goal) => onStageGoalChange(stageIdx, goal)}
goalOptimizing={goalOptimizingIdx === stageIdx}
anyGoalOptimizing={goalOptimizingIdx !== null}
@@ -1655,7 +1762,6 @@ function StageSection({
positionConfigMode,
positionOptions,
onSeatPositionChange,
onSeatJobResponsibilitiesChange,
onGoalChange,
goalOptimizing,
anyGoalOptimizing,
@@ -1680,7 +1786,6 @@ function StageSection({
positionConfigMode: boolean;
positionOptions: readonly PositionOption[];
onSeatPositionChange: (agentId: string, positionId: PositionId | null) => void;
onSeatJobResponsibilitiesChange: (agentId: string, jobResponsibilities: string) => void;
onGoalChange: (goal: string) => void;
/** 本 Stage 的阶段目标正在 AI 优化中。 */
goalOptimizing: boolean;
@@ -1848,9 +1953,6 @@ function StageSection({
showPositionConfig={positionConfigMode}
positionOptions={positionOptions}
onPositionChange={(positionId) => onSeatPositionChange(seat.agent_id, positionId)}
onJobResponsibilitiesChange={(value) =>
onSeatJobResponsibilitiesChange(seat.agent_id, value)
}
onDetail={() => onSeatDetail(seat.agent_id, seat.name)}
/>
))}
@@ -1880,7 +1982,6 @@ function SeatStrip({
showPositionConfig,
positionOptions,
onPositionChange,
onJobResponsibilitiesChange,
onDetail,
}: {
seat: ChainSeat;
@@ -1905,7 +2006,6 @@ function SeatStrip({
/** Current position directory, injected by the business-chain editor. */
positionOptions: readonly PositionOption[];
onPositionChange: (positionId: PositionId | null) => void;
onJobResponsibilitiesChange: (jobResponsibilities: string) => void;
onDetail: () => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
@@ -1981,17 +2081,6 @@ function SeatStrip({
{seat.description}
</div>
)}
<label className="mt-1.5 flex flex-col gap-0.5">
<span className="text-[10px] font-medium text-muted-foreground">岗位职责</span>
<textarea
value={seat.job_responsibilities ?? ""}
onChange={(event) => onJobResponsibilitiesChange(event.currentTarget.value)}
onPointerDown={(event) => event.stopPropagation()}
placeholder="填写该席位在本业务链中的岗位职责"
rows={2}
className="w-full resize-none rounded border border-border bg-background px-2 py-1 text-[11px] leading-4 text-foreground outline-none placeholder:text-muted-foreground/60 focus:border-primary/50"
/>
</label>
</div>
{/* 岗位协同从专用入口进来时才展示。使用 TDesign Select,未分配不会阻塞普通圆桌使用。 */}
@@ -66,7 +66,7 @@ import { useRouteNavigate } from "@/core/route-base";
import { cn } from "@/lib/utils";
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
import { deleteChain, listChains, setChainPublic, type ChainScope } from "../api/chains";
import { approveChainReview, deleteChain, listChains, setChainPublic, type ChainScope } from "../api/chains";
import { chainTagClasses, type BusinessChain } from "../lib/business-chain";
import { getAvatarTypeFor } from "../lib/roundtable-constants";
@@ -165,18 +165,30 @@ export default function BusinessChainListPage() {
}, []);
// ── 操作:发布 / 取消发布 ────────────────────────────────────────────────
// 「待审核」筛选里 = 管理员审核发布(is_public=true 并清除待审核标记);
// 其它筛选里 = 常规发布 / 取消发布。
const handleTogglePublic = useCallback(
async (chain: BusinessChain) => {
setPublishingId(chain.id);
try {
await setChainPublic(chain.id, !chain.isPublic);
await refresh(scope);
await loadStats();
toast.success(
chain.isPublic
? `已取消发布「${chain.title || "未命名链条"}」`
: `「${chain.title || "未命名链条"}」已发布到公共选用库`,
);
const updated =
scope === "pending"
? await approveChainReview(chain.id)
: await setChainPublic(chain.id, !chain.isPublic);
if (updated) {
await refresh(scope);
await loadStats();
toast.success(
scope === "pending"
? `「${chain.title || "未命名链条"}」审核通过并已发布到公共选用库`
: chain.isPublic
? `已取消发布「${chain.title || "未命名链条"}」`
: `「${chain.title || "未命名链条"}」已发布到公共选用库`,
);
} else {
toast.error("链条已不存在,可能被删除");
await refresh(scope);
}
} catch (err) {
toast.error(`操作失败:${err instanceof Error ? err.message : String(err)}`);
} finally {
@@ -317,13 +329,18 @@ 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", "公共列表"],
...(isAdmin ? ([["all", "全部链条"]] as const) : []),
...(isAdmin
? ([
["pending", "待审核"],
["all", "全部链条"],
] as const)
: []),
] as [ChainScope, string][]
).map(([key, label]) => (
<button
@@ -596,14 +613,20 @@ function EmptyState({
) : (
<>
<h3 className="text-base font-bold text-zinc-700 dark:text-[#C5CFE0]">
{scope === "public" ? "还没有公共链条" : "还没有业务链条"}
{scope === "public"
? "还没有公共链条"
: scope === "pending"
? "暂无待审核链条"
: "还没有业务链条"}
</h3>
<p className="mt-1 pb-4 text-xs leading-relaxed text-zinc-400 dark:text-[#6E88B0]">
{scope === "public"
? "公共链条是其他账号(或你自己)发布出来、所有人可见的链条。去「我的链条」里发布一条试试。"
: "业务链条是可复用的有序智能体编排。点右上角「新建链条」去创建第一条。"}
: scope === "pending"
? "业务链条在编辑器里点「发布 → 确认审核」后会进入这里,由管理员审核发布。"
: "业务链条是可复用的有序智能体编排。点右上角「新建链条」去创建第一条。"}
</p>
{scope !== "public" && (
{scope !== "public" && scope !== "pending" && (
<button
type="button"
onClick={onCreate}
@@ -663,7 +686,11 @@ function ChainCard({
<UsersIcon className="mr-0.5 inline size-3" />
<span>{chain.seats.length} 席位</span>
</span>
{chain.isPublic ? (
{chain.pendingReview ? (
<span className="rounded-full border border-amber-200/60 bg-amber-50 px-2 py-0.5 text-xs font-semibold text-amber-700 dark:border-[#FAAD14]/30 dark:bg-[#FAAD14]/15 dark:text-[#FAAD14]">
待审核
</span>
) : chain.isPublic ? (
<span className="rounded-full border border-emerald-200/40 bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-700 dark:border-[#52C41A]/30 dark:bg-[#52C41A]/15 dark:text-[#52C41A]">
已发布
</span>
@@ -940,6 +967,11 @@ function ChainTable({
<td className="px-6 py-4 align-top">
<div className="flex items-center gap-2 text-sm font-semibold text-zinc-900 transition-colors group-hover:text-[#CF1322] dark:text-[#E8EDF5] dark:group-hover:text-[#FF7875]">
{chain.title || "未命名链条"}
{chain.pendingReview && (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-50 px-1.5 py-0.5 text-[9px] font-medium text-amber-700 dark:bg-[#FAAD14]/15 dark:text-[#FAAD14]">
待审核
</span>
)}
{chain.isPublic && (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-1.5 py-0.5 text-[9px] font-medium text-emerald-600 dark:bg-[#52C41A]/15 dark:text-[#52C41A]">
<GlobeIcon className="size-2.5" />