feat: add workflow orchestration and media governance

This commit is contained in:
xz
2026-08-24 22:36:43 +08:00
parent 9811469a64
commit aa0e0be27a
16 changed files with 775 additions and 13 deletions
+64 -2
View File
@@ -130,16 +130,17 @@ import {
probeModelConnector,
updateModelConnector
} from "./execution.mjs";
import { requireStorageQuota, storageSummary } from "./storage.mjs";
import { reclaimStorage, requireStorageQuota, storageCleanupPreview, storageSummary } from "./storage.mjs";
import { backupSummary, createDatabaseBackup } from "./backup.mjs";
import { systemReadiness } from "./readiness.mjs";
import { dispatchNotificationEvent, listUserNotificationPreferences, listUserNotifications, markAllUserNotificationsRead, markUserNotificationRead, notificationDeliveries, updateUserNotificationPreference } from "./notifications.mjs";
import { composeProject, listCompositions } from "./composition.mjs";
import { listProjectArtifacts } from "./media-artifacts.mjs";
import { listProjectArtifacts, readArtifactContent } from "./media-artifacts.mjs";
import { runWorkerOnce, startLocalWorker, workerStatus } from "./worker.mjs";
import { createSsoTicket, handleOidcCallback, redeemSsoTicket, startOidcLogin } from "./oidc.mjs";
import { handleSamlCallback, isSamlProvider, samlServiceProviderMetadata, startSamlLogin } from "./saml.mjs";
import { listWorkItems } from "./work-items.mjs";
import { createWorkflowTemplate, instantiateWorkflow, listWorkflowRuns, listWorkflowTemplates, updateWorkflowTemplate } from "./workflows.mjs";
import { addTaskLink, createProjectTask, createTaskComment, getProjectTask, listProjectActivity, listProjectTasks, listTaskComments, removeTaskLink, updateProjectTask } from "./tasks.mjs";
import { searchPlatform } from "./search.mjs";
import { consumeRateLimit, rateLimitHeaders, rateLimitIdentity } from "./rate-limit.mjs";
@@ -1909,6 +1910,20 @@ createServer(async (req, res) => {
return send(res, 200, { storage: await storageSummary(context) });
}
if (req.method === "GET" && pathname === "/api/system/storage/cleanup-preview") {
const context = resolveContext(req.headers, url.searchParams);
requirePermission(context, "system:settings:view");
return send(res, 200, { cleanup: await storageCleanupPreview(context, { olderThanDays: url.searchParams.get("olderThanDays") || "" }) });
}
if (req.method === "POST" && pathname === "/api/system/storage/reclaim") {
const context = resolveContext(req.headers, url.searchParams);
requirePermission(context, "system:settings:edit");
const result = await reclaimStorage(context, await readBody(req));
addAudit({ context, action: "system.storage.reclaimed", targetType: "storage_cleanup", targetId: `cleanup-${Date.now()}`, metadata: { deletedCount: result.deleted.length, deletedBytes: result.deletedBytes, retentionDays: result.policy.retentionDays } });
return send(res, 200, { cleanup: result, storage: await storageSummary(context) });
}
if (req.method === "GET" && pathname === "/api/billing") {
const context = resolveContext(req.headers, url.searchParams);
requirePermission(context, "usage:view");
@@ -2482,6 +2497,53 @@ createServer(async (req, res) => {
return send(res, 200, { artifacts: listProjectArtifacts(context, { shotId: url.searchParams.get("shotId") || "", jobId: url.searchParams.get("jobId") || "", limit: url.searchParams.get("limit") || 200 }) });
}
const mediaArtifactContentMatch = pathname.match(/^\/api\/production\/media-artifacts\/([^/]+)\/content$/);
if (req.method === "GET" && mediaArtifactContentMatch) {
const context = resolveContext(req.headers, url.searchParams);
if (!hasPermission(context, "delivery:view") && !hasPermission(context, "qa:review")) throw httpError(403, "permission_denied", "当前角色没有媒体证据访问权限");
try {
const payload = await readArtifactContent(context, decodeURIComponent(mediaArtifactContentMatch[1]), url.searchParams.get("frame") || "");
return sendBinary(res, 200, payload.content, payload.contentType, payload.fileName, { etag: `"${payload.contentSha256}"` });
} catch (error) {
if (error.message === "media_artifact_not_found") throw httpError(404, "media_artifact_not_found", "媒体证据不存在或不属于当前项目");
if (error.message === "media_artifact_path_invalid") throw httpError(422, "media_artifact_path_invalid", "媒体证据路径不是 storage/ 下的安全路径");
if (error.code === "ENOENT") throw httpError(404, "media_artifact_content_missing", "媒体文件尚未写入本地存储", { path: error.path || "" });
throw error;
}
}
if (req.method === "GET" && pathname === "/api/workflows/templates") {
const context = resolveContext(req.headers, url.searchParams);
if (!hasPermission(context, "workflow:manage") && !hasPermission(context, "job:create") && !hasPermission(context, "script:read") && !hasPermission(context, "project:create")) throw httpError(403, "permission_denied", "当前角色没有流程模板访问权限");
return send(res, 200, { templates: listWorkflowTemplates(context, { includeArchived: url.searchParams.get("includeArchived") === "1" }), runs: context.project ? listWorkflowRuns(context, { limit: 20 }) : [] });
}
if (req.method === "POST" && pathname === "/api/workflows/templates") {
const context = resolveContext(req.headers, url.searchParams);
requirePermission(context, "workflow:manage");
return send(res, 201, { template: createWorkflowTemplate(context, await readBody(req)), templates: listWorkflowTemplates(context) });
}
const workflowTemplateMatch = pathname.match(/^\/api\/workflows\/templates\/([^/]+)$/);
if (req.method === "PATCH" && workflowTemplateMatch) {
const context = resolveContext(req.headers, url.searchParams);
requirePermission(context, "workflow:manage");
return send(res, 200, { template: updateWorkflowTemplate(context, decodeURIComponent(workflowTemplateMatch[1]), await readBody(req)), templates: listWorkflowTemplates(context) });
}
const workflowInstantiateMatch = pathname.match(/^\/api\/workflows\/templates\/([^/]+)\/instantiate$/);
if (req.method === "POST" && workflowInstantiateMatch) {
const context = resolveContext(req.headers, url.searchParams);
requirePermission(context, "job:create");
return send(res, 201, await instantiateWorkflow(context, decodeURIComponent(workflowInstantiateMatch[1]), await readBody(req)));
}
if (req.method === "GET" && pathname === "/api/workflows/runs") {
const context = resolveContext(req.headers, url.searchParams);
if (!hasPermission(context, "job:create") && !hasPermission(context, "workflow:manage")) throw httpError(403, "permission_denied", "当前角色没有流程运行记录访问权限");
return send(res, 200, { runs: listWorkflowRuns(context, { status: url.searchParams.get("status") || "", limit: url.searchParams.get("limit") || 50 }) });
}
if (req.method === "POST" && pathname === "/api/production/compose") {
const context = resolveContext(req.headers, url.searchParams);
return send(res, 201, await composeProject(context, await readBody(req)));