fix(client): reject empty imported brand rules
This commit is contained in:
parent
b21f34ad98
commit
2156598a2d
@ -28,6 +28,7 @@ function markdownSections(text) {
|
||||
}
|
||||
|
||||
const excludedSection = title => /人物|模特|素材索引|资源索引|主图|图库|头像/i.test(title);
|
||||
const brandRule = /品牌风格|品牌调性|视觉风格|视觉规范|色彩|排版|提示词|验收/;
|
||||
function selectSections(text, include) {
|
||||
const levels = [];
|
||||
const selected = [];
|
||||
@ -37,11 +38,13 @@ function selectSections(text, include) {
|
||||
const excluded = Boolean(parent?.excluded) || excludedSection(section.title);
|
||||
const allowed = !excluded && (include(section.title) || Boolean(parent?.allowed));
|
||||
levels[section.level] = { allowed, excluded };
|
||||
if (allowed) selected.push(section.lines.join('\n').trim());
|
||||
if (allowed) selected.push(section);
|
||||
}
|
||||
return selected.filter(Boolean).join('\n\n');
|
||||
return selected;
|
||||
}
|
||||
|
||||
const sectionText = sections => sections.map(section => section.lines.join('\n').trim()).join('\n\n');
|
||||
|
||||
function brandSection(text, name) {
|
||||
const sections = markdownSections(text);
|
||||
const start = sections.findIndex(section => section.title === name);
|
||||
@ -51,7 +54,7 @@ function brandSection(text, name) {
|
||||
// An exact shop heading can carry a directly labelled tone before any
|
||||
// subheading. Only explicitly identified rule lines are admitted here.
|
||||
const intro = sections[start].lines.slice(1).filter(line =>
|
||||
/品牌风格|品牌调性|视觉风格|视觉规范|色彩|排版|提示词|验收/.test(line) && !excludedSection(line));
|
||||
brandRule.test(line) && !excludedSection(line));
|
||||
if (intro.length) result.push(`#### 品牌视觉规范\n${intro.join('\n')}`);
|
||||
for (const section of sections.slice(start + 1)) {
|
||||
if (section.level <= level) break;
|
||||
@ -80,11 +83,18 @@ export async function importEnterpriseRules(platform, sourceRoot) {
|
||||
for (const shop of platform.shops) {
|
||||
const name = shop.name;
|
||||
if (!markdownSections(styles).some(section => section.title === name)) continue;
|
||||
const sourceFile = name === '顾家' ? 'references/gujia-visual-style.md' : 'references/brand-style-prompts.md';
|
||||
const source = name === '顾家' ? gujia : brandSection(styles, name);
|
||||
const text = selectSections(source, title => /品牌风格|品牌调性|视觉风格|视觉规范|色彩|排版|提示词|验收/.test(title));
|
||||
const sections = selectSections(source, title => brandRule.test(title));
|
||||
if (!sections.some(({ title, lines }) =>
|
||||
(brandRule.test(title) && /[::]\s*\S/.test(title)) || lines.slice(1).some(line => line.trim())
|
||||
)) {
|
||||
throw Object.assign(Error(`品牌「${name}」的规则来源 ${sourceFile} 缺少可导入的规范内容`), { code: 'RULE_BRAND_SOURCE' });
|
||||
}
|
||||
const text = sectionText(sections);
|
||||
const tone = text.match(/品牌(?:风格|调性)[::]([^\n]+)/)?.[1]?.trim() || '';
|
||||
const brandId = `enterprise:${shop.id}`;
|
||||
catalog.brands.push({ id: brandId, name, revision: sha(text), source: `enterprise-knowledge:${name === '顾家' ? 'references/gujia-visual-style.md' : 'references/brand-style-prompts.md'}`,
|
||||
catalog.brands.push({ id: brandId, name, revision: sha(text), source: `enterprise-knowledge:${sourceFile}`,
|
||||
profile: { name, tone, fontFallbacks: [...FONT_FALLBACKS], visualRules: Object.fromEntries(chunks(text).map((part, i) => [`sourcePart${i + 1}`, part])),
|
||||
adaptation: '原文规范离线迁入;字体按候选顺序回退,实际可用性取决于运行环境,不宣称已移植源PS模板字体。' } });
|
||||
catalog.shops.push({ ...shop, brandId, revision: digest(shop), source: `material-platform:categories:${shop.id}`, platform: 'material-library', rules: [`当前素材库店铺为「${shop.name}」,店铺ID为${shop.id}。只使用此店铺的规范;渠道与售后政策未提供,不得自行补造。`] });
|
||||
@ -98,7 +108,7 @@ export async function importEnterpriseRules(platform, sourceRoot) {
|
||||
const guide = await read('template-adapter/' + template.usage_guide);
|
||||
const mapping = JSON.parse(await read('template-adapter/' + template.mapping_asset_path));
|
||||
if (!template.template_id || !template.brand || !template.category || mapping.brand !== template.brand || mapping.category !== template.category || mapping.template_id !== template.template_id) throw Object.assign(Error('模板登记与映射范围不一致,未建立绑定'), { code: 'RULE_MAPPING_IDENTITY' });
|
||||
const relevant = selectSections(guide, title => /屏号.*固定|固定.*元素|详情.*结构|页面.*结构/.test(title));
|
||||
const relevant = sectionText(selectSections(guide, title => /屏号.*固定|固定.*元素|详情.*结构|页面.*结构/.test(title)));
|
||||
if (!Array.isArray(mapping.screens) || !mapping.screens.length || mapping.screens.some(s => !s.screen_id)) throw Object.assign(Error('模板缺少真实屏幕映射,未建立绑定'), { code: 'RULE_MAPPING_IDENTITY' });
|
||||
const screens = mapping.screens.map(s => ({ id: s.screen_id, title: s.title, purpose: s.purpose, role: s.page_role }));
|
||||
const rules = [`当前品类模板:${template.template_name}(${template.template_id},映射版本${template.mapping_version})。`,
|
||||
|
||||
@ -29,21 +29,38 @@ test('enterprise import uses exact real catalog identities and sourced category
|
||||
assert.throws(() => resolveRules({ brandId: 'enterprise:s1', shopId: 's1', categoryId: 'c2' }, c), /尚未配置/);
|
||||
assert.throws(() => resolveRules({ brandId: 'enterprise:s2', shopId: 's2', categoryId: 'c1' }, c), /尚未配置/);
|
||||
const before = JSON.stringify(a);
|
||||
await f.write('references/gujia-visual-style.md', '## 品牌风格:新的温暖风格\n## 人物素材索引\n索引\n## 提示词与验收\n新验收规范\n');
|
||||
await f.write('references/gujia-visual-style.md', '## 品牌调性:新的温暖风格\n## 视觉规范\n新验收规范\n');
|
||||
const next = await importEnterpriseRules(f.platform, f.root);
|
||||
assert.equal(next.brands.find(brand => brand.name === '顾家').profile.tone, '新的温暖风格');
|
||||
assert.notEqual(next.revision, c.revision);
|
||||
assert.notEqual(resolveRules({ brandId: 'enterprise:s1', shopId: 's1', categoryId: 'c1' }, next).version, a.version);
|
||||
assert.equal(JSON.stringify(a), before);
|
||||
assert.ok(c.sourceFiles.every(s => /^[a-f0-9]{64}$/.test(s.sha256)));
|
||||
} finally { await cleanup(f.dir); }
|
||||
});
|
||||
test('missing or escaped knowledge sources fail closed, with no fixture fallback', async () => {
|
||||
test('a source outside the authorized directory is rejected', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
await fs.writeFile(path.join(f.dir, 'outside.json'), '{}');
|
||||
await f.write('template-adapter/references/template-catalog.json', JSON.stringify({ templates: [{ ...f.template, mapping_asset_path: '../../outside.json' }] }));
|
||||
await assert.rejects(importEnterpriseRules(f.platform, f.root), /超出授权目录/);
|
||||
await f.write('references/gujia-visual-style.md', '不再符合规范结构');
|
||||
await assert.rejects(importEnterpriseRules(f.platform, f.root), /来源结构已改变/);
|
||||
} finally { await cleanup(f.dir); }
|
||||
});
|
||||
|
||||
test('a missing source is rejected without substituting default rules', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
await fs.unlink(path.join(f.root, 'references/gujia-visual-style.md'));
|
||||
await assert.rejects(importEnterpriseRules(f.platform, f.root), { code: 'ENOENT' });
|
||||
} finally { await cleanup(f.dir); }
|
||||
});
|
||||
|
||||
test('a registered brand must have rule content in a recognized section', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
for (const text of ['不再符合规范结构', '## 品牌风格\n', '## 人物素材索引\n### 视觉规范\n不属于品牌规则']) {
|
||||
await f.write('references/gujia-visual-style.md', text);
|
||||
await assert.rejects(importEnterpriseRules(f.platform, f.root), { code: 'RULE_BRAND_SOURCE' });
|
||||
}
|
||||
} finally { await cleanup(f.dir); }
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user