feat: add style kit production standards
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
import { dbAll, dbGet, dbRun, withTransaction } from "./db.mjs";
|
||||
import { addAudit, hasPermission, httpError, requireEntitlement, requirePermission, requireProjectWritable } from "./tenant.mjs";
|
||||
|
||||
const now = () => new Date().toISOString();
|
||||
const makeId = (prefix) => `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
|
||||
const DEFAULT_FORBIDDEN_TERMS = [
|
||||
"split-screen",
|
||||
"comic panel",
|
||||
"collage",
|
||||
"contact sheet",
|
||||
"storyboard",
|
||||
"多格",
|
||||
"拼图",
|
||||
"分屏",
|
||||
"故事板拼图"
|
||||
];
|
||||
|
||||
const DEFAULT_STYLE_KIT = {
|
||||
id: "",
|
||||
name: "平台默认单画面生产标准",
|
||||
scopeMode: "default",
|
||||
status: "active",
|
||||
visualProfile: {
|
||||
styleFamily: "国产漫画 / 国漫 2D 动画",
|
||||
cameraLanguage: "one continuous scene, one complete frame",
|
||||
continuityLocks: ["character", "costume", "location", "prop", "weather", "camera"]
|
||||
},
|
||||
subtitlePreset: {
|
||||
language: "zh-CN",
|
||||
position: "bottom-safe-area",
|
||||
maxCharsPerLine: 18
|
||||
},
|
||||
voicePolicy: {
|
||||
finalVoiceMode: "fixed-reference-required",
|
||||
forbidRandomNativeVoice: true,
|
||||
fallbackMouthPlan: ["侧脸", "背影", "低头", "远景", "反应镜头", "环境插入镜头"]
|
||||
},
|
||||
deliverySpec: {
|
||||
aspectRatio: "9:16",
|
||||
resolution: "1080x1920",
|
||||
fps: 24,
|
||||
clipDurationSec: [5, 10],
|
||||
exportFormat: "mp4"
|
||||
},
|
||||
promptRules: {
|
||||
positivePrefix: "ONE SINGLE COMPLETE 9:16 CHINESE ANIMATION FRAME, one continuous scene, one complete image.",
|
||||
negativeTerms: DEFAULT_FORBIDDEN_TERMS,
|
||||
renderNotes: ["batch size must be 1", "do not generate storyboards or panels"]
|
||||
},
|
||||
singleFramePolicy: {
|
||||
enabled: true,
|
||||
imageOutputCount: 1,
|
||||
batchSize: 1,
|
||||
forbiddenTerms: DEFAULT_FORBIDDEN_TERMS,
|
||||
requiredPhrase: "one single complete frame"
|
||||
},
|
||||
namingRules: {
|
||||
shotPattern: "{projectId}/S{season}E{episode}/shot-{shotNumber}-{assetKind}-{version}",
|
||||
deliveryPattern: "{projectName}-{episodeId}-{releaseVersion}"
|
||||
},
|
||||
qaPolicy: {
|
||||
gates: ["single-frame", "continuity", "voice-rights", "subtitle-asr-alignment", "clip-transition"],
|
||||
requireLockedAssets: true
|
||||
},
|
||||
metadata: { source: "platform-default" }
|
||||
};
|
||||
|
||||
function parseJson(value, fallback) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed === undefined || parsed === null ? fallback : parsed;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function jsonObject(value, fallback = {}) {
|
||||
if (value === undefined) return fallback;
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseJson(value, fallback);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback;
|
||||
}
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function uniqueList(values = []) {
|
||||
return [...new Set(values.map((item) => String(item || "").trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
export function styleKitForbiddenTerms(styleKit) {
|
||||
return uniqueList([
|
||||
...DEFAULT_FORBIDDEN_TERMS,
|
||||
...(styleKit?.singleFramePolicy?.forbiddenTerms || []),
|
||||
...(styleKit?.promptRules?.forbiddenTerms || [])
|
||||
]);
|
||||
}
|
||||
|
||||
export function styleKitNegativeTerms(styleKit) {
|
||||
return uniqueList([
|
||||
...DEFAULT_FORBIDDEN_TERMS,
|
||||
...(styleKit?.promptRules?.negativeTerms || []),
|
||||
...(styleKit?.singleFramePolicy?.forbiddenTerms || [])
|
||||
]);
|
||||
}
|
||||
|
||||
function canReadStyleKits(context) {
|
||||
return [
|
||||
"style:read",
|
||||
"style:manage",
|
||||
"script:read",
|
||||
"job:create",
|
||||
"project:manage",
|
||||
"model:manage",
|
||||
"compliance:manage"
|
||||
].some((permission) => hasPermission(context, permission));
|
||||
}
|
||||
|
||||
function requireStyleRead(context) {
|
||||
if (!canReadStyleKits(context)) throw httpError(403, "permission_denied", "当前角色没有风格标准访问权限");
|
||||
}
|
||||
|
||||
function accessibleProjectIds(context) {
|
||||
return new Set((context.projects || []).map((project) => project.id).filter(Boolean));
|
||||
}
|
||||
|
||||
function ensureProjectInContext(context, projectId = "") {
|
||||
const id = String(projectId || context.project?.id || "").trim();
|
||||
if (!id) throw httpError(400, "project_required", "项目级风格标准必须绑定项目");
|
||||
const project = dbGet(
|
||||
`SELECT p.*, w.organization_id
|
||||
FROM projects p
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.id = ? AND p.workspace_id = ? AND w.organization_id = ?`,
|
||||
[id, context.workspace.id, context.organization.id]
|
||||
);
|
||||
if (!project) throw httpError(404, "project_not_found", "项目不存在或不属于当前工作区", { projectId: id });
|
||||
if (!accessibleProjectIds(context).has(project.id) && context.project?.id !== project.id) {
|
||||
throw httpError(403, "project_forbidden", "当前用户没有该项目访问权", { projectId: id });
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
function styleKitRowVisible(context, id) {
|
||||
const projectIds = [...accessibleProjectIds(context)];
|
||||
const projectClause = projectIds.length ? `OR sk.project_id IN (${projectIds.map(() => "?").join(",")})` : "";
|
||||
const row = dbGet(
|
||||
`SELECT sk.*, w.name AS workspace_name, p.name AS project_name, u.display_name AS creator_name
|
||||
FROM style_kits sk
|
||||
LEFT JOIN workspaces w ON w.id = sk.workspace_id
|
||||
LEFT JOIN projects p ON p.id = sk.project_id
|
||||
LEFT JOIN users u ON u.id = sk.created_by
|
||||
WHERE sk.id = ? AND sk.organization_id = ?
|
||||
AND (
|
||||
(sk.workspace_id IS NULL AND sk.project_id IS NULL)
|
||||
OR sk.workspace_id = ?
|
||||
${projectClause}
|
||||
)`,
|
||||
[id, context.organization.id, context.workspace.id, ...projectIds]
|
||||
);
|
||||
if (!row) throw httpError(404, "style_kit_not_found", "风格标准不存在或不属于当前组织/工作区", { styleKitId: id });
|
||||
return row;
|
||||
}
|
||||
|
||||
function styleKitPayload(row, source = "") {
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
organizationId: row.organization_id,
|
||||
workspaceId: row.workspace_id || "",
|
||||
projectId: row.project_id || "",
|
||||
scopeMode: row.scope_mode,
|
||||
name: row.name,
|
||||
description: row.description || "",
|
||||
status: row.status,
|
||||
visualProfile: parseJson(row.visual_profile_json, {}),
|
||||
subtitlePreset: parseJson(row.subtitle_preset_json, {}),
|
||||
voicePolicy: parseJson(row.voice_policy_json, {}),
|
||||
deliverySpec: parseJson(row.delivery_spec_json, {}),
|
||||
promptRules: parseJson(row.prompt_rules_json, {}),
|
||||
singleFramePolicy: parseJson(row.single_frame_policy_json, {}),
|
||||
namingRules: parseJson(row.naming_rules_json, {}),
|
||||
qaPolicy: parseJson(row.qa_policy_json, {}),
|
||||
metadata: parseJson(row.metadata_json, {}),
|
||||
workspaceName: row.workspace_name || "",
|
||||
projectName: row.project_name || "",
|
||||
creatorName: row.creator_name || row.created_by || "",
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
...(source ? { source } : {})
|
||||
};
|
||||
}
|
||||
|
||||
function bindingPayload(row) {
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
organizationId: row.organization_id,
|
||||
workspaceId: row.workspace_id,
|
||||
projectId: row.project_id,
|
||||
styleKitId: row.style_kit_id,
|
||||
enforcement: row.enforcement,
|
||||
notes: row.notes || "",
|
||||
createdBy: row.created_by,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStyleKitBody(context, body = {}, current = null) {
|
||||
const scopeMode = String(body.scopeMode ?? body.scope_mode ?? current?.scope_mode ?? "workspace").trim();
|
||||
if (!["organization", "workspace", "project"].includes(scopeMode)) throw httpError(400, "style_kit_scope_invalid", "风格标准作用域只能是 organization、workspace 或 project");
|
||||
const project = scopeMode === "project" ? ensureProjectInContext(context, body.projectId ?? body.project_id ?? current?.project_id) : null;
|
||||
const name = String(body.name ?? current?.name ?? "").trim().slice(0, 100);
|
||||
if (name.length < 2) throw httpError(400, "style_kit_name_required", "风格标准名称至少需要 2 个字符");
|
||||
const status = String(body.status ?? current?.status ?? "draft").trim();
|
||||
if (!["draft", "active", "paused", "archived"].includes(status)) throw httpError(400, "style_kit_status_invalid", "风格标准状态无效");
|
||||
const currentJson = (column, fallback) => current ? parseJson(current[column], fallback) : fallback;
|
||||
const styleKit = {
|
||||
scopeMode,
|
||||
name,
|
||||
description: String(body.description ?? current?.description ?? "").trim().slice(0, 800),
|
||||
status,
|
||||
workspaceId: scopeMode === "organization" ? null : context.workspace.id,
|
||||
projectId: scopeMode === "project" ? project.id : null,
|
||||
visualProfile: jsonObject(body.visualProfile ?? body.visual_profile, currentJson("visual_profile_json", DEFAULT_STYLE_KIT.visualProfile)),
|
||||
subtitlePreset: jsonObject(body.subtitlePreset ?? body.subtitle_preset, currentJson("subtitle_preset_json", DEFAULT_STYLE_KIT.subtitlePreset)),
|
||||
voicePolicy: jsonObject(body.voicePolicy ?? body.voice_policy, currentJson("voice_policy_json", DEFAULT_STYLE_KIT.voicePolicy)),
|
||||
deliverySpec: jsonObject(body.deliverySpec ?? body.delivery_spec, currentJson("delivery_spec_json", DEFAULT_STYLE_KIT.deliverySpec)),
|
||||
promptRules: jsonObject(body.promptRules ?? body.prompt_rules, currentJson("prompt_rules_json", DEFAULT_STYLE_KIT.promptRules)),
|
||||
singleFramePolicy: jsonObject(body.singleFramePolicy ?? body.single_frame_policy, currentJson("single_frame_policy_json", DEFAULT_STYLE_KIT.singleFramePolicy)),
|
||||
namingRules: jsonObject(body.namingRules ?? body.naming_rules, currentJson("naming_rules_json", DEFAULT_STYLE_KIT.namingRules)),
|
||||
qaPolicy: jsonObject(body.qaPolicy ?? body.qa_policy, currentJson("qa_policy_json", DEFAULT_STYLE_KIT.qaPolicy)),
|
||||
metadata: jsonObject(body.metadata ?? body.metadata_json, currentJson("metadata_json", {}))
|
||||
};
|
||||
styleKit.promptRules.negativeTerms = styleKitNegativeTerms(styleKit);
|
||||
styleKit.singleFramePolicy = {
|
||||
enabled: true,
|
||||
imageOutputCount: 1,
|
||||
batchSize: 1,
|
||||
...styleKit.singleFramePolicy,
|
||||
forbiddenTerms: styleKitForbiddenTerms(styleKit)
|
||||
};
|
||||
styleKit.voicePolicy = {
|
||||
...styleKit.voicePolicy,
|
||||
forbidRandomNativeVoice: styleKit.voicePolicy.forbidRandomNativeVoice !== false
|
||||
};
|
||||
return styleKit;
|
||||
}
|
||||
|
||||
function scopedStyleKitRows(context, { includeArchived = false } = {}) {
|
||||
const projectIds = [...accessibleProjectIds(context)];
|
||||
const projectClause = projectIds.length ? `OR sk.project_id IN (${projectIds.map(() => "?").join(",")})` : "";
|
||||
return dbAll(
|
||||
`SELECT sk.*, w.name AS workspace_name, p.name AS project_name, u.display_name AS creator_name
|
||||
FROM style_kits sk
|
||||
LEFT JOIN workspaces w ON w.id = sk.workspace_id
|
||||
LEFT JOIN projects p ON p.id = sk.project_id
|
||||
LEFT JOIN users u ON u.id = sk.created_by
|
||||
WHERE sk.organization_id = ?
|
||||
AND (? = 1 OR sk.status != 'archived')
|
||||
AND (
|
||||
(sk.workspace_id IS NULL AND sk.project_id IS NULL)
|
||||
OR sk.workspace_id = ?
|
||||
${projectClause}
|
||||
)
|
||||
ORDER BY CASE sk.scope_mode WHEN 'project' THEN 0 WHEN 'workspace' THEN 1 ELSE 2 END, sk.updated_at DESC, sk.name`,
|
||||
[context.organization.id, includeArchived ? 1 : 0, context.workspace.id, ...projectIds]
|
||||
);
|
||||
}
|
||||
|
||||
function currentBinding(context, projectId = "") {
|
||||
const project = ensureProjectInContext(context, projectId || context.project?.id);
|
||||
const row = dbGet(
|
||||
`SELECT b.*
|
||||
FROM project_style_kit_bindings b
|
||||
WHERE b.organization_id = ? AND b.workspace_id = ? AND b.project_id = ?`,
|
||||
[context.organization.id, context.workspace.id, project.id]
|
||||
);
|
||||
return bindingPayload(row);
|
||||
}
|
||||
|
||||
function activeStyleKitCandidate(context, projectId = "") {
|
||||
const project = ensureProjectInContext(context, projectId || context.project?.id);
|
||||
const binding = dbGet(
|
||||
`SELECT sk.*, w.name AS workspace_name, p.name AS project_name, u.display_name AS creator_name,
|
||||
b.id AS binding_id, b.organization_id AS binding_organization_id,
|
||||
b.workspace_id AS binding_workspace_id, b.project_id AS binding_project_id,
|
||||
b.style_kit_id AS binding_style_kit_id, b.enforcement AS binding_enforcement,
|
||||
b.notes AS binding_notes, b.created_by AS binding_created_by,
|
||||
b.created_at AS binding_created_at, b.updated_at AS binding_updated_at
|
||||
FROM project_style_kit_bindings b
|
||||
JOIN style_kits sk ON sk.id = b.style_kit_id
|
||||
LEFT JOIN workspaces w ON w.id = sk.workspace_id
|
||||
LEFT JOIN projects p ON p.id = sk.project_id
|
||||
LEFT JOIN users u ON u.id = sk.created_by
|
||||
WHERE b.organization_id = ? AND b.workspace_id = ? AND b.project_id = ?
|
||||
AND sk.organization_id = b.organization_id AND sk.status = 'active'
|
||||
AND (
|
||||
(sk.scope_mode = 'organization' AND sk.workspace_id IS NULL AND sk.project_id IS NULL)
|
||||
OR (sk.scope_mode = 'workspace' AND sk.workspace_id = b.workspace_id AND sk.project_id IS NULL)
|
||||
OR (sk.scope_mode = 'project' AND sk.project_id = b.project_id)
|
||||
)
|
||||
LIMIT 1`,
|
||||
[context.organization.id, context.workspace.id, project.id]
|
||||
);
|
||||
if (binding) {
|
||||
return {
|
||||
styleKit: styleKitPayload(binding, "project-binding"),
|
||||
binding: {
|
||||
id: binding.binding_id,
|
||||
organizationId: binding.binding_organization_id,
|
||||
workspaceId: binding.binding_workspace_id,
|
||||
projectId: binding.binding_project_id,
|
||||
styleKitId: binding.binding_style_kit_id,
|
||||
enforcement: binding.binding_enforcement,
|
||||
notes: binding.binding_notes || "",
|
||||
createdBy: binding.binding_created_by,
|
||||
createdAt: binding.binding_created_at,
|
||||
updatedAt: binding.binding_updated_at
|
||||
},
|
||||
project
|
||||
};
|
||||
}
|
||||
const fallback = dbGet(
|
||||
`SELECT sk.*, w.name AS workspace_name, p.name AS project_name, u.display_name AS creator_name
|
||||
FROM style_kits sk
|
||||
LEFT JOIN workspaces w ON w.id = sk.workspace_id
|
||||
LEFT JOIN projects p ON p.id = sk.project_id
|
||||
LEFT JOIN users u ON u.id = sk.created_by
|
||||
WHERE sk.organization_id = ? AND sk.status = 'active'
|
||||
AND (
|
||||
sk.project_id = ?
|
||||
OR (sk.project_id IS NULL AND sk.workspace_id = ?)
|
||||
OR (sk.project_id IS NULL AND sk.workspace_id IS NULL)
|
||||
)
|
||||
ORDER BY CASE
|
||||
WHEN sk.project_id = ? THEN 0
|
||||
WHEN sk.workspace_id = ? THEN 1
|
||||
ELSE 2
|
||||
END, sk.updated_at DESC
|
||||
LIMIT 1`,
|
||||
[context.organization.id, project.id, context.workspace.id, project.id, context.workspace.id]
|
||||
);
|
||||
return { styleKit: fallback ? styleKitPayload(fallback, fallback.project_id ? "project-active" : fallback.workspace_id ? "workspace-default" : "organization-default") : null, binding: null, project };
|
||||
}
|
||||
|
||||
export function effectiveStyleKitForProject(context, projectId = "") {
|
||||
if (!context?.organization?.id || !context?.workspace?.id) {
|
||||
return { styleKit: { ...DEFAULT_STYLE_KIT, source: "default-contract" }, binding: null, project: null };
|
||||
}
|
||||
const effective = activeStyleKitCandidate(context, projectId);
|
||||
return {
|
||||
...effective,
|
||||
styleKit: effective.styleKit || { ...DEFAULT_STYLE_KIT, source: "default-contract" }
|
||||
};
|
||||
}
|
||||
|
||||
export function listStyleKits(context, options = {}) {
|
||||
requireStyleRead(context);
|
||||
const effective = context.project ? effectiveStyleKitForProject(context) : { styleKit: { ...DEFAULT_STYLE_KIT, source: "default-contract" }, binding: null, project: null };
|
||||
return {
|
||||
kits: scopedStyleKitRows(context, { includeArchived: options.includeArchived }).map(styleKitPayload),
|
||||
effective,
|
||||
projects: (context.projects || []).map((project) => ({ id: project.id, name: project.name, status: project.status })),
|
||||
summary: {
|
||||
total: Number(dbGet("SELECT COUNT(*) AS count FROM style_kits WHERE organization_id = ? AND status != 'archived'", [context.organization.id])?.count || 0),
|
||||
active: Number(dbGet("SELECT COUNT(*) AS count FROM style_kits WHERE organization_id = ? AND status = 'active'", [context.organization.id])?.count || 0),
|
||||
currentWorkspace: Number(dbGet("SELECT COUNT(*) AS count FROM style_kits WHERE organization_id = ? AND workspace_id = ? AND status != 'archived'", [context.organization.id, context.workspace.id])?.count || 0),
|
||||
boundProjects: Number(dbGet("SELECT COUNT(*) AS count FROM project_style_kit_bindings WHERE organization_id = ? AND workspace_id = ?", [context.organization.id, context.workspace.id])?.count || 0)
|
||||
},
|
||||
defaults: DEFAULT_STYLE_KIT
|
||||
};
|
||||
}
|
||||
|
||||
export function getStyleKit(context, styleKitId) {
|
||||
requireStyleRead(context);
|
||||
return { styleKit: styleKitPayload(styleKitRowVisible(context, String(styleKitId || "").trim())) };
|
||||
}
|
||||
|
||||
export function createStyleKit(context, body = {}) {
|
||||
requirePermission(context, "style:manage");
|
||||
requireEntitlement(context, "limit.style_kits", 1);
|
||||
const kit = normalizeStyleKitBody(context, body);
|
||||
const id = String(body.id || makeId("stylekit")).trim();
|
||||
const timestamp = now();
|
||||
dbRun(
|
||||
`INSERT INTO style_kits(
|
||||
id, organization_id, workspace_id, project_id, scope_mode, name, description, status,
|
||||
visual_profile_json, subtitle_preset_json, voice_policy_json, delivery_spec_json,
|
||||
prompt_rules_json, single_frame_policy_json, naming_rules_json, qa_policy_json,
|
||||
metadata_json, created_by, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
id,
|
||||
context.organization.id,
|
||||
kit.workspaceId,
|
||||
kit.projectId,
|
||||
kit.scopeMode,
|
||||
kit.name,
|
||||
kit.description,
|
||||
kit.status,
|
||||
JSON.stringify(kit.visualProfile),
|
||||
JSON.stringify(kit.subtitlePreset),
|
||||
JSON.stringify(kit.voicePolicy),
|
||||
JSON.stringify(kit.deliverySpec),
|
||||
JSON.stringify(kit.promptRules),
|
||||
JSON.stringify(kit.singleFramePolicy),
|
||||
JSON.stringify(kit.namingRules),
|
||||
JSON.stringify(kit.qaPolicy),
|
||||
JSON.stringify(kit.metadata),
|
||||
context.user.id,
|
||||
timestamp,
|
||||
timestamp
|
||||
]
|
||||
);
|
||||
addAudit({ context, action: "style_kit.created", targetType: "style_kit", targetId: id, metadata: { name: kit.name, scopeMode: kit.scopeMode, status: kit.status } });
|
||||
return { styleKit: styleKitPayload(styleKitRowVisible(context, id)), ...listStyleKits(context, { includeArchived: true }) };
|
||||
}
|
||||
|
||||
export function updateStyleKit(context, styleKitId, body = {}) {
|
||||
requirePermission(context, "style:manage");
|
||||
const id = String(styleKitId || "").trim();
|
||||
const current = styleKitRowVisible(context, id);
|
||||
const kit = normalizeStyleKitBody(context, body, current);
|
||||
const timestamp = now();
|
||||
dbRun(
|
||||
`UPDATE style_kits
|
||||
SET workspace_id = ?, project_id = ?, scope_mode = ?, name = ?, description = ?, status = ?,
|
||||
visual_profile_json = ?, subtitle_preset_json = ?, voice_policy_json = ?, delivery_spec_json = ?,
|
||||
prompt_rules_json = ?, single_frame_policy_json = ?, naming_rules_json = ?, qa_policy_json = ?,
|
||||
metadata_json = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
kit.workspaceId,
|
||||
kit.projectId,
|
||||
kit.scopeMode,
|
||||
kit.name,
|
||||
kit.description,
|
||||
kit.status,
|
||||
JSON.stringify(kit.visualProfile),
|
||||
JSON.stringify(kit.subtitlePreset),
|
||||
JSON.stringify(kit.voicePolicy),
|
||||
JSON.stringify(kit.deliverySpec),
|
||||
JSON.stringify(kit.promptRules),
|
||||
JSON.stringify(kit.singleFramePolicy),
|
||||
JSON.stringify(kit.namingRules),
|
||||
JSON.stringify(kit.qaPolicy),
|
||||
JSON.stringify(kit.metadata),
|
||||
timestamp,
|
||||
id
|
||||
]
|
||||
);
|
||||
addAudit({ context, action: "style_kit.updated", targetType: "style_kit", targetId: id, metadata: { name: kit.name, scopeMode: kit.scopeMode, status: kit.status } });
|
||||
return { styleKit: styleKitPayload(styleKitRowVisible(context, id)), ...listStyleKits(context, { includeArchived: true }) };
|
||||
}
|
||||
|
||||
export function bindProjectStyleKit(context, projectId, body = {}) {
|
||||
requirePermission(context, "style:manage");
|
||||
const project = ensureProjectInContext(context, projectId);
|
||||
requireProjectWritable({ ...context, project });
|
||||
const styleKitId = String(body.styleKitId || body.style_kit_id || "").trim();
|
||||
const timestamp = now();
|
||||
if (!styleKitId) {
|
||||
dbRun("DELETE FROM project_style_kit_bindings WHERE organization_id = ? AND workspace_id = ? AND project_id = ?", [context.organization.id, context.workspace.id, project.id]);
|
||||
addAudit({ context: { ...context, project }, action: "style_kit.unbound", targetType: "project", targetId: project.id, metadata: { projectId: project.id } });
|
||||
return listStyleKits({ ...context, project }, { includeArchived: true });
|
||||
}
|
||||
const kit = styleKitPayload(styleKitRowVisible(context, styleKitId));
|
||||
if (kit.status !== "active") throw httpError(409, "style_kit_not_active", "只有 active 状态的风格标准可以绑定到项目", { styleKitId, status: kit.status });
|
||||
if (kit.scopeMode === "project" && kit.projectId !== project.id) throw httpError(403, "style_kit_project_mismatch", "项目级风格标准不能绑定到其他项目", { styleKitId, kitProjectId: kit.projectId, projectId: project.id });
|
||||
if (kit.scopeMode === "workspace" && kit.workspaceId !== context.workspace.id) throw httpError(403, "style_kit_workspace_mismatch", "工作区级风格标准不能跨工作区绑定", { styleKitId, workspaceId: kit.workspaceId });
|
||||
const enforcement = String(body.enforcement || "locked").trim();
|
||||
if (!["advisory", "locked", "strict"].includes(enforcement)) throw httpError(400, "style_kit_enforcement_invalid", "项目风格绑定强度无效");
|
||||
const notes = String(body.notes || "").trim().slice(0, 500);
|
||||
withTransaction(() => {
|
||||
dbRun(
|
||||
`INSERT INTO project_style_kit_bindings(id, organization_id, workspace_id, project_id, style_kit_id, enforcement, notes, created_by, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(project_id) DO UPDATE SET style_kit_id = excluded.style_kit_id, enforcement = excluded.enforcement, notes = excluded.notes, created_by = excluded.created_by, updated_at = excluded.updated_at`,
|
||||
[`binding-${project.id}-style-kit`, context.organization.id, context.workspace.id, project.id, styleKitId, enforcement, notes, context.user.id, timestamp, timestamp]
|
||||
);
|
||||
});
|
||||
addAudit({ context: { ...context, project }, action: "style_kit.bound", targetType: "project", targetId: project.id, metadata: { projectId: project.id, styleKitId, enforcement } });
|
||||
return listStyleKits({ ...context, project }, { includeArchived: true });
|
||||
}
|
||||
|
||||
export function getProjectStyleKit(context, projectId = "") {
|
||||
requireStyleRead(context);
|
||||
const project = ensureProjectInContext(context, projectId || context.project?.id);
|
||||
return effectiveStyleKitForProject({ ...context, project }, project.id);
|
||||
}
|
||||
Reference in New Issue
Block a user