/* ===== Screens Part 3: Brief, Content, Monitor, Reports, Review ===== */ // ========== 05 · BRIEF COMPOSER ========== const UNIVERSAL_CONTENT_BOUNDARIES = { hardBlocks: [ '违法犯罪、武器制作、恐怖主义、组织仇恨、人口贩运、诈骗和禁限售交易', '儿童安全、性剥削、未成年人裸露、诱导未成年人互动', '仇恨言论、定向骚扰、威胁、网暴动员和人肉搜索', '自伤自杀、危险挑战、会诱导模仿的不安全行为', '隐私泄露、身份证件/住址/联系方式/黑客数据', '钓鱼、恶意软件、虚假抽奖、金字塔骗局、误导性外链', '未授权音乐/视频/图片/商标/达人素材复用', ], cautionClaims: [ '医疗、金融、法律、安全、就业、公共事件、灾害、政策和危机信息必须有来源', '产品能力、续航、价格、交付、销量、排名、用户收益不得绝对化', 'AIGC 或显著编辑的真实人物/真实场景需按平台要求标识', '商业合作、赠品、佣金、联盟链接、达人激励必须披露', '禁止刷量、买粉、互关互赞、评论农场、重复铺量和诱导互动', ], }; const PLATFORM_CONTENT_BOUNDARIES = [ { id: 'xhs', platform: '小红书', family: 'visual-search-commerce', source: 'GitHub 家族映射 + 国内平台官方源待接入', formatRules: ['真实体验优先,商业笔记需显性标注', '封面/标题不能制造虚假结果、虚假测评或虚假用户经历', '图文证据需能支撑前后对比、清单推荐和排名判断'], linkRules: ['私域引流、二维码、抽奖、外链购买和导流话术需单独审核', '不使用“评论区见”“私信发链接”规避平台规则'], creatorRules: ['达人必须披露合作关系、赠品或试用权益', '不能伪装素人自发体验或虚构购买记录'], reviewTriggers: ['全网最低、闭眼入、官方认证、必买、治疗/收益/安全承诺', '竞品拉踩、截图证据、用户评论截图、价格和库存承诺'], }, { id: 'douyin', platform: '抖音 / TikTok-style', family: 'short-video-recommendation', source: 'OpenTermsArchive TikTok Community Guidelines + 抖音官方源待接入', formatRules: ['前 3 秒钩子不能误导能力、价格、结果、风险或公共事件', '危险动作、驾驶/设备演示和挑战类内容必须避免诱导模仿', '真实人物或真实场景 AIGC 需按平台要求标识'], linkRules: ['直播、私信、橱窗、外链和私域导流需检查交易和合规风险', '禁止以承诺福利、破解、返利诱导跳转'], creatorRules: ['品牌合作、付费口播、赠品试用、佣金和挂链必须披露', '不得使用刷量、互赞、评论脚本或虚假评价'], reviewTriggers: ['挑战、实测、秒杀、限时抢、很快到账、无人驾驶、真实事故/灾害画面', 'AI 换脸、仿声、公众人物表态或产品背书'], }, { id: 'wechat-official', platform: '微信公众号', family: 'longform-editorial', source: 'OpenTermsArchive WeChat Acceptable Use Policy + 微信公众平台官方源待接入', formatRules: ['标题、封面和摘要不得标题党、恐慌化或假冒官方通知', '长文需区分事实、推断、观点和品牌立场', '技术、政策和行业判断需保留来源与边界'], linkRules: ['外链、小程序、二维码、表单收集、抽奖和诱导分享需审核', '不得导向诈骗、违规交易、侵权下载或隐私收集'], creatorRules: ['转载、引用、配图、数据图和第三方素材需保留授权或来源', '评论精选和回复不得泄露用户隐私或升级争议'], reviewTriggers: ['政策解读、行业排名、技术参数、价格交付、融资/收益、抽奖福利', '医疗/金融/法律/公共安全类表述'], }, { id: 'wechat-channels', platform: '视频号', family: 'wechat-short-video-live', source: 'OpenTermsArchive WeChat Acceptable Use Policy + 视频号官方源待接入', formatRules: ['短视频钩子不能夸大承诺或制造未证实事实', '直播口播的价格、库存、交付、能力和福利需预审', '评论区承接需避免争吵、隐私泄露和未授权承诺'], linkRules: ['私域添加、企业微信、群聊、二维码、直播间转化链路需单独审查', '不以福利承诺诱导用户离开平台完成高风险交易'], creatorRules: ['官方账号、达人账号和员工账号身份要清楚', '品牌合作和达人权益不得隐藏'], reviewTriggers: ['直播优惠、限量、交付时间、配置确定、私信领取、群内报价', '用户投诉、事故、负面评论和售后承诺'], }, { id: 'youtube', platform: 'YouTube', family: 'long-video-search', source: 'OpenTermsArchive YouTube Community Guidelines', formatRules: ['标题、缩略图、描述和置顶评论必须匹配实际内容', '长测/技术视频需展示测试条件、样本和限制', '避免危险行为、误导元数据、重复搬运和未授权第三方内容'], linkRules: ['描述、口播、画面 URL 和置顶评论都算外链风险', '不得导向成人、诈骗、恶意软件、侵权下载、仇恨/骚扰或禁限售交易'], creatorRules: ['赞助、联盟链接、赠品、借测和付费推广需披露', '不得买量、互订、诱导订阅交换或虚假互动'], reviewTriggers: ['缩略图夸张承诺、未授权音乐/素材、外链下载、枪械/危险操作、医疗误导、AI 公共事件'], }, { id: 'instagram', platform: 'Instagram', family: 'visual-social', source: 'OpenTermsArchive Instagram / Facebook Community Guidelines', formatRules: ['账号身份、品牌资产和视觉素材不得造成冒充或虚假来源', 'Reels / Stories 不得使用震惊、血腥、性化或骚扰性创意', '付费内容标准比自然帖更严'], linkRules: ['bio link、贴纸、私信和购物标签需检查诈骗、侵权和敏感品类风险', '不得利用私人困难或敏感属性做定向诱导'], creatorRules: ['创作者关系、素材授权、音乐和 logo 使用需清楚', '不得伪装官方、伪装达人或规避封禁'], reviewTriggers: ['before/after、用户肖像、音乐授权、抽奖、敏感属性暗示、竞品 logo', '公共人物 AI 图像或仿声'], }, { id: 'weibo-x', platform: '微博 / X-style', family: 'public-conversation', source: 'OpenTermsArchive X Community Guidelines + 微博官方源待接入', formatRules: ['热点参与必须与品牌事实相关,不借灾害、公共事件或争议蹭话题', '短文本声明需可被截图独立理解', '避免引战、网暴动员和与用户争夺解释权'], linkRules: ['话题、转评、抽奖、外链和私信承接需检查诱导互动和诈骗风险', '不发布隐私信息、截图曝光或未授权聊天记录'], creatorRules: ['达人转发和话题共创需披露合作关系', '不得组织水军、刷话题、控评或伪造声量'], reviewTriggers: ['热搜借势、声明回应、竞品事故、用户投诉、抽奖、价格战', '政治/公共安全/灾害相关词'], }, { id: 'bilibili', platform: 'B站 / YouTube long-form style', family: 'community-long-video', source: 'OpenTermsArchive YouTube Community Guidelines + B站官方源待接入', formatRules: ['标题/封面不能承诺视频里没有交付的测评结论', '技术拆解和实测需标注条件、方法、限制和样本', '避免危险驾驶、设备误用和可模仿的高风险操作'], linkRules: ['简介、评论置顶、动态和充电/店铺跳转需检查侵权、诈骗和导流风险', '第三方下载、未授权素材包和敏感交易链接禁用'], creatorRules: ['恰饭、借测、送测、赞助、联合出品和素材来源需披露', '弹幕/评论引导不能煽动攻击竞品或用户'], reviewTriggers: ['首发爆料、实测排行、拆机、事故画面、竞品对比、未授权 BGM/影视素材', '无人驾驶/安全能力绝对化表达'], }, ]; const AIGC_PLATFORM_VARIANT_RULES = [ { id: 'xhs-seeding', platform: '小红书', label: '图文 / 短视频种草', len: '短视频 + 笔记', tone: '真实体验 · 搜索友好', hooks: ['标题承接统一主题,但更短更可搜', '前三行给场景或问题,不伪装素人体验', '封面和正文证据必须能支撑卖点'] }, { id: 'wechat-channels-short-video', platform: '视频号', label: '短视频 / 直播承接', len: '15-90s', tone: '官方口播 · 场景解释', hooks: ['首行直接进入主题', '口播保留概念车边界', '评论区承接不做价格/交付承诺'] }, { id: 'wechat-mp-longform', platform: '微信公众号', label: '长图文 / 深度解释', len: '800-1800 字', tone: '官方叙事 · 解释型', hooks: ['标题稳定承接统一主题', '正文区分事实、概念、愿景和活动信息', '适合放完整 CTA 与风险边界'] }, { id: 'youtube-video-search', platform: 'YouTube', label: '视频搜索 / 海外解释', len: '30s-3min', tone: 'English · clear claim boundary', hooks: ['Title must match the actual footage', 'Description explains concept limits', 'Avoid unsupported performance or delivery claims'] }, { id: 'instagram-visual-social', platform: 'Instagram', label: 'Reels / 视觉社交', len: 'Reel / Post', tone: 'Visual launch signal', hooks: ['Theme line stays concise', 'Visual identity and source must be clear', 'No implied endorsement or fake partnership'] }, ]; const isBriefExternalSocialPost = (post = {}) => { const type = `${post.source_type || ''} ${post.asset_type || ''} ${post.source_post_type || ''} ${post.relationship_status || ''}`.toLowerCase(); return Boolean( post.is_official_post === false || type.includes('earned') || type.includes('ugc') || type.includes('kol') || type.includes('external') ); }; const getBriefContentCount = (brief = {}, postLookup = null) => { const item = brief || {}; if (postLookup) { const ownedAssetCount = (item.assetPostIds || []).filter(id => postLookup.has(id)).length; return ownedAssetCount || (item.platformVariants || []).length || 0; } return (item.assetPostIds || []).length || (item.platformVariants || []).length || 0; }; const getBriefPublishState = (brief = {}) => { const raw = String(brief?.status || '').toLowerCase(); if (['published', 'active', 'live', 'ready'].includes(raw)) return 'published'; if (['draft', 'pending', 'unpublished'].includes(raw)) return 'draft'; return getBriefContentCount(brief) ? 'published' : 'draft'; }; const getBriefPublishStatusMeta = (brief = {}) => { const state = getBriefPublishState(brief); return state === 'published' ? { state, label: '已发布', tagClass: 'accent' } : { state, label: '草稿', tagClass: '' }; }; const getBriefPostDate = (post = {}) => post.published_at || post.publish_time || post.created_at || ''; const makePostLookup = (posts = []) => { const map = new Map(); posts.forEach(post => { [post.post_id, post.content_id, post.id].filter(Boolean).forEach(id => map.set(id, post)); }); return map; }; const getBriefAssetPosts = (brief = {}, posts = []) => { const ids = new Set((brief?.assetPostIds || []).filter(Boolean)); if (!ids.size) return []; return posts.filter(post => ids.has(post.post_id) || ids.has(post.content_id) || ids.has(post.id)); }; const getBriefSortTime = (brief = {}, postLookup = null) => { const item = brief || {}; const assetDates = (item.assetPostIds || []) .map(id => postLookup?.get(id)) .filter(Boolean) .map(getBriefPostDate); const contentDates = [ ...assetDates, ...(item.platformVariants || []).map(variant => variant.publishedAt), ].filter(Boolean).map(value => { const t = new Date(String(value).replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')).getTime(); return Number.isFinite(t) ? t : 0; }).filter(Boolean); if (contentDates.length) return Math.max(...contentDates); const fallbackDates = [ item.endDate, item.startDate, item.updatedAt, item.createdAt, ].filter(Boolean).map(value => { const t = new Date(String(value).replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')).getTime(); return Number.isFinite(t) ? t : 0; }); return Math.max(0, ...fallbackDates); }; const formatBriefDate = (brief = {}, postLookup = null) => { const t = getBriefSortTime(brief, postLookup); if (!t) return '未排期'; return new Date(t).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }); }; const briefTypeLabel = (brief = {}) => { const item = brief || {}; return item.parentCampaignId ? '子 Brief' : item.type === 'campaign' ? 'Campaign' : item.type === 'series' ? '栏目' : 'Brief'; }; const getAssignedBriefId = (post = {}) => post.primaryMasterBriefId || post.briefMapping?.primaryMasterBriefId || ''; const getBriefOptionLabel = (brief = {}, postLookup = null) => { const count = getBriefContentCount(brief, postLookup); return `${brief.name || brief.id}${count ? ` · ${count}` : ''}`; }; const cleanBriefText = (value = '') => { if (typeof value !== 'string') return ''; return value .replace(/<[^>]+>/g, ' ') .replace(/ | /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/\s+/g, ' ') .trim(); }; const getBriefPostTitle = (post = {}) => cleanBriefText(post.title || post.caption?.title || post.raw?.title || '未命名内容'); const getBriefPostBody = (post = {}) => [ post.body, post.caption?.text, post.caption?.content, post.caption, post.description, post.text, post.raw?.desc, post.raw?.Digest, post.raw?.digest, post.raw?.note_card?.desc, post.raw?.object_desc?.description, post.raw?.description, post.raw?.caption, post.raw?.edge_media_to_caption?.edges?.[0]?.node?.text, ].map(cleanBriefText).find(value => value) || ''; const getBriefPostLabel = (post = {}) => { const date = getBriefPostDate(post) || '未排期'; const platform = post.platform || '平台'; const title = getBriefPostTitle(post); return `${date} · ${platform} · ${title}`.slice(0, 120); }; const Brief = () => { const [campaigns, setCampaigns] = React.useState([]); const [briefId, setBriefId] = React.useState(''); const [marketHandoffs, setMarketHandoffs] = React.useState([]); const [socialPosts, setSocialPosts] = React.useState([]); const [strategyThesis, setStrategyThesis] = React.useState(null); const [briefView, setBriefView] = React.useState('briefs'); const [mappingDrafts, setMappingDrafts] = React.useState({}); const [mappingStatus, setMappingStatus] = React.useState({}); const [briefCreateOpen, setBriefCreateOpen] = React.useState(false); const [newBriefTitle, setNewBriefTitle] = React.useState(''); const [editingTitleId, setEditingTitleId] = React.useState(''); const [titleDraft, setTitleDraft] = React.useState(''); const [briefMutationStatus, setBriefMutationStatus] = React.useState(''); React.useEffect(() => { fetch(socialApiUrl('/api/initiatives')) .then(response => response.json()) .then(data => { const rows = data.initiatives || data.campaigns || []; setCampaigns(rows); setBriefId(current => current || [...rows].sort((a, b) => getBriefSortTime(b) - getBriefSortTime(a))[0]?.id || ''); }) .catch(() => {}); fetch(socialApiUrl('/api/market/handoffs?target_module=strategy&limit=5')) .then(response => response.json()) .then(data => setMarketHandoffs(data.handoffs || [])) .catch(() => setMarketHandoffs([])); fetch(socialApiUrl('/api/social/sync/status')) .then(response => response.json()) .then(data => setSocialPosts(data.posts || [])) .catch(() => setSocialPosts([])); fetch(socialApiUrl('/api/strategy/thesis')) .then(response => response.json()) .then(data => setStrategyThesis(data.strategyThesis || null)) .catch(() => setStrategyThesis(null)); }, []); const ownedSocialPosts = React.useMemo(() => socialPosts.filter(post => !isBriefExternalSocialPost(post)), [socialPosts]); const postLookup = React.useMemo(() => makePostLookup(ownedSocialPosts), [ownedSocialPosts]); const sortedBriefs = React.useMemo( () => [...campaigns].sort((a, b) => getBriefSortTime(b, postLookup) - getBriefSortTime(a, postLookup) || getBriefContentCount(b, postLookup) - getBriefContentCount(a, postLookup) || String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN')), [campaigns, postLookup] ); const activeCampaign = sortedBriefs.find(item => item.id === briefId) || sortedBriefs[0] || null; const configurablePosts = React.useMemo( () => [...ownedSocialPosts].filter(post => post.post_id || post.content_id || post.id).sort((a, b) => getPostTimeValue(b) - getPostTimeValue(a)), [ownedSocialPosts] ); const upsertBrief = React.useCallback((nextBrief) => { if (!nextBrief?.id) return; setCampaigns(prev => { const exists = prev.some(item => item.id === nextBrief.id); return exists ? prev.map(item => item.id === nextBrief.id ? nextBrief : item) : [nextBrief, ...prev]; }); }, []); const createBrief = async () => { const title = newBriefTitle.trim(); if (!title) { setBriefMutationStatus('请先填写 Brief 标题'); return; } setBriefMutationStatus('creating'); try { const response = await fetch(socialApiUrl('/api/initiatives'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ type: 'standalone', name: title, status: 'draft', stage: 'Brief 编排', goals: [], platformFocus: [], assetPostIds: [], assetCampaignLabels: [title], masterBrief: { title: `${title} Master Brief` }, }), }); const data = await response.json(); if (!response.ok || data.ok === false) throw new Error(data.error || data.message || 'Brief 创建失败'); upsertBrief(data.initiative); setBriefId(data.initiative.id); setBriefCreateOpen(false); setNewBriefTitle(''); setBriefMutationStatus('created'); window.setTimeout(() => setBriefMutationStatus(''), 1200); } catch (error) { setBriefMutationStatus(error.message || 'Brief 创建失败'); } }; const beginTitleEdit = () => { if (!activeCampaign?.id) return; setEditingTitleId(activeCampaign.id); setTitleDraft(activeCampaign.name || ''); setBriefMutationStatus(''); }; const saveBriefTitle = async () => { if (!activeCampaign?.id) return; const nextTitle = titleDraft.trim(); if (!nextTitle) { setBriefMutationStatus('标题不能为空'); return; } setBriefMutationStatus('saving-title'); try { const response = await fetch(socialApiUrl(`/api/initiatives/${encodeURIComponent(activeCampaign.id)}`), { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: nextTitle, assetCampaignLabels: activeCampaign.assetCampaignLabels?.length ? activeCampaign.assetCampaignLabels : [nextTitle], }), }); const data = await response.json(); if (!response.ok || data.ok === false) throw new Error(data.error || data.message || '标题保存失败'); upsertBrief(data.initiative); setEditingTitleId(''); setBriefMutationStatus('saved-title'); window.setTimeout(() => setBriefMutationStatus(''), 1200); } catch (error) { setBriefMutationStatus(error.message || '标题保存失败'); } }; const updateBriefPublishState = async (nextState) => { if (!activeCampaign?.id) return; setBriefMutationStatus('saving-status'); try { const response = await fetch(socialApiUrl(`/api/initiatives/${encodeURIComponent(activeCampaign.id)}`), { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ status: nextState === 'published' ? 'published' : 'draft' }), }); const data = await response.json(); if (!response.ok || data.ok === false) throw new Error(data.error || data.message || '状态保存失败'); upsertBrief(data.initiative); setBriefMutationStatus('saved-status'); window.setTimeout(() => setBriefMutationStatus(''), 1200); } catch (error) { setBriefMutationStatus(error.message || '状态保存失败'); } }; const assignPostBriefFromBrief = async (post, targetBriefId) => { const postId = post?.post_id || post?.content_id || post?.id || ''; if (!postId) return; setMappingStatus(prev => ({ ...prev, [postId]: 'saving' })); try { const response = await fetch(socialApiUrl('/api/social/posts/brief'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ postId, briefId: targetBriefId }), }); const data = await response.json(); if (!response.ok || data.ok === false) throw new Error(data.error || data.message || 'Brief 配置失败'); setSocialPosts(data.posts || []); setCampaigns(data.initiatives || campaigns); setMappingDrafts(prev => ({ ...prev, [postId]: targetBriefId || '' })); setMappingStatus(prev => ({ ...prev, [postId]: 'saved' })); window.setTimeout(() => setMappingStatus(prev => ({ ...prev, [postId]: '' })), 1200); } catch (error) { setMappingStatus(prev => ({ ...prev, [postId]: error.message || 'error' })); } }; return (

