feat: expand commercial ai drama platform

This commit is contained in:
xz
2026-08-31 21:09:48 +08:00
parent f06e583c9b
commit 8caf947b80
35 changed files with 10181 additions and 188 deletions
+317 -7
View File
@@ -15,9 +15,11 @@ import {
buildContextPayload,
createAsset,
createAssetVersion,
ensureOrganizationEntitlements,
getAsset,
httpError,
orgMembers,
organizationEntitlements,
organizationRolePolicies,
updateOrganizationRolePolicy,
previewInvitation,
@@ -41,7 +43,11 @@ import {
apiClients,
identityCenter,
publicIdentityProviders,
createModelCatalogEntry,
createModelRoute,
updateIdentityPolicy,
updateModelCatalogEntry,
updateModelRoute,
saveIdentityProvider,
probeIdentityProvider,
createDirectorySync,
@@ -59,8 +65,10 @@ import {
scimPatchUser,
scimDeleteUser,
serviceHealth,
scanAssetGovernance,
updateAssetLock,
updateAssetRights,
listAssetGovernanceReviews,
restoreAssetVersion,
updateServiceHealth,
usageSummary,
@@ -82,7 +90,17 @@ import {
workspaceMembers,
listAuditEvents,
getAuditEvent,
exportAuditEvents
exportAuditEvents,
scopedModelCatalog,
scopedModelRoutes,
subscriptionPlanTemplates,
createSubscriptionPlanTemplate,
updateSubscriptionPlanTemplate,
updateOrganizationEntitlement,
requireEntitlement,
listCommercialApprovalRequests,
createCommercialApprovalRequest,
decideCommercialApprovalRequest
} from "./tenant.mjs";
import { platformData, platformResearchMatrix } from "../src/platform/platformData.js";
import { authMode, authenticate, cancelMfaSetup, changePassword, completeMfaChallenge, completeMfaEnrollment, createMfaChallenge, createMfaEnrollmentChallenge, createSession, disableMfa, enableMfa, listSecurityEvents, listUserDevices, listUserSessions, mfaRequiredForUser, mfaStatus, recordSecurityEvent, revokeAllUserSessions, revokeOtherUserSessions, revokeSession, revokeUserSession, safeUser, sessionIdentity, startMfaEnrollment, startMfaSetup, trustUserDevice, untrustUserDevice } from "./auth.mjs";
@@ -99,6 +117,7 @@ import {
createSeason,
createShot,
decideReview,
getDeliveryClearance,
importScript,
listDeliveryBatches,
listDeliveryChannels,
@@ -112,6 +131,7 @@ import {
runMediaQa,
restoreShotVersion,
rollbackDeliveryBatch,
runDeliveryClearance,
publishDeliveryRelease,
decideDeliveryRelease,
savePromptVersion,
@@ -123,11 +143,16 @@ import {
} from "./production.mjs";
import {
createGenerationJob,
previewGenerationJob,
executeGenerationJob,
getGenerationJob,
listGenerationJobs,
decideModelRouteApproval,
endpointInfo,
listModelRouteApprovalRequests,
previewModelRouteResolution,
probeModelConnector,
requestModelRouteApproval,
updateModelConnector
} from "./execution.mjs";
import { reclaimStorage, requireStorageQuota, storageCleanupPreview, storageSummary } from "./storage.mjs";
@@ -145,6 +170,23 @@ import { addTaskLink, createProjectTask, createTaskComment, getProjectTask, list
import { searchPlatform } from "./search.mjs";
import { consumeRateLimit, rateLimitHeaders, rateLimitIdentity } from "./rate-limit.mjs";
import { createDeliveryAccessLink, listDeliveryAccessFeedback, listDeliveryAccessLinks, readPublicDeliveryFile, resolvePublicDeliveryPortal, revokeDeliveryAccessLink, submitPublicDeliveryFeedback } from "./delivery-portal.mjs";
import {
archiveKnowledgeDocument,
createKnowledgeContextPack,
getKnowledgeContextPack,
getKnowledgeDocument,
importKnowledgeDocument,
listKnowledgeDocumentVersions,
listKnowledgeContextPacks,
listKnowledgeDocuments,
materializeKnowledgeContextPack,
materializeKnowledgeDocument,
restoreKnowledgeDocument,
restoreKnowledgeDocumentVersion,
reviewKnowledgeDocument,
updateKnowledgeDocument,
searchKnowledge
} from "./knowledge.mjs";
const port = Number(process.env.AI_DRAMA_API_PORT || 8787);
const root = resolve(import.meta.dirname, "..");
@@ -667,6 +709,7 @@ function createOrganization(context, body) {
dbRun("INSERT INTO quota_allocations(id, organization_id, workspace_id, metric, limit_value, used_value, unit, period_start, period_end, created_at, updated_at) VALUES (?, ?, ?, 'clip', 2400, 0, 'clips', datetime('now', 'start of month'), datetime('now', 'start of month', '+1 month', '-1 second'), ?, ?)", [createId("quota"), organizationId, workspaceId, timestamp, timestamp]);
dbRun("INSERT INTO audit_logs(id, organization_id, workspace_id, actor_user_id, action, target_type, target_id, result, metadata_json, created_at) VALUES (?, ?, ?, ?, 'organization.created', 'organization', ?, 'ok', ?, ?)", [createId("aud"), organizationId, workspaceId, context.user.id, organizationId, JSON.stringify({ createdFrom: context.organization.id }), timestamp]);
});
ensureOrganizationEntitlements(organizationId);
return dbGet("SELECT * FROM organizations WHERE id = ?", [organizationId]);
}
@@ -688,6 +731,7 @@ function updateOrganization(context, organizationId, body) {
function createWorkspace(context, body) {
requirePermission(context, "workspace:create");
requireEntitlement(context, "limit.workspaces", 1);
const name = String(body.name || "").trim();
if (!name) throw httpError(400, "name_required", "工作区名称不能为空");
const id = body.id || createId("ws");
@@ -742,6 +786,7 @@ function updateWorkspace(context, workspaceId, body) {
function createProject(context, body) {
requirePermission(context, "project:create");
requireEntitlement(context, "limit.projects", 1);
const name = String(body.name || "").trim();
if (!name) throw httpError(400, "name_required", "项目名称不能为空");
const id = body.id || createId("project");
@@ -836,7 +881,13 @@ async function uploadAssetVersion(context, assetId, body) {
contentSha256,
mimeType: body.mimeType || "application/octet-stream",
rightsStatus: body.rightsStatus || "needs-evidence",
versionNote: body.versionNote || "本地文件版本上传"
versionNote: body.versionNote || "本地文件版本上传",
provenance: body.provenance,
risk: body.risk,
tags: body.tags,
licenseScope: body.licenseScope,
expiresAt: body.expiresAt,
metadata: body.metadata
});
}
@@ -868,7 +919,7 @@ function mimeForPath(pathname) {
}
async function assetContent(context, assetId) {
if (!hasPermission(context, "asset:edit") && !hasPermission(context, "voice:edit")) {
if (!hasPermission(context, "asset:edit") && !hasPermission(context, "voice:edit") && !hasPermission(context, "compliance:manage")) {
throw httpError(403, "permission_denied", "当前角色没有资产预览权限");
}
const asset = getAsset(context, assetId);
@@ -894,7 +945,9 @@ async function assetContent(context, assetId) {
}
async function verifyAssetContent(context, assetId) {
requirePermission(context, "asset:edit");
if (!hasPermission(context, "asset:edit") && !hasPermission(context, "compliance:manage")) {
requirePermission(context, "asset:edit");
}
const asset = getAsset(context, assetId);
if (!asset) throw httpError(404, "asset_not_found", "资产不存在或不属于当前项目");
const version = asset.currentVersion;
@@ -1054,11 +1107,13 @@ function updateProjectMember(context, projectId, userId, body) {
function registerModel(context, body) {
requirePermission(context, "model:manage");
requireEntitlement(context, "limit.model_connectors", 1);
const id = body.id || createId("model");
const timestamp = new Date().toISOString();
const label = String(body.label || "未命名本地模型").trim();
const endpoint = String(body.endpoint || "http://127.0.0.1:7860").trim();
const costMode = String(body.costMode || "local").trim();
const kind = String(body.kind || "http-json").trim();
if (!label || !endpoint) throw httpError(400, "adapter_fields_required", "连接器名称和地址不能为空");
if (!["local", "mixed", "cloud"].includes(costMode)) throw httpError(400, "cost_mode_invalid", "成本策略无效");
const { local } = endpointInfo(endpoint);
@@ -1066,9 +1121,11 @@ function registerModel(context, body) {
const requestedApproval = Boolean(body.approvalRequired);
if (costMode !== "local" && !requestedApproval) throw httpError(400, "external_connector_approval_required", "混合或外部连接器必须开启审批策略");
const approvalRequired = costMode === "local" ? requestedApproval : true;
if (costMode !== "local") requireEntitlement(context, "feature.external_cloud_connectors", 1);
if (kind === "comfyui") requireEntitlement(context, "feature.comfyui_adapter", 1);
const protocol = body.protocol && typeof body.protocol === "object" ? body.protocol : {};
const authEnv = String(body.authEnv || protocol.authEnv || "").trim();
dbRun("INSERT INTO model_connectors(id, organization_id, workspace_id, label, kind, capabilities_json, endpoint, status, cost_mode, approval_required, protocol_json, auth_env, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'not-connected', ?, ?, ?, ?, ?, ?, ?)", [id, context.organization.id, context.workspace.id, label, body.kind || "http-json", JSON.stringify(body.capability || ["custom"]), endpoint, costMode, approvalRequired ? 1 : 0, JSON.stringify(protocol), authEnv, context.user.id, timestamp, timestamp]);
dbRun("INSERT INTO model_connectors(id, organization_id, workspace_id, label, kind, capabilities_json, endpoint, status, cost_mode, approval_required, protocol_json, auth_env, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'not-connected', ?, ?, ?, ?, ?, ?, ?)", [id, context.organization.id, context.workspace.id, label, kind, JSON.stringify(body.capability || ["custom"]), endpoint, costMode, approvalRequired ? 1 : 0, JSON.stringify(protocol), authEnv, context.user.id, timestamp, timestamp]);
addAudit({ context, action: "model.registered", targetType: "model_connector", targetId: id, metadata: { label, endpoint, costMode, approvalRequired } });
return parseModelRow(dbGet("SELECT * FROM model_connectors WHERE id = ?", [id]));
}
@@ -1077,6 +1134,7 @@ function systemConfigPayload() {
return {
settings: systemSettings(),
featureFlags: featureFlags(),
planTemplates: subscriptionPlanTemplates({ includeArchived: true }),
notifications: notificationChannels(),
notificationDeliveries: [],
apiClients: apiClients(),
@@ -1149,6 +1207,7 @@ function updateNotificationChannel(context, body) {
function createApiClient(context, body) {
requirePermission(context, "api_client:manage");
requireEntitlement(context, "limit.api_clients", 1);
const timestamp = new Date().toISOString();
const id = `client-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
const issued = issueApiClientKey();
@@ -1656,6 +1715,50 @@ createServer(async (req, res) => {
return send(res, 200, organizationCommercial(context));
}
const organizationEntitlementsMatch = pathname.match(/^\/api\/organizations\/([^/]+)\/entitlements$/);
if (req.method === "GET" && organizationEntitlementsMatch) {
const organizationId = decodeURIComponent(organizationEntitlementsMatch[1]);
const context = contextWith({ organizationId, workspaceId: "", projectId: "" }, req);
if (!hasPermission(context, "usage:view") && !hasPermission(context, "billing:manage") && !hasPermission(context, "quota:manage")) {
throw httpError(403, "permission_denied", "当前角色没有查看组织套餐权益的权限");
}
return send(res, 200, organizationEntitlements(organizationId));
}
const organizationEntitlementMatch = pathname.match(/^\/api\/organizations\/([^/]+)\/entitlements\/([^/]+)$/);
if (req.method === "PATCH" && organizationEntitlementMatch) {
const organizationId = decodeURIComponent(organizationEntitlementMatch[1]);
const entitlementKey = decodeURIComponent(organizationEntitlementMatch[2]);
const context = contextWith({ organizationId, workspaceId: "", projectId: "" }, req);
return send(res, 200, updateOrganizationEntitlement(context, organizationId, entitlementKey, await readBody(req)));
}
const organizationCommercialApprovalsMatch = pathname.match(/^\/api\/organizations\/([^/]+)\/commercial-approvals$/);
if (req.method === "GET" && organizationCommercialApprovalsMatch) {
const organizationId = decodeURIComponent(organizationCommercialApprovalsMatch[1]);
const context = contextWith({ organizationId, workspaceId: "", projectId: "" }, req);
return send(res, 200, listCommercialApprovalRequests(context, organizationId, {
status: url.searchParams.get("status") || "",
type: url.searchParams.get("type") || "",
mine: url.searchParams.get("mine") === "1",
limit: url.searchParams.get("limit") || 80
}));
}
if (req.method === "POST" && organizationCommercialApprovalsMatch) {
const organizationId = decodeURIComponent(organizationCommercialApprovalsMatch[1]);
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, createCommercialApprovalRequest(context, organizationId, await readBody(req)));
}
const organizationCommercialApprovalDecisionMatch = pathname.match(/^\/api\/organizations\/([^/]+)\/commercial-approvals\/([^/]+)\/decision$/);
if (req.method === "POST" && organizationCommercialApprovalDecisionMatch) {
const organizationId = decodeURIComponent(organizationCommercialApprovalDecisionMatch[1]);
const approvalId = decodeURIComponent(organizationCommercialApprovalDecisionMatch[2]);
const context = contextWith({ organizationId, workspaceId: "", projectId: "" }, req);
return send(res, 200, decideCommercialApprovalRequest(context, organizationId, approvalId, await readBody(req)));
}
const organizationCommercialExportMatch = pathname.match(/^\/api\/organizations\/([^/]+)\/commercial\/export$/);
if (req.method === "GET" && organizationCommercialExportMatch) {
const organizationId = decodeURIComponent(organizationCommercialExportMatch[1]);
@@ -2045,7 +2148,7 @@ createServer(async (req, res) => {
if (req.method === "GET" && pathname === "/api/assets") {
const context = resolveContext(req.headers, url.searchParams);
if (!hasPermission(context, "asset:edit") && !hasPermission(context, "voice:edit")) {
if (!hasPermission(context, "asset:edit") && !hasPermission(context, "voice:edit") && !hasPermission(context, "compliance:manage")) {
throw httpError(403, "permission_denied", "当前角色没有资产库访问权限");
}
return send(res, 200, { assets: scopedAssets(context) });
@@ -2065,7 +2168,7 @@ createServer(async (req, res) => {
const assetMatch = pathname.match(/^\/api\/assets\/([^/]+)$/);
if (req.method === "GET" && assetMatch) {
const context = resolveContext(req.headers, url.searchParams);
if (!hasPermission(context, "asset:edit") && !hasPermission(context, "voice:edit")) {
if (!hasPermission(context, "asset:edit") && !hasPermission(context, "voice:edit") && !hasPermission(context, "compliance:manage")) {
throw httpError(403, "permission_denied", "当前角色没有资产库访问权限");
}
const asset = getAsset(context, decodeURIComponent(assetMatch[1]));
@@ -2098,6 +2201,18 @@ createServer(async (req, res) => {
return send(res, 200, { verification: await verifyAssetContent(context, decodeURIComponent(assetVerifyMatch[1])) });
}
const assetGovernanceMatch = pathname.match(/^\/api\/assets\/([^/]+)\/governance$/);
if (req.method === "GET" && assetGovernanceMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, listAssetGovernanceReviews(context, decodeURIComponent(assetGovernanceMatch[1])));
}
const assetGovernanceScanMatch = pathname.match(/^\/api\/assets\/([^/]+)\/governance\/scan$/);
if (req.method === "POST" && assetGovernanceScanMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, scanAssetGovernance(context, decodeURIComponent(assetGovernanceScanMatch[1]), await readBody(req)));
}
const assetVersionRestoreMatch = pathname.match(/^\/api\/assets\/([^/]+)\/versions\/([^/]+)\/restore$/);
if (req.method === "POST" && assetVersionRestoreMatch) {
const context = resolveContext(req.headers, url.searchParams);
@@ -2144,6 +2259,20 @@ createServer(async (req, res) => {
return send(res, 200, { models: scopedModels(context), runners: runtimeRunners() });
}
if (req.method === "GET" && pathname === "/api/platform/model-catalog") {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:read");
else requirePermission(context, "model:manage");
return send(res, 200, { catalog: scopedModelCatalog(context), connectors: scopedModels(context) });
}
if (req.method === "GET" && pathname === "/api/platform/model-routes") {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:read");
else requirePermission(context, "model:manage");
return send(res, 200, { routes: scopedModelRoutes(context), catalog: scopedModelCatalog(context) });
}
const modelConnectorMatch = pathname.match(/^\/api\/platform\/models\/([^/]+)$/);
if (req.method === "PATCH" && modelConnectorMatch) {
const context = resolveContext(req.headers, url.searchParams);
@@ -2396,6 +2525,23 @@ createServer(async (req, res) => {
return send(res, 200, { featureFlags: featureFlags() });
}
if (req.method === "GET" && pathname === "/api/system/plan-templates") {
const context = resolveContext(req.headers, url.searchParams);
requirePermission(context, "system:settings:view");
return send(res, 200, { planTemplates: subscriptionPlanTemplates({ includeArchived: true }) });
}
if (req.method === "POST" && pathname === "/api/system/plan-templates") {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, createSubscriptionPlanTemplate(context, await readBody(req)));
}
const planTemplateMatch = pathname.match(/^\/api\/system\/plan-templates\/([^/]+)$/);
if (req.method === "PATCH" && planTemplateMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, updateSubscriptionPlanTemplate(context, decodeURIComponent(planTemplateMatch[1]), await readBody(req)));
}
if (req.method === "POST" && pathname === "/api/system/feature-flags") {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, { featureFlags: updateFeatureFlag(context, await readBody(req)) });
@@ -2457,6 +2603,154 @@ createServer(async (req, res) => {
return send(res, 201, { model, models: scopedModels(context) });
}
if (req.method === "POST" && pathname === "/api/platform/model-catalog") {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:write");
const entry = createModelCatalogEntry(context, await readBody(req));
return send(res, 201, { entry, catalog: scopedModelCatalog(context) });
}
const modelCatalogMatch = pathname.match(/^\/api\/platform\/model-catalog\/([^/]+)$/);
if (req.method === "PATCH" && modelCatalogMatch) {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:write");
const entry = updateModelCatalogEntry(context, decodeURIComponent(modelCatalogMatch[1]), await readBody(req));
return send(res, 200, { entry, catalog: scopedModelCatalog(context) });
}
if (req.method === "POST" && pathname === "/api/platform/model-routes") {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:write");
const route = createModelRoute(context, await readBody(req));
return send(res, 201, { route, routes: scopedModelRoutes(context) });
}
if (req.method === "POST" && pathname === "/api/platform/model-routes/resolve") {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:read");
return send(res, 200, { resolution: previewModelRouteResolution(context, await readBody(req)) });
}
if (req.method === "GET" && pathname === "/api/platform/model-route-approvals") {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:read");
return send(res, 200, listModelRouteApprovalRequests(context, {
status: url.searchParams.get("status") || "",
mine: url.searchParams.get("mine") === "1",
limit: url.searchParams.get("limit") || 80
}));
}
if (req.method === "POST" && pathname === "/api/platform/model-route-approvals") {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:write");
return send(res, 201, requestModelRouteApproval(context, await readBody(req)));
}
const modelRouteApprovalDecisionMatch = pathname.match(/^\/api\/platform\/model-route-approvals\/([^/]+)\/decision$/);
if (req.method === "POST" && modelRouteApprovalDecisionMatch) {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:write");
return send(res, 200, decideModelRouteApproval(context, decodeURIComponent(modelRouteApprovalDecisionMatch[1]), await readBody(req)));
}
const modelRouteMatch = pathname.match(/^\/api\/platform\/model-routes\/([^/]+)$/);
if (req.method === "PATCH" && modelRouteMatch) {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "models:write");
const route = updateModelRoute(context, decodeURIComponent(modelRouteMatch[1]), await readBody(req));
return send(res, 200, { route, routes: scopedModelRoutes(context) });
}
if (req.method === "GET" && pathname === "/api/knowledge/library") {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, listKnowledgeDocuments(context));
}
if (req.method === "POST" && pathname === "/api/knowledge/library/import") {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, importKnowledgeDocument(context, await readBody(req)));
}
if (req.method === "GET" && pathname === "/api/knowledge/search") {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, searchKnowledge(context, {
query: url.searchParams.get("q") || url.searchParams.get("query") || "",
sourceType: url.searchParams.get("sourceType") || url.searchParams.get("source_type") || "all",
scopeMode: url.searchParams.get("scopeMode") || url.searchParams.get("scope_mode") || "all",
limit: url.searchParams.get("limit") || 24
}));
}
if (req.method === "GET" && pathname === "/api/knowledge/context-packs") {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, listKnowledgeContextPacks(context));
}
if (req.method === "POST" && pathname === "/api/knowledge/context-packs") {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, createKnowledgeContextPack(context, await readBody(req)));
}
const knowledgePackMatch = pathname.match(/^\/api\/knowledge\/context-packs\/([^/]+)$/);
if (req.method === "GET" && knowledgePackMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, getKnowledgeContextPack(context, decodeURIComponent(knowledgePackMatch[1])));
}
const knowledgePackMaterializeMatch = pathname.match(/^\/api\/knowledge\/context-packs\/([^/]+)\/materialize$/);
if (req.method === "POST" && knowledgePackMaterializeMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, materializeKnowledgeContextPack(context, decodeURIComponent(knowledgePackMaterializeMatch[1]), await readBody(req)));
}
const knowledgeDocumentMatch = pathname.match(/^\/api\/knowledge\/library\/([^/]+)$/);
if (req.method === "GET" && knowledgeDocumentMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, getKnowledgeDocument(context, decodeURIComponent(knowledgeDocumentMatch[1])));
}
if (req.method === "PATCH" && knowledgeDocumentMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, updateKnowledgeDocument(context, decodeURIComponent(knowledgeDocumentMatch[1]), await readBody(req)));
}
const knowledgeReviewMatch = pathname.match(/^\/api\/knowledge\/library\/([^/]+)\/review$/);
if (req.method === "POST" && knowledgeReviewMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, reviewKnowledgeDocument(context, decodeURIComponent(knowledgeReviewMatch[1]), await readBody(req)));
}
const knowledgeArchiveMatch = pathname.match(/^\/api\/knowledge\/library\/([^/]+)\/archive$/);
if (req.method === "POST" && knowledgeArchiveMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, archiveKnowledgeDocument(context, decodeURIComponent(knowledgeArchiveMatch[1])));
}
const knowledgeRestoreMatch = pathname.match(/^\/api\/knowledge\/library\/([^/]+)\/restore$/);
if (req.method === "POST" && knowledgeRestoreMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, restoreKnowledgeDocument(context, decodeURIComponent(knowledgeRestoreMatch[1])));
}
const knowledgeVersionsMatch = pathname.match(/^\/api\/knowledge\/library\/([^/]+)\/versions$/);
if (req.method === "GET" && knowledgeVersionsMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, listKnowledgeDocumentVersions(context, decodeURIComponent(knowledgeVersionsMatch[1])));
}
const knowledgeVersionRestoreMatch = pathname.match(/^\/api\/knowledge\/library\/([^/]+)\/versions\/([^/]+)\/restore$/);
if (req.method === "POST" && knowledgeVersionRestoreMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, restoreKnowledgeDocumentVersion(context, decodeURIComponent(knowledgeVersionRestoreMatch[1]), decodeURIComponent(knowledgeVersionRestoreMatch[2])));
}
const knowledgeMaterializeMatch = pathname.match(/^\/api\/knowledge\/library\/([^/]+)\/materialize$/);
if (req.method === "POST" && knowledgeMaterializeMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, materializeKnowledgeDocument(context, decodeURIComponent(knowledgeMaterializeMatch[1]), await readBody(req)));
}
if (req.method === "GET" && pathname === "/api/qa") {
const context = resolveContext(req.headers, url.searchParams);
requirePermission(context, "qa:review");
@@ -2581,6 +2875,16 @@ createServer(async (req, res) => {
return send(res, 201, createDelivery(context, await readBody(req)));
}
const deliveryClearanceMatch = pathname.match(/^\/api\/production\/deliveries\/([^/]+)\/clearance$/);
if (req.method === "GET" && deliveryClearanceMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 200, getDeliveryClearance(context, decodeURIComponent(deliveryClearanceMatch[1])));
}
if (req.method === "POST" && deliveryClearanceMatch) {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, runDeliveryClearance(context, decodeURIComponent(deliveryClearanceMatch[1]), await readBody(req)));
}
const deliveryReleasesMatch = pathname.match(/^\/api\/production\/deliveries\/([^/]+)\/releases$/);
if (req.method === "GET" && deliveryReleasesMatch) {
const context = resolveContext(req.headers, url.searchParams);
@@ -2659,6 +2963,12 @@ createServer(async (req, res) => {
return send(res, 201, await createGenerationJob(context, await readBody(req)));
}
if (req.method === "POST" && pathname === "/api/jobs/preview") {
const context = resolveContext(req.headers, url.searchParams);
if (context.apiClient) requireApiScope(context, "jobs:read");
return send(res, 200, previewGenerationJob(context, await readBody(req)));
}
const jobDetailMatch = pathname.match(/^\/api\/jobs\/([^/]+)$/);
if (req.method === "GET" && jobDetailMatch) {
const context = resolveContext(req.headers, url.searchParams);