mirror of
https://github.com/plolpl789/dsh-raw-html.git
synced 2026-09-28 12:02:55 +08:00
324 lines
23 KiB
JavaScript
324 lines
23 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* dsh-raw-html —— 前端渲染补丁脚本(VCP 视觉通感协议支持)v6
|
||
*
|
||
* 修改 @deepseek-ai/dsh-web-frontend 的 dist bundle(index-*.js):
|
||
*
|
||
* A. pu() 的 case"html" 分支(v6.35 三态化版):
|
||
* localStorage['dsh.rawHtml'] !== '0' 时(未设置=默认开 / "1"=开 / "0"=显式关闭),
|
||
* 调 window.__vcpStable.render() 把 HTML 流式内容转成 React 元素;
|
||
* - 【表情包修复】Markdown 图片语法  先转 <img>(URL 白名单
|
||
* http/https、data:image/*、相对路径,其余协议不生成 img)
|
||
* - 【流式渲染 + 稳定区固化 v6】已渲染块固化 + 只重渲染尾巴:
|
||
* 轻量 HTML 扫描器把流式内容切成「已闭合块 + 尾巴」;已闭合块解析一次
|
||
* 并缓存(元素引用跨帧不变 → React 跳过 diff → DOM 不重建 → 动画真循环、
|
||
* 超长卡片不掉帧);vcp-root 未闭合期间内部闭合子块照样固化,容器开标签
|
||
* 单独解析并包裹组装;每帧只解析新增段;前缀失配(回退/多消息切换)与
|
||
* 容器闭合帧走全量兜底。
|
||
* - 【动画防闪】tail 流式中剥一次性动画(保留 infinite),固化块保留全部
|
||
* 动画(一次性动画在块稳定时播放一次,不闪烁);最终帧(非流式)tail
|
||
* 动画全保留。缓存键区分流式/非流式,避免动画状态串用。
|
||
*
|
||
* B. vc() 属性循环增强 v2(DOM → React 元素转换器):
|
||
* - onclick="input('...')" 属性 → React onClick 处理器(桥接 window.__dshInput)
|
||
* - 过滤 script / iframe / object / embed 标签
|
||
* - style 危险属性过滤(position:fixed / z-index>=1000 / content:)
|
||
* - URL 协议白名单(href: http/https/mailto/相对路径/锚点;src: 同 + data:image)
|
||
*
|
||
* C. vc() 定义前注入 v6 稳定区模块(patch/v6-inject.js):
|
||
* 扫描器 + 状态机 + 组装逻辑,挂载到 window.__vcpStable。
|
||
*
|
||
* 健康检查:补丁写入后自动运行 `node --check` 校验 bundle 语法;
|
||
* 校验失败自动回滚到本次备份并中止。
|
||
*
|
||
* 开关状态由 dsh-raw-html 插件的「</>」按钮写入 localStorage。
|
||
*
|
||
* 用法:
|
||
* node patch-frontend.cjs [bundle路径]
|
||
* 备份:
|
||
* 同目录生成 index-*.js.bak-<时间戳>;恢复时把 .bak 改回原名即可。
|
||
*/
|
||
'use strict'
|
||
|
||
const fs = require('node:fs')
|
||
const path = require('node:path')
|
||
const { execFileSync } = require('node:child_process')
|
||
|
||
/**
|
||
* 自动探测 dsh-web-frontend 的 dist bundle:
|
||
* 依次检查参数指定 → 环境变量 → 常见安装位置(npm 全局 / 用户 .dsh profile)。
|
||
* 找不到时返回 null(由调用方提示手动指定)。
|
||
*/
|
||
function findBundle() {
|
||
// 0) 命令行参数优先
|
||
const arg = process.argv[2]
|
||
if (arg) return fs.existsSync(arg) ? arg : null
|
||
|
||
// 1) 环境变量
|
||
if (process.env.DSH_WEB_FRONTEND_DIST) {
|
||
const p = process.env.DSH_WEB_FRONTEND_DIST
|
||
if (fs.existsSync(p)) return p
|
||
}
|
||
|
||
// 2) 常见安装位置
|
||
const candidates = []
|
||
const addDir = (d) => {
|
||
if (!d || !fs.existsSync(d)) return
|
||
try {
|
||
const assets = path.join(d, 'dist', 'assets')
|
||
if (!fs.existsSync(assets)) return
|
||
for (const f of fs.readdirSync(assets)) {
|
||
if (/^index-[\w-]+\.js$/.test(f)) candidates.push(path.join(assets, f))
|
||
}
|
||
} catch {
|
||
// 目录不可读则跳过。
|
||
}
|
||
}
|
||
addDir(path.join(process.env.APPDATA || '', 'npm', 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-web-frontend'))
|
||
addDir(path.join(process.env.APPDATA || '', 'npm', 'node_modules', '@deepseek-ai', 'dsh-web-frontend'))
|
||
addDir(path.join(process.env.USERPROFILE || '', '.dsh', 'profiles', 'web', 'node_modules', '@deepseek-ai', 'dsh-web-frontend'))
|
||
addDir(path.join(process.env.USERPROFILE || '', '.dsh', 'profiles', 'node_modules', '@deepseek-ai', 'dsh-web-frontend'))
|
||
return candidates[0] || null
|
||
}
|
||
|
||
const file = findBundle()
|
||
|
||
if (!file) {
|
||
console.error('[patch] 未自动找到 dsh-web-frontend 的 dist bundle。')
|
||
console.error('[patch] 用法: node patch-frontend.cjs <bundle路径>')
|
||
console.error('[patch] 例如: node patch-frontend.cjs "C:\\...\\dsh-web-frontend\\dist\\assets\\index-xxx.js"')
|
||
process.exit(1)
|
||
}
|
||
|
||
let t = fs.readFileSync(file, 'utf8')
|
||
|
||
// ---- 锚点 A:case"html" 分支(当前 bundle v4 状态)------------------------
|
||
const CASE_HTML_BEFORE = String.raw`case"html":return function(){if(typeof localStorage!=="undefined"&&localStorage.getItem("dsh.rawHtml")==="1"){try{const v=(n.value||"").replace(/!\[([^\]]*)\]\(([^)]+)\)/g,function(m,a,u){u=u.trim();if(!/^(https?:|data:image\/|\/)/i.test(u))return m;return"<img alt=\""+((a||"").replace(/"/g,"""))+"\" src=\""+u.replace(/"/g,""")+"\">"}),w=i.streaming?v.replace(/animation:([^;"']*);?/g,function(m,x){return/infinite/.test(x)?m:""}):v,F=window.__vcpFast||(window.__vcpFast={c:new Map(),last:"",el:null,hits:0,builds:0,ms:0,lastLog:0}),t0=typeof performance!=="undefined"&&performance.now?performance.now():0;if(F.c.has(w)){F.hits++;if(t0&&performance.now()-F.lastLog>2000){F.lastLog=performance.now();console.debug("[vcp-fast] HIT size="+F.c.size+" hits="+F.hits+" builds="+F.builds)}return F.c.get(w)}let R=null;if(F.last!==""&&F.el!==null&&w.startsWith(F.last)&&(/<\/[a-zA-Z][^>]*>\s*$/.test(F.last)||/<[a-zA-Z][^>]*\/>\s*$/.test(F.last))){const tb=new DOMParser().parseFromString(w.slice(F.last.length),"text/html").body,to=[];for(let k=0;k<tb.childNodes.length;k++)to.push(vc(tb.childNodes[k],k));if(to.length===0)R=F.el;else R=f.jsx(f.Fragment,{children:[F.el,to.length===1?to[0]:f.jsx(f.Fragment,{children:to})]})}else{const b=new DOMParser().parseFromString(w,"text/html").body;if(!b.childNodes.length)return null;const o=[];for(let k=0;k<b.childNodes.length;k++)o.push(vc(b.childNodes[k],k));R=o.length===1?o[0]:f.jsx(f.Fragment,{children:o})}F.c.set(w,R);if(F.c.size>200)F.c.clear();F.last=w;F.el=R;F.builds++;if(t0){F.ms+=performance.now()-t0;if(performance.now()-F.lastLog>2000){F.lastLog=performance.now();console.debug("[vcp-fast] BUILD size="+F.c.size+" avg="+(F.ms/F.builds).toFixed(2)+"ms last="+w.length)}}return R}catch(e){}}return n.value}();`
|
||
|
||
// v5:图片 URL 白名单(http/https/data:image/相对路径),其余协议不生成 img
|
||
const CASE_HTML_AFTER = String.raw`case"html":return function(){if(typeof localStorage!=="undefined"&&localStorage.getItem("dsh.rawHtml")==="1"){try{const v=(n.value||"").replace(/!\[([^\]]*)\]\(([^)]+)\)/g,function(m,a,u){u=u.trim();if(!/^(https?:|data:image\/|\/)/i.test(u))return m;return"<img alt=\""+((a||"").replace(/"/g,"""))+"\" src=\""+u.replace(/"/g,""")+"\">"}),w=i.streaming?v.replace(/animation:([^;"']*);?/g,function(m,x){return/infinite/.test(x)?m:""}):v,F=window.__vcpFast||(window.__vcpFast={c:new Map(),last:"",el:null,hits:0,builds:0,ms:0,lastLog:0}),t0=typeof performance!=="undefined"&&performance.now?performance.now():0;if(F.c.has(w)){F.hits++;if(t0&&performance.now()-F.lastLog>2000){F.lastLog=performance.now();console.debug("[vcp-fast] HIT size="+F.c.size+" hits="+F.hits+" builds="+F.builds)}return F.c.get(w)}let R=null;if(F.last!==""&&F.el!==null&&w.startsWith(F.last)&&(/<\/[a-zA-Z][^>]*>\s*$/.test(F.last)||/<[a-zA-Z][^>]*\/>\s*$/.test(F.last))){const tb=new DOMParser().parseFromString(w.slice(F.last.length),"text/html").body,to=[];for(let k=0;k<tb.childNodes.length;k++)to.push(vc(tb.childNodes[k],k));if(to.length===0)R=F.el;else R=f.jsx(f.Fragment,{children:[F.el,to.length===1?to[0]:f.jsx(f.Fragment,{children:to})]})}else{const b=new DOMParser().parseFromString(w,"text/html").body;if(!b.childNodes.length)return null;const o=[];for(let k=0;k<b.childNodes.length;k++)o.push(vc(b.childNodes[k],k));R=o.length===1?o[0]:f.jsx(f.Fragment,{children:o})}F.c.set(w,R);if(F.c.size>200)F.c.clear();F.last=w;F.el=R;F.builds++;if(t0){F.ms+=performance.now()-t0;if(performance.now()-F.lastLog>2000){F.lastLog=performance.now();console.debug("[vcp-fast] BUILD size="+F.c.size+" avg="+(F.ms/F.builds).toFixed(2)+"ms last="+w.length)}}return R}catch(e){}}return n.value`
|
||
|
||
// v6:稳定区固化 —— case"html" 改为调 window.__vcpStable.render(模块由锚点 C 注入)
|
||
// 注意:BEFORE 是含尾部 `}();` 的完整分支文本,AFTER 必须同样以 `}();` 结尾(IIFE 调用收尾)
|
||
const CASE_HTML_V6_AFTER = String.raw`case"html":return function(){if(typeof localStorage!=="undefined"&&localStorage.getItem("dsh.rawHtml")==="1"){try{var vr=window.__vcpStable&&window.__vcpStable.render(n.value,i.streaming);if(vr!==null&&vr!==undefined)return vr}catch(e){}}return n.value}();`
|
||
|
||
// v6.35(先生定调 2026-08-29):html 分支开关三态化 ——
|
||
// 原 v6 要求 dsh.rawHtml==="1" 才渲染;但该键只在用户点过「</>」按钮后才存在,
|
||
// 新环境/清缓存后为 undefined → 一切裸 HTML(含 VCP 卡)显示源码(本次事故根因)。
|
||
// 改为 !== "0":undefined(从未设置)= 默认渲染(配合 v6-inject 启动自检落盘 "1"),
|
||
// 用户经「</>」按钮显式关闭("0")才显示源码;并加 console.warn 诊断回退原因。
|
||
const CASE_HTML_V7_AFTER = String.raw`case"html":return function(){if(typeof localStorage!=="undefined"&&localStorage.getItem("dsh.rawHtml")!=="0"){try{var vr=window.__vcpStable&&window.__vcpStable.render(n.value,i.streaming);if(vr!==null&&vr!==undefined)return vr}catch(e){if(typeof console!=="undefined")console.warn("[vcp] html 渲染异常,已回退源码",e)}}else if(typeof console!=="undefined")console.warn("[vcp] html 渲染已关闭(dsh.rawHtml===\"0\",</> 按钮可重新开启)");return n.value}();`
|
||
|
||
// ---- 锚点 B:vc() 属性循环(当前 bundle 增强 v2 状态)-----------------------
|
||
const VC_LOOP_BEFORE = String.raw`for(const c of i.attributes){if(c.name==="onclick"){const m=/^input\s*\(\s*['"]([\s\S]*?)['"]\s*\)\s*;?\s*$/.exec(c.value);if(m)s.onClick=function(){const fn=window.__dshInput;fn&&fn(m[1])};continue}if(c.name==="style"){let sv=c.value;sv=sv.replace(/position\s*:\s*fixed\s*;?/gi,"").replace(/z-index\s*:\s*\d{4,}\s*;?/gi,"").replace(/content\s*:\s*[^;"']*;?/gi,"");s.style=hp(sv);continue}if(c.name==="class"){s.className=c.value;continue}if(c.name==="href"&&!/^(https?:|mailto:|\/|#)/i.test(c.value))continue;if(c.name==="src"&&!/^(https?:|data:image\/|\/|#)/i.test(c.value))continue;s[c.name]=c.value}`
|
||
|
||
// v2.1:修复 content 过滤——旧正则 [^;"']* 匹配不了引号包裹的值(content:"文字"),
|
||
// 导致伪元素文字注入漏网;改为 [^;]* 到分号为止全剥
|
||
const VC_LOOP_AFTER = String.raw`for(const c of i.attributes){if(c.name==="onclick"){const m=/^input\s*\(\s*['"]([\s\S]*?)['"]\s*\)\s*;?\s*$/.exec(c.value);if(m)s.onClick=function(){const fn=window.__dshInput;fn&&fn(m[1])};continue}if(c.name==="style"){let sv=c.value;sv=sv.replace(/position\s*:\s*fixed\s*;?/gi,"").replace(/z-index\s*:\s*\d{4,}\s*;?/gi,"").replace(/content\s*:[^;]*;?/gi,"");s.style=hp(sv);continue}if(c.name==="class"){s.className=c.value;continue}if(c.name==="href"&&!/^(https?:|mailto:|\/|#)/i.test(c.value))continue;if(c.name==="src"&&!/^(https?:|data:image\/|\/|#)/i.test(c.value))continue;s[c.name]=c.value}`
|
||
|
||
// ---- 锚点 B:vc 属性循环 content 正则加词边界(v6.5 修复)------------------
|
||
// 旧正则 /content\s*:[^;]*;?/ 会误伤 justify-content(content 是 justify-content 的子串),
|
||
// 把「justify-content: center」剥成「justify-」,并与后随的 animation 粘连成
|
||
// 「justify-animation」,导致 animation 属性丢失、动画失效。加后行断言 (?<![\w-])
|
||
// 让 content 前面不能是单词字符或连字符,仅匹配独立的 content: 属性。
|
||
const CONTENT_RE_BEFORE = String.raw`replace(/content\s*:[^;]*;?/gi,"")`
|
||
const CONTENT_RE_AFTER = String.raw`replace(/(?<![\w-])content\s*:[^;]*;?/gi,"")`
|
||
|
||
// ---- 锚点 D:style 分支加 ref 回调锁定文字属性(v6.11 优先级锁定)-----------
|
||
// 需求:VCP 卡片内联 style 的文字颜色/字体/字号必须最高优先级,不被其他
|
||
// 字体/主题插件的全局 !important 规则覆盖。React 内联 style 不支持 !important
|
||
// (style 值带 "!important" 后缀会被 setProperty 吞掉),唯一可靠方式是在 ref
|
||
// 回调里对文字相关属性 el.style.setProperty(prop, val, "important")。
|
||
// 锁定属性:color/font-family/font-size/font-weight/font-style/line-height/
|
||
// letter-spacing/text-align/text-shadow(不含布局与 animation,避免影响流式动画)。
|
||
const STYLE_REF_BEFORE = String.raw`s.style=hp(sv);continue}`
|
||
const STYLE_REF_AFTER = String.raw`s.style=hp(sv);s.ref=function(el){if(el)for(const d of sv.split(";")){const j=d.indexOf(":");if(j===-1)continue;const p=d.slice(0,j).trim(),v=d.slice(j+1).trim();if(v&&/^(color|font-family|font-size|font-weight|font-style|line-height|letter-spacing|text-align|text-shadow)$/.test(p))el.style.setProperty(p,v,"important")}};continue}`
|
||
|
||
// ---- 锚点 E:代码围栏兜底(v6.30 → v6.32 升级)---------------------------
|
||
// Ox Alpha 实测:协议只说「直接输出 HTML」未禁止围栏,模型把卡片包在
|
||
// ```html ... ``` 里 → markdown 解析为 code 节点 → 显示源码。
|
||
// 兜底:代码块内容以 <div id="vcp-root" 开头 → 直接调 vcp render(围栏剥离、
|
||
// 裸 HTML 渲染),符合「HTML 即渲染」语义;判定保守,普通代码示例不受影响。
|
||
// v6.32 修复:围栏兜底必须带渲染开关检查(localStorage['dsh.rawHtml']==='1'),
|
||
// 否则关闭渲染插件后围栏 HTML 仍被强制渲染成卡片(先生实测发现)。
|
||
const CODE_CASE_RAW = String.raw`case"code":return wp(n,r,i);`
|
||
const CODE_CASE_V1 = String.raw`case"code":{try{const _v=(n.value||"").trim();if(/^<div\s+id=["']vcp-root["']/i.test(_v)){const _r=window.__vcpStable&&window.__vcpStable.render(n.value,i.streaming);if(_r!==null&&_r!==undefined)return _r}}catch(_e){}return wp(n,r,i)}`
|
||
const CODE_CASE_V2 = String.raw`case"code":{try{const _v=(n.value||"").trim();if(typeof localStorage!=="undefined"&&localStorage.getItem("dsh.rawHtml")==="1"&&/^<div\s+id=["']vcp-root["']/i.test(_v)){const _r=window.__vcpStable&&window.__vcpStable.render(n.value,i.streaming);if(_r!==null&&_r!==undefined)return _r}}catch(_e){}return wp(n,r,i)}`
|
||
// v6.35(先生定调 2026-08-29):围栏兜底三态化 —— 原 v6.32 带 === "1" 开关检查,
|
||
// 开关未设置(undefined)时围栏包 VCP 卡也显示源码(本次事故形态)。改为 !== "0":
|
||
// 白名单(vcp-root 开头)仍保留防误伤普通代码块,显式关闭("0")才不接管。
|
||
const CODE_CASE_V3 = String.raw`case"code":{try{const _v=(n.value||"").trim();if(typeof localStorage!=="undefined"&&localStorage.getItem("dsh.rawHtml")!=="0"&&/^<div\s+id=["']vcp-root["']/i.test(_v)){const _r=window.__vcpStable&&window.__vcpStable.render(n.value,i.streaming);if(_r!==null&&_r!==undefined)return _r}}catch(_e){if(typeof console!=="undefined")console.warn("[vcp] code 围栏渲染异常,已回退代码块",_e)}return wp(n,r,i)}`
|
||
|
||
// ---- 锚点 F:bc(markdown 组件)解析前做卡片空行修复(v6.37 · 先生 2026-08-29)----
|
||
// CommonMark type 6 规则:<div> 等块级标签【不能打断段落】——消息以
|
||
// 「文字前言 + 换行 + <div id="vcp-root">」(中间无空行)输出时,mdast 把
|
||
// <div> 开标签当【段落内联 HTML】,后续 <div class="paper"> 等脱离 vcp-root
|
||
// 变成兄弟节点 → 根容器背景生效但子选择器全失效(先生实测「最外围框住、
|
||
// 内部格式全掉」)。修复:解析前调用注入块的 fixVcpBlank(window.__vcpStable
|
||
// 挂载)把该换行补成空行,让卡片成为独立 htmlFlow;无 vcp-root 或已是空行
|
||
// 时幂等不动。流式 Mp / 非流式 bp 共用同一 t。
|
||
const BC_BEFORE = String.raw`const bc=R.memo(function({text:r,streaming:i=!1,codeLabels:s,fileMentions:u}){const c=R.useRef(null),h=R.useRef(s),p=R.useMemo(()=>i?((c.current===null||h.current!==s)&&(c.current=new Mp(s),h.current=s),c.current.render(r)):(c.current=null,bp(r,s,u)),[r,i,s,u]);return f.jsx("div",{className:S1.markdown,children:p})})`
|
||
const BC_AFTER = String.raw`const bc=R.memo(function({text:r,streaming:i=!1,codeLabels:s,fileMentions:u}){const c=R.useRef(null),h=R.useRef(s),t=(window.__vcpStable&&window.__vcpStable.fixBlank?window.__vcpStable.fixBlank(r):r),p=R.useMemo(()=>i?((c.current===null||h.current!==s)&&(c.current=new Mp(s),h.current=s),c.current.render(t)):(c.current=null,bp(t,s,u)),[r,i,s,u]);return f.jsx("div",{className:S1.markdown,children:p})})`
|
||
|
||
// ---- 锚点 C:vc() 定义前注入 v6 稳定区模块 ----
|
||
// 模块源码来自同目录 v6-inject.js(无 import/export,依赖闭包 vc/hp/f 与全局 window/DOMParser)。
|
||
// BEFORE 用「hp 函数结束 + vc 函数开始」的拼接锚点,保证唯一且幂等(注入后不再命中)。
|
||
const VC_DEF_BEFORE = String.raw`}function vc(n,r){`
|
||
let VC_INJECT = ''
|
||
try {
|
||
VC_INJECT = fs.readFileSync(path.join(__dirname, 'v6-inject.js'), 'utf8')
|
||
} catch (e) {
|
||
console.error('[patch] 读取 patch/v6-inject.js 失败:', e.message)
|
||
process.exit(1)
|
||
}
|
||
const VC_DEF_AFTER = '}' + VC_INJECT + ';function vc(n,r){'
|
||
|
||
// ---- v6 模块更新:已注入(含标记)→ 就地替换整块;未注入 → 走锚点 C ----
|
||
// v6.12 起支持幂等重打:v6-inject.js 首尾带 __DSH_V6_INJECT_START__/__END__ 标记,
|
||
// 重跑补丁时直接把旧块换成新块,无需先恢复备份;旧版无标记注入也兼容升级。
|
||
let v6Handled = false
|
||
{
|
||
const START_MARK = '/*__DSH_V6_INJECT_START__*/'
|
||
const END_MARK = '/*__DSH_V6_INJECT_END__*/'
|
||
const si = t.indexOf(START_MARK)
|
||
const ei = t.indexOf(END_MARK)
|
||
if (si !== -1 && ei !== -1 && ei > si) {
|
||
t = t.slice(0, si) + VC_INJECT + t.slice(ei + END_MARK.length)
|
||
v6Handled = true
|
||
console.log('[patch] v6 模块就地更新(幂等重打)')
|
||
} else if (t.indexOf('window.__vcpStable = { render: render }') !== -1) {
|
||
// 旧版无标记注入(v6.12 之前打的补丁):定位旧块首尾升级
|
||
const oldStart = ';(function () {\n // 无障碍:'
|
||
const oldEnd = 'window.__vcpStable = { render: render }\n})()'
|
||
const s2 = t.indexOf(oldStart)
|
||
const e2 = t.indexOf(oldEnd)
|
||
if (s2 !== -1 && e2 !== -1 && e2 > s2) {
|
||
t = t.slice(0, s2) + VC_INJECT + t.slice(e2 + oldEnd.length)
|
||
v6Handled = true
|
||
console.log('[patch] 旧版 v6 模块就地升级(无标记注入)')
|
||
} else {
|
||
console.error('[patch] 检测到旧版 v6 注入但无法定位旧块边界,请先恢复 .bak 备份再重打')
|
||
process.exit(1)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 围栏兜底升级状态机(v6.32 → v6.35):v6.32 带 === "1" 开关检查(新环境
|
||
// undefined 时 VCP 卡显示源码);v6.35 三态化(!== "0":未设置默认开、显式关闭才拒)。
|
||
let codeCaseHandled = false
|
||
if (t.indexOf(CODE_CASE_V3) !== -1) {
|
||
codeCaseHandled = true
|
||
console.log('[patch] 围栏兜底已是最新版(v6.35 三态化),跳过')
|
||
} else if (t.indexOf(CODE_CASE_V2) !== -1) {
|
||
t = t.replace(CODE_CASE_V2, CODE_CASE_V3)
|
||
codeCaseHandled = true
|
||
console.log('[patch] 围栏兜底升级:v6.32 开关检查 → v6.35 三态化')
|
||
} else if (t.indexOf(CODE_CASE_V1) !== -1) {
|
||
t = t.replace(CODE_CASE_V1, CODE_CASE_V3)
|
||
codeCaseHandled = true
|
||
console.log('[patch] 围栏兜底升级:v6.30 纯白名单 → v6.35 三态化')
|
||
}
|
||
|
||
// ---- html 分支升级状态机(v6/v5 → v6.35):同上三态化,消除「开关未设置 =
|
||
// 一切裸 HTML 显示源码」的陷阱;旧版(v5 无 render 调用 / v6 === "1")一律升级。
|
||
let htmlCaseHandled = false
|
||
if (t.indexOf(CASE_HTML_V7_AFTER) !== -1) {
|
||
htmlCaseHandled = true
|
||
console.log('[patch] html 分支已是最新版(v6.35 三态化),跳过')
|
||
} else if (t.indexOf(CASE_HTML_V6_AFTER) !== -1) {
|
||
t = t.replace(CASE_HTML_V6_AFTER, CASE_HTML_V7_AFTER)
|
||
htmlCaseHandled = true
|
||
console.log('[patch] html 分支升级:v6 开关检查 → v6.35 三态化')
|
||
} else if (t.indexOf(CASE_HTML_AFTER) !== -1) {
|
||
t = t.replace(CASE_HTML_AFTER, CASE_HTML_V7_AFTER)
|
||
htmlCaseHandled = true
|
||
console.log('[patch] html 分支升级:v5 → v6.35 三态化')
|
||
}
|
||
|
||
/**
|
||
* 替换表:label / from(必须唯一命中)/ to。
|
||
* v6 补丁 = 锚点 C(注入稳定区模块)+ 锚点 A(case"html" 改调 render)。
|
||
* 注:vc 属性循环(content 过滤 v2.1)已在 v5.1 应用,无需重复。
|
||
* 幂等:已应用的锚点(to 已存在)自动跳过;未应用的锚点(from 唯一命中)应用。
|
||
*/
|
||
const REPLACEMENTS = []
|
||
if (!v6Handled) {
|
||
REPLACEMENTS.push({
|
||
label: 'C.注入 v6 稳定区模块(vc 定义前)',
|
||
from: VC_DEF_BEFORE,
|
||
to: VC_DEF_AFTER,
|
||
})
|
||
}
|
||
if (!codeCaseHandled) {
|
||
REPLACEMENTS.push({
|
||
label: 'E.case"code" 围栏兜底(v6.35 三态化:vcp-root 白名单 + 显式关闭才拒)',
|
||
from: CODE_CASE_RAW,
|
||
to: CODE_CASE_V3,
|
||
})
|
||
}
|
||
REPLACEMENTS.push(
|
||
{
|
||
label: 'B.vc content 正则加词边界(防误伤 justify-content)',
|
||
from: CONTENT_RE_BEFORE,
|
||
to: CONTENT_RE_AFTER,
|
||
},
|
||
{
|
||
label: 'D.vc style 分支加 ref 锁定文字属性优先级',
|
||
from: STYLE_REF_BEFORE,
|
||
to: STYLE_REF_AFTER,
|
||
},
|
||
)
|
||
if (!htmlCaseHandled) {
|
||
REPLACEMENTS.push({
|
||
label: 'A.case"html" 分支(v5/v6 → v6.35 三态化:未设置默认开)',
|
||
from: CASE_HTML_BEFORE,
|
||
to: CASE_HTML_V7_AFTER,
|
||
})
|
||
}
|
||
REPLACEMENTS.push({
|
||
label: 'F.bc 卡片空行修复(v6.37:文字+换行+<div> 撕裂 → 补空行成独立 htmlFlow)',
|
||
from: BC_BEFORE,
|
||
to: BC_AFTER,
|
||
})
|
||
|
||
let changed = 0
|
||
for (const r of REPLACEMENTS) {
|
||
const count = t.split(r.from).length - 1
|
||
if (count === 1) {
|
||
t = t.split(r.from).join(r.to)
|
||
changed += 1
|
||
} else if (count === 0 && t.indexOf(r.to) !== -1) {
|
||
console.log(`[patch] 锚点 "${r.label}" 已应用,跳过`)
|
||
} else {
|
||
console.error(`[patch] 锚点 "${r.label}" 命中 ${count} 次(需要恰好 1 次),中止,未写入任何修改`)
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
// 备份原文件(仅当本次确有改动且尚无备份时)
|
||
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||
const bak = `${file}.bak-${stamp}`
|
||
fs.copyFileSync(file, bak)
|
||
fs.writeFileSync(file, t, 'utf8')
|
||
|
||
console.log(`[patch] 完成:${changed} 处补丁已写入`)
|
||
console.log(`[patch] 文件: ${file}`)
|
||
console.log(`[patch] 备份: ${bak}`)
|
||
|
||
// 健康检查:node --check 校验 bundle 语法;失败自动回滚
|
||
try {
|
||
execFileSync(process.execPath, ['--check', file], { stdio: 'pipe' })
|
||
console.log('[patch] 健康检查通过:node --check OK')
|
||
} catch (e) {
|
||
if (fs.existsSync(bak)) {
|
||
fs.copyFileSync(bak, file)
|
||
console.error('[patch] 健康检查失败,已回滚到本次备份,未留下损坏产物!')
|
||
console.error('[patch] 失败详情:', String(e.stderr || e.message).split('\n')[0])
|
||
} else {
|
||
console.error('[patch] 健康检查失败且无备份可回滚,请手工修复!')
|
||
}
|
||
process.exit(1)
|
||
}
|
||
|
||
console.log('[patch] 刷新浏览器即可生效(浏览器缓存较旧时请强刷 Ctrl+F5)')
|