Brief 编排器 · 从策略对象到执行的翻译层

Brief 按内容发布日期倒序排列;平台变体规则进入 AIGC 内容生成目录。

{briefView === 'briefs' && ( )}
{briefView === 'content-map' ? ( setMappingDrafts(prev => ({ ...prev, [postId]: briefId }))} onSave={assignPostBriefFromBrief} /> ) : (

Brief 列表

{sortedBriefs.length}
{sortedBriefs.map(item => { const statusMeta = getBriefPublishStatusMeta(item); return (
setBriefId(item.id)} style={{ padding:'11px 14px', borderLeft: activeCampaign?.id === item.id ? '2px solid var(--accent)' : '2px solid transparent', borderBottom:'1px solid var(--hairline)', background: activeCampaign?.id === item.id ? 'var(--accent-tint)' : 'transparent', cursor:'pointer', }} >
{item.name} {getBriefContentCount(item, postLookup)}
{formatBriefDate(item, postLookup)} · {briefTypeLabel(item)} {statusMeta.label}
); })}
{briefCreateOpen ? (
setNewBriefTitle(event.target.value)} onKeyDown={event => { if (event.key === 'Enter') createBrief(); if (event.key === 'Escape') { setBriefCreateOpen(false); setNewBriefTitle(''); } }} placeholder="输入 Brief 标题" autoFocus />
{briefMutationStatus && briefMutationStatus !== 'creating' && {briefMutationStatus === 'created' ? '已创建' : briefMutationStatus}}
) : ( )}
{editingTitleId === activeCampaign?.id ? (
setTitleDraft(event.target.value)} onKeyDown={event => { if (event.key === 'Enter') saveBriefTitle(); if (event.key === 'Escape') setEditingTitleId(''); }} style={{height:32, minWidth:0}} autoFocus />
) : (

{activeCampaign?.name || 'Brief'}

)} {getBriefContentCount(activeCampaign, postLookup)} 内容 {getBriefPublishStatusMeta(activeCampaign).label}
边界自检通过 {editingTitleId !== activeCampaign?.id && ( )}

