/* ===== Shared Brand Memory Store ===== */ (function initBrandMemoryStore() { if (window.BrandMemoryStore) return; const DB_NAME = 'aurelia-longform-agent'; const DB_VERSION = 1; const KV_STORE = 'kv'; const PROFILE_KEY = 'brandProfiles'; const SCHEMA_VERSION = 2; const nowStamp = () => new Date().toLocaleString('zh-CN', { hour12: false }); const listFromValue = (value) => { if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean); if (typeof value !== 'string') return []; return value.split(/[、,,;;\n]/).map(item => item.trim()).filter(Boolean); }; const uniq = (items = []) => Array.from(new Set(items.filter(Boolean))); const mergeMemoryItems = (seedItems = [], currentItems = []) => { const map = new Map(); [...seedItems, ...currentItems].forEach((item, index) => { if (!item) return; const key = item.id || `${item.title || item.label || item.name || 'item'}-${index}`; map.set(key, { ...(map.get(key) || {}), ...item, id: key }); }); return Array.from(map.values()); }; const mergeColorTokens = (seedTokens = [], currentTokens = []) => { const seen = new Set(); return [...seedTokens, ...currentTokens].filter(token => { const key = `${String(token.hex || '').toUpperCase()}|${token.alpha || ''}`; if (seen.has(key)) return false; seen.add(key); return true; }); }; const socialNumber = (value) => Number(value || 0); const socialInteraction = (post = {}) => socialNumber(post.likes) + socialNumber(post.comments) + socialNumber(post.shares) + socialNumber(post.saves); const getSocialPostTime = (post = {}) => { const raw = String(post.published_at || post.publish_time || post.created_at || '').trim(); if (!raw) return 0; const parsed = new Date(raw.replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')); return Number.isNaN(parsed.getTime()) ? 0 : parsed.getTime(); }; const isExternalSocialPost = (post = {}) => { const type = `${post.source_type || ''} ${post.asset_type || ''} ${post.relationship_status || ''}`.toLowerCase(); return post.is_official_post === false || type.includes('earned') || type.includes('ugc') || type.includes('kol') || type.includes('external'); }; const isExternalSocialAccount = (account = {}) => { const type = `${account.source_type || ''} ${account.account_type || ''} ${account.relationship_status || ''} ${account.positioning || ''}`.toLowerCase(); return account.is_official_account === false || type.includes('earned') || type.includes('ugc') || type.includes('kol') || type.includes('external'); }; const normalizeProductNarrative = (narrative = {}, product = {}) => ({ positioning: narrative.positioning || product.positioning || '', coreCapabilities: listFromValue(narrative.coreCapabilities), userScenarios: listFromValue(narrative.userScenarios), proofPoints: listFromValue(narrative.proofPoints), comparisonBoundaries: listFromValue(narrative.comparisonBoundaries), requiredMentions: listFromValue(narrative.requiredMentions || product.requiredClaims), forbiddenClaims: listFromValue(narrative.forbiddenClaims || product.forbiddenClaims), }); const normalizeProductProfile = (product = {}, index = 0) => ({ id: product.id || `product-${Date.now()}-${index}`, name: product.name || `Product ${index + 1}`, aliases: listFromValue(product.aliases), positioning: product.positioning || '', audience: listFromValue(product.audience), tone: listFromValue(product.tone), requiredClaims: listFromValue(product.requiredClaims), forbiddenClaims: listFromValue(product.forbiddenClaims), narrative: normalizeProductNarrative(product.narrative, product), source: product.source || 'brand-product-profile', factStatus: product.factStatus || 'confirmed', provenance: product.provenance || 'documented', sourceEvidenceId: product.sourceEvidenceId || '', sourceRefs: listFromValue(product.sourceRefs), updatedAt: product.updatedAt || product.createdAt || 'seed', }); const normalizeBrandNarrative = (narrative = {}) => ({ positioning: narrative.positioning || '', worldview: narrative.worldview || '', valueProposition: narrative.valueProposition || '', voice: listFromValue(narrative.voice), requiredThemes: listFromValue(narrative.requiredThemes), forbiddenAngles: listFromValue(narrative.forbiddenAngles), proofTypes: listFromValue(narrative.proofTypes), }); const normalizeConstraint = (constraint = {}, index = 0) => ({ id: constraint.id || `guardrail-${Date.now()}-${index}`, label: constraint.label || constraint.name || `护栏 ${index + 1}`, category: constraint.category || 'brand', scope: listFromValue(constraint.scope).length ? listFromValue(constraint.scope) : ['strategy', 'brief', 'content', 'longform', 'monitor', 'reports', 'reply'], severity: constraint.severity || 'required', source: constraint.source || 'brand-memory', factStatus: constraint.factStatus || (constraint.status === 'pending' ? 'pending-confirmation' : 'confirmed'), provenance: constraint.provenance || 'documented', sourceEvidenceId: constraint.sourceEvidenceId || '', sourceRefs: listFromValue(constraint.sourceRefs), description: constraint.description || constraint.rule || '', enabled: constraint.enabled !== false, defaultLocked: Boolean(constraint.defaultLocked), forbiddenTerms: listFromValue(constraint.forbiddenTerms), requiredTerms: listFromValue(constraint.requiredTerms), rules: constraint.rules || {}, updatedAt: constraint.updatedAt || constraint.createdAt || 'seed', }); const inferTraceStatus = (item = {}, fallbackStatus = 'confirmed') => { const raw = item.factStatus || item.traceStatus || item.status || fallbackStatus; if (raw === 'active') return 'confirmed'; if (raw === 'pending') return 'pending-confirmation'; return raw || fallbackStatus; }; const normalizeTraceItem = (item = {}, fallback = {}) => ({ ...item, factStatus: inferTraceStatus(item, fallback.factStatus || 'confirmed'), provenance: item.provenance || fallback.provenance || 'documented', source: item.source || fallback.source || 'brand-memory', sourceEvidenceId: item.sourceEvidenceId || fallback.sourceEvidenceId || '', sourceRefs: listFromValue(item.sourceRefs).length ? listFromValue(item.sourceRefs) : listFromValue(fallback.sourceRefs), conflicts: listFromValue(item.conflicts).length ? listFromValue(item.conflicts) : listFromValue(fallback.conflicts), }); const normalizeOperatingStage = (stage = {}) => { const traced = normalizeTraceItem(stage, { source: stage.source || stage.sourceEvidenceId || 'brand-memory' }); return { ...traced, sourceRefs: listFromValue(stage.sourceRefs), operatingPreferences: Array.isArray(stage.operatingPreferences) ? stage.operatingPreferences.map(item => normalizeTraceItem(item, { factStatus: stage.factStatus || 'confirmed', provenance: stage.provenance || 'documented', source: stage.source || stage.sourceEvidenceId || 'brand-memory', sourceRefs: stage.sourceRefs || [], conflicts: stage.conflicts || [], })) : [], }; }; const normalizeVisualSystem = (visualSystem = {}) => ({ ...visualSystem, colorTokens: Array.isArray(visualSystem.colorTokens) ? visualSystem.colorTokens.map(item => normalizeTraceItem(item, { source: item.source || 'MONTX_GUIDELINES_V1.1.pptx', sourceEvidenceId: 'source-vi-guidelines-v11' })) : [], viGuidelines: Array.isArray(visualSystem.viGuidelines) ? visualSystem.viGuidelines.map(item => normalizeTraceItem(item, { source: item.file || item.source || 'MONTX_GUIDELINES_V1.1.pptx', sourceEvidenceId: 'source-vi-guidelines-v11' })) : [], logoAssets: Array.isArray(visualSystem.logoAssets) ? visualSystem.logoAssets.map(item => normalizeTraceItem(item, { source: item.source || 'brand-logos', sourceEvidenceId: 'source-vi-guidelines-v11' })) : [], logoLockups: Array.isArray(visualSystem.logoLockups) ? visualSystem.logoLockups.map(item => normalizeTraceItem(item, { source: 'MONTX_GUIDELINES_V1.1.pptx', sourceEvidenceId: 'source-vi-guidelines-v11' })) : [], typography: Array.isArray(visualSystem.typography) ? visualSystem.typography.map(item => normalizeTraceItem(item, { source: 'MONTX_GUIDELINES_V1.1.pptx', sourceEvidenceId: 'source-vi-guidelines-v11' })) : [], applicationRules: Array.isArray(visualSystem.applicationRules) ? visualSystem.applicationRules.map(item => normalizeTraceItem(item, { source: 'MONTX_GUIDELINES_V1.1.pptx', sourceEvidenceId: 'source-vi-guidelines-v11' })) : [], }); const MONTX_PRODUCT_PROFILES = [ { id: 'montx-p10', name: 'P10', aliases: ['MONTX P10', 'P10', 'Pickup 01', '未来先锋皮卡', '全域移动地空母舰'], positioning: 'MONTX P10 是概念阶段的未来先锋皮卡,以“全域移动地空母舰”承载地空双栖、全域适应与机能美学叙事。', audience: ['跨界多用途尝鲜型用户', '户外探索者', '科考/摄影/露营/指挥等复合场景用户'], tone: ['开拓', '机能', '先锋', '克制', '概念边界清晰'], requiredClaims: ['标注概念车叙事', '强调全域适应/全域智能/全域美学', '把载人飞行器等表达限定为概念愿景'], forbiddenClaims: ['量产承诺', '完全自动驾驶', '无人驾驶', '绝对安全', '已上市/可交付', '确定配置/价格/交付时间'], narrative: { positioning: 'P10 是征服地表与天空的“全域移动地空母舰”,将皮卡从单一装载工具升维为支持立体出行的未来装备。', coreCapabilities: ['地空双栖概念', '四轮分布式驱动电机', '智能底盘与底盘扭矩矢量控制', '车顶/车斗模块化', '战机灰机能涂装与雨林线'], userScenarios: ['越野穿越', '摄影/科考', '露营', '指挥车', '无人区探索', '空中侦察/补给概念'], proofPoints: ['地空一体视觉张力', '全域机能美学', '主动生态伴随', '从工具到平台的升维'], comparisonBoundaries: ['不做竞品压制', '不把概念车能力写成量产承诺', '不暗示自动驾驶替代驾驶员'], requiredMentions: ['概念车', '全域移动地空母舰', '全域适应', '全域智能', '全域美学'], forbiddenClaims: ['量产承诺', '完全自动驾驶', '无人驾驶', '绝对安全', '已上市/可交付', '确定配置/价格/交付时间'], }, source: 'gac-brand-assets-2026-04', }, { id: 'montx-v10', name: 'V10', aliases: ['MONTX V10', 'V10', '概念 VAN', '全域移动整备舱', '数字游民大本营'], positioning: 'MONTX V10 是概念阶段的全域移动整备舱,面向数字游民、户外探索与移动办公/旅居复合场景。', audience: ['数字游民', '户外探索者', '移动办公用户', '长周期旅居用户', '跨界多用途尝鲜型用户'], tone: ['理性浪漫', '工业秩序感', '克制', '温度感', '概念边界清晰'], requiredClaims: ['标注概念车叙事', '说明移动办公/旅居第三空间', '强调模块化空间与文明秩序感'], forbiddenClaims: ['量产承诺', '完全自动驾驶', '替代驾驶员', '绝对安全', '已上市/可交付', '确定配置/价格/交付时间'], narrative: { positioning: 'V10 是能在任意场景下切换工作、生活与旅行的“全域移动整备舱”,是数字游民的大本营和户外探险移动的家。', coreCapabilities: ['线控滑移方向盘概念', '智能双舱', '模块化座舱', '工作/社交/睡眠场景切换', '洗漱系统/模块化收纳/基础卫浴功能', '时间感知自动进化概念'], userScenarios: ['移动办公', '长周期旅居', '荒野驻留', '社交沙龙', '户外探险', '跨市场左右舵切换概念'], proofPoints: ['工业硬装与户外软装融合', '四开式尾门展开', '自循环基地', '隐形管家', '文明秩序与尊严'], comparisonBoundaries: ['不把自动切换、无感调节写成量产承诺', '不暗示无人驾驶或替代驾驶员', '不使用夸张竞品对比'], requiredMentions: ['概念车', '全域移动整备舱', '数字游民', '移动办公/旅居第三空间', '工业秩序感'], forbiddenClaims: ['量产承诺', '完全自动驾驶', '替代驾驶员', '绝对安全', '已上市/可交付', '确定配置/价格/交付时间'], }, source: 'gac-brand-assets-2026-04', }, ]; const DEFAULT_BRAND_PROFILES = [ { id: 'montx', name: 'MONTX', version: SCHEMA_VERSION, activeSkillId: 'montx-digital-editorial', products: MONTX_PRODUCT_PROFILES, narrative: { positioning: 'MONTX / 领越是广汽领程面向全球市场的“全域跨界新物种”,服务希望一部车满足商业、生活与探索多重需求的用户。', worldview: '移动出行世界中,场景、功能与用途的边界正在融合和重塑;MONTX 以跨界开拓精神,让用户在事业与生活之间获得更自由的全域场景体验。', valueProposition: '以一车多能、自由切换、全域适应和共创生态,帮助用户拓展事业与生活的边界。', voice: ['开拓', '全域', '机能', '先锋', '共创', '克制', '概念边界清晰'], requiredThemes: ['全域跨界新物种', 'New Breed. No Boundaries.', '一车多能', '自由切换', '全域智能', '全域适应', '全域美学', '共创更自由美好的事业与生活'], forbiddenAngles: ['把概念车写成量产承诺', '完全自动驾驶承诺', '无人驾驶暗示', '绝对安全', '过度玩梗', '轻浮自嘲', '竞品碾压', '财富自由夸大承诺'], proofTypes: ['品牌核心讯息', '产品概念车叙事', '真实用户场景', '共创机制', '视觉规范', '领导讲话/新闻稿'], }, visualSystem: { colorTokens: [ { id: 'montx-white', name: 'White', hex: '#FFFFFF', rgb: '255, 255, 255', cmyk: 'C0 M0 Y0 K0', pms: 'Bright White', role: 'default-logo-typography', usage: '黑白体系默认色,用于白底黑标、黑底白标和关键资产。', source: 'MONTX_GUIDELINES_V1.1.pptx' }, { id: 'montx-black', name: 'Black', hex: '#000000', rgb: '0, 0, 0', cmyk: 'C0 M0 Y0 K100', pms: 'Black 6C', role: 'default-logo-typography', usage: '黑白体系默认色,用于 Logo、字体和高对比文字。', source: 'MONTX_GUIDELINES_V1.1.pptx' }, { id: 'montx-earth-red', name: 'Earth Red', hex: '#580018', rgb: '88, 0, 24', cmyk: 'C0 M100 Y73 K65', pms: '7421C', role: 'accent-exception', usage: '品牌差异化强调色;仅在需要强品牌识别或重点信息时使用。', source: 'MONTX_GUIDELINES_V1.1.pptx' }, { id: 'montx-limestone-grey', name: 'Limestone Grey', hex: '#E1E0D8', rgb: '225, 224, 216', cmyk: 'C0 M0 Y4 K12', pms: 'Cool Gray 1C', role: 'accent-exception-background', usage: '天然地形感浅底色;用于背景、留白区、内容承载底色。', source: 'MONTX_GUIDELINES_V1.1.pptx' }, { id: 'montx-earth-red-watermark-10', name: 'Earth Red 10% 水印', hex: '#580018', rgb: '88, 0, 24', alpha: 0.1, role: 'watermark', usage: '巨大数字水印、低干扰背景编号;不得影响正文可读性。', source: 'MONTX_DEFAULT_SKILL.md' }, ], semanticColors: { background: '#E1E0D8', accent: '#580018', text: '#000000', inverseText: '#FFFFFF', watermark: 'rgba(88, 0, 24, 0.1)', }, colorRules: [ '主视觉系统采用骨白、酒红、黑、白的高对比组合。', '酒红用于结构性模块和装饰线,不作为渐变或柔化光效。', '黑字仅用于骨白底;白字仅用于酒红底,避免低对比叠加。', '水印使用酒红 10% 透明度,不能影响正文可读性。', '禁止新增渐变、毛玻璃、阴影等柔化色彩效果。', ], viGuidelines: [ { id: 'identity-guidelines-v1-2026-04', name: 'MONTX Identity Guidelines V1.0 April 2026', file: 'MONTX_GUIDELINES_V1.1.pptx', pages: 30, source: 'gac-brand-assets-2026-04' }, ], logoAssets: [ { id: 'social-avatar-brandmark', name: 'Social Avatar Brandmark', type: 'brandmark', src: 'assets/brand-logos/montx-social-avatar.png', background: '#580018', usage: '官方社媒头像与方形品牌触点,适合品牌记忆主档直接展示。', source: 'MONTX社媒平台账号主页品牌头像.png' }, { id: 'wordmark-ink', name: 'MONTX Wordmark Ink', type: 'wordmark', src: 'assets/brand-logos/montx-wordmark-ai-04-ink.png', background: '#FFFFFF', usage: '浅底或白底主品牌横向露出。', source: 'brand-logos' }, { id: 'wordmark-white', name: 'MONTX Wordmark White', type: 'wordmark', src: 'assets/brand-logos/montx-wordmark-ai-04-white.png', background: '#000000', usage: '黑底、深色图像或视频尾帧主品牌横向露出。', source: 'brand-logos' }, { id: 'wordmark-default', name: 'MONTX Wordmark', type: 'wordmark', src: 'assets/brand-logos/montx-wordmark.png', background: '#E1E0D8', usage: '骨白底传播物料与品牌资产预览。', source: 'brand-logos' }, ], logoLockups: [ { id: 'brandmark', name: 'Brandmark', usage: '独立图形标识,用于头像、车标、图标化场景和强识别触点。', isolation: '四周保留 50% X 安全区。' }, { id: 'horizontal-lockup', name: 'Horizontal Lockup', usage: 'Brandmark + MONTX wordmark 横向组合,用于主品牌露出。', construction: 'Wordmark 与 brandmark 按视觉中心对齐,保持固定比例与间距。', isolation: '横向组合四周保留规范安全区。' }, { id: 'vertical-lockup', name: 'Vertical Lockup', usage: '上下组合,用于空间受限或居中陈列。', construction: 'Wordmark 高度为 brandmark 的 33%,间距为 brandmark 的 66%,整体数学居中。', isolation: '四周保留规范安全区。' }, { id: 'horizontal-bilingual', name: 'Horizontal Bilingual', usage: '中英横向组合,仅用于中文市场。', construction: '中文“领越”与 MONTX 横向并列;wordmark 高度为 brandmark 的 75%。', isolation: '横向中英组合安全区为 150% X。' }, { id: 'vertical-bilingual', name: 'Vertical Bilingual', usage: '中英上下组合,用于中文市场的居中品牌露出。', construction: 'Brandmark、MONTX、领越垂直排列,保持固定比例和中心轴。', isolation: '四周保留规范安全区。' }, ], logoRules: [ 'Brandmark 四周保留 50% X 安全区。', 'Horizontal Bilingual 仅用于中文市场,横向中英组合安全区为 150% X。', 'Logo 常规使用黑/白;Earth Red 与 Limestone Grey 仅作为例外使用,适合独立露出并需要更强品牌识别的场景。', 'Logo 不得拉伸、压缩、旋转、重绘、改变比例或随意改变组合关系。', 'Logo 放置可使用预设位置系统:左上、右上、左下、右下、居中等,不得贴边或侵入安全区。', ], typography: [ { id: '5ceta-mono', name: '5Ceta Mono', role: 'primary-latin-headline', usage: '主拉丁字体,用于标题、主信息和关键传播元素。' }, { id: 'helvetica-neue-roman', name: 'Helvetica Neue Roman', role: 'secondary-body-copy', usage: '辅助字体,用于副标题、正文和说明文字。' }, { id: 'source-han-sans-cn-bold', name: 'SourceHanSansCN-Bold', role: 'chinese-digital-editorial', usage: '长图文中文标题与重点模块,可与 Helvetica 体系搭配。' }, ], layoutRules: ['1080px 画布', '图片全通栏', '图文分离', '禁止圆角拼图'], imageRules: ['社媒头像使用 1080 x 1080 PNG/JPG', '产品 Logo 可使用 brushed metal / chrome / internal illuminated 工艺表达'], applicationRules: [ { id: 'rich-logo-pages', name: 'Rich Logo Pages', usage: '整页展示 brandmark、wordmark 和中英组合时,保持足够留白和固定组合比例。' }, { id: 'product-logo-placement', name: 'Product Logo Placement', usage: '车头、车尾、轮毂、储物/尾门等产品触点按结构中心或预设区域放置,不破坏车身线条。' }, { id: 'product-logo-finishes', name: 'Product Logo Finishes', usage: '产品标识可使用 Brushed metal、Chrome、Internal illuminated 等工艺。' }, { id: 'website-layout', name: 'Website Layout', usage: '官网首屏采用大图场景、克制顶部导航和清晰 Logo 位置;避免复杂装饰。' }, { id: 'social-layout', name: 'Social Layout', usage: '社媒主页头像、封面和内容流保持 Logo 清晰,头像优先使用 brandmark。' }, { id: 'online-promotion', name: 'Online Promotion Layout', usage: '移动端推广保留 Logo 安全区,文字不压 Logo,不破坏主视觉。' }, { id: 'car-show-booth', name: 'Car Show Booth Display', usage: '展台招牌、门头、立面和车区使用大比例 MONTX/领越标识,保持远距可识别。' }, { id: 'dealership-stationery', name: 'Dealership Stationery', usage: '经销商物料包含 letterhead、envelope、business card、folder、tag、flag 等版式。' }, { id: 'video-end-frame', name: 'Video End Frame / CTA', usage: '视频结尾使用黑底 Logo lockup,可搭配网址或 CTA 文案。' }, ], }, campaigns: [ { id: 'niuma-ye-xiaoyao', name: '牛马野逍遥共创计划', status: 'active', allowedContexts: ['共创计划', '活动复盘', 'KOL 联合发起人语境'], forbiddenContexts: ['品牌常规语气', '用户自嘲', '官方硬广标题'], source: 'gac-brand-assets-2026-04' }, { id: 'hyper-teaser', name: 'Hyper 预热视频', status: 'active', allowedContexts: ['Hyper Performance', 'Hyper Intelligence', 'Hyper Design', 'Hyper Capability', 'Hyper Safety'], forbiddenContexts: ['脱离产品证据的性能夸张'], source: 'gac-brand-assets-2026-04' }, ], preferences: [ { id: 'scene-first-branding', type: 'content-style', polarity: 'prefer', stability: 'medium', label: '用场景定义品牌', description: '优先从商业、生活、探索等真实场景进入品牌叙事,而不是先堆配置或参数。', source: 'gac-brand-assets-2026-04', status: 'confirmed' }, { id: 'boundary-aware-concept', type: 'claim-style', polarity: 'prefer', stability: 'long-term', label: '概念边界清晰', description: 'P10/V10 的愿景能力可以讲,但必须用概念车、愿景、探索方向等语气标注边界。', source: '新闻稿 / 讲话稿', status: 'confirmed' }, { id: 'co-creation-context', type: 'campaign-style', polarity: 'prefer', stability: 'medium', label: '用户共创优先', description: '涉及牛马野逍遥时强调真开源、真共创、真定制,避免把用户共创写成普通活动噱头。', source: '牛马野逍遥-0415.pptx', status: 'confirmed' }, { id: 'avoid-light-meme', type: 'tone-boundary', polarity: 'avoid', stability: 'long-term', label: '避免轻浮玩梗', description: '品牌可以先锋、开拓、机能,但不能用轻浮自嘲、过度玩梗或竞品碾压来换短期互动。', source: 'brand-policy', status: 'confirmed' }, ], decisions: [ { id: 'decision-concept-boundary', status: 'confirmed', type: 'brand-boundary', title: 'P10/V10 统一按概念车叙事处理', rationale: '载人飞行器、线控滑移方向盘、时间感知自动进化等属于愿景和概念能力,不能写成量产承诺。', impact: ['strategy', 'brief', 'content', 'longform', 'monitor'], source: '新闻稿 / 讲话稿', decidedAt: '2026-04' }, { id: 'decision-niuma-context', status: 'confirmed', type: 'campaign-boundary', title: '“牛马”只保留在共创项目语境', rationale: '该表达属于“牛马野逍遥共创计划”的活动命名,不进入品牌常规口吻,也不替用户自嘲。', impact: ['strategy', 'brief', 'content', 'reply'], source: '牛马野逍遥-0415.pptx', decidedAt: '2026-04' }, { id: 'decision-official-social-only', status: 'confirmed', type: 'memory-governance', title: '品牌记忆只沉淀官方社媒内容', rationale: '外部达人和 earned media 是市场感知/传播复盘证据,不代表品牌自有表达,不能直接写入品牌记忆。', impact: ['memory', 'monitor', 'reports'], source: '人工确认', decidedAt: nowStamp() }, ], evidenceSources: [ { id: 'source-core-message-0420', title: '领越品牌核心讯息-0420-V1.pdf', type: 'brand-core', stability: 'long-term', status: 'confirmed', confidence: 'high', facts: ['品牌定位', '品牌口号', '品牌愿景', '核心价值主张'], sourcePath: '/Users/J/Downloads/广汽品牌资产/领越品牌核心讯息-0420-V1.pdf' }, { id: 'source-brand-strategy-0418', title: '广汽领程新品牌品牌策略方案-0418_final.pptx', type: 'brand-strategy', stability: 'long-term', status: 'confirmed', confidence: 'high', facts: ['目标用户', '品牌世界观', '场景边界', '传播主张'], sourcePath: '/Users/J/Downloads/广汽品牌资产/广汽领程新品牌品牌策略方案-0418_final.pptx' }, { id: 'source-vi-guidelines-v11', title: 'MONTX_GUIDELINES_V1.1.pptx', type: 'visual-system', stability: 'long-term', status: 'confirmed', confidence: 'high', facts: ['Logo 规范', '色彩规范', '字体规范', '应用规范'], sourcePath: '/Users/J/Downloads/广汽品牌资产/MONTX_GUIDELINES_V1.1.pptx' }, { id: 'source-news-product-speech', title: '新闻稿 / 讲话稿 / 产品稿', type: 'product-narrative', stability: 'medium', status: 'confirmed', confidence: 'medium', facts: ['P10 叙事', 'V10 叙事', '概念车边界', '共创表述'], sourcePath: '/Users/J/Downloads/广汽品牌资产/【4月23日无更新】15-新闻稿' }, { id: 'source-official-social', title: '官方社媒账号与已发布内容', type: 'approved-social-content', stability: 'short-term', status: 'pending', confidence: 'medium', facts: ['官方账号', '客户认可内容', '内容表现'], sourcePath: 'social-sync-state.json' }, { id: 'source-social-account-opening-0410', title: '广汽领程社媒账号开通建议0410final(1).pdf', type: 'social-platform-strategy', stability: 'medium-term', status: 'superseded-draft', confidence: 'medium', facts: ['平台分阶段布局', '账号开通流程', '8周推进路径', '平台淘汰/待定规则'], sourcePath: '/Users/J/Downloads/广汽品牌资产/广汽领程社媒账号开通建议0410final(1).pdf' }, ], memoryConflicts: [ { id: 'conflict-youtube-account-opening-0410', status: 'pending', severity: 'needs-confirmation', title: 'YouTube 开通节奏与当前执行冲突', message: '0410 初稿把 YouTube 归入 2027 蓄水期/海外增量盘,但当前品牌同步数据里已有官方 YouTube 账号 MONTX Tech。', sourceEvidenceId: 'source-social-account-opening-0410', draftClaim: 'YouTube 2027 蓄水期启动,偏静态评测、设计解读、技术前瞻、概念车展示、工厂探秘。', currentFact: 'YouTube · MONTX Tech 已作为海外视频官方账号存在。', recommendedResolution: 'accept_current', options: ['accept_current', 'keep_as_draft', 'needs_more_review'], upstream: ['0410 社媒账号开通建议初稿', '官方账号同步数据'], downstream: ['strategy', 'brief', 'content', 'monitor', 'reports'], createdAt: '2026-05-02', }, ], projectOperatingStages: [ { id: 'stage-brand-launch-2026', name: '品牌发布期', period: '2026 年起', factStatus: 'confirmed', provenance: 'documented', sourceEvidenceId: 'source-social-account-opening-0410', source: '广汽领程社媒账号开通建议0410final(1).pdf', sourceRefs: ['PAGE 3', 'PAGE 8', 'PAGE 11', 'PAGE 12'], stageGoals: ['大规模用户触达', '构建品牌粉丝基盘', '传递品牌形象', '建立专业形象与用户心智'], stageObjectives: ['账号搭建', '基础曝光', '内容方向验证', '评论反馈收集'], brandStrategyBoundaries: ['内容不依赖量产车', '强调品牌故事、技术长文、视觉资产和场景种草', '概念车能力不写成量产承诺'], operatingPreferences: [ { id: 'brand-launch-fb-ig', label: '海外发布期首批优先 Facebook + Instagram', factStatus: 'confirmed', provenance: 'documented', sourceRefs: ['PAGE 11', 'PAGE 12'] }, { id: 'brand-launch-ig-reels', label: 'Instagram 内容结构可参考图片 60% + Reels 30% + UGC 互动 10%', factStatus: 'confirmed', provenance: 'documented', sourceRefs: ['PAGE 8'] }, { id: 'brand-launch-weekly-frequency', label: '每周 2-3 条暂不能从现有资料直接确认,需要后续运营资料补证', factStatus: 'pending-confirmation', provenance: 'inferred-gap', sourceRefs: ['PAGE 23'] }, ], platformPlan: { overseas: ['Facebook', 'Instagram'], domestic: ['微信生态', '小红书'], }, downstream: ['strategy', 'brief', 'content', 'monitor', 'reports'], }, { id: 'stage-auto-show-2026', name: '车展传播期', period: '2026 北京车展前后', factStatus: 'inferred', provenance: 'inferred-from-approved-content', sourceEvidenceId: 'source-news-product-speech', source: '新闻稿 / 讲话稿 / 官方社媒内容', sourceRefs: ['source-news-product-speech', 'source-official-social'], stageGoals: ['首秀曝光', '解释 P10/V10 概念边界', '收集评论反馈', '识别用户误解和高兴趣点'], stageObjectives: ['基础曝光', '评论反馈收集', '概念误解纠偏', '高互动素材复用'], brandStrategyBoundaries: ['强调全域跨界新物种', '不自称房车', '不把概念能力写成量产承诺', '慎用末世感和逃离现实语气'], operatingPreferences: [ { id: 'auto-show-short-video', label: '短视频优先承接首秀、产品细节和场景解释', factStatus: 'inferred', provenance: 'inferred-from-approved-content', sourceRefs: ['source-official-social'] }, { id: 'auto-show-youtube-current', label: 'YouTube 已按当前执行事实前置进入海外视频阵地,但与 0410 初稿存在冲突待确认', factStatus: 'conflict-adjusted', provenance: 'conflict-adjusted', sourceRefs: ['conflict-youtube-account-opening-0410'] }, ], platformPlan: { overseas: ['Instagram', 'YouTube'], domestic: ['视频号', '微信公众号', '小红书'], }, conflicts: ['conflict-youtube-account-opening-0410'], downstream: ['strategy', 'brief', 'content', 'monitor', 'reports'], }, { id: 'stage-account-startup-8w', name: '账号启动期', period: '8 周推进路径', factStatus: 'confirmed', provenance: 'documented', sourceEvidenceId: 'source-social-account-opening-0410', source: '广汽领程社媒账号开通建议0410final(1).pdf', sourceRefs: ['PAGE 22', 'PAGE 23'], stageGoals: ['账号可对外展示', '内容节奏稳定', '消息与留资入口打通', '首轮投放数据可复盘'], stageObjectives: ['命名规范', '资料补齐', '权限与验证', '四大内容支柱上线', '首批内容 6-8 条上线', '小预算测试素材', '输出月度复盘'], brandStrategyBoundaries: ['准备材料包含 Logo、封面、头像、品牌简介、Slogan、官网链接', '法务合规需覆盖商标授权、版权确认、对外表述边界、隐私政策'], operatingPreferences: [ { id: 'startup-global-first', label: '先全球主账号,再看重点市场是否拆区域号', factStatus: 'confirmed', provenance: 'documented', sourceRefs: ['PAGE 22'] }, { id: 'startup-8-week-gate', label: '以账号展示、内容节奏、消息/留资、投放复盘作为阶段判断标准', factStatus: 'confirmed', provenance: 'documented', sourceRefs: ['PAGE 23'] }, ], platformPlan: { overseas: ['Facebook', 'Instagram', 'YouTube(当前执行事实,待冲突确认)'], domestic: ['微信生态', '小红书'], }, downstream: ['memory', 'strategy', 'content', 'monitor', 'reports'], }, { id: 'stage-growth-2027', name: '品牌成长期', period: '2027 年起', factStatus: 'confirmed', provenance: 'documented-draft', sourceEvidenceId: 'source-social-account-opening-0410', source: '广汽领程社媒账号开通建议0410final(1).pdf', sourceRefs: ['PAGE 3', 'PAGE 12', 'PAGE 13'], stageGoals: ['迎合市场触媒偏好', '触达更多目标市场', '进一步提升品牌认知度', '赋能开网、合作伙伴和供应链业务拓展'], stageObjectives: ['流量爆发', '深度种草', '区域阵地验证', '私域沉淀'], brandStrategyBoundaries: ['TikTok/Snapchat 属区域增量或验证阵地,不直接当全球统一阵地', 'YouTube 在初稿中属于蓄水期,但当前已前置执行,需保留冲突标记'], operatingPreferences: [ { id: 'growth-platform-experiments', label: 'TikTok、Snapchat 结合区域验证择机启动', factStatus: 'confirmed', provenance: 'documented-draft', sourceRefs: ['PAGE 12'] }, { id: 'growth-youtube-draft', label: 'YouTube 初稿建议 2027 做设计解读、技术前瞻、概念车展示等蓄水内容', factStatus: 'conflict-adjusted', provenance: 'conflict-adjusted', sourceRefs: ['PAGE 8', 'PAGE 12', 'conflict-youtube-account-opening-0410'] }, ], platformPlan: { overseas: ['YouTube', 'TikTok', 'LinkedIn', 'Snapchat'], domestic: ['抖音', '微博', 'B站', '微信生态'], }, conflicts: ['conflict-youtube-account-opening-0410'], downstream: ['strategy', 'brief', 'content', 'monitor', 'reports'], }, ], constraints: [ { id: 'concept-boundary', label: '概念车能力必须标注边界', category: 'claim', scope: ['strategy', 'brief', 'content', 'longform', 'monitor', 'reports'], severity: 'required', source: 'gac-brand-assets-2026-04', description: 'P10/V10 的载人飞行器、线控滑移方向盘、时间感知自动进化等表达必须标注为概念车/愿景叙事,不能写成量产承诺。', defaultLocked: true, forbiddenTerms: ['已量产', '即将交付', '确定上市', '标配', '价格'] }, { id: 'no-autonomous-overclaim', label: '禁止自动驾驶过度承诺', category: 'claim', scope: ['strategy', 'brief', 'content', 'longform', 'monitor', 'reports', 'reply'], severity: 'required', source: 'gac-brand-assets-2026-04', description: '涉及自动识别、自动接管、主动调整、无人区侦察等表达时,不得暗示完全自动驾驶、无人驾驶、替代驾驶员或绝对安全。', defaultLocked: true, forbiddenTerms: ['完全自动驾驶', '无人驾驶', '替代驾驶员', '绝对安全', '解放双手'] }, { id: 'niuma-context-only', label: '“牛马”仅限共创项目语境', category: 'campaign', scope: ['strategy', 'brief', 'content', 'longform', 'monitor', 'reply'], severity: 'required', source: 'gac-brand-assets-2026-04', description: '“牛马”只允许出现在“牛马野逍遥”共创计划相关语境,不进入品牌常规口吻,不替用户自嘲。', defaultLocked: true, forbiddenTerms: ['牛马'] }, { id: 'wealth-freedom-caution', label: '慎用财富自由', category: 'claim', scope: ['strategy', 'brief', 'content', 'longform', 'monitor', 'reports', 'reply'], severity: 'required', source: 'gac-brand-assets-2026-04', description: '“财富自由”只能作为讲话稿历史素材引用,常规生成中需避免金融收益、成本收益或商业成功的夸大承诺。', defaultLocked: true, forbiddenTerms: ['财富自由'] }, { id: 'no-gradient', label: '禁止渐变 / 毛玻璃 / 阴影', category: 'visual', scope: ['longform', 'content'], severity: 'required', source: 'brand-policy', description: '保持硬核对比和纯色块,不使用柔化视觉效果。', defaultLocked: true, forbiddenTerms: ['渐变', '毛玻璃', '阴影'] }, { id: 'no-image-text-overlay', label: '禁止图片压字', category: 'visual', scope: ['longform', 'content'], severity: 'required', source: 'brand-policy', description: '图片必须独立呈现,文字区块与图片分离。', defaultLocked: true, forbiddenTerms: ['图片压字', '文字覆盖图片'] }, { id: 'full-bleed-image', label: '图片必须全通栏', category: 'visual', scope: ['longform'], severity: 'required', source: 'brand-policy', description: '图片模块以 1080px 宽度铺满,不做拼图和局部圆角。', defaultLocked: true }, { id: 'no-rounded-collage', label: '禁止圆角与拼图', category: 'visual', scope: ['longform'], severity: 'required', source: 'brand-policy', description: 'Image Block 必须 w-full block,不加圆角、边距、拼接。', defaultLocked: true, forbiddenTerms: ['圆角拼图', '拼图'] }, { id: 'normal-body-weight', label: '正文禁止单字加粗', category: 'typography', scope: ['longform'], severity: 'required', source: 'brand-policy', description: '除标题外,段落文本强制 normal,不做局部加粗强调。', defaultLocked: true }, { id: 'canvas-1080', label: '画布宽度固定 1080px', category: 'visual', scope: ['longform'], severity: 'required', source: 'brand-policy', description: 'HTML 预览与 PNG 导出目标均为 1080px 宽。', defaultLocked: true }, { id: 'single-file-html', label: '单文件 HTML', category: 'delivery', scope: ['longform'], severity: 'required', source: 'brand-policy', description: '所有样式集成在导出的 HTML 中,便于后续转图。', defaultLocked: true }, { id: 'no-interaction', label: '无交互动画', category: 'delivery', scope: ['longform'], severity: 'required', source: 'brand-policy', description: '长图图文为静态交付,移除 hover、动画和滚动效果。', defaultLocked: true }, ], learnings: [], sources: ['领越品牌核心讯息-0420-V1.pdf', '广汽领程新品牌品牌策略方案-0418_final.pptx', 'MONTX_GUIDELINES_V1.1.pptx', '新闻稿 / 讲话稿 / 牛马野逍遥 / TVC'], updatedAt: 'seed', }, { id: 'aurelia', name: 'Aurelia Motors', version: SCHEMA_VERSION, activeSkillId: 'aurelia-editorial-reserved', products: [ { id: 'aurelia-ax7', name: 'AX-7', aliases: ['Aurelia AX-7', 'AX-7'], positioning: 'Aurelia Motors 旗舰纯电轿跑 SUV 首发产品,承载静奢、东方家庭和高端智能出行叙事。', audience: ['中高端家庭用户', '纯电 SUV 关注者', '品牌首发关注者'], tone: ['静奢', '克制', '高级', '家庭场景'], requiredClaims: ['旗舰首发', '静奢气质', '家庭第二台车场景'], forbiddenClaims: ['促销秒杀', '赛道碾压', '暴力加速'], narrative: { positioning: 'AX-7 是 Aurelia Motors 的旗舰首发产品,承担品牌进入中高端纯电轿跑 SUV 市场的叙事任务。', coreCapabilities: ['静奢设计', '家庭场景舒适性', '高端纯电体验'], userScenarios: ['家庭第二台车', '城市周末出行', '品牌首发认知建立'], proofPoints: ['设计语言', '空间与舒适性', '纯电体验', '品牌调性'], comparisonBoundaries: ['不以赛道和暴力加速作为主叙事', '不做低价促销化表达'], requiredMentions: ['AX-7 旗舰首发', '静奢', '家庭场景'], forbiddenClaims: ['促销秒杀', '碾压竞品', '暴力加速'] }, source: 'workspace-seed' }, { id: 'aurelia-ax5', name: 'AX-5', aliases: ['Aurelia AX-5', 'AX-5'], positioning: 'Aurelia Motors 城市家庭产品线。', audience: ['城市家庭用户'], tone: ['克制', '实用', '温和'], requiredClaims: ['城市家庭', '日常使用'], forbiddenClaims: ['赛道化表达'], narrative: { positioning: 'AX-5 面向城市家庭日常使用。' }, source: 'workspace-seed' }, { id: 'aurelia-concept-s', name: 'Concept-S', aliases: ['Aurelia Concept-S', 'Concept-S'], positioning: 'Aurelia Motors 概念产品线,用于未来设计方向表达。', audience: ['品牌关注者', '设计媒体'], tone: ['前瞻', '克制', '设计感'], requiredClaims: ['未来设计方向'], forbiddenClaims: ['量产承诺'], narrative: { positioning: 'Concept-S 用于表达 Aurelia Motors 的未来设计方向。' }, source: 'workspace-seed' }, ], narrative: { positioning: 'Aurelia Motors 是中高端纯电出行品牌,强调静奢、克制和东方家庭场景。', worldview: '电动豪华不应依赖喧闹性能叙事,而应通过克制设计、家庭场景和长期信任建立心智。', valueProposition: '以静奢设计和可靠体验服务高品质家庭出行。', voice: ['克制', '静奢', '高级', '温和'], requiredThemes: ['静奢', '东方家庭', '纯电体验', '长期信任'], forbiddenAngles: ['促销化表达', '赛道暴力叙事', '竞品碾压'], proofTypes: ['设计语言', '家庭场景', '用户洞察', '产品体验证据'], }, constraints: [ { id: 'aurelia-tone', label: '克制 / 静奢 / 非促销', category: 'tone', scope: ['strategy', 'brief', 'content', 'longform', 'monitor', 'reports', 'reply'], severity: 'required', source: 'brand-policy', description: '避免硬广、秒杀、碾压等转化型表达。', defaultLocked: true, forbiddenTerms: ['秒杀', '硬广', '碾压'] }, { id: 'aurelia-no-racing', label: '回避赛车对比', category: 'claim', scope: ['strategy', 'brief', 'content', 'longform'], severity: 'required', source: 'brand-policy', description: '不以赛道、暴力加速、竞品压制作为主叙事。', defaultLocked: true, forbiddenTerms: ['赛道碾压', '暴力加速'] }, ], learnings: [], decisions: [], sources: ['workspace-seed'], updatedAt: 'seed', }, ]; const normalizeBrandProfile = (brand = {}, index = 0) => ({ ...brand, id: brand.id || `brand-${index + 1}`, name: brand.name || `Brand ${index + 1}`, version: Math.max(Number(brand.version || 1), SCHEMA_VERSION), narrative: normalizeBrandNarrative(brand.narrative), products: (Array.isArray(brand.products) ? brand.products : []).map(normalizeProductProfile), constraints: (brand.constraints || []).map((constraint, itemIndex) => normalizeTraceItem(normalizeConstraint(constraint, itemIndex), { source: constraint.source || 'brand-policy' })), campaigns: Array.isArray(brand.campaigns) ? brand.campaigns.map(item => normalizeTraceItem(item, { source: 'gac-brand-assets-2026-04' })) : [], preferences: Array.isArray(brand.preferences) ? brand.preferences.map(item => normalizeTraceItem(item, { source: item.source || 'brand-memory' })) : [], projectOperatingStages: Array.isArray(brand.projectOperatingStages) ? brand.projectOperatingStages.map(normalizeOperatingStage) : [], visualSystem: normalizeVisualSystem(brand.visualSystem || {}), socialAssets: brand.socialAssets || { accounts: [], approvedPosts: [], commentsSummary: {}, updatedAt: '' }, learnings: Array.isArray(brand.learnings) ? brand.learnings.map(item => normalizeTraceItem(item, { factStatus: item.status === 'confirmed' ? 'confirmed' : 'pending-confirmation', provenance: 'pending-writeback', source: item.source || 'brand-memory' })) : [], decisions: Array.isArray(brand.decisions) ? brand.decisions.map(item => normalizeTraceItem(item, { source: item.source || 'manual' })) : [], evidenceSources: Array.isArray(brand.evidenceSources) ? brand.evidenceSources.map(item => normalizeTraceItem(item, { factStatus: item.status || 'confirmed', provenance: item.status === 'superseded-draft' ? 'documented-draft' : 'documented', source: item.sourcePath || item.title || 'source' })) : [], memoryConflicts: Array.isArray(brand.memoryConflicts) ? brand.memoryConflicts.map(item => normalizeTraceItem(item, { factStatus: 'pending-confirmation', provenance: 'conflict-detected', source: item.sourceEvidenceId || 'conflict-source', conflicts: [item.id] })) : [], sources: Array.isArray(brand.sources) ? brand.sources : [], updatedAt: brand.updatedAt || 'seed', }); const normalizeBrandProfiles = (brands = DEFAULT_BRAND_PROFILES) => brands.map(normalizeBrandProfile); const mergeSeedDefaults = (saved = []) => { const seed = normalizeBrandProfiles(DEFAULT_BRAND_PROFILES); const savedProfiles = normalizeBrandProfiles(saved?.length ? saved : seed); const merged = seed.map(seedBrand => { const current = savedProfiles.find(brand => brand.id === seedBrand.id); if (!current) return seedBrand; const constraints = [ ...current.constraints, ...seedBrand.constraints.filter(seedRule => !current.constraints.some(rule => rule.id === seedRule.id)), ]; const products = [ ...current.products, ...seedBrand.products.filter(seedProduct => !current.products.some(product => product.id === seedProduct.id)), ]; return normalizeBrandProfile({ ...seedBrand, ...current, version: Math.max(Number(current.version || 1), SCHEMA_VERSION), visualSystem: { ...(seedBrand.visualSystem || {}), ...(current.visualSystem || {}), colorTokens: mergeColorTokens(seedBrand.visualSystem?.colorTokens || [], current.visualSystem?.colorTokens || []), semanticColors: { ...(seedBrand.visualSystem?.semanticColors || {}), ...(current.visualSystem?.semanticColors || {}), }, colorRules: uniq([...(seedBrand.visualSystem?.colorRules || []), ...(current.visualSystem?.colorRules || [])]), viGuidelines: current.visualSystem?.viGuidelines?.length ? current.visualSystem.viGuidelines : (seedBrand.visualSystem?.viGuidelines || []), logoAssets: current.visualSystem?.logoAssets?.length ? current.visualSystem.logoAssets : (seedBrand.visualSystem?.logoAssets || []), logoLockups: current.visualSystem?.logoLockups?.length ? current.visualSystem.logoLockups : (seedBrand.visualSystem?.logoLockups || []), logoRules: uniq([...(seedBrand.visualSystem?.logoRules || []), ...(current.visualSystem?.logoRules || [])]), typography: current.visualSystem?.typography?.length ? current.visualSystem.typography : (seedBrand.visualSystem?.typography || []), layoutRules: uniq([...(seedBrand.visualSystem?.layoutRules || []), ...(current.visualSystem?.layoutRules || [])]), imageRules: uniq([...(seedBrand.visualSystem?.imageRules || []), ...(current.visualSystem?.imageRules || [])]), applicationRules: current.visualSystem?.applicationRules?.length ? current.visualSystem.applicationRules : (seedBrand.visualSystem?.applicationRules || []), }, constraints, products, campaigns: mergeMemoryItems(seedBrand.campaigns || [], current.campaigns || []), preferences: mergeMemoryItems(seedBrand.preferences || [], current.preferences || []), projectOperatingStages: mergeMemoryItems(seedBrand.projectOperatingStages || [], current.projectOperatingStages || []), decisions: mergeMemoryItems(seedBrand.decisions || [], current.decisions || []), evidenceSources: mergeMemoryItems(seedBrand.evidenceSources || [], current.evidenceSources || []), memoryConflicts: mergeMemoryItems(seedBrand.memoryConflicts || [], current.memoryConflicts || []), learnings: mergeMemoryItems(seedBrand.learnings || [], current.learnings || []), sources: uniq([...(seedBrand.sources || []), ...(current.sources || [])]), }); }); return [ ...merged, ...savedProfiles.filter(brand => !seed.some(seedBrand => seedBrand.id === brand.id)), ]; }; const BrandMemoryDB = { open() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { if (!req.result.objectStoreNames.contains(KV_STORE)) req.result.createObjectStore(KV_STORE); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); }, async get(key) { try { const db = await BrandMemoryDB.open(); return await new Promise((resolve, reject) => { const req = db.transaction(KV_STORE, 'readonly').objectStore(KV_STORE).get(key); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } catch (error) { console.warn('BrandMemory IndexedDB read failed', error); return null; } }, async set(key, value) { try { const db = await BrandMemoryDB.open(); await new Promise((resolve, reject) => { const req = db.transaction(KV_STORE, 'readwrite').objectStore(KV_STORE).put(value, key); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); } catch (error) { console.warn('BrandMemory IndexedDB write failed', error); } }, }; const loadProfiles = async () => mergeSeedDefaults(await BrandMemoryDB.get(PROFILE_KEY)); const saveProfiles = async (profiles) => BrandMemoryDB.set(PROFILE_KEY, normalizeBrandProfiles(profiles)); const getBrandProfile = (brandId, brands = DEFAULT_BRAND_PROFILES) => { const profiles = normalizeBrandProfiles(brands); return profiles.find(brand => brand.id === brandId) || profiles[0] || null; }; const getProductProfile = (brand, productId) => { const products = brand?.products || []; return products.find(product => product.id === productId) || products[0] || null; }; const getActiveConstraints = (brand, scope) => (brand?.constraints || []).filter(rule => { if (rule.enabled === false) return false; if (!scope) return true; const scopes = listFromValue(rule.scope); return !scopes.length || scopes.includes(scope); }); const updateProfileList = (profiles, brandId, updater) => normalizeBrandProfiles(profiles).map(brand => { if (brand.id !== brandId) return brand; const next = typeof updater === 'function' ? updater(brand) : updater; return normalizeBrandProfile({ ...brand, ...next, updatedAt: nowStamp() }); }); const saveBrandProfile = async (brandId, updater) => { const profiles = await loadProfiles(); const next = updateProfileList(profiles, brandId, updater); await saveProfiles(next); return getBrandProfile(brandId, next); }; const termAllowedByContext = (term, context = {}) => { if (term !== '牛马') return false; const campaignId = String(context.campaignId || '').toLowerCase(); const campaignName = String(context.campaignName || context.campaign || ''); return campaignId.includes('niuma') || campaignName.includes('牛马野逍遥'); }; const runBrandGuardrails = (input = '', context = {}) => { const text = typeof input === 'string' ? input : JSON.stringify(input || {}); const brand = context.brand || getBrandProfile(context.brandId, context.brands); const constraints = getActiveConstraints(brand, context.scope); const violations = []; const warnings = []; const requiredFixes = []; constraints.forEach(rule => { (rule.forbiddenTerms || []).forEach(term => { if (!term || !text.includes(term) || termAllowedByContext(term, context)) return; const item = { ruleId: rule.id, severity: rule.severity || 'required', category: rule.category || 'brand', term, message: `${rule.label}: 命中禁用表达“${term}”。`, suggestion: rule.description || '请按品牌记忆护栏改写。', }; violations.push(item); if (item.severity === 'required') requiredFixes.push(item); else warnings.push(item); }); (rule.requiredTerms || []).forEach(term => { if (!term || text.includes(term)) return; warnings.push({ ruleId: rule.id, severity: rule.severity || 'required', category: rule.category || 'brand', term, message: `${rule.label}: 建议补充“${term}”。`, suggestion: rule.description || '请补充必要品牌表达。', }); }); }); return { passed: violations.length === 0, brandId: brand?.id || context.brandId || '', scope: context.scope || 'all', version: brand?.version || SCHEMA_VERSION, checkedRules: constraints.length, violations, warnings, requiredFixes, constraintSnapshot: constraints.map(({ id, label, category, scope, severity, updatedAt }) => ({ id, label, category, scope, severity, updatedAt })), }; }; const recordBrandLearning = (brandId, learning) => saveBrandProfile(brandId, brand => ({ ...brand, learnings: [...(brand.learnings || []), { id: `learning-${Date.now()}`, status: 'pending', createdAt: nowStamp(), ...learning }], })); const recordBrandDecision = (brandId, decision) => saveBrandProfile(brandId, brand => ({ ...brand, decisions: [...(brand.decisions || []), { id: `decision-${Date.now()}`, createdAt: nowStamp(), ...decision }], })); const mergeRowsById = (rows = [], idKey) => { const map = new Map(); rows.forEach(row => { if (!row?.[idKey]) return; map.set(row[idKey], { ...(map.get(row[idKey]) || {}), ...row }); }); return Array.from(map.values()); }; const buildSocialMemorySnapshot = ({ brandId = 'montx', accounts = [], posts = [], comments = [], lastSync = '' } = {}) => { const mergedPosts = mergeRowsById(posts, 'post_id'); const officialPosts = mergedPosts .filter(post => !isExternalSocialPost(post)) .sort((a, b) => socialInteraction(b) - socialInteraction(a) || getSocialPostTime(b) - getSocialPostTime(a)); const officialPostAccountIds = new Set(officialPosts.map(post => post.account_id).filter(Boolean)); const scopedAccounts = mergeRowsById(accounts, 'account_id') .filter(account => !isExternalSocialAccount(account)) .filter(account => account.brand_id === brandId || officialPostAccountIds.has(account.account_id)) .map(account => ({ account_id: account.account_id, platform: account.platform || '', account_name: account.account_name || account.handle || '', handle: account.handle || '', homepage_url: account.homepage_url || '', owner: account.owner || '', positioning: account.positioning || '', status: account.status || 'active', follower_delta_period: socialNumber(account.follower_delta_period), source: account.provider || account.source || 'social-sync', })); const accountIds = new Set(scopedAccounts.map(account => account.account_id)); const scopedOfficialPosts = officialPosts .filter(post => post.brand_id === brandId || accountIds.has(post.account_id)) .sort((a, b) => socialInteraction(b) - socialInteraction(a) || getSocialPostTime(b) - getSocialPostTime(a)); const postIds = new Set(scopedOfficialPosts.map(post => post.post_id)); const scopedComments = mergeRowsById(comments, 'comment_id').filter(comment => postIds.has(comment.post_id)); const approvedPosts = scopedOfficialPosts.slice(0, 12).map(post => ({ post_id: post.post_id, account_id: post.account_id || '', platform: post.platform || '', title: post.corrected_title || post.display_title || post.title || post.manual_title || post.raw?.display_title || '未命名内容', fullText: post.title && post.title !== (post.corrected_title || post.display_title) ? post.title : '', url: post.url || post.manual_metrics?.url || post.raw?.share_url || '', content_type: post.content_type || '', campaign: post.campaign || '', product: post.product || '', published_at: post.published_at || post.publish_time || '', cover_url: post.cached_cover_url || post.media_url || post.raw?.image?.thumbnail || '', metrics: { views: socialNumber(post.views || post.plays || post.reads || post.organic_impressions), likes: socialNumber(post.likes), comments: socialNumber(post.comments), shares: socialNumber(post.shares), saves: socialNumber(post.saves), interaction: socialInteraction(post), }, source: post.provider || post.data_channel || post.source_type || 'social-sync', memoryRole: 'approved-content-evidence', })); const positive = scopedComments.filter(comment => socialNumber(comment.sentiment) > 0.15).length; const risk = scopedComments.filter(comment => comment.risk_tag || socialNumber(comment.sentiment) < -0.15).length; return { source: 'social-sync', brandId, accounts: scopedAccounts, approvedPosts, commentsSummary: { total: scopedComments.length, positive, risk, neutral: Math.max(0, scopedComments.length - positive - risk), }, updatedAt: lastSync || nowStamp(), learningId: `social-memory-${brandId}-${approvedPosts.map(post => post.post_id).join('|').slice(0, 80) || 'empty'}`, }; }; const applySocialMemorySnapshot = (brand = {}, snapshot = {}) => { const learning = { id: snapshot.learningId || `social-memory-${Date.now()}`, status: 'pending', type: 'approved-social-content', source: 'social-sync', title: '官方社媒已发布内容归档', text: `已归档 ${snapshot.accounts?.length || 0} 个官方账号、${snapshot.approvedPosts?.length || 0} 条已发布内容,作为客户认可表达与内容证据池。`, createdAt: nowStamp(), evidencePostIds: (snapshot.approvedPosts || []).map(post => post.post_id), }; return normalizeBrandProfile({ ...brand, socialAssets: { ...(brand.socialAssets || {}), accounts: snapshot.accounts || [], approvedPosts: snapshot.approvedPosts || [], commentsSummary: snapshot.commentsSummary || {}, updatedAt: snapshot.updatedAt || nowStamp(), }, learnings: [ ...(brand.learnings || []).filter(item => item.id !== learning.id), learning, ], updatedAt: nowStamp(), }); }; const applyMemoryConflictResolution = (brand = {}, conflictId, resolution = 'accept_current', note = '') => { const stampedAt = nowStamp(); const conflicts = brand.memoryConflicts || []; const conflict = conflicts.find(item => item.id === conflictId); if (!conflict) return normalizeBrandProfile(brand); const status = resolution === 'needs_more_review' ? 'pending' : resolution === 'keep_as_draft' ? 'archived' : 'resolved'; const nextConflicts = conflicts.map(item => item.id === conflictId ? { ...item, status, resolution, resolutionNote: note, resolvedAt: status === 'resolved' || status === 'archived' ? stampedAt : item.resolvedAt, } : item); const resolutionText = { accept_current: '以当前执行事实为准,初稿仅作为策略演进证据。', keep_as_draft: '仅保留为历史初稿,不进入当前平台策略。', needs_more_review: '保留待确认状态,需要用户补充新版资料或人工判断。', }[resolution] || resolution; const nextDecisions = resolution === 'needs_more_review' ? (brand.decisions || []) : mergeMemoryItems(brand.decisions || [], [{ id: `decision-${conflict.id}-${resolution}`, status: 'confirmed', type: 'conflict-resolution', title: `冲突处理:${conflict.title}`, rationale: `${resolutionText}${note ? ` 备注:${note}` : ''}`, impact: conflict.downstream || ['memory'], source: '人工确认', decidedAt: stampedAt, }]); const nextEvidenceSources = (brand.evidenceSources || []).map(source => ( source.id === conflict.sourceEvidenceId && resolution === 'keep_as_draft' ? { ...source, status: 'superseded-draft', updatedAt: stampedAt } : source )); return normalizeBrandProfile({ ...brand, memoryConflicts: nextConflicts, decisions: nextDecisions, evidenceSources: nextEvidenceSources, updatedAt: stampedAt, }); }; const useBrandMemoryProfiles = () => { const [brands, setBrands] = React.useState(() => normalizeBrandProfiles(DEFAULT_BRAND_PROFILES)); const [ready, setReady] = React.useState(false); React.useEffect(() => { let mounted = true; loadProfiles().then(profiles => { if (!mounted) return; setBrands(profiles); setReady(true); }); return () => { mounted = false; }; }, []); React.useEffect(() => { if (ready) saveProfiles(brands); }, [ready, brands]); return { brands, setBrands, ready }; }; const useBrandMemory = (brandId) => { const state = useBrandMemoryProfiles(); const brand = getBrandProfile(brandId, state.brands); const updateBrand = (nextBrandOrUpdater) => { if (!brand) return; state.setBrands(prev => updateProfileList(prev, brand.id, nextBrandOrUpdater)); }; return { ...state, brand, updateBrand, activeConstraints: getActiveConstraints(brand), runGuardrails: (input, context = {}) => runBrandGuardrails(input, { ...context, brand, brandId: brand?.id || brandId }), }; }; window.BrandMemoryStore = { SCHEMA_VERSION, DEFAULT_BRAND_PROFILES, MONTX_PRODUCT_PROFILES, normalizeBrandProfiles, normalizeBrandProfile, normalizeConstraint, loadProfiles, saveProfiles, getBrandProfile, getProductProfile, getActiveConstraints, saveBrandProfile, recordBrandLearning, recordBrandDecision, buildSocialMemorySnapshot, applySocialMemorySnapshot, applyMemoryConflictResolution, runBrandGuardrails, useBrandMemoryProfiles, useBrandMemory, }; })();