import { useCallback, useState } from 'react'; import { apiFetch, consoleRpc } from '../console-utils/remoteApi.js'; type PlanningSetupStatus = { projectId: string; ready: boolean; principalId?: string; trustedPackPublisher: boolean; projectSponsor: boolean; catalogReady: boolean; registryRevision: number; activeProfile?: { revisionId: string; digest: string }; planningPolicyReady: boolean; disclosureReady: boolean; disclosureConfigReady: boolean; modelProviderAlias?: string; availableAgents: string[]; availableTools: string[]; availableSkills: string[]; diagnostics: string[]; }; type WorkroomTask = { key: string; title: string; status: string; revision: number; attempt: number; maxAttempts: number; reportRef?: string; terminalReason?: string; currentAssignmentId?: string; blockers?: Array<{ id: string; kind: string; owner: string; reason: string; deadline: number }>; latestProgress?: { summary: string; completedUnits?: number; totalUnits?: number }; }; type WorkroomAssignment = { id: string; taskKey: string; role: string; status: string; owner: string; attempt: number; leaseExpiresAt: number; latestProgress?: { summary: string; completedUnits?: number; totalUnits?: number }; outcome?: string; }; type WorkroomWait = { id: string; taskKey: string; status: string; owner: string; route: string; riskTier: string; deadline: number; }; type RunListItem = { runId: string; projectId: string; status: string; title: string; sequence: number; now: number; cancelRequested: boolean; tasks: Record; assignments: Record; reviewerAssignments: Record; sponsorGates: Record; }; type RunDetail = RunListItem; export default function WorkroomRunsPage() { const [projectId, setProjectId] = useState(''); const [runs, setRuns] = useState([]); const [selected, setSelected] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [detailView, setDetailView] = useState<'tasks' | 'assignments' | 'acceptance'>('tasks'); const [planning, setPlanning] = useState(null); const [planningBusy, setPlanningBusy] = useState(false); const loadPlanning = useCallback(async () => { if (!projectId.trim()) { setError('请输入 projectId'); return; } setPlanningBusy(true); setError(''); try { setPlanning(await consoleRpc('workroom.profile.status', { projectId: projectId.trim(), })); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setPlanningBusy(false); } }, [projectId]); const bootstrapPlanning = useCallback(async () => { if (!planning) return; setPlanningBusy(true); setError(''); try { const next = await consoleRpc('workroom.profile.bootstrap', { operationId: `console:planning-bootstrap:${planning.projectId}:${Date.now()}`, projectId: planning.projectId, expectedRegistryRevision: planning.registryRevision, includeTools: planning.availableTools, includeSkills: planning.availableSkills, }); setPlanning(next); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setPlanningBusy(false); } }, [planning]); const loadRuns = useCallback(async () => { if (!projectId.trim()) { setError('请输入 projectId'); return; } setLoading(true); setError(''); setSelected(null); try { const res = await apiFetch( `/api/agent/workroom/runs?projectId=${encodeURIComponent(projectId.trim())}`, ); const body = await res.json() as { success?: boolean; error?: string; data?: { runs?: RunListItem[] } }; if (!res.ok || !body.success) { throw new Error(body.error ?? `HTTP ${res.status}`); } setRuns(body.data?.runs ?? []); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setRuns([]); } finally { setLoading(false); } }, [projectId]); const loadRunDetail = useCallback(async (runId: string, runProjectId: string) => { setLoading(true); setError(''); try { const res = await apiFetch(`/api/agent/workroom/runs/${encodeURIComponent(runId)}?projectId=${encodeURIComponent(runProjectId)}`); const body = await res.json() as { success?: boolean; error?: string; data?: RunDetail }; if (!res.ok || !body.success || !body.data) { throw new Error(body.error ?? `HTTP ${res.status}`); } setSelected(body.data); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, []); return (
Project control plane

Workroom 控制台

查看 Journal + CAS Kernel 投影出的 Run、Task、Assignment 与验收门;普通聊天不会隐式创建 Workroom。

只读事实投影
运行与持久化配置 ai.sessions.useDatabase · 修改后需重启

数据库 Journal(默认)

`ai.sessions.useDatabase` 不为 false 时写入 `workroom_events`;Database Root Host 未就绪会阻止 candidate generation 发布。

{'ai:\n  sessions:\n    useDatabase: true'}

原子文件 Journal

显式设为 false 后使用 `.zhin/workroom-journal`。后端在进程启动时固定,热重载不会切换事实源。

{'ai:\n  sessions:\n    useDatabase: false'}
配置项属于 Agent session storage;切换 Journal 后端必须重启 Host,不能只做热重载。打开配置编辑器

规划与披露配置

诊断 Catalog、Sponsor、Profile、Planning Policy 与 P12 模型披露 authority,并以当前 generation 的真实能力完成初始化。

{planning ? (

{planning.ready ? 'Workroom 编排已就绪,可以提交 /work' : 'Workroom 编排尚未就绪'}

principal: {planning.principalId ?? '未绑定'} · Profile: {planning.activeProfile?.revisionId ?? '未激活'} · Policy: {planning.planningPolicyReady ? '已发布' : '未发布'} · Disclosure: {planning.disclosureReady ? '已发布' : '未发布'}

{planning.diagnostics.length > 0 ?
    {planning.diagnostics.map(item =>
  • {item}
  • )}
: null} {!planning.principalId || !planning.trustedPackPublisher ? (
首次配置所需 YAML(保存后重启)
{'http:\n  tokens:\n    - token: ${WORKROOM_SPONSOR_TOKEN}\n      scope: full\n      principalId: workroom-admin\nai:\n  workroom:\n    trustedPackPublishers:\n      - workroom-admin'}

重启后用 WORKROOM_SPONSOR_TOKEN 登录 Console,并在 Project sponsors 中加入 workroom-admin。

) : null} {!planning.disclosureReady && planning.modelProviderAlias && !planning.disclosureConfigReady ? (
模型处理方披露契约(保存后重启)

下面的占位符不能原样保存;必须按模型提供商合同与账号级设置填写。外部 Provider 只有在已禁止训练时才能初始化。

{`ai:\n  workroom:\n    disclosure:\n      modelProviders:\n        ${planning.modelProviderAlias}:\n          endpoint: REPLACE_WITH_CONTRACTED_ENDPOINT\n          processingRegions: [REPLACE_WITH_CONTRACTED_REGION]\n          maxConfidentiality: project_internal\n          external: true\n          noTraining: REPLACE_WITH_PROVIDER_GUARANTEE\n          loggingMode: REPLACE_WITH_LOGGING_MODE\n          maximumRetentionSeconds: REPLACE_WITH_MAX_RETENTION_SECONDS\n          allowsRedisclosure: REPLACE_WITH_PROVIDER_GUARANTEE\n          supportsDeletion: REPLACE_WITH_PROVIDER_GUARANTEE`}

OpenRouter 还需在其 Privacy/Guardrail 中启用 ZDR,并禁止 data collection;YAML 声明不会替你修改第三方账号策略。

) : null} {!planning.ready && planning.catalogReady && planning.trustedPackPublisher && planning.projectSponsor ? (

{planning.activeProfile ? planning.planningPolicyReady ? '为当前 Project 发布加密的 P12 模型披露 authority。' : '为当前 active Profile 发布默认 Planning Policy 与 P12 模型披露 authority。' : `将当前 ${planning.availableAgents.length} 个 Agent、${planning.availableTools.length} 个 Tool、${planning.availableSkills.length} 个 Skill 固定进首个 Profile,并发布 Planning Policy 与 P12 披露 authority。`}

) : null}
) : null}
{error ?

{error}

: null}

Runs ({runs.length})

{runs.length === 0 ? (

无数据

) : (
    {runs.map((item) => (
  • ))}
)}

Run 详情

{!selected ? (

选择左侧 Run

) : ( <>
{selected.title}
{selected.runId}
{selected.status}
Sequence
{selected.sequence}
Tasks
{Object.keys(selected.tasks).length}
Assignments
{Object.keys(selected.assignments ?? {}).length}
Acceptance waits
{Object.keys(selected.reviewerAssignments ?? {}).length + Object.keys(selected.sponsorGates ?? {}).length}
{(['tasks', 'assignments', 'acceptance'] as const).map((view) => )}
{detailView === 'tasks' &&
    {Object.values(selected.tasks).map((task) => (
  • {task.key}
    {task.title}
    {task.status}
    rev {task.revision}attempt {task.attempt}/{task.maxAttempts}{task.currentAssignmentId ? assignment {task.currentAssignmentId} : null}
    {task.blockers?.map((blocker) =>
    {blocker.kind} · {blocker.reason}
    owner {blocker.owner}
    )} {task.reportRef ?
    report: {task.reportRef}
    : null} {task.terminalReason ?
    {task.terminalReason}
    : null}
  • ))}
} {detailView === 'assignments' &&
    {Object.values(selected.assignments ?? {}).length === 0 ?
  • 暂无 Assignment
  • : Object.values(selected.assignments ?? {}).map((assignment) => (
  • {assignment.id}
    {assignment.taskKey} · {assignment.role}
    {assignment.status}
    owner {assignment.owner}attempt {assignment.attempt}lease {new Date(assignment.leaseExpiresAt).toLocaleString()}
    {assignment.latestProgress ?
    Progress · {assignment.latestProgress.summary}{assignment.latestProgress.totalUnits ?
    : null}
    : null}
  • ))}
} {detailView === 'acceptance' &&
    {[...Object.values(selected.reviewerAssignments ?? {}), ...Object.values(selected.sponsorGates ?? {})].length === 0 ?
  • 当前没有 Reviewer / Sponsor 等待项
  • : [...Object.values(selected.reviewerAssignments ?? {}), ...Object.values(selected.sponsorGates ?? {})].map((wait) => (
  • {wait.taskKey} · {wait.route}
    {wait.id}
    {wait.status}
    risk {wait.riskTier}owner {wait.owner}deadline {new Date(wait.deadline).toLocaleString()}
  • ))}
} )}
); } function statusTone(status: string): string { if (/completed|accepted|approved|passed|execution_completed/u.test(status)) return 'border-emerald-500/25 bg-emerald-500/5 text-emerald-700 dark:text-emerald-300'; if (/failed|lost|rejected|expired/u.test(status)) return 'border-red-500/25 bg-red-500/5 text-red-700 dark:text-red-300'; if (/blocked|replan|rework|cancell/u.test(status)) return 'border-amber-500/25 bg-amber-500/5 text-amber-700 dark:text-amber-300'; return 'border-sky-500/25 bg-sky-500/5 text-sky-700 dark:text-sky-300'; }