品牌边界 · 自动注入

QA Checklist

8 / 8
{['品牌定位一致','命题清晰','人群边界','CTA 明确','平台语法适配','禁用词扫描','法务风险','审核流程'].map((c, i) => (
{c}
))}
)}
); }; const BriefMarketContextCard = ({ handoff }) => { const pack = handoff?.contextPack || {}; const snapshot = pack.market_snapshot || {}; const risks = pack.risk_notes || []; const whiteSpaces = pack.competitive_benchmark?.white_spaces || []; return (

Market Context

{handoff ? 'ready' : 'none'}
{handoff ? ( <>

{snapshot.summary}

{whiteSpaces.slice(0, 2).map(item => {item})} {risks.slice(0, 2).flatMap(item => item.review_reason || []).map(item => {item})}
) : (
暂无市场感知 handoff。
)}
); }; const BriefContentMappingView = ({ posts = [], briefs = [], postLookup = null, mappingDrafts = {}, mappingStatus = {}, onDraftChange, onSave }) => { const briefById = React.useMemo(() => new Map(briefs.map(brief => [brief.id, brief])), [briefs]); return (

内容挂载 · 根据内容主体选择 Brief

{posts.length}
这里只处理“已发内容 {'->'} Brief”的映射。先读发布日期、平台、标题和正文主体,再选择对应 Brief 保存。
发布日期 平台 标题 / 正文 目标 Brief 操作
{posts.map(post => { const postId = post.post_id || post.content_id || post.id; const currentBriefId = getAssignedBriefId(post); const draftBriefId = Object.prototype.hasOwnProperty.call(mappingDrafts, postId) ? mappingDrafts[postId] : currentBriefId; const currentBrief = briefById.get(currentBriefId); const status = mappingStatus[postId] || ''; const body = getBriefPostBody(post); return (
{getBriefPostDate(post) || '-'}
{post.platform || '-'}
{getBriefPostTitle(post)} {body || '正文为空;请根据标题、平台和原始内容链接判断。'} 当前 Brief:{currentBrief?.name || '未配置'}
{status && status !== 'saving' && {status === 'saved' ? '已保存' : status}}
); })} {!posts.length &&
暂无可挂载的已发内容。
}
); }; const MasterBriefBody = ({ brief, campaign, strategyThesis }) => ( <> {strategyThesis ? (
{strategyThesis.coreThesis} {strategyThesis.marketBelief}
{(strategyThesis.contentParadigm || []).slice(0, 9).map(item => {item})}
) : null}
{brief?.background || `${campaign?.name || '当前策略对象'} 的背景待补充。`}
{brief?.objective || '主目标待补充。'}
{brief?.audienceSummary || '目标人群待补充。'}
{brief?.coreMessage || campaign?.coreMessage || '核心命题待确认。'}
    {(brief?.sellingPoints?.length ? brief.sellingPoints : campaign?.sellingPoints || []).map(item =>
  1. {item}
  2. )}
{brief?.cta || campaign?.cta || 'CTA 待补充。'}
); const BriefMetaStrip = ({ brief, postLookup }) => (
postLookup.has(id)).length : (brief?.assetPostIds || []).length)}/>
); const BriefContainedContent = ({ brief = {}, socialPosts = [] }) => { const item = brief || {}; const variants = item.platformVariants || []; const assetPosts = getBriefAssetPosts(item, socialPosts).sort((a, b) => { const at = new Date(String(getBriefPostDate(a)).replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')).getTime() || 0; const bt = new Date(String(getBriefPostDate(b)).replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')).getTime() || 0; return bt - at; }); const rows = assetPosts.length ? assetPosts.map(post => ({ platform: post.platform || '-', title: post.title || post.caption || post.description || '未命名内容', publishedAt: getBriefPostDate(post), id: post.post_id || post.content_id || post.id })) : variants.map((variant, index) => ({ ...variant, id: `${variant.platform}-${variant.title}-${index}` })); if (!rows.length) { return (
暂无已挂接的平台内容。
); } return (
{rows.map(row => (
{row.platform} {row.title || row.unifiedTopic} {row.publishedAt || '-'}
))}
); }; const ContentBoundaryBriefBody = ({ brief, campaign }) => { const campaignBoundaries = brief?.contentBoundaries?.length ? brief.contentBoundaries : campaign?.contentBoundaries || ['延续品牌长期定位和本轮核心命题', '不要把短期热点凌驾于品牌记忆之上', '所有内容必须保留清晰 CTA']; const riskBoundaries = brief?.riskBoundaries?.length ? brief.riskBoundaries : campaign?.riskBoundaries || ['禁用夸张促销表达', '避免无证据竞品拉踩', '能力表达需带条件和场景']; return ( <>
Brief 只输出内容边界和风险约束;平台变体规则、达人开场、场景脚本、多达人改写版由 AIGC 内容生成模块继续叠加。
); }; const BoundaryNoteList = ({ items, tone = 'accent', columns = 1 }) => { const colors = { accent: ['var(--accent)', 'var(--accent-tint)'], warn: ['var(--warn)', 'var(--warn-tint)'], danger: ['var(--danger)', 'var(--danger-tint)'], info: ['var(--info)', 'var(--info-tint)'], }; const [color, bg] = colors[tone] || colors.accent; return (
{items.map(item => (
{item}
))}
); }; const PlatformBoundaryCard = ({ item }) => (
{item.platform}
{item.family}
source
{item.source}
); const MiniBoundaryGroup = ({ label, items, warn }) => (
{label}
); const CreatorContentVariantBody = ({ creator }) => ( <>
{creator.avatar}
{creator.handle}
适配风格:{creator.style}
{creator.opening}
); const AigcPlatformRuleBody = ({ rule }) => { const boundary = PLATFORM_CONTENT_BOUNDARIES.find(item => rule?.platform && (item.platform === rule.platform || item.platform.includes(rule.platform) || rule.platform.includes(item.platform))); return (
{rule.tone}
    {rule.hooks.map(item =>
  • {item}
  • )}
{boundary ? (
) : null}
); }; const Section = ({ label, children }) => (
{label}
{children}
); const KV = ({ label, value }) => (
{label}
{value}
); const BoundaryRow = ({ type, label, items }) => { const colors = { block: ['var(--danger)', 'var(--danger-tint)'], avoid: ['var(--warn)', 'var(--warn-tint)'], prefer: ['var(--accent)', 'var(--accent-tint)'] }; const [c, bg] = colors[type]; return (
{label}
{items.map(i => {i})}
); }; // ========== 06 · CONTENT GENERATION + QA ========== const PR_REVIEW_SKILL_SUMMARY = { name: 'Chief Public Relations Officer', principles: [ '危机中先处理情绪,再处理事实', '品牌不要和用户争夺解释权', '不要说“你误会了 / 本意不是 / 过度解读”', '能改文案就不要硬解释', ], }; const runPrContentAudit = (draft = {}) => { const text = `${draft.title || ''}\n${draft.hook || ''}\n${draft.body || ''}`; const rules = [ { level: 'S3', token: '玻璃心', note: '默认用户敏感,容易升级舆情' }, { level: 'S2', token: '你误会了', note: '品牌与用户争夺解释权' }, { level: 'S2', token: '本意不是', note: '先解释自己,未承接用户情绪' }, { level: 'S2', token: '过度解读', note: '指责用户解读,容易被截图传播' }, { level: 'S2', token: '牛马', note: '品牌替用户自嘲,存在冒犯风险' }, { level: 'S2', token: '买不起', note: '可能被理解为品牌轻视用户' }, { level: 'S2', token: '秒杀', note: '过强促销/对比表达,需改写' }, { level: 'S1', token: '很快', note: '模糊能力表达,建议量化' }, { level: 'S1', token: '绝了', note: '口语化夸张,需判断是否符合品牌语气' }, ]; const hits = 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 CONTENT_DRAFTS = [ { title: '当城市开始静音,奥瑞利亚把"家"开到了夜里', hook: '前3行:雨后停车场一滴水声', body: '以静奢为锚。女儿在后排睡着。一公里路,像时间被装进抽屉。', platform: '小红书', score: 92, deviation: 0.04 }, { title: '26dB · 比你家卧室还安静的车', hook: '前3行:数字对比 · 引发好奇', body: '26dB 意味着什么?我们请 4 位父亲在车里读睡前故事。', platform: '小红书', score: 88, deviation: 0.08 }, { title: '第二台车,为什么越来越多人选静音?', hook: '前3s:静音开关声对比', body: '15 秒切片 · 3 个场景 · 0 参数。', platform: '抖音', score: 85, deviation: 0.12 }, { title: '夜归的人,需要怎样的车位', hook: '图集 · 9 张夜间停车美学', body: '回家那一刻才是真正的驾驶。', platform: '微博', score: 79, deviation: 0.18 }, ]; const CREATOR_CONTENT_VARIANTS = [ { id: 'muxi', handle: '@木兮的车库', avatar: '木', platform: '小红书', role: '策略型', style: '家庭视角 · 细腻克制', must: ['必须出现 3 岁女儿场景', '必须出现夜间小区停车', '必须出现后排拉开'], avoid: ['避免过度对比', '避免讲 0-100 加速'], opening: '"上周末接小朋友下课,雨刚停,车位一片水光。我第一次觉得,静是一种可以被看见的奢侈。"' }, { id: 'wlsc', handle: '@未来说车', avatar: '未', platform: '抖音', role: '策略型', style: '理性工程 · 拆解型', must: ['静音实测数据', '夜充全流程录制', '对比传统燃油而非竞品'], avoid: ['不要主观情绪', '不要使用 "秒杀"'], opening: '"今天不聊豪华词汇,只把夜间停车、家充和后排静音拆成三段实测。"' }, { id: 'lucia', handle: '@Lucia的设计日记', avatar: 'L', platform: '小红书', role: '策略型', style: '设计师视角 · 图像感', must: ['出现工作室夜归场景', '出现材质特写', '配色参考她的 vision board'], avoid: ['避免技术参数堆砌', '避免俯拍视角'], opening: '"收工时城市的颜色会变低,车内材质和灯光反而决定了我愿不愿意慢下来。"' }, ]; const Content = () => { const [selected, setSelected] = React.useState(0); const [selectedCreator, setSelectedCreator] = React.useState(0); const [selectedPlatformRule, setSelectedPlatformRule] = React.useState(0); const d = CONTENT_DRAFTS[selected]; const creator = CREATOR_CONTENT_VARIANTS[selectedCreator]; const platformRule = AIGC_PLATFORM_VARIANT_RULES[selectedPlatformRule] || AIGC_PLATFORM_VARIANT_RULES[0]; const prAudit = runPrContentAudit(d); return (

