# 问题 修复 文件 1 前端构建失败(引号错误) size="small type=" → size="small" type=" PosterHistoryPage.vue 2 migrate_014 ORM vs 缺失列 全部改为原始 SQL,不再引用 ORM 模型 migrate_014.py 3 cleanup 字段名错误 output_path → ppt_path cleanup.py 4 文案生成 case 越权 添加 case.user_id != user_id 校验 poster/service.py 5 存储路径未接通持久化卷 全部改用 get_storage_root()(默认 /app/api/storage/insurance) config.py, ppt/routes.py, poster/service.py, poster/tasks.py 高风险问题修复 # 问题 修复 文件 6 migrate_019 rollback 撤销成功字段 每个 ALTER 后立即 commit,失败只回滚当前语句 migrate_019.py 7 迁移锁 Windows 不兼容 + 句柄未持久化 全局变量保存锁句柄,支持 Windows msvcrt api/insurance/db/__init__.py 8 PDF 校验异常时放行 异常返回 False(文件损坏) security.py 9 健康检查始终返回成功 缺少关键资源时返回 503 + missing 列表 poster/routes.py 10 短密钥掩码泄露原值 ≤4 字符返回 **** ppt_admin_service.py 11 设置无键名白名单 添加 _ALLOWED_SETTING_KEYS 白名单 ppt_admin_service.py 12 容器重启任务永久 stuck 添加 recover_stale_tasks() 启动恢复函数 poster/tasks.py, ppt/parse_worker.py
166 lines
7.8 KiB
JavaScript
166 lines
7.8 KiB
JavaScript
import crypto from 'node:crypto';
|
||
import { readFileSync } from 'node:fs';
|
||
import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs';
|
||
|
||
// Catalog B: stagings rather than styles. A composition organizes attention,
|
||
// sequence, or manipulation on a surface and must survive being dressed in
|
||
// any committed visual identity; it deliberately carries no palette or type
|
||
// half. Surface-scope seeds draw from here (plus catalog A duals); direction
|
||
// seeds pair one composition with a chosen world for the first surface.
|
||
|
||
export const COMPOSITION_GRAMMAR_PREFIXES = [
|
||
'Staging/hierarchy:',
|
||
'Sequence/attention:',
|
||
'Controls/state:',
|
||
'Adaptation:',
|
||
];
|
||
|
||
// Surfaces align with the skill's modes: a persuade staging and an operate
|
||
// staging are different species, and read/experience surfaces get their own.
|
||
export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']);
|
||
|
||
export function compositionContentHash(composition) {
|
||
const payload = [
|
||
composition?.form ?? '',
|
||
composition?.lineage ?? '',
|
||
JSON.stringify(composition?.tags ?? []),
|
||
JSON.stringify(composition?.grammar ?? []),
|
||
composition?.spark ?? '',
|
||
composition?.webLeverage ?? '',
|
||
].join('\n');
|
||
return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12);
|
||
}
|
||
|
||
export function validateCompositionEntry(composition, { existingForms = new Map() } = {}) {
|
||
const errors = [];
|
||
const id = composition?.id || '(unknown)';
|
||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(composition?.id || '')) {
|
||
errors.push(`invalid composition id: ${String(composition?.id)}`);
|
||
}
|
||
const normalized = normalizeConceptForm(composition?.form);
|
||
if (!normalized) {
|
||
errors.push(`composition ${id} needs a form`);
|
||
} else if (existingForms.has(normalized)) {
|
||
errors.push(`duplicate composition form: ${id} and ${existingForms.get(normalized)}`);
|
||
}
|
||
if (typeof composition?.form !== 'string'
|
||
|| composition.form.trim().length < 40
|
||
|| composition.form.trim().length > 360
|
||
|| !composition.form.includes(',')) {
|
||
errors.push(`composition ${id} must name a staging and its structural mechanism after a comma`);
|
||
}
|
||
if (typeof composition?.lineage !== 'string'
|
||
|| composition.lineage.trim().length < 12
|
||
|| composition.lineage.trim().length > 200) {
|
||
errors.push(`composition ${id} needs lineage metadata of 12–200 characters`);
|
||
}
|
||
if (!COMPOSITION_SURFACES.has(composition?.surface)) {
|
||
errors.push(`composition ${id} needs a surface of ${[...COMPOSITION_SURFACES].join(', ')}`);
|
||
}
|
||
if (!Array.isArray(composition?.tags)
|
||
|| composition.tags.length !== 3
|
||
|| composition.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||
errors.push(`composition ${id} must have exactly three structural tags`);
|
||
}
|
||
if (!Array.isArray(composition?.grammar)
|
||
|| composition.grammar.length !== COMPOSITION_GRAMMAR_PREFIXES.length
|
||
|| composition.grammar.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||
errors.push(`composition ${id} needs grammar with exactly four rules of 12–180 characters`);
|
||
} else {
|
||
const unique = new Set(composition.grammar.map(normalizeConceptForm));
|
||
if (unique.size !== COMPOSITION_GRAMMAR_PREFIXES.length) {
|
||
errors.push(`composition ${id} has duplicate grammar rules`);
|
||
}
|
||
if (composition.grammar.some((rule, index) => !rule.startsWith(COMPOSITION_GRAMMAR_PREFIXES[index]))) {
|
||
errors.push(`composition ${id} grammar must use staging, sequence, controls, and adaptation prefixes in order`);
|
||
}
|
||
}
|
||
if (typeof composition?.spark !== 'string'
|
||
|| composition.spark.trim().length < 80
|
||
|| composition.spark.trim().length > 320) {
|
||
errors.push(`composition ${id} needs a vivid spark of 80–320 characters`);
|
||
}
|
||
if (typeof composition?.webLeverage !== 'string'
|
||
|| composition.webLeverage.trim().length < 20
|
||
|| composition.webLeverage.trim().length > 240) {
|
||
errors.push(`composition ${id} needs web leverage of 20–240 characters`);
|
||
}
|
||
return errors;
|
||
}
|
||
|
||
export function readCompositionCatalog(catalogPath, reviewsPath) {
|
||
const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
|
||
const reviewData = JSON.parse(readFileSync(reviewsPath, 'utf8'));
|
||
const reviews = reviewData.reviews || {};
|
||
const familiesById = new Map((catalog.families || []).map(family => [family.id, family]));
|
||
const compositions = (catalog.compositions || []).map(composition => ({
|
||
...composition,
|
||
familyLabel: familiesById.get(composition.familyId)?.label || null,
|
||
status: reviews[composition.id]?.status || 'pending',
|
||
review: reviews[composition.id] || null,
|
||
}));
|
||
return { catalog, reviewData, reviews, compositions };
|
||
}
|
||
|
||
export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } = {}) {
|
||
const errors = [];
|
||
const familyIds = new Set();
|
||
const ids = new Set();
|
||
const forms = new Map();
|
||
|
||
if (!Number.isInteger(catalog?.schemaVersion) || catalog.schemaVersion < 1) {
|
||
errors.push('composition catalog schemaVersion must be a positive integer');
|
||
}
|
||
if (typeof catalog?.qualityBar?.principle !== 'string' || catalog.qualityBar.principle.trim().length < 80) {
|
||
errors.push('composition qualityBar.principle must define the staging bar');
|
||
}
|
||
if (!Array.isArray(catalog?.families) || catalog.families.length < 4) {
|
||
errors.push('composition catalog needs at least four families');
|
||
}
|
||
for (const family of catalog?.families || []) {
|
||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(family.id || '')) errors.push(`invalid composition family id: ${String(family.id)}`);
|
||
if (familyIds.has(family.id)) errors.push(`duplicate composition family id: ${family.id}`);
|
||
familyIds.add(family.id);
|
||
if (typeof family.description !== 'string' || family.description.trim().length < 40) {
|
||
errors.push(`composition family ${family.id || '(unknown)'} needs a description`);
|
||
}
|
||
}
|
||
for (const composition of catalog?.compositions || []) {
|
||
if (ids.has(composition.id)) errors.push(`duplicate composition id: ${composition.id}`);
|
||
ids.add(composition.id);
|
||
if (!familyIds.has(composition.familyId)) {
|
||
errors.push(`composition ${composition.id} must belong to a declared family, got: ${String(composition.familyId)}`);
|
||
}
|
||
errors.push(...validateCompositionEntry(composition, { existingForms: forms }));
|
||
const normalized = normalizeConceptForm(composition.form);
|
||
if (normalized) forms.set(normalized, composition.id);
|
||
}
|
||
if (minimumTotal !== undefined && (catalog?.compositions || []).length < minimumTotal) {
|
||
errors.push(`expected at least ${minimumTotal} compositions, found ${(catalog?.compositions || []).length}`);
|
||
}
|
||
for (const [id, review] of Object.entries(reviewData?.reviews || {})) {
|
||
if (!ids.has(id)) errors.push(`composition review references missing entry: ${id}`);
|
||
if (!CONCEPT_STATUSES.has(review?.status)) errors.push(`invalid composition review status for ${id}`);
|
||
if (typeof review?.formHash !== 'string' || !review.formHash.trim()) {
|
||
errors.push(`composition review ${id} needs a formHash`);
|
||
} else {
|
||
const entry = (catalog?.compositions || []).find(composition => composition.id === id);
|
||
if (entry && review.formHash !== compositionContentHash(entry)) {
|
||
errors.push(`composition review ${id} is stale: content changed since review`);
|
||
}
|
||
}
|
||
if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) {
|
||
errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`);
|
||
}
|
||
}
|
||
return {
|
||
errors,
|
||
stats: {
|
||
families: familyIds.size,
|
||
compositions: (catalog?.compositions || []).length,
|
||
approved: Object.values(reviewData?.reviews || {}).filter(review => review?.status === 'approved').length,
|
||
rejected: Object.values(reviewData?.reviews || {}).filter(review => review?.status === 'rejected').length,
|
||
},
|
||
};
|
||
}
|