@ethanyu99/model-replace-js
v4.0.0
Published
A framework-agnostic DOM text replacement runtime for model aliases.
Readme
Model Replace JS
一个与框架无关的 DOM 文本替换脚本。业务项目只需引入一个 CDN <script>,无需安装浏览器插件,也无需修改 React/Vue 渲染逻辑。
浏览器数据源统一使用 internal-model-proxy:实时读取总开关,一次获取三个规则组,按 LLM > SKU > 模型 CMS 合并,并保存完整缓存和备份。
快速接入
发布 npm 包后,构建产物 dist/snippet.html 会生成带固定版本和 SRI 的完整标签:
<script
defer
src="https://cdn.jsdelivr.net/npm/@ethanyu99/[email protected]/dist/model-replace.min.js"
integrity="构建生成的 sha384"
crossorigin="anonymous"
data-model-replace
></script>建议把标签放在 <head> 中、业务应用 bundle 之前。脚本会在 DOM 可用后扫描现有 Text Node,并通过 MutationObserver 继续处理 SPA 动态内容和弹窗表单。
脚本启动时先请求 proxy 的总开关;明确开启后读取规则并执行替换。只有网络请求失败时才允许沿用该 proxy 上次确认的开关状态。 三个规则组通过一次聚合请求返回,相同关键词采用 LLM > SKU > 模型 CMS,全部为全局规则。 已有完整缓存时立即分片替换;冷启动默认前台等待 200 毫秒,超时后继续后台加载并自动补扫。 已成功替换的元素保持只处理一次。
默认使用国内 proxy:https://internal-model-proxy.paigod.work。
海外页面设置 data-proxy-base-url="https://internal-model-proxy.pplabs.tech",两个环境使用同一份 JS。
构建产物 dist/snippet-cn.html、dist/snippet-overseas.html 分别提供带固定版本和 SRI 的完整标签。
跨域业务页面必须先在入口配置 CORS;也可通过 data-proxy-base-url 指向同源网关。
映射配置
飞书 skuCode / 代号 可通过 npm run sync:sku 预览并同步到 SKU CMS,支持去重、
PATCH 更新、字段类型兼容、备份和失败恢复。配置与执行方式见 SKU 同步脚本。
完整模型映射可通过 npm run sync:model 推导关键词,并同步到 model CMS;
--apply 会新增、更新并清理表外规则,保存备份及无法用关键词完整表达的模型差异。
配置与执行方式见 模型关键词同步脚本。
手动规则示例维护在以下文件中;不会嵌入运行时或用作网络失败的兜底:
config/model-aliases.json格式:
{
"version": "2026-07-28-01",
"enabled": true,
"rules": {
"source-model": "public-model"
}
}构建时会:
- 构建从 proxy 读取数据的 CDN bundle。
- 复制示例 JSON 到
dist/model-aliases.json,供显式手动规则模式参考。 - 运行时将成功加载或缓存的规则按 source 长度降序编译,优先匹配完整模型 ID,再匹配系列词。
Proxy 规则数据源
浏览器只访问同一个 proxy 的两个接口:
| 接口 | 用途 |
| --- | --- |
| /api/items/open_model_replace/1 | 总开关,cache: "no-store";网络失败时可用上次确认的状态 |
| /api/model-replace/rules | 一次返回三个有效规则数组,浏览器不再分页 |
{
"llm_list": [{ "goal_keyword": "original-model", "replace_keyword": "display-model" }],
"sku_list": [{ "goal_keyword": "ORIGINAL_SKU", "replace_keyword": "DISPLAY_SKU" }],
"model_replace_keyword": [{ "goal_keyword": "original-keyword", "replace_keyword": "display-keyword" }]
}三个数组均须存在。每条记录的原词和替换词须为非空字符串,客户端去掉首尾空白并忽略其他字段。
任一数组缺失或记录无效时,整轮更新失败,保留上一份完整快照;合法空数组用于清除对应组的旧规则。
所有远程规则全局生效,不再解析 include_page_paths、exclude_domain 或 scopes。
配置入口
<script defer src="CDN_URL" data-model-replace
data-proxy-base-url="https://rules.example.com/gateway/model-replace"
></script>该配置请求 https://rules.example.com/gateway/model-replace/api/model-replace/rules,
总开关也使用同一个主机和路径前缀。支持 /model-proxy 这样的相对路径。
await ModelReplace.start({
proxyBaseUrl: "https://rules.example.com/gateway/model-replace",
startupTimeoutMs: 200,
refreshTimeoutMs: 15000,
});也可在加载脚本前设置全局变量,或者构建时注入:
window.MODEL_REPLACE_PROXY_BASE_URL = "https://rules.example.com";MODEL_REPLACE_PROXY_BASE_URL="https://rules.example.com" npm run build优先级:启动参数 / Script 属性 → 全局变量 → 构建变量 → 默认 Railway 地址。 显式空字符串、无效协议或带用户名/密码的 URL 会保持开关关闭,不回退默认主机。 请求不携带 Cookie、Basic Auth 或 Bearer Token,也不跟随重定向。上游鉴权只配置在 proxy 服务端。
从分源配置迁移
移除 cmsBaseUrl、skuBaseUrl、apiBaseUrl、ppioBaseUrl、baseUrl 及各来源的 *RulesUrl / rulesUrl,
以及对应的 data-*、MODEL_REPLACE_CMS_BASE_URL、MODEL_REPLACE_SKU_BASE_URL、MODEL_REPLACE_API_BASE_URL、
MODEL_REPLACE_PPIO_BASE_URL、MODEL_REPLACE_BASE_URL 配置。统一改为 proxyBaseUrl / data-proxy-base-url / MODEL_REPLACE_PROXY_BASE_URL。
旧地址配置不再参与请求,CMS 总开关也不会直连原上游。
导出常量改为 DEFAULT_PROXY_BASE_URL、DEFAULT_RULES_URL、DEFAULT_OPEN_STATUS_URL;旧分源 URL 常量已移除。
api: false / data-api="false" 只排除 llm_list,sku: false / data-sku="false" 只排除 sku_list。
ppio / data-ppio 仅作为 api 布尔开关的兼容别名,新开关优先。
这些开关不增加网络请求;完整聚合结果仍只请求一次。
服务端同步工具 sync:model、sync:sku 继续直接管理 CMS;它们不在浏览器 bundle 中。
历史分源代理工具 保留维护,但不是新客户端的聚合服务。
新的聚合服务部署在 internal-model-proxy。
源码更新不会改变已经发布的 npm/CDN 资产;使用新协议前需发布新版本并更新页面的脚本地址,不要覆盖已有版本。
合并、缓存与故障处理
同原词去除首尾空白并忽略大小写,优先级固定为 llm_list > sku_list > model_replace_keyword。 不同关键词合并后按长度优先匹配,保留原词等于替换词的映射,保护完整名称不被短词误替换。 优先级解决同原词冲突;不同原词应避免共用同一别名,以免反向恢复出现歧义。
客户端把三个组作为一个完整快照写入 model-replace:proxy-rules:v1,并保存 :backup。
快照按完整聚合 URL 隔离,最多保留 10 个 URL;旧 v3/v4 分源缓存不迁移,避免不同协议和规则范围混用。
主缓存损坏时读取完整备份,localStorage 不可用时保留页面内存快照。
getStatus().sources 保留 api、cms-sku、cms 三个来源 ID,对应 LLM、SKU 和模型 CMS;它们的请求 URL 相同。
默认浏览器缓存有效期为 5 分钟。新鲜快照不重复下载规则,但每次启动仍实时检查总开关。
过期快照先使用、再后台刷新;请求或响应校验失败时保留整份旧快照。
没有完整缓存时,前台默认等待 200 毫秒;超时仅结束前台等待,页面保持原词,rulesPending: true,后台继续加载。
完整结果返回后一次提交并自动补扫。请求最多等待 5 秒,后台整轮期限默认 15 秒,均包含响应体读取。
后台失败且没有完整快照时保持页面原样:不启动 DOM 扫描、MutationObserver 或表单改写,状态为 active: false。
远程模式不会使用内置或显式传入的手动规则兜底;合法的空数组表示清空规则,不视为网络异常。
失败后不会自动持续重试,可再次 start() 发起新一轮加载。
proxy 在服务端每分钟刷新;浏览器缓存的 5 分钟期限独立生效,服务端轮询不会主动推送更新给已打开的页面。
可用 cacheTtlMs: 0 让每次启动重新获取规则。客户端不请求上游分页,也不在出错时切换到 CMS / API 原站。
每次启动先实时读取总开关,并将成功确认的布尔值按完整 URL 保存到 model-replace:proxy-open-status:v1。
断网、CORS 拦截、请求超时或响应流中断时可沿用缓存状态;只有缓存状态为开启且有完整规则可用时才替换页面。
没有缓存时保持页面原样。明确关闭、HTTP 错误(包括 401/503)或无效开关响应时保持关闭,不会被旧的开启状态覆盖。
开关状态和规则快照均有页面内存缓存,localStorage 不可用时仍可在本页重复启动时使用;跨页面则需要可用的持久缓存。
浏览器网络接入
Proxy 域名的 HTTP 200 不代表跨域页面可读。跨域入口须对业务页面设置精确的
Access-Control-Allow-Origin,并为多个允许的 Origin 正确设置 Vary: Origin;可由既有 Ingress / 网关处理。
国内入口需允许实际国内业务页面,海外入口需允许实际海外业务页面;JS 无法绕过浏览器的 CORS 限制。
若采用同源网关,例如 data-proxy-base-url="/model-proxy",则无需浏览器跨域授权。
本项目不更改公司网段限制、网关鉴权或 proxy 的 CORS 策略。
页面 CSP 的 connect-src 需允许所选 proxy / 网关。总开关的真实值由 CMS 管理,接入改造不会自动打开它。
DOM 按文档顺序增量遍历,优先处理前面的字段,每批目标预算 4 毫秒,通过 scheduler.yield() 让出
主线程,并定期插入普通定时器,避免高优先级续执行饿死业务定时器;不支持该 API 时
全部使用定时器。动态新增节点合并排队,开放 Shadow DOM 和大型下拉框的 option 同样分批处理。
预算在元素之间检查,单个巨大文本节点、浏览器布局和 GC 仍可能超出预算。
start() 在本轮初始扫描完成或进入等待后台数据状态后返回。业务启动不应依赖它:
无需主动等待时使用 void ModelReplace.start(),页面渲染和交互可照常继续。
已成功替换的 DOM 仍保持每个元素只替换一次,规则刷新仅影响未标记或新增元素。
因此旧缓存或后台失败降级产生的已替换结果不会被重做;要求首帧绝对一致时应在 BFF
或服务端渲染阶段完成映射。stop() 或再次 start() 会取消前一轮加载和未完成的扫描分片。
关键词匹配继续忽略大小写并保留输入大小写样式,例如 grok → mars、Grok → Mars、
GROK → MARS。
Script 配置
| 属性 | 默认值 | 说明 |
| --- | --- | --- |
| data-model-replace | - | 标识当前 CDN 脚本 |
| data-auto-start | true | 设为 false 后由业务代码手动启动 |
| data-proxy-base-url | https://internal-model-proxy.paigod.work | 海外使用 https://internal-model-proxy.pplabs.tech;保留路径前缀 |
| data-api | true | 是否使用聚合响应中的 LLM 规则 |
| data-sku | true | 是否使用聚合响应中的 SKU 规则 |
| data-remote-rules | true | 是否加载远程规则;手动 API 传入 ruleSet 时默认 false |
| data-startup-timeout-ms | 200 | 冷启动前台规则等待上限,超时后继续后台加载,须为正数 |
| data-refresh-timeout-ms | 15000 | 每轮后台规则加载的硬期限,超时取消未完成请求,须为正数 |
| data-cache-ttl-ms | 300000 | 完整规则缓存有效期;设为 0 时每次启动都在后台重新验证 |
| data-observe | true | 是否监听后续 DOM 变化 |
| data-form-controls | true | 是否直接替换表单控件的真实 value |
| data-preserve-form-values | false | 表单显示别名,但原生提交保留原始 value;需同时开启 data-form-controls |
| data-debug | false | 输出启动状态和规则加载错误 |
| data-ignore-selector | 内置选择器 | 完全覆盖默认忽略选择器 |
| data-exclude-selector | 空 | 排除整个子树及其 Shadow DOM、表单控件,例如 wujie-app;保留 3.1.0 行为 |
页面局部不希望被替换时:
<section data-model-replace-ignore>
这里保留原始模型名称
</section>Text Node 扫描默认忽略 script、style、表单控件、可编辑元素和带忽略标记的区域;表单控件由独立的 value 替换逻辑处理。
表单 value 替换
Runtime 会直接改写 input、textarea、button 和 select option 的真实 value,select 的 option 显示文本和显式 label 也会一起替换。因此原生 FormData 和基于当前 DOM value 的提交请求会使用替换后的名称。
替换前 value:claude-fable-5
页面展示: Venus-Cathedral-5
提交 value: Venus-Cathedral-5如果目标是“编辑时显示别名、提交时使用原始名称”,开启保留模式:
<script
src="CDN_URL"
data-model-replace
data-form-controls="true"
data-preserve-form-values="true"
></script>保留模式会用别名显示文本输入框、文本域、按钮和 option label/text,但隐藏字段、复选框、单选框和 option value 保持原始值。Runtime 使用 WeakMap 记录发生替换的控件原值与展示值,并在浏览器生成原生 FormData 时恢复字符串字段。React、Vue 等受控表单应先用 ModelReplace.replaceValue() 把接口数据转换成展示状态,再把该状态交给表单;请求序列化前使用 ModelReplace.restoreValue() 恢复原始名称。两个结构化 API 使用 WeakMap 保留循环引用和共享引用,并跳过 Date、Dayjs 等非普通对象。
const displayValues = ModelReplace.replaceValue(apiPayload);
form.setFieldsValue(displayValues);
const requestPayload = ModelReplace.restoreValue(form.getFieldsValue(true));只修改受控输入框的 DOM value 会被 React/Vue 后续渲染重新写回。Runtime 不会在 focus 后重复处理已标记元素,因此受控表单必须使用上述展示状态方案。restoreText()/restoreValue() 只会反向转换目标名称唯一的规则;存在重复目标名称时保持原值,避免恢复到错误模型。
初始页面、动态弹窗和新插入控件都会自动处理,也支持 Runtime 与目标 DOM 分属不同同源 Realm 的 Wujie/ShadowRoot 场景。每个实际发生替换的 DOM 元素会写入 data-replace="true",并同时记录在 WeakMap 中;同一个元素在当前页面生命周期内最多替换一次,即使业务代码移除该属性也不会再次处理。初始值未命中规则的控件仍会监听 input/change,直到第一次成功替换。隐藏字段、复选框、单选框和多选 select 都会处理;文件输入框因浏览器安全限制不会修改。需要完全禁用时:
<script src="CDN_URL" data-model-replace data-form-controls="false"></script>全局 API
IIFE bundle 会暴露 window.ModelReplace:
await ModelReplace.start();
ModelReplace.stop();
ModelReplace.refresh();
await ModelReplace.whenIdle();
ModelReplace.replaceText("Opus");
ModelReplace.replaceValue({ model: "claude-opus" });
ModelReplace.restoreText("Marble");
ModelReplace.restoreValue({ model: "venus-marble" });
ModelReplace.getStatus();getStatus() 的 isOpen 表示是否已通过 proxy 总开关并启用替换,controlReplacementCount
表示当前启动周期中发生过表单控件替换的次数。sources 分别报告 CMS / SKU CMS / API 的
requestedUrl、resolvedUrl、state(idle/loading/ready/error/disabled)、
cache(none/fresh/stale)、ruleCount、rulesVersion、cachedAt、error 和 fallback(失败时为 cache/none)。
手动规则模式或总开关关闭时,sources 为空。rulesPending 表示尚未提交初始规则,
scanning 表示存在未完成的 DOM 分片。
也可以手动传入规则,此时默认不加载远程规则,总开关仍通过 proxy 读取。
若显式设置 remoteRules: true 则只使用 proxy 数据或其缓存,忽略手动规则,不把它们作为失败兜底:
await ModelReplace.start({
ruleSet: {
version: "custom-1",
rules: { "source-model": "public-model" },
},
});事件:
window.addEventListener("modelreplace:ready", (event) => {
console.log(event.detail);
});
window.addEventListener("modelreplace:updated", (event) => {
console.log("动态规则已进入内存", event.detail);
});
window.addEventListener("modelreplace:error", (event) => {
console.error(event.detail);
});ModelReplace.refresh() 将扫描加入队列,只处理尚未成功替换过的元素。它仍返回数字,
现在表示调用期间已同步完成的替换数,大页面的其余工作会分片继续;需要完成结果时,
先调用 refresh(),再 await ModelReplace.whenIdle() 并读取 getStatus()。
whenIdle() 只等待当前 DOM 队列,不等待网络;后台规则提交会发送 modelreplace:updated,
该事件表示规则已提交、补扫已调度,可以在事件中调用 whenIdle() 等补扫完成。
不同项目如何接入
以下示例中的 CDN_URL 应替换为固定版本地址:
https://cdn.jsdelivr.net/npm/@ethanyu99/[email protected]/dist/model-replace.min.js原生 HTML / 传统服务端模板
直接放在 <head> 中:
<script defer src="CDN_URL" data-model-replace></script>适用于静态 HTML、PHP、Java/JSP、Go Template、Django Template 等服务端页面。
React + Vite
在项目根目录的 index.html 中,将脚本放在 React 入口之前:
<head>
<script defer src="CDN_URL" data-model-replace></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>不需要修改 React 组件。脚本的 Observer 会继续处理路由切换和 setState 产生的新 DOM。
Vue + Vite
同样在 index.html 中、Vue 入口之前引入:
<script defer src="CDN_URL" data-model-replace></script>
<script type="module" src="/src/main.ts"></script>Vue Router 和响应式更新生成的新节点会被自动处理。
Angular
在 src/index.html 的 <head> 中添加:
<script defer src="CDN_URL" data-model-replace></script>不建议写入 angular.json 的 scripts 数组,因为那样不方便给标签配置 SRI、crossorigin 和 data-* 参数。
Next.js
SSR 页面必须等 hydration 开始后再修改 DOM,避免服务端 HTML 与客户端首帧不一致。在 App Router 的 app/layout.tsx 中:
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html lang="zh-CN">
<body>{children}</body>
<Script
src="CDN_URL"
strategy="afterInteractive"
data-model-replace=""
/>
</html>
);
}不要使用 beforeInteractive 自动启动,否则脚本可能在 React hydration 前改动服务端文本。
Nuxt
先在 nuxt.config.ts 中加载脚本但关闭自动启动:
export default defineNuxtConfig({
app: {
head: {
script: [
{
src: "CDN_URL",
defer: true,
"data-model-replace": "",
"data-auto-start": "false",
},
],
},
},
});再创建 plugins/model-replace.client.ts,等应用挂载后启动:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook("app:mounted", async () => {
await window.ModelReplace?.start();
});
});微前端
建议只在主应用 Shell 中加载一次,不要让每个子应用分别创建 Observer:
<!-- host/index.html -->
<script defer src="CDN_URL" data-model-replace></script>子应用卸载时无需停止主应用 Runtime。需要隔离某个子应用时,在其根节点添加:
<div id="sub-app" data-model-replace-ignore></div>WordPress / CMS
可以通过主题模板、全局 Header 配置或脚本管理插件加入:
<script defer src="CDN_URL" data-model-replace></script>普通文本和表单控件默认都会显示别名,提交值也会被替换;需要保留原始提交值时设置
data-preserve-form-values="true"。富文本编辑区仍保留原始内容。
SSR 通用原则
- CSR/SPA 可以在应用入口前加载并自动启动。
- React SSR、Vue SSR 等页面应在 hydration 后启动,避免 hydration mismatch。
- hydration 后启动可能短暂显示原始文本;如果业务不能接受闪烁,应改用 BFF 或服务端模板阶段完成映射。
部署到其他 CDN 平台
除了 npm + jsDelivr/unpkg,也可以直接部署 release/cdn-<version>/ 下的静态文件。
Nginx
将静态文件复制到版本目录,例如:
/var/www/cdn/model-replace/4.0.0/model-replace.min.js推荐响应头:
location /model-replace/ {
add_header Access-Control-Allow-Origin "*" always;
add_header Cache-Control "public, max-age=31536000, immutable" always;
try_files $uri =404;
}AWS S3 / Cloudflare R2 / 对象存储
先生成发布目录:
npm run release:build然后上传整个版本目录:
aws s3 sync release/cdn-4.0.0 s3://YOUR_BUCKET/model-replace/4.0.0 \
--cache-control "public,max-age=31536000,immutable"对象存储需要正确设置 .js 的 Content-Type: text/javascript、JSON 的
application/json,并允许 GET/HEAD 跨域访问。Proxy 的总开关和聚合接口都必须
配置 CORS。
内部制品/CDN 平台
直接上传 release/cdn-<version>/ 即可。业务项目引用带版本的路径,不应覆盖已经发布的版本目录;升级时发布新目录并更新 <script src>。
CSP
使用第三方 CDN 时,需要把域名加入 CSP:
Content-Security-Policy: script-src 'self' https://cdn.jsdelivr.net还需要把默认或自定义规则接口域名加入 CSP 的 connect-src。
本地构建与验证
npm install
npm test
npm run dev打开 http://localhost:4173。演示页包含初始文本、动态插入文本、直接替换提交值的表单和动态弹窗。
构建产物:
dist/
├── model-replace.js
├── model-replace.js.map
├── model-replace.min.js
├── model-replace.min.js.map
├── model-replace-4.0.0.min.js
├── model-aliases.json
├── manifest.json
└── snippet.htmlmanifest.json 包含文件大小、SHA-384、规则版本和 jsDelivr/unpkg 地址。
生成正式发布包:
npm run release:build输出:
release/
├── cdn-4.0.0/ # 可直接上传 Nginx/S3/R2
└── ethanyu99-model-replace-js-4.0.0.tgz # 可执行 npm publish 的包发布到 CDN
本项目通过 npm 发布,jsDelivr 和 unpkg 会自动分发 npm 包内容。
首次发布前确认 package.json 的包名属于你的 npm 用户或组织,然后登录:
npm login
npm run publish:check
npm run publish:cdn -- --dry-run
npm run publish:cdn正式发布新版本(例如下一个功能版本):
npm version minor
npm run release:build
npm run publish:check
git push origin main --follow-tags
npm run publish:cdn当前主远端是 GitLab,因此发布 npm/CDN 默认使用上面的手动命令。仓库中的
.github/workflows/release.yml 仅在同步到 GitHub 且配置 NPM_TOKEN Secret 后,
才会在推送 v* Tag 时自动发布。
发布后固定使用明确版本,避免线上引用 latest:
https://cdn.jsdelivr.net/npm/@ethanyu99/[email protected]/dist/model-replace.min.js
https://unpkg.com/@ethanyu99/[email protected]/dist/model-replace.min.js能力边界
- 普通内容只修改 DOM Text Node,不修改接口响应、React Props 或框架状态。
- 默认表单模式会修改当前 DOM value、默认值、placeholder 以及 option 的 value/显示文本。开启
preserveFormValues后,表单可见内容显示别名,原生FormData恢复原始名称;React/Vue 受控表单应以replaceValue()生成展示状态,并在请求层调用restoreValue()。成功替换后,该元素后续由业务框架写入的新值不会再次替换。 - 文件输入框、富文本、Canvas 和 CSS
content不处理。 - 支持页面本身及开放的 Shadow DOM;关闭的 Shadow DOM 和跨域 iframe 无法处理。
stop()会停止后续监听,但不会还原已经替换的 Text Node 或表单 value;需要还原时刷新页面。- 生产环境应固定版本、配置 SRI,并在 CSP 的
script-src中允许所用 CDN 域名。