内容生成 · 品牌一致性质检

AIGC 不是自由创作。每一条内容生成时受品牌、策略、brief、平台、达人、风险词六层约束。

草稿队列

{CONTENT_DRAFTS.length}
{CONTENT_DRAFTS.map((c, i) => (
setSelected(i)} style={{ padding:'12px 14px', borderLeft: selected === i ? '2px solid var(--accent)' : '2px solid transparent', borderBottom:'1px solid var(--hairline)', background: selected === i ? 'var(--accent-tint)' : 'transparent', cursor:'pointer', }} >
{c.platform} 85 ? 'var(--accent)' : c.score > 75 ? 'var(--ink-2)' : 'var(--warn)'}}>{c.score}
{c.title}
偏航 {c.deviation.toFixed(2)}
))}

达人内容创作

{CREATOR_CONTENT_VARIANTS.length}
{CREATOR_CONTENT_VARIANTS.map((item, i) => (
setSelectedCreator(i)} style={{ padding:'12px 14px', borderLeft: selectedCreator === i ? '2px solid var(--accent)' : '2px solid transparent', borderBottom:'1px solid var(--hairline)', background: selectedCreator === i ? 'var(--accent-tint)' : 'transparent', cursor:'pointer', }} >
{item.handle} {item.platform}
{item.role} · {item.style}
))}

平台变体规则

{AIGC_PLATFORM_VARIANT_RULES.length}
{AIGC_PLATFORM_VARIANT_RULES.map((item, i) => (
setSelectedPlatformRule(i)} style={{ padding:'12px 14px', borderLeft: selectedPlatformRule === i ? '2px solid var(--accent)' : '2px solid transparent', borderBottom:'1px solid var(--hairline)', background: selectedPlatformRule === i ? 'var(--accent-tint)' : 'transparent', cursor:'pointer', }} >
{item.platform} {item.len}
{item.label} · {item.tone}
))}

{creator.handle} · 达人改写预览

GPT-Aurelia v2
{CONTENT_DRAFTS.slice(0, 4).map((c, i) => (
0{i+1} {c.title} 预估 CTR {(3 + i * 0.4).toFixed(1)}%
))}
{d.hook}
{d.body}

质检维度

PR 舆情检测

{prAudit.level}
{PR_REVIEW_SKILL_SUMMARY.name}
{prAudit.action}
{(prAudit.hits.length ? prAudit.hits : [{ token: '无红旗表达', note: '继续监测评论区反馈' }]).map(hit => (
{hit.token} · {hit.note}
))}

修订建议

