商品描述字段从 Day 11 上线至今一直只存纯文本,商家想强调卖点只能堆表情符号,买家看到的是一大段没有层次的平铺文字。Day 27 的目标就是把这块补齐:让商家能用受限的富文本排版,有加粗、小标题和列表,也能在正文插图,同时不给跨站脚本留缝隙。
技术选型上没有引入成熟的富文本编辑器。完整编辑器意味着几十 KB 体积和一套自带样式,还要处理服务端渲染的水合问题;清洗环节若用浏览器端的库,又得配虚拟 DOM 才能在服务端跑。最后两头都自己写:编辑器退化成一排按钮直接往文本域里插标签,清洗器是零依赖纯函数,单遍扫描字符串,约两百三十行。

工具栏一共五个按钮:加粗、斜体、小标题、无序列表和插图。前四个拿到文本域选区,把选中文字用对应标签包起来,没有选中就插入一对空标签并把光标移到中间。插图按钮弹出模态框让商家粘贴图片地址,地址不是加密链接时插入按钮直接禁用,提交时再校验一次做双保险。文本域下方的实时预览区调用的是和详情页完全相同的清洗函数,所见即所得。
清洗器的核心是一份很短的标签白名单,只放段落、换行、加粗、斜体、两级小标题、有序无序列表、列表项、图片和链接。名单之外的一切都当普通文本处理:普通标签只剥掉标签本身、保留里面的文字;危险标签连内容一起丢弃,扫描时先找到配对的闭合标签再整段跳过,避免脚本内容被当文本吐出来。属性同样走白名单,所有以 on 开头的事件属性和行内样式一律剥掉。

地址协议是第二道关。图片和链接只接受加密的超文本协议,或以单个斜杠开头的站内相对路径。这条规则专门堵两类写法:一是省略协议、用双斜杠开头指向外站,这种地址会顺着当前协议加载,很容易被当成安全链接放过去;二是把脚本或内联数据塞进地址里。校验不通过时图片整个不输出,链接则只剥开标签、保留可见文字。
链接还有一处处理:商家手写的链接通常不带新窗口和关联属性,清洗器强制补上,防止新页面反过来篡改原页面。剥掉开标签时要记一笔,把对应的闭合标签一并剥掉,否则页面里会留下孤立的闭合标签吃掉后文。
兼容老数据的办法很直接:整段描述里连一个尖括号都没有,就判定为纯文本原样返回,渲染时靠样式保留换行。Day 11 到 Day 26 录入的商品完全不受影响。

测试跑了四轮端到端脚本,最终四十七项断言全部通过,零失败零跳过,分成工具栏交互、渲染效果、脚本剥离和回归四段。功能代码没出缺陷,四轮返工全是脚本自己的问题:密码写错导致登录失败、误用不存在的接口取详情、断言选择器命中外层容器、取老数据用了不支持的查询参数。
遗留的口子也记进了日志:未配对的尖括号走转义兜底,图片替代文本还没强制要求,表格和视频内嵌暂不在白名单内。下一站 Day 28 是商家数据看板。
OpenClaw—AI研究