/* ========================================================================= * editor-compliance.js — 发布前合规自检(融合 yuwen-publish-precheck 方法论) * 纯前端引擎:不依赖 LLM / 不发网络请求,离线可用。 * 架构对齐原技能:① 词面扫描(定位候选) ② 语境判定(按平台/商业/行业过滤) * ③ 保意修复(收缩主张/补披露/最小改写/删除,绝不教绕审) * 参考:https://github.com/yuwen-cool/yuwen-publish-precheck (Other 许可,方法论借鉴 + 自研规则包) * ========================================================================= */ (function () { 'use strict'; // 平台展示名 -> 内部 key(与 YCat 平台选择器对齐) var PLATFORM_NAME_MAP = { '微信公众号': 'wechat', '微信': 'wechat', '知乎': 'zhihu', '小红书': 'xiaohongshu', '小红书笔记': 'xiaohongshu', '微博': 'weibo', '抖音': 'douyin', '微信视频号': 'shipinhao', '视频号': 'shipinhao', 'B站': 'bilibili', '哔哩哔哩': 'bilibili', 'bilibili': 'bilibili', '百家号': 'baijiahao', '今日头条': 'toutiao', '头条': 'toutiao' }; // 自动识别行业(按内容判定,不依赖用户自称) var MEDICAL_KW = ['治疗', '治愈', '根治', '疗程', '偏方', '秘方', '降糖', '降血压', '降压', '抗癌', '癌症', '无副作用', '药到病除', '瘦身', '减肥', '排毒', '燃脂', '月瘦', '保健品', '医美', '调理']; var FINANCE_KW = ['理财', '基金', '股票', '炒股', '保险', '贷款', '年化', '收益率', '荐股', '配资', '杠杆', '保本', '稳赚', '投资']; // ---- 规则库(自研,对齐原技能 common/commercial/industry/platform 分层)---- // severity: high(必改) / medium(建议改) / low(仅提示) // kind: 'violation' | 'myth' // fix: 'narrow'(收缩主张/改写) | 'remove'(删除) | 'disclose'(需人工补披露,不自动改) // alt: 字符串或 function(match)->string,用于 narrow/remove 的自动修复 var RULES = [ // —— 通用:虚假/绝对化宣传 —— { id: 'R01', title: '绝对化 / 无法证实的最高级用语', severity: 'high', kind: 'violation', fix: 'narrow', pattern: '最(?:好|强|新|快|全|大|优|牛|强)|第一|国家级|唯一|顶级|极品|万能|包治|药到病除|100%|百分之百|全网首发|史上最低|全网最低', fixNote: '改为有边界的表达(如"较优""行业前列"),避免无法证实的最高级。' }, { id: 'R02', title: '虚假时限 / 紧迫营销', severity: 'medium', kind: 'violation', fix: 'narrow', commercialOnly: true, pattern: '最后一天|错过?再?等一年|仅此一次|倒计时|错过再无|史上最低|全网最低|仅限今天|活动最后', fixNote: '去掉无法证实的时限压力,改为中性表述。' }, { id: 'R03', title: '诱导互动话术', severity: 'medium', kind: 'violation', fix: 'remove', commercialOnly: true, pattern: '点赞|转发|评论区见|双击屏幕|左下角小?卡片|关注我|私信我|加微信|扫码|微信号|加我微信|主页领', fixNote: '平台对诱导点赞/转发/关注的管控较严,建议删除此类话术。' }, { id: 'R04', title: '引流到私域', severity: 'medium', kind: 'violation', fix: 'remove', commercialOnly: true, pattern: '加微信|微信搜|私聊|小号|绿软|telegram|搬砖|内部群|免费带飞', fixNote: '商业引流到站外/私域需符合平台规则,建议删除或走官方组件。' }, { id: 'R05', title: '贬低竞品 / 对比拉踩', severity: 'low', kind: 'violation', fix: 'narrow', pattern: '其他(?:家|品牌|平台)?都是?坑|只有我们正规|别家都是骗|就我们靠谱', fixNote: '客观陈述自身优势即可,避免贬低他人。' }, { id: 'R06', title: '迷信 / 风水类表述', severity: 'low', kind: 'violation', fix: 'remove', pattern: '转运|招财|辟邪|旺风水|改命|消灾|开光', fixNote: '此类表述易被判定违规,建议删除。' }, // —— 医疗健康(强监管)—— { id: 'R10', title: '医疗功效 / 诊疗断言', severity: 'high', kind: 'violation', fix: 'disclose', industries: ['medical'], pattern: '治疗|治愈|根治|治好|疗程|偏方|秘方|降糖|降血压|降压|抗癌|延长生命|无副作用|纯天然无添加|药到病除', fixNote: '医疗健康内容需:①标注"内容仅供参考,不构成诊疗建议" ②带货需相关资质 ③有数据请附来源。' }, { id: 'R11', title: '瘦身 / 功效承诺', severity: 'medium', kind: 'violation', fix: 'disclose', industries: ['medical'], pattern: '月瘦|瘦八斤|躺着瘦|排毒|燃脂|暴瘦|轻松瘦|不运动瘦', fixNote: '瘦身/功效宣称需有依据,避免绝对化效果承诺,并标注"效果因人而异"。' }, { id: 'R12', title: '视频号医疗严控', severity: 'high', kind: 'violation', fix: 'disclose', industries: ['medical'], platforms: ['shipinhao'], pattern: '治疗|治愈|根治|偏方|秘方|降糖|降压|抗癌|无副作用|药到病除', fixNote: '微信视频号对医疗健康内容管控极严,非资质账号建议不发医疗断言。' }, // —— 金融理财(强监管)—— { id: 'R20', title: '金融收益承诺 / 荐股', severity: 'high', kind: 'violation', fix: 'disclose', industries: ['finance'], commercialOnly: true, pattern: '稳赚|保本|保收益|年化|收益率|荐股|翻倍|内部消息|稳赚不赔|零风险|保证收益|带你飞|跟单', fixNote: '金融内容不得承诺收益/保本,荐股需资质;须加风险提示。' }, { id: 'R21', title: '配资 / 加杠杆引导', severity: 'medium', kind: 'violation', fix: 'disclose', industries: ['finance'], commercialOnly: true, pattern: '加杠杆|配资|带你飞|跟单操作', fixNote: '杠杆/配资类引导风险高,需明确风险提示与资质。' }, // —— 平台特定 —— { id: 'R30', title: '小红书未申明商业合作', severity: 'medium', kind: 'violation', fix: 'disclose', platforms: ['xiaohongshu'], commercialOnly: true, pattern: '合作|赞助|广告|下单|链接在|找我买|私信下单', fixNote: '小红书商业笔记需明确标注"合作"/"广告",否则违规。' }, { id: 'R31', title: '抖音诱导关注话术', severity: 'medium', kind: 'violation', fix: 'remove', platforms: ['douyin'], pattern: '双击屏幕|左下角|主页领|点关注|关注我', fixNote: '抖音对诱导关注话术管控较严,建议删除。' }, { id: 'R32', title: '公众号诱导分享', severity: 'medium', kind: 'violation', fix: 'remove', platforms: ['wechat'], pattern: '转发到朋友圈|分享给好友|集赞|转发抽奖|转发抽', fixNote: '公众号诱导分享(集赞/转发抽奖)属违规,建议删除。' }, { id: 'R33', title: '知乎外链 / 私域引流', severity: 'medium', kind: 'violation', fix: 'remove', platforms: ['zhihu'], commercialOnly: true, pattern: '加微信|私聊|我的公众号|关注公众号|小号', fixNote: '知乎对外链/站外引流管控较严,建议删除。' }, // —— 辟谣:没必要的自我审查(myth)—— { id: 'M01', title: '自我审查代称「赚米」', severity: 'low', kind: 'myth', pattern: '赚米|赚米了|搞米', mythNote: '“赚米”是“赚钱”的自我审查代称,平台并未要求这样写,正常写“赚钱”即可,不算违规。', suggestion: '改回“赚钱”。' }, { id: 'M02', title: '违规代称「绿软」', severity: 'low', kind: 'myth', pattern: '绿软|绿色软件', mythNote: '“绿软/绿色软件”常被用作“破解软件”的代称,本身属于违规内容,不是安全表达。', suggestion: '不要用此代称;涉破解软件本身违规。' }, { id: 'M03', title: '暗语「喝茶/喝口茶」', severity: 'low', kind: 'myth', pattern: '喝茶|喝口茶', mythNote: '“喝茶/喝口茶”常被用作私下交易的暗语,平台会识别,并非安全表达。', suggestion: '正常直说即可,避免暗语。' }, { id: 'M04', title: '灰产代称「课件/资料免费领」', severity: 'low', kind: 'myth', pattern: '课件|资料免费领|学习资料', mythNote: '引流场景下的“课件/资料免费领”常被判定违规。', suggestion: '正常表述,避免暗示灰产。' } ]; // 绝对化用语的保意改写(收缩主张,不删实质信息) function narrowAbs(m) { if (/^最/.test(m)) return '较' + m.slice(1); if (m === '第一') return '领先'; if (m === '国家级') return '行业级'; if (m === '唯一') return '少有'; if (m === '顶级') return '优质'; if (m === '极品') return '精品'; if (m === '万能') return '通用'; if (m === '包治' || m === '药到病除') return '辅助'; if (m === '100%' || m === '百分之百') return '多数'; if (m === '全网首发') return '全新发布'; if (m === '史上最低' || m === '全网最低') return '价格优惠'; return m; } function narrowGeneric(m) { return '相关'; } function narrowRival(m) { return '各平台各有侧重'; } // 给规则计算自动修复替换值 function fixReplacement(rule, match) { if (rule.fix === 'remove') return ''; if (rule.fix === 'narrow') { if (typeof rule.alt === 'function') return rule.alt(match); if (typeof rule.alt === 'string') return rule.alt; if (rule.id === 'R01') return narrowAbs(match); if (rule.id === 'R05') return narrowRival(match); return narrowGeneric(match); } return match; // disclose:不自动改 } // ---- 工具 ---- function stripHtml(html) { return (html || '') .replace(//gi, ' ') .replace(//gi, ' ') .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ').replace(/&/g, '&') .replace(/</g, '<').replace(/>/g, '>') .replace(/"/g, '"').replace(/\s+/g, ' ').trim(); } function getSelectedPlatformKeys() { var keys = []; try { var nodes = document.querySelectorAll('#platformPickerGrid .platform-item.selected'); nodes.forEach(function (n) { var name = (n.querySelector('.platform-name') || {}).textContent || ''; var key = PLATFORM_NAME_MAP[(name || '').trim()] || null; if (key && keys.indexOf(key) < 0) keys.push(key); }); } catch (e) {} return keys; } function autoIndustries(text) { var set = {}; MEDICAL_KW.forEach(function (k) { if (text.indexOf(k) >= 0) set.medical = true; }); FINANCE_KW.forEach(function (k) { if (text.indexOf(k) >= 0) set.finance = true; }); return set; } function ruleApplies(rule, ctx) { if (rule.commercialOnly && !ctx.commercial) return false; if (rule.platforms && rule.platforms.length) { if (!ctx.allMode && !rule.platforms.some(function (p) { return ctx.platforms.indexOf(p) >= 0; })) return false; } if (rule.industries && rule.industries.length) { var hit = rule.industries.some(function (i) { return ctx.industries[i]; }); if (!hit) return false; } return true; } function findHits(text, rule, radius) { var hits = []; var re; try { re = new RegExp(rule.pattern, 'gi'); } catch (e) { return hits; } var m; while ((m = re.exec(text)) !== null) { if (!m[0]) { re.lastIndex++; continue; } if (m.index === re.lastIndex) re.lastIndex++; var start = m.index, end = start + m[0].length; var left = Math.max(0, start - radius), right = Math.min(text.length, end + radius); var snippet = (left ? '…' : '') + text.slice(left, right).replace(/\s+/g, ' ').trim() + (right < text.length ? '…' : ''); var line = text.slice(0, start).split('\n').length; hits.push({ match: m[0], line: line, context: snippet }); if (hits.length > 200) break; } return hits; } // ---- 主扫描 ---- function scan(text, opts) { opts = opts || {}; var radius = 24; var commercial = !!opts.commercial; var platforms = opts.platforms || []; var allMode = platforms.length === 0; var industries = opts.industries || {}; var auto = autoIndustries(text); industries.medical = industries.medical || auto.medical; industries.finance = industries.finance || auto.finance; var ctx = { commercial: commercial, platforms: platforms, allMode: allMode, industries: industries }; var candidates = []; var myths = []; RULES.forEach(function (rule) { if (!ruleApplies(rule, ctx)) return; var hits = findHits(text, rule, radius); hits.forEach(function (h) { if (rule.kind === 'myth') { myths.push({ ruleId: rule.id, match: h.match, line: h.line, context: h.context, note: rule.mythNote, suggestion: rule.suggestion }); } else { candidates.push({ ruleId: rule.id, title: rule.title, severity: rule.severity, fix: rule.fix, match: h.match, line: h.line, context: h.context, fixNote: rule.fixNote, autoFix: (rule.fix === 'narrow' || rule.fix === 'remove') }); } }); }); var sevOrder = { high: 4, medium: 2, low: 1 }; candidates.sort(function (a, b) { return (sevOrder[b.severity] - sevOrder[a.severity]) || (a.line - b.line); }); myths.sort(function (a, b) { return a.line - b.line; }); return { candidates: candidates, myths: myths, industries: industries, allMode: allMode, platforms: platforms }; } function verdictOf(report) { var high = 0, med = 0, low = 0; report.candidates.forEach(function (c) { if (c.severity === 'high') high++; else if (c.severity === 'medium') med++; else low++; }); var v; if (high > 0) v = { level: 'high', text: '不建议直接发(' + high + ' 处高风险必改)' }; else if (med > 0) v = { level: 'medium', text: '可以发,但建议先改 ' + med + ' 处' }; else if (low > 0) v = { level: 'low', text: '可以发(有 ' + low + ' 处小提示)' }; else v = { level: 'ok', text: '可以发(本次检查范围内未发现阻断问题)' }; return v; } // ---- 渲染 ---- function esc(s) { return (s || '').replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function render(report, opts) { var body = document.getElementById('cmpBody'); if (!body) return; var v = verdictOf(report); var sevColor = { high: '#d23b3b', medium: '#c47d09', low: '#64748b', ok: '#169a52' }; var sevText = { high: '必改', medium: '建议改', low: '提示' }; var html = ''; // 逐平台结论 + 统计条 html += '
'; html += '
结论:' + esc(v.text) + '
'; var scope = report.allMode ? '全部已选/默认平台' : (report.platforms.join('、') || '当前平台'); html += '
扫描范围:' + esc(scope) + (report.industries.medical ? ' · 医疗健康' : '') + (report.industries.finance ? ' · 金融理财' : '') + (opts.commercial ? ' · 含商业' : '') + '
'; html += '
'; var high = 0, med = 0, low = 0; report.candidates.forEach(function (c) { if (c.severity === 'high') high++; else if (c.severity === 'medium') med++; else low++; }); html += '
'; html += statBox(high, '高风险', sevColor.high); html += statBox(med, '建议改', sevColor.medium); html += statBox(low, '小提示', sevColor.low); html += statBox(report.myths.length, '辟谣', '#7c3aed'); html += '
'; if (report.candidates.length === 0 && report.myths.length === 0) { html += '
未发现词面候选 👍
语义风险仍需你自行把关——本工具只降发布前可见风险,不承诺 100% 过审。
'; } // 问题清单 if (report.candidates.length) { html += '
问题清单(按严重程度)
'; report.candidates.forEach(function (c) { html += '
'; html += '' + (sevText[c.severity] || c.severity) + ''; html += '
'; html += '
第 ' + c.line + ' 行 · ' + esc(c.title) + ' (' + esc(c.ruleId) + ')
'; html += '
“' + esc(c.match) + '” — ' + esc(c.context) + '
'; if (c.fixNote) html += '
改法:' + esc(c.fixNote) + (c.autoFix ? ' 可一键自动处理' : ' 需人工补') + '
'; html += '
'; }); } // 辟谣提示 if (report.myths.length) { html += '
辟谣提示(这些其实不用改)
'; report.myths.forEach(function (m) { html += '
'; html += '
“' + esc(m.match) + '” — ' + esc(m.note) + '
'; if (m.suggestion) html += '
建议:' + esc(m.suggestion) + '
'; html += '
'; }); } // 发布前检查单 html += '
发布前检查单
'; html += '
☐ 标注是否含 AI 生成 / 虚构内容 ☐ 商业合作是否已声明 ☐ 医疗/金融是否已附资质与风险提示 ☐ 图片/引用是否已获授权
'; body.innerHTML = html; // 进度 + 修复按钮 var prog = document.getElementById('cmpProgress'); if (prog) prog.textContent = report.candidates.length + ' 候选 / ' + report.myths.length + ' 辟谣'; var fixBtn = document.getElementById('cmpFixBtn'); if (fixBtn) { var autoCount = report.candidates.filter(function (c) { return c.autoFix; }).length; if (autoCount > 0) { fixBtn.style.display = ''; fixBtn.textContent = '一键修复 ' + autoCount + ' 处可自动处理项'; } else fixBtn.style.display = 'none'; } } function statBox(n, label, color) { return '
' + n + '' + label + '
'; } // ---- 动作 ---- function getText() { try { return getEditorContent ? getEditorContent() : (window.editor ? window.editor.getHtml() : ''); } catch (e) { return ''; } } function runComplianceCheck() { var html = getText(); if (!html || !html.replace(/<[^>]+>/g, '').trim()) { safeToast && safeToast('warn', '编辑器没有内容可扫描'); return; } var text = stripHtml(html); var commercial = document.getElementById('cmpCommercial') && document.getElementById('cmpCommercial').checked; var medical = document.getElementById('cmpMedical') && document.getElementById('cmpMedical').checked; var finance = document.getElementById('cmpFinance') && document.getElementById('cmpFinance').checked; var platforms = getSelectedPlatformKeys(); var industries = {}; if (medical) industries.medical = true; if (finance) industries.finance = true; var report = scan(text, { commercial: commercial, platforms: platforms, industries: industries }); window.__cmpReport = report; window.__cmpOpts = { commercial: commercial }; render(report, { commercial: commercial }); var modal = document.getElementById('complianceModal'); if (modal) { modal.style.display = 'flex'; } } function openComplianceModal() { var modal = document.getElementById('complianceModal'); if (!modal) { safeToast && safeToast('error', '合规自检模块未加载'); return; } modal.style.display = 'flex'; runComplianceCheck(); } function closeComplianceModal() { var modal = document.getElementById('complianceModal'); if (modal) modal.style.display = 'none'; } // 一键修复:对 narrow/remove 类规则做最小改写(保意),disclose 类跳过由人工补 function applyComplianceFixes() { var report = window.__cmpReport; if (!report) return; var html = getText(); if (!html) return; // 用纯文本做替换更安全(避免误伤标签);但需映射回 HTML。 // 这里直接在 HTML 字符串上做大小写不敏感替换(匹配词均为中文/ASCII 短语,不会命中标签)。 var applied = 0; report.candidates.forEach(function (c) { if (!c.autoFix) return; var rule = RULES.filter(function (r) { return r.id === c.ruleId; })[0]; if (!rule) return; try { var re = new RegExp(rule.pattern, 'gi'); html = html.replace(re, function (m) { applied++; return fixReplacement(rule, m); }); } catch (e) {} }); if (applied === 0) { safeToast && safeToast('info', '没有可自动处理的项,请人工补披露'); return; } try { if (window.editor && typeof window.editor.setHtml === 'function') { window.editor.setHtml(html); safeToast && safeToast('success', '已自动处理 ' + applied + ' 处,其余请人工补披露'); // 重新扫描确认 setTimeout(function () { runComplianceCheck(); }, 350); } else { safeToast && safeToast('error', '编辑器实例不可用'); } } catch (e) { safeToast && safeToast('error', '写入失败:' + e.message); } } // 暴露到全局(供 editor.html 内联 onclick 调用) window.openComplianceModal = openComplianceModal; window.closeComplianceModal = closeComplianceModal; window.runComplianceCheck = runComplianceCheck; window.applyComplianceFixes = applyComplianceFixes; })();