2
表述模糊
"充电很快" → 建议量化为"夜间家充 7h 满"
平台建议
小红书标题可加入表情分隔,提升点击
); }; const PlatformMock = ({ platform, title, body, hook, creator }) => (
{creator?.avatar || 'M'}
{creator?.handle || '@木兮的车库'}
{platform} · 达人内容草稿
DRAFT
{/* placeholder image */}
[ 封面图 · 夜景停车场 · 待生成 ]
{title}
{hook}

{body}
{['#静奢风', '#AX7', '#夜归人', '#第二台车'].map(t => {t})}
); const QaScoreCard = ({ score }) => (

总分

QA v2
{score}
推荐发布
高于基准 14 分 · 无红旗项
); const QaDim = ({ label, v, ok }) => (
{label} {v}
); // ========== 07 · MONITOR ========== const getSocialAccount = (accountId) => (SOCIAL_ACCOUNTS || []).find(account => account.account_id === accountId) || {}; const getPostComments = (postId) => (SOCIAL_COMMENTS || []).filter(comment => comment.post_id === postId); const commentPrRank = { S0: 0, S1: 1, S2: 2, S3: 3, S4: 4 }; const commentLegalRank = { L0: 0, L1: 1, L2: 2, L3: 3, L4: 4 }; const trustBarrierPattern = /不敢买|不敢入|不敢下单|信不过|没信心|跑路|割韭菜|ppt车|概念版权/i; const smallFactoryPattern = /小厂|小品牌|小公司|新势力/i; const brandBackgroundClarifiedPattern = /不是小厂|不算小厂|不是什么小厂|背后.{0,12}(广汽|领程|大厂|主机厂|车企)|有.{0,12}(广汽|领程|大厂|主机厂|车企).{0,8}背景|原来.{0,12}(广汽|领程|大厂|主机厂|车企)/i; const isNegativeBrandTrustText = (text = '') => trustBarrierPattern.test(text) || (smallFactoryPattern.test(text) && !brandBackgroundClarifiedPattern.test(text)); const getCommentRiskRank = (comment = {}) => { const sentiment = Number(comment.sentiment || 0); const prRank = commentPrRank[String(comment.pr_level || comment.public_opinion_level || '').toUpperCase()] || 0; const legalRank = commentLegalRank[String(comment.legal_level || '').toUpperCase()] || 0; const text = `${comment.text || ''} ${comment.keyword || ''} ${comment.risk_tag || ''}`; if (prRank >= 3 || legalRank >= 3 || sentiment < -0.55) return 3; if (comment.risk_tag || prRank >= 2 || legalRank >= 2 || sentiment < -0.15 || isNegativeBrandTrustText(text)) return 2; return 1; }; const isRiskComment = (comment = {}) => getCommentRiskRank(comment) >= 2; const positiveCommentPattern = /喜欢|想要|期待|终于|太帅|帅|好看|酷|真香|种草|不错|可以|给力|厉害|赞|支持|nice|love|cool|awesome|amazing|great|😍|❤️|👍|🔥/i; const isPositiveComment = (comment = {}) => { if (isRiskComment(comment)) return false; if (socialNumber(comment.sentiment) > 0.15) return true; const prRank = commentPrRank[String(comment.pr_level || comment.public_opinion_level || '').toUpperCase()] || 0; const legalRank = commentLegalRank[String(comment.legal_level || '').toUpperCase()] || 0; const riskLevel = String(comment.pr_risk_level || '').toLowerCase(); const text = `${comment.text || ''} ${comment.keyword || ''}`; if (isNegativeBrandTrustText(text)) return false; return prRank === 0 && legalRank <= 1 && riskLevel !== 'medium' && positiveCommentPattern.test(text); }; const getCommentSentimentMeta = (comment = {}) => { const score = socialNumber(comment.sentiment); if (isRiskComment(comment)) return { tone: 'risk', label: '负面', score }; if (isPositiveComment(comment)) return { tone: 'good', label: '正向', score }; return { tone: 'neutral', label: '中性', score }; }; const commentConcernMeta = [ { id: 'price_purchase', label: '价格/购买门槛', short: '价格购买', keywords: ['价格', '多少钱', '售价', '报价', '贵', '便宜', '买不起', '买得起', 'price', 'cost', 'expensive', 'cheap'] }, { id: 'availability_delivery', label: '上市/量产/交付', short: '上市交付', keywords: ['上市', '量产', '开卖', '发售', '交付', '预定', '预订', '什么时候', 'available', 'release', 'launch', 'deliver', 'when can', 'want one'] }, { id: 'product_specs', label: '配置/性能/能力', short: '配置能力', keywords: ['续航', '电池', '空间', '座椅', '内饰', '配置', '动力', '越野', '尺寸', '参数', '功能', '智能', 'range', 'battery', 'interior', 'feature', 'spec'] }, { id: 'design_aesthetic', label: '外观/设计/科技感', short: '外观设计', keywords: ['外观', '设计', '造型', '方盒', '好看', '帅', '酷', '丑', '垃圾桶', '棺材', 'cybertruck', 'design', 'cool', 'beautiful', 'ugly', 'tescoba'] }, { id: 'usage_scenario', label: '使用场景/生活方式', short: '使用场景', keywords: ['露营', '房车', '自驾', '旅行', '办公', '数字游民', '钓鱼', '越野穿越', 'camping', 'travel', 'fishing', 'home', 'office'] }, { id: 'safety_reliability', label: '安全/可靠性', short: '安全可靠', keywords: ['安全', '事故', '失灵', '质量', '耐用', '可靠', '坏', '故障', '保命', '战争', 'toyota', 'reliable', 'safety', 'war'] }, { id: 'brand_naming', label: '品牌/命名/表达', short: '品牌命名', keywords: ['牛马', '名字', '命名', '冒犯', '玻璃心', '本意不是', '品牌', 'name', 'brand'] }, { id: 'proof_trust', label: '真实性/证据信任', short: '证据信任', keywords: ['ppt', '概念', '概念版权', '小厂', '不敢买', '不敢入', '信不过', '没信心', '实验室', '真车', '实车', '量产版', '真的假的', '证据', 'concept', 'real', 'prototype', 'fake'] }, { id: 'competitor_compare', label: '竞品/国家对比', short: '竞品对比', keywords: ['特斯拉', 'tesla', 'cybertruck', 'toyota', 'lada', '中国', 'поднебесная', 'китай', 'китайцы'] }, { id: 'complaint_after_sales', label: '投诉/售后', short: '投诉售后', keywords: ['投诉', '退款', '维权', '售后', '赔偿', '客服', 'complaint', 'refund', 'service'] }, { id: 'positive_interest', label: '正向喜爱/种草', short: '正向喜爱', keywords: ['喜欢', '想要', '期待', '太帅', '好看', '酷', '真香', '种草', '不错', '厉害', '赞', 'nice', 'love', 'awesome', 'amazing', 'super', 'бомба', 'респект', 'супер'] }, { id: 'other', label: '其他/待观察', short: '其他', keywords: [] }, ]; const commentConcernById = Object.fromEntries(commentConcernMeta.map(item => [item.id, item])); const getCommentConcern = (comment = {}) => { if (comment.user_concern_category) { const meta = commentConcernById[comment.user_concern_category] || {}; return { id: comment.user_concern_category, label: comment.user_concern_label || meta.label || '其他/待观察', short: meta.short || comment.user_concern_label || '其他' }; } const text = `${comment.text || ''} ${comment.keyword || ''} ${comment.risk_tag || ''}`.toLowerCase(); const hit = commentConcernMeta.find(item => item.id !== 'other' && item.keywords.some(keyword => text.includes(String(keyword).toLowerCase()))); if (hit) return hit; if (socialNumber(comment.sentiment) > 0.15) return commentConcernById.positive_interest; return commentConcernById.other; }; const socialNumber = (value) => Number(value || 0); const socialInteraction = (post = {}) => socialNumber(post.likes) + socialNumber(post.comments) + socialNumber(post.shares) + socialNumber(post.saves); const isExternalSocialPost = (post = {}) => { const type = `${post.source_type || ''} ${post.asset_type || ''} ${post.source_post_type || ''} ${post.relationship_status || ''}`.toLowerCase(); return Boolean( post.is_official_post === false || type.includes('earned') || type.includes('ugc') || type.includes('kol') || type.includes('external') ); }; const needsChineseTranslation = (text = '') => { const clean = String(text || '').trim(); return Boolean(clean && !/[\u3400-\u9FFF]/.test(clean) && /[A-Za-z\u00C0-\u024F\u0400-\u04FF]/.test(clean)); }; const socialTranslationKey = (type, id) => `${type}:${id}`; const ContentTitleLink = ({ title, url, className = '', linkClassName = 'content-title-link' }) => { const content = {title || '未命名内容'}; return url ? {content} : content; }; const getSocialPostUrl = (post = {}, account = {}) => { const raw = post.raw || {}; const candidates = [ post.url, post.link, post.permalink, post.share_url, post.manual_metrics?.url, raw.url, raw.link, raw.permalink, raw.share_url, raw.share_link, raw.note_url, raw.web_url, raw.share_info?.link, ]; const direct = candidates.map(value => String(value || '').trim()).find(value => /^https?:\/\//i.test(value)); if (direct) return direct; if (post.platform === '小红书') { const noteId = String(raw.id || raw.note_id || raw.noteId || '').trim(); if (noteId) return `https://www.xiaohongshu.com/explore/${noteId}`; } return account.homepage_url || ''; }; const assetScopeMeta = { owned: { label: '自有账号', desc: '官方账号发布内容' }, external: { label: '外部声量', desc: 'KOL / UGC / 第三方自然内容' }, }; const getMonitorDataset = (brandId = 'montx') => { const posts = (SOCIAL_POSTS || []).filter(post => post.brand_id === brandId); const fallbackPosts = posts.length ? posts : (SOCIAL_POSTS || []); const accountIds = new Set(fallbackPosts.map(post => post.account_id)); const accounts = (SOCIAL_ACCOUNTS || []).filter(account => account.brand_id === brandId || accountIds.has(account.account_id)); const postIds = new Set(fallbackPosts.map(post => post.post_id)); const comments = (SOCIAL_COMMENTS || []).filter(comment => postIds.has(comment.post_id)); return { accounts, posts: fallbackPosts, comments }; }; const mergeSocialRows = (baseRows = [], extraRows = [], idKey) => { const map = new Map(); [...baseRows, ...extraRows].forEach(row => { if (!row?.[idKey]) return; map.set(row[idKey], { ...(map.get(row[idKey]) || {}), ...row }); }); return Array.from(map.values()); }; const monitorRiskRank = { high: 3, medium: 2, low: 1 }; const monitorRiskLabel = { high: 'HIGH', medium: 'MED', low: 'LOW' }; const monitorRiskLevelByRank = { 3: 'high', 2: 'medium', 1: 'low' }; const contentSourceMeta = { api: 'API', manual: '人工台账', external: '外部UGC', mixed: 'API+人工', unknown: '未标记', }; const getContentSourceGroup = (post = {}) => { const sourceText = `${post.data_channel || ''} ${post.provider || ''} ${post.source_type || ''} ${post.asset_type || ''} ${Array.isArray(post.data_sources) ? post.data_sources.join(' ') : ''}`.toLowerCase(); if (isExternalSocialPost(post)) return 'external'; if (/api_plus_manual|manual.*api|api.*manual/.test(sourceText)) return 'mixed'; if (/manual|sheet|workbook|台账/.test(sourceText)) return 'manual'; if (/api|tikhub|justone|sync/.test(sourceText)) return 'api'; return 'unknown'; }; const getMonitorPostBriefMeta = (post = {}) => { const brief = post.brief || {}; const id = brief.primaryBriefId || post.primaryMasterBriefId || post.briefMapping?.primaryMasterBriefId || ''; const name = brief.primaryBriefName || post.briefMapping?.masterBriefGroup || post.briefMapping?.unifiedTopic || ''; const parent = brief.parentCampaignId || post.parentCampaignId || post.briefMapping?.parentCampaignId || ''; return { id, name, parent, assigned: Boolean(brief.assigned || id || name || parent), }; }; const getMonitorPostBriefFilterValue = (post = {}) => { const meta = getMonitorPostBriefMeta(post); return meta.id || meta.name || meta.parent || 'unassigned'; }; const getMonitorPostBriefFilterLabel = (post = {}) => { const meta = getMonitorPostBriefMeta(post); return meta.name || meta.id || meta.parent || '未挂 Brief'; }; const getPostTimeValue = (post = {}) => { const raw = String(post.published_at || post.publish_time || post.created_at || '').trim(); if (!raw) return 0; const normalized = raw.replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/'); const parsed = new Date(normalized); return Number.isNaN(parsed.getTime()) ? 0 : parsed.getTime(); }; const getPostCompositeRisk = (post = {}, postComments = []) => { const baseRank = monitorRiskRank[post.risk_level] || 1; const riskRows = postComments.filter(isRiskComment); const maxCommentRank = postComments.reduce((max, comment) => Math.max(max, getCommentRiskRank(comment)), 1); const rank = Math.max(baseRank, maxCommentRank); const highComments = postComments.filter(comment => getCommentRiskRank(comment) >= 3).length; return { level: monitorRiskLevelByRank[rank] || 'low', rank, totalComments: postComments.length, riskComments: riskRows.length, highComments, }; }; const monitorActionLabel = { observed: '已观察', 'needs-response': '待回应', 'needs-answer': '待解答', 'needs-review': '待复核', reviewing: '复核中', resolved: '已关闭', }; const socialApiUrl = (pathname) => (window.location.protocol === 'file:' ? `http://localhost:4173${pathname}` : pathname); const Monitor = ({ onNav, workspaceContext }) => { const [ticks, setTicks] = React.useState(0); const brandId = workspaceContext?.brandId || BRAND?.id || 'montx'; const activeBrand = (WORKSPACE_BRANDS || []).find(brand => brand.id === brandId) || BRAND; const baseDataset = getMonitorDataset(brandId); const [syncedAssets, setSyncedAssets] = React.useState({ accounts: [], posts: [], comments: [], postDailyMetricSnapshots: [], postDailyMetricBackfill: null, lastSync: '', nextRun: '', status: 'idle', errors: [], runs: [], syncLog: null }); const [syncingPlatform, setSyncingPlatform] = React.useState(''); const accounts = mergeSocialRows(baseDataset.accounts, syncedAssets.accounts, 'account_id'); const posts = mergeSocialRows(baseDataset.posts, syncedAssets.posts, 'post_id'); const comments = mergeSocialRows(baseDataset.comments, syncedAssets.comments || [], 'comment_id'); const postDailyMetricSnapshots = syncedAssets.postDailyMetricSnapshots || []; const [assetScope, setAssetScope] = React.useState('owned'); const [workTab, setWorkTab] = React.useState('content'); const [filters, setFilters] = React.useState({ platform: 'all', product: 'all', risk: 'all', status: 'all' }); const [contentFilters, setContentFilters] = React.useState({ query: '', campaign: 'all', brief: 'all', contentType: 'all', dateRange: 'all', commentScope: 'all', source: 'all', sort: 'risk' }); const [syncLogOpen, setSyncLogOpen] = React.useState(false); const [commentConcernFilter, setCommentConcernFilter] = React.useState('all'); const [activeTrendPostId, setActiveTrendPostId] = React.useState(''); const [activeCommentId, setActiveCommentId] = React.useState(''); const [commentActions, setCommentActions] = React.useState({}); const [replyDraft, setReplyDraft] = React.useState(''); const [replyGenerating, setReplyGenerating] = React.useState(false); const [translations, setTranslations] = React.useState({}); const [translationStatus, setTranslationStatus] = React.useState('idle'); const [manualUploadOpen, setManualUploadOpen] = React.useState(false); const [manualUploadPreview, setManualUploadPreview] = React.useState(null); const [manualUploadStatus, setManualUploadStatus] = React.useState('idle'); const [manualUploadError, setManualUploadError] = React.useState(''); const [manualUploadResolutions, setManualUploadResolutions] = React.useState({}); React.useEffect(() => { const int = setInterval(() => setTicks(t => t + 1), 2000); return () => clearInterval(int); }, []); const applySyncPayload = React.useCallback((data = {}) => { setSyncedAssets({ accounts: data.accounts || [], posts: data.posts || [], comments: data.comments || [], postDailyMetricSnapshots: data.postDailyMetricSnapshots || [], postDailyMetricBackfill: data.postDailyMetricBackfill || null, lastSync: data.lastFinishedAt || '', nextRun: data.nextRunAt || '', status: data.status || 'idle', errors: data.errors || [], runs: data.runs || [], syncLog: data.syncLog || null, }); }, []); const loadSyncStatus = React.useCallback(async () => { try { const response = await fetch(socialApiUrl('/api/social/sync/status')); const data = await response.json(); if (response.ok) applySyncPayload(data); } catch (error) { setSyncedAssets(prev => ({ ...prev, errors: [{ platform: 'local', message: error.message || '读取同步状态失败' }] })); } }, [applySyncPayload]); React.useEffect(() => { loadSyncStatus(); const int = setInterval(loadSyncStatus, 30000); return () => clearInterval(int); }, [loadSyncStatus]); const ownedPosts = posts.filter(post => !isExternalSocialPost(post)); const externalPosts = posts.filter(isExternalSocialPost); const scopedPosts = assetScope === 'external' ? externalPosts : ownedPosts; const commentsByPost = React.useMemo(() => { const map = new Map(); comments.forEach(comment => { const list = map.get(comment.post_id) || []; list.push(comment); map.set(comment.post_id, list); }); return map; }, [comments]); const getRowRisk = React.useCallback((post) => getPostCompositeRisk(post, commentsByPost.get(post.post_id) || []), [commentsByPost]); const getDisplayedPostComments = React.useCallback((post) => Math.max(pickNativeMetric(post, 'comments'), (commentsByPost.get(post.post_id) || []).length), [commentsByPost]); const getDisplayedPostInteraction = React.useCallback((post) => pickNativeMetric(post, 'likes') + getDisplayedPostComments(post) + pickNativeMetric(post, 'shares') + pickNativeMetric(post, 'reposts') + pickNativeMetric(post, 'saves'), [getDisplayedPostComments]); const scopedPostIds = new Set(scopedPosts.map(post => post.post_id)); const scopedComments = comments.filter(comment => scopedPostIds.has(comment.post_id)); const scopedAccountIds = new Set(scopedPosts.map(post => post.account_id).filter(Boolean)); const scopedAccounts = accounts.filter(account => scopedAccountIds.has(account.account_id)); const followerDelta = scopedAccounts.reduce((sum, account) => sum + socialNumber(account.follower_delta_period), 0); const followerGrowthStarts = scopedAccounts.map(account => account.follower_delta_start_date).filter(Boolean).sort(); const followerGrowthEnds = scopedAccounts.map(account => account.follower_delta_end_date).filter(Boolean).sort(); const followerGrowthStart = followerGrowthStarts[0] || ''; const followerGrowthEnd = followerGrowthEnds[followerGrowthEnds.length - 1] || ''; const ownedViews = ownedPosts.reduce((sum, post) => sum + getPostContentViews(post), 0); const externalViews = externalPosts.reduce((sum, post) => sum + getPostContentViews(post), 0); const scopedPendingMetrics = scopedPosts.filter(isPostMetricPending).length; const externalPendingMetrics = externalPosts.filter(isPostMetricPending).length; const campaignOptions = Array.from(new Set(scopedPosts.map(post => post.campaign).filter(Boolean))).sort((a, b) => String(a).localeCompare(String(b), 'zh-CN')); const briefOptionRows = Array.from(scopedPosts.reduce((map, post) => { const id = getMonitorPostBriefFilterValue(post); if (!map.has(id)) map.set(id, getMonitorPostBriefFilterLabel(post)); return map; }, new Map()).entries()) .map(([id, label]) => ({ id, label })) .sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN')); const briefOptions = briefOptionRows.map(item => item.id); const briefLabelById = briefOptionRows.reduce((map, item) => ({ ...map, [item.id]: item.label }), {}); const contentTypeOptions = Array.from(new Set(scopedPosts.map(post => post.content_type || post.type).filter(Boolean))).sort((a, b) => String(a).localeCompare(String(b), 'zh-CN')); const sourceOptions = Array.from(new Set(scopedPosts.map(getContentSourceGroup))).filter(Boolean); const contentQuery = contentFilters.query.trim().toLowerCase(); const now = Date.now(); const filteredPosts = scopedPosts.filter(post => { const rowRisk = getRowRisk(post); const rowComments = commentsByPost.get(post.post_id) || []; if (filters.platform !== 'all' && post.platform !== filters.platform) return false; if (filters.product !== 'all' && post.product !== filters.product) return false; if (filters.risk !== 'all' && rowRisk.level !== filters.risk) return false; if (filters.status !== 'all' && post.status !== filters.status) return false; if (contentFilters.campaign !== 'all' && post.campaign !== contentFilters.campaign) return false; if (contentFilters.brief !== 'all' && getMonitorPostBriefFilterValue(post) !== contentFilters.brief) return false; if (contentFilters.contentType !== 'all' && (post.content_type || post.type) !== contentFilters.contentType) return false; if (contentFilters.source !== 'all' && getContentSourceGroup(post) !== contentFilters.source) return false; if (contentFilters.commentScope === 'has-comments' && rowComments.length === 0 && socialNumber(post.comments) === 0) return false; if (contentFilters.commentScope === 'no-comments' && (rowComments.length > 0 || socialNumber(post.comments) > 0)) return false; if (contentFilters.commentScope === 'risk-comments' && rowRisk.riskComments === 0) return false; if (contentFilters.dateRange !== 'all') { const days = Number(contentFilters.dateRange.replace('d', '')) || 0; const time = getPostTimeValue(post); if (!time || now - time > days * 24 * 60 * 60 * 1000) return false; } if (contentQuery) { const account = accounts.find(item => item.account_id === post.account_id) || {}; const briefMeta = getMonitorPostBriefMeta(post); const haystack = [ post.title, post.caption, post.product, post.campaign, briefMeta.id, briefMeta.name, briefMeta.parent, post.platform, post.content_type, post.creator_handle, account.handle, account.account_name, ].join(' ').toLowerCase(); if (!haystack.includes(contentQuery)) return false; } return true; }).sort((a, b) => { if (contentFilters.sort === 'newest') return getPostTimeValue(b) - getPostTimeValue(a); if (contentFilters.sort === 'views') return socialNumber(b.views || b.plays || b.reads) - socialNumber(a.views || a.plays || a.reads); if (contentFilters.sort === 'comments') return getDisplayedPostComments(b) - getDisplayedPostComments(a); if (contentFilters.sort === 'engagement') return getDisplayedPostInteraction(b) - getDisplayedPostInteraction(a); return getRowRisk(b).rank - getRowRisk(a).rank || getPostTimeValue(b) - getPostTimeValue(a); }); const visiblePosts = filteredPosts; const trendSnapshotsByPost = React.useMemo(() => { const map = new Map(); postDailyMetricSnapshots.forEach(snapshot => { if (!snapshot?.post_id) return; const rows = map.get(snapshot.post_id) || []; rows.push(snapshot); map.set(snapshot.post_id, rows); }); map.forEach(rows => rows.sort((a, b) => String(a.date || '').localeCompare(String(b.date || '')))); return map; }, [postDailyMetricSnapshots]); const activeTrendPost = activeTrendPostId ? posts.find(post => post.post_id === activeTrendPostId) : null; const activeTrendAccount = activeTrendPost ? accounts.find(account => account.account_id === activeTrendPost.account_id) || getSocialAccount(activeTrendPost.account_id) : {}; const activeTrendSnapshots = activeTrendPostId ? trendSnapshotsByPost.get(activeTrendPostId) || [] : []; const commentConcernCounts = scopedComments.reduce((map, comment) => { const concern = getCommentConcern(comment); map[concern.id] = (map[concern.id] || 0) + 1; return map; }, {}); const commentConcernOptions = commentConcernMeta.filter(item => item.id !== 'other' && commentConcernCounts[item.id]).concat(commentConcernCounts.other ? [commentConcernById.other] : []); const commentStream = scopedComments.filter(comment => commentConcernFilter === 'all' || getCommentConcern(comment).id === commentConcernFilter); const riskComments = scopedComments.filter(isRiskComment); const openRiskComments = riskComments.filter(comment => (commentActions[comment.comment_id] || comment.action_status) !== 'resolved'); const totalViews = scopedPosts.reduce((sum, post) => sum + getPostContentViews(post), 0); const totalInteraction = scopedPosts.reduce((sum, post) => sum + getDisplayedPostInteraction(post), 0); const totalComments = scopedPosts.reduce((sum, post) => sum + getDisplayedPostComments(post), 0); const avgSentiment = scopedComments.length ? scopedComments.reduce((sum, comment) => sum + socialNumber(comment.sentiment), 0) / scopedComments.length : 0; const topRisk = [...riskComments].sort((a, b) => socialNumber(a.sentiment) - socialNumber(b.sentiment))[0]; const activeComment = commentStream.find(comment => comment.comment_id === activeCommentId) || topRisk || commentStream[0] || scopedComments[0] || null; const activeCommentPost = activeComment ? posts.find(post => post.post_id === activeComment.post_id) : null; const activeCommentAccount = activeCommentPost ? accounts.find(account => account.account_id === activeCommentPost.account_id) || getSocialAccount(activeCommentPost.account_id) : {}; const activeCommentConcern = activeComment ? getCommentConcern(activeComment) : null; const activeCommentSentiment = activeComment ? getCommentSentimentMeta(activeComment) : null; const activeCommentSourceUrl = activeCommentPost ? getSocialPostUrl(activeCommentPost, activeCommentAccount) : ''; const platformOptions = Array.from(new Set((scopedPosts.length ? scopedPosts : posts).map(post => post.platform).filter(Boolean))); const productOptions = Array.from(new Set(scopedPosts.map(post => post.product).filter(Boolean))); const statusOptions = Array.from(new Set(scopedPosts.map(post => post.status).filter(Boolean))); const externalTranslationCandidates = React.useMemo(() => { if (assetScope !== 'external') return []; const items = []; visiblePosts.forEach(post => { const text = post.title || post.caption || ''; const key = socialTranslationKey('post', post.post_id); if (needsChineseTranslation(text) && !translations[key]) items.push({ id: key, type: 'post_title', text }); }); scopedComments.forEach(comment => { const key = socialTranslationKey('comment', comment.comment_id); if (needsChineseTranslation(comment.text) && !translations[key]) items.push({ id: key, type: 'comment', text: comment.text }); }); return items.slice(0, 24); }, [assetScope, visiblePosts, scopedComments, translations]); const externalTranslationSignature = externalTranslationCandidates.map(item => item.id).join('|'); const externalTranslatedCount = Object.values(translations).filter(item => item?.translation).length; const opportunities = scopedComments .filter(comment => !isRiskComment(comment) && comment.keyword) .slice(0, 3) .map(comment => ({ t: comment.keyword, p: comment.platform, pct: comment.action_status === 'observed' ? '可沉淀' : '待响应' })); const updateFilter = (key, value) => setFilters(prev => ({ ...prev, [key]: value })); const updateContentFilter = (key, value) => setContentFilters(prev => ({ ...prev, [key]: value })); const resetFilters = () => setFilters({ platform: 'all', product: 'all', risk: 'all', status: 'all' }); const resetContentFilters = () => setContentFilters({ query: '', campaign: 'all', brief: 'all', contentType: 'all', dateRange: 'all', commentScope: 'all', source: 'all', sort: 'risk' }); const changeAssetScope = (scope) => { setAssetScope(scope); resetFilters(); resetContentFilters(); setCommentConcernFilter('all'); setActiveCommentId(''); setActiveTrendPostId(''); setWorkTab('content'); }; const setCommentStatus = (commentId, status) => setCommentActions(prev => ({ ...prev, [commentId]: status })); React.useEffect(() => { if (!externalTranslationSignature || translationStatus === 'loading') return; let cancelled = false; const items = externalTranslationCandidates; setTranslationStatus('loading'); fetch(socialApiUrl('/api/social/translate'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ items }), }) .then(response => response.json().then(data => ({ ok: response.ok, data }))) .then(({ data }) => { if (cancelled) return; setTranslationStatus(data.ok === false ? 'error' : 'ready'); setTranslations(prev => { const next = { ...prev }; (data.translations || []).forEach(item => { next[item.id] = { translation: item.translation || '', language: item.language || '', error: item.error || '', source: data.source || 'unknown', }; }); return next; }); }) .catch(error => { if (cancelled) return; setTranslationStatus('error'); setTranslations(prev => { const next = { ...prev }; items.forEach(item => { next[item.id] = { translation: '', language: '', error: error.message || '翻译失败', source: 'client-error' }; }); return next; }); }); return () => { cancelled = true; }; }, [externalTranslationSignature]); const syncStatusText = syncedAssets.status === 'running' ? '同步中' : syncedAssets.status === 'error' || syncedAssets.status === 'partial' ? '部分失败' : 'Live'; const lastSyncText = formatBeijingTime(syncedAssets.lastSync, '等待首次同步'); const nextRunText = formatBeijingTime(syncedAssets.nextRun, '每日自动同步'); const formatShortSyncTime = (value, fallback) => formatBeijingTime(value, fallback, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); const lastSyncShortText = formatShortSyncTime(syncedAssets.lastSync, '待同步'); const nextRunShortText = formatShortSyncTime(syncedAssets.nextRun, '每日'); const syncErrorText = (syncedAssets.errors || []) .map((item, index) => `${index + 1}. ${item.platform || item.source || '同步'}:${item.message || item.error || JSON.stringify(item)}`) .join('\n') || '暂无异常详情'; const syncAllAccounts = async () => { setSyncingPlatform('all'); setSyncedAssets(prev => ({ ...prev, status: 'running', errors: [] })); try { const response = await fetch(socialApiUrl('/api/social/sync?trigger=manual'), { method: 'POST' }); const data = await response.json(); if (!response.ok) throw new Error(data.error || data.message || '同步失败'); applySyncPayload(data); } catch (error) { setSyncedAssets(prev => ({ ...prev, status: 'error', errors: [{ platform: 'local', message: error.message || '同步失败' }] })); } finally { setSyncingPlatform(''); } }; const generateReply = async (comment = activeComment) => { if (!comment) return; setActiveCommentId(comment.comment_id); const post = posts.find(item => item.post_id === comment.post_id) || {}; const account = accounts.find(item => item.account_id === post.account_id) || {}; setReplyGenerating(true); setReplyDraft('DeepSeek 正在生成回应建议...'); try { const response = await fetch(socialApiUrl('/api/social/comment/reply-suggestion'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ comment, post, account }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || data.message || '生成失败'); setReplyDraft([ data.reply || `感谢你的反馈。关于「${comment.keyword || comment.risk_tag || '这个问题'}」,我们会补充说明。`, '', `舆情等级:${data.publicOpinionLevel || data.prLevel || 'S1'}`, `法务等级:${data.legalLevel || 'L1'}`, `语气:${data.tone || '克制、专业'}`, `风险:${data.riskLevel || 'medium'}`, `动作:${data.nextAction || '人工复核后使用'}`, data.legalNextAction ? `法务动作:${data.legalNextAction}` : '', data.rationale ? `判断:${data.rationale}` : '', data.riskPoints?.length ? `风险点:${data.riskPoints.join(';')}` : '', data.legalRiskTypes?.length ? `法务类型:${data.legalRiskTypes.join(';')}` : '', data.legalRiskPoints?.length ? `法务风险:${data.legalRiskPoints.join(';')}` : '', data.replacementSuggestion ? `文案替换:${data.replacementSuggestion}` : '', data.reviewSuggestion ? `复盘建议:${data.reviewSuggestion}` : '', data.legalEscalation ? `升级建议:${data.legalEscalation}` : '', `来源:${data.source || 'local'}${data.model ? ` · ${data.model}` : ''}${data.modelError ? ` · ${data.modelError}` : ''}`, ].filter(Boolean).join('\n')); } catch (error) { setReplyDraft(`建议回应:感谢你指出「${comment.keyword || comment.risk_tag || '这个问题'}」。这条内容会补充说明适用场景、产品边界和后续节奏,避免造成误解。\n\n来源:local-fallback · ${error.message || '生成失败'}`); } finally { setReplyGenerating(false); } }; const readManualUploadFile = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const bytes = new Uint8Array(reader.result || []); let chunk = ''; let encoded = ''; for (let i = 0; i < bytes.length; i += 1) { chunk += String.fromCharCode(bytes[i]); if (chunk.length >= 0x8000) { encoded += btoa(chunk); chunk = ''; } } if (chunk) encoded += btoa(chunk); resolve({ name: file.name, size: file.size, contentBase64: encoded }); }; reader.onerror = () => reject(reader.error || new Error('文件读取失败')); reader.readAsArrayBuffer(file); }); const previewManualUpload = async (event) => { const selectedFiles = Array.from(event.target.files || []); event.target.value = ''; if (!selectedFiles.length) return; setManualUploadStatus('previewing'); setManualUploadError(''); setManualUploadPreview(null); setManualUploadResolutions({}); try { const files = await Promise.all(selectedFiles.map(readManualUploadFile)); const response = await fetch(socialApiUrl('/api/social/manual-upload/preview'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ files }), }); const data = await response.json(); if (!response.ok || data.ok === false) throw new Error(data.error || data.message || '上传预检失败'); const conflicts = data.preview?.conflicts || []; setManualUploadPreview(data); setManualUploadResolutions(Object.fromEntries(conflicts.map(conflict => [ conflict.id, conflict.recommended_action === 'use_upload' ? 'use_upload' : 'keep_system', ]))); setManualUploadStatus('ready'); } catch (error) { setManualUploadStatus('error'); setManualUploadError(error.message || '上传预检失败'); } }; const applyManualUpload = async () => { if (!manualUploadPreview?.sessionId) return; setManualUploadStatus('applying'); setManualUploadError(''); try { const response = await fetch(socialApiUrl('/api/social/manual-upload/apply'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: manualUploadPreview.sessionId, resolutions: manualUploadResolutions }), }); const data = await response.json(); if (!response.ok || data.ok === false) throw new Error(data.error || data.message || '写入失败'); applySyncPayload(data); setManualUploadStatus('applied'); setTimeout(() => setManualUploadOpen(false), 700); } catch (error) { setManualUploadStatus('error'); setManualUploadError(error.message || '写入失败'); } }; return (

