/* ===== Screens Part 4: Longform Agent ===== */
const MONTX_DEFAULT_SKILL = `# MONTX Digital Editorial Design System
你是顶级数字媒体画册设计师。你的任务是严格执行 **Avant-garde Bauhaus Grid(先锋建筑与包豪斯网格)** 风格排版,专用于静态长图图文设计。
## 1. 核心视觉哲学
- **工业秩序感**:利用粗犷的几何分割替代诗意留白。
- **硬核对比**:大面积纯色块撞色(酒红 vs 骨白),严禁使用渐变、毛玻璃或阴影。
- **图文分离**:图片必须全通栏撑满,文字区块独立呈现,绝不允许在图片上覆盖文字。
## 2. 品牌规格 (Style Tokens)
- **画布限制**:最外层容器 \`width: 1080px\`,居中对齐。
- **色彩规范 (Strict)**:
- 全局背景:\`#E1E0D8\` (骨白)
- 强调色/模块色:\`#580018\` (酒红)
- 文本色:\`#000000\` (黑) 于骨白底;\`#FFFFFF\` (白) 于酒红底。
- **字体规范**:
- 英文:\`Helvetica\`
- 中文:\`SourceHanSansCN-Bold\` (思源黑体加粗)
- 字号:正文 \`34px - 38px\`,大标题 \`110px+\`,小标题 \`40px - 70px\`。
- **线条规范**:装饰线及边框统一使用酒红色,厚度为 \`12px\` 或 \`24px\`。
## 3. 结构组件库 (Components)
### A. Header (页眉)
- 顶部 \`24px\` 酒红实线封顶。
- 标题 \`MONTX V10\` 单行大写,使用 \`title-giant\` (110px) 样式。
- 下方紧跟 \`12px\` 装饰线及引言段落。
### B. Image Block (全通栏图片)
- 属性:\`w-full block\`,\`referrerpolicy="no-referrer"\`。
- 禁止:圆角、外边距、拼图。
### C. Narrative Block (文本模块)
- **水印背景**:使用极其巨大的实心数字(如 01, 02),颜色 \`#580018\`,透明度 \`10%\`。
- **排版逻辑**:
- 章节标题:酒红底白字块,内边距 \`px-16 py-8\`。
- 正文段落:左侧 \`12px\` 酒红实心垂直线(border-l),字体样式为常规权重,严禁单字加粗。
### D. Grid Content (网格化内容)
- 采用 \`grid-cols-12\` 结构。
- 左侧(4-5列)通常为酒红纯色块,用于平衡视觉权重。
- 右侧(7-8列)为文字细分项,使用 \`border-t-[2px]\` 进行横向切割。
### E. Footer (页脚)
- 背景:\`#580018\`。
- 内容:单行大标题 + 官网链接。
## 4. 交付约束
- **Single-File Mandate**:所有 HTML、Tailwind CSS 必须集成在一个文件内。
- **No Interaction**:移除所有 hover、动画、滚动效果。
- **Text Weight**:除标题外,所有段落文本强制执行 \`font-weight: normal\`,禁止任何形式的单字加粗。`;
const LONGFORM_DEFAULT_STYLE_TEMPLATE_ID = 'montx-default';
const LONGFORM_CUSTOM_STYLE_TEMPLATE_ID = 'custom-upload';
const LONGFORM_DEFAULT_STYLE_TEMPLATE = {
id: LONGFORM_DEFAULT_STYLE_TEMPLATE_ID,
name: 'MONTX 默认',
fileName: 'MONTX_DEFAULT_SKILL.md',
markdown: MONTX_DEFAULT_SKILL,
};
const LONGFORM_NARRATIVE_MODES = [
{ id: 'brand_product', label: '品牌 + 产品', description: '先建立 MONTX 的判断,再用 P10/V10 作为证据落点。' },
{ id: 'brand', label: '品牌向', description: '强调 MONTX 的世界观、价值主张和表达边界,避免过度产品广告化。' },
{ id: 'product', label: '产品向', description: '围绕所选产品的能力、场景、证据和边界展开。' },
{ id: 'brief_only', label: '仅 Brief', description: '优先执行当前 brief,不主动加入品牌或产品叙事。' },
];
const getLongformNarrativeMode = (mode = 'brand_product') => LONGFORM_NARRATIVE_MODES.find(item => item.id === mode) || LONGFORM_NARRATIVE_MODES[0];
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 LONGFORM_BRANDS = [
{
id: 'montx',
name: 'MONTX',
activeSkillId: 'montx-digital-editorial',
products: MONTX_PRODUCT_PROFILES,
narrative: {
positioning: 'MONTX / 领越是广汽领程面向全球市场的“全域跨界新物种”,服务希望一部车满足商业、生活与探索多重需求的用户。',
worldview: '移动出行世界中,场景、功能与用途的边界正在融合和重塑;MONTX 以跨界开拓精神,让用户在事业与生活之间获得更自由的全域场景体验。',
valueProposition: '以一车多能、自由切换、全域适应和共创生态,帮助用户拓展事业与生活的边界。',
voice: ['开拓', '全域', '机能', '先锋', '共创', '克制', '概念边界清晰'],
requiredThemes: ['全域跨界新物种', 'New Breed. No Boundaries.', '一车多能', '自由切换', '全域智能', '全域适应', '全域美学', '共创更自由美好的事业与生活'],
forbiddenAngles: ['把概念车写成量产承诺', '完全自动驾驶承诺', '无人驾驶暗示', '绝对安全', '过度玩梗', '轻浮自嘲', '竞品碾压', '财富自由夸大承诺'],
proofTypes: ['品牌核心讯息', '产品概念车叙事', '真实用户场景', '共创机制', '视觉规范', '领导讲话/新闻稿'],
},
constraints: [
{ id: 'concept-boundary', label: '概念车能力必须标注边界', severity: 'required', source: 'gac-brand-assets-2026-04', description: 'P10/V10 的载人飞行器、线控滑移方向盘、时间感知自动进化等表达必须标注为概念车/愿景叙事,不能写成量产承诺。', defaultLocked: true, forbiddenTerms: ['已量产', '即将交付', '确定上市', '标配', '价格'] },
{ id: 'no-autonomous-overclaim', label: '禁止自动驾驶过度承诺', severity: 'required', source: 'gac-brand-assets-2026-04', description: '涉及自动识别、自动接管、主动调整、无人区侦察等表达时,不得暗示完全自动驾驶、无人驾驶、替代驾驶员或绝对安全。', defaultLocked: true, forbiddenTerms: ['完全自动驾驶', '无人驾驶', '替代驾驶员', '绝对安全', '解放双手'] },
{ id: 'niuma-context-only', label: '“牛马”仅限共创项目语境', severity: 'required', source: 'gac-brand-assets-2026-04', description: '“牛马”只允许出现在“牛马野逍遥”共创计划相关语境,不进入品牌常规口吻,不替用户自嘲。', defaultLocked: true, forbiddenTerms: ['牛马'] },
{ id: 'wealth-freedom-caution', label: '慎用财富自由', severity: 'required', source: 'gac-brand-assets-2026-04', description: '“财富自由”只能作为讲话稿历史素材引用,常规生成中需避免金融收益、成本收益或商业成功的夸大承诺。', defaultLocked: true, forbiddenTerms: ['财富自由'] },
{ id: 'no-gradient', label: '禁止渐变 / 毛玻璃 / 阴影', severity: 'required', source: 'brand-policy', description: '保持硬核对比和纯色块,不使用柔化视觉效果。', defaultLocked: true },
{ id: 'no-image-text-overlay', label: '禁止图片压字', severity: 'required', source: 'brand-policy', description: '图片必须独立呈现,文字区块与图片分离。', defaultLocked: true },
{ id: 'full-bleed-image', label: '图片必须全通栏', severity: 'required', source: 'brand-policy', description: '图片模块以 1080px 宽度铺满,不做拼图和局部圆角。', defaultLocked: true },
{ id: 'no-rounded-collage', label: '禁止圆角与拼图', severity: 'required', source: 'brand-policy', description: 'Image Block 必须 w-full block,不加圆角、边距、拼接。', defaultLocked: true },
{ id: 'normal-body-weight', label: '正文禁止单字加粗', severity: 'required', source: 'brand-policy', description: '除标题外,段落文本强制 normal,不做局部加粗强调。', defaultLocked: true },
{ id: 'canvas-1080', label: '画布宽度固定 1080px', severity: 'required', source: 'brand-policy', description: 'HTML 预览与 PNG 导出目标均为 1080px 宽。', defaultLocked: true },
{ id: 'single-file-html', label: '单文件 HTML', severity: 'required', source: 'brand-policy', description: '所有样式集成在导出的 HTML 中,便于后续转图。', defaultLocked: true },
{ id: 'no-interaction', label: '无交互动画', severity: 'required', source: 'brand-policy', description: '长图图文为静态交付,移除 hover、动画和滚动效果。', defaultLocked: true },
],
},
{
id: 'aurelia',
name: 'Aurelia Motors',
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: '克制 / 静奢 / 非促销', severity: 'required', source: 'brand-policy', description: '避免硬广、秒杀、碾压等转化型表达。', defaultLocked: true },
{ id: 'aurelia-no-racing', label: '回避赛车对比', severity: 'required', source: 'brand-policy', description: '不以赛道、暴力加速、竞品压制作为主叙事。', defaultLocked: true },
],
},
];
const LONGFORM_STEPS = ['需求输入', '内容推演', '图片分配', 'HTML 预览', 'PNG 导出'];
const LONGFORM_BLOCK_TYPES = [
{ id: 'hero', label: '开场 Hero', type: 'Header', layoutModule: 'Header', description: '主标题、导语和首张主视觉,负责建立第一屏判断。', required: true },
{ id: 'narrative', label: '叙事段落', type: 'Narrative', layoutModule: 'Narrative Block', description: '观点推进、用户问题、背景说明或论证正文。', required: true },
{ id: 'tech_breakdown', label: '技术拆解', type: 'Narrative', layoutModule: 'Narrative Block', description: '把复杂机制拆成可理解的步骤、原理和边界。', required: false },
{ id: 'image_proof', label: '图片证据', type: 'Image', layoutModule: 'Image Block', description: '用于承载真实场景、产品图、道路图或视觉证据组。', required: false },
{ id: 'metrics', label: '指标信息', type: 'Grid', layoutModule: 'Grid Content', description: '适合参数、能力清单、场景覆盖和高密度信息。', required: false },
{ id: 'compare', label: '对比解释', type: 'Grid', layoutModule: 'Grid Content', description: '适合能力边界、前后对比、同类方案差异说明。', required: false },
{ id: 'timeline', label: '流程时间线', type: 'Narrative', layoutModule: 'Narrative Block', description: '适合流程、决策链路、阶段递进和使用步骤。', required: false },
{ id: 'cta', label: '收束 CTA', type: 'Footer', layoutModule: 'Footer', description: '结尾行动、品牌收束、官网或预约入口。', required: true },
];
const getLongformBlockType = (section = {}) => {
if (typeof section === 'string') return LONGFORM_BLOCK_TYPES.find(block => block.id === section) || LONGFORM_BLOCK_TYPES[1];
if (section.blockType) return getLongformBlockType(section.blockType);
const haystack = `${section.type || ''} ${section.layoutModule || ''} ${section.title || ''}`.toLowerCase();
if (/header|hero|开场|标题/.test(haystack)) return LONGFORM_BLOCK_TYPES[0];
if (/footer|cta|官网|预约|收束/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'cta');
if (/image|图片|证据|场景/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'image_proof');
if (/grid|指标|参数|覆盖|清单/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'metrics');
if (/对比|差异|竞品/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'compare');
if (/流程|路径|时间线|阶段|链路/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'timeline');
if (/技术|原理|算法|传感器|决策/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'tech_breakdown');
return LONGFORM_BLOCK_TYPES[1];
};
const normalizeLongformSection = (section = {}, index = 0) => {
const block = getLongformBlockType(section);
return {
id: section.id || `${block.type.toLowerCase()}-${Date.now()}-${index}`,
type: block.type,
blockType: block.id,
title: section.title || LONGFORM_DEFAULT_SECTIONS[index % LONGFORM_DEFAULT_SECTIONS.length]?.title || block.label,
body: section.body || '',
imageIds: Array.isArray(section.imageIds) ? section.imageIds : [],
layoutModule: block.layoutModule,
imageHint: section.imageHint || '',
};
};
const LONGFORM_DEFAULT_SECTIONS = [
{ id: 'hero', type: 'Header', blockType: 'hero', title: 'MONTX V10', body: '一篇面向微信公众号的长图文,先建立观点,再用强秩序视觉把阅读节奏压实。', imageIds: [], layoutModule: 'Header', imageHint: '可放置一张建立主题气质的全通栏主视觉,避免文字压图。' },
{ id: 's1', type: 'Narrative', blockType: 'narrative', title: '为什么现在需要一篇长文', body: '用户不是缺少信息,而是缺少一个被组织过的判断。长图文的任务,是把分散需求压缩成可阅读、可转发、可记忆的结构。', imageIds: [], layoutModule: 'Narrative Block', imageHint: '选择能证明问题现场或用户情境的图片,作为段落后的全通栏证据。' },
{ id: 's2', type: 'Image', blockType: 'image_proof', title: '场景证据', body: '图片用于承载情绪和现实感,必须全通栏展示,不在图上叠字。', imageIds: [], layoutModule: 'Image Block', imageHint: '优先放最有信息量的实拍图,可连续添加多张形成证据组。' },
{ id: 's3', type: 'Grid', blockType: 'metrics', title: '内容推演框架', body: '目标读者 / 核心论点 / 证据链 / CTA 被拆成网格化模块,便于后续转成长图。', imageIds: [], layoutModule: 'Grid Content', imageHint: '适合补充细节、对比、局部特写或流程节点图片。' },
{ id: 'footer', type: 'Footer', blockType: 'cta', title: '前往官网', body: 'montx.example.com', imageIds: [], layoutModule: 'Footer', imageHint: '通常不配图;如需品牌收束,可放产品或品牌环境图。' },
];
const LONGFORM_MOCK_TASKS = [
{
id: 'lf-ax-001',
brandId: 'montx',
productId: 'montx-v10',
narrativeMode: 'brand_product',
title: 'MONTX V10 旅行编辑长图',
prompt: '为微信公众号设计一篇面向高净值旅行用户的长图文,主题是大溪地海岛资产与现代生活方式。',
status: 'HTML 已生成',
activeStyleTemplateId: LONGFORM_DEFAULT_STYLE_TEMPLATE_ID,
skillMarkdown: MONTX_DEFAULT_SKILL,
images: [],
sections: LONGFORM_DEFAULT_SECTIONS,
html: '',
messages: [
{ role: 'assistant', text: '把这篇公众号长图文的目标、读者、语气和必须覆盖的观点发给我。我会先推演结构,再把它拆成可排版的 section。' },
{ role: 'user', text: '为微信公众号设计一篇面向高净值旅行用户的长图文,主题是大溪地海岛资产与现代生活方式。' },
],
overrides: [],
qa: { score: 92, issues: ['2 个图片位待上传'], exportReady: true },
updatedAt: '2026-04-27 09:40',
},
{
id: 'lf-ax-002',
brandId: 'montx',
productId: 'montx-v10',
narrativeMode: 'brand',
title: '城市更新观点稿',
prompt: '从设计媒体视角写一篇观点长文,解释为什么工业秩序感正在回到数字出版。',
status: '待上传图片',
activeStyleTemplateId: LONGFORM_DEFAULT_STYLE_TEMPLATE_ID,
skillMarkdown: MONTX_DEFAULT_SKILL,
images: [],
sections: LONGFORM_DEFAULT_SECTIONS.map((s, i) => ({ ...s, id: `${s.id}-2`, title: i === 0 ? 'GRID IS BACK' : s.title })),
html: '',
messages: [
{ role: 'assistant', text: '请给我这次长图文的目标和读者,我会推演文章结构。' },
{ role: 'user', text: '从设计媒体视角写一篇观点长文,解释为什么工业秩序感正在回到数字出版。' },
],
overrides: [{ constraintId: 'full-bleed-image', reason: '客户要求保留一张证件式人物肖像,暂不全通栏。', createdAt: '2026-04-27 10:12' }],
qa: { score: 76, issues: ['存在 1 项解除约束'], exportReady: true },
updatedAt: '2026-04-27 10:12',
},
];
const longformId = (prefix = 'lf') => `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 7)}`;
const MONTX_EXPORT_CSS = `
.montx-canvas{width:1080px;margin:0 auto;background:var(--lf-bg,#E1E0D8);color:var(--lf-text,#000);font-family:var(--lf-font,Helvetica,Arial,"Source Han Sans CN","Noto Sans SC",sans-serif);overflow:hidden;}
.montx-topline{height:24px;background:var(--lf-accent,#580018);}
.montx-giant{padding:58px 64px 24px;font-size:118px;line-height:.9;font-weight:900;letter-spacing:-.06em;text-transform:uppercase;}
.montx-rule{height:12px;background:var(--lf-accent,#580018);margin:0 64px 36px;}
.montx-lead{padding:0 64px 72px;font-size:38px;line-height:1.35;font-weight:400;}
.montx-image{display:block;width:100%;border:0;border-radius:0;margin:0;}
.montx-narrative{position:relative;min-height:360px;padding:88px 64px 92px;}
.montx-watermark{position:absolute;right:48px;top:18px;font-size:220px;line-height:1;color:var(--lf-watermark,rgba(88,0,24,.1));font-weight:900;}
.montx-section-title{display:inline-block;background:var(--lf-accent,#580018);color:var(--lf-inverse,#fff);font-size:54px;line-height:1;font-weight:900;padding:18px 28px;margin-bottom:36px;}
.montx-body{position:relative;z-index:1;border-left:12px solid var(--lf-accent,#580018);padding-left:34px;font-size:36px;line-height:1.55;font-weight:400;}
.montx-grid{display:grid;grid-template-columns:4fr 8fr;border-top:24px solid var(--lf-accent,#580018);border-bottom:24px solid var(--lf-accent,#580018);}
.montx-grid-left{background:var(--lf-accent,#580018);color:var(--lf-inverse,#fff);font-size:64px;line-height:1.05;font-weight:900;padding:54px 44px;}
.montx-grid-right{padding:50px 58px;font-size:34px;line-height:1.5;font-weight:400;}
.montx-footer{background:var(--lf-accent,#580018);color:var(--lf-inverse,#fff);padding:60px 64px 72px;}
.montx-footer h2{margin:0 0 18px;font-size:86px;line-height:.95;font-weight:900;letter-spacing:-.04em;}
.montx-footer p{margin:0;font-size:34px;font-weight:400;}
`;
const escapeHtml = (value = '') => String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
const inferLongformConstraintCategory = (constraint = {}) => {
const text = `${constraint.label || ''} ${constraint.description || ''}`.toLowerCase();
if (/图片|image|圆角|拼图|压字|全通栏/.test(text)) return 'image';
if (/字号|画布|宽度|html|动画|交互|渐变|阴影|毛玻璃|layout|style/.test(text)) return 'style';
if (/自动驾驶|解放双手|承诺|竞品|促销|话术|claim/.test(text)) return 'claim';
return 'brand';
};
const normalizeLongformConstraint = (constraint, index = 0) => ({
id: constraint.id || longformId('guard'),
label: constraint.label || `品牌护栏 ${index + 1}`,
description: constraint.description || '',
severity: constraint.severity || 'required',
category: constraint.category || inferLongformConstraintCategory(constraint),
scope: constraint.scope || ['generation', 'preview', 'export', 'qa'],
defaultLocked: constraint.defaultLocked !== false,
enabled: constraint.enabled !== false,
forbiddenTerms: constraint.forbiddenTerms || [],
requiredTerms: constraint.requiredTerms || [],
rules: constraint.rules || {},
examples: constraint.examples || { bad: [], good: [] },
source: constraint.source || 'brand-policy',
createdAt: constraint.createdAt || 'seed',
updatedAt: constraint.updatedAt || constraint.createdAt || 'seed',
});
const longformListFromValue = (value = '') => Array.isArray(value)
? value
: String(value || '').split(/[;;,,、\n]/).map(item => item.trim()).filter(Boolean);
const normalizeBrandNarrative = (narrative = {}) => ({
positioning: narrative.positioning || '',
worldview: narrative.worldview || '',
valueProposition: narrative.valueProposition || '',
voice: longformListFromValue(narrative.voice),
requiredThemes: longformListFromValue(narrative.requiredThemes),
forbiddenAngles: longformListFromValue(narrative.forbiddenAngles),
proofTypes: longformListFromValue(narrative.proofTypes),
});
const normalizeProductNarrative = (narrative = {}, product = {}) => ({
positioning: narrative.positioning || product.positioning || '',
coreCapabilities: longformListFromValue(narrative.coreCapabilities),
userScenarios: longformListFromValue(narrative.userScenarios),
proofPoints: longformListFromValue(narrative.proofPoints),
comparisonBoundaries: longformListFromValue(narrative.comparisonBoundaries),
requiredMentions: longformListFromValue(narrative.requiredMentions || product.requiredClaims),
forbiddenClaims: longformListFromValue(narrative.forbiddenClaims || product.forbiddenClaims),
});
const normalizeLongformProduct = (product = {}, index = 0) => ({
id: product.id || longformId('product'),
name: product.name || `Product ${index + 1}`,
aliases: longformListFromValue(product.aliases),
positioning: product.positioning || '',
audience: longformListFromValue(product.audience),
tone: longformListFromValue(product.tone),
requiredClaims: longformListFromValue(product.requiredClaims),
forbiddenClaims: longformListFromValue(product.forbiddenClaims),
narrative: normalizeProductNarrative(product.narrative, product),
source: product.source || 'brand-product-profile',
updatedAt: product.updatedAt || product.createdAt || 'seed',
});
const normalizeLongformBrandProfiles = (brands = LONGFORM_BRANDS) => brands.map(brand => ({
...brand,
narrative: normalizeBrandNarrative(brand.narrative),
products: (Array.isArray(brand.products) ? brand.products : brand.id === 'montx' ? MONTX_PRODUCT_PROFILES : []).map(normalizeLongformProduct),
constraints: (brand.constraints || []).map(normalizeLongformConstraint),
}));
const normalizeSharedBrandProfiles = (brands) => {
const source = brands || window.BrandMemoryStore?.DEFAULT_BRAND_PROFILES || LONGFORM_BRANDS;
return window.BrandMemoryStore?.normalizeBrandProfiles
? window.BrandMemoryStore.normalizeBrandProfiles(source)
: normalizeLongformBrandProfiles(source);
};
const getLongformBrand = (brandId, brands) => {
const source = brands || window.BrandMemoryStore?.DEFAULT_BRAND_PROFILES || LONGFORM_BRANDS;
return window.BrandMemoryStore?.getBrandProfile
? window.BrandMemoryStore.getBrandProfile(brandId, source)
: source.find(b => b.id === brandId) || source[0] || LONGFORM_BRANDS[0];
};
const getLongformProduct = (brand, productId) => {
if (window.BrandMemoryStore?.getProductProfile) return window.BrandMemoryStore.getProductProfile(brand, productId);
const products = brand?.products || [];
return products.find(product => product.id === productId) || products[0] || null;
};
const inferLongformProductId = (task = {}, brand) => {
const text = `${task.productId || ''} ${task.title || ''} ${task.prompt || ''} ${(task.sections || []).map(section => section.title).join(' ')}`.toLowerCase();
return (brand?.products || []).find(product => {
const names = [product.name, ...(product.aliases || [])].filter(Boolean).map(item => item.toLowerCase());
return names.some(name => text.includes(name));
})?.id || '';
};
const normalizeLongformTask = (task, brands = LONGFORM_BRANDS) => {
const brand = getLongformBrand(task.brandId, brands);
const inferredProductId = inferLongformProductId(task, brand);
const product = getLongformProduct(brand, task.productId || inferredProductId);
return {
...task,
images: Array.isArray(task.images) ? task.images : [],
overrides: Array.isArray(task.overrides) ? task.overrides : [],
messages: Array.isArray(task.messages) ? task.messages : [],
productId: task.productId || product?.id || '',
narrativeMode: task.narrativeMode || 'brand_product',
sections: (task.sections || LONGFORM_DEFAULT_SECTIONS).map(normalizeLongformSection),
};
};
const parseSkillMarkdown = (markdown = '') => {
const title = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || 'Untitled Skill';
const hex = Array.from(new Set(markdown.match(/#[0-9A-Fa-f]{6}/g) || []));
const width = markdown.match(/width:\s*(\d+)px/)?.[1] || markdown.match(/画布限制.*?(\d+)px/)?.[1] || '1080';
const fontLines = (markdown.match(/(?:Helvetica|SourceHanSansCN|思源黑体|字体规范|font-weight)/g) || []).slice(0, 6);
const components = Array.from(markdown.matchAll(/^###\s+(.+)$/gm)).map(m => m[1].trim());
const bans = Array.from(markdown.matchAll(/(?:禁止|严禁|移除)([^。\n]+)/g)).map(m => m[0].trim()).slice(0, 8);
const delivery = Array.from(markdown.matchAll(/\*\*([^*]+)\*\*:([^。\n]+)/g)).map(m => `${m[1]}:${m[2]}`).filter(s => /Mandate|Interaction|Weight|交付|Text/.test(s));
return { title, hex, width, fontLines, components, bans, delivery, raw: markdown };
};
const LONGFORM_THEME_PRESETS = [
{ key: /Neo Pop|新波普/i, bg: '#F7EA2E', accent: '#F92672', text: '#101010', inverse: '#FFFFFF', font: 'Impact, Helvetica, Arial, "Noto Sans SC", sans-serif' },
{ key: /Nordic|北欧/i, bg: '#F4F0E8', accent: '#8FA99A', text: '#22302A', inverse: '#FFFFFF', font: 'Helvetica, Arial, "Noto Sans SC", sans-serif' },
{ key: /Vogue|高奢/i, bg: '#F7F2EA', accent: '#1A1A1A', text: '#17120E', inverse: '#F7F2EA', font: 'Georgia, "Times New Roman", "Noto Serif SC", serif' },
{ key: /Tech-Nomad|科技游牧/i, bg: '#071A1F', accent: '#00E0C7', text: '#EAFBF8', inverse: '#071A1F', font: 'Helvetica, Arial, "Noto Sans SC", sans-serif' },
{ key: /Clean Data|清洁数据/i, bg: '#F4F7F8', accent: '#126E82', text: '#102027', inverse: '#FFFFFF', font: 'Helvetica, Arial, "Noto Sans SC", sans-serif' },
{ key: /Cinematic|电影剧照/i, bg: '#111111', accent: '#B68D40', text: '#F2E8D8', inverse: '#111111', font: 'Georgia, "Times New Roman", "Noto Serif SC", serif' },
{ key: /Human Documentary|人文纪实/i, bg: '#EFE2D2', accent: '#9A3A2D', text: '#211A16', inverse: '#FFF8EC', font: 'Georgia, "Noto Serif SC", serif' },
{ key: /Archive Academia|档案学术/i, bg: '#ECE6D8', accent: '#2B3A67', text: '#1D2230', inverse: '#F7F1E5', font: 'Georgia, "Noto Serif SC", serif' },
{ key: /Organic Nature|自然有机/i, bg: '#F1EAD9', accent: '#587A46', text: '#263322', inverse: '#FFF8EA', font: 'Helvetica, Arial, "Noto Sans SC", sans-serif' },
{ key: /Bauhaus|包豪斯|MONTX/i, bg: '#E1E0D8', accent: '#580018', text: '#000000', inverse: '#FFFFFF', font: 'Helvetica, Arial, "Source Han Sans CN", "Noto Sans SC", sans-serif' },
];
const LONGFORM_LAYOUT_PRESETS = [
{ id: 'pop', className: 'lf-layout-pop', label: 'Neo Pop Poster', key: /Neo Pop|新波普/i },
{ id: 'nordic', className: 'lf-layout-nordic', label: 'Nordic Minimal', key: /Nordic|北欧/i },
{ id: 'vogue', className: 'lf-layout-vogue', label: 'Vogue Editorial', key: /Vogue|高奢/i },
{ id: 'tech', className: 'lf-layout-tech', label: 'Tech Nomad', key: /Tech-Nomad|科技游牧/i },
{ id: 'data', className: 'lf-layout-data', label: 'Clean Data Report', key: /Clean Data|清洁数据/i },
{ id: 'cinematic', className: 'lf-layout-cinematic', label: 'Cinematic Storyboard', key: /Cinematic|电影剧照/i },
{ id: 'documentary', className: 'lf-layout-documentary', label: 'Human Documentary', key: /Human Documentary|人文纪实/i },
{ id: 'archive', className: 'lf-layout-archive', label: 'Archive Academia', key: /Archive Academia|档案学术/i },
{ id: 'organic', className: 'lf-layout-organic', label: 'Organic Nature', key: /Organic Nature|自然有机/i },
{ id: 'bauhaus', className: 'lf-layout-bauhaus', label: 'Bauhaus Grid', key: /Bauhaus|包豪斯|MONTX/i },
];
const LONGFORM_FONT_SIZE_PRESETS = {
'lf-layout-bauhaus': { heroTitlePx: 118, leadPx: 38, sectionTitlePx: 54, bodyPx: 36, gridLeftPx: 64, gridRightPx: 34, footerTitlePx: 86, footerBodyPx: 34 },
'lf-layout-nordic': { heroTitlePx: 74, leadPx: 30, sectionTitlePx: 38, bodyPx: 30, gridLeftPx: 38, gridRightPx: 29, footerTitlePx: 48, footerBodyPx: 34 },
'lf-layout-pop': { heroTitlePx: 126, leadPx: 34, sectionTitlePx: 48, bodyPx: 31, gridLeftPx: 58, gridRightPx: 30, footerTitlePx: 86, footerBodyPx: 34 },
'lf-layout-vogue': { heroTitlePx: 92, leadPx: 28, sectionTitlePx: 46, bodyPx: 29, gridLeftPx: 58, gridRightPx: 28, footerTitlePx: 86, footerBodyPx: 34 },
'lf-layout-tech': { heroTitlePx: 96, leadPx: 30, sectionTitlePx: 34, bodyPx: 28, gridLeftPx: 42, gridRightPx: 28, footerTitlePx: 86, footerBodyPx: 34 },
'lf-layout-data': { heroTitlePx: 76, leadPx: 28, sectionTitlePx: 32, bodyPx: 27, gridLeftPx: 38, gridRightPx: 27, footerTitlePx: 86, footerBodyPx: 34 },
'lf-layout-cinematic': { heroTitlePx: 88, leadPx: 28, sectionTitlePx: 32, bodyPx: 29, gridLeftPx: 46, gridRightPx: 28, footerTitlePx: 86, footerBodyPx: 34 },
'lf-layout-documentary': { heroTitlePx: 82, leadPx: 30, sectionTitlePx: 40, bodyPx: 30, gridLeftPx: 44, gridRightPx: 29, footerTitlePx: 86, footerBodyPx: 34 },
'lf-layout-archive': { heroTitlePx: 70, leadPx: 28, sectionTitlePx: 36, bodyPx: 28, gridLeftPx: 34, gridRightPx: 27, footerTitlePx: 86, footerBodyPx: 34 },
'lf-layout-organic': { heroTitlePx: 78, leadPx: 30, sectionTitlePx: 38, bodyPx: 30, gridLeftPx: 44, gridRightPx: 29, footerTitlePx: 86, footerBodyPx: 34 },
};
const LONGFORM_LAYOUT_CSS = `
.longform-render .montx-giant,.longform-render .montx-section-title,.longform-render .montx-footer h2{letter-spacing:0;}
.longform-render.lf-text-center .montx-giant,.longform-render.lf-text-center .montx-lead,.longform-render.lf-text-center .montx-section-title,.longform-render.lf-text-center .montx-body,.longform-render.lf-text-center .montx-grid-left,.longform-render.lf-text-center .montx-grid-right,.longform-render.lf-text-center .montx-footer{text-align:center;}
.longform-render.lf-text-center .montx-section-title,.longform-render.lf-text-center .montx-body,.longform-render.lf-text-center .montx-lead{margin-left:auto;margin-right:auto;}
.longform-render.lf-text-center .montx-body,.longform-render.lf-text-center .montx-grid-right{border-left:0;padding-left:0;}
.longform-render.lf-layout-nordic .montx-topline{height:0;}
.longform-render.lf-layout-nordic .montx-giant{padding:96px 110px 18px;font-size:74px;line-height:1.08;font-weight:400;text-transform:none;}
.longform-render.lf-layout-nordic .montx-rule{height:1px;margin:0 110px 42px;background:color-mix(in oklab,var(--lf-accent) 45%,var(--lf-bg));}
.longform-render.lf-layout-nordic .montx-lead{padding:0 110px 96px;font-size:30px;line-height:1.75;}
.longform-render.lf-layout-nordic .montx-narrative{min-height:0;padding:96px 120px;}
.longform-render.lf-layout-nordic .montx-watermark{display:none;}
.longform-render.lf-layout-nordic .montx-section-title{display:block;background:transparent;color:var(--lf-text);padding:0;margin:0 0 28px;font-size:38px;font-weight:400;border-bottom:1px solid color-mix(in oklab,var(--lf-accent) 35%,var(--lf-bg));padding-bottom:22px;}
.longform-render.lf-layout-nordic .montx-body{border-left:0;padding-left:0;font-size:30px;line-height:1.85;}
.longform-render.lf-layout-nordic .montx-grid{display:block;border:0;padding:92px 110px;background:color-mix(in oklab,var(--lf-accent) 8%,var(--lf-bg));}
.longform-render.lf-layout-nordic .montx-grid-left{background:transparent;color:var(--lf-text);font-size:38px;padding:0 0 28px;font-weight:400;}
.longform-render.lf-layout-nordic .montx-grid-right{padding:0;font-size:29px;line-height:1.8;}
.longform-render.lf-layout-nordic .montx-footer{padding:92px 110px;background:var(--lf-bg);color:var(--lf-text);border-top:1px solid color-mix(in oklab,var(--lf-accent) 35%,var(--lf-bg));}
.longform-render.lf-layout-nordic .montx-footer h2{font-size:48px;font-weight:400;}
.longform-render.lf-layout-pop .montx-topline{height:42px;background:repeating-linear-gradient(90deg,var(--lf-accent) 0 90px,var(--lf-text) 90px 108px);}
.longform-render.lf-layout-pop .montx-giant{padding:56px 48px 22px;font-size:126px;line-height:.88;text-transform:uppercase;}
.longform-render.lf-layout-pop .montx-rule{height:0;margin:0 48px 24px;border-top:16px solid var(--lf-text);border-bottom:16px solid var(--lf-accent);}
.longform-render.lf-layout-pop .montx-lead{padding:28px 48px 72px;font-size:34px;line-height:1.35;background:var(--lf-text);color:var(--lf-inverse);}
.longform-render.lf-layout-pop .montx-narrative{padding:72px 54px;min-height:340px;border-top:10px solid var(--lf-text);}
.longform-render.lf-layout-pop .montx-watermark{right:34px;top:18px;font-size:180px;color:color-mix(in oklab,var(--lf-accent) 30%,transparent);}
.longform-render.lf-layout-pop .montx-section-title{font-size:48px;border:8px solid var(--lf-text);box-shadow:12px 12px 0 var(--lf-text);transform:rotate(-1deg);margin-bottom:44px;}
.longform-render.lf-layout-pop .montx-body{border-left:0;padding:34px;font-size:31px;line-height:1.5;background:color-mix(in oklab,var(--lf-inverse) 88%,var(--lf-bg));outline:6px solid var(--lf-text);}
.longform-render.lf-layout-pop .montx-grid{grid-template-columns:5fr 7fr;border:12px solid var(--lf-text);}
.longform-render.lf-layout-pop .montx-grid-left{font-size:58px;padding:48px 36px;text-transform:uppercase;}
.longform-render.lf-layout-pop .montx-grid-right{font-size:30px;padding:48px 42px;background:var(--lf-bg);}
.longform-render.lf-layout-pop .montx-footer{border-top:18px solid var(--lf-text);}
.longform-render.lf-layout-vogue .montx-topline{height:0;}
.longform-render.lf-layout-vogue .montx-giant{padding:116px 88px 28px;font-size:92px;line-height:1.02;font-weight:400;text-transform:uppercase;}
.longform-render.lf-layout-vogue .montx-rule{width:180px;height:1px;margin:0 88px 42px;background:var(--lf-text);}
.longform-render.lf-layout-vogue .montx-lead{padding:0 88px 120px;font-size:28px;line-height:1.85;max-width:760px;}
.longform-render.lf-layout-vogue .montx-image{margin:0 0 72px;}
.longform-render.lf-layout-vogue .montx-narrative{padding:104px 88px 116px;min-height:0;}
.longform-render.lf-layout-vogue .montx-watermark{font-size:92px;right:88px;top:92px;color:color-mix(in oklab,var(--lf-text) 10%,transparent);}
.longform-render.lf-layout-vogue .montx-section-title{background:transparent;color:var(--lf-text);font-size:46px;font-weight:400;padding:0;margin:0 0 46px;max-width:720px;}
.longform-render.lf-layout-vogue .montx-body{border-left:0;padding-left:0;font-size:29px;line-height:1.9;max-width:720px;}
.longform-render.lf-layout-vogue .montx-grid{display:block;border:0;padding:104px 88px;background:color-mix(in oklab,var(--lf-text) 5%,var(--lf-bg));}
.longform-render.lf-layout-vogue .montx-grid-left{background:transparent;color:var(--lf-text);font-size:58px;font-weight:400;padding:0 0 42px;}
.longform-render.lf-layout-vogue .montx-grid-right{padding:0 0 0 180px;font-size:28px;line-height:1.85;border-left:1px solid var(--lf-text);}
.longform-render.lf-layout-vogue .montx-footer{padding:110px 88px;background:var(--lf-text);color:var(--lf-bg);}
.longform-render.lf-layout-tech{background:linear-gradient(180deg,var(--lf-bg),color-mix(in oklab,var(--lf-accent) 12%,var(--lf-bg)));}
.longform-render.lf-layout-tech .montx-topline{height:18px;}
.longform-render.lf-layout-tech .montx-giant{padding:58px 56px 22px;font-size:96px;line-height:.95;text-transform:uppercase;border-left:18px solid var(--lf-accent);}
.longform-render.lf-layout-tech .montx-rule{height:2px;margin:0 56px 34px;background:var(--lf-accent);}
.longform-render.lf-layout-tech .montx-lead{padding:0 56px 78px;font-size:30px;line-height:1.6;}
.longform-render.lf-layout-tech .montx-narrative{padding:70px 56px;min-height:320px;border-top:1px solid color-mix(in oklab,var(--lf-accent) 50%,var(--lf-bg));}
.longform-render.lf-layout-tech .montx-watermark{font-size:148px;color:color-mix(in oklab,var(--lf-accent) 20%,transparent);}
.longform-render.lf-layout-tech .montx-section-title{background:transparent;color:var(--lf-accent);border:1px solid var(--lf-accent);font-size:34px;padding:14px 18px;}
.longform-render.lf-layout-tech .montx-body{border-left:2px solid var(--lf-accent);font-size:28px;line-height:1.7;}
.longform-render.lf-layout-tech .montx-grid{grid-template-columns:3fr 9fr;border-top:1px solid var(--lf-accent);border-bottom:1px solid var(--lf-accent);}
.longform-render.lf-layout-tech .montx-grid-left{font-size:42px;padding:42px 32px;}
.longform-render.lf-layout-tech .montx-grid-right{font-size:28px;padding:42px 46px;}
.longform-render.lf-layout-data .montx-topline{height:12px;}
.longform-render.lf-layout-data .montx-giant{padding:60px 64px 20px;font-size:76px;line-height:1.02;text-transform:none;}
.longform-render.lf-layout-data .montx-rule{height:1px;margin:0 64px 28px;}
.longform-render.lf-layout-data .montx-lead{padding:0 64px 58px;font-size:28px;line-height:1.6;}
.longform-render.lf-layout-data .montx-narrative{padding:54px 64px;min-height:260px;border-top:1px solid color-mix(in oklab,var(--lf-accent) 25%,var(--lf-bg));}
.longform-render.lf-layout-data .montx-watermark{font-size:72px;right:64px;top:54px;color:color-mix(in oklab,var(--lf-accent) 18%,transparent);}
.longform-render.lf-layout-data .montx-section-title{background:transparent;color:var(--lf-accent);font-size:32px;padding:0;margin-bottom:20px;text-transform:uppercase;}
.longform-render.lf-layout-data .montx-body{border-left:0;padding:26px 30px;font-size:27px;line-height:1.65;background:color-mix(in oklab,var(--lf-accent) 7%,var(--lf-bg));border-top:4px solid var(--lf-accent);}
.longform-render.lf-layout-data .montx-grid{grid-template-columns:4fr 8fr;border-top:4px solid var(--lf-accent);border-bottom:1px solid var(--lf-accent);}
.longform-render.lf-layout-data .montx-grid-left{font-size:38px;padding:36px;}
.longform-render.lf-layout-data .montx-grid-right{font-size:27px;padding:36px;}
.longform-render.lf-layout-cinematic .montx-topline{height:0;}
.longform-render.lf-layout-cinematic .montx-giant{padding:110px 70px 28px;font-size:88px;line-height:.98;text-transform:uppercase;}
.longform-render.lf-layout-cinematic .montx-rule{height:2px;margin:0 70px 36px;background:var(--lf-accent);}
.longform-render.lf-layout-cinematic .montx-lead{padding:0 70px 104px;font-size:28px;line-height:1.8;}
.longform-render.lf-layout-cinematic .montx-image{border-top:48px solid #000;border-bottom:48px solid #000;}
.longform-render.lf-layout-cinematic .montx-narrative{padding:88px 70px;min-height:360px;}
.longform-render.lf-layout-cinematic .montx-watermark{font-size:64px;right:70px;top:52px;color:var(--lf-accent);}
.longform-render.lf-layout-cinematic .montx-section-title{background:transparent;color:var(--lf-accent);font-size:32px;padding:0;margin-bottom:34px;text-transform:uppercase;}
.longform-render.lf-layout-cinematic .montx-body{border-left:0;padding:26px 0 0;font-size:29px;line-height:1.8;border-top:1px solid var(--lf-accent);}
.longform-render.lf-layout-cinematic .montx-grid{display:block;border:0;padding:82px 70px;background:#000;}
.longform-render.lf-layout-cinematic .montx-grid-left{background:transparent;color:var(--lf-accent);font-size:46px;padding:0 0 30px;}
.longform-render.lf-layout-cinematic .montx-grid-right{padding:0;font-size:28px;line-height:1.75;color:var(--lf-text);}
.longform-render.lf-layout-documentary .montx-topline{height:10px;}
.longform-render.lf-layout-documentary .montx-giant{padding:72px 70px 28px;font-size:82px;line-height:1.05;text-transform:none;font-weight:400;}
.longform-render.lf-layout-documentary .montx-rule{height:3px;margin:0 70px 34px;}
.longform-render.lf-layout-documentary .montx-lead{padding:0 70px 84px;font-size:30px;line-height:1.75;}
.longform-render.lf-layout-documentary .montx-narrative{padding:76px 70px;min-height:330px;}
.longform-render.lf-layout-documentary .montx-watermark{left:70px;right:auto;top:54px;font-size:60px;color:var(--lf-accent);}
.longform-render.lf-layout-documentary .montx-section-title{background:transparent;color:var(--lf-text);font-size:40px;padding:0 0 0 92px;margin-bottom:26px;}
.longform-render.lf-layout-documentary .montx-body{border-left:0;padding-left:92px;font-size:30px;line-height:1.78;}
.longform-render.lf-layout-documentary .montx-grid{display:block;border:0;padding:76px 70px;border-top:6px solid var(--lf-accent);}
.longform-render.lf-layout-documentary .montx-grid-left{background:transparent;color:var(--lf-accent);font-size:44px;padding:0 0 26px;}
.longform-render.lf-layout-documentary .montx-grid-right{padding:0;font-size:29px;line-height:1.75;}
.longform-render.lf-layout-archive .montx-topline{height:16px;}
.longform-render.lf-layout-archive .montx-giant{padding:70px 62px 22px;font-size:70px;line-height:1.08;text-transform:uppercase;border-bottom:1px solid var(--lf-accent);}
.longform-render.lf-layout-archive .montx-rule{height:0;margin:0 62px 34px;}
.longform-render.lf-layout-archive .montx-lead{padding:0 62px 74px;font-size:28px;line-height:1.75;}
.longform-render.lf-layout-archive .montx-narrative{padding:66px 62px;min-height:310px;border-top:1px dashed var(--lf-accent);}
.longform-render.lf-layout-archive .montx-watermark{left:62px;right:auto;top:42px;font-size:46px;color:var(--lf-accent);}
.longform-render.lf-layout-archive .montx-section-title{background:transparent;color:var(--lf-text);font-size:36px;padding:0 0 0 78px;margin-bottom:22px;text-decoration:underline;text-underline-offset:8px;}
.longform-render.lf-layout-archive .montx-body{border-left:1px solid var(--lf-accent);padding-left:26px;margin-left:78px;font-size:28px;line-height:1.75;}
.longform-render.lf-layout-archive .montx-grid{grid-template-columns:3fr 9fr;border:1px solid var(--lf-accent);}
.longform-render.lf-layout-archive .montx-grid-left{font-size:34px;padding:34px;background:transparent;color:var(--lf-accent);border-right:1px solid var(--lf-accent);}
.longform-render.lf-layout-archive .montx-grid-right{font-size:27px;padding:34px;}
.longform-render.lf-layout-organic .montx-topline{height:0;}
.longform-render.lf-layout-organic .montx-giant{padding:82px 82px 24px;font-size:78px;line-height:1.08;text-transform:none;font-weight:500;}
.longform-render.lf-layout-organic .montx-rule{height:10px;width:160px;margin:0 82px 36px;border-radius:20px;background:var(--lf-accent);}
.longform-render.lf-layout-organic .montx-lead{padding:0 82px 88px;font-size:30px;line-height:1.78;}
.longform-render.lf-layout-organic .montx-image{padding:0 44px 44px;box-sizing:border-box;}
.longform-render.lf-layout-organic .montx-narrative{padding:82px;min-height:320px;}
.longform-render.lf-layout-organic .montx-watermark{font-size:120px;color:color-mix(in oklab,var(--lf-accent) 16%,transparent);}
.longform-render.lf-layout-organic .montx-section-title{background:color-mix(in oklab,var(--lf-accent) 16%,var(--lf-bg));color:var(--lf-text);font-size:38px;padding:18px 24px;margin-bottom:30px;}
.longform-render.lf-layout-organic .montx-body{border-left:8px solid var(--lf-accent);font-size:30px;line-height:1.78;}
.longform-render.lf-layout-organic .montx-grid{display:block;border:0;padding:82px;background:color-mix(in oklab,var(--lf-accent) 10%,var(--lf-bg));}
.longform-render.lf-layout-organic .montx-grid-left{background:transparent;color:var(--lf-accent);font-size:44px;padding:0 0 28px;}
.longform-render.lf-layout-organic .montx-grid-right{padding:0;font-size:29px;line-height:1.78;}
`;
const hexToRgb = (hex = '#580018') => {
const clean = hex.replace('#', '');
const value = clean.length === 3 ? clean.split('').map(ch => ch + ch).join('') : clean;
const num = Number.parseInt(value, 16);
return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255,
};
};
const getLongformTheme = (markdown = MONTX_DEFAULT_SKILL) => {
const skill = typeof markdown === 'string' ? parseSkillMarkdown(markdown) : markdown;
const preset = LONGFORM_THEME_PRESETS.find(item => item.key.test(skill.title)) || LONGFORM_THEME_PRESETS[LONGFORM_THEME_PRESETS.length - 1];
const bg = skill.hex[0] || preset.bg;
const accent = skill.hex[1] || preset.accent;
const text = skill.hex.find(color => ![bg, accent].includes(color)) || preset.text;
const inverse = preset.inverse;
const rgb = hexToRgb(accent);
return {
bg,
accent,
text,
inverse,
font: preset.font,
watermark: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.1)`,
};
};
const getLongformLayoutPreset = (markdown = MONTX_DEFAULT_SKILL) => {
const skill = typeof markdown === 'string' ? parseSkillMarkdown(markdown) : markdown;
return LONGFORM_LAYOUT_PRESETS.find(item => item.key.test(skill.title)) || LONGFORM_LAYOUT_PRESETS[LONGFORM_LAYOUT_PRESETS.length - 1];
};
const getLongformThemeVars = (markdown = MONTX_DEFAULT_SKILL) => {
const theme = getLongformTheme(markdown);
return {
'--lf-bg': theme.bg,
'--lf-accent': theme.accent,
'--lf-text': theme.text,
'--lf-inverse': theme.inverse,
'--lf-watermark': theme.watermark,
'--lf-font': theme.font,
};
};
const getLongformHeroSection = (task) => (task.sections || []).find(section => section.type === 'Header') || (task.sections || [])[0] || {};
const getLongformTextUnits = (text = '') => Array.from(String(text)).reduce((sum, ch) => {
if (/[,。?!:;、,.!?;:\s]/.test(ch)) return sum + 0.45;
if (/[\x00-\xff]/.test(ch)) return sum + 0.55;
return sum + 1;
}, 0);
const getLongformSingleLineHeroPx = (title = '') => {
const units = Math.max(1, getLongformTextUnits(title));
return Math.max(46, Math.min(118, Math.floor((900 / units) * 0.96)));
};
const getLongformHeroTitleStyle = (task = {}) => {
const overrides = task.layoutOverrides || {};
if (!overrides.heroTitleSingleLine) return {};
return {
fontSize: `${overrides.heroTitleFontPx || getLongformSingleLineHeroPx(getLongformHeroSection(task).title)}px`,
whiteSpace: 'nowrap',
letterSpacing: '0',
};
};
const getLongformRenderOverrideClass = (task = {}) => task.layoutOverrides?.textAlign === 'center' ? 'lf-text-center' : '';
const getLongformRenderTypography = (task = {}) => {
const layout = getLongformLayoutPreset(task.skillMarkdown || MONTX_DEFAULT_SKILL);
const preset = LONGFORM_FONT_SIZE_PRESETS[layout.className] || LONGFORM_FONT_SIZE_PRESETS['lf-layout-bauhaus'];
return {
...preset,
heroTitlePx: task.layoutOverrides?.heroTitleSingleLine
? (task.layoutOverrides.heroTitleFontPx || getLongformSingleLineHeroPx(getLongformHeroSection(task).title))
: preset.heroTitlePx,
layoutId: layout.id,
layoutLabel: layout.label,
};
};
const extractLongformTypographyRules = (task = {}, brand = LONGFORM_BRANDS[0]) => {
const markdown = task.skillMarkdown || MONTX_DEFAULT_SKILL;
const rules = {};
const sources = [];
const heroMin = markdown.match(/(?:大标题|主标题|标题)[^\n\d]{0,24}(\d+)\s*px\s*\+/i)?.[1];
const bodyRange = markdown.match(/正文[^\n\d]{0,24}(\d+)\s*px\s*(?:-|到|至|~)\s*(\d+)\s*px?/i);
const sectionRange = markdown.match(/(?:小标题|章节标题)[^\n\d]{0,24}(\d+)\s*px\s*(?:-|到|至|~)\s*(\d+)\s*px?/i);
if (heroMin) {
rules.heroTitleMinPx = Number(heroMin);
sources.push(`style skill: 大标题 ${heroMin}px+`);
}
if (bodyRange) {
rules.bodyFontMinPx = Number(bodyRange[1]);
rules.bodyFontMaxPx = Number(bodyRange[2]);
sources.push(`style skill: 正文 ${bodyRange[1]}-${bodyRange[2]}px`);
}
if (sectionRange) {
rules.sectionTitleMinPx = Number(sectionRange[1]);
rules.sectionTitleMaxPx = Number(sectionRange[2]);
sources.push(`style skill: 小标题 ${sectionRange[1]}-${sectionRange[2]}px`);
}
getActiveLongformConstraints(brand).forEach(rule => {
if (!rule.rules) return;
if (rule.rules.heroTitleMinPx) {
rules.heroTitleMinPx = rule.rules.heroTitleMinPx;
sources.push(`品牌护栏「${rule.label}」: 主标题 ${rule.rules.heroTitleMinPx}px+`);
}
if (rule.rules.bodyFontMinPx && rule.rules.bodyFontMaxPx) {
rules.bodyFontMinPx = rule.rules.bodyFontMinPx;
rules.bodyFontMaxPx = rule.rules.bodyFontMaxPx;
sources.push(`品牌护栏「${rule.label}」: 正文 ${rule.rules.bodyFontMinPx}-${rule.rules.bodyFontMaxPx}px`);
}
});
const normalBodyRule = getActiveLongformConstraints(brand).find(rule => /正文.*加粗|font-weight|单字加粗|normal/i.test(`${rule.label} ${rule.description}`));
if (normalBodyRule) sources.push(`品牌护栏「${normalBodyRule.label}」: 正文 normal,不做局部加粗`);
return { rules, sources };
};
const buildLongformTypographyAudit = (task = {}, brand = LONGFORM_BRANDS[0]) => {
const typography = getLongformRenderTypography(task);
const { rules, sources } = extractLongformTypographyRules(task, brand);
const issues = [];
if (rules.heroTitleMinPx && typography.heroTitlePx < rules.heroTitleMinPx) {
issues.push(`主标题 ${typography.heroTitlePx}px 低于 ${rules.heroTitleMinPx}px+`);
}
const bodyTargets = [
['正文', typography.bodyPx],
['网格正文', typography.gridRightPx],
['引言', typography.leadPx],
];
bodyTargets.forEach(([label, px]) => {
if (rules.bodyFontMinPx && px < rules.bodyFontMinPx) issues.push(`${label} ${px}px 低于正文下限 ${rules.bodyFontMinPx}px`);
if (rules.bodyFontMaxPx && px > rules.bodyFontMaxPx) issues.push(`${label} ${px}px 高于正文上限 ${rules.bodyFontMaxPx}px`);
});
if (rules.sectionTitleMinPx && typography.sectionTitlePx < rules.sectionTitleMinPx) {
issues.push(`小标题 ${typography.sectionTitlePx}px 低于 ${rules.sectionTitleMinPx}px`);
}
if (rules.sectionTitleMaxPx && typography.sectionTitlePx > rules.sectionTitleMaxPx) {
issues.push(`小标题 ${typography.sectionTitlePx}px 高于 ${rules.sectionTitleMaxPx}px`);
}
return { typography, rules, sources, issues };
};
const LONGFORM_PR_REVIEW_RULES = [
{ level: 'S3', token: '玻璃心', label: '不要默认用户玻璃心' },
{ level: 'S2', token: '你误会了', label: '不要和用户争夺解释权' },
{ level: 'S2', token: '本意不是', label: '避免先解释品牌本意' },
{ level: 'S2', token: '过度解读', label: '避免指责用户解读' },
{ level: 'S2', token: '牛马', label: '品牌不能替用户自嘲' },
{ level: 'S2', token: '买不起', label: '避免轻视用户购买能力' },
{ level: 'S2', token: '秒杀', label: '高风险促销/比较表达' },
{ level: 'S2', token: '解放双手', label: '智能驾驶高风险承诺' },
{ level: 'S2', token: '完全自动驾驶', label: '自动驾驶边界风险' },
{ level: 'S1', token: '很快', label: '模糊能力表达,建议量化' },
];
const buildLongformPrAudit = (task = {}) => {
const text = [
task.title || '',
task.prompt || '',
...(task.sections || []).flatMap(section => [section.title || '', section.body || '']),
].join('\n');
const hits = LONGFORM_PR_REVIEW_RULES.filter(rule => text.includes(rule.token));
const rank = { S0: 0, S1: 1, S2: 2, S3: 3, S4: 4 };
const level = hits.reduce((max, hit) => rank[hit.level] > rank[max] ? hit.level : max, 'S0');
const action = {
S0: '无明显舆情风险,仅需监测',
S1: '轻微争议,建议优化表达',
S2: '中度舆情风险,建议修改内容并准备回应',
S3: '严重舆情风险,建议统一口径并正式说明',
S4: '重大危机,需启动危机公关机制',
}[level];
return { level, hits, action };
};
const styleObjectToHtml = (style = {}) => {
const css = Object.entries(style).map(([key, value]) => `${key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}:${value}`).join(';');
return css ? ` style="${escapeHtml(css)}"` : '';
};
const buildLongformThemeCss = (markdown = MONTX_DEFAULT_SKILL, selector = '.montx') => {
const theme = getLongformTheme(markdown);
return `${selector}{--lf-bg:${theme.bg};--lf-accent:${theme.accent};--lf-text:${theme.text};--lf-inverse:${theme.inverse};--lf-watermark:${theme.watermark};--lf-font:${theme.font};}`;
};
const buildLongformHtml = (task, brand) => {
const theme = getLongformTheme(task.skillMarkdown || MONTX_DEFAULT_SKILL);
const layout = getLongformLayoutPreset(task.skillMarkdown || MONTX_DEFAULT_SKILL);
const overrideClass = getLongformRenderOverrideClass(task);
const cssSource = `${MONTX_EXPORT_CSS}\n${LONGFORM_LAYOUT_CSS}`;
const css = `body{margin:0;background:${theme.bg};}${buildLongformThemeCss(task.skillMarkdown || MONTX_DEFAULT_SKILL)}${cssSource
.replaceAll('.montx-canvas', '.montx')
.replaceAll('.montx-topline', '.topline')
.replaceAll('.montx-giant', '.giant')
.replaceAll('.montx-rule', '.rule')
.replaceAll('.montx-lead', '.lead')
.replaceAll('.montx-image', '.image')
.replaceAll('.montx-narrative', '.narrative')
.replaceAll('.montx-watermark', '.watermark')
.replaceAll('.montx-section-title', '.section-title')
.replaceAll('.montx-body', '.body')
.replaceAll('.montx-grid-left', '.grid-left')
.replaceAll('.montx-grid-right', '.grid-right')
.replaceAll('.montx-grid', '.grid')
.replaceAll('.montx-footer', '.footer')}`;
const sections = task.sections.map((s, i) => {
const imgs = task.images.filter(img => (s.imageIds || []).includes(img.id));
if (s.type === 'Header') {
return `
${escapeHtml(s.title)}
${escapeHtml(s.body)}
${buildLongformImageHtml(imgs)}`;
}
if (s.type === 'Image') {
return imgs.length
? buildLongformImageHtml(imgs)
: `${String(i).padStart(2, '0')}
${escapeHtml(s.title)}
${escapeHtml(s.body)}
[ 图片待上传并分配 ]
`;
}
if (s.type === 'Grid') {
return `${escapeHtml(s.title)}
${escapeHtml(s.body)}
${buildLongformImageHtml(imgs)}`;
}
if (s.type === 'Footer') {
return `${buildLongformImageHtml(imgs)}`;
}
return `${String(i).padStart(2, '0')}
${escapeHtml(s.title)}
${escapeHtml(s.body)}
${buildLongformImageHtml(imgs)}`;
}).join('\n');
return `${escapeHtml(task.title)}${sections}`;
};
const getLongformGuardrailInput = (task = {}) => [
task.title,
task.prompt,
task.briefing?.coreQuestion,
task.briefing?.tone,
...(task.sections || []).flatMap(section => [section.title, section.body]),
].filter(Boolean).join('\n');
const calculateLongformQa = (task, brand) => {
const validSectionIds = new Set((task.sections || []).map(section => section.id));
const unassigned = (task.images || []).filter(img => !img.assignedSectionId || !validSectionIds.has(img.assignedSectionId)).length;
const overrides = (task.overrides || []).length;
const widthIssue = getActiveLongformConstraints(brand).find(c => c.id === 'canvas-1080') ? 0 : 1;
const typographyAudit = buildLongformTypographyAudit(task, brand);
const prAudit = buildLongformPrAudit(task);
const guardrailAudit = window.BrandMemoryStore?.runBrandGuardrails
? window.BrandMemoryStore.runBrandGuardrails(getLongformGuardrailInput(task), {
brand,
brandId: brand?.id,
productId: task.productId,
campaignId: task.campaignId,
campaignName: task.campaignName,
scope: 'longform',
})
: { violations: [], requiredFixes: [] };
const normalizedSections = (task.sections || []).map(normalizeLongformSection);
const blockIds = new Set(normalizedSections.map(section => section.blockType));
const issues = [];
if (unassigned) issues.push(`${unassigned} 张图片未分配 section`);
if (overrides) issues.push(`${overrides} 项品牌护栏已申请解除`);
if (widthIssue) issues.push('缺少 1080px 画布规则');
if (!task.skillMarkdown) issues.push('尚未上传或选择 markdown skill');
if (!blockIds.has('hero')) issues.push('缺少开场 Hero 模块');
if (!blockIds.has('cta')) issues.push('缺少收束 CTA 模块');
if (['product', 'brand_product'].includes(task.narrativeMode || 'brand_product') && !getLongformProduct(brand, task.productId)) {
issues.push('叙事模式需要绑定产品 Profile');
}
typographyAudit.issues.forEach(issue => issues.push(`字体约束:${issue}`));
if (!['S0', 'S1'].includes(prAudit.level)) issues.push(`PR 舆情审核:${prAudit.level} ${prAudit.action}`);
guardrailAudit.violations.forEach(item => issues.push(`品牌护栏:${item.message}`));
return {
score: Math.max(52, 96 - unassigned * 6 - overrides * 10 - (task.skillMarkdown ? 0 : 18) - typographyAudit.issues.length * 6 - guardrailAudit.requiredFixes.length * 10 - (prAudit.level === 'S1' ? 4 : prAudit.level === 'S2' ? 10 : prAudit.level === 'S3' ? 18 : prAudit.level === 'S4' ? 28 : 0) - (!blockIds.has('hero') ? 8 : 0) - (!blockIds.has('cta') ? 6 : 0) - ((['product', 'brand_product'].includes(task.narrativeMode || 'brand_product') && !getLongformProduct(brand, task.productId)) ? 8 : 0)),
issues,
exportReady: Boolean(task.sections.length && task.skillMarkdown),
guardrailAudit,
};
};
const buildLongformInspect = (task, brand, skill) => {
const sections = (task.sections || []).map(normalizeLongformSection);
const validSectionIds = new Set(sections.map(section => section.id));
const assignedImages = (task.images || []).filter(img => img.assignedSectionId && validSectionIds.has(img.assignedSectionId));
const unassignedImages = (task.images || []).filter(img => !img.assignedSectionId || !validSectionIds.has(img.assignedSectionId));
const blockIds = new Set(sections.map(section => section.blockType));
const typographyAudit = buildLongformTypographyAudit(task, brand);
const prAudit = buildLongformPrAudit(task);
const guardrailAudit = window.BrandMemoryStore?.runBrandGuardrails
? window.BrandMemoryStore.runBrandGuardrails(getLongformGuardrailInput(task), {
brand,
brandId: brand?.id,
productId: task.productId,
campaignId: task.campaignId,
campaignName: task.campaignName,
scope: 'longform',
})
: { passed: true, violations: [], checkedRules: getActiveLongformConstraints(brand).length };
const makeItem = (label, value, state = 'ok', detail = '') => ({ label, value, state, detail });
const groups = [
{
id: 'brief',
title: 'Brief Readiness',
items: [
makeItem('标题', task.title || '未命名', task.title ? 'ok' : 'warn'),
makeItem('结构化 Brief', task.briefing ? '已解析' : '未导入', task.briefing ? 'ok' : 'warn', task.briefing?.coreQuestion || '可先导入/粘贴 Brief 以提高稳定性'),
makeItem('叙事模式', getLongformNarrativeMode(task.narrativeMode).label, 'ok'),
makeItem('产品 Profile', getLongformProduct(brand, task.productId)?.name || '未绑定', getLongformProduct(brand, task.productId) ? 'ok' : 'warn'),
],
},
{
id: 'dsl',
title: 'Block DSL',
items: [
makeItem('Section 数量', `${sections.length}`, sections.length >= 5 && sections.length <= 8 ? 'ok' : 'warn', '建议 5-8 个 section'),
makeItem('开场 Hero', blockIds.has('hero') ? '存在' : '缺失', blockIds.has('hero') ? 'ok' : 'warn'),
makeItem('收束 CTA', blockIds.has('cta') ? '存在' : '缺失', blockIds.has('cta') ? 'ok' : 'warn'),
makeItem('模块类型', `${blockIds.size} 类`, blockIds.size >= 3 ? 'ok' : 'warn', '建议包含叙事、证据/图片、信息或 CTA 等类型'),
],
},
{
id: 'image',
title: 'Image Mapping',
items: [
makeItem('图片总数', `${(task.images || []).length}`, (task.images || []).length ? 'ok' : 'warn'),
makeItem('已分配', `${assignedImages.length}/${(task.images || []).length}`, unassignedImages.length ? 'warn' : 'ok'),
makeItem('图片全通栏', getActiveLongformConstraints(brand).some(rule => rule.id === 'full-bleed-image') ? '护栏启用' : '未启用', getActiveLongformConstraints(brand).some(rule => rule.id === 'full-bleed-image') ? 'ok' : 'warn'),
makeItem('图片压字', '未检测到', 'ok'),
],
},
{
id: 'wechat',
title: 'WeChat Compatibility',
items: [
makeItem('画布宽度', `${skill.width || 1080}px`, String(skill.width || '1080') === '1080' ? 'ok' : 'warn'),
makeItem('单文件 HTML', getActiveLongformConstraints(brand).some(rule => rule.id === 'single-file-html') ? '护栏启用' : '未启用', getActiveLongformConstraints(brand).some(rule => rule.id === 'single-file-html') ? 'ok' : 'warn'),
makeItem('公众号 HTML 复制', '未接入', 'idle', '当前仅导出整张 PNG,富文本复制留到下一步'),
makeItem('PNG 长图', task.html ? '可导出' : '待生成', task.html ? 'ok' : 'warn'),
],
},
{
id: 'policy',
title: 'Brand QA',
items: [
makeItem('品牌护栏', `${guardrailAudit.checkedRules} active`, guardrailAudit.violations.length ? 'warn' : 'ok', guardrailAudit.violations.map(item => item.message).join(';')),
makeItem('Override', `${(task.overrides || []).length}`, (task.overrides || []).length ? 'warn' : 'ok'),
makeItem('字号约束', typographyAudit.issues.length ? `${typographyAudit.issues.length} risks` : '通过', typographyAudit.issues.length ? 'warn' : 'ok', typographyAudit.issues.join(';')),
makeItem('QA Score', `${task.qa?.score || 0}`, (task.qa?.score || 0) >= 80 ? 'ok' : 'warn'),
],
},
{
id: 'pr',
title: 'PR Reputation QA',
items: [
makeItem('PR Skill', 'Chief Public Relations Officer', 'ok'),
makeItem('舆情等级', prAudit.level, ['S0', 'S1'].includes(prAudit.level) ? 'ok' : 'warn', prAudit.action),
makeItem('风险表达', prAudit.hits.length ? `${prAudit.hits.length} hits` : '未命中', prAudit.hits.length ? 'warn' : 'ok', prAudit.hits.map(hit => `${hit.token}: ${hit.label}`).join(';')),
makeItem('回应原则', '先情绪后事实', 'ok', '不说“你误会了 / 本意不是 / 过度解读”;品牌不与用户争夺解释权'),
],
},
];
return {
groups,
issues: groups.flatMap(group => group.items.filter(item => item.state === 'warn').map(item => `${group.title}:${item.label} ${item.value}`)),
};
};
const normalizeModelSections = (sections = []) => {
const allowedTypes = new Set(['Header', 'Narrative', 'Image', 'Grid', 'Footer']);
const fallbackModule = { Header: 'Header', Narrative: 'Narrative Block', Image: 'Image Block', Grid: 'Grid Content', Footer: 'Footer' };
return sections.slice(0, 8).map((section, i) => {
const type = allowedTypes.has(section.type) ? section.type : (i === 0 ? 'Header' : i === sections.length - 1 ? 'Footer' : 'Narrative');
return normalizeLongformSection({
id: `${type.toLowerCase()}-${Date.now()}-${i}`,
type,
blockType: section.blockType,
title: section.title || LONGFORM_DEFAULT_SECTIONS[i % LONGFORM_DEFAULT_SECTIONS.length].title,
body: section.body || '',
imageIds: [],
layoutModule: section.layoutModule || fallbackModule[type],
imageHint: section.imageHint || '',
}, i);
});
};
const initialLongformMessages = (task) => task.messages?.length ? task.messages : [
{ role: 'assistant', text: '把这篇公众号长图文的目标、读者、语气和必须覆盖的观点发给我。我会先推演结构,再把它拆成可排版的 section。' },
{ role: 'user', text: task.prompt },
];
const buildLocalLongformDraft = (promptOverride = '', fallbackTitle = '') => {
const title = promptOverride.split(/[,。,.\n]/)[0].replace(/^为微信公众号设计一篇/, '').slice(0, 24) || fallbackTitle || 'LONGFORM V10';
const sections = LONGFORM_DEFAULT_SECTIONS.map((s, i) => ({
...s,
id: `${s.type.toLowerCase()}-${i}-${Date.now()}`,
title: i === 0 ? title : s.title,
body: i === 1 ? `基于输入需求:「${promptOverride}」。Longform Agent 将先确定读者、论点和证据,再交给 MONTX 网格系统完成长图表达。` : s.body,
})).map(normalizeLongformSection);
return { title, sections };
};
const syncLongformSectionsWithImages = (sections = [], images = []) => sections.map(section => ({
...section,
imageIds: images.filter(img => img.assignedSectionId === section.id).map(img => img.id),
}));
const getLongformImageHint = (section, index = 0) => {
if (section?.imageHint) return section.imageHint;
const hints = {
Header: '建议使用一张建立主题气质的全通栏主视觉,不在图片上压字。',
Narrative: '建议补充能证明段落观点的真实场景、人物或环境图片。',
Image: '建议放置最强证据图,可连续添加多张形成视觉证据组。',
Grid: '建议选择细节、对比、步骤或局部特写,辅助网格化信息拆解。',
Footer: '通常不配图;如需收束,可使用品牌环境图或产品全景。',
};
return hints[section?.type] || `建议选择与第 ${index + 1} 段标题和正文证据直接相关的图片。`;
};
const buildLongformImageHtml = (images = []) => images
.map(img => `
`)
.join('');
const findLongformConstraint = (brand, query = '') => {
const clean = query.trim().toLowerCase();
if (!clean) return null;
return (brand.constraints || []).find(rule => {
const haystack = `${rule.id} ${rule.label} ${rule.description}`.toLowerCase();
return haystack.includes(clean) || clean.includes(rule.label.toLowerCase());
}) || null;
};
const getActiveLongformConstraints = (brand, scope = 'longform') => window.BrandMemoryStore?.getActiveConstraints
? window.BrandMemoryStore.getActiveConstraints(brand, scope)
: (brand?.constraints || []).filter(rule => rule.enabled !== false);
const extractLongformTerms = (text = '') => {
const quoted = Array.from(text.matchAll(/[“"「『《]([^”"」』》]+)[”"」』》]/g)).map(match => match[1].trim());
const known = ['完全自动驾驶', '解放双手', '无人驾驶', '绝对安全', '碾压竞品', '秒杀', '促销', '渐变', '毛玻璃', '阴影', '图片压字', '圆角拼图'];
return Array.from(new Set([...quoted, ...known.filter(term => text.includes(term))]));
};
const inferLongformGuardrailRules = (text = '') => {
const rules = {};
const heroMin = text.match(/(?:主标题|大标题|标题)[^\d]*(\d+)\s*px?\+?/i)?.[1];
const bodyRange = text.match(/正文[^\d]*(\d+)\s*(?:-|到|至|~)\s*(\d+)\s*px?/i);
const canvasWidth = text.match(/(?:画布|宽度)[^\d]*(\d+)\s*px?/i)?.[1];
if (heroMin) rules.heroTitleMinPx = Number(heroMin);
if (bodyRange) {
rules.bodyFontMinPx = Number(bodyRange[1]);
rules.bodyFontMaxPx = Number(bodyRange[2]);
}
if (canvasWidth) rules.canvasWidthPx = Number(canvasWidth);
return rules;
};
const buildLongformGuardrailDraft = (input = '', brand) => {
const text = input.trim();
if (!text) return null;
const matched = findLongformConstraint(brand, text);
const isDisable = /(停用|禁用|关闭|取消|不再启用|disable)/i.test(text);
const isEdit = /(修改|改成|改为|更新|调整|编辑|补充)/i.test(text) || (matched && !isDisable);
const terms = extractLongformTerms(text);
const rules = inferLongformGuardrailRules(text);
let label = '自定义品牌护栏';
if (/字号|主标题|大标题|正文|画布|px/i.test(text)) label = '字号与画布约束';
else if (/自动驾驶|解放双手|无人驾驶|承诺|安全/.test(text)) label = '禁止自动驾驶过度承诺';
else if (/图片|压字|全通栏|圆角|拼图/.test(text)) label = '图片使用约束';
else if (/渐变|毛玻璃|阴影|视觉|风格/.test(text)) label = '视觉风格约束';
const operation = isDisable ? 'disable' : isEdit && matched ? 'edit' : 'add';
const target = matched ? matched.id : '';
return {
id: longformId('draft'),
operation,
target,
targetLabel: matched?.label || '',
constraint: normalizeLongformConstraint({
id: operation === 'add' ? longformId('guard') : matched?.id,
label: operation === 'edit' && matched ? matched.label : label,
description: text,
severity: 'required',
category: inferLongformConstraintCategory({ label, description: text }),
source: 'user-natural-language',
forbiddenTerms: /(禁止|避免|不要|不得|不能|风险|承诺)/.test(text) ? terms : [],
requiredTerms: /(必须|需要|保持|统一|强制)/.test(text) ? terms : [],
rules,
createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
}),
};
};
const runLongformLayoutCommand = (text, task) => {
const normalized = text.trim();
const updatedAt = new Date().toLocaleString('zh-CN', { hour12: false });
const wantsHeroSingleLine = /(主标题|大标题|标题)/.test(normalized) && /(一行|单行|缩小|放一行|不换行)/.test(normalized);
const wantsTextCenter = /(所有|全部|全局|整篇|全文|文字|文本|正文|标题).*(居中|居中显示)|居中显示|文字居中/.test(normalized);
const wantsTextDefault = /(取消|恢复|默认).*(居中|对齐)|左对齐|恢复默认对齐|默认对齐/.test(normalized);
if (wantsTextCenter || wantsTextDefault) {
const layoutOverrides = {
...(task.layoutOverrides || {}),
textAlignRequest: normalized,
updatedAt,
};
if (wantsTextDefault) {
delete layoutOverrides.textAlign;
} else {
layoutOverrides.textAlign = 'center';
}
return {
taskPatch: { layoutOverrides, status: '排版已调整' },
reply: wantsTextDefault
? '已恢复默认文字对齐方式。正文和 section 内容保持不变。'
: '已将长图预览中的标题、正文、段落标题和页脚文字设置为居中显示。正文和 section 内容保持不变。',
};
}
if (wantsHeroSingleLine) {
const title = getLongformHeroSection(task).title || task.title || '';
const fontPx = getLongformSingleLineHeroPx(title);
const layoutOverrides = {
...(task.layoutOverrides || {}),
heroTitleSingleLine: true,
heroTitleFontPx: fontPx,
heroTitleRequest: normalized,
updatedAt,
};
return {
taskPatch: { layoutOverrides, status: '排版已调整' },
reply: `已把主标题设置为单行适配,当前估算字号为 ${fontPx}px。正文和 section 内容保持不变。注意:如果低于 MONTX 大标题 110px+ 的品牌约束,QA 会标记风险。`,
};
}
return null;
};
const runLongformQaCommand = (text, task, brand) => {
const normalized = text.trim();
const isTypographyQuestion = /(字体|字号|字重|font|主标题|大标题|正文|小标题).*(约束|限制|规范|合规|符合|检查|多少|多大|被约束|有没有)|(?:约束|限制|规范|合规|符合|检查).*(字体|字号|字重|主标题|大标题|正文|小标题)/i.test(normalized);
if (!isTypographyQuestion) return null;
const audit = buildLongformTypographyAudit(task, brand);
const family = getLongformTheme(task.skillMarkdown || MONTX_DEFAULT_SKILL).font;
const sourceText = audit.sources.length ? audit.sources.join(';') : '当前未解析到明确字号约束';
const valueText = [
`主标题 ${audit.typography.heroTitlePx}px`,
`引言 ${audit.typography.leadPx}px`,
`正文 ${audit.typography.bodyPx}px`,
`网格正文 ${audit.typography.gridRightPx}px`,
`小标题 ${audit.typography.sectionTitlePx}px`,
].join(';');
const conclusion = audit.issues.length
? `结论:存在 ${audit.issues.length} 项风险:${audit.issues.join(';')}。`
: '结论:当前可解析的字体/字号约束均通过。';
return {
taskPatch: {},
reply: `可以检查。当前字体族为 ${family}。约束来源:${sourceText}。当前渲染:${audit.typography.layoutLabel},${valueText}。${conclusion}`,
};
};
const classifyLongformIntentLocal = (input = '') => {
const text = String(input).trim();
const hasStyleKeyword = /(字体|字号|字重|font|样式|排版|布局|居中|对齐|主标题|大标题|正文|小标题|颜色|色彩|模板|风格|style)/i.test(text);
const hasQuestionSignal = /(是否|有没有|有无|吗|么|多少|多大|为什么|检查|合规|符合|约束|限制|规范|被约束)/.test(text);
const hasAdjustmentSignal = /(改|修改|调整|设为|设置|变成|换成|缩小|放大|居中|左对齐|右对齐|一行|单行|不换行|恢复|取消)/.test(text);
if (/^(查看护栏|解除护栏[::]|恢复护栏[::])/.test(text)) {
return { type: 'brand_guardrail', target: 'brand_guardrail', shouldRegenerateContent: false, shouldPreserveCopy: true, confidence: 0.98, rationale: '本地规则识别为品牌护栏命令', source: 'local' };
}
if (hasStyleKeyword && hasQuestionSignal) {
return { type: 'style_query', target: 'typography_or_style', shouldRegenerateContent: false, shouldPreserveCopy: true, confidence: 0.9, rationale: '本地规则识别为字体或样式问询', source: 'local' };
}
if (hasStyleKeyword && hasAdjustmentSignal) {
return { type: 'style_adjustment', target: 'typography_or_layout', shouldRegenerateContent: false, shouldPreserveCopy: true, confidence: 0.9, rationale: '本地规则识别为字体或样式调整', source: 'local' };
}
if (/(配图|图片|图池|image|主图|插图)/i.test(text) && /(分配|添加|选择|一键|换|删除|移动)/.test(text)) {
return { type: 'image_assignment', target: 'images', shouldRegenerateContent: false, shouldPreserveCopy: true, confidence: 0.82, rationale: '本地规则识别为图片分配操作', source: 'local' };
}
return { type: 'content_generation', target: 'longform_content', shouldRegenerateContent: true, shouldPreserveCopy: false, confidence: 0.68, rationale: '本地规则未识别为局部操作,默认进入内容推演', source: 'local' };
};
const normalizeLongformIntent = (intent = {}, fallbackText = '') => {
const allowed = new Set(['content_generation', 'style_query', 'style_adjustment', 'brand_guardrail', 'image_assignment', 'export_or_draft', 'unknown']);
const local = classifyLongformIntentLocal(fallbackText);
const type = allowed.has(intent.type) ? intent.type : local.type;
const shouldPreserveCopy = Boolean(intent.shouldPreserveCopy || ['style_query', 'style_adjustment', 'brand_guardrail', 'image_assignment', 'export_or_draft'].includes(type));
return {
type,
target: intent.target || local.target || 'unknown',
shouldRegenerateContent: shouldPreserveCopy ? false : Boolean(intent.shouldRegenerateContent || type === 'content_generation'),
shouldPreserveCopy,
confidence: Number(intent.confidence || local.confidence || 0),
rationale: intent.rationale || local.rationale || '',
source: intent.source || 'unknown',
modelError: intent.modelError || '',
};
};
const protectLongformIntent = (intent, text) => {
const normalized = normalizeLongformIntent(intent, text);
const local = classifyLongformIntentLocal(text);
if (local.shouldPreserveCopy && normalized.shouldRegenerateContent) {
return {
...local,
source: `${normalized.source || 'model'} + local-safety`,
rationale: `本地安全闸覆盖:${local.rationale}`,
modelIntent: normalized,
};
}
return normalized;
};
const applyLongformGuardrailDraft = (brand, draft) => {
if (!draft) return brand;
const updatedAt = new Date().toLocaleString('zh-CN', { hour12: false });
if (draft.operation === 'disable') {
return {
...brand,
constraints: brand.constraints.map(rule => rule.id === draft.target ? { ...rule, enabled: false, updatedAt } : rule),
};
}
if (draft.operation === 'edit') {
return {
...brand,
constraints: brand.constraints.map(rule => rule.id === draft.target ? {
...rule,
description: draft.constraint.description,
category: draft.constraint.category,
forbiddenTerms: Array.from(new Set([...(rule.forbiddenTerms || []), ...(draft.constraint.forbiddenTerms || [])])),
requiredTerms: Array.from(new Set([...(rule.requiredTerms || []), ...(draft.constraint.requiredTerms || [])])),
rules: { ...(rule.rules || {}), ...(draft.constraint.rules || {}) },
enabled: true,
updatedAt,
} : rule),
};
}
return {
...brand,
constraints: [...brand.constraints, { ...draft.constraint, updatedAt }],
};
};
const LONGFORM_MODEL_STAGE = {
idle: '待输入需求',
classifying: 'LLM 判断需求类型',
importing: '导入测试素材',
imported: '测试素材已导入',
thinking: '解析需求 / DeepSeek 调用中',
success: '结构生成完成 / HTML 已生成',
fallback: '本地兜底 / 失败原因',
};
const LongformDB = {
open() {
return new Promise((resolve, reject) => {
const req = indexedDB.open('aurelia-longform-agent', 1);
req.onupgradeneeded = () => req.result.createObjectStore('kv');
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
},
async get(key) {
try {
const db = await LongformDB.open();
return await new Promise((resolve, reject) => {
const req = db.transaction('kv', 'readonly').objectStore('kv').get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
} catch (e) {
console.warn('Longform IndexedDB read failed', e);
return null;
}
},
async set(key, value) {
try {
const db = await LongformDB.open();
await new Promise((resolve, reject) => {
const req = db.transaction('kv', 'readwrite').objectStore('kv').put(value, key);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
});
} catch (e) {
console.warn('Longform IndexedDB write failed', e);
}
},
};
const useLongformTasks = () => {
const [tasks, setTasks] = React.useState([]);
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
let mounted = true;
LongformDB.get('tasks').then(saved => {
if (!mounted) return;
const initialSource = saved?.length ? saved : LONGFORM_MOCK_TASKS;
const initial = initialSource.map(task => {
const normalized = normalizeLongformTask(task);
return { ...normalized, html: normalized.html || buildLongformHtml(normalized, getLongformBrand(normalized.brandId)) };
});
setTasks(initial);
setReady(true);
});
return () => { mounted = false; };
}, []);
React.useEffect(() => {
if (ready) LongformDB.set('tasks', tasks);
}, [ready, tasks]);
return { tasks, setTasks, ready };
};
const useLongformBrandProfiles = () => {
if (window.BrandMemoryStore?.useBrandMemoryProfiles) return window.BrandMemoryStore.useBrandMemoryProfiles();
const [brands, setBrands] = React.useState(() => normalizeSharedBrandProfiles());
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
let mounted = true;
LongformDB.get('brandProfiles').then(saved => {
if (!mounted) return;
setBrands(normalizeSharedBrandProfiles(saved?.length ? saved : LONGFORM_BRANDS));
setReady(true);
});
return () => { mounted = false; };
}, []);
React.useEffect(() => {
if (ready) LongformDB.set('brandProfiles', brands);
}, [ready, brands]);
return { brands, setBrands, ready };
};
const useLongformStyleTemplates = () => {
const [remoteTemplates, setRemoteTemplates] = React.useState([]);
React.useEffect(() => {
let mounted = true;
fetch('/api/longform/style-templates')
.then(response => response.ok ? response.json() : Promise.reject(new Error(`模板服务返回 ${response.status}`)))
.then(data => {
if (mounted) setRemoteTemplates(Array.isArray(data.styleTemplates) ? data.styleTemplates : []);
})
.catch(error => {
console.warn('Longform style templates unavailable', error);
if (mounted) setRemoteTemplates([]);
});
return () => { mounted = false; };
}, []);
return React.useMemo(() => [LONGFORM_DEFAULT_STYLE_TEMPLATE, ...remoteTemplates], [remoteTemplates]);
};
const getActiveStyleTemplateId = (task, styleTemplates) => {
if (task.activeStyleTemplateId) return task.activeStyleTemplateId;
const matched = styleTemplates.find(template => template.markdown === task.skillMarkdown);
if (matched) return matched.id;
return task.skillMarkdown === MONTX_DEFAULT_SKILL ? LONGFORM_DEFAULT_STYLE_TEMPLATE_ID : LONGFORM_CUSTOM_STYLE_TEMPLATE_ID;
};
const LongformAgent = ({ workspaceContext }) => {
const { tasks, setTasks, ready } = useLongformTasks();
const { brands, setBrands, ready: brandsReady } = useLongformBrandProfiles();
const styleTemplates = useLongformStyleTemplates();
const [selectedId, setSelectedId] = React.useState(null);
const selected = tasks.find(t => t.id === selectedId);
const upsertTask = (task) => {
const normalizedTask = normalizeLongformTask(task, brands);
const brand = getLongformBrand(normalizedTask.brandId, brands);
const qa = calculateLongformQa(normalizedTask, brand);
const next = { ...normalizedTask, qa, html: buildLongformHtml({ ...normalizedTask, qa }, brand), updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) };
setTasks(prev => prev.map(t => t.id === task.id ? next : t));
return next;
};
const createTask = (brandId = workspaceContext?.brandId || 'montx', productId = '') => {
const brand = getLongformBrand(brandId, brands);
const product = getLongformProduct(brand, productId);
const task = {
id: longformId(),
brandId,
productId: product?.id || '',
narrativeMode: 'brand_product',
title: '未命名长图文推演',
prompt: '输入这篇微信公众号长图文的目标、读者、语气、必须覆盖的观点。',
status: '推演中',
activeStyleTemplateId: LONGFORM_DEFAULT_STYLE_TEMPLATE_ID,
skillMarkdown: MONTX_DEFAULT_SKILL,
images: [],
sections: LONGFORM_DEFAULT_SECTIONS.map(s => ({ ...s, id: `${s.id}-${Date.now()}` })),
html: '',
messages: [
{ role: 'assistant', text: '把这篇公众号长图文的目标、读者、语气和必须覆盖的观点发给我。我会先推演结构,再把它拆成可排版的 section。' },
{ role: 'user', text: '输入这篇微信公众号长图文的目标、读者、语气、必须覆盖的观点。' },
],
overrides: [],
qa: { score: 90, issues: [], exportReady: true },
updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
};
const saved = { ...task, html: buildLongformHtml(task, brand) };
setTasks(prev => [saved, ...prev]);
setSelectedId(saved.id);
};
const updateBrand = (brandId, nextBrand) => setBrands(prev => prev.map(brand => brand.id === brandId ? normalizeSharedBrandProfiles([nextBrand])[0] : brand));
if (!ready || !brandsReady) {
return ;
}
return selected ? (
setSelectedId(null)} onChange={upsertTask}/>
) : (
);
};
const LongformList = ({ tasks, brands, workspaceContext, onUpdateBrand, onNew, onOpen }) => {
const [brandId, setBrandId] = React.useState(workspaceContext?.brandId || 'montx');
const [guardrailExpanded, setGuardrailExpanded] = React.useState(false);
React.useEffect(() => {
if (workspaceContext?.brandId) setBrandId(workspaceContext.brandId);
}, [workspaceContext?.brandId]);
const brand = getLongformBrand(brandId, brands);
const defaultProduct = getLongformProduct(brand);
const activeConstraints = getActiveLongformConstraints(brand);
const visibleConstraints = guardrailExpanded ? brand.constraints : brand.constraints.slice(0, 4);
const brandTasks = tasks.filter(t => t.brandId === brandId);
const overrideCount = brandTasks.filter(t => t.overrides.length).length;
const riskCount = brandTasks.filter(t => t.qa.issues?.length || t.overrides.length).length;
React.useEffect(() => {
window.__LONGFORM_THINKING_CONTEXT = { mode: 'list', tasks, brand, exportState: '' };
window.dispatchEvent(new CustomEvent('longform-thinking-change'));
}, [tasks, brandId, brands]);
return (
Longform Agent · 长图文内容工厂
多品牌长文推演、品牌护栏治理、markdown style skill、图片分配与整张 PNG 导出。
Workspace · {brand.name} · {(brand.products || []).length} products share memory
继承策略 · Workspace Policy
来自品牌记忆 · {brand.name} · {activeConstraints.length} active
product.name).join(' / ') || '未配置'}/>
0}/>
{(brand.products || []).map(product => (
{brand.name} {product.name}
{product.positioning || '暂无产品定位'}
{tasks.filter(task => task.productId === product.id).length} tasks
))}
{visibleConstraints.map(c => (
{c.label}
{c.description}
{c.enabled === false ? 'disabled' : c.category}
))}
Longform 只消费当前 Workspace 的品牌/产品叙事与护栏;规则编辑已归属到「品牌记忆」。
长图文推演列表
{tasks.length} drafts
IndexedDB autosave
{tasks.map(task => {
const taskBrand = getLongformBrand(task.brandId, brands);
const taskProduct = getLongformProduct(taskBrand, task.productId);
return (
onOpen(task.id)}>
{taskBrand.name}
{taskProduct &&
{taskProduct.name}}
{getLongformNarrativeMode(task.narrativeMode).label}
{task.title}
{task.prompt}
{parseSkillMarkdown(task.skillMarkdown).title}
{task.images.length} 图
{task.sections.length} 段
QA {task.qa.score}
{task.overrides.length ? 'Override' : task.status}
{task.updatedAt}
);
})}
);
};
const GuardrailMaintenancePanel = ({ brand, onApply }) => {
const [input, setInput] = React.useState('');
const [draft, setDraft] = React.useState(null);
const parseDraft = () => setDraft(buildLongformGuardrailDraft(input, brand));
const applyDraft = () => {
if (!draft) return;
onApply(applyLongformGuardrailDraft(brand, draft));
setInput('');
setDraft(null);
};
return (
护栏维护
自然语言会先解析成变更草案,确认后才写入品牌 profile。
IndexedDB policy
{brand.constraints.map(rule => (
{rule.enabled === false ? 'disabled' : rule.category}
{rule.label}
{rule.description}
))}
);
};
const buildBrandProductMaintenanceDraft = (input = '', brand) => {
const text = input.trim();
if (!text) return null;
const changes = [];
const brandPatch = { narrative: {} };
const productPatches = [];
const addProducts = [];
const parseListValue = (value = '') => longformListFromValue(String(value).replace(/^(是|为|成|:|:)/, ''));
const findField = (label) => text.match(new RegExp(`(?:${label})[^。;;\\n]*(?:是|为|改成|改为|设为|:|:)\\s*([^。;;\\n]+)`))?.[1]?.trim() || '';
const findForbidden = () => Array.from(text.matchAll(/(?:禁止|禁用|不要|不得|不能|避免)(?:说|使用|出现|表达)?\s*([^。;;\n]+)/g))
.flatMap(match => parseListValue(match[1].replace(/等.*$/, '')));
const findRequired = () => Array.from(text.matchAll(/(?:必须|需要|要|提及|强调)(?:表达|覆盖|说明|提到)?\s*([^。;;\n]+)/g))
.flatMap(match => parseListValue(match[1].replace(/等.*$/, '')));
const newProduct = text.match(/(?:新增|添加|创建)产品\s*([A-Za-z0-9-]+)/i)?.[1];
if (newProduct) {
const product = normalizeLongformProduct({
id: longformId('product'),
name: newProduct,
aliases: [newProduct],
source: 'natural-language',
updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
});
addProducts.push(product);
changes.push(`新增产品「${newProduct}」`);
}
const product = (brand.products || []).find(item => {
const names = [item.name, ...(item.aliases || [])].filter(Boolean);
return names.some(name => new RegExp(`(^|[^A-Za-z0-9])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^A-Za-z0-9]|$)`, 'i').test(text));
});
const brandTone = findField('品牌(?:语气|口吻|voice)');
if (brandTone) {
brandPatch.narrative.voice = parseListValue(brandTone);
changes.push(`更新品牌语气:${brandPatch.narrative.voice.join(' / ')}`);
}
const brandPositioning = findField('品牌定位');
if (brandPositioning) {
brandPatch.narrative.positioning = brandPositioning;
changes.push('更新品牌定位');
}
const worldview = findField('(?:世界观|品牌世界观)');
if (worldview) {
brandPatch.narrative.worldview = worldview;
changes.push('更新品牌世界观');
}
const valueProposition = findField('(?:价值主张|品牌主张)');
if (valueProposition) {
brandPatch.narrative.valueProposition = valueProposition;
changes.push('更新价值主张');
}
const forbidden = findForbidden();
const required = findRequired();
if (product) {
const productPatch = { id: product.id, patch: {}, narrativePatch: {} };
const positioning = findField(`${product.name}.*(?:定位|叙事定位)|产品定位`);
if (positioning) {
productPatch.patch.positioning = positioning;
productPatch.narrativePatch.positioning = positioning;
changes.push(`更新 ${product.name} 定位`);
}
const productTone = findField(`${product.name}.*(?:语气|口吻)|产品语气`);
if (productTone) {
productPatch.patch.tone = parseListValue(productTone);
changes.push(`更新 ${product.name} 语气`);
}
if (/核心能力/.test(text)) {
const value = findField(`${product.name}.*核心能力|核心能力`);
if (value) {
productPatch.narrativePatch.coreCapabilities = parseListValue(value);
changes.push(`更新 ${product.name} 核心能力`);
}
}
if (/场景|用户场景/.test(text)) {
const value = findField(`${product.name}.*(?:用户场景|场景)|用户场景|场景`);
if (value) {
productPatch.narrativePatch.userScenarios = parseListValue(value);
changes.push(`更新 ${product.name} 用户场景`);
}
}
if (forbidden.length) {
productPatch.patch.forbiddenClaims = Array.from(new Set([...(product.forbiddenClaims || []), ...forbidden]));
productPatch.narrativePatch.forbiddenClaims = Array.from(new Set([...(product.narrative?.forbiddenClaims || []), ...forbidden]));
changes.push(`更新 ${product.name} 禁用表达:${forbidden.join(' / ')}`);
}
if (required.length) {
productPatch.patch.requiredClaims = Array.from(new Set([...(product.requiredClaims || []), ...required]));
productPatch.narrativePatch.requiredMentions = Array.from(new Set([...(product.narrative?.requiredMentions || []), ...required]));
changes.push(`更新 ${product.name} 必须提及:${required.join(' / ')}`);
}
productPatches.push(productPatch);
} else {
if (forbidden.length) {
brandPatch.narrative.forbiddenAngles = Array.from(new Set([...(brand.narrative?.forbiddenAngles || []), ...forbidden]));
changes.push(`更新品牌禁用角度:${forbidden.join(' / ')}`);
}
if (required.length) {
brandPatch.narrative.requiredThemes = Array.from(new Set([...(brand.narrative?.requiredThemes || []), ...required]));
changes.push(`更新品牌必须主题:${required.join(' / ')}`);
}
}
if (!changes.length) {
changes.push('未识别到明确字段,请补充品牌/产品名和要更新的定位、语气、禁用表达或必须提及。');
}
return { input: text, brandPatch, productPatches, addProducts, changes, applyable: changes.some(change => !change.startsWith('未识别')) };
};
const applyBrandProductMaintenanceDraft = (brand, draft) => {
if (!draft?.applyable) return brand;
const updatedAt = new Date().toLocaleString('zh-CN', { hour12: false });
return {
...brand,
narrative: { ...(brand.narrative || {}), ...(draft.brandPatch?.narrative || {}) },
products: [
...(brand.products || []).map(product => {
const found = draft.productPatches.find(item => item.id === product.id);
if (!found) return product;
return {
...product,
...(found.patch || {}),
narrative: { ...(product.narrative || {}), ...(found.narrativePatch || {}) },
updatedAt,
};
}),
...(draft.addProducts || []),
],
updatedAt,
};
};
const BrandProductMaintenancePanel = ({ brand, onApply }) => {
const [input, setInput] = React.useState('');
const [draft, setDraft] = React.useState(null);
const [advancedOpen, setAdvancedOpen] = React.useState(false);
const [draftBrand, setDraftBrand] = React.useState(() => ({
...brand,
narrative: { ...(brand.narrative || {}) },
products: (brand.products || []).map(product => ({ ...product, narrative: { ...(product.narrative || {}) } })),
}));
React.useEffect(() => {
setDraftBrand({ ...brand, narrative: { ...(brand.narrative || {}) }, products: (brand.products || []).map(product => ({ ...product, narrative: { ...(product.narrative || {}) } })) });
setDraft(null);
setInput('');
}, [brand.id, brand.updatedAt, brand.products?.length]);
const parseListValue = (value = '') => longformListFromValue(value);
const parseDraft = () => setDraft(buildBrandProductMaintenanceDraft(input, brand));
const applyNaturalDraft = () => {
if (!draft?.applyable) return;
onApply(applyBrandProductMaintenanceDraft(brand, draft));
setInput('');
setDraft(null);
};
const updateBrandNarrative = (patch) => setDraftBrand(current => ({ ...current, narrative: { ...(current.narrative || {}), ...patch }, updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) }));
const updateProduct = (productId, patch) => setDraftBrand(current => ({ ...current, products: current.products.map(product => product.id === productId ? { ...product, ...patch, updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) } : product) }));
const updateProductNarrative = (productId, patch) => setDraftBrand(current => ({ ...current, products: current.products.map(product => product.id === productId ? { ...product, narrative: { ...(product.narrative || {}), ...patch }, updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) } : product) }));
const addProduct = () => setDraftBrand(current => ({ ...current, products: [...(current.products || []), normalizeLongformProduct({ id: longformId('product'), name: '新产品', source: 'user-maintained', updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) })] }));
const removeProduct = (productId) => setDraftBrand(current => ({ ...current, products: current.products.filter(product => product.id !== productId) }));
const applyAdvancedDraft = () => onApply({ ...draftBrand, updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) });
return (
品牌与产品维护
默认使用自然语言维护;完整字段编辑收进高级模式。
BRAND
{brand.name}
{brand.narrative?.positioning || '暂无品牌定位'}
{(brand.narrative?.voice || []).slice(0, 6).map(item => {item})}
{(brand.products || []).map(product => (
PRODUCT
{product.name}
{product.narrative?.positioning || product.positioning || '暂无产品定位'}
))}
{advancedOpen && (
{(draftBrand.products || []).map(product => (
))}
)}
);
};
const LongformContextStack = ({
brand,
product,
task,
skill,
contextTab,
onContextTab,
activeStyleTemplateId,
isCustomStyle,
styleTemplates,
onStyleTemplateChange,
onSkillUpload,
}) => {
const briefing = task.briefing || {};
const tabs = [
['brand', 'Brand'],
['briefing', 'Brief'],
['writing', 'Writing'],
['style', 'Style'],
];
return (
Context Stack
{brand.name} · {briefing.coreQuestion || task.title}
{tabs.map(([id, label]) => (
))}
{contextTab === 'brand' && (
品牌
{brand.name}
{brand.narrative?.positioning || '暂无品牌定位'}
{(brand.narrative?.voice || []).slice(0, 6).map(item => {item})}
{getActiveLongformConstraints(brand).length} guardrails
{product && (
产品
{product.name}
)}
)}
{contextTab === 'briefing' && (
核心问题
{briefing.coreQuestion || task.title}
{briefing.goal || task.prompt}
{(briefing.mustCover || []).slice(0, 5).map(item => {item})}
{briefing.avoidClaims?.length ? {briefing.avoidClaims.length} avoid : null}
)}
{contextTab === 'writing' && (
叙事模式
{getLongformNarrativeMode(task.narrativeMode).label}
{getLongformNarrativeMode(task.narrativeMode).description}
技术拆解
克制
高信息密度
Humanize later
)}
{contextTab === 'style' && (
)}
);
};
const LongformDetail = ({ task, brands = LONGFORM_BRANDS, styleTemplates = [LONGFORM_DEFAULT_STYLE_TEMPLATE], workspaceContext, onBack, onChange }) => {
const [step, setStep] = React.useState(1);
const [exportState, setExportState] = React.useState('');
const [outlineCollapsed, setOutlineCollapsed] = React.useState(false);
const [skillCollapsed, setSkillCollapsed] = React.useState(true);
const [imagesCollapsed, setImagesCollapsed] = React.useState(true);
const [contextTab, setContextTab] = React.useState('brand');
const [mainView, setMainView] = React.useState('sections');
const previewRef = React.useRef(null);
const previewShellRef = React.useRef(null);
const [previewScale, setPreviewScale] = React.useState(1);
const [previewHeight, setPreviewHeight] = React.useState(0);
const [chatInput, setChatInput] = React.useState('');
const [chatMessages, setChatMessages] = React.useState(() => initialLongformMessages(task));
const [modelStatus, setModelStatus] = React.useState('idle');
const [imagePickerSectionId, setImagePickerSectionId] = React.useState('');
const brand = getLongformBrand(task.brandId, brands);
const skill = React.useMemo(() => parseSkillMarkdown(task.skillMarkdown), [task.skillMarkdown]);
const activeStyleTemplateId = getActiveStyleTemplateId(task, styleTemplates);
const isCustomStyle = activeStyleTemplateId === LONGFORM_CUSTOM_STYLE_TEMPLATE_ID;
const product = getLongformProduct(brand, task.productId);
const inspect = React.useMemo(() => buildLongformInspect(task, brand, skill), [task, brand, skill]);
const publishThinking = (extra = {}, taskSnapshot = task, messagesSnapshot = chatMessages) => {
const lastUser = [...messagesSnapshot].reverse().find(message => message.role === 'user');
const lastAgent = [...messagesSnapshot].reverse().find(message => message.role === 'assistant');
const status = extra.modelStatus || modelStatus;
const snapshotSkill = parseSkillMarkdown(taskSnapshot.skillMarkdown || task.skillMarkdown);
const previousThinking = window.__LONGFORM_THINKING_CONTEXT;
const nextLastAgent = extra.lastAgentMessage ?? lastAgent?.text ?? '';
const keepPreviousStage = !extra.generationStage
&& previousThinking?.mode === 'detail'
&& previousThinking?.task?.id === taskSnapshot.id
&& previousThinking?.lastAgentMessage === nextLastAgent
&& previousThinking?.generationStage;
window.__LONGFORM_THINKING_CONTEXT = {
mode: 'detail',
task: taskSnapshot,
brand,
product: getLongformProduct(brand, taskSnapshot.productId),
narrativeMode: taskSnapshot.narrativeMode || 'brand_product',
skill: snapshotSkill,
styleTemplateName: styleTemplates.find(template => template.id === getActiveStyleTemplateId(taskSnapshot, styleTemplates))?.name || (taskSnapshot.activeStyleTemplateId === LONGFORM_CUSTOM_STYLE_TEMPLATE_ID ? 'Custom Upload' : snapshotSkill.title),
exportState,
modelStatus: status,
generationStage: extra.generationStage || (keepPreviousStage ? previousThinking.generationStage : LONGFORM_MODEL_STAGE[status] || taskSnapshot.status),
currentInput: extra.currentInput ?? chatInput,
messages: messagesSnapshot,
lastUserMessage: extra.lastUserMessage ?? lastUser?.text ?? '',
lastAgentMessage: nextLastAgent,
modelError: extra.modelError || '',
};
window.dispatchEvent(new CustomEvent('longform-thinking-change'));
};
React.useEffect(() => {
setChatInput('');
setChatMessages(initialLongformMessages(task));
setModelStatus('idle');
setImagePickerSectionId('');
}, [task.id]);
React.useEffect(() => {
publishThinking();
}, [task, brand, skill, styleTemplates, exportState, modelStatus, chatInput, chatMessages]);
React.useEffect(() => {
const syncPreviewScale = () => {
const shell = previewShellRef.current;
const canvas = previewRef.current;
if (!shell || !canvas) return;
const available = Math.max(320, shell.clientWidth - 2);
setPreviewScale(Math.min(1, available / 1080));
setPreviewHeight(canvas.scrollHeight || canvas.offsetHeight || 0);
};
const raf = requestAnimationFrame(syncPreviewScale);
const observer = window.ResizeObserver ? new ResizeObserver(syncPreviewScale) : null;
if (observer && previewShellRef.current) observer.observe(previewShellRef.current);
window.addEventListener('resize', syncPreviewScale);
return () => {
cancelAnimationFrame(raf);
observer?.disconnect();
window.removeEventListener('resize', syncPreviewScale);
};
}, [task.sections, task.images, task.title, task.prompt, task.skillMarkdown, task.layoutOverrides, outlineCollapsed]);
const patchTask = (patch) => onChange({ ...task, ...patch });
const generateFromPrompt = (promptOverride = task.prompt, messages = task.messages) => {
const draft = buildLocalLongformDraft(promptOverride, task.title);
patchTask({ ...draft, prompt: promptOverride, messages, status: '待上传图片' });
setStep(2);
};
const runGuardrailCommand = (text, nextMessages) => {
const normalized = text.trim();
if (normalized === '查看护栏') {
const reply = `当前 ${brand.name} 强制护栏共 ${brand.constraints.length} 条:${brand.constraints.map(rule => rule.label).join(';')}。可输入「解除护栏:规则名;原因:...」或「恢复护栏:规则名」。`;
return { taskPatch: {}, reply };
}
const releaseMatch = normalized.match(/^解除护栏[::]\s*([^;;\n]+)(?:[;;]\s*原因[::]\s*(.+))?$/);
if (releaseMatch) {
const rule = findLongformConstraint(brand, releaseMatch[1]);
const reason = (releaseMatch[2] || '').trim();
if (!rule) return { taskPatch: {}, reply: `没有找到「${releaseMatch[1].trim()}」对应的品牌护栏。可以输入「查看护栏」确认规则名。` };
if (!reason) return { taskPatch: {}, reply: `解除「${rule.label}」需要填写原因。格式:解除护栏:${rule.label};原因:客户明确要求。` };
const override = { constraintId: rule.id, reason, createdAt: new Date().toLocaleString('zh-CN', { hour12: false }) };
const overrides = task.overrides.some(item => item.constraintId === rule.id)
? task.overrides.map(item => item.constraintId === rule.id ? override : item)
: [...task.overrides, override];
return { taskPatch: { overrides }, reply: `已记录解除申请:「${rule.label}」。原因:${reason}。QA 与列表页会标记 Override 风险。` };
}
const restoreMatch = normalized.match(/^恢复护栏[::]\s*(.+)$/);
if (restoreMatch) {
const rule = findLongformConstraint(brand, restoreMatch[1]);
if (!rule) return { taskPatch: {}, reply: `没有找到「${restoreMatch[1].trim()}」对应的品牌护栏。可以输入「查看护栏」确认规则名。` };
const overrides = task.overrides.filter(item => item.constraintId !== rule.id);
const changed = overrides.length !== task.overrides.length;
return {
taskPatch: { overrides },
reply: changed ? `已恢复品牌护栏:「${rule.label}」。` : `「${rule.label}」当前没有解除记录,无需恢复。`,
};
}
return null;
};
const requestLongformIntent = async (text, nextMessages) => {
const local = classifyLongformIntentLocal(text);
try {
const response = await fetch('/api/longform/intent', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
prompt: text,
messages: nextMessages,
brand,
product,
narrativeMode: task.narrativeMode || 'brand_product',
skill,
skillMarkdown: task.skillMarkdown,
briefing: task.briefing,
sections: task.sections.map(section => ({ type: section.type, blockType: section.blockType, title: section.title, body: section.body, layoutModule: section.layoutModule })),
localIntent: local,
}),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `意图服务返回 ${response.status}`);
return protectLongformIntent(data, text);
} catch (error) {
return protectLongformIntent({ ...local, modelError: error.message }, text);
}
};
const sendLongformMessage = async () => {
const text = chatInput.trim();
if (!text || ['thinking', 'classifying'].includes(modelStatus)) return;
const nextMessages = [...chatMessages, { role: 'user', text }];
const classifyingMessages = [...nextMessages, { role: 'assistant', text: '正在判断需求类型,确认是否需要重新推演文案。' }];
const classifyingTask = { ...task, messages: nextMessages, status: '需求类型判断中' };
setChatMessages(classifyingMessages);
setChatInput('');
setModelStatus('classifying');
patchTask({ messages: nextMessages, status: '需求类型判断中' });
publishThinking({
modelStatus: 'classifying',
generationStage: 'LLM 判断需求类型',
currentInput: text,
lastUserMessage: text,
lastAgentMessage: '正在判断需求类型,确认是否需要重新推演文案。',
}, classifyingTask, classifyingMessages);
const intent = await requestLongformIntent(text, nextMessages);
const finishLocalIntent = (reply, taskPatch = {}, stage = '局部命令已处理', status = task.status) => {
const finalMessages = [...nextMessages, { role: 'assistant', text: reply }];
const commandTask = { ...task, ...taskPatch, messages: finalMessages, status };
setChatMessages(finalMessages);
setModelStatus('idle');
patchTask(commandTask);
publishThinking({
modelStatus: 'idle',
generationStage: stage,
currentInput: '',
lastUserMessage: text,
lastAgentMessage: reply,
}, commandTask, finalMessages);
};
if (intent.type === 'brand_guardrail') {
const guardrailCommand = runGuardrailCommand(text, nextMessages);
if (guardrailCommand) {
finishLocalIntent(
guardrailCommand.reply,
guardrailCommand.taskPatch,
`需求类型:品牌护栏 / ${intent.source}`,
guardrailCommand.taskPatch.overrides ? '护栏已更新' : task.status,
);
} else {
finishLocalIntent(`已识别为品牌护栏需求,但这条指令还不够明确。可输入「查看护栏」「解除护栏:规则名;原因:...」或「恢复护栏:规则名」。文案未被修改。`, {}, `需求类型:品牌护栏 / ${intent.source}`);
}
return;
}
if (intent.type === 'style_query') {
const qaCommand = runLongformQaCommand(text, task, brand);
const reply = qaCommand?.reply || `已识别为字体/样式问询,文案不会被修改。当前未能匹配到更具体的检查项,可以问「主标题字体是否符合品牌约束」「正文字号是多少」或「检查字体约束」。`;
finishLocalIntent(reply, qaCommand?.taskPatch || {}, `需求类型:样式问询 / ${intent.source}`);
return;
}
if (intent.type === 'style_adjustment') {
const layoutCommand = runLongformLayoutCommand(text, task);
const reply = layoutCommand?.reply || `已识别为字体/样式调整,因此没有重新推演文案。当前原型可直接执行:主标题单行适配、全文居中、恢复默认对齐。其他样式调整会在下一步接入更细的布局 override。`;
finishLocalIntent(reply, layoutCommand?.taskPatch || {}, `需求类型:样式调整 / ${intent.source}`, layoutCommand ? '排版已调整' : task.status);
return;
}
if (intent.type === 'image_assignment' || intent.shouldPreserveCopy) {
const reply = intent.type === 'image_assignment'
? '已识别为图片分配需求,因此不会重新推演文案。请使用图片池的一键配图,或在 Section Map 中点击 no image / add image 直接选图。'
: `已识别为不应改写文案的局部需求(${intent.type}),本次没有进入内容推演。`;
finishLocalIntent(reply, {}, `需求类型:${intent.type} / ${intent.source}`);
return;
}
const pendingMessages = [...nextMessages, { role: 'assistant', text: `已识别为内容推演需求(${intent.source}),正在调用 DeepSeek 生成长图文推演,请稍等。` }];
const pendingTask = { ...task, prompt: text, messages: nextMessages, status: '模型推演中' };
setChatMessages(pendingMessages);
setModelStatus('thinking');
patchTask({ prompt: text, messages: nextMessages, status: '模型推演中' });
publishThinking({
modelStatus: 'thinking',
generationStage: `需求类型:内容推演 / ${intent.source}`,
currentInput: text,
lastUserMessage: text,
lastAgentMessage: pendingMessages[pendingMessages.length - 1].text,
}, pendingTask, pendingMessages);
try {
const response = await fetch('/api/longform/generate', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
prompt: text,
messages: nextMessages,
brand,
product,
narrativeMode: task.narrativeMode || 'brand_product',
skill,
skillMarkdown: task.skillMarkdown,
briefing: task.briefing,
images: task.images.map(img => ({ name: img.name, assignedSectionId: img.assignedSectionId })),
sections: task.sections.map(section => ({ type: section.type, blockType: section.blockType, title: section.title, body: section.body, layoutModule: section.layoutModule })),
}),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `模型服务返回 ${response.status}`);
const sections = normalizeModelSections(data.sections || []);
if (!sections.length) throw new Error('模型未返回 sections');
const assistantMessage = data.assistantMessage || '已完成一版长图文结构推演,并同步生成 1080px HTML 长图预览。';
const finalMessages = [...nextMessages, { role: 'assistant', text: assistantMessage }];
const generatedTask = {
...task,
title: data.title || buildLocalLongformDraft(text, task.title).title,
prompt: text,
briefing: task.briefing,
narrativeMode: task.narrativeMode || 'brand_product',
sections,
messages: finalMessages,
status: 'HTML 已生成',
};
setChatMessages(finalMessages);
patchTask(generatedTask);
setStep(2);
setModelStatus('success');
publishThinking({
modelStatus: 'success',
generationStage: '结构生成完成 / HTML 已生成',
currentInput: '',
lastUserMessage: text,
lastAgentMessage: assistantMessage,
}, generatedTask, finalMessages);
} catch (error) {
const fallback = buildLocalLongformDraft(text, task.title);
const failureHint = /Failed to fetch|NetworkError|Load failed/i.test(error.message)
? '无法连接本地生成服务。请确认页面通过 http://localhost:4173 打开,并刷新后重试。'
: '模型服务返回异常。请检查 DeepSeek API key、网络或服务端日志后重试。';
const finalMessages = [
...nextMessages,
{ role: 'assistant', text: `真实模型生成暂不可用(${error.message}),已先用本地规则完成一版推演。${failureHint}` },
];
const fallbackTask = { ...task, ...fallback, prompt: text, messages: finalMessages, status: '本地推演已生成' };
setChatMessages(finalMessages);
patchTask(fallbackTask);
setStep(2);
setModelStatus('fallback');
publishThinking({
modelStatus: 'fallback',
generationStage: '本地兜底 / 失败原因',
currentInput: '',
lastUserMessage: text,
lastAgentMessage: finalMessages[finalMessages.length - 1].text,
modelError: error.message,
}, fallbackTask, finalMessages);
}
};
const importBootstrap = async () => {
setModelStatus('importing');
publishThinking({
modelStatus: 'importing',
generationStage: '导入测试素材',
lastAgentMessage: '正在读取本地 Brief 和配图。',
});
try {
const response = await fetch('/api/longform/bootstrap');
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `素材服务返回 ${response.status}`);
const prompt = data.briefMarkdown || task.prompt;
const briefing = data.briefing || null;
const title = prompt.match(/^#\s+(.+)$/m)?.[1]?.trim() || '智能驾驶技术拆解 ADAS Tech Breakdown';
const images = (data.images || []).map(img => ({
id: longformId('img'),
name: img.name,
dataUrl: img.dataUrl,
assignedSectionId: '',
}));
const messages = [
{ role: 'assistant', text: `已导入测试 Brief 和 ${images.length} 张本地配图。下一步可直接点击发送并推演,调用真实模型生成 section。` },
];
setChatMessages(messages);
setChatInput(prompt);
patchTask({ title, prompt, briefing, messages, images, status: '待模型推演' });
setStep(1);
setModelStatus('imported');
publishThinking({
modelStatus: 'imported',
generationStage: '测试素材已导入',
currentInput: prompt,
lastAgentMessage: briefing ? `${messages[0].text} 已解析结构化 Briefing:${briefing.coreQuestion || briefing.title}` : messages[0].text,
}, { ...task, title, prompt, briefing, messages, images, status: '待模型推演' }, messages);
} catch (error) {
const finalMessages = [
...chatMessages,
{ role: 'assistant', text: `无法导入测试 Brief/配图(${error.message})。请通过本地测试服务器打开页面后再试。` },
];
setChatMessages(finalMessages);
patchTask({ messages: finalMessages, status: '素材导入失败' });
setModelStatus('fallback');
publishThinking({
modelStatus: 'fallback',
generationStage: '本地素材导入失败',
lastAgentMessage: finalMessages[finalMessages.length - 1].text,
modelError: error.message,
}, { ...task, messages: finalMessages, status: '素材导入失败' }, finalMessages);
}
};
const handleSkillUpload = (file) => {
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const skillMarkdown = String(reader.result || '');
const nextTask = { ...task, skillMarkdown, activeStyleTemplateId: LONGFORM_CUSTOM_STYLE_TEMPLATE_ID, status: '自定义 STYLE 已上传' };
patchTask(nextTask);
publishThinking({
modelStatus,
generationStage: '自定义 STYLE 已上传',
lastAgentMessage: '已读取自定义 markdown style skill,HTML 预览将按当前内容刷新。',
}, nextTask, chatMessages);
};
reader.readAsText(file);
};
const switchStyleTemplate = (templateId) => {
const template = styleTemplates.find(item => item.id === templateId);
if (!template) return;
const nextTask = {
...task,
activeStyleTemplateId: template.id,
skillMarkdown: template.markdown,
status: 'HTML 已生成',
};
patchTask(nextTask);
publishThinking({
modelStatus,
generationStage: `STYLE 已切换:${template.name}`,
lastAgentMessage: `已切换 STYLE 模板为「${template.name}」,当前内容、图片分配和护栏设置保持不变。`,
}, nextTask, chatMessages);
};
const switchProduct = (productId) => {
const nextProduct = getLongformProduct(brand, productId);
const nextTask = { ...task, productId, status: '产品 Profile 已切换' };
patchTask(nextTask);
publishThinking({
modelStatus,
generationStage: `产品已切换:${nextProduct?.name || productId}`,
lastAgentMessage: `已将当前任务绑定到 ${brand.name} ${nextProduct?.name || productId}。内容和图片分配保持不变,后续推演会继承该产品 Profile。`,
}, nextTask, chatMessages);
};
const switchNarrativeMode = (narrativeMode) => {
const mode = getLongformNarrativeMode(narrativeMode);
const nextTask = { ...task, narrativeMode: mode.id, status: '叙事模式已切换' };
patchTask(nextTask);
publishThinking({
modelStatus,
generationStage: `叙事模式已切换:${mode.label}`,
lastAgentMessage: `已切换为「${mode.label}」叙事。${mode.description} 当前内容和配图保持不变,下一次内容推演会按该模式组装 prompt。`,
}, nextTask, chatMessages);
};
const handleImageUpload = (files) => {
const list = Array.from(files || []);
Promise.all(list.map(file => new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => resolve({ id: longformId('img'), name: file.name, dataUrl: reader.result, assignedSectionId: '' });
reader.readAsDataURL(file);
}))).then(images => patchTask({ images: [...task.images, ...images], status: '图片分配中' }));
};
const assignImage = (imageId, sectionId) => {
const images = task.images.map(img => img.id === imageId ? { ...img, assignedSectionId: sectionId } : img);
const sections = syncLongformSectionsWithImages(task.sections, images);
patchTask({ images, sections, status: 'HTML 已生成' });
};
const toggleSectionImage = (sectionId, imageId) => {
const images = task.images.map(img => {
if (img.id !== imageId) return img;
return { ...img, assignedSectionId: img.assignedSectionId === sectionId ? '' : sectionId };
});
const sections = syncLongformSectionsWithImages(task.sections, images);
patchTask({ images, sections, status: 'HTML 已生成' });
};
const updateSectionBlock = (sectionId, blockType) => {
const block = getLongformBlockType(blockType);
const sections = task.sections.map(section => section.id === sectionId
? normalizeLongformSection({ ...section, blockType: block.id, type: block.type, layoutModule: block.layoutModule }, 0)
: section);
patchTask({ sections, status: 'Block DSL 已更新' });
};
const autoAssignImages = () => {
const validSectionIds = new Set(task.sections.map(section => section.id));
const normalizedImages = task.images.map(img => validSectionIds.has(img.assignedSectionId) ? img : { ...img, assignedSectionId: '' });
const unassignedImages = normalizedImages.filter(img => !img.assignedSectionId);
const sectionLoad = new Map(task.sections.map(section => [
section.id,
normalizedImages.filter(img => img.assignedSectionId === section.id).length,
]));
const emptySections = task.sections.filter(section => (sectionLoad.get(section.id) || 0) === 0);
const nextSectionFor = (index) => {
if (index < emptySections.length) return emptySections[index];
return task.sections.reduce((best, section) => {
if (!best) return section;
return (sectionLoad.get(section.id) || 0) < (sectionLoad.get(best.id) || 0) ? section : best;
}, null);
};
let assignedCount = 0;
const images = normalizedImages.map(img => {
if (img.assignedSectionId) return img;
const nextSection = nextSectionFor(assignedCount);
if (!nextSection) return img;
sectionLoad.set(nextSection.id, (sectionLoad.get(nextSection.id) || 0) + 1);
assignedCount += 1;
return { ...img, assignedSectionId: nextSection.id };
});
const sections = syncLongformSectionsWithImages(task.sections, images);
const configuredCount = images.filter(img => img.assignedSectionId).length;
const reply = unassignedImages.length
? `已按 section 顺序完成一键配图:新增分配 ${assignedCount} 张,当前 ${configuredCount}/${images.length} 张图片已配置进内容推演。`
: `当前 ${configuredCount}/${images.length} 张图片已配置进内容推演,无需新增分配。`;
const messages = [...chatMessages, { role: 'assistant', text: reply }];
setChatMessages(messages);
patchTask({ images, sections, messages, status: 'HTML 已生成' });
publishThinking({
modelStatus,
generationStage: '一键配图完成',
lastAgentMessage: reply,
}, { ...task, images, sections, messages, status: 'HTML 已生成' }, messages);
};
const applyOverride = (constraintId, reason) => {
const clean = reason.trim();
if (!clean) return;
const overrides = task.overrides.some(o => o.constraintId === constraintId)
? task.overrides.map(o => o.constraintId === constraintId ? { ...o, reason: clean, createdAt: new Date().toLocaleString('zh-CN', { hour12: false }) } : o)
: [...task.overrides, { constraintId, reason: clean, createdAt: new Date().toLocaleString('zh-CN', { hour12: false }) }];
patchTask({ overrides });
};
const removeOverride = (constraintId) => patchTask({ overrides: task.overrides.filter(o => o.constraintId !== constraintId) });
const exportPng = async () => {
setExportState('preparing');
try {
if (!window.html2canvas) throw new Error('html2canvas 未加载');
const iframe = document.createElement('iframe');
iframe.style.position = 'fixed';
iframe.style.left = '-1200px';
iframe.style.top = '0';
iframe.style.width = '1080px';
iframe.style.height = '1px';
iframe.style.border = '0';
iframe.srcdoc = task.html || buildLongformHtml(task, brand);
document.body.appendChild(iframe);
await new Promise((resolve, reject) => {
iframe.onload = resolve;
iframe.onerror = () => reject(new Error('导出 iframe 加载失败'));
});
const doc = iframe.contentDocument;
const node = doc.querySelector('.montx');
if (!node) throw new Error('找不到长图预览容器');
const images = Array.from(doc.images || []);
await Promise.all(images.map(img => img.complete ? Promise.resolve() : new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve;
})));
iframe.style.height = `${node.scrollHeight}px`;
const exportTheme = getLongformTheme(task.skillMarkdown || MONTX_DEFAULT_SKILL);
const canvas = await window.html2canvas(node, {
backgroundColor: exportTheme.bg,
scale: 1,
useCORS: true,
allowTaint: true,
width: node.scrollWidth,
height: node.scrollHeight,
windowWidth: node.scrollWidth,
windowHeight: node.scrollHeight,
});
iframe.remove();
const link = document.createElement('a');
link.download = `${task.title || 'longform-agent'}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
setExportState('success');
} catch (err) {
document.querySelectorAll('iframe').forEach(frame => {
if (frame.style.left === '-1200px') frame.remove();
});
console.error(err);
setExportState(`failed: ${err.message}`);
}
};
return (
Longform Agent · {task.title}
独立长文推演工作台:{brand.name}{product ? ` · ${product.name}` : ''} · 继承品牌护栏 · 生成 1080px HTML 长图并导出 PNG
{(brand.products || []).length > 0 && (
)}
{LONGFORM_STEPS.map((s, i) => (
))}
图片池
{task.images.length}
{!imagesCollapsed && (
{task.images.map(img => (
{img.name}
))}
)}
patchTask({ title })}
modelStatus={modelStatus}
/>
{mainView === 'sections' ? `${task.sections.length} sections` : mainView === 'preview' ? '1080px preview' : `${inspect.issues.length} risks`}
{task.status}
{task.sections.map((s, i) => {
const sectionImages = task.images.filter(img => img.assignedSectionId === s.id);
const pickerOpen = imagePickerSectionId === s.id;
return (
{String(i + 1).padStart(2, '0')}
{s.title}
模块
{getLongformBlockType(s).description}
{s.body}
配图建议:{getLongformImageHint(s, i)}
{sectionImages.map(img => (
))}
{pickerOpen && (
{task.images.length ? task.images.map(img => (
)) :
图片池为空,请先上传本地图片。
}
)}
);
})}
);
};
const ConstraintOverridePanel = ({ brand, task, onApply, onRemove }) => {
const [reasons, setReasons] = React.useState({});
const [collapsed, setCollapsed] = React.useState(true);
return (
setCollapsed(v => !v)}>
品牌护栏继承
{brand.constraints.length}
·
{task.overrides.length}
{!collapsed && (
{brand.constraints.map(c => {
const override = task.overrides.find(o => o.constraintId === c.id);
return (
{c.label}
{override ? `已解除:${override.reason}` : c.description}
{override ? (
) : (
setReasons(r => ({ ...r, [c.id]: e.target.value }))}/>
)}
);
})}
)}
);
};
const LongformChatComposer = ({ brand, task, messages, input, onInput, onSend, onImportBootstrap, onTitleChange, modelStatus }) => {
const streamRef = React.useRef(null);
React.useEffect(() => {
const node = streamRef.current;
if (!node) return;
node.scrollTop = node.scrollHeight;
}, [messages, modelStatus]);
const statusLabel = {
idle: 'local draft',
classifying: 'intent check',
importing: 'importing',
imported: 'brief ready',
thinking: 'DeepSeek',
success: 'model done',
fallback: 'local fallback',
}[modelStatus] || 'local draft';
return (
对话推演
{statusLabel}
护栏 {brand.constraints.length}
{brand.name} 护栏状态
{brand.constraints.map(rule => {
const override = (task.overrides || []).find(item => item.constraintId === rule.id);
return (
{rule.label}
{override ? `Override:${override.reason}` : 'Locked · 默认继承'}
);
})}
Override {task.overrides.length}
命令:查看护栏 / 解除护栏:规则名;原因:... / 恢复护栏:规则名
onTitleChange(e.target.value)} aria-label="任务标题"/>
{messages.map((message, i) => (
{message.role === 'assistant' ? 'Agent' : 'You'}
{message.text}
))}
);
};
const SkillInspector = ({ markdown }) => {
const skill = parseSkillMarkdown(markdown);
return (
Skill Inspector
{skill.title}
{skill.hex.map(c => {c})}
{skill.components.map(c => {c})}
);
};
const LongformInspectPanel = ({ inspect, task }) => (
READINESS
{inspect.issues.length ? `${inspect.issues.length} 个风险项` : '可进入导出检查'}
Inspect 用于在微信 HTML / PNG 交付前检查 Brief、Block DSL、图片、护栏和公众号兼容性。
BLOCK DSL
{(task.sections || []).map(section => getLongformBlockType(section).label).join(' / ')}
当前 DSL 会随草稿保存,后续可直接映射到微信富文本模块。
{inspect.groups.map(group => (
{group.title}
{group.items.map(item => (
{item.label}
{item.value}
{item.detail && {item.detail}}
))}
))}
);
const SkillMiniInspector = ({ skill }) => (
{skill.hex.map(c => {c})}
{skill.components.join(' / ') || 'No component heading parsed'}
);
const LongformThinkingRail = () => {
const [ctx, setCtx] = React.useState(() => window.__LONGFORM_THINKING_CONTEXT || { mode: 'list', tasks: LONGFORM_MOCK_TASKS, brand: LONGFORM_BRANDS[0] });
const [history, setHistory] = React.useState([]);
const thinkingBodyRef = React.useRef(null);
const latestThinkingRef = React.useRef(null);
React.useEffect(() => {
const update = () => setCtx(window.__LONGFORM_THINKING_CONTEXT || ctx);
window.addEventListener('longform-thinking-change', update);
return () => window.removeEventListener('longform-thinking-change', update);
}, []);
const task = ctx.task;
const brand = ctx.brand || getLongformBrand(task?.brandId);
const skill = ctx.skill || parseSkillMarkdown(task?.skillMarkdown || MONTX_DEFAULT_SKILL);
const tasks = ctx.tasks || [];
const status = ctx.modelStatus || 'idle';
const stage = ctx.generationStage || LONGFORM_MODEL_STAGE[status] || '待输入需求';
const validSectionIds = task ? new Set(task.sections.map(section => section.id)) : new Set();
const unassigned = task ? task.images.filter(i => !i.assignedSectionId || !validSectionIds.has(i.assignedSectionId)).length : 0;
const assigned = task ? task.images.filter(i => i.assignedSectionId && validSectionIds.has(i.assignedSectionId)).length : 0;
const checks = task ? [
['生成阶段', stage, status === 'fallback' ? 'warn' : status === 'thinking' ? 'idle' : 'ok'],
['1080px 画布', '通过', 'ok'],
['图片压字', '未检测到', 'ok'],
['未分配图片', `${unassigned} 张`, unassigned ? 'warn' : 'ok'],
['解除护栏', `${task.overrides.length} 项`, task.overrides.length ? 'warn' : 'ok'],
['Markdown skill', skill.title, task.skillMarkdown ? 'ok' : 'warn'],
['导出状态', ctx.exportState || '待导出', String(ctx.exportState).startsWith('failed') ? 'warn' : ctx.exportState === 'success' ? 'ok' : 'idle'],
] : [
['品牌护栏', `${brand.constraints.length} required`, 'ok'],
['任务草稿', `${tasks.length} drafts`, 'idle'],
['Override 风险', `${tasks.filter(t => t.overrides?.length).length} tasks`, tasks.some(t => t.overrides?.length) ? 'warn' : 'ok'],
];
const userSummary = ctx.lastUserMessage
? `最近需求:${ctx.lastUserMessage.slice(0, 52)}${ctx.lastUserMessage.length > 52 ? '…' : ''}`
: ctx.currentInput
? `待发送输入:${ctx.currentInput.slice(0, 52)}${ctx.currentInput.length > 52 ? '…' : ''}`
: '等待用户输入需求';
const agentSummary = ctx.lastAgentMessage
? `最近回复:${ctx.lastAgentMessage.slice(0, 52)}${ctx.lastAgentMessage.length > 52 ? '…' : ''}`
: '等待 Agent 输出';
const modelMessage = status === 'thinking'
? 'DeepSeek 调用中,等待结构化 sections 返回'
: status === 'success'
? 'DeepSeek 已返回,结构生成完成'
: status === 'fallback'
? `已进入本地兜底${ctx.modelError ? `:${ctx.modelError}` : ''}`
: status === 'importing' || status === 'imported'
? stage
: '模型待命';
const lines = task ? [
['requirement-agent', userSummary],
['model-agent', modelMessage],
['section-agent', `当前结构 ${task.sections.length} 个 section`],
['style-skill-agent', `模板:${ctx.styleTemplateName || skill.title} · 版式:${getLongformLayoutPreset(skill).label} · 读取 ${skill.components.length} 个组件规则`],
['image-mapping-agent', `${assigned}/${task.images.length} 张图片已分配`],
['brand-guardrail', `${brand.constraints.length - task.overrides.length}/${brand.constraints.length} 条护栏保持锁定`],
['html-layout-agent', status === 'thinking' ? '等待模型返回后刷新 HTML 预览' : '预览容器固定 1080px,可转 PNG'],
['message-agent', agentSummary],
] : [
['workspace-policy', `${brand.name} 品牌护栏已加载`],
['draft-index', `IndexedDB 草稿列表:${tasks.length} 个任务`],
['risk-scan', `发现 ${tasks.filter(t => t.qa?.issues?.length || t.overrides?.length).length} 个需关注任务`],
];
React.useEffect(() => {
const signature = `${task?.id || 'list'}|${status}|${stage}|${ctx.lastUserMessage || ''}|${ctx.lastAgentMessage || ''}|${assigned}|${unassigned}|${skill.title}|${tasks.length}`;
const stamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
setHistory(prev => {
if (prev[prev.length - 1]?.signature === signature) return prev;
return [...prev.slice(-18), { signature, stamp, stage, lines }];
});
requestAnimationFrame(() => {
latestThinkingRef.current?.scrollIntoView({ block: 'end' });
});
}, [task?.id, status, stage, ctx.lastUserMessage, ctx.lastAgentMessage, assigned, unassigned, skill.title, tasks.length]);
return (
);
};
const LongformProcessPanel = ({ task, brand, skill, exportState }) => (
思考过程 · Longform Agent
QA {task.qa.score}
PNG 导出检查
!i.assignedSectionId).length} 张`} warn={task.images.some(i => !i.assignedSectionId)}/>
0}/>
{task.qa.issues?.map(issue => (
{issue}
))}
Longform Thinking
{[
['requirement-agent', '已从输入中提取目标、读者和文章类型'],
['longform-agent', '生成 5 段式长图文叙事骨架'],
['style-skill-agent', `读取 ${skill.components.length} 个组件规则`],
['image-mapping-agent', `${task.images.filter(i => i.assignedSectionId).length}/${task.images.length} 张图片已分配`],
['brand-guardrail', `${brand.constraints.length - task.overrides.length}/${brand.constraints.length} 条护栏保持锁定`],
['html-layout-agent', '预览容器固定 1080px,可转 PNG'],
].map(([src, msg], i) => (
))}
Skill Inspector
{skill.hex.map(c => {c})}
{skill.components.map(c => {c})}
);
const LongformQaPanel = ({ task, brand, skill, exportState }) => (
PNG 导出检查
QA {task.qa.score}
!i.assignedSectionId).length} 张`} warn={task.images.some(i => !i.assignedSectionId)}/>
0}/>
{task.qa.issues?.length > 0 && (
{task.qa.issues.map(issue => {issue})}
)}
);
const LongformCanvas = ({ task, refEl, scale = 1 }) => {
const layout = getLongformLayoutPreset(task.skillMarkdown || MONTX_DEFAULT_SKILL);
const overrideClass = getLongformRenderOverrideClass(task);
return (
{task.sections.map((s, i) => {
const imgs = task.images.filter(img => img.assignedSectionId === s.id);
const imageNodes = imgs.map(img =>

);
if (s.type === 'Header') {
return
{s.title}
{s.body}
{imageNodes}
;
}
if (s.type === 'Image') {
return imgs.length ? imageNodes : (
{String(i).padStart(2, '0')}
{s.title}
{s.body}
[ 图片待上传并分配 ]
);
}
if (s.type === 'Grid') {
return
{imageNodes};
}
if (s.type === 'Footer') {
return
{imageNodes};
}
return (
{String(i).padStart(2, '0')}
{s.title}
{s.body}
{imageNodes}
);
})}
);
};
Object.assign(window, {
LongformAgent,
LongformThinkingRail,
useLongformBrandProfiles,
getLongformBrand,
getActiveLongformConstraints,
normalizeLongformBrandProfiles: normalizeSharedBrandProfiles,
BrandProductMaintenancePanel,
GuardrailMaintenancePanel,
});