+ );
+}
+
+export function AssetLibraryPage({ project, contextOverrides, canApproveAssets = false }) {
const [filter, setFilter] = useState("全部");
const [query, setQuery] = useState("");
const [assets, setAssets] = useState([]);
@@ -206,8 +357,10 @@ export function AssetLibraryPage({ project, contextOverrides }) {
const [showUpload, setShowUpload] = useState(false);
const [showVersions, setShowVersions] = useState(false);
const [showBinding, setShowBinding] = useState(false);
- const [assetForm, setAssetForm] = useState({ name: "", kind: "reference", subtitle: "本地项目资产", tags: "", usage: "当前项目", detail: "", lock: "", rightsStatus: "needs-evidence" });
- const [uploadForm, setUploadForm] = useState({ name: "", kind: "reference", subtitle: "本地导入资产", tags: "本地导入, 待版权证据", usage: "当前项目", detail: "", lock: "", rightsStatus: "needs-evidence" });
+ const [assetForm, setAssetForm] = useState({ name: "", kind: "reference", subtitle: "本地项目资产", tags: "", usage: "当前项目", detail: "", lock: "", rightsStatus: "needs-evidence", sourceType: "original", sourceName: "", sourceRef: "", licenseScope: "project" });
+ const [uploadForm, setUploadForm] = useState({ name: "", kind: "reference", subtitle: "本地导入资产", tags: "本地导入, 待版权证据", usage: "当前项目", detail: "", lock: "", rightsStatus: "needs-evidence", sourceType: "user-upload", sourceName: "", sourceRef: "", licenseScope: "project" });
+ const [rightsForm, setRightsForm] = useState(assetGovernanceDefaults(null));
+ const [reviewHistory, setReviewHistory] = useState([]);
const [pendingFile, setPendingFile] = useState(null);
const [bindingForm, setBindingForm] = useState({ shotId: project.shots[0]?.id || "", usageRole: "continuity" });
const inputRef = useRef(null);
@@ -233,6 +386,11 @@ export function AssetLibraryPage({ project, contextOverrides }) {
useEffect(() => { loadAssets(); }, [project.series?.id, contextOverrides?.organizationId, contextOverrides?.workspaceId, contextOverrides?.projectId]);
+ useEffect(() => {
+ setRightsForm(assetGovernanceDefaults(selected));
+ setReviewHistory(selected?.governanceReviews || []);
+ }, [selected?.id, selected?.currentVersionId]);
+
useEffect(() => {
let cancelled = false;
let objectUrl = "";
@@ -242,7 +400,8 @@ export function AssetLibraryPage({ project, contextOverrides }) {
setPreviewText("");
setPreviewError("");
setPreviewLoading(false);
- if (!selected?.id || !selected.currentVersion?.storage_path || selected.currentVersion.storage_path.endsWith("metadata.json")) return;
+ const storagePath = selected?.currentVersion?.storage_path || "";
+ if (!selected?.id || !storagePath || storagePath.endsWith("metadata.json") || !storagePath.startsWith("storage/")) return;
setPreviewLoading(true);
try {
const payload = await fetchAssetContent(selected.id, contextOverrides);
@@ -280,7 +439,7 @@ export function AssetLibraryPage({ project, contextOverrides }) {
const checked = validateAssetFile(file);
const name = checked.name.replace(/\.[^.]+$/, "") || checked.name;
setPendingFile(checked);
- setUploadForm({ name, kind: inferAssetKind(checked), subtitle: "本地导入资产", tags: "本地导入, 待版权证据", usage: "当前项目", detail: "", lock: "", rightsStatus: "needs-evidence" });
+ setUploadForm({ name, kind: inferAssetKind(checked), subtitle: "本地导入资产", tags: "本地导入, 待版权证据", usage: "当前项目", detail: "", lock: "", rightsStatus: "needs-evidence", sourceType: "user-upload", sourceName: checked.name, sourceRef: `local-upload/${checked.name}`, licenseScope: "project" });
setShowUpload(true);
setNotice("");
} catch (error) {
@@ -316,7 +475,9 @@ export function AssetLibraryPage({ project, contextOverrides }) {
lock: uploadForm.lock,
mimeType: pendingFile.type || "application/octet-stream",
rightsStatus: uploadForm.rightsStatus,
- tags: uploadForm.tags.split(",").map((tag) => tag.trim()).filter(Boolean)
+ tags: uploadForm.tags.split(",").map((tag) => tag.trim()).filter(Boolean),
+ provenance: { sourceType: uploadForm.sourceType, sourceName: uploadForm.sourceName || pendingFile.name, sourceRef: uploadForm.sourceRef },
+ licenseScope: uploadForm.licenseScope
}, contextOverrides);
replaceAsset(asset.asset);
setShowUpload(false);
@@ -334,10 +495,16 @@ export function AssetLibraryPage({ project, contextOverrides }) {
if (!assetForm.name.trim()) return;
setBusy(true);
try {
- const payload = await createAsset({ ...assetForm, name: assetForm.name.trim(), tags: assetForm.tags.split(",").map((tag) => tag.trim()).filter(Boolean) }, contextOverrides);
+ const payload = await createAsset({
+ ...assetForm,
+ name: assetForm.name.trim(),
+ tags: assetForm.tags.split(",").map((tag) => tag.trim()).filter(Boolean),
+ provenance: { sourceType: assetForm.sourceType, sourceName: assetForm.sourceName || assetForm.name.trim(), sourceRef: assetForm.sourceRef },
+ licenseScope: assetForm.licenseScope
+ }, contextOverrides);
replaceAsset(payload.asset);
setShowCreate(false);
- setAssetForm({ name: "", kind: "reference", subtitle: "本地项目资产", tags: "", usage: "当前项目", detail: "", lock: "", rightsStatus: "needs-evidence" });
+ setAssetForm({ name: "", kind: "reference", subtitle: "本地项目资产", tags: "", usage: "当前项目", detail: "", lock: "", rightsStatus: "needs-evidence", sourceType: "original", sourceName: "", sourceRef: "", licenseScope: "project" });
setNotice(`资产“${payload.asset.name}”已创建为 v1 草稿。`);
} catch (error) {
setNotice(error.message);
@@ -468,6 +635,50 @@ export function AssetLibraryPage({ project, contextOverrides }) {
}
}
+ async function loadGovernanceReviews() {
+ if (!selected?.id) return;
+ setBusy(true);
+ try {
+ const payload = await fetchAssetGovernanceReviews(selected.id, contextOverrides);
+ setReviewHistory(payload.reviews || []);
+ setNotice(`${selected.name} 的审核记录已刷新。`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function scanSelectedGovernance() {
+ if (!selected?.id) return;
+ setBusy(true);
+ try {
+ const payload = await scanAssetGovernance(selected.id, assetGovernancePayload(rightsForm, rightsForm.rightsStatus), contextOverrides);
+ const next = replaceAsset(payload.asset);
+ setReviewHistory(next.governanceReviews || []);
+ setNotice(`${selected.name} 风险扫描完成:${payload.scan.status === "blocked" ? "存在阻断项" : payload.scan.status === "review" ? "需要复核" : "低风险"},评分 ${payload.scan.score}。`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function updateSelectedRights(rightsStatus) {
+ if (!selected?.id) return;
+ setBusy(true);
+ try {
+ const payload = await updateAssetRights(selected.id, assetGovernancePayload(rightsForm, rightsStatus), contextOverrides);
+ const next = replaceAsset(payload.asset);
+ setReviewHistory(next.governanceReviews || []);
+ setNotice(`${selected.name} 授权状态已更新为 ${rightsStatus === "approved" ? "已批准" : rightsStatus === "submitted" ? "已提交" : rightsStatus === "rejected" ? "已驳回" : rightsStatus}。`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
return (
setShowVersions(true)} onShowBinding={() => setShowBinding(true)} onToggleLock={toggleLock} />}
- {showUpload && pendingFile && event.target === event.currentTarget && (setShowUpload(false), setPendingFile(null))}>
}
- {showCreate && event.target === event.currentTarget && setShowCreate(false)}>
}
+ {showUpload && pendingFile && (
+ event.target === event.currentTarget && (setShowUpload(false), setPendingFile(null))}>
+
+
+ )}
+ {showCreate && (
+ event.target === event.currentTarget && setShowCreate(false)}>
+
+
+ )}
{showVersions && selected && event.target === event.currentTarget && setShowVersions(false)}>
VERSION HISTORY
{selected.name}
{selected.versions.map((version) =>
v{version.version_number}{selected.currentVersionId === version.id ? " · 当前" : ""}{version.metadata?.versionNote || "版本记录"}
{version.storage_path}{version.content_sha256 ? ` · SHA ${version.content_sha256.slice(0, 12)}…` : ""}{selected.currentVersionId !== version.id &&
}
)}
}
{showBinding && selected && event.target === event.currentTarget && setShowBinding(false)}>
}
diff --git a/src/components/EnterprisePages.jsx b/src/components/EnterprisePages.jsx
index 5928419..e33f132 100644
--- a/src/components/EnterprisePages.jsx
+++ b/src/components/EnterprisePages.jsx
@@ -20,6 +20,7 @@ import {
LogIn,
ListFilter,
Network,
+ Pencil,
Play,
Plus,
RefreshCw,
@@ -70,6 +71,16 @@ import {
createSystemBackup,
probeModel,
registerModel,
+ fetchModelCatalog,
+ createModelCatalog,
+ updateModelCatalog,
+ fetchModelRoutes,
+ createModelRoute,
+ resolveModelRoute,
+ fetchModelRouteApprovals,
+ requestModelRouteApproval,
+ decideModelRouteApproval,
+ updateModelRoute,
retryJob,
revokeAuthSession,
revokeOtherAuthSessions,
@@ -85,6 +96,8 @@ import {
saveSystemConfig,
startMfaSetup,
updateFeatureFlag,
+ createSystemPlanTemplate,
+ updateSystemPlanTemplate,
updateNotificationChannel,
previewInvitation,
registerInvitedUser,
@@ -96,6 +109,8 @@ import {
fetchSsoProviders,
fetchIdentityCenter,
fetchOrganizationCommercial,
+ createCommercialApproval,
+ decideCommercialApproval,
fetchOrganizationUsage,
fetchOrganizationInvoices,
exportOrganizationCommercial,
@@ -109,6 +124,7 @@ import {
resetSystemUserPassword,
revokeSystemUserSessions,
updateOrganizationBilling,
+ updateOrganizationEntitlement,
generateOrganizationInvoice,
updateOrganizationInvoiceStatus,
updateCostCenter,
@@ -146,8 +162,8 @@ import {
import { downloadJson } from "../lib/exporters";
function toneForStatus(status) {
- if (["ready", "active", "completed", "pass", "enforced"].includes(status)) return "ok";
- if (["failed", "blocked", "error", "disabled"].includes(status)) return "warn";
+ if (["ready", "active", "completed", "pass", "enforced", "approved"].includes(status)) return "ok";
+ if (["failed", "blocked", "error", "disabled", "rejected", "expired", "cancelled"].includes(status)) return "warn";
return "neutral";
}
@@ -177,6 +193,13 @@ function SectionBar({ title, detail, action }) {
return {title}
{detail && {detail}}{action}
;
}
+function formatMetricValue(value, digits = 2) {
+ const number = Number(value || 0);
+ if (!Number.isFinite(number)) return String(value ?? "");
+ if (Number.isInteger(number)) return String(number);
+ return number.toFixed(digits).replace(/\.?0+$/, "");
+}
+
function ContextLink({ children, onClick }) {
return ;
}
@@ -240,6 +263,36 @@ const taskPriorityLabels = {
low: "低优先级"
};
+const commercialApprovalTypeLabels = {
+ entitlement_overage: "套餐超额",
+ feature_enablement: "功能开通",
+ budget_increase: "预算提升",
+ external_connector: "外部连接器",
+ compliance_review: "法务复核",
+ delivery_exception: "交付例外",
+ storage_retention: "留存策略"
+};
+
+const commercialApprovalStatusLabels = {
+ submitted: "待审批",
+ approved: "已通过",
+ rejected: "已拒绝",
+ cancelled: "已取消"
+};
+
+const commercialApprovalPriorityLabels = {
+ urgent: "紧急",
+ high: "高",
+ medium: "普通",
+ low: "低"
+};
+
+function approvalStatusTone(status) {
+ if (status === "approved") return "active";
+ if (status === "rejected" || status === "cancelled") return "failed";
+ return "needs-evidence";
+}
+
function formatTaskDue(value, overdue = false) {
if (!value) return "无截止时间";
const date = new Date(value);
@@ -1226,7 +1279,9 @@ export function AdminUsagePage({ contextOverrides, platformContext }) {
const permissions = new Set(platformContext?.context?.permissions || []);
const canManagePlan = permissions.has("billing:manage");
const canManageQuota = permissions.has("quota:manage");
- const [data, setData] = useState({ billing: null, seat: {}, quotas: [], usage: { usage: [] } });
+ const canManageEntitlements = canManageQuota;
+ const canReviewCommercialApprovals = Boolean(platformContext?.context?.systemAdmin || ["billing:manage", "quota:manage", "model:approve", "compliance:manage", "delivery:approve"].some((permission) => permissions.has(permission)));
+ const [data, setData] = useState({ billing: null, seat: {}, quotas: [], usage: { usage: [] }, entitlements: [], entitlementSummary: {}, planTemplates: [], commercialApprovals: [], commercialApprovalSummary: {}, commercialApprovalCatalog: [], commercialApprovalReviewTypes: [] });
const [usageData, setUsageData] = useState({ items: [], summary: { eventCount: 0, totalUnits: 0, totalCost: 0, byKind: [] }, pagination: { page: 1, pageSize: 25, total: 0, totalPages: 0 }, facets: { workspaces: [], projects: [], users: [], kinds: [], costCenters: [] } });
const [invoiceData, setInvoiceData] = useState({ invoices: [], summary: { count: 0, amount: 0, draft: 0, issued: 0, paid: 0, overdue: 0, void: 0 }, pagination: { page: 1, pageSize: 8, total: 0, totalPages: 0 } });
const [invoiceFilters, setInvoiceFilters] = useState({ status: "", query: "", page: 1, pageSize: 8 });
@@ -1238,7 +1293,10 @@ export function AdminUsagePage({ contextOverrides, platformContext }) {
});
const [planForm, setPlanForm] = useState({ planName: "", billingCycle: "monthly", currency: "CNY", baseFee: 0, seatUnitPrice: 0, storageUnitPrice: 0, clipUnitPrice: 0, seatLimit: 0, storageGb: 0, monthlyClipQuota: 0, quotaWarningPercent: 80, localRunnerOnly: true, cloudConnectorsRequireApproval: true });
const [quotaDrafts, setQuotaDrafts] = useState({});
+ const [entitlementDrafts, setEntitlementDrafts] = useState({});
const [costCenterDrafts, setCostCenterDrafts] = useState({});
+ const [approvalDraft, setApprovalDraft] = useState({ requestType: "entitlement_overage", targetKey: "limit.generation_jobs_monthly", requestedValue: 2900, priority: "high", title: "", businessReason: "本地 Runner 试制批次需要追加镜头生成额度,仍遵守单画面、连续性台账和固定声线规则。", evidenceRef: "" });
+ const [approvalDecisionNotes, setApprovalDecisionNotes] = useState({});
const [loading, setLoading] = useState(true);
const [usageLoading, setUsageLoading] = useState(false);
const [busy, setBusy] = useState("");
@@ -1295,6 +1353,7 @@ export function AdminUsagePage({ contextOverrides, platformContext }) {
cloudConnectorsRequireApproval: Boolean(result.billing?.cloud_connectors_require_approval)
});
setQuotaDrafts(Object.fromEntries((result.quotas || []).map((quota) => [quota.id, quota.limitValue])));
+ setEntitlementDrafts(Object.fromEntries((result.entitlements || []).map((entitlement) => [entitlement.key, { limitValue: entitlement.limitValue, enabled: entitlement.enabled, enforcement: entitlement.enforcement, overrideReason: entitlement.overrideReason || "" }])));
setCostCenterDrafts(Object.fromEntries((result.costCenters || []).map((center) => [center.id, center.monthly_budget])));
setNotice("");
} catch (error) {
@@ -1409,6 +1468,78 @@ export function AdminUsagePage({ contextOverrides, platformContext }) {
}
}
+ function setEntitlementDraft(key, patch) {
+ setEntitlementDrafts((current) => ({ ...current, [key]: { ...(current[key] || {}), ...patch } }));
+ }
+
+ async function saveEntitlement(entitlement) {
+ if (!canManageEntitlements) return;
+ const draft = entitlementDrafts[entitlement.key] || {};
+ setBusy(`entitlement-${entitlement.key}`);
+ try {
+ const result = await updateOrganizationEntitlement(organizationId, entitlement.key, {
+ limitValue: Number(draft.limitValue ?? entitlement.limitValue),
+ enabled: draft.enabled ?? entitlement.enabled,
+ enforcement: draft.enforcement || entitlement.enforcement,
+ overrideReason: draft.overrideReason || "管理员手动调整套餐权益"
+ }, contextOverrides);
+ setData(result);
+ setEntitlementDrafts(Object.fromEntries((result.entitlements || []).map((item) => [item.key, { limitValue: item.limitValue, enabled: item.enabled, enforcement: item.enforcement, overrideReason: item.overrideReason || "" }])));
+ setNotice(`${entitlement.label} 权益已更新,并写入审计日志`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy("");
+ }
+ }
+
+ async function submitCommercialApproval(event) {
+ event.preventDefault();
+ if (!organizationId) return;
+ setBusy("commercial-approval-submit");
+ try {
+ await createCommercialApproval(organizationId, {
+ requestType: approvalDraft.requestType,
+ targetKey: approvalDraft.targetKey,
+ requestedValue: Number(approvalDraft.requestedValue || 0),
+ priority: approvalDraft.priority,
+ title: approvalDraft.title,
+ businessReason: approvalDraft.businessReason,
+ evidence: approvalDraft.evidenceRef ? { evidenceRef: approvalDraft.evidenceRef } : {}
+ }, contextOverrides);
+ await load();
+ setNotice("商业治理申请已提交,管理员审批前不会自动放开额度或外部能力");
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy("");
+ }
+ }
+
+ async function decideCommercialRequest(request, decision) {
+ if (!request?.id) return;
+ setBusy(`commercial-approval-${request.id}-${decision}`);
+ try {
+ const result = await decideCommercialApproval(organizationId, request.id, {
+ decision,
+ decisionNote: approvalDecisionNotes[request.id] || (decision === "approved" ? "审批通过,按申请自动同步组织治理策略。" : "审批拒绝,保留原因供申请人调整后重提。")
+ }, contextOverrides);
+ if (result.commercial) {
+ setData(result.commercial);
+ setQuotaDrafts(Object.fromEntries((result.commercial.quotas || []).map((item) => [item.id, item.limitValue])));
+ setEntitlementDrafts(Object.fromEntries((result.commercial.entitlements || []).map((item) => [item.key, { limitValue: item.limitValue, enabled: item.enabled, enforcement: item.enforcement, overrideReason: item.overrideReason || "" }])));
+ setCostCenterDrafts(Object.fromEntries((result.commercial.costCenters || []).map((item) => [item.id, item.monthly_budget])));
+ } else {
+ await load();
+ }
+ setNotice(decision === "approved" ? "申请已通过,自动生效项已写入后台策略和审计日志" : decision === "cancelled" ? "申请已取消" : "申请已拒绝并保留审批记录");
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy("");
+ }
+ }
+
async function saveCostCenter(center) {
if (!canManagePlan) return;
setBusy(center.id);
@@ -1505,6 +1636,35 @@ export function AdminUsagePage({ contextOverrides, platformContext }) {
const invoicePageSize = Number(invoicePagination.pageSize || 8);
const invoiceStart = invoiceTotal ? (invoicePage - 1) * invoicePageSize + 1 : 0;
const invoiceEnd = Math.min(invoicePage * invoicePageSize, invoiceTotal);
+ const entitlementSummary = data.entitlementSummary || {};
+ const planTemplates = data.planTemplates || [];
+ const entitlements = data.entitlements || [];
+ const entitlementCategories = [
+ ["tenant", "租户与组织"],
+ ["production", "生产流水线"],
+ ["knowledge", "知识库"],
+ ["modelops", "模型中台"],
+ ["delivery", "交付"],
+ ["storage", "存储"],
+ ["system", "系统集成"],
+ ["feature", "功能开关"]
+ ].filter(([key]) => entitlements.some((entitlement) => entitlement.category === key));
+ const commercialApprovalSummary = data.commercialApprovalSummary || {};
+ const commercialApprovals = data.commercialApprovals || [];
+ const commercialApprovalCatalog = data.commercialApprovalCatalog || [];
+ function targetsForApprovalType(requestType) {
+ if (requestType === "budget_increase") return (data.costCenters || []).map((center) => ({ value: center.code, label: `${center.name} · 当前 ${formatMetricValue(center.monthly_budget)} ${center.currency}`, suggested: Number(center.monthly_budget || 0) + 100, current: Number(center.monthly_budget || 0) }));
+ if (requestType === "feature_enablement" || requestType === "external_connector") return entitlements.filter((item) => item.key.startsWith("feature.")).map((item) => ({ value: item.key, label: `${item.label} · ${item.enabled ? "已开通" : "未开通"}`, suggested: 1, current: item.enabled ? 1 : 0 }));
+ if (requestType === "compliance_review") return [{ value: "commercial-rights-review", label: "商用版权 / 原创素材复核", suggested: 1, current: 0 }, { value: "voice-consent-review", label: "固定声线授权复核", suggested: 1, current: 0 }, { value: "single-frame-contract-review", label: "单画面生成契约复核", suggested: 1, current: 0 }];
+ return entitlements.filter((item) => !item.key.startsWith("feature.")).map((item) => ({ value: item.key, label: `${item.label} · 当前 ${formatMetricValue(item.limitValue)} ${item.unit}`, suggested: Number(item.limitValue || 0) + Math.max(1, Math.ceil(Number(item.limitValue || 0) * 0.2)), current: Number(item.limitValue || 0) }));
+ }
+ const commercialApprovalTargets = targetsForApprovalType(approvalDraft.requestType);
+ const currentApprovalTarget = commercialApprovalTargets.find((item) => item.value === approvalDraft.targetKey) || commercialApprovalTargets[0] || null;
+ const currentUserId = platformContext?.context?.currentUser?.id;
+ function changeApprovalType(requestType) {
+ const targets = targetsForApprovalType(requestType);
+ setApprovalDraft((current) => ({ ...current, requestType, targetKey: targets[0]?.value || "", requestedValue: targets[0]?.suggested ?? current.requestedValue }));
+ }
return (
} />
@@ -1513,14 +1673,49 @@ export function AdminUsagePage({ contextOverrides, platformContext }) {
.85 ? "warn" : "ok"} />
- .85 ? "warn" : "ok"} />
+ .85 ? "warn" : "ok"} />
已占用{seat.active || 0}
待入组{seat.pending || 0}
可用{seat.remaining || 0}
新邀请和 SSO/SCIM 入组都会经过席位检查;达到上限时由后端返回 `409 seat_limit_reached`。
- 工作区指标已用上限剩余周期操作
{(data.quotas || []).map((quota) =>
{quota.workspaceName}{quota.metric === "clip" ? "生成片段" : quota.metric === "storage" ? "存储" : quota.metric}{quota.usedValue} {quota.unit}{quota.remainingValue} {quota.unit}{String(quota.period_end || "").slice(0, 10)}
)}{!(data.quotas || []).length &&
当前组织还没有工作区配额。
}
+
+ } />
+
+
{commercialApprovalSummary.total || 0}全部申请
+
{commercialApprovalSummary.submitted || 0}待审批
+
{commercialApprovalSummary.approved || 0}已通过
+
{commercialApprovalSummary.rejected || 0}已拒绝
+
+
+
+
申请对象数值申请人状态审批备注操作
+ {commercialApprovals.map((request) => {
+ const canDecide = request.status === "submitted" && canReviewCommercialApprovals;
+ const canCancel = request.status === "submitted" && request.requester?.id === currentUserId;
+ return
{commercialApprovalPriorityLabels[request.priority] || request.priority}{request.title}{request.requestTypeLabel || commercialApprovalTypeLabels[request.requestType] || request.requestType} · {request.businessReason}
{request.targetKey}{formatMetricValue(request.currentValue)} → {formatMetricValue(request.requestedValue)} {request.unit}{request.requester?.name || request.requester?.id}{request.projectName || request.workspaceName || "组织级"}
{canDecide && <>>}{canCancel && }{!canDecide && !canCancel && {request.reviewer?.name || "等待处理"}}
;
+ })}
+ {!commercialApprovals.length &&
当前没有商业治理申请。
}
+
+
+
+
+ {planTemplates.map((plan) =>
{plan.name}
{plan.seatLimit} 席 · {plan.storageGb}GB · {plan.monthlyClipQuota} clips/月{plan.description})}
+ {entitlementSummary.blocked || 0}已阻断
{entitlementSummary.warning || 0}预警
{entitlementSummary.overridden || 0}组织覆盖
{entitlements.filter((item) => item.key.startsWith("feature.") && item.enabled).length}已开通功能
+ {entitlementCategories.map(([category, label]) => {label}{entitlements.filter((item) => item.category === category).length} 项
权益已用额度开通执行状态操作
{entitlements.filter((item) => item.category === category).map((entitlement) => { const draft = entitlementDrafts[entitlement.key] || {}; const badgeStatus = entitlement.status === "blocked" ? "failed" : entitlement.status === "warning" ? "needs-evidence" : entitlement.status === "disabled" ? "disabled" : "active"; return
; })}
)}
+
+ 工作区指标已用上限剩余周期操作
{(data.quotas || []).map((quota) =>
{quota.workspaceName}{quota.metric === "clip" ? "生成片段" : quota.metric === "storage" ? "存储" : quota.metric}{formatMetricValue(quota.usedValue)} {quota.unit}{formatMetricValue(quota.remainingValue)} {quota.unit}{String(quota.period_end || "").slice(0, 10)}
)}{!(data.quotas || []).length &&
当前组织还没有工作区配额。
}
{(data.quotaWarnings || []).map((warning) =>
)}{!(data.quotaWarnings || []).length &&
当前没有达到预警阈值的额度。
}
{(data.billingHistory || []).slice(0, 5).map((event) =>
{formatAdminDate(event.created_at)}
{event.event_type}{event.actor_name || "system"}
)}{!(data.billingHistory || []).length &&
还没有套餐变更记录。
}
成本中心本月使用月度预算剩余利用率操作
{(data.costCenters || []).map((center) =>
{center.name}{center.used} {center.currency}{center.remaining} {center.currency}= 100 ? "failed" : center.utilization >= 80 ? "needs-evidence" : "active"} label={`${center.utilization}%`} />
)}{!(data.costCenters || []).length &&
当前组织没有成本中心。
}
{(data.costCenterDetail || []).slice(0, 8).map((item) =>
{item.code}{item.workspaceName}
{item.units}{item.cost} CNY · {item.events} 事件)}
@@ -1709,15 +1904,128 @@ function ModelConnectorFields({ form, setForm, widePrefix = "" }) {
export function AdminModelsPage({ platformContext, contextOverrides, onModelsChanged }) {
const platform = platformContext?.platform || {};
const [models, setModels] = useState(platform.modelRegistry || []);
+ const [catalog, setCatalog] = useState(platform.modelCatalog || []);
+ const [routes, setRoutes] = useState(platform.modelRouting || []);
const [form, setForm] = useState(emptyModelConnectorForm);
const [editingModel, setEditingModel] = useState(null);
const [editForm, setEditForm] = useState(emptyModelConnectorForm);
+ const [catalogForm, setCatalogForm] = useState({ connectorId: "", displayName: "", modelKey: "", family: "", capabilities: "chat", contextWindow: 131072, maxOutputTokens: 8192, locality: "local", estimatedCny: "0.03", status: "active", approvalStatus: "approved", notes: "" });
+ const [editingCatalog, setEditingCatalog] = useState(null);
+ const [catalogEditForm, setCatalogEditForm] = useState({ connectorId: "", displayName: "", modelKey: "", family: "", capabilities: "", contextWindow: 0, maxOutputTokens: 0, locality: "local", estimatedCny: "0", status: "active", approvalStatus: "approved", notes: "" });
+ const [routeForm, setRouteForm] = useState({ name: "", workflowKey: "knowledge-ingest", operationKey: "chunk-and-extract", primaryModelId: "", fallbackModelId: "", policyMode: "prefer-local", approvalMode: "follow-model", budgetLimitCny: "10", status: "active", notes: "" });
+ const [editingRoute, setEditingRoute] = useState(null);
+ const [routeEditForm, setRouteEditForm] = useState({ name: "", workflowKey: "", operationKey: "", primaryModelId: "", fallbackModelId: "", policyMode: "prefer-local", approvalMode: "follow-model", budgetLimitCny: "0", status: "active", notes: "" });
+ const [resolutionForm, setResolutionForm] = useState({ workflowKey: "ai-manhua-drama", operationKey: "image-keyframe", kind: "单画面关键帧", adapter: "owned-model-platform", modelId: "", localOnly: true, estimatedCostCny: "0" });
+ const [resolution, setResolution] = useState(null);
+ const [approvals, setApprovals] = useState([]);
+ const [approvalSummary, setApprovalSummary] = useState({ submitted: 0, approved: 0, rejected: 0, expired: 0 });
+ const [approvalReason, setApprovalReason] = useState("本次生产需要使用受控模型路由,请审批一次执行窗口。");
+ const [approvalBusy, setApprovalBusy] = useState("");
const [notice, setNotice] = useState("");
+ const permissions = new Set(platformContext?.context?.permissions || []);
+ const canApproveRoutes = Boolean(platformContext?.context?.systemAdmin || permissions.has("model:approve") || permissions.has("model:manage"));
+
useEffect(() => setModels(platform.modelRegistry || []), [platform.modelRegistry]);
+ useEffect(() => setCatalog(platform.modelCatalog || []), [platform.modelCatalog]);
+ useEffect(() => setRoutes(platform.modelRouting || []), [platform.modelRouting]);
+ useEffect(() => {
+ if (!catalogForm.connectorId && (platform.modelRegistry || [])[0]?.id) setCatalogForm((current) => ({ ...current, connectorId: (platform.modelRegistry || [])[0].id }));
+ }, [platform.modelRegistry]);
+ useEffect(() => {
+ if (!routeForm.primaryModelId && (platform.modelCatalog || [])[0]?.id) setRouteForm((current) => ({ ...current, primaryModelId: (platform.modelCatalog || [])[0].id }));
+ }, [platform.modelCatalog]);
+ useEffect(() => {
+ loadModelOps();
+ }, [contextOverrides.organizationId, contextOverrides.workspaceId]);
+
function syncModels(nextModels) {
setModels(nextModels);
onModelsChanged?.(nextModels);
}
+
+ function catalogFormFromEntry(entry) {
+ return {
+ connectorId: entry.connectorId || entry.connector_id || "",
+ displayName: entry.display_name || entry.displayName || "",
+ modelKey: entry.model_key || entry.modelKey || "",
+ family: entry.family || "",
+ capabilities: (entry.capabilities || []).join(", "),
+ contextWindow: entry.contextWindow || entry.context_window || 0,
+ maxOutputTokens: entry.maxOutputTokens || entry.max_output_tokens || 0,
+ locality: entry.cost?.locality || "local",
+ estimatedCny: String(entry.cost?.estimatedCny ?? entry.cost?.estimated_cny ?? 0),
+ status: entry.status || "active",
+ approvalStatus: entry.approval_status || entry.approvalStatus || "approved",
+ notes: entry.metadata?.notes || ""
+ };
+ }
+
+ function routeFormFromEntry(route) {
+ return {
+ name: route.name || "",
+ workflowKey: route.workflow_key || route.workflowKey || "",
+ operationKey: route.operation_key || route.operationKey || "",
+ primaryModelId: route.primaryModelId || route.primary_model_id || "",
+ fallbackModelId: route.fallbackModelId || route.fallback_model_id || "",
+ policyMode: route.policy_mode || route.policyMode || "prefer-local",
+ approvalMode: route.approval_mode || route.approvalMode || "follow-model",
+ budgetLimitCny: String(route.budgetLimitCny ?? route.budget_limit_cny ?? 0),
+ status: route.status || "active",
+ notes: route.policy?.notes || ""
+ };
+ }
+
+ function buildCatalogPayload(source) {
+ return {
+ connectorId: source.connectorId,
+ displayName: source.displayName,
+ modelKey: source.modelKey,
+ family: source.family,
+ capabilities: source.capabilities.split(",").map((item) => item.trim()).filter(Boolean),
+ contextWindow: Number(source.contextWindow || 0),
+ maxOutputTokens: Number(source.maxOutputTokens || 0),
+ cost: {
+ mode: "estimated",
+ estimatedCny: Number(source.estimatedCny || 0),
+ locality: source.locality || "local"
+ },
+ status: source.status,
+ approvalStatus: source.approvalStatus,
+ metadata: { notes: source.notes || "" }
+ };
+ }
+
+ function buildRoutePayload(source) {
+ return {
+ name: source.name,
+ workflowKey: source.workflowKey,
+ operationKey: source.operationKey,
+ primaryModelId: source.primaryModelId,
+ fallbackModelId: source.fallbackModelId || "",
+ policyMode: source.policyMode,
+ approvalMode: source.approvalMode,
+ budgetLimitCny: Number(source.budgetLimitCny || 0),
+ status: source.status,
+ policy: { notes: source.notes || "" }
+ };
+ }
+
+ async function loadModelOps() {
+ try {
+ const [catalogResult, routeResult, approvalResult] = await Promise.all([
+ fetchModelCatalog(contextOverrides),
+ fetchModelRoutes(contextOverrides),
+ fetchModelRouteApprovals({}, contextOverrides)
+ ]);
+ setCatalog(catalogResult.catalog || []);
+ setRoutes(routeResult.routes || []);
+ setApprovals(approvalResult.approvals || []);
+ setApprovalSummary(approvalResult.summary || { submitted: 0, approved: 0, rejected: 0, expired: 0 });
+ } catch (error) {
+ setNotice(error.message);
+ }
+ }
+
async function submit(event) {
event.preventDefault();
try {
@@ -1729,15 +2037,18 @@ export function AdminModelsPage({ platformContext, contextOverrides, onModelsCha
syncModels(result.models || [result.model, ...models]);
setForm((current) => ({ ...current, label: "" }));
setNotice("模型连接器已写入当前组织");
+ await loadModelOps();
} catch (error) {
setNotice(error.message);
}
}
+
function openEditor(model) {
setEditingModel(model);
setEditForm(modelConnectorForm(model));
setNotice("");
}
+
async function saveEdit(event) {
event.preventDefault();
if (!editingModel) return;
@@ -1751,10 +2062,12 @@ export function AdminModelsPage({ platformContext, contextOverrides, onModelsCha
syncModels(nextModels);
setEditingModel(null);
setNotice("模型连接器配置已更新,密钥仍只通过环境变量引用");
+ await loadModelOps();
} catch (error) {
setNotice(error.message);
}
}
+
async function probe(model) {
try {
const result = await probeModel(model.id, contextOverrides);
@@ -1764,6 +2077,7 @@ export function AdminModelsPage({ platformContext, contextOverrides, onModelsCha
setNotice(error.message);
}
}
+
async function toggle(model) {
try {
const nextStatus = model.status === "paused" ? "ready" : "paused";
@@ -1771,23 +2085,210 @@ export function AdminModelsPage({ platformContext, contextOverrides, onModelsCha
const nextModels = models.map((item) => item.id === model.id ? result.model : item);
syncModels(nextModels);
setNotice(`${model.label} 已${nextStatus === "ready" ? "启用" : "暂停"}`);
+ await loadModelOps();
} catch (error) {
setNotice(error.message);
}
}
+
+ async function submitCatalog(event) {
+ event.preventDefault();
+ try {
+ const result = await createModelCatalog(buildCatalogPayload(catalogForm), contextOverrides);
+ setCatalog(result.catalog || []);
+ setCatalogForm({ connectorId: models[0]?.id || "", displayName: "", modelKey: "", family: "", capabilities: "chat", contextWindow: 131072, maxOutputTokens: 8192, locality: "local", estimatedCny: "0.03", status: "active", approvalStatus: "approved", notes: "" });
+ setNotice("模型目录条目已创建");
+ } catch (error) {
+ setNotice(error.message);
+ }
+ }
+
+ async function saveCatalogEdit(event) {
+ event.preventDefault();
+ if (!editingCatalog) return;
+ try {
+ const result = await updateModelCatalog(editingCatalog.id, buildCatalogPayload(catalogEditForm), contextOverrides);
+ setCatalog((result.catalog || catalog).map((item) => item.id === editingCatalog.id ? result.entry || item : item));
+ setEditingCatalog(null);
+ setNotice("模型目录条目已更新");
+ await loadModelOps();
+ } catch (error) {
+ setNotice(error.message);
+ }
+ }
+
+ async function submitRoute(event) {
+ event.preventDefault();
+ try {
+ const result = await createModelRoute(buildRoutePayload(routeForm), contextOverrides);
+ setRoutes(result.routes || []);
+ setRouteForm({ name: "", workflowKey: "knowledge-ingest", operationKey: "chunk-and-extract", primaryModelId: catalog[0]?.id || "", fallbackModelId: "", policyMode: "prefer-local", approvalMode: "follow-model", budgetLimitCny: "10", status: "active", notes: "" });
+ setNotice("路由策略已创建");
+ } catch (error) {
+ setNotice(error.message);
+ }
+ }
+
+ async function saveRouteEdit(event) {
+ event.preventDefault();
+ if (!editingRoute) return;
+ try {
+ const result = await updateModelRoute(editingRoute.id, buildRoutePayload(routeEditForm), contextOverrides);
+ setRoutes((result.routes || routes).map((item) => item.id === editingRoute.id ? result.route || item : item));
+ setEditingRoute(null);
+ setNotice("路由策略已更新");
+ await loadModelOps();
+ } catch (error) {
+ setNotice(error.message);
+ }
+ }
+
+ async function runRouteResolution(event) {
+ event?.preventDefault?.();
+ try {
+ const payload = {
+ ...resolutionForm,
+ localOnly: Boolean(resolutionForm.localOnly),
+ estimatedCostCny: Number(resolutionForm.estimatedCostCny || 0),
+ modelId: resolutionForm.modelId || undefined
+ };
+ const result = await resolveModelRoute(payload, contextOverrides);
+ setResolution(result.resolution);
+ setNotice(result.resolution.guard.status === "pass" ? "路由试算通过,可按该策略进入任务队列。" : result.resolution.guard.status === "approval-required" ? "路由试算需要审批,执行前会被审批门拦截。" : "路由试算发现阻断项,请调整连接器、模型或成本策略。");
+ } catch (error) {
+ setResolution(null);
+ setNotice(error.message);
+ }
+ }
+
+ async function submitRouteApproval() {
+ try {
+ setApprovalBusy("submit");
+ const payload = {
+ ...resolutionForm,
+ localOnly: Boolean(resolutionForm.localOnly),
+ estimatedCostCny: Number(resolutionForm.estimatedCostCny || 0),
+ modelId: resolutionForm.modelId || undefined,
+ reason: approvalReason,
+ approvalScope: "single-run"
+ };
+ const result = await requestModelRouteApproval(payload, contextOverrides);
+ setApprovals(result.approvals || []);
+ setApprovalSummary(result.summary || approvalSummary);
+ setNotice(`模型路由审批单已提交:${result.approval?.id || ""}`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setApprovalBusy("");
+ }
+ }
+
+ async function decideRouteApproval(approval, status) {
+ if (!approval?.id) return;
+ try {
+ setApprovalBusy(approval.id);
+ const result = await decideModelRouteApproval(approval.id, {
+ status,
+ expiresHours: status === "approved" ? 24 : undefined,
+ note: status === "approved" ? "批准一次受控模型执行,24 小时内有效。" : "当前连接器、成本或模型证据不满足放行要求。"
+ }, contextOverrides);
+ setApprovals(result.approvals || []);
+ setApprovalSummary(result.summary || approvalSummary);
+ setNotice(status === "approved" ? `已批准审批单:${approval.id}` : `已驳回审批单:${approval.id}`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setApprovalBusy("");
+ }
+ }
+
+ function approvalLabel(status) {
+ return ({ submitted: "待审批", approved: "已批准", rejected: "已驳回", expired: "已过期", cancelled: "已取消" })[status] || status || "未知";
+ }
+
return (
} />
{notice && {notice}
}
- } />
- 名称能力Endpoint成本审批状态操作
{models.map((model) =>
{model.label}{(model.capability || []).join(" · ")}{model.endpoint}{model.last_probe_at ? `最近探活 ${model.latency_ms || "-"}ms` : "尚未探活"}{model.costMode || model.cost_mode}{model.approvalRequired || model.approval_required ? "需要审批" : "无需"}{["ready", "paused"].includes(model.status) &&
}
)}
+
+ 名称能力Endpoint成本审批状态操作
{models.map((model) =>
{model.label}{(model.capability || []).join(" · ")}{model.endpoint}{model.last_probe_at ? `最近探活 ${model.latency_ms || "-"}ms` : "尚未探活"}{model.endpointPolicy?.locality === "public" ? "公网 endpoint" : "本地/内网 endpoint"} · {model.secretPolicy?.required ? `${model.secretPolicy.authEnv || "未配置密钥变量"} ${model.secretPolicy.present ? "已配置" : "缺失"}` : "无需密钥"}{model.costMode || model.cost_mode}{model.approvalRequired || model.approval_required ? "需要审批" : "无需"}{["ready", "paused"].includes(model.status) &&
}
)}
-
+
自有模型平台优先
单画面生成契约
云端节点调用
ComfyUI 桥接
+
+ 试算} />
+
+ {resolution &&
+
+ {resolution.guard.status === "pass" ?
:
}
+
{resolution.guard.status === "pass" ? "准入通过" : resolution.guard.status === "approval-required" ? "需要审批" : "已阻断"}{resolution.source} · {resolution.request.workflowKey || "未指定工作流"} / {resolution.request.operationKey || "未指定操作"}
+
+
+
+
命中路由{resolution.route?.routeName || "未命中路由"}{resolution.route?.policyMode || "direct"} · {resolution.route?.approvalMode || "follow-connector"}
+
模型{resolution.model?.displayName || "直接连接器"}{resolution.model?.modelKey || resolution.connector?.id || "-"}
+
连接器{resolution.connector?.label || "未解析"}{resolution.connector?.kind || "-"} · {resolution.connector?.costMode || "-"}
+
成本/预算{Number(resolution.guard.estimatedCny || 0).toFixed(2)} / {Number(resolution.guard.budgetLimitCny || 0).toFixed(2)} CNY{resolution.guard.localOnly ? "local-only 预检" : "允许混合策略"}
+
+
+ {(resolution.guard.reasons || []).map((reason) =>
{reason.code}{reason.message}
)}
+ {!resolution.guard.reasons?.length &&
no_blockers没有发现审批或阻断原因。
}
+
+
查看试算 JSON
{JSON.stringify(resolution, null, 2)}
+
}
+
+
+ 刷新审批} />
+
+
待审批{approvalSummary.submitted || 0}需要模型管理员处理
+
已批准{approvalSummary.approved || 0}一次执行窗口
+
已驳回{approvalSummary.rejected || 0}保留审计记录
+
已过期{approvalSummary.expired || 0}需重新申请
+
+
+
审批单申请人路由 / 模型连接器成本状态有效期操作
+ {approvals.map((approval) =>
+
{approval.id}
+
{approval.requesterName}{approval.projectName || approval.projectId || "工作区级"}
+
{approval.routeName || "直接连接器"}{approval.modelDisplayName || approval.modelEntryId || approval.request?.kind || "-"}
+
{approval.connectorLabel || approval.connectorId}{approval.connectorKind || "-"}
+
{Number(approval.estimatedCostCny || 0).toFixed(2)} CNY{approval.localOnly ? "local-only" : "mixed allowed"}
+
+
{approval.expiresAt ? formatAdminDate(approval.expiresAt) : approval.status === "submitted" ? "待审批" : "无"}
+
{approval.status === "submitted" && canApproveRoutes ? <>> : {approval.decision_note || approval.reason || "已归档"}}
+
)}
+ {!approvals.length &&
当前没有模型路由审批单。使用上方试算器命中“需要审批”的路由后可以提交申请。
}
+
+
+
+
+ 名称模型 Key连接器能力成本审批状态操作
{catalog.map((entry) =>
{entry.display_name || entry.displayName}{entry.model_key || entry.modelKey}{entry.family || "generic"}{entry.connectorLabel || entry.connector_id || entry.connectorId}{(entry.capabilities || []).join(" · ")}{Number(entry.cost?.estimatedCny || 0).toFixed(2)} CNY{entry.cost?.locality || "local"}{entry.approval_status || entry.approvalStatus} )}
+
+
+
+
+
+
+
+ 名称工作流操作主模型回退模型预算模式状态操作
{routes.map((route) =>
{route.name}{route.workflow_key || route.workflowKey}{route.operation_key || route.operationKey}{route.primaryModelLabel || route.primary_model_id || route.primaryModelId}{route.fallbackModelLabel || route.fallback_model_id || route.fallbackModelId || "无"}{Number(route.budgetLimitCny || route.budget_limit_cny || 0).toFixed(2)} CNY{route.policy_mode || route.policyMode}{route.approval_mode || route.approvalMode} )}
+
+
{editingModel && event.target === event.currentTarget && setEditingModel(null)}>
MODEL CONNECTOR
编辑连接器 · {editingModel.label}
修改会写入当前组织的连接器注册表。平台只保存环境变量名称,不会把 API Key 写入数据库。
}
+ {editingCatalog && event.target === event.currentTarget && setEditingCatalog(null)}>
MODEL CATALOG
编辑模型条目 · {editingCatalog.display_name || editingCatalog.displayName}
}
+ {editingRoute && event.target === event.currentTarget && setEditingRoute(null)}>
ROUTING POLICY
编辑路由策略 · {editingRoute.name}
}
);
}
@@ -2458,8 +2959,76 @@ function readinessBadge(check) {
return map[check.status] || { status: "neutral", label: check.status || "未知" };
}
+function emptyPlanTemplateForm() {
+ return {
+ id: "",
+ tierKey: "",
+ name: "",
+ description: "",
+ billingCycle: "monthly",
+ currency: "CNY",
+ baseFee: 0,
+ seatLimit: 5,
+ storageGb: 256,
+ monthlyClipQuota: 500,
+ supportSla: "工作日响应",
+ status: "active",
+ limitsJson: JSON.stringify({ workspaces: 2, projects: 8, modelConnectors: 6, apiClients: 3, knowledgeDocuments: 80, knowledgeContextPacks: 40, deliveryChannels: 3 }, null, 2),
+ featuresJson: JSON.stringify({ batchGeneration: false, privateDeliveryPortal: true, comfyuiAdapter: false, externalCloudConnectors: false }, null, 2),
+ connectorPolicyJson: JSON.stringify({ allowedCostModes: ["local"], externalApprovalRequired: true }, null, 2)
+ };
+}
+
+function planTemplateFormFromPlan(plan) {
+ return {
+ id: plan.id || "",
+ tierKey: plan.tierKey || "",
+ name: plan.name || "",
+ description: plan.description || "",
+ billingCycle: plan.billingCycle || "monthly",
+ currency: plan.currency || "CNY",
+ baseFee: Number(plan.baseFee || 0),
+ seatLimit: Number(plan.seatLimit || 1),
+ storageGb: Number(plan.storageGb || 1),
+ monthlyClipQuota: Number(plan.monthlyClipQuota || 1),
+ supportSla: plan.supportSla || "",
+ status: plan.status || "active",
+ limitsJson: JSON.stringify(plan.limits || {}, null, 2),
+ featuresJson: JSON.stringify(plan.features || {}, null, 2),
+ connectorPolicyJson: JSON.stringify(plan.connectorPolicy || {}, null, 2)
+ };
+}
+
+function parsePlanTemplateForm(form) {
+ const parseObject = (value, label) => {
+ try {
+ const parsed = JSON.parse(value || "{}");
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not-object");
+ return parsed;
+ } catch {
+ throw new Error(`${label} 必须是 JSON 对象`);
+ }
+ };
+ return {
+ tierKey: form.tierKey,
+ name: form.name,
+ description: form.description,
+ billingCycle: form.billingCycle,
+ currency: form.currency,
+ baseFee: Number(form.baseFee || 0),
+ seatLimit: Number(form.seatLimit || 0),
+ storageGb: Number(form.storageGb || 0),
+ monthlyClipQuota: Number(form.monthlyClipQuota || 0),
+ supportSla: form.supportSla,
+ status: form.status,
+ limits: parseObject(form.limitsJson, "limits"),
+ features: parseObject(form.featuresJson, "features"),
+ connectorPolicy: parseObject(form.connectorPolicyJson, "connectorPolicy")
+ };
+}
+
export function SystemSettingsPage({ section = "overview", contextOverrides }) {
- const [config, setConfig] = useState({ settings: [], featureFlags: [], notifications: [], apiClients: [], apiClientScopes: fallbackApiClientScopes });
+ const [config, setConfig] = useState({ settings: [], featureFlags: [], planTemplates: [], notifications: [], apiClients: [], apiClientScopes: fallbackApiClientScopes });
const [health, setHealth] = useState({ services: [], summary: {} });
const [readiness, setReadiness] = useState({ checks: [], summary: {}, backups: { backups: [], count: 0 } });
const [storage, setStorage] = useState(null);
@@ -2473,6 +3042,8 @@ export function SystemSettingsPage({ section = "overview", contextOverrides }) {
const [apiKeyNotice, setApiKeyNotice] = useState("");
const [backupBusy, setBackupBusy] = useState(false);
const [cleanupBusy, setCleanupBusy] = useState(false);
+ const [planTemplateForm, setPlanTemplateForm] = useState(emptyPlanTemplateForm());
+ const [planTemplateBusy, setPlanTemplateBusy] = useState("");
async function load() {
setLoading(true);
@@ -2565,6 +3136,38 @@ export function SystemSettingsPage({ section = "overview", contextOverrides }) {
}
}
+ async function savePlanTemplate(event) {
+ event.preventDefault();
+ setPlanTemplateBusy("save");
+ try {
+ const body = parsePlanTemplateForm(planTemplateForm);
+ const result = planTemplateForm.id
+ ? await updateSystemPlanTemplate(planTemplateForm.id, body, contextOverrides)
+ : await createSystemPlanTemplate(body, contextOverrides);
+ setConfig((current) => ({ ...current, planTemplates: result.planTemplates || current.planTemplates }));
+ setPlanTemplateForm(planTemplateFormFromPlan(result.planTemplate || (result.planTemplates || []).find((plan) => plan.tierKey === body.tierKey) || body));
+ setNotice(`套餐模板“${body.name}”已保存,并写入系统审计日志`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setPlanTemplateBusy("");
+ }
+ }
+
+ async function archivePlanTemplate(plan) {
+ setPlanTemplateBusy(plan.id);
+ try {
+ const result = await updateSystemPlanTemplate(plan.id, { ...plan, status: plan.status === "archived" ? "active" : "archived" }, contextOverrides);
+ setConfig((current) => ({ ...current, planTemplates: result.planTemplates || current.planTemplates }));
+ setPlanTemplateForm(planTemplateFormFromPlan(result.planTemplate || plan));
+ setNotice(`套餐模板“${plan.name}”已${plan.status === "archived" ? "恢复" : "归档"}`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setPlanTemplateBusy("");
+ }
+ }
+
async function createBackup() {
setBackupBusy(true);
try {
@@ -2618,6 +3221,28 @@ export function SystemSettingsPage({ section = "overview", contextOverrides }) {
{(section === "overview" || section === "deployment") && } />{(readiness.checks || []).map((check) => { const badge = readinessBadge(check); return
{check.label}
{check.detail}{check.envName &&
{check.envName}}
; })}
}
{(section !== "notifications" && section !== "overview" && section !== "features") && SQLite / system_settings} />{visibleSettings.map((setting) =>
{settingLabels[setting.category] || setting.category} · {setting.key}{setting.description}
setSetting(setting.key, value)} />)}
}
{(section === "overview" || section === "generation" || section === "deployment" || section === "features") && {config.featureFlags.map((flag) =>
{flag.label}{flag.description}
)}
}
+ {(section === "overview" || section === "features") &&
+ setPlanTemplateForm(emptyPlanTemplateForm())}>新建模板} />
+ {(config.planTemplates || []).map((plan) =>
{plan.name}
{plan.seatLimit} 席 · {plan.storageGb}GB · {plan.monthlyClipQuota} clips/月{plan.description})}
+
+ }
{(section === "storage" || section === "overview") && {storage ? `${storage.usedGb} GB` : "—"}已使用 / {storage?.limitGb || 0} GB
文件系统测量{storage ? `${storage.usedBytes.toLocaleString()} / ${storage.limitBytes.toLocaleString()} bytes` : "—"}
资产、任务结果和本地合成输出均计入存储配额。最大文件{storage?.largestFiles?.slice(0, 4).map((file) =>
{file.relativePath}{(file.bytes / 1024 ** 2).toFixed(2)} MB
) ||
暂无文件}
}
{(section === "storage" || section === "overview") && {cleanup?.candidates?.length > 0 && }} />{cleanup?.candidates?.length || 0}无引用临时文件
{((cleanup?.totalBytes || 0) / 1024 ** 2).toFixed(2)} MB预计可释放
{cleanup?.policy?.provider || "local-filesystem"}存储提供方
{cleanup?.cutoff ? cleanup.cutoff.slice(0, 10) : "—"}本次截止日期
{cleanup?.candidates?.length > 0 ? {cleanup.candidates.slice(0, 8).map((item) =>
{item.relativePath}{(item.bytes / 1024 ** 2).toFixed(2)} MB{item.reason} · {item.updatedAt?.replace("T", " ").slice(0, 19)}
)}{cleanup.candidates.length > 8 &&
还有 {cleanup.candidates.length - 8} 个候选文件,执行时会按服务端预览结果处理。}
: 没有满足保留周期且未被生产证据引用的临时文件。先调整“存储 · storage.retention_days”后重新生成预览。
}}
{(section === "overview" || section === "deployment" || section === "storage") && {backupBusy ? "创建中…" : "立即创建快照"}} />当前业务真源:SQLite快照写入 `data/backups/`,创建动作进入审计日志。
{(readiness.backups?.backups || []).slice(0, 6).map((backup) =>
{backup.relativePath}{formatAdminDate(backup.modifiedAt)} · {(Number(backup.bytes || 0) / 1024 ** 2).toFixed(2)} MB
)}{!(readiness.backups?.backups || []).length &&
还没有数据库快照。
}
}
diff --git a/src/components/KnowledgeLibraryPage.jsx b/src/components/KnowledgeLibraryPage.jsx
new file mode 100644
index 0000000..ef013b0
--- /dev/null
+++ b/src/components/KnowledgeLibraryPage.jsx
@@ -0,0 +1,780 @@
+import React, { useEffect, useMemo, useState } from "react";
+import {
+ AlertTriangle,
+ Archive,
+ BookOpenText,
+ CheckCircle2,
+ Database,
+ Download,
+ FileCheck2,
+ FileSearch,
+ History,
+ Layers3,
+ PackageCheck,
+ RefreshCw,
+ RotateCcw,
+ Save,
+ Search,
+ ShieldAlert,
+ ShieldCheck,
+ Sparkles,
+ Tags,
+ WandSparkles
+} from "lucide-react";
+import {
+ archiveKnowledgeLibraryDocument,
+ createJob,
+ createKnowledgeContextPack,
+ fetchKnowledgeContextPacks,
+ fetchKnowledgeDocument,
+ fetchKnowledgeLibrary,
+ fetchKnowledgeLibraryDocumentVersions,
+ importKnowledgeLibraryDocument,
+ materializeKnowledgeContextPack,
+ materializeKnowledgeLibraryDocument,
+ previewJob,
+ restoreKnowledgeLibraryDocument,
+ restoreKnowledgeLibraryDocumentVersion,
+ reviewKnowledgeLibraryDocument,
+ searchKnowledgeLibrary,
+ updateKnowledgeLibraryDocument
+} from "../lib/api";
+
+function SuiteHeader({ kicker, title, description, actions }) {
+ return (
+
+
+
{kicker}
+
{title}
+
{description}
+
+ {actions &&
{actions}
}
+
+ );
+}
+
+function SuiteMetric({ icon: Icon, label, value, detail, tone = "neutral" }) {
+ return (
+
+
+
{label}{value}{detail}
+
+ );
+}
+
+function SuiteStatus({ status }) {
+ const config = {
+ indexed: ["已建索引", "ok"],
+ ingested: ["已导入", "neutral"],
+ draft: ["草稿", "neutral"],
+ active: ["可复用", "ok"],
+ archived: ["已归档", "neutral"],
+ workspace: ["工作区级", "ok"],
+ project: ["项目级", "warn"],
+ approved: ["商用已批", "ok"],
+ submitted: ["证据待审", "warn"],
+ "needs-evidence": ["缺授权证据", "warn"],
+ rejected: ["已驳回", "danger"],
+ expired: ["授权过期", "danger"],
+ pass: ["风险通过", "ok"],
+ review: ["需复核", "warn"],
+ warn: ["提示风险", "warn"],
+ blocked: ["阻断", "danger"],
+ unscanned: ["未扫描", "neutral"]
+ }[status] || [status || "未设置", "neutral"];
+ return {config[0]};
+}
+
+const rightsOptions = [
+ ["needs-evidence", "缺授权证据"],
+ ["submitted", "证据待审核"],
+ ["approved", "商用已批准"],
+ ["rejected", "已驳回"],
+ ["expired", "授权过期"]
+];
+
+function provenanceFromDraft(draft) {
+ return {
+ sourceLabel: draft.provenance?.sourceLabel || "",
+ author: draft.provenance?.author || "",
+ rightsOwner: draft.provenance?.rightsOwner || "",
+ evidenceRef: draft.provenance?.evidenceRef || "",
+ licenseNote: draft.provenance?.licenseNote || "",
+ sourceUrl: draft.provenance?.sourceUrl || "",
+ importedFrom: draft.provenance?.importedFrom || "manual"
+ };
+}
+
+function provenanceFromDocument(document) {
+ return {
+ sourceLabel: document?.provenance?.sourceLabel || "",
+ author: document?.provenance?.author || "",
+ rightsOwner: document?.provenance?.rightsOwner || "",
+ evidenceRef: document?.provenance?.evidenceRef || "",
+ licenseNote: document?.provenance?.licenseNote || "",
+ sourceUrl: document?.provenance?.sourceUrl || "",
+ importedFrom: document?.provenance?.importedFrom || "manual"
+ };
+}
+
+function dedupe(values) {
+ return [...new Set((values || []).map((item) => String(item || "").trim()).filter(Boolean))];
+}
+
+function entityList(document, key) {
+ const fromAnalysis = document?.analysis?.entities?.[key] || [];
+ const fromChunks = (document?.chunks || []).flatMap((chunk) => chunk.entities?.[key] || []);
+ return dedupe([...fromAnalysis, ...fromChunks]);
+}
+
+function downloadJson(filename, payload) {
+ const blob = new Blob([JSON.stringify(payload || {}, null, 2)], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement("a");
+ anchor.href = url;
+ anchor.download = filename;
+ anchor.click();
+ URL.revokeObjectURL(url);
+}
+
+const defaultDraft = {
+ title: "《雾港来信》小说原稿",
+ sourceType: "novel",
+ scopeMode: "workspace",
+ rightsStatus: "needs-evidence",
+ provenance: {
+ sourceLabel: "原创小说导入",
+ author: "",
+ rightsOwner: "",
+ evidenceRef: "",
+ licenseNote: "未填写授权证明前,只允许作为待审核素材。",
+ sourceUrl: "",
+ importedFrom: "manual"
+ },
+ tags: "原创,待审核,小说",
+ content: [
+ "第一章 夜航",
+ "海雾刚压到码头上,顾临舟就看见灯塔下的白色信封。信封背面只有一行字:不要让任何人知道你今晚见过林栀。",
+ "",
+ "顾临舟:你不是说已经离开雾港了吗?",
+ "林栀:我回来只做一件事,把当年的录音带找出来。",
+ "",
+ "第二章 空仓库",
+ "他们沿着生锈的货轨走进旧仓库,风把卷帘门吹得作响。顾临舟意识到,仓库里提前有人来过,脚印还没被潮气完全抹掉。"
+ ].join("\n")
+};
+
+const sourceTypes = [
+ ["all", "全部来源"],
+ ["novel", "小说"],
+ ["script", "剧本"],
+ ["worldbook", "世界观"],
+ ["character-bible", "人物设定"],
+ ["reference", "文字参考"]
+];
+
+export function KnowledgeLibraryPage({ platformContext, contextOverrides }) {
+ const permissions = new Set(platformContext?.context?.permissions || []);
+ const canEditKnowledge = permissions.has("script:edit");
+ const canManageCompliance = permissions.has("compliance:manage");
+ const [library, setLibrary] = useState({ documents: [], summary: {} });
+ const [packs, setPacks] = useState({ packs: [], summary: {} });
+ const [selectedId, setSelectedId] = useState("");
+ const [detail, setDetail] = useState(null);
+ const [versions, setVersions] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [detailLoading, setDetailLoading] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [notice, setNotice] = useState("");
+ const [query, setQuery] = useState("");
+ const [draft, setDraft] = useState(defaultDraft);
+ const [editDraft, setEditDraft] = useState(null);
+ const [reviewDraft, setReviewDraft] = useState({
+ decision: "submitted",
+ rightsStatus: "submitted",
+ sourceLabel: "",
+ author: "",
+ rightsOwner: "",
+ evidenceRef: "",
+ licenseNote: "",
+ sourceUrl: "",
+ tags: "",
+ notes: ""
+ });
+ const [selectedChunkIds, setSelectedChunkIds] = useState([]);
+ const [searchForm, setSearchForm] = useState({ query: "蓝伞 地铁口", sourceType: "all", scopeMode: "all", limit: 12 });
+ const [searchResult, setSearchResult] = useState({ results: [], summary: {} });
+ const [selectedSearchChunkIds, setSelectedSearchChunkIds] = useState([]);
+ const [activePack, setActivePack] = useState(null);
+ const [packBudget, setPackBudget] = useState("1200");
+ const [searchLoading, setSearchLoading] = useState(false);
+ const [jobPreview, setJobPreview] = useState(null);
+
+ async function loadLibrary(nextSelectedId = "") {
+ setLoading(true);
+ try {
+ const result = await fetchKnowledgeLibrary(contextOverrides);
+ setLibrary(result);
+ const preferredId = nextSelectedId || selectedId || result.documents?.[0]?.id || "";
+ setSelectedId(preferredId);
+ if (!nextSelectedId && !selectedId && result.documents?.[0]?.id) setNotice("");
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function loadPacks() {
+ try {
+ const result = await fetchKnowledgeContextPacks(contextOverrides);
+ setPacks(result);
+ setActivePack((current) => current || result.packs?.[0] || null);
+ } catch {
+ setPacks({ packs: [], summary: {} });
+ }
+ }
+
+ async function loadDetail(documentId) {
+ if (!documentId) {
+ setDetail(null);
+ setVersions([]);
+ return;
+ }
+ setDetailLoading(true);
+ try {
+ const result = await fetchKnowledgeDocument(documentId, contextOverrides);
+ setDetail(result.document);
+ setSelectedChunkIds((result.document.chunks || []).map((chunk) => chunk.id));
+ setEditDraft({
+ title: result.document.title || "",
+ sourceType: result.document.sourceType || result.document.source_type || "novel",
+ content: result.document.content || ""
+ });
+ const provenance = provenanceFromDocument(result.document);
+ setReviewDraft({
+ decision: "submitted",
+ rightsStatus: result.document.rightsStatus || "needs-evidence",
+ sourceLabel: provenance.sourceLabel,
+ author: provenance.author,
+ rightsOwner: provenance.rightsOwner,
+ evidenceRef: provenance.evidenceRef,
+ licenseNote: provenance.licenseNote,
+ sourceUrl: provenance.sourceUrl,
+ tags: (result.document.tags || []).join(","),
+ notes: result.document.latestReview?.notes || ""
+ });
+ const versionResult = await fetchKnowledgeLibraryDocumentVersions(documentId, contextOverrides);
+ setVersions(versionResult.versions || []);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setDetailLoading(false);
+ }
+ }
+
+ async function runKnowledgeSearch(nextForm = searchForm, { preselect = true } = {}) {
+ setSearchLoading(true);
+ try {
+ const result = await searchKnowledgeLibrary(nextForm, contextOverrides);
+ setSearchResult(result);
+ if (preselect) setSelectedSearchChunkIds((result.results || []).slice(0, 3).map((item) => item.id));
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setSearchLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ loadLibrary();
+ loadPacks();
+ runKnowledgeSearch(searchForm);
+ }, [contextOverrides.organizationId, contextOverrides.workspaceId, contextOverrides.projectId]);
+
+ useEffect(() => {
+ if (selectedId) loadDetail(selectedId);
+ else setDetail(null);
+ }, [selectedId]);
+
+ const filteredDocuments = useMemo(() => (library.documents || []).filter((item) => `${item.title} ${item.source_type} ${item.sourceType} ${item.summary} ${item.rightsStatus} ${item.riskStatus} ${(item.tags || []).join(" ")} ${item.provenance?.sourceLabel || ""}`.toLowerCase().includes(query.toLowerCase())), [library.documents, query]);
+ const characters = entityList(detail, "characters");
+ const locations = entityList(detail, "locations");
+ const props = entityList(detail, "props");
+ const selectedPackChunkIds = selectedSearchChunkIds.length ? selectedSearchChunkIds : selectedChunkIds;
+
+ async function submitImport() {
+ setBusy(true);
+ try {
+ const result = await importKnowledgeLibraryDocument({
+ ...draft,
+ provenance: provenanceFromDraft(draft),
+ tags: draft.tags
+ }, contextOverrides);
+ setNotice(`已导入《${result.document.title}》,生成 ${result.document.chunkCount} 个知识片段。`);
+ await loadLibrary(result.document.id);
+ await loadDetail(result.document.id);
+ await runKnowledgeSearch({ ...searchForm, query: result.document.title }, { preselect: true });
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ function updateDraftProvenance(patch) {
+ setDraft((current) => ({ ...current, provenance: { ...(current.provenance || {}), ...patch } }));
+ }
+
+ function reviewPayload(decision = reviewDraft.decision) {
+ const rightsStatus = decision === "approved" ? "approved" : decision === "rejected" ? "rejected" : reviewDraft.rightsStatus;
+ return {
+ decision,
+ rightsStatus,
+ tags: reviewDraft.tags,
+ notes: reviewDraft.notes,
+ provenance: {
+ sourceLabel: reviewDraft.sourceLabel,
+ author: reviewDraft.author,
+ rightsOwner: reviewDraft.rightsOwner,
+ evidenceRef: reviewDraft.evidenceRef,
+ licenseNote: reviewDraft.licenseNote,
+ sourceUrl: reviewDraft.sourceUrl,
+ importedFrom: "manual"
+ }
+ };
+ }
+
+ async function saveDocumentUpdate() {
+ if (!detail?.id || !editDraft) return;
+ setBusy(true);
+ try {
+ const result = await updateKnowledgeLibraryDocument(detail.id, {
+ ...editDraft,
+ rightsStatus: reviewDraft.rightsStatus,
+ tags: reviewDraft.tags,
+ provenance: reviewPayload().provenance
+ }, contextOverrides);
+ setNotice(`已保存《${result.document.title}》为 v${result.document.currentVersionNumber},并重新扫描治理风险。`);
+ await loadLibrary(result.document.id);
+ await loadDetail(result.document.id);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function submitGovernance(decision) {
+ if (!detail?.id) return;
+ setBusy(true);
+ try {
+ const result = await reviewKnowledgeLibraryDocument(detail.id, reviewPayload(decision), contextOverrides);
+ setNotice(decision === "approved" ? "素材已批准商用,审批记录已写入审计链。" : decision === "rejected" ? "素材已驳回,生产入口会阻断该素材。" : "素材证据已提交审核。");
+ await loadLibrary(result.document.id);
+ await loadDetail(result.document.id);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function toggleArchiveDocument() {
+ if (!detail?.id) return;
+ setBusy(true);
+ try {
+ const result = detail.status === "archived"
+ ? await restoreKnowledgeLibraryDocument(detail.id, contextOverrides)
+ : await archiveKnowledgeLibraryDocument(detail.id, contextOverrides);
+ setNotice(result.document.status === "archived" ? "素材已归档,检索与引用包创建会隐藏它。" : "素材已恢复索引,可重新检索和引用。");
+ await loadLibrary(result.document.id);
+ await loadDetail(result.document.id);
+ await runKnowledgeSearch(searchForm, { preselect: false });
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function restoreVersion(versionKey) {
+ if (!detail?.id || !versionKey) return;
+ setBusy(true);
+ try {
+ const result = await restoreKnowledgeLibraryDocumentVersion(detail.id, versionKey, contextOverrides);
+ setNotice(`已从历史版本恢复,并生成新的当前版本 v${result.document.currentVersionNumber}。`);
+ await loadLibrary(result.document.id);
+ await loadDetail(result.document.id);
+ await runKnowledgeSearch({ ...searchForm, query: result.document.title }, { preselect: false });
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function materializeSelected(pack = activePack) {
+ const chunkIds = pack?.chunkIds?.length ? pack.chunkIds : selectedChunkIds;
+ if (!detail?.id && !chunkIds.length) {
+ setNotice("请先选择知识片段或上下文包。");
+ return;
+ }
+ setBusy(true);
+ try {
+ const result = pack?.id
+ ? await materializeKnowledgeContextPack(pack.id, { title: `${pack.name || "知识库上下文包"} · 剧本草稿` }, contextOverrides)
+ : await materializeKnowledgeLibraryDocument(detail?.id || "", {
+ title: `${detail?.title || "知识库素材"} · 剧本草稿`,
+ chunkIds
+ }, contextOverrides);
+ setNotice(`已送入剧本工厂,生成剧本版本 ${result.importedScript?.version_number || ""}。`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function buildContextPack() {
+ setBusy(true);
+ try {
+ const result = await createKnowledgeContextPack({
+ name: searchForm.query ? `检索包:${searchForm.query}` : `${detail?.title || "选中素材"} · 上下文包`,
+ query: searchForm.query,
+ sourceType: searchForm.sourceType === "all" ? "mixed" : searchForm.sourceType,
+ scopeMode: searchForm.scopeMode === "project" ? "project" : "workspace",
+ maxTokens: Number(packBudget || 1200),
+ chunkIds: selectedPackChunkIds
+ }, contextOverrides);
+ setActivePack(result.pack);
+ setPacks({ packs: result.packs || [result.pack, ...(packs.packs || [])], summary: result.summary || packs.summary });
+ setNotice(`已创建上下文包“${result.pack.name}”,包含 ${result.pack.selectedCount} 个片段、约 ${result.pack.tokenEstimate} tokens。`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function previewScriptJob() {
+ if (!activePack?.id) {
+ setNotice("请先创建或选择一个上下文包。");
+ return;
+ }
+ setBusy(true);
+ try {
+ const result = await previewJob({
+ kind: "场景草稿",
+ workflowKey: "script-pipeline",
+ operationKey: "scene-draft",
+ adapter: "owned-model-platform",
+ knowledgePackId: activePack.id,
+ inputs: { prompt: "基于知识库上下文包生成 5-10 秒连续短剧镜头草稿,保留引用证据。" }
+ }, contextOverrides);
+ setJobPreview(result.preview);
+ setNotice("已生成剧本拆解任务预览,引用包已写入请求合同。");
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function createScriptJob() {
+ if (!activePack?.id) {
+ setNotice("请先创建或选择一个上下文包。");
+ return;
+ }
+ setBusy(true);
+ try {
+ const result = await createJob({
+ kind: "场景草稿",
+ workflowKey: "script-pipeline",
+ operationKey: "scene-draft",
+ adapter: "owned-model-platform",
+ knowledgePackId: activePack.id,
+ inputs: { prompt: "基于知识库上下文包生成 5-10 秒连续短剧镜头草稿,保留引用证据。" }
+ }, contextOverrides);
+ setJobPreview(result.job?.request || null);
+ setNotice(`已创建剧本拆解任务:${result.job?.id || ""}`);
+ } catch (error) {
+ setNotice(error.message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ function toggleChunk(chunkId) {
+ setSelectedChunkIds((current) => current.includes(chunkId) ? current.filter((item) => item !== chunkId) : [...current, chunkId]);
+ }
+
+ function toggleSearchChunk(chunkId) {
+ setSelectedSearchChunkIds((current) => current.includes(chunkId) ? current.filter((item) => item !== chunkId) : [...current, chunkId]);
+ }
+
+ function updateSearchForm(patch) {
+ setSearchForm((current) => ({ ...current, ...patch }));
+ }
+
+ return (
+
+
+
+
+ >}
+ />
+ {notice && {notice}
}
+
+
+
+
+
+
+
+
+
+
文档目录
{loading ? "读取中…" : `${filteredDocuments.length} / ${library.documents?.length || 0} 份素材`}
+
+
+
+ setQuery(event.target.value)} placeholder="搜索标题、来源或摘要" />
+
+
+ {filteredDocuments.map((document) => (
+
+ ))}
+ {!filteredDocuments.length &&
没有匹配的知识文档。
}
+
+
+
+
+
导入新素材
本地规则分块,可后续切换自有模型
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
检索与引用包
{searchLoading ? "检索中…" : `${searchResult.total || 0} 个匹配片段`}
+
+
+
+
+
+
+
+
+
+ {(searchResult.results || []).map((result) => (
+
+ ))}
+ {!searchResult.results?.length &&
还没有检索结果。输入关键词后点击检索。
}
+
+
+
+
+
上下文包
{activePack ? activePack.name : "未选择"}
+
+
+
+
+
+
+
+
+ {(packs.packs || []).map((pack) => (
+
+ ))}
+ {!packs.packs?.length &&
还没有保存的上下文包。
}
+
+ {activePack &&
+
+ 引用预览
+
+
+
{activePack.promptContext}
+
}
+
+
+ {jobPreview &&
+
+
剧本拆解任务合同预览
+
+
+ {JSON.stringify(jobPreview, null, 2)}
+ }
+
+
+
+
文档详情
{detailLoading ? "读取中…" : (detail?.title || "未选择文档")}
+
+
+
+
+
+
+
+ {detail ? (
+ <>
+
+
摘要{detail.summary || "暂无摘要"}
+
解析器{detail.analysis?.parser || "local-rule-v2"}
+
章节{detail.analysis?.chapterCount || 0}
+
片段{detail.chunkCount || 0}
+
版权状态
+
风险状态
+
当前版本v{detail.currentVersionNumber || 1} / {detail.versionCount || versions.length || 1}
+
最新审批{detail.latestReview ? `${detail.latestReview.decision} · ${detail.latestReview.createdAt.slice(0, 10)}` : "暂无审批记录"}
+
+
+
+
+
来源与授权
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {canManageCompliance &&
}
+ {canManageCompliance &&
}
+
+
+
+
+
本地风险扫描
+
+
+
+
+ {detail.riskScore || 0}
+ {detail.risk?.scanner || "local-governance-v1"}
+
+
+ {(detail.risk?.issues || []).map((issue) => (
+
+
+
{issue.code}
+
{issue.message}
+ {issue.matches?.length ?
{issue.matches.join(" / ")} : null}
+
+ ))}
+ {!detail.risk?.issues?.length &&
本地扫描未发现阻断项。
}
+
+
+
+
+
版本历史
+
+
+
+ {versions.map((version) => (
+
+ ))}
+ {!versions.length &&
暂无版本快照。
}
+
+
+
+
+
+
素材正文编辑
+
+
+
+
+
+
+
+
+
+
角色{characters.map((item) => )}{!characters.length && 未识别}
+
场景{locations.map((item) => )}{!locations.length && 未识别}
+
道具{props.map((item) => )}{!props.length && 未识别}
+
+
+ {detail.chunks?.map((chunk) => (
+
+
+
{chunk.content}
+
+
+
+
+
+ {(chunk.keywords || []).map((keyword) => {keyword})}
+
+
+ ))}
+
+ >
+ ) : (
+ 选择一份素材后,可查看分块、实体和入剧本动作。
+ )}
+
+
+
+ );
+}
diff --git a/src/components/ProductionPages.jsx b/src/components/ProductionPages.jsx
index e64004b..d18735c 100644
--- a/src/components/ProductionPages.jsx
+++ b/src/components/ProductionPages.jsx
@@ -54,12 +54,14 @@ import {
fetchCompositions,
fetchDeliveryBatches,
fetchDeliveryChannels,
+ fetchDeliveryClearance,
fetchDeliveries,
fetchDeliveryReleases,
fetchDeliveryAccessFeedback,
fetchDeliveryAccessLinks,
fetchJob,
fetchJobs,
+ previewJob,
fetchMediaArtifacts,
fetchMediaArtifactContent,
fetchWorkflowTemplates,
@@ -78,6 +80,7 @@ import {
fetchProductionShotVersions,
restoreProductionShotVersion,
rollbackDeliveryBatch,
+ runDeliveryClearance,
publishDeliveryRelease,
revokeDeliveryAccessLink,
updateAssetLock,
@@ -657,6 +660,8 @@ export function GenerationQueuePage({ project, contextOverrides, canCreateJob, c
const [selectedWorkflowId, setSelectedWorkflowId] = useState("");
const [workflowMode, setWorkflowMode] = useState("plan");
const [workflowBusy, setWorkflowBusy] = useState(false);
+ const [resolvedPreview, setResolvedPreview] = useState(null);
+ const [previewLoading, setPreviewLoading] = useState(false);
async function load() {
setLoading(true);
@@ -688,6 +693,26 @@ export function GenerationQueuePage({ project, contextOverrides, canCreateJob, c
useEffect(() => { loadWorkflows(); }, [contextOverrides.organizationId, contextOverrides.workspaceId, contextOverrides.projectId]);
+ useEffect(() => {
+ if (!canCreateJob || !activeShot?.id || !adapterId) {
+ setResolvedPreview(null);
+ return undefined;
+ }
+ let cancelled = false;
+ setPreviewLoading(true);
+ previewJob({ kind: "关键帧", shotId: activeShot.id, adapter: adapterId }, contextOverrides)
+ .then((result) => {
+ if (!cancelled) setResolvedPreview(result);
+ })
+ .catch(() => {
+ if (!cancelled) setResolvedPreview(null);
+ })
+ .finally(() => {
+ if (!cancelled) setPreviewLoading(false);
+ });
+ return () => { cancelled = true; };
+ }, [canCreateJob, activeShot?.id, adapterId, contextOverrides.organizationId, contextOverrides.workspaceId, contextOverrides.projectId]);
+
const selected = jobs.find((job) => job.id === selectedId) || jobs[0];
useEffect(() => {
if (!selected) {
@@ -707,7 +732,11 @@ export function GenerationQueuePage({ project, contextOverrides, canCreateJob, c
const countBy = (status) => jobs.filter((job) => job.status === status).length;
const selectedAdapterId = (selectedDetail || selected)?.adapter_id || (selectedDetail || selected)?.adapter || adapterId;
const selectedAdapter = adapters.find((item) => item.id === selectedAdapterId) || adapters.find((item) => item.id === adapterId);
- const approvalRequired = Boolean(selectedAdapter?.approvalRequired || selectedAdapter?.approval_required) && selectedAdapter?.costMode !== "local" && selectedAdapter?.cost_mode !== "local";
+ const selectedRouting = (selectedDetail || selected)?.routing;
+ const approvalRequired = Boolean(selectedRouting?.requiresApproval || ((selectedAdapter?.approvalRequired || selectedAdapter?.approval_required) && selectedAdapter?.costMode !== "local" && selectedAdapter?.cost_mode !== "local"));
+ const effectiveRequestPreview = resolvedPreview?.preview || requestPreview;
+ const previewResolution = resolvedPreview?.resolution;
+ const voicePreflight = effectiveRequestPreview?.preflight?.voiceReference;
function selectJob(job) {
setSelectedId(job.id);
@@ -790,10 +819,10 @@ export function GenerationQueuePage({ project, contextOverrides, canCreateJob, c
{jobs.length}项目任务
{countBy("queued") + countBy("blocked")}等待执行
{countBy("running")}执行中
{countBy("completed")}已完成
} />{selectedWorkflow && {selectedWorkflow.steps?.map((step, index) =>
{String(index + 1).padStart(2, "0")}{step.label}{step.jobKind && {step.jobKind}}
)}
}{workflowRuns.length > 0 && {workflowRuns.slice(0, 4).map((run) =>
{run.id}{run.shotId || "项目级"} · {run.created_at?.replace("T", " ").slice(0, 19) || "刚刚"}
{run.jobIds?.length || 0} 个任务 · {run.steps?.length || 0} 个步骤)}
}
-
setFilter(event.target.value)}>} />{filtered.map((job) =>
)}{!filtered.length &&
{loading ? "正在读取实时任务…" : "当前筛选没有任务。"}
}
-
{(selectedDetail || selected) ? <>} />任务类型{jobText(selectedDetail || selected)}
镜头{(selectedDetail || selected).shot_id || (selectedDetail || selected).shotId || "全局"}
适配器{(selectedDetail || selected).adapter_id || (selectedDetail || selected).adapter}
尝试次数{(selectedDetail || selected).attempts || 0}
{(selectedDetail || selected).errorMessage && {(selectedDetail || selected).errorMessage}
}{approvalRequired && 混合/外部连接器需要本次批准{canApproveExternal ? "批准只对当前任务生效,并会进入审计日志。" : "当前角色没有模型管理权限,任务只能保持阻塞。"}
{canApproveExternal &&
}
}{["queued", "blocked", "failed", "cancelled"].includes((selectedDetail || selected).status) && canCreateJob &&
}{["failed", "cancelled"].includes((selectedDetail || selected).status) && canManageJobs &&
}{["queued", "running"].includes((selectedDetail || selected).status) && canManageJobs &&
}
请求合同
{JSON.stringify((selectedDetail || selected).request || {}, null, 2)}结果证据
{JSON.stringify((selectedDetail || selected).result || {}, null, 2)} > : 选择一个任务查看请求、状态和执行证据。
}
+
setFilter(event.target.value)}>} />{filtered.map((job) =>
)}{!filtered.length &&
{loading ? "正在读取实时任务…" : "当前筛选没有任务。"}
}
+
{(selectedDetail || selected) ? <>} />任务类型{jobText(selectedDetail || selected)}
镜头{(selectedDetail || selected).shot_id || (selectedDetail || selected).shotId || "全局"}
路由策略{selectedRouting?.routeName || "未命中路由"}
模型{selectedRouting?.modelDisplayName || selectedRouting?.modelKey || "直接连接器"}
连接器{(selectedDetail || selected).adapter_id || (selectedDetail || selected).adapter}
尝试次数{(selectedDetail || selected).attempts || 0}
{(selectedDetail || selected).errorMessage && {(selectedDetail || selected).errorMessage}
}{approvalRequired && {selectedRouting ? "模型路由需要本次审批" : "混合/外部连接器需要本次批准"}{canApproveExternal ? "批准只对当前任务生效,并会进入审计日志。" : "当前角色没有模型管理权限,任务只能保持阻塞。"}
{canApproveExternal &&
}
}{["queued", "blocked", "failed", "cancelled"].includes((selectedDetail || selected).status) && canCreateJob &&
}{["failed", "cancelled"].includes((selectedDetail || selected).status) && canManageJobs &&
}{["queued", "running"].includes((selectedDetail || selected).status) && canManageJobs &&
}
请求合同
{JSON.stringify((selectedDetail || selected).request || {}, null, 2)}结果证据
{JSON.stringify((selectedDetail || selected).result || {}, null, 2)} > : 选择一个任务查看请求、状态和执行证据。
}
- {adapters.map((adapter) =>
)}
前置任务未完成时,服务端会把当前任务保持在 blocked,不允许越过生产顺序。
{activeShot?.id || "当前镜头"} 请求预览
{JSON.stringify(requestPreview || {}, null, 2)}单画面图片输出固定为 1;禁止分屏、拼图、故事板和 contact sheet;连续视频必须登记上一段实际末帧;执行失败保留 request、attempt 和 error。
+ {adapters.map((adapter) =>
)}
{previewResolution && 本次预览路由{previewResolution.route?.routeName || "未命中路由"}
实际模型{previewResolution.route?.modelDisplayName || previewResolution.route?.modelKey || previewResolution.adapter?.label}
连接器{previewResolution.route?.connectorLabel || previewResolution.adapter?.label}
策略状态{voicePreflight?.status === "blocked" ? "声线未授权" : previewResolution.route?.requiresApproval ? "需要审批" : previewLoading ? "解析中" : "可直接执行"}
}{previewLoading && 正在按当前工作流解析模型路由…
}前置任务未完成时,服务端会把当前任务保持在 blocked,不允许越过生产顺序。
{activeShot?.id || "当前镜头"} 请求预览
{JSON.stringify(effectiveRequestPreview || {}, null, 2)}单画面图片输出固定为 1;禁止分屏、拼图、故事板和 contact sheet;连续视频必须登记上一段实际末帧;执行失败保留 request、attempt 和 error。
;
}
@@ -905,6 +934,29 @@ function DeliveryReleasePanel({ deliveries, channels, releasesByDelivery, canApp
return {!availableDeliveries.length && 当前没有已批准的交付版本;发布申请会被后端质量门阻断。
}申请渠道状态审批信息输出操作
{selectedReleases.map((release) =>
{release.delivery_version}{release.id}
{release.channel?.name || release.channel_name}{release.channel?.kind || release.channel_kind}申请:{release.requested_by_name || "草稿"}审批:{release.reviewed_by_name || "待处理"}{release.decision_note && {release.decision_note}}{release.output_path || "尚未发布"}{canApproveDelivery && release.status === "draft" && }{canApproveDelivery && release.status === "submitted" && <>>}{canApproveDelivery && release.status === "rejected" && }{canApproveDelivery && ["approved", "failed"].includes(release.status) && }
)}{!selectedReleases.length &&
当前交付版本还没有发布申请。
}
;
}
+function DeliveryClearancePanel({ deliveries, clearancesByDelivery, canApproveDelivery, onRun }) {
+ const [deliveryId, setDeliveryId] = useState("");
+ const selectedDeliveryId = deliveryId && deliveries.some((delivery) => delivery.id === deliveryId) ? deliveryId : deliveries[0]?.id || "";
+ const selectedDelivery = deliveries.find((delivery) => delivery.id === selectedDeliveryId);
+ const entry = clearancesByDelivery[selectedDeliveryId] || {};
+ const clearance = entry.clearance || entry.latest?.report || null;
+ const latest = entry.latest || null;
+ const counts = clearance?.certificate?.counts || {};
+ const blockers = clearance?.blockers || [];
+ const reviewItems = clearance?.reviewItems || [];
+ const assets = clearance?.sections?.assets || [];
+ const voices = clearance?.sections?.voices || [];
+ const knowledge = clearance?.sections?.knowledge?.materials || [];
+ useEffect(() => {
+ if (!deliveryId && deliveries[0]?.id) setDeliveryId(deliveries[0].id);
+ }, [deliveries]);
+ function downloadCertificate() {
+ if (!clearance) return;
+ downloadJson(`${selectedDelivery?.version || selectedDeliveryId}-clearance-certificate.json`, clearance.certificate ? { ...clearance.certificate, report: clearance } : clearance);
+ }
+ return } />{clearance ? <>清算状态{clearance.status === "pass" ? "可发行" : clearance.status === "blocked" ? "已阻断" : "需复核"}
硬阻断{counts.blockers ?? blockers.length}
复核项{counts.reviewItems ?? reviewItems.length}
资产 / 声线{(counts.assets || assets.length)} / {(counts.voices || voices.length)}
知识素材{counts.knowledgeMaterials ?? knowledge.length}
媒体证据{counts.mediaItems || 0}
{blockers.length > 0 && 硬阻断{blockers.slice(0, 10).map((item, index) => {item.shotId || item.assetId || item.documentId || item.packId || item.voiceId || item.type} · {item.reason || item.status})}
}{reviewItems.length > 0 && 需要复核{reviewItems.slice(0, 8).map((item, index) => {item.assetId || item.documentId || item.packId || item.voiceId || item.type} · {item.reason})}
}资产授权{assets.slice(0, 5).map((asset) => {asset.name}{asset.kind} · {asset.rightsStatus} · {asset.licenseScope || "未写范围"})}{!assets.length && 当前交付批次未绑定资产。}
固定声线{voices.slice(0, 5).map((voice) => {voice.name || voice.voiceId}{voice.rightsStatus} · {voice.lineCount} 句对白)}{!voices.length && 当前镜头没有登记固定声线。}
知识素材{knowledge.slice(0, 5).map((item) => {item.title}{item.type} · {item.rightsStatus || item.riskStatus})}{!knowledge.length && 当前交付未引用知识库素材。}
> : 选择交付版本后可预览当前清算状态;运行清算会落库生成发行证书。
};
+}
+
function portalExpiryValue() {
const date = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
return date.toISOString().slice(0, 16);
@@ -1043,6 +1095,7 @@ export function DeliveryOpsPage({ contextOverrides, canApproveDelivery }) {
const [batches, setBatches] = useState([]);
const [channels, setChannels] = useState([]);
const [releasesByDelivery, setReleasesByDelivery] = useState({});
+ const [clearancesByDelivery, setClearancesByDelivery] = useState({});
const [notice, setNotice] = useState("");
const [blockers, setBlockers] = useState([]);
const [loading, setLoading] = useState(true);
@@ -1055,14 +1108,16 @@ export function DeliveryOpsPage({ contextOverrides, canApproveDelivery }) {
try {
const [result, compositionResult, channelResult] = await Promise.all([fetchDeliveries(contextOverrides), fetchCompositions(contextOverrides), fetchDeliveryChannels(contextOverrides)]);
const nextDeliveries = result.deliveries || [];
- const [batchResults, releaseResults] = await Promise.all([
+ const [batchResults, releaseResults, clearanceResults] = await Promise.all([
Promise.all(nextDeliveries.map((delivery) => fetchDeliveryBatches(delivery.id, contextOverrides))),
- Promise.all(nextDeliveries.map((delivery) => fetchDeliveryReleases(delivery.id, contextOverrides)))
+ Promise.all(nextDeliveries.map((delivery) => fetchDeliveryReleases(delivery.id, contextOverrides))),
+ Promise.all(nextDeliveries.map((delivery) => fetchDeliveryClearance(delivery.id, contextOverrides).catch((error) => ({ error: error.message }))))
]);
setDeliveries(nextDeliveries);
setBatches(batchResults.flatMap((item) => item.batches || []));
setChannels(channelResult.channels || []);
setReleasesByDelivery(Object.fromEntries(nextDeliveries.map((delivery, index) => [delivery.id, releaseResults[index]?.releases || []])));
+ setClearancesByDelivery(Object.fromEntries(nextDeliveries.map((delivery, index) => [delivery.id, clearanceResults[index] || {}])));
setCompositions(compositionResult.compositions || []);
} catch (error) { setNotice(error.message); } finally { setLoading(false); }
}
@@ -1078,6 +1133,7 @@ export function DeliveryOpsPage({ contextOverrides, canApproveDelivery }) {
async function createRelease(deliveryId, body) { try { const result = await createDeliveryRelease(deliveryId, body, contextOverrides); updateReleaseResult(result); setNotice(`发布申请 ${result.release.id} 已提交,等待审批。`); } catch (error) { setNotice(error.message); setBlockers(error.payload?.details?.blockers || []); } }
async function decideRelease(release, status, note) { try { const result = await decideDeliveryRelease(release.id, { status, note }, contextOverrides); updateReleaseResult(result); setNotice(status === "approved" ? "发布申请已批准。" : status === "rejected" ? "发布申请已驳回。" : status === "draft" ? "发布申请已退回草稿。" : "发布申请已重新提交。"); } catch (error) { setNotice(error.message); setBlockers(error.payload?.details?.blockers || []); } }
async function publishRelease(release) { try { const result = await publishDeliveryRelease(release.id, contextOverrides); updateReleaseResult(result); setNotice(`发布完成:${result.release.output_path || "本地发布记录已写入"}`); } catch (error) { setNotice(error.message); setBlockers(error.payload?.details?.blockers || []); } }
+ async function runClearance(deliveryId) { try { const result = await runDeliveryClearance(deliveryId, {}, contextOverrides); setClearancesByDelivery((current) => ({ ...current, [deliveryId]: result })); setBlockers(result.clearance?.blockers || []); setNotice(result.clearance?.status === "blocked" ? "权利清算发现硬阻断,发布已被保护性拦截。" : result.clearance?.status === "pass" ? "权利清算通过,发行证书已写入本地 storage。" : "权利清算完成,存在可发布前复核的提醒项。"); } catch (error) { setNotice(error.message); setBlockers(error.payload?.details?.blockers || []); } }
async function writeProject() { try { const result = await writeExports(contextOverrides); setNotice(`工程目录已写入 ${result.exportRoot},共 ${result.files?.length || 0} 个文件。`); } catch (error) { setNotice(error.message); } }
async function compose(dryRun) {
setComposeBusy(true);
@@ -1088,5 +1144,5 @@ export function DeliveryOpsPage({ contextOverrides, canApproveDelivery }) {
} catch (error) { setNotice(error.message); } finally { setComposeBusy(false); }
}
const approved = deliveries.filter((item) => item.status === "approved").length;
- return >} />{notice && { setNotice(""); setBlockers([]); }}>{notice}}{blockers.length > 0 && 发布 / 审批阻断项{blockers.map((blocker, index) => {blocker.shotId || blocker.type || "检查项"} · {reviewLabels[blocker.lane] || blocker.reason || blocker.status || "未通过"})}}{deliveries.length}交付版本
{approved}已批准
{deliveries.filter((item) => item.status === "draft").length}草稿
{Object.values(releasesByDelivery).flat().filter((release) => release.status === "published").length}已发布
{compositions.length > 0 && {compositions.slice(0, 5).map((composition) =>
{composition.version}{composition.output_path || composition.outputPath}
{composition.tool} · {composition.dryRun ? "dry-run" : "真实执行"})}
}版本渠道Manifest状态审批人操作
{deliveries.map((delivery) =>
{delivery.version}{delivery.channel}{delivery.manifest_path}{delivery.approved_by_name || "待审批"}{delivery.status === "draft" &&
}
)}{!deliveries.length &&
还没有交付版本,先创建一个草稿。
}
双层审批交付版本先审批,发布申请再审批,普通审片角色只能查看。
版本可回溯分镜、Prompt、资产、批次、发布申请和结果全部保留版本。
本地发布默认写入 storage/releases;Webhook 仅允许 localhost 或私网。
;
+ return >} />{notice && { setNotice(""); setBlockers([]); }}>{notice}}{blockers.length > 0 && 发布 / 审批阻断项{blockers.map((blocker, index) => {blocker.shotId || blocker.assetId || blocker.documentId || blocker.voiceId || blocker.type || "检查项"} · {reviewLabels[blocker.lane] || blocker.reason || blocker.status || "未通过"})}}{deliveries.length}交付版本
{approved}已批准
{deliveries.filter((item) => item.status === "draft").length}草稿
{Object.values(releasesByDelivery).flat().filter((release) => release.status === "published").length}已发布
{compositions.length > 0 && {compositions.slice(0, 5).map((composition) =>
{composition.version}{composition.output_path || composition.outputPath}
{composition.tool} · {composition.dryRun ? "dry-run" : "真实执行"})}
}版本渠道Manifest状态审批人操作
{deliveries.map((delivery) =>
{delivery.version}{delivery.channel}{delivery.manifest_path}{delivery.approved_by_name || "待审批"}{delivery.status === "draft" &&
}
)}{!deliveries.length &&
还没有交付版本,先创建一个草稿。
}
权利清算交付审批、发布审批和执行发布都会强制复核资产、声线、知识素材和媒体证据。
版本可回溯分镜、Prompt、资产、批次、发布申请、证书和发布结果全部保留版本。
本地发布默认写入 storage/releases;Webhook 仅允许 localhost 或私网。
;
}
diff --git a/src/lib/api.js b/src/lib/api.js
index 866d5c0..ad3dbbf 100644
--- a/src/lib/api.js
+++ b/src/lib/api.js
@@ -330,6 +330,10 @@ export async function createJob(body, overrides = {}) {
return apiFetch("/api/jobs", { method: "POST", body: JSON.stringify(body) }, overrides);
}
+export async function previewJob(body, overrides = {}) {
+ return apiFetch("/api/jobs/preview", { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
export async function fetchAssets(overrides = {}) {
return apiFetch("/api/assets", {}, overrides);
}
@@ -358,6 +362,14 @@ export async function verifyAsset(assetId, overrides = {}) {
return apiFetch(`/api/assets/${encodeURIComponent(assetId)}/verify`, { method: "POST", body: JSON.stringify({}) }, overrides);
}
+export async function fetchAssetGovernanceReviews(assetId, overrides = {}) {
+ return apiFetch(`/api/assets/${encodeURIComponent(assetId)}/governance`, {}, overrides);
+}
+
+export async function scanAssetGovernance(assetId, body = {}, overrides = {}) {
+ return apiFetch(`/api/assets/${encodeURIComponent(assetId)}/governance/scan`, { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
export async function createAssetVersion(assetId, body, overrides = {}) {
return apiFetch(`/api/assets/${encodeURIComponent(assetId)}/versions`, { method: "POST", body: JSON.stringify(body) }, overrides);
}
@@ -471,6 +483,27 @@ export async function fetchOrganizationCommercial(organizationId, overrides = {}
return apiFetch(`/api/organizations/${encodeURIComponent(organizationId)}/commercial`, {}, overrides);
}
+export async function fetchOrganizationEntitlements(organizationId, overrides = {}) {
+ return apiFetch(`/api/organizations/${encodeURIComponent(organizationId)}/entitlements`, {}, overrides);
+}
+
+export async function updateOrganizationEntitlement(organizationId, entitlementKey, body, overrides = {}) {
+ return apiFetch(`/api/organizations/${encodeURIComponent(organizationId)}/entitlements/${encodeURIComponent(entitlementKey)}`, { method: "PATCH", body: JSON.stringify(body) }, overrides);
+}
+
+export async function fetchCommercialApprovals(organizationId, filters = {}, overrides = {}) {
+ const query = usageQuery(filters);
+ return apiFetch(`/api/organizations/${encodeURIComponent(organizationId)}/commercial-approvals${query ? `?${query}` : ""}`, {}, overrides);
+}
+
+export async function createCommercialApproval(organizationId, body, overrides = {}) {
+ return apiFetch(`/api/organizations/${encodeURIComponent(organizationId)}/commercial-approvals`, { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function decideCommercialApproval(organizationId, approvalId, body, overrides = {}) {
+ return apiFetch(`/api/organizations/${encodeURIComponent(organizationId)}/commercial-approvals/${encodeURIComponent(approvalId)}/decision`, { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
function usageQuery(filters = {}) {
const params = new URLSearchParams();
for (const key of ["query", "workspaceId", "projectId", "userId", "kind", "unitName", "costCenter", "status", "from", "to", "page", "pageSize", "format"]) {
@@ -573,6 +606,18 @@ export async function updateFeatureFlag(key, enabled, overrides = {}) {
return apiFetch("/api/system/feature-flags", { method: "POST", body: JSON.stringify({ key, enabled }) }, overrides);
}
+export async function fetchSystemPlanTemplates(overrides = {}) {
+ return apiFetch("/api/system/plan-templates", {}, overrides);
+}
+
+export async function createSystemPlanTemplate(body, overrides = {}) {
+ return apiFetch("/api/system/plan-templates", { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function updateSystemPlanTemplate(planId, body, overrides = {}) {
+ return apiFetch(`/api/system/plan-templates/${encodeURIComponent(planId)}`, { method: "PATCH", body: JSON.stringify(body) }, overrides);
+}
+
export async function updateNotificationChannel(channel, overrides = {}) {
return apiFetch("/api/system/notifications", { method: "POST", body: JSON.stringify(channel) }, overrides);
}
@@ -686,6 +731,71 @@ export async function materializeProductionScript(body, overrides = {}) {
return apiFetch("/api/production/script/materialize", { method: "POST", body: JSON.stringify(body) }, overrides);
}
+export async function fetchKnowledgeLibrary(overrides = {}) {
+ return apiFetch("/api/knowledge/library", {}, overrides);
+}
+
+export async function fetchKnowledgeDocument(documentId, overrides = {}) {
+ return apiFetch(`/api/knowledge/library/${encodeURIComponent(documentId)}`, {}, overrides);
+}
+
+export async function importKnowledgeLibraryDocument(body, overrides = {}) {
+ return apiFetch("/api/knowledge/library/import", { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function updateKnowledgeLibraryDocument(documentId, body, overrides = {}) {
+ return apiFetch(`/api/knowledge/library/${encodeURIComponent(documentId)}`, { method: "PATCH", body: JSON.stringify(body) }, overrides);
+}
+
+export async function reviewKnowledgeLibraryDocument(documentId, body, overrides = {}) {
+ return apiFetch(`/api/knowledge/library/${encodeURIComponent(documentId)}/review`, { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function archiveKnowledgeLibraryDocument(documentId, overrides = {}) {
+ return apiFetch(`/api/knowledge/library/${encodeURIComponent(documentId)}/archive`, { method: "POST", body: JSON.stringify({}) }, overrides);
+}
+
+export async function restoreKnowledgeLibraryDocument(documentId, overrides = {}) {
+ return apiFetch(`/api/knowledge/library/${encodeURIComponent(documentId)}/restore`, { method: "POST", body: JSON.stringify({}) }, overrides);
+}
+
+export async function fetchKnowledgeLibraryDocumentVersions(documentId, overrides = {}) {
+ return apiFetch(`/api/knowledge/library/${encodeURIComponent(documentId)}/versions`, {}, overrides);
+}
+
+export async function restoreKnowledgeLibraryDocumentVersion(documentId, versionKey, overrides = {}) {
+ return apiFetch(`/api/knowledge/library/${encodeURIComponent(documentId)}/versions/${encodeURIComponent(versionKey)}/restore`, { method: "POST", body: JSON.stringify({}) }, overrides);
+}
+
+export async function searchKnowledgeLibrary(params = {}, overrides = {}) {
+ const search = new URLSearchParams();
+ if (params.query) search.set("q", params.query);
+ if (params.sourceType) search.set("sourceType", params.sourceType);
+ if (params.scopeMode) search.set("scopeMode", params.scopeMode);
+ if (params.limit) search.set("limit", String(params.limit));
+ return apiFetch(`/api/knowledge/search${search.toString() ? `?${search.toString()}` : ""}`, {}, overrides);
+}
+
+export async function fetchKnowledgeContextPacks(overrides = {}) {
+ return apiFetch("/api/knowledge/context-packs", {}, overrides);
+}
+
+export async function createKnowledgeContextPack(body, overrides = {}) {
+ return apiFetch("/api/knowledge/context-packs", { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function fetchKnowledgeContextPack(packId, overrides = {}) {
+ return apiFetch(`/api/knowledge/context-packs/${encodeURIComponent(packId)}`, {}, overrides);
+}
+
+export async function materializeKnowledgeContextPack(packId, body, overrides = {}) {
+ return apiFetch(`/api/knowledge/context-packs/${encodeURIComponent(packId)}/materialize`, { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function materializeKnowledgeLibraryDocument(documentId, body, overrides = {}) {
+ return apiFetch(`/api/knowledge/library/${encodeURIComponent(documentId)}/materialize`, { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
export async function updateProductionBible(body, overrides = {}) {
return apiFetch("/api/production/bible", { method: "PATCH", body: JSON.stringify(body) }, overrides);
}
@@ -783,6 +893,14 @@ export async function approveDelivery(deliveryId, body = {}, overrides = {}) {
return apiFetch(`/api/production/deliveries/${encodeURIComponent(deliveryId)}/approve`, { method: "POST", body: JSON.stringify(body) }, overrides);
}
+export async function fetchDeliveryClearance(deliveryId, overrides = {}) {
+ return apiFetch(`/api/production/deliveries/${encodeURIComponent(deliveryId)}/clearance`, {}, overrides);
+}
+
+export async function runDeliveryClearance(deliveryId, body = {}, overrides = {}) {
+ return apiFetch(`/api/production/deliveries/${encodeURIComponent(deliveryId)}/clearance`, { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
export async function fetchDeliveryReleases(deliveryId, overrides = {}) {
return apiFetch(`/api/production/deliveries/${encodeURIComponent(deliveryId)}/releases`, {}, overrides);
}
@@ -902,6 +1020,52 @@ export async function probeModel(modelId, overrides = {}) {
return apiFetch(`/api/platform/models/${encodeURIComponent(modelId)}/probe`, { method: "POST", body: JSON.stringify({}) }, overrides);
}
+export async function fetchModelCatalog(overrides = {}) {
+ return apiFetch("/api/platform/model-catalog", {}, overrides);
+}
+
+export async function createModelCatalog(body, overrides = {}) {
+ return apiFetch("/api/platform/model-catalog", { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function updateModelCatalog(entryId, body, overrides = {}) {
+ return apiFetch(`/api/platform/model-catalog/${encodeURIComponent(entryId)}`, { method: "PATCH", body: JSON.stringify(body) }, overrides);
+}
+
+export async function fetchModelRoutes(overrides = {}) {
+ return apiFetch("/api/platform/model-routes", {}, overrides);
+}
+
+export async function createModelRoute(body, overrides = {}) {
+ return apiFetch("/api/platform/model-routes", { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function resolveModelRoute(body, overrides = {}) {
+ return apiFetch("/api/platform/model-routes/resolve", { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function fetchModelRouteApprovals(filters = {}, overrides = {}) {
+ const params = new URLSearchParams();
+ for (const [key, value] of Object.entries(filters || {})) {
+ if (value === undefined || value === null || value === "") continue;
+ params.set(key, String(value));
+ }
+ const query = params.toString();
+ return apiFetch(`/api/platform/model-route-approvals${query ? `?${query}` : ""}`, {}, overrides);
+}
+
+export async function requestModelRouteApproval(body, overrides = {}) {
+ return apiFetch("/api/platform/model-route-approvals", { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function decideModelRouteApproval(approvalId, body, overrides = {}) {
+ return apiFetch(`/api/platform/model-route-approvals/${encodeURIComponent(approvalId)}/decision`, { method: "POST", body: JSON.stringify(body) }, overrides);
+}
+
+export async function updateModelRoute(routeId, body, overrides = {}) {
+ return apiFetch(`/api/platform/model-routes/${encodeURIComponent(routeId)}`, { method: "PATCH", body: JSON.stringify(body) }, overrides);
+}
+
export async function fetchJobs(overrides = {}) {
return apiFetch("/api/jobs", {}, overrides);
}
diff --git a/src/styles.css b/src/styles.css
index e05f586..f20b4a8 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -3428,6 +3428,18 @@ body {
.search-box svg { color: var(--faint); }
.search-box input { width: 100%; min-height: 32px; color: var(--ink); border: 0; outline: 0; font: inherit; font-size: 11px; }
.mono { color: var(--blue) !important; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px !important; }
+.model-secret-meta {
+ display: block;
+ margin-top: 3px;
+ color: #6d7a72;
+ font-family: Inter, system-ui, sans-serif;
+ font-size: 10px;
+ font-weight: 700;
+ white-space: nowrap;
+}
+.model-secret-meta.configured { color: #1f7a55; }
+.model-secret-meta.missing { color: #a05b1b; }
+.model-secret-meta.not-required { color: var(--faint); }
.queue-table .enterprise-table-head,
.queue-table .enterprise-table-row { grid-template-columns: 42px minmax(140px, 1.15fr) minmax(160px, 1.2fr) minmax(120px, .9fr) 65px 50px 85px 85px minmax(130px, .9fr); min-width: 1100px; }
@@ -3499,6 +3511,65 @@ body {
.quota-edit-input { display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: 10px; }
.quota-edit-input input { width: 88px; min-height: 30px; padding: 0 7px; color: var(--ink); background: #ffffff; border: 1px solid var(--line); border-radius: 6px; outline: 0; font: inherit; }
.quota-edit-input input:disabled { color: var(--faint); background: #f3f6f3; }
+.commercial-approval-card { display: grid; gap: 14px; }
+.commercial-approval-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
+.commercial-approval-summary > div { min-height: 62px; padding: 11px; border: 1px solid #e4eae5; border-radius: 7px; background: #ffffff; }
+.commercial-approval-summary strong { display: block; color: var(--ink); font-size: 19px; }
+.commercial-approval-summary span { display: block; margin-top: 4px; color: var(--faint); font-size: 10px; }
+.commercial-approval-form { display: grid; grid-template-columns: minmax(150px, .75fr) minmax(260px, 1.5fr) minmax(110px, .55fr) minmax(110px, .55fr); gap: 12px; align-items: end; padding: 12px; border: 1px solid #e2e8e2; border-radius: 7px; background: #f8faf8; }
+.commercial-approval-form label { display: grid; gap: 6px; min-width: 0; color: var(--muted); font-size: 11px; }
+.commercial-approval-form input,
+.commercial-approval-form select,
+.commercial-approval-form textarea { width: 100%; min-height: 34px; padding: 0 9px; color: var(--ink); background: #ffffff; border: 1px solid var(--line); border-radius: 7px; outline: 0; font: inherit; }
+.commercial-approval-form textarea { min-height: 76px; padding: 9px; resize: vertical; line-height: 1.5; }
+.commercial-approval-form .approval-title-field,
+.commercial-approval-form .approval-reason-field,
+.commercial-approval-form .approval-policy-note,
+.commercial-approval-form .form-actions { grid-column: 1 / -1; }
+.approval-policy-note { display: flex; align-items: center; gap: 8px; min-height: 36px; padding: 8px 10px; color: var(--muted); background: #eef6f1; border: 1px solid #d9e9df; border-radius: 7px; font-size: 11px; }
+.approval-policy-note svg { flex: 0 0 auto; color: var(--accent); }
+.commercial-approval-table .enterprise-table-head,
+.commercial-approval-table .enterprise-table-row { grid-template-columns: minmax(250px, 1.45fr) minmax(170px, .8fr) minmax(120px, .65fr) minmax(150px, .8fr) 92px minmax(150px, .8fr) minmax(156px, .9fr); min-width: 1100px; }
+.commercial-approval-table .enterprise-table-row { min-height: 76px; }
+.approval-request-main,
+.approval-note-input,
+.usage-event-cell { min-width: 0; }
+.approval-request-main { display: grid; gap: 4px; }
+.approval-request-main strong { overflow: hidden; color: var(--ink); text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
+.approval-request-main small { overflow: hidden; color: var(--faint); text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
+.approval-note-input input { width: 100%; min-height: 30px; padding: 0 8px; color: var(--ink); background: #ffffff; border: 1px solid var(--line); border-radius: 6px; outline: 0; font: inherit; font-size: 11px; }
+.approval-note-input input:disabled { color: var(--faint); background: #f3f6f3; }
+.approval-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-width: 0; }
+.approval-actions > span { overflow: hidden; color: var(--faint); text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
+.entitlement-matrix-card { display: grid; gap: 14px; }
+.plan-template-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
+.plan-template-strip > div { display: grid; gap: 6px; min-height: 116px; padding: 12px; border: 1px solid #e2e8e2; border-radius: 7px; background: #f8faf8; }
+.plan-template-strip > div > div { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
+.plan-template-strip strong { overflow: hidden; color: var(--ink); text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
+.plan-template-strip span { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
+.plan-template-strip small { color: var(--faint); line-height: 1.5; }
+.system-plan-management-card { display: grid; gap: 14px; }
+.system-plan-template-strip > div { position: relative; }
+.system-plan-template-strip > div.selected { border-color: #8fc7aa; box-shadow: 0 0 0 3px rgba(36, 111, 87, .08); }
+.plan-template-actions { display: flex; flex-wrap: wrap; justify-content: flex-start !important; gap: 7px; margin-top: auto; }
+.system-plan-template-form { padding: 12px; border: 1px solid #e2e8e2; border-radius: 7px; background: #f8faf8; }
+.system-plan-template-form textarea { width: 100%; min-height: 82px; padding: 9px; color: var(--ink); background: #ffffff; border: 1px solid var(--line); border-radius: 7px; outline: 0; font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; resize: vertical; }
+.system-plan-template-form textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(36, 111, 87, .1); }
+.system-plan-template-form .plan-template-description,
+.system-plan-template-form .approval-policy-note { grid-column: 1 / -1; }
+.system-plan-template-form .plan-template-json { grid-column: span 1; }
+.entitlement-summary-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
+.entitlement-summary-row > div { min-height: 68px; padding: 12px; border: 1px solid #e4eae5; border-radius: 7px; background: #ffffff; }
+.entitlement-summary-row strong { display: block; color: var(--ink); font-size: 20px; }
+.entitlement-summary-row span { display: block; margin-top: 5px; color: var(--faint); font-size: 11px; }
+.entitlement-category { display: grid; gap: 8px; }
+.entitlement-table .enterprise-table-head,
+.entitlement-table .enterprise-table-row { grid-template-columns: minmax(220px, 1.35fr) 92px 145px 96px 92px 96px 86px; min-width: 960px; }
+.entitlement-table .enterprise-table-row { min-height: 64px; }
+.entitlement-table select { min-height: 30px; padding: 0 8px; color: var(--ink); background: #ffffff; border: 1px solid var(--line); border-radius: 6px; outline: 0; font: inherit; font-size: 11px; }
+.entitlement-name-cell { display: grid; gap: 4px; min-width: 0; }
+.entitlement-name-cell strong { overflow: hidden; color: var(--ink); text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
+.entitlement-name-cell small { overflow: hidden; color: var(--faint); text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
.commercial-usage-list { display: grid; gap: 7px; }
.commercial-usage-list > div { display: grid; grid-template-columns: minmax(0, 1fr) 70px 70px; gap: 10px; align-items: center; min-height: 42px; padding: 7px 0; border-bottom: 1px solid #e7ece8; }
.commercial-usage-list > div:last-child { border-bottom: 0; }
@@ -3788,6 +3859,14 @@ body {
.commercial-plan-form { grid-template-columns: 1fr; }
.commercial-plan-form .form-actions { grid-column: auto; }
.commercial-seat-meter { grid-template-columns: 1fr; }
+ .commercial-approval-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .commercial-approval-form { grid-template-columns: 1fr; }
+ .commercial-approval-form .approval-title-field,
+ .commercial-approval-form .approval-reason-field,
+ .commercial-approval-form .approval-policy-note,
+ .commercial-approval-form .form-actions { grid-column: auto; }
+ .plan-template-strip,
+ .entitlement-summary-row { grid-template-columns: 1fr; }
.commercial-usage-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.usage-filter-search,
.usage-filter-actions { grid-column: 1 / -1; }
@@ -4268,6 +4347,13 @@ body {
.suite-status.warn > span { background: #d0852e; }
+.suite-status.danger {
+ color: #9a2f2a;
+ background: #fde9e8;
+}
+
+.suite-status.danger > span { background: #c84740; }
+
.suite-toolbar {
display: flex;
align-items: center;
@@ -4312,6 +4398,465 @@ body {
min-width: 0;
}
+.knowledge-library-layout,
+.knowledge-detail-grid {
+ display: grid;
+ grid-template-columns: 340px minmax(0, 1fr);
+ gap: 16px;
+ min-width: 0;
+}
+
+.knowledge-detail-grid {
+ grid-template-columns: minmax(0, 1fr);
+ margin-top: 16px;
+}
+
+.knowledge-doc-list,
+.knowledge-chunk-list {
+ display: grid;
+ gap: 10px;
+ margin-top: 14px;
+}
+
+.knowledge-doc-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+ min-width: 0;
+ padding: 12px 14px;
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ text-align: left;
+}
+
+.knowledge-doc-row.selected {
+ border-color: #98b3ff;
+ background: #f4f8ff;
+}
+
+.knowledge-doc-row strong,
+.knowledge-chunk-toggle strong {
+ display: block;
+ color: var(--ink);
+ font-size: 13px;
+}
+
+.knowledge-doc-row span,
+.knowledge-chunk-toggle span {
+ color: var(--faint);
+ font-size: 11px;
+}
+
+.knowledge-doc-statuses {
+ display: grid;
+ gap: 6px;
+ justify-items: end;
+}
+
+.knowledge-import-form,
+.knowledge-summary-grid {
+ display: grid;
+ gap: 12px;
+}
+
+.knowledge-import-form label,
+.knowledge-search-controls label,
+.knowledge-governance-form label,
+.knowledge-editor-grid label {
+ display: grid;
+ gap: 6px;
+ min-width: 0;
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.knowledge-import-form input,
+.knowledge-import-form select,
+.knowledge-import-form textarea,
+.knowledge-search-controls input,
+.knowledge-search-controls select,
+.knowledge-governance-form input,
+.knowledge-governance-form select,
+.knowledge-governance-form textarea,
+.knowledge-editor-grid input,
+.knowledge-editor-grid select,
+.knowledge-editor-grid textarea {
+ width: 100%;
+ min-width: 0;
+ min-height: 36px;
+ padding: 8px 10px;
+ color: var(--ink);
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 7px;
+ outline: 0;
+ font: inherit;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.knowledge-import-form textarea,
+.knowledge-governance-form textarea,
+.knowledge-editor-grid textarea {
+ line-height: 1.6;
+ resize: vertical;
+}
+
+.knowledge-import-grid,
+.knowledge-summary-grid,
+.knowledge-entity-columns {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.knowledge-summary-grid div,
+.knowledge-entity-columns > div {
+ display: grid;
+ gap: 6px;
+ padding: 12px 14px;
+ background: #fbfcfd;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.knowledge-summary-grid span,
+.knowledge-entity-columns > div > span {
+ color: var(--faint);
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.knowledge-summary-grid strong {
+ color: var(--ink);
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.knowledge-entity-columns {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 16px;
+}
+
+.knowledge-governance-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.15fr) minmax(280px, .85fr) minmax(260px, .7fr);
+ gap: 12px;
+ margin-top: 16px;
+}
+
+.knowledge-governance-panel,
+.knowledge-editor-panel {
+ min-width: 0;
+ padding: 14px;
+ background: #fbfcfd;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.knowledge-governance-form,
+.knowledge-editor-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ margin-top: 12px;
+}
+
+.knowledge-governance-form .wide,
+.knowledge-editor-grid .wide {
+ grid-column: 1 / -1;
+}
+
+.knowledge-governance-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+.knowledge-risk-score {
+ display: grid;
+ grid-template-columns: 24px auto minmax(0, 1fr);
+ gap: 8px;
+ align-items: center;
+ margin-top: 12px;
+ padding: 10px 12px;
+ color: var(--muted);
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.knowledge-risk-score svg { color: var(--accent-2); }
+.knowledge-risk-score strong { color: var(--ink); font-size: 22px; }
+.knowledge-risk-score span { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
+
+.knowledge-risk-list,
+.knowledge-version-list {
+ display: grid;
+ gap: 8px;
+ margin-top: 12px;
+ max-height: 280px;
+ overflow: auto;
+}
+
+.knowledge-risk-list > div {
+ display: grid;
+ gap: 6px;
+ padding: 10px;
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.knowledge-risk-list strong {
+ color: var(--ink);
+ font-size: 12px;
+}
+
+.knowledge-risk-list p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.55;
+}
+
+.knowledge-risk-list span {
+ color: var(--faint);
+ font-size: 11px;
+}
+
+.knowledge-version-list button {
+ display: grid;
+ grid-template-columns: 32px minmax(0, 1fr) 92px 18px;
+ gap: 8px;
+ align-items: center;
+ min-height: 42px;
+ padding: 8px 10px;
+ color: var(--ink);
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ text-align: left;
+}
+
+.knowledge-version-list button:hover {
+ border-color: var(--accent);
+}
+
+.knowledge-version-list span {
+ color: var(--accent);
+ font-size: 12px;
+ font-weight: 800;
+}
+
+.knowledge-version-list strong {
+ overflow: hidden;
+ font-size: 12px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.knowledge-version-list em {
+ color: var(--faint);
+ font-size: 10px;
+ font-style: normal;
+ white-space: nowrap;
+}
+
+.knowledge-editor-panel {
+ margin-top: 16px;
+}
+
+.knowledge-entity-columns > div > div {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.knowledge-entity-columns button,
+.knowledge-chunk-tags span {
+ padding: 5px 10px;
+ color: var(--muted);
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ font-size: 11px;
+}
+
+.knowledge-entity-columns em {
+ color: var(--faint);
+ font-size: 11px;
+ font-style: normal;
+}
+
+.knowledge-chunk-card {
+ display: grid;
+ gap: 10px;
+ padding: 14px;
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.knowledge-chunk-card.active {
+ border-color: #98b3ff;
+ background: #f5f8ff;
+}
+
+.knowledge-chunk-toggle {
+ display: grid;
+ grid-template-columns: 16px minmax(0, 1fr);
+ gap: 10px;
+ align-items: start;
+}
+
+.knowledge-chunk-toggle input {
+ margin-top: 4px;
+}
+
+.knowledge-chunk-card p {
+ margin: 0;
+ color: var(--muted);
+ line-height: 1.65;
+ white-space: pre-wrap;
+}
+
+.knowledge-chunk-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.knowledge-chunk-statuses {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.knowledge-rag-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr);
+ gap: 16px;
+ margin-top: 16px;
+ min-width: 0;
+}
+
+.knowledge-search-controls {
+ display: grid;
+ grid-template-columns: minmax(220px, 1.4fr) minmax(120px, 0.7fr) minmax(120px, 0.7fr) minmax(120px, 0.6fr);
+ gap: 12px;
+ align-items: end;
+}
+
+.knowledge-search-results,
+.knowledge-pack-list,
+.knowledge-pack-actions {
+ display: grid;
+ gap: 10px;
+ margin-top: 14px;
+}
+
+.knowledge-result-row {
+ display: grid;
+ grid-template-columns: 16px minmax(0, 1fr) 110px;
+ gap: 12px;
+ align-items: start;
+ padding: 12px;
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.knowledge-result-row.active {
+ border-color: #98b3ff;
+ background: #f5f8ff;
+}
+
+.knowledge-result-row input {
+ margin-top: 4px;
+}
+
+.knowledge-result-main {
+ display: grid;
+ gap: 5px;
+ min-width: 0;
+}
+
+.knowledge-result-main strong {
+ color: var(--ink);
+ font-size: 13px;
+}
+
+.knowledge-result-main small,
+.knowledge-result-meta {
+ color: var(--faint);
+ font-size: 11px;
+}
+
+.knowledge-result-main em {
+ color: var(--muted);
+ font-size: 12px;
+ font-style: normal;
+ line-height: 1.6;
+}
+
+.knowledge-result-meta {
+ justify-self: end;
+ padding-top: 2px;
+ white-space: nowrap;
+}
+
+.knowledge-pack-actions {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.knowledge-pack-actions button {
+ justify-content: center;
+}
+
+.knowledge-pack-list button {
+ display: grid;
+ gap: 4px;
+ padding: 11px 12px;
+ text-align: left;
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.knowledge-pack-list button.selected {
+ border-color: #2f6fed;
+ background: #f4f8ff;
+}
+
+.knowledge-pack-list strong {
+ color: var(--ink);
+ font-size: 13px;
+}
+
+.knowledge-pack-list span {
+ color: var(--faint);
+ font-size: 11px;
+}
+
+.knowledge-pack-preview,
+.knowledge-job-preview {
+ margin-top: 14px;
+}
+
+.knowledge-pack-preview pre,
+.knowledge-job-preview pre {
+ max-height: 380px;
+ overflow: auto;
+ white-space: pre-wrap;
+}
+
.suite-filter-rail,
.asset-inspector,
.voice-inspector,
@@ -4488,6 +5033,15 @@ body {
gap: 7px;
}
+.asset-card-statuses {
+ display: flex;
+ flex: 0 0 auto;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 4px;
+ max-width: 168px;
+}
+
.asset-card-title strong {
overflow: hidden;
font-size: 13px;
@@ -4596,6 +5150,190 @@ body {
gap: 7px;
}
+.commercial-asset-inspector {
+ max-height: calc(100vh - 150px);
+ overflow: auto;
+ padding-right: 2px;
+}
+
+.asset-risk-panel {
+ display: grid;
+ gap: 8px;
+ padding: 10px;
+ background: #f7f9f7;
+ border: 1px solid #e1e8e3;
+ border-radius: 7px;
+}
+
+.asset-risk-panel.blocked {
+ background: #fff7ef;
+ border-color: #edcaa6;
+}
+
+.asset-risk-panel.clear {
+ background: #edf7f1;
+ border-color: #bdddc9;
+}
+
+.asset-risk-panel > div:first-child {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+}
+
+.asset-risk-panel > div:first-child svg {
+ color: var(--accent-2);
+}
+
+.asset-risk-panel strong {
+ flex: 1;
+ color: var(--ink);
+ font-size: 12px;
+}
+
+.asset-risk-panel p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 10px;
+ line-height: 1.55;
+}
+
+.asset-risk-list {
+ display: grid;
+ gap: 6px;
+}
+
+.asset-risk-list > div {
+ display: grid;
+ gap: 3px;
+ padding: 7px;
+ background: #ffffff;
+ border: 1px solid rgba(36, 74, 55, .1);
+ border-radius: 6px;
+}
+
+.asset-risk-list span {
+ color: var(--accent-2);
+ font-size: 9px;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.asset-risk-list strong {
+ font-size: 11px;
+}
+
+.asset-governance-form {
+ display: grid;
+ gap: 9px;
+ padding: 10px;
+ background: #fbfcfb;
+ border: 1px solid var(--line);
+ border-radius: 7px;
+}
+
+.governance-form-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.governance-form-grid label {
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+ color: var(--faint);
+ font-size: 10px;
+ font-weight: 800;
+}
+
+.governance-form-grid .full-span {
+ grid-column: 1 / -1;
+}
+
+.governance-form-grid input,
+.governance-form-grid select,
+.governance-form-grid textarea {
+ width: 100%;
+ min-height: 34px;
+ padding: 0 9px;
+ color: var(--ink);
+ background: #ffffff;
+ border: 1px solid #dbe4de;
+ border-radius: 6px;
+ outline: 0;
+ font: inherit;
+ font-size: 11px;
+}
+
+.governance-form-grid textarea {
+ min-height: 68px;
+ padding-top: 8px;
+ resize: vertical;
+}
+
+.governance-action-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 7px;
+}
+
+.asset-review-history {
+ display: grid;
+ gap: 8px;
+ padding-top: 2px;
+}
+
+.asset-review-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.asset-review-head strong {
+ color: var(--ink);
+ font-size: 12px;
+}
+
+.asset-review-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 4px 8px;
+ padding: 8px;
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+}
+
+.asset-review-row > div {
+ display: grid;
+ gap: 2px;
+ min-width: 0;
+}
+
+.asset-review-row strong {
+ overflow: hidden;
+ color: var(--ink);
+ font-size: 11px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.asset-review-row span,
+.asset-review-row small,
+.asset-review-empty {
+ color: var(--faint);
+ font-size: 9px;
+}
+
+.asset-review-row small {
+ grid-column: 1 / -1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.visually-hidden {
position: absolute;
width: 1px;
@@ -5888,6 +6626,31 @@ body {
.delivery-release-row > span:nth-child(2),
.delivery-release-row > span:nth-child(4) { display: grid; gap: 3px; min-width: 0; }
.delivery-release-row .row-actions { align-items: center; }
+.delivery-clearance-card { display: grid; gap: 12px; }
+.delivery-clearance-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: end; }
+.delivery-clearance-toolbar label { display: grid; gap: 5px; width: min(260px, 100%); color: var(--muted); font-size: 10px; font-weight: 800; }
+.delivery-clearance-toolbar select { min-height: 34px; padding: 0 9px; color: var(--ink); background: #fbfcfb; border: 1px solid var(--line); border-radius: 6px; outline: 0; font: inherit; font-size: 11px; }
+.delivery-clearance-toolbar select:focus { border-color: var(--accent); }
+.delivery-clearance-summary { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; }
+.delivery-clearance-summary > div { display: grid; gap: 4px; min-height: 58px; padding: 10px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
+.delivery-clearance-summary span { color: var(--faint); font-size: 9px; font-weight: 800; }
+.delivery-clearance-summary strong { overflow: hidden; color: var(--ink); font-size: 16px; text-overflow: ellipsis; white-space: nowrap; }
+.delivery-clearance-summary .ok strong { color: var(--accent); }
+.delivery-clearance-summary .warn strong { color: #a34f22; }
+.delivery-clearance-blockers,
+.delivery-clearance-review { display: grid; gap: 5px; padding: 10px 11px; border-radius: 7px; font-size: 10px; }
+.delivery-clearance-blockers { color: #85451d; background: #fff0e2; border: 1px solid #ecc7a3; }
+.delivery-clearance-review { color: #725727; background: #fff9e9; border: 1px solid #ead7a2; }
+.delivery-clearance-blockers strong,
+.delivery-clearance-review strong { color: var(--ink); font-size: 11px; }
+.delivery-clearance-blockers span,
+.delivery-clearance-review span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.delivery-clearance-lanes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
+.delivery-clearance-lanes > div { display: grid; align-content: start; gap: 7px; min-width: 0; padding: 10px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
+.delivery-clearance-lanes strong { color: var(--ink); font-size: 11px; }
+.delivery-clearance-lanes span { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 3px 6px; align-items: center; min-width: 0; color: var(--muted); font-size: 10px; }
+.delivery-clearance-lanes span small { grid-column: 2; overflow: hidden; color: var(--faint); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
+.delivery-clearance-lanes > div > small { color: var(--faint); font-size: 10px; }
.delivery-table-head,
.delivery-table-row { display: grid; grid-template-columns: 80px 85px minmax(200px, 1fr) 85px 100px minmax(150px, .8fr); gap: 10px; align-items: center; min-width: 760px; }
.delivery-table-row { min-height: 62px; color: var(--muted); border-bottom: 1px solid #e7ece8; font-size: 11px; }
@@ -5904,6 +6667,223 @@ body {
.queue-table .enterprise-table-head,
.queue-table .enterprise-table-row { grid-template-columns: 42px minmax(140px, 1.15fr) minmax(160px, 1.2fr) minmax(120px, .9fr) 65px 50px 85px 85px minmax(130px, .9fr); min-width: 1100px; }
+.model-resolution-panel { margin-top: 14px; }
+.model-resolution-form {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 10px;
+ align-items: end;
+}
+.model-resolution-form label {
+ display: grid;
+ gap: 6px;
+ min-width: 0;
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 800;
+}
+.model-resolution-form input,
+.model-resolution-form select {
+ width: 100%;
+ min-width: 0;
+ min-height: 36px;
+ padding: 8px 10px;
+ color: var(--ink);
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 7px;
+ font: inherit;
+ font-size: 12px;
+}
+.model-resolution-wide {
+ grid-column: span 2;
+}
+.model-resolution-wide textarea {
+ width: 100%;
+ min-width: 0;
+ min-height: 72px;
+ padding: 8px 10px;
+ resize: vertical;
+ color: var(--ink);
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 7px;
+ font: inherit;
+ font-size: 12px;
+ line-height: 1.5;
+}
+.model-resolution-checkbox {
+ display: inline-flex !important;
+ gap: 8px !important;
+ align-items: center;
+ align-self: center;
+ min-height: 36px;
+ padding: 8px 10px;
+ background: #f7faf8;
+ border: 1px solid var(--line);
+ border-radius: 7px;
+}
+.model-resolution-checkbox input {
+ flex: 0 0 auto;
+ width: 16px;
+ min-width: 16px;
+ min-height: 16px;
+ padding: 0;
+ accent-color: var(--accent);
+}
+.model-resolution-result {
+ display: grid;
+ gap: 12px;
+ margin-top: 14px;
+}
+.model-resolution-banner {
+ display: grid;
+ grid-template-columns: 28px minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+ min-height: 54px;
+ padding: 12px;
+ background: #eef7f1;
+ border: 1px solid #cfe4d6;
+ border-radius: 8px;
+}
+.model-resolution-banner.approval-required {
+ background: #fff6e9;
+ border-color: #ead4ad;
+}
+.model-resolution-banner.blocked {
+ background: #fff0ef;
+ border-color: #ecc4c0;
+}
+.model-resolution-banner svg { color: var(--accent); }
+.model-resolution-banner.blocked svg,
+.model-resolution-banner.approval-required svg { color: var(--accent-2); }
+.model-resolution-banner strong {
+ display: block;
+ color: var(--ink);
+ font-size: 14px;
+}
+.model-resolution-banner span {
+ color: var(--muted);
+ font-size: 12px;
+}
+.model-resolution-cards {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 10px;
+}
+.model-resolution-cards > div {
+ display: grid;
+ gap: 5px;
+ min-width: 0;
+ padding: 11px 12px;
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+.model-resolution-cards span,
+.model-resolution-cards small {
+ overflow: hidden;
+ color: var(--faint);
+ font-size: 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.model-resolution-cards strong {
+ overflow: hidden;
+ color: var(--ink);
+ font-size: 13px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.model-resolution-reasons {
+ display: grid;
+ gap: 8px;
+}
+.model-resolution-reasons > div {
+ display: grid;
+ grid-template-columns: 108px 190px minmax(0, 1fr);
+ gap: 10px;
+ align-items: center;
+ min-height: 40px;
+ padding: 8px 10px;
+ background: #ffffff;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+.model-resolution-reasons strong {
+ color: var(--ink);
+ font-size: 12px;
+}
+.model-resolution-reasons span {
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.5;
+}
+.model-resolution-json {
+ padding: 10px 12px;
+ background: #101b17;
+ border-radius: 8px;
+}
+.model-resolution-json summary {
+ cursor: pointer;
+ color: #dfe8df;
+ font-size: 12px;
+ font-weight: 800;
+}
+.model-resolution-json pre {
+ max-height: 340px;
+ overflow: auto;
+ color: #f5fbf5;
+ white-space: pre-wrap;
+}
+.model-approval-panel { margin-top: 14px; }
+.model-approval-kpis {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 10px;
+ margin-bottom: 12px;
+}
+.model-approval-kpis > div {
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+ padding: 11px 12px;
+ background: #f8fbf8;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+.model-approval-kpis span,
+.model-approval-kpis small {
+ overflow: hidden;
+ color: var(--faint);
+ font-size: 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.model-approval-kpis strong {
+ color: var(--ink);
+ font-size: 19px;
+}
+.model-approval-table .enterprise-table-head,
+.model-approval-table .enterprise-table-row {
+ grid-template-columns: minmax(132px, 1.2fr) minmax(108px, .9fr) minmax(150px, 1.3fr) minmax(132px, 1.1fr) 98px 88px 118px minmax(142px, 1fr);
+ min-width: 1180px;
+}
+.model-approval-table .enterprise-table-row {
+ min-height: 64px;
+}
+.muted-mini {
+ display: block;
+ max-width: 180px;
+ overflow: hidden;
+ color: var(--faint);
+ font-size: 10px;
+ line-height: 1.45;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
@media (max-width: 1180px) {
.production-template-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.production-three-column { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@@ -5916,6 +6896,7 @@ body {
.production-two-column,
.episode-catalog-layout,
.director-production-layout,
+ .knowledge-rag-grid,
.qa-review-layout { grid-template-columns: 1fr; }
.studio-banner,
.studio-workbench,
@@ -5923,7 +6904,14 @@ body {
.studio-preview-summary,
.studio-preview-stats,
.studio-continuity-grid,
- .studio-queue-grid { grid-template-columns: 1fr; }
+ .knowledge-governance-grid,
+ .model-resolution-form,
+ .model-resolution-cards,
+ .model-approval-kpis,
+ .governance-form-grid,
+ .knowledge-search-controls,
+ .studio-queue-grid { grid-template-columns: 1fr; }
+ .model-resolution-wide { grid-column: auto; }
.studio-banner,
.studio-stage-head { flex-direction: column; }
.studio-banner-side { width: 100%; }
@@ -5931,6 +6919,8 @@ body {
.studio-inspector { min-width: 0; }
.asset-governance-bar,
.qa-review-summary,
+ .delivery-clearance-summary,
+ .delivery-clearance-lanes,
.delivery-contract-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.production-template-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@@ -5953,14 +6943,22 @@ body {
.studio-preview-copy,
.studio-preview-stats { grid-template-columns: 1fr; }
.studio-continuity-grid,
+ .knowledge-pack-actions,
+ .knowledge-result-row,
.studio-queue-grid { grid-template-columns: 1fr; }
- .studio-definition-list { grid-template-columns: 76px minmax(0, 1fr); }
+ .knowledge-result-meta { justify-self: start; }
+ .studio-definition-list { grid-template-columns: 76px minmax(0, 1fr); }
+ .commercial-asset-inspector { max-height: none; }
+ .asset-card-statuses { justify-content: flex-start; max-width: none; }
+ .governance-action-row button { flex: 1 1 140px; justify-content: center; }
.studio-queue-actions { flex-direction: column; }
.studio-queue-actions button { width: 100%; justify-content: flex-start; }
.production-three-column,
.production-template-grid,
.asset-governance-bar,
.qa-review-summary,
+ .delivery-clearance-summary,
+ .delivery-clearance-lanes,
.delivery-contract-grid { grid-template-columns: 1fr; }
.production-three-column > :last-child { grid-column: auto; }
.asset-toolbar { align-items: stretch; flex-direction: column; }
@@ -5972,6 +6970,8 @@ body {
.review-comment-form { grid-template-columns: 1fr; }
.delivery-create-bar { align-items: stretch; flex-direction: column; }
.delivery-create-bar label { width: 100%; }
+ .delivery-clearance-toolbar { align-items: stretch; flex-direction: column; }
+ .delivery-clearance-toolbar label { width: 100%; }
.delivery-channel-form,
.delivery-release-form { grid-template-columns: 1fr; align-items: stretch; }
.channel-approval-toggle { min-height: 30px; }
@@ -5983,6 +6983,7 @@ body {
@media (max-width: 1200px) {
.asset-library-layout { grid-template-columns: 150px minmax(0, 1fr); }
+ .knowledge-library-layout { grid-template-columns: 1fr; }
.asset-inspector { grid-column: 1 / -1; }
.asset-inspector > .asset-visual { min-height: 80px; }
.inspector-actions { grid-template-columns: repeat(2, minmax(0, 180px)); }
@@ -5991,12 +6992,24 @@ body {
@media (max-width: 980px) {
.suite-metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.asset-library-layout,
+ .knowledge-library-layout,
+ .knowledge-rag-grid,
+ .knowledge-import-grid,
+ .knowledge-governance-grid,
+ .knowledge-governance-form,
+ .knowledge-editor-grid,
+ .model-resolution-form,
+ .model-resolution-cards,
+ .knowledge-search-controls,
+ .knowledge-summary-grid,
+ .knowledge-entity-columns,
.voice-layout,
.assistant-layout,
.batch-layout { grid-template-columns: 1fr; }
.suite-filter-rail { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: center; }
.suite-filter-title { grid-column: 1 / -1; }
.suite-rail-divider { display: none; }
+ .knowledge-pack-actions { grid-template-columns: 1fr; }
.suite-rail-note { grid-column: 1 / -1; }
.voice-inspector,
.assistant-action-panel { width: 100%; }
@@ -6017,6 +7030,12 @@ body {
.suite-filter-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.suite-toolbar { align-items: stretch; flex-direction: column; }
.suite-search { min-width: 0; }
+ .knowledge-doc-row { align-items: flex-start; flex-direction: column; }
+ .knowledge-doc-statuses { justify-items: start; }
+ .model-resolution-banner,
+ .model-resolution-reasons > div { grid-template-columns: 1fr; justify-items: start; }
+ .knowledge-version-list button { grid-template-columns: 32px minmax(0, 1fr) 18px; }
+ .knowledge-version-list em { grid-column: 2; white-space: normal; }
.inspector-actions { grid-template-columns: 1fr; }
.batch-control-bar { grid-template-columns: 1fr; }
.batch-policy { grid-column: auto; }
@@ -6103,6 +7122,10 @@ body {
.generation-job-kind strong { overflow: hidden; color: var(--ink); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.generation-job-kind small { overflow: hidden; color: var(--faint); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
.generation-job-list > button > span:not(.generation-job-kind) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.generation-job-route { display: grid; gap: 2px; min-width: 0; }
+.generation-job-route strong, .generation-job-route small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.generation-job-route strong { color: var(--ink); font-size: 10px; }
+.generation-job-route small { color: var(--faint); font-size: 9px; }
.generation-detail-card { min-height: 560px; }
.generation-detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 11px; }
.generation-detail-grid > div { display: grid; gap: 4px; min-width: 0; padding: 9px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
@@ -6122,6 +7145,11 @@ body {
.json-heading h3 { margin: 0; color: var(--ink); font-size: 12px; }
.generation-json-grid pre, .generation-adapter-card .request-block pre { max-height: 280px; margin: 0; overflow: auto; padding: 10px; color: var(--blue); background: #17201d; border-radius: 7px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9px; line-height: 1.55; white-space: pre-wrap; }
.generation-adapter-card .request-block { margin-top: 12px; }
+.routing-preview { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 12px; padding: 10px; background: #f3f8f3; border: 1px solid #dceadd; border-radius: 7px; }
+.routing-preview > div { display: grid; gap: 3px; min-width: 0; }
+.routing-preview span { color: var(--faint); font-size: 9px; }
+.routing-preview strong { overflow: hidden; color: var(--ink); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
+.routing-preview-loading { margin-top: 8px; color: var(--faint); font-size: 10px; }
.generation-contract-row { display: flex; gap: 7px; align-items: flex-start; margin-top: 11px; padding: 10px; color: var(--muted); background: #f3f8f3; border: 1px solid #dceadd; border-radius: 7px; font-size: 10px; line-height: 1.5; }
.generation-contract-row svg { flex: 0 0 auto; color: var(--accent); }
.dependency-selector { display: grid; grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); gap: 10px; align-items: end; margin-top: 12px; padding: 10px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
@@ -6270,6 +7298,7 @@ body {
@media (max-width: 900px) {
.generation-queue-layout { grid-template-columns: 1fr; }
.generation-detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .routing-preview { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 680px) {
@@ -6290,6 +7319,7 @@ body {
.account-device-row { grid-template-columns: 32px minmax(0, 1fr); align-items: start; }
.account-device-state, .account-device-actions { grid-column: 2; justify-self: start; }
.generation-detail-grid, .generation-json-grid { grid-template-columns: 1fr; }
+ .routing-preview { grid-template-columns: 1fr; }
.generation-approval-box { align-items: flex-start; flex-wrap: wrap; }
.generation-approval-box label { width: 100%; }
.generation-job-list > button { grid-template-columns: minmax(150px, 1fr) 65px auto 15px; }