社媒资产 · Live Monitor

{activeBrand.name} 的账号、文章、评论和回应建议集中监控。系统每日自动同步,支持手动即时刷新。

北京时间 {lastSyncShortText} → {nextRunShortText}
= 0 ? '+' : ''}${formatCompactNumber(followerDelta)}`} meta={assetScope === 'external' ? `评论 ${formatCompactNumber(totalComments)} · 账号 ${scopedAccounts.length}` : (followerGrowthStart ? `${followerGrowthStart} - ${followerGrowthEnd}` : `当前视图 ${scopedPosts.length} · 全部 ${posts.length}`)} tone={assetScope === 'external' || followerDelta > 0 ? 'good' : 'neutral'} /> = 0 ? '+' : ''}${avgSentiment.toFixed(2)}`} tone={openRiskComments.length ? 'risk' : 'good'}/>
updateFilter('platform', value)}/> updateFilter('product', value)}/> updateFilter('risk', value)} render={value => value === 'all' ? '全部' : monitorRiskLabel[value]}/> updateFilter('status', value)} render={value => value === 'all' ? '全部' : value}/> {assetScope === 'external' && translationStatus === 'loading' ? `已译 ${externalTranslatedCount} · DeepSeek 翻译中` : assetScope === 'external' ? `已译 ${externalTranslatedCount}` : `匹配 ${filteredPosts.length}/${scopedPosts.length} 篇`}
{workTab === 'content' && (

内容资产库

{assetScopeMeta[assetScope].label} · {visiblePosts.length} 篇命中 · 内容池筛选

{visiblePosts.length}
updateContentFilter('campaign', value)}/> updateContentFilter('brief', value)} render={value => briefLabelById[value] || value}/> updateContentFilter('contentType', value)}/> updateContentFilter('dateRange', value)} render={value => ({ '7d': '近 7 天', '14d': '近 14 天', '30d': '近 30 天' }[value] || value)}/> updateContentFilter('commentScope', value)} render={value => ({ 'has-comments': '有评论', 'risk-comments': '有风险评论', 'no-comments': '无评论' }[value] || value)}/> updateContentFilter('source', value)} render={value => contentSourceMeta[value] || value}/> updateContentFilter('sort', value === 'all' ? 'risk' : value)} render={value => ({ risk: '风险优先', newest: '最新发布', views: '观看/阅读', comments: '评论数', engagement: '互动数' }[value] || value)}/>
{visiblePosts.map(p => { const account = accounts.find(item => item.account_id === p.account_id) || getSocialAccount(p.account_id); const rowRisk = getRowRisk(p); const titleTranslation = translations[socialTranslationKey('post', p.post_id)]; const postUrl = getSocialPostUrl(p, account); const postViews = getPostContentViews(p); const metricPending = isPostMetricPending(p); const displayedComments = getDisplayedPostComments(p); const trendRows = trendSnapshotsByPost.get(p.post_id) || []; const briefMeta = getMonitorPostBriefMeta(p); return ( ); })}
文章 渠道 观看/阅读 点赞 评论 收藏 转发 趋势 风险
{isExternalSocialPost(p) && titleTranslation?.translation && 译:{titleTranslation.translation}} {p.publish_time || '-'} · {p.product || '-'} · {p.campaign || '-'} {briefMeta.assigned && Brief · {briefMeta.name || briefMeta.id || briefMeta.parent}} {isExternalSocialPost(p) && 外部声量 · {p.relationship_status || 'organic'} · {p.creator_handle || 'KOL/UGC'}}
{p.platform} {account.handle || account.account_name || '-'}
{metricPending ? 待补数 : {formatCompactNumber(postViews)}} {formatCompactNumber(pickNativeMetric(p, 'likes'))} {formatCompactNumber(displayedComments)} {formatCompactNumber(pickNativeMetric(p, 'saves'))} {formatCompactNumber(pickNativeMetric(p, 'shares') + pickNativeMetric(p, 'reposts'))} {rowRisk.level === 'high' && HIGH · {rowRisk.riskComments}} {rowRisk.level === 'medium' && MED · {rowRisk.riskComments}} {rowRisk.level === 'low' && LOW}
{!filteredPosts.length && scopedPosts.length > 0 && (
当前筛选没有命中文章。可以清空内容池筛选或调整平台、产品、风险条件。
)} {!scopedPosts.length && (
暂无文章数据。可以点击同步获取平台资产。
)}
)} {workTab === 'comments' && (

评论池

{assetScopeMeta[assetScope].label} · {commentStream.length}/{scopedComments.length} 条 · 按用户关心问题分类

{commentStream.length}
{commentConcernOptions.map(item => ( ))}
setActiveCommentId(comment.comment_id)} expanded />
)} {workTab === 'risk' && (

风险队列

{assetScopeMeta[assetScope].label} · {riskComments.length} 条风险评论 · {openRiskComments.length} 条待处理

{openRiskComments.length} open
{riskComments.map(comment => { const post = posts.find(item => item.post_id === comment.post_id); const status = commentActions[comment.comment_id] || comment.action_status || 'observed'; const commentTranslation = translations[socialTranslationKey('comment', comment.comment_id)]; return ( ); })} {!riskComments.length &&
暂无风险评论
}