将机场ss节点批量转换成ss字符串链接批量添加到passwall
最近我使用的机场节点都更新了，我在一个新的 immortalwrt 环境上使用原有的订阅链接，始终无法订阅到节点，我还一直以为是我找的订阅转换服务有问题。试了好几个服务，都无法订阅到节点。 测试了这些订阅转换服务（subscription converter） https://sub.cmliussss.com/ https://acl4ssr-sub.github.io/ https://subconverters.com/ 订阅全部都失败了。passwall 给出的错误要么是“订阅失败，可能是订阅地址无效，或是网络问题，请诊断”，要么是 “成功解析【xxx_airport】节点数量: 0”。 我开始了研究。 旧 immortalwrt 环境上的 passwall 版本 # opkg list-installed | grep passwall luci-app-passwall - 25.9.23-r1 luci-i18n-passwall-zh-cn - 25.270.37028~65fa739 新 immortalwrt 环境上的 passwall 版本 # opkg list-installed | grep passwall luci-app-passwall - 25.12.16-r1 luci-i18n-passwall-zh-cn - 25.350.06350~ba7f272 版本确实不一样，源里的版本升级了。但因为 op 源里一般只保存最新的软件包，我放弃了降级，也放弃了卸载和重新安装同样的版本（要去找同版本 ipk 文件），后面的经历证明此举明智。 我开始对订阅链接产生兴趣，看看能发现什么规律。 果然！发现规律了，我使用的机场提供的订阅链接，我一般只使用 ?clash=1 的那个，在 passwall 中订阅时需要先转换，把节点目标设成 shadowsocks(SIP002) 注：SIP002 是 Shadowsocks 官方定义的标准 URI 格式规范（即标准的“ss://”链接格式） 我把 clash=1 的订阅拉到本地发现就是 clash 的 yaml 文本，最终发现所有节点都是 type: ssr，原来是这样！！！就是因为 clash 的订阅链接只返回了 ssr 节点，但我本地的 passwall 使用 ss/trojan/vmess/vless/hysteria2 节点，因为没有安装 ssr。 我又从机场的web后台复制了一个 ss 节点的 ss:// 链接手动导入，可以成功导入！遗憾的是，机场提供了批量复制 ssr 链接功能，但是没有提供批量复制 ss 链接功能。于是我开始想办法手动转换。 我本地访问了 quanx 的订阅链接 ?list=quantumultx，得到一个94行的文本，乍一看就能看出规律，47个节点，每个节点各有一个 ss 链接和一个 ssr 链接。长得像这样 shadowsocks=xxx.com:3044, method=chacha20-ietf, password=xxx, ssr-protocol=auth_aes128_sha1, ssr-protocol-param=xxx, obfs=plain, obfs-host="xxx, tag=Standard|台湾|IEPL|01 shadowsocks=xxx.com:30333, method=rc4-md5, password=xxx, obfs=http, obfs-host=xxx, obfs-uri=/, tag=Standard|台湾|IEPL|01 于是我将他们转到 linux 下， cat > sub.txt<<'EOF' 94 行文本 EOF 执行命令 grep -v 'ssr-protocol=' sub.txt > sub_new.txt，将 ssr 节点对应的行全都删掉，重定向到新文件，新文件里全部都是 ss 节点信息。 最后我就是找规律，看机场给的 ss 链接是根据什么规律生成的，然后将剩下的 47 个节点数据全部转换成 ss:// 文本链接。附上我使用的 java 代码（java17） import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; public class SSUrlParser { public static String parseAndGenerate(String line) throws Exception { Map<String, String> map = new HashMap<>(); for (String part : line.split(",")) { String[] kv = part.trim().split("=", 2); map.put(kv[0], kv[1]); } String[] hostPort = map.get("shadowsocks").split(":"); String host = hostPort[0]; int port = Integer.parseInt(hostPort[1]); String userInfo = map.get("method") + ":" + map.get("password"); String userInfoBase64 = Base64.getEncoder().encodeToString(userInfo.getBytes(StandardCharsets.UTF_8)); // 插件参数 String pluginRaw = "obfs-local;obfs=" + map.get("obfs") + ";obfs-host=" + map.get("obfs-host"); String pluginEncoded = URLEncoder.encode(pluginRaw, StandardCharsets.UTF_8.toString()); // group 参数：Base64 编码 String group = map.getOrDefault("tag", ""); // 没有单独 group 时用 tag // String groupBase64 = Base64.getEncoder().encodeToString(group.getBytes(StandardCharsets.UTF_8)); String groupName = "机场名称特有字符串"; // 这里的规律，可能不同机场不一样 String groupBase64 = Base64.getEncoder() .withoutPadding() .encodeToString(groupName.getBytes(StandardCharsets.UTF_8)); // tag 参数 URL encode String tagEncoded = URLEncoder.encode(group, StandardCharsets.UTF_8.toString()); return "ss://" + userInfoBase64 + "@" + host + ":" + port + "/?plugin=" + pluginEncoded + "&group=" + groupBase64 + "#" + tagEncoded; } public static void main(String[] args) throws Exception { String input = """ shadowsocks=xxx.com:30333, method=rc4-md5, password=xxx, obfs=http, obfs-host=xxx, obfs-uri=/, tag=Standard|台湾|IEPL|01 shadowsocks=xxx.com:30334, method=rc4-md5, password=xxx, obfs=http, obfs-host=xxx, obfs-uri=/, tag=Standard|台湾|IEPL|02 shadowsocks=xxx.com:30335, method=rc4-md5, password=xxx, obfs=http, obfs-host=xxx, obfs-uri=/, tag=Standard|台湾|IEPL|03 """; // 使用 Text Blocks 特性 // 遍历每一行 input.lines() .map(String::trim) // 去掉首尾空白 .filter(line -> !line.isEmpty()) // 判空 .forEach(line -> { try { System.out.println(parseAndGenerate(line)); } catch (Exception e) { throw new RuntimeException("当前行计算出了问题..."); } }); // 调用自定义方法 } } 上述代码执行后即可将机场ss节点信息全都转换成 ss链接，但是注意，这可能不适用于你的机场。最终我将47行ss链接成功一次性导入 passwall：节点列表标签页下 -> 通过链接添加节点功能 -> 一行一个，全部复制到这里，可以添加一个单独的分组。 没有使用订阅的方式（机场没有提供符合我需求的订阅链接），成功批量导入节点！完美。 不过后来我发现，将 ?list=quantumultx 这个订阅链接作为源，然后转换为 ShadowsocksSIP002，最终生成临时订阅链接放进 passwall，成功订阅 😂️，解析到了93个，应该是把所有的节点 ss/ssr 都识别出来了，passwall 这边每个节点显示了两次，至于为什么还少一个（应该94个），就不再探究了。 最终学到了： ss:// URL 中，只有 method / password / host / port / plugin 是“协议级可推导”的 group 和 tag 都是“订阅生成器的业务字段” 还有 String groupBase64 = Base64.getEncoder() .withoutPadding() .encodeToString(groupName.getBytes(StandardCharsets.UTF_8)); 和 String groupBase64 = Base64.getEncoder() .encodeToString(groupName.getBytes(StandardCharsets.UTF_8)); 的区别。
使用JavaScript脚本快速获取B站观看历史记录页面的视频标题和URL
哔哩哔哩观看历史记录的页面地址是 https://www.bilibili.com/account/history 有时候我想分享最近观看的几个视频给朋友，尤其是在我使用关键字搜索之后的列表，会有好几个条目，一个一个打开并复制出一个干净的标题和干净的视频地址太麻烦了。于是我用js写了一段脚本快速获取页面上的视频标题和URL 源码如下 // 使用 querySelectorAll 选择符合路径的 div const targetDiv = document.querySelector('#history_list'); // 检查 targetDiv 是否存在 if (targetDiv) { // 在 targetDiv 内选择所有的 li 标签 const liTags = targetDiv.querySelectorAll('li'); let title_url = ''; // 遍历每一个 li 标签，获取其内容 liTags.forEach(liTag => { const titleA = liTag.querySelector('a.title'); // 获取 class 名称是 title 的那个 a 标签 if (titleA) { let url = titleA.getAttribute('href'); // 获取链接地址 let title = titleA.textContent; if (url) { let originStr = url.trim(); // 原始字符串 url = `https:${originStr.replace(/\?.*$/, '')}`; // 利用正则格式化，前面拼上 https: 顺便把半角问号和后面的字符串都remove掉 title_url += '- ' + title + ' ' + url + '\n'; // 拼接英文减号和空格，再拼上视频标题和空格，最后拼上视频地址 } else { title_url += '- No title attribute\n'; // 如果没有 title 属性，添加提示 } } }); console.log(title_url) } else { console.log('无法找到目标 div'); } 使用方法 打开历史记录页面，搜索某关键词（搜索是可选项，不搜索也可以，但是js脚本只会收集出页面已加载的内容） 按下键盘F12，切换到 Console 标签 粘贴如上代码后回车即可。如果此网站不允许粘贴，临时在Console中键入allow pasting 回车即可。不过B站的web是允许粘贴的 举例，我使用富士搜索出如下视频 执行js代码之后快速获取了这些视频的标题和URL 刚好我顺手记录一下这些视频的地址。下面各二级标题就是我使用标题搜出来的结果（基于我最近观看的视频）。不太清楚，B站历史记录的最大条目好像是 1000？（有知道的朋友欢迎留言评论） 富士 索尼相机套用富士胶片模拟滤镜教程（完美解决索尼直出色彩问题）非预设 https://www.bilibili.com/video/BV1Db4y1y7qs 索尼原图直出，拍出富士胶片感 https://www.bilibili.com/video/BV1Pw411r7Cc 仅￥905！56mm F1.8！锐度爆表，画质惊喜！富士用户原地痛哭，铭匠光学 X卡口自动对焦镜头 https://www.bilibili.com/video/BV14H4y1y7BU 日本富士山出事了，火山口发现3名遇难者 https://www.bilibili.com/video/BV1a6421Z7Kr 【御成phot】注定爆款：富士XT50深度测试 https://www.bilibili.com/video/BV1GS411N7rZ 佳能索尼尼康宾得松下富士徕卡哈苏奥之心理光蔡司，数码CMOS技术哪家强？众通社 https://www.bilibili.com/video/BV14C411x7AN 索尼 索尼相机套用富士胶片模拟滤镜教程（完美解决索尼直出色彩问题）非预设 https://www.bilibili.com/video/BV1Db4y1y7qs 羡慕隔壁的颜色？3分钟教你玩转索尼直出，有手就能学会！ https://www.bilibili.com/video/BV18a4y1Z7tB 索尼原图直出，拍出富士胶片感 https://www.bilibili.com/video/BV1Pw411r7Cc 为什么你的索尼拍照不好看？搞懂直出的秘诀！ https://www.bilibili.com/video/BV1CS421d7B3 索尼直出也能打？A7M4创意外观全解析！ https://www.bilibili.com/video/BV165411U73D 索尼相机人像直出设置教程-以ZVE10为例（适用三代机 A6400 M3 R3 R4 A9 ZV1 RX100M5A等） https://www.bilibili.com/video/BV1WM411e7ex 【简单教程】索尼菜单太复杂？快来看看我怎么设置A7C2的吧 https://www.bilibili.com/video/BV1Ht421J7Eo 把记忆留在那片森林，感动之旅拍摄之终章丨索尼A7C2 & SEL2450G丨拍照姿势教学丨日常感人像拍摄引导丨摄影Vlog丨破碎感情绪片 https://www.bilibili.com/video/BV1Hx4y1t7pz 自用，索尼人像直出滤镜参数。一定要会用呀～ https://www.bilibili.com/video/BV1UG411X71j 索尼A7m4逆天预设直出 https://www.bilibili.com/video/BV1Hr4y1b7To 简单学习｜准确且高效！2分钟快速设置一个索尼A7C2的好用对焦模式 https://www.bilibili.com/video/BV1Jz421h7J6 我使用最多的索尼镜头：FE35mm F1.8 https://www.bilibili.com/video/BV1MY4y1c7C5 开箱vlog|索尼A7C二代和一代有什么区别？ https://www.bilibili.com/video/BV1Bu4y1c7Xu 索尼S&Q实用教学｜三个实际应用技巧 https://www.bilibili.com/video/BV1y841177oy 索尼相机用户必看！8个必须了解的设定 https://www.bilibili.com/video/BV1ZE421A7Np 索尼USB连接的四种模式的区别：USB流式传输、MSC、MTP、遥控拍摄 https://www.bilibili.com/video/BV1Hc411R7UH 索尼A7C2,腾龙28-200,最近天气不错，练手拍照合集，纠结要不要换24-105······ https://www.bilibili.com/video/BV1RAhCeUEyw 【第一视角拍妹】索尼a7m4腾龙28200闪光灯人像拍摄，希望对你有帮助！ https://www.bilibili.com/video/BV1Db421H79u 索尼或发布FE 24-200mm F2.8-4.5 G OSS https://www.bilibili.com/read/cv28071789 适马2870mm F2.8，越用越香！| 索尼相机变焦镜头推荐 https://www.bilibili.com/video/BV11T411q7Z7 索尼a7c2 适马2870 草原天路 乌兰布统 达达线热阿线 之旅 https://www.bilibili.com/video/BV12J3keJE6o 影像狗 | 索尼A7C2镜头怎么选？看完这个视频就够了！ https://www.bilibili.com/video/BV1tb421H7V8 索尼FE 35mm F1.8有多香？ https://www.bilibili.com/video/BV1R441117qp 【最强性价比85？】索尼FE口三85测评 https://www.bilibili.com/video/BV1Fo4y1H7L1 背刺原厂？永诺85mm F1.8 vs 索尼85mm F1.8 https://www.bilibili.com/video/BV1aW4y127nX 【御成索尼】一次看够！8种索尼创意外观直出对比 https://www.bilibili.com/video/BV1Py411a7C4 超简单的索尼人像直出设置 https://www.bilibili.com/video/BV1Lo4y1b7aC 索尼相机人像直出设置教程-以ZVE10为例（适用三代机 A6400 M3 R3 https://www.bilibili.com/read/cv22042777 【御成测评】35 F1.8哪家强？索尼/佳能/尼康35mm定焦横评测试！ https://www.bilibili.com/video/BV1dv411M7SR 「索尼20-70」那些零零散散的夏天碎片成了夏天最后的镜头 https://www.bilibili.com/video/BV15a4y1D7tn 索尼20 70『夏天的味道』你还记得吗？ https://www.bilibili.com/video/BV1um42157tW 【国内首测】索尼官方样张摄影师为你揭示FE35MM F1.8的秘密。（已重新编辑声音） https://www.bilibili.com/video/BV1hx411f7NQ 索尼35mm 1.8上手测试 https://www.bilibili.com/video/BV1us421T7G6 索尼20-70 F4 G镜头拍视频画质很好就是光圈有点小！ https://www.bilibili.com/video/BV17b4y1A7NX 索尼2070镜头3个月挂机体验：优缺点一览，二手掉价的原因揭秘！ https://www.bilibili.com/video/BV1bS411P7RY 【御成phot】完美的天选打工人：索尼20-70G购买分享 https://www.bilibili.com/video/BV1m4421w7PN 索尼镜头多到挑花眼？｜新人向｜索尼镜头选购指南 https://www.bilibili.com/video/BV13i421e7FV 【2024年618摄影选购】索尼篇（镜头+机身超详细解析，保姆级）建议收藏和三连 https://www.bilibili.com/video/BV1Cm421K7au 【收藏向】索尼E卡口选购指南必看网站/索尼E卡口镜头排行评测必看 Sony Alpha Blog网站使用指南 https://www.bilibili.com/video/BV19Z421y7X8 入坑索尼全画幅微单，5只镜头推荐 https://www.bilibili.com/video/BV15b411c77M 【御成索尼】不是GM大师买不起，而是平替更有性价比！5枚索尼E卡口平替镜头推荐 https://www.bilibili.com/video/BV1rc411U79m 索尼相机可选这6支性价比高的镜头 https://www.bilibili.com/video/BV1kR4y1o7qL 前方预警！索尼E口镜头最不值得买TOP5 https://www.bilibili.com/video/BV1xY411J7mw 真便携？光圈够？适合照片还是视频？索尼20-70半年使用体验报告 https://www.bilibili.com/video/BV1FW4y1P7b5 【器材指南】索尼A7C2新手快速设置指南 | 自定义按键、波轮设置方案分享 https://www.bilibili.com/video/BV1tz421878x 2023最强轻量旗舰？索尼A6700微单上手 https://www.bilibili.com/video/BV1ck4y1N7iw 索尼A7C2 小小的 也很可爱捏 https://www.bilibili.com/video/BV11M4m1m72r 为什么我要把索尼20-70G换到四年前的腾龙28-200？ https://www.bilibili.com/video/BV1Ei42117Eo 为啥手机cmos都是索尼造，佳能尼康干啥去了？ https://www.bilibili.com/video/BV1vF4m1A7oP 佳能索尼尼康宾得松下富士徕卡哈苏奥之心理光蔡司，数码CMOS技术哪家强？众通社 https://www.bilibili.com/video/BV14C411x7AN 购物指南｜索尼用户爱不释手的摄影配件居然是······！ https://www.bilibili.com/video/BV1ar421w7es 索尼24-50 F2.8 G，一颗让你上手之后直呼真香的镜头！ https://www.bilibili.com/video/BV1jS421o7C4 第四期，索尼相机，HDR照片拍与摄制作 https://www.bilibili.com/video/BV1bt4y1H7XX 第五期，索尼相机变焦模式的正确设置 https://www.bilibili.com/video/BV17Y411T775 第六期，索尼相机视频录制曝光模式的正确设置 https://www.bilibili.com/video/BV1mB4y1q7FX 第七期，索尼相机拍摄视频时帧速率、对焦区域还有对焦模式的简单设置 https://www.bilibili.com/video/BV16t4y1t7jr 第八期，索尼相机的斑马线设定 https://www.bilibili.com/video/BV18e4y1B7nP 索尼相机S-log2，S-log3的对比 https://www.bilibili.com/video/BV1gY41187WF 第三期，索尼相机的测光模式设置（下） https://www.bilibili.com/video/BV1X54y1o7na 第三期，索尼相机的测光模式设置（上）。测光模式看这一篇就足够了 https://www.bilibili.com/video/BV165411X72f 第二期，索尼相机的白平衡设置（下） https://www.bilibili.com/video/BV1Ut4y1W7cZ 第二期，索尼相机的白平衡设置（上） https://www.bilibili.com/video/BV1hv4y1N7Sm 第一期，索尼相机的对焦模式与对焦区域。 https://www.bilibili.com/video/BV14Z4y1a7y9 只知道用直方图看曝光 有用斑马线来看曝光吗 索尼相机斑马线用法 https://www.bilibili.com/video/BV1yr4y1g7ZV 2023年索尼全画幅E卡口定焦镜头小盘点之35mm焦段【52】原厂/腾龙/适马，手动/自动，超轻/健身？ https://www.bilibili.com/video/BV1oR4y1B73b 『索尼35mm F1.8 vs 三阳35mm F1.8』适马35mm F1.2表示我就看看不说话 https://www.bilibili.com/video/BV1V54115716 索尼A7M4超好用的斑马线功能，照片过曝克星！ https://www.bilibili.com/video/BV1vY411d7Kd 【丽江旅拍】如果只用一支35mm的镜头拍摄 | 索尼35GM https://www.bilibili.com/video/BV1Jq4y1B7Cx 索尼 FE 35mm F1.8 户外人像 https://www.bilibili.com/video/BV1U54y1s7fk 认识相机卡口和法兰距，佳能尼康索尼各品牌卡口讲解，相机知识 https://www.bilibili.com/video/BV1Gr4y1q7Sc 寻找地球上最舒服的耳机：AirPods丨开放式耳机丨韶音游泳耳机丨华为耳夹丨索尼LinkBuds丨三星BudsLive https://www.bilibili.com/video/BV1Kr421c75u 这个600块的索尼相机震撼了我的思想！超便宜的α微单相机｜sony a3000｜拍照可放大看细节出片可观｜视频能力比预期高！ https://www.bilibili.com/video/BV1vZ421s7Ai 【索尼相机肩带安装教程】实用美观的相机肩带安装方法 | 索尼a6400 https://www.bilibili.com/video/BV1sT4y1j7oZ 直出 羡慕隔壁的颜色？3分钟教你玩转索尼直出，有手就能学会！ https://www.bilibili.com/video/BV18a4y1Z7tB 索尼相机套用富士胶片模拟滤镜教程（完美解决索尼直出色彩问题）非预设 https://www.bilibili.com/video/BV1Db4y1y7qs 索尼原图直出，拍出富士胶片感 https://www.bilibili.com/video/BV1Pw411r7Cc 为什么你的索尼拍照不好看？搞懂直出的秘诀！ https://www.bilibili.com/video/BV1CS421d7B3 索尼直出也能打？A7M4创意外观全解析！ https://www.bilibili.com/video/BV165411U73D 索尼相机人像直出设置教程-以ZVE10为例（适用三代机 A6400 M3 R3 R4 A9 ZV1 RX100M5A等） https://www.bilibili.com/video/BV1WM411e7ex 自用，索尼人像直出滤镜参数。一定要会用呀～ https://www.bilibili.com/video/BV1UG411X71j 索尼A7m4逆天预设直出 https://www.bilibili.com/video/BV1Hr4y1b7To 【御成索尼】一次看够！8种索尼创意外观直出对比 https://www.bilibili.com/video/BV1Py411a7C4 超简单的索尼人像直出设置 https://www.bilibili.com/video/BV1Lo4y1b7aC 索尼相机人像直出设置教程-以ZVE10为例（适用三代机 A6400 M3 R3 https://www.bilibili.com/read/cv22042777 创意外观 索尼直出也能打？A7M4创意外观全解析！ https://www.bilibili.com/video/BV165411U73D 【御成索尼】一次看够！8种索尼创意外观直出对比 https://www.bilibili.com/video/BV1Py411a7C4 【御成phot】阿索直呼内行：3个创意外观预设分享！ https://www.bilibili.com/video/BV111421R7ud ok, that's all.
使用自定义JavaScript脚本功能禁止memos最新版默认打开的链接预览功能
我的 memos 当前版本是 0.22.0，忘了是这个版本开始的还是上一两个版本开始的，当你在 memos 中间插入了普通的链接文本，保存后鼠标经过时会自动预览链接内容，这个功能对我来说有点蛋疼，影响阅读。蛋疼的是有时候 pop up 出来的页面元素还不会自动消失，就更加影响阅读了。 我在其 issues 列表新建了一个 issue，能不能增加一个选项，允许关闭鼠标经过链接时的链接自动预览？ 有人回复了我，详情可去浏览原 issue。 热心网友提供了自定义 JavaScript 脚本禁止memos自动获取链接标题等内容并展示到页面上，脚本如下 window.fetch = new Proxy(window.fetch, { apply(target, thisArg, args) { const url = args[0]; // 检查请求的URL是否是我们想要阻止的API if (url.includes('/memos.api.v1.MarkdownService/GetLinkMetadata')) { // 返回一个空的Promise，模拟请求被取消 return Promise.resolve({ ok: true, json: () => Promise.resolve({}), }); } // 如果不是我们要阻止的请求，就正常执行Fetch return target.apply(thisArg, args); } }); 可以看到，memos 的 api /memos.api.v1.MarkdownService/GetLinkMetadata'，阻止他发送 http 请求即可，返回了空的 Promise，模拟请求被取消，这样鼠标在页面经过链接时也不会请求链接标题，也就不会影响阅读，达成了我的目的。感谢热心网友。
[可能是]最好的压缩图片程序，使用 squoosh-cli 批量压缩图片
前言 去年以前我写博客都没有压缩图片的意识，有时候文章加载的图片过多，有些图片过大就会导致浏览体验很不好。后来用惯了 squoosh 项目之后每次写文章都会压缩一下配图，主要是 squoosh 太优秀了，还是 freeware。 去年我有制作一个 squoosh docker 镜像，可以在本地轻松使用 docker 开启一个压缩图片的服务，但是那时每次都是一张一张图片的压缩，因为我每张图片都调整了一下 quality，为的就是一个非常 suitable 的 size for local storage，我自建的图床运行在本地，存储在本地 ssd 的，所以以前有这个想法也很正常。但是这次大量图片压缩的需求让我明白了批量的意义——尽管可能达不到图片压缩后的质量与存储大小的完美平衡点，但在写博客（图片质量）和本地存储都做一点妥协之后，带来极大的愉悦轻松感，这是非常值得的。 去年那篇 squoosh 文章在此 做了一个 squoosh docker 镜像，写博客贴图直接在本地压缩文件，安全且高效 友情提醒：squoosh 官方是有提供 web 服务的，地址在 https://squoosh.app 但是居然没有 Copy npx command 功能，不知道以前有没有。 正文 下面我讲讲如何使用 squoosh-cli 进行图片的批量压缩，之所以推荐在自建的 docker 容器中运行 squoosh-cli 命令，是因为容器里是 Linux 环境，命令行友好，不像 cmd 和 powershell 可能需要各种转义，引号，还有通配符问题。 首先你可以删掉之前创建的 squoosh 容器，之前没有加 volume，这次我们需要加一个 volume 映射。 在运行 squoosh 容器的 linux 上先创建一个文件夹保存 squoosh 文件，路径最好有通过 samba 等协议挂载到资源管理器上。 # mkdir -p /home/dk/squooshfiles # 设置一下文件夹的所有者和权限 # chown -R dk:dk /home/dk/squooshfiles # chmod -R 0755 /home/dk/squooshfiles 下面启动 squoosh 容器，命令如下 docker run -d --name squoosh \ --restart unless-stopped \ -p 7701:8080 \ -v /home/dk/squooshfiles:/app/squooshfiles \ dko0/squoosh:1.12.0 解析： 这将在容器中自动创建目录 /app/squooshfiles 本地对 /home/dk/squooshfiles 的修改和容器中对 /app/squooshfiles 的修改将完全同步，所以请创建一个完全独立的 squooshfiles 目录，只用于 squoosh 批量压缩图片 docker hub 上的镜像我没有去更新，使用以前的就行，只需要多执行一个命令安装 squoosh cli 包，下文有提到 然后进入容器安装 squoosh-cli npm 包，执行 docker exec -it squoosh ash 后进入 # npm i -g @squoosh/cli 我之前有尝试在本地 win 环境下安装这个，但是本地 node 版本已经 18 了，无法安装这个包，是因为 node 版本太新，所以我就干脆在这个容器上进行了。一个容器多用——压缩单张图片时用户可以网页上操作，多张图片压缩场景时，打开终端进入容器操作即可。 安装完成之后执行 squoosh-cli --help 应能获取命令行帮助。 squoosh-cli 最有用的就是批量，另外强大的一点是也能直接复制使用网页上的配置，但可惜的是 https://squoosh.app 官方提供的这个服务没有 Copy npx command 功能了，所以感觉我这个容器镜像创建的刚刚好，自建后刚好满足这个需求。 一个 npx command 类似于下面这样，记录了当前压缩算法和详细压缩配置 npx @squoosh/cli --mozjpeg \ '{"quality":40,"baseline":false,"arithmetic":false,"progressive":true,"optimize_coding":true,"smoothing":0,"color_space":3,"quant_table":3,"trellis_multipass":false,"trellis_opt_zero":false,"trellis_opt_table":false,"trellis_loops":1,"auto_subsample":true,"chroma_subsample":2,"separate_chroma_quality":false,"chroma_quality":75}' 一个重要的插曲，解决报错 TypeError: Cannot read properties of undefined (reading 'writeText') 这是 Chrome 限制了非安全网站的 API 使用，在非安全网站上 block 了 navigator.clipboard 的 api 使用权限，所以无法拷贝 npx 命令出来。下面解决这个问题。 open chrome，type chrome://flags/ in address bar, then find item Insecure origins treated as secure, type http://10.10.10.5:7701 then enable it and relaunch your chrome. Chrome 在此处认为的安全的网站 必须 https 或 localhost 或 127.0.0.1 所以我们有必要加个白名单，这样好操作。 ps: http://10.10.10.5:7701 是我 squoosh 的运行环境地址。如果有多个地址需要设“白名单”，使用英文逗号隔开它们即可。 在 /home/dk/squooshfiles 创建目录 0805，然后把要批量压缩的照片拷贝进去（我一般是 iPhone 拍摄，使用 Documents 将照片复制到 debian 11 上，走 samba），接下来就是批量压缩了。 使用 squoosh-cli 替换掉 npx 拷贝出来的命令中的 npx @squoosh/cli，然后执行 # squoosh-cli --mozjpeg '{"quality":40,"baseline":false,"arithmetic":false,"progressive":true,"optimize_coding":true,"smoothing":0,"color_space":3,"quant_table":3,"trellis_multipass":false,"trellis_opt_zero":false,"trellis_opt_table":false,"trellis_loops":1,"auto_subsample":true,"chroma_subsample":2,"separate_chroma_quality":false,"chroma_quality":75}' \ -d output \ -s -dk-compressed \ *.jpg 解释 mozjpeg 是最常用的压缩算法了，综合来说最推荐。（具体各种算法还没有研究 quality 就是压缩质量了，在网页端可以直接预览压缩后的效果 -d 设置输出目录 -s 设置一个输出的文件名后缀，默认是空字符串 最后的 files，可以使用简单的通配符，比如 *.jpeg 只压缩当前目录下的所有 jpeg 扩展名图片 批量压缩成功。 在 win 本地通过 samba4 挂载后的预览，非常方便。 把图片从 output1 路径中拷贝出来一份，对比原有图片和有损压缩后的效果，非常赞 最后感谢 squoosh 项目，写博客压缩图片利器，完全免费开源，非常推荐各位博主使用。 squoosh cli npm package: https://www.npmjs.com/package/@squoosh/cli squoosh cli: https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli 由于我使用 dk 这个用户访问 samba，对应路径的 user and group 都设置成了 dk，然而上述压缩图片的命令是用 root 用户执行的，在 win 笔记本上挂载 samba 之后不能删除其中的文件也无法创建文件夹（就是没有写权限），后面我就优化了一下，在容器中也创建同样的用户和组，容器中所有的操作都在 dk 用户下执行。 # 查看宿主机这边 dk 这个用户的 uid 和 gid 都是 1000，用户名和组名都是 dk，可以使用 id dk 这个命令查看 # 容器里的 adduser addgroup 命令都是 busybox 封装的非完整版 # 创建 gid 是 1000 的组 dk # addgroup -g 1000 dk # 创建 uid 是 1000 的用户 dk，并设置组 dk，在交互式命令行中设置用户 dk 的密码是 123456 # adduser -G dk -u 1000 dk # 切换到 dk 这个用户操作 # su - dk ps: 如果不熟悉 adduser addgroup 命令，请事先看看帮助文档再执行命令 动图演示
部署一个 memos 作为个人微博站点和备忘录
我在 pve-debian11 的机器上部署了一个 memos，这是一个最近很流行的 flomo 开源版，完全免费和开源，还有很多社区驱动的第三方应用，比如 iOS/Android 客户端、Chrome 插件、Telegram Bot 等，它开源在 https://github.com/usememos/memos 。我也部署一个玩玩。 直接 docker run docker run -d --name memos -p 5230:5230 -v /mnt/seagateFour/dockerdata/memos:/var/opt/memos neosmemo/memos:0.11.2 更新 memos 的方法 停止现有容器 docker stop memos # 或者 docker container stop memos 删除现有容器 docker container rm memos 拉取最新镜像 (2023-04-05) # 我喜欢拉取具体版本的镜像，不喜欢拉取 tag 是 latest 的镜像 docker pull neosmemo/memos:0.12.0 启动容器 docker run -d --name memos --restart unless-stopped -p 5230:5230 -v /mnt/seagateFour/dockerdata/memos:/var/opt/memos neosmemo/memos:0.12.0 上面删除现有容器是最啰嗦的写法了，直接 rm 也可以，但是这样每个步骤都是最具体的。 通过 OpenWrt 暴露了一个端口为其服务，在软路由 nginx 侧做了反向代理。现在服务地址是 https://emby.940304.xyz:15230 而后我又为其加上了另一层反向代理，主要是想干掉端口号。新的服务地址 https://m.xkkkx.com 但是这个地址通过 Cloudflare 代理并转发了，所以会有些慢，个人还是直接使用 15230 的地址使用，也欢迎大家注册。 2023-08-22 23:16:49 补充：因为 memos 更新的比较频繁，故新增了一个 shell 脚本用于更新 memos 生成文件 /home/dk/scripts_dir/update_memos.sh，内容如下 #!/bin/bash # 从命令行获取版本号参数 version=$1 # 检查版本号是否为空 if [ -z "$version" ]; then echo "version variable required." exit 1 fi # 执行命令 docker pull neosmemo/memos:$version docker stop memos docker container rm memos docker run -d --name memos --restart unless-stopped -p 5230:5230 -v /mnt/seagateFour/dockerdata/memos:/var/opt/memos neosmemo/memos:$version 添加可执行权限之后，以后需要更新时就直接执行 shell 脚本并传入最新版本号就行。比如 ./update_memos.sh 0.14.4 回车即可。
写了一个适配兰空图床 Lsky Pro 的 PicGo 图片上传插件
继续上文，基于 imgkb.com 图床开发了一个 PicGo 插件 为了减少影响，新写了一个适配所有 兰空图床 的 PicGo 上传插件—— picgo-plugin-lankong ，这个实用性更高。 Lsky Pro 是一个用于在线上传、管理图片的图床程序，中文名：兰空图床，你可以将它作为自己的云上相册，亦可以当作你的写作贴图库。 兰空图床始于 2017 年 10 月，最早的版本由 ThinkPHP 5 开发，后又经历了数个版本的迭代，在 2021 年末启动了新的重写计划并于 2022 年 3 月份发布全新的 2.0 版本。 以上引用来自 lsky pro v2 docs 版本日志 v1.0.1 支持兰空图床 V1 的版本正式发布。released on 2022-03-22 v1.0.2 增加 Lsky Pro Version 开关，支持兰空图床 V2。开关默认关闭，关闭代表 V1，打开即可使用 V2 版本的兰空图床 API 上传。released on 2022-03-23 v1.0.3 表单增加存储策略 ID 录入，仅供 V2 用户使用。V1 以及 V2 使用默认存储策略时请留空。released on 2022-03-26 v1.0.4 版本选择改成下拉菜单样式; https agent 代理对象的创建放在分支条件中增加程序性能。released on 2022-03-30 v1.0.5 增加同步删除功能，仅支持 V2。released on 2022-03-30 v1.0.6 修复同步删除的 bug。released on 2022-04-01 安装和使用 1. 在线安装(推荐) 打开 PicGo 详细窗口，选择插件设置，搜索 lankong (author 是 hellodk) 安装。 2. 离线安装 克隆该项目，解压缩到路径 /path/to/picgo-plugin-lankong 进入以下目录 Windows: %APPDATA%\picgo\ Linux: $XDG_CONFIG_HOME/picgo/ or ~/.config/picgo/ macOS: ~/Library/Application\ Support/picgo/ 在对应系统的 PicGo 程序配置文件路径下执行 npm install /path/to/picgo-plugin-lankong，然后重启应用即可。 3. 使用方法 Lsky Pro Version，在下拉菜单中选择 Lsky Pro 版本，V1 还是 V2，默认 V1 填写图床的 server url，注意不要以 / 结束，比如 https://example.com 就是没问题的 填写 token Strategy ID，存储策略 ID，如果是 V1 或 V2 使用默认存储策略的用户，请留空；除非你知道具体 ID，否则请留空 Sync Delete 同步删除选项，只支持 V2，默认关闭，开启后在 PicGo 相册中删除图片可同步删除图床上的文件 Ignore certificate error 开关请见下面说明 由于有些站点使用 Let's Encrypt 颁发的免费证书，有效期只有 90 天，在测试上传中可能遇到 certificate has expired 错误，请打开开关 Ignore certificate error 即可成功上传。 补充说明 token 的获取方式 兰空图床 V1 token 的获取方式很简单，注册后进入个人设置页面就能看到，复制后使用即可 兰空图床 V2 token 的获取方式： 登录到一个 V2 版本的兰空图床，比如 https://dogimg.com 进入 API 接口页面 https://dogimg.com/api 查看获取一个 token 的方式 使用 postman 之类的 api 调试工具发起一个 http post 请求即可生成一个 token，请求时的细节如下 请求 url: https://dogimg.com/api/v1/tokens 请求方法: POST 设置请求头 Accept 的值为 application/json 请求体中使用 json 语法填入邮箱和密码 { "email": "your_username@example.com", "password":"your_password" } 请求成功后得到返回信息中的 token，使用 Bearer 拼接拿到的 token 作为 Auth token 填入 PicGo 设置中。注意 Bearer 和 返回的 token 之间有个空格，请严格按照格式填写，程序不会校验这个细节 如图 如果觉得项目有用，欢迎点个免费的 star ⭐️️ 激励一下我。感谢！ 4. 兰空图床推荐 以下站点都是 mjj 大佬用爱发电的，希望所有人都能合理、妥善使用，并抱有感激之情。 imgkb 这个站点使用的 Let's Encrypt 证书，上传可能会失败，可能需要打开 Ignore certificate error 选项 2022-03-23 下午更新：作者已经更新了网站证书，给作者点赞，现在这个选项可以不用打开了。这个也推荐 img.ink 推荐 dogimg.com 推荐，这是一个 V2 版本的兰空图床 end.
Clash Transparent Proxy 透明代理/网关 在 Linux 设备上的实践和分享
最近折腾 Clash 透明代理/网关，记录一些心路历程，也分享一些我收集到的互联网上关于透明代理写得很好的博客文章。 我的目标是在家里局域网中某台 7x24h working 的 Linux 设备上安装 Clash Core（或者 Clash Premium），然后主路由 Openwrt 修改 lan dhcp config，将 dhcp default gateway 和 default dns 发 offer 给请求 wifi 连接的局域网设备，这样就不需要设备主动修改网关和 DNS（指向透明代理设备的 IP）。为什么不直接使用 OpenWRT 的 Passwall、OpenClash、Shadowsocks R Plus + 这些？因为我用起来总有些问题…… 注意：其他普通路由器固件可能不支持修改 dhcp 默认的 gateway 和 dns 本文大概分四个部分：如何安装部署、openwrt 指定客户端获得的默认网关和 dns、透明代理设备的静态 ip 问题、文章分享 一、如何安装部署 Clash 透明代理 关于如何安装部署 clash 透明代理，网上文章很多，后面我也会贴上很多链接。在此非常推荐这个：Clash-Linux-折腾笔记 二、openwrt 的 dhcp 指定客户端获得的网关和 dns 地址 我分享一下 openwrt 如何设置 dhcp 下发的默认 gateway 和 dns, luci 访问到 http://YOUR_ROUTER_LAN_IP:PORT/cgi-bin/luci/admin/network/network/lan，将高级设置里的 dhcp 选项设置如下 或者 ssh 到 openwrt 设备，通过修改 /etc/config/dhcp 也能达到效果 config dhcp 'lan' option interface 'lan' option start '100' option limit '150' option leasetime '12h' option ra_slaac '1' list ra_flags 'managed-config' list ra_flags 'other-config' list dhcp_option '3,10.10.10.4' list dhcp_option '6,10.10.10.4' 在 config dhcp 'lan' 下新增如下两行即可。这样指定后重启网络 /etc/init.d/network restart 然后让设备 renew dhcp lease 或者“忘记网络”重新连接。重新连接上之后设备获得的网关和 dns 就是透明代理设备的 IP 10.10.10.4 了。否则会是 openwrt lan 接口的 ip（比如我的是 10.10.10.1） list dhcp_option '3,10.10.10.4' list dhcp_option '6,10.10.10.4' 关于 dhcp option 请参考 此帖 option 号 含义 3 设置网关地址选项 6 设置DNS服务器地址选项 四、透明代理设备的静态 ip 问题 可以看到 Clash-Linux-折腾笔记 中是强制指定了客户端使用 static ip，但是通过 openwrt 的 /etc/config/dhcp 也可以做到。 config host option name 't400' option dns '1' option mac '00:1c:25:a2:54:c6' option ip '10.10.10.3' option leasetime 'infinite' config host option name 'n1-armbian' option dns '1' option mac 'fc:7c:02:4e:69:f4' option ip '10.10.10.4' option leasetime 'infinite' leasetime 指定为 infinite 无限期 上面的设备 n1-armbian 就是我的透明代理设备。 然后看看 n1-armbian 的网络配置，文件 /etc/network/interfaces source /etc/network/interfaces.d/* #auto eth0 allow-hotplug eth0 no-auto-down eth0 iface eth0 inet dhcp hwaddress fc:7c:02:4e:69:f4 # Local loopback auto lo iface lo inet loopback 四、文章分享 [N1盒子] N1 使用armbian系统设置clash作透明网关 https://www.right.com.cn/forum/thread-712362-1-1.html Clash透明代理实现方式总结 https://vlike.work/tech/trans_proxy.html Clash 作为网关的透明代理 https://www.wogong.net/blog/2020/11/clash-transparent-proxy Clash使用教程 http://zhangshen147.online/post/clash-use 在N1的armbian中使用clash搭建透明代理 https://zorz.cc/post/n1-armbian-clash-tproxy.html Docker Clash 和 透明代理 https://wayjam.me/posts/docker-clash-with-tproxy/ 使用 Debian 10 搭建旁路代理网关/透明代理 https://pengjiayou.com/debian-10-transparent-gateway/ V2RAY 白话文指南 透明代理 https://www.bookset.io/read/v2ray-guide/930901e7691b15ce.md Clash-Linux-折腾笔记 https://github.com/yuanlam/Clash-Linux 树莓派 + V2Ray 配置翻墙路由器 https://moecm.com/tproxy-with-v2ray-on-rpi/ 对于所谓“旁路由”的疑惑 https://www.v2ex.com/t/663066 又是一个本地 dns 转发器 简单易用的DNS分组/转发器 https://github.com/wolf-joe/ts-dns 在 Ubuntu18.04 上使用 clash 部署旁路代理网关（透明代理） https://breakertt.moe/2019/08/20/clash_gateway/ '透明代理(REDIRECT) · V2Ray 配置指南|V2Ray 白话文教程' https://toutyrater.github.io/app/transparent_proxy.html '懵逼的HTTP、Socket与TCP - 简书' https://www.jianshu.com/p/a5410f895d6b '如何让【不支持】代理的网络软件，通过代理进行联网（不同平台的 N 种方法） | by 编程随想 | Medium' https://program-think.medium.com/%E5%A6%82%E4%BD%95%E8%AE%A9-%E4%B8%8D%E6%94%AF%E6%8C%81-%E4%BB%A3%E7%90%86%E7%9A%84%E7%BD%91%E7%BB%9C%E8%BD%AF%E4%BB%B6-%E9%80%9A%E8%BF%87%E4%BB%A3%E7%90%86%E8%BF%9B%E8%A1%8C%E8%81%94%E7%BD%91-%E4%B8%8D%E5%90%8C%E5%B9%B3%E5%8F%B0%E7%9A%84-n-%E7%A7%8D%E6%96%B9%E6%B3%95-eb22b8c04623 Clash TProxy Mode https://lancellc.gitbook.io/clash/start-clash/clash-udp-tproxy-support 又双叒叕是一个本地 dns 转发器 DNSCrypt-Proxy https://github.com/XXpE3/clash_raspberrypi/blob/master/dnscrypt-proxy.md 该链接并不是原始项目，原始项目是 https://github.com/DNSCrypt/dnscrypt-proxy (有 8.1k stars 呢) 浅谈在代理环境中的 DNS 解析行为 链接：https://blog.skk.moe/post/what-happend-to-dns-in-proxy/ 代替 Surge 增强模式——使用 KoolClash 作为代理网关 链接：https://blog.skk.moe/post/alternate-surge-koolclash-as-gateway/ KoolClash 的工作机制 https://blog.skk.moe/post/how-koolclash-worked/ iptables的四表五链与NAT工作原理 https://tinychen.com/20200414-iptables-principle-introduction/ 第一篇万字长文：围绕透明代理的又一次探究 https://moecm.com/something-about-v2ray-with-tproxy/ 感谢各位大佬的无私分享！！！ 这里有很多写得深度的文章，读来受益匪浅。 文章最后：Clash 是个优秀的项目，衍生出来众多平台的版本，感谢开发者们的辛勤付出。 至于 clash tun mode，以及结合其他 dns 转发器之类的实践，暂时不做了，因为现在我的网络已满足当下的需求。
使用 OwO.js 和表情速查项目为 Typecho 博客评论区增加更多表情
2021-11-12 14:03:41 update 由于很多表情资源 404 或者访问速度不佳，我又懒得将它们保存下来然后托管到一个访问速度不错的图床上，于是我将 owo 表情区的大部分表情全删了。虽有不舍，但是更看重博客访问速度，虽然表情的加载不影响博客的阅读，但是看到页面仍在加载就很不爽。于是更新此段。 表情速查 项目 首先感谢这个有趣、实用的项目，表情速查 使用 OwO.js 为 Typecho 博客程序的评论区添加各种表情的教程，可以优先阅读一下我之前的一篇文章，使用 OwO.js 为 Typecho 博客添加表情评论教程。 如果你熟悉了以上操作，那么现在增加更多表情包就很简单了。借助以上 表情速查 的项目，我们可以很容易获取到相关表情的 cdn 资源，以及 json 源文件。比如 所有途径 --> 7bu图床 的 QQ 表情包 我的需求是需要为每个 标签增加 style='width: 32px' 属性，一个一个手动添加不现实，那么通过程序来实现吧。复制 Twikoo 的 json 原字符串信息，粘贴给下面 java 源代码的 source 对象（idea 会自动转义）。 创建 Java 项目 创建一个 Java maven based project，这个项目的目的是为了给每个 img 标签都添加宽度为 32px 的 style 约束，否则如果图片源的 size 不确定的话，可能导致不够统一和美观。所以我建议给每张图片设定一个统一的大小。使用 style='width: 32px' 即可。 添加如下依赖 <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.30</version> </dependency> </dependencies> MainService.java 源码 import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; /** * @author hellodk * @date 2021-09-21 15:38 */ public class MainService { public static void main(String[] args) { String source = "{\n" + "\t\"bilibiliHotKey\": {\n" + "\t\t\"type\": \"image\",\n" + "\t\t\"container\": [\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-1\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-10\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-11\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-12\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-13\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-14\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-15\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-16\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-17\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-18\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-19\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-2\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-20\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-21\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-22\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-23\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-24\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-25\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-26\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-27\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-28\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-29\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-3\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-30\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-31\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-32\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-4\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-5\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-6\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-7\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-8\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"text\": \"bilibiliHotKey-9\",\n" + "\t\t\t\t\"icon\": \" \"\n" + "\t\t\t}\n" + "\t\t]\n" + "\t}\n" + "}"; JSONObject jsonObject = JSONObject.parseObject(source); JSONObject specificSticker = jsonObject.getJSONObject("bilibiliHotKey"); JSONArray jsonArray = specificSticker.getJSONArray("container"); for (int i = 0; i < jsonArray.size(); i++) { JSONObject item = jsonArray.getJSONObject(i); String icon = item.getString("icon"); StringBuilder sb = new StringBuilder(icon); int index = sb.lastIndexOf("'"); sb.insert(index + 1, " style='width: 32px'"); item.put("icon", sb.toString()); } System.out.println(jsonObject.toJSONString()); } } 这样重新生成的 json 对象每个 img 标签就包含 style 属性。 上面的代码生成的 json 对象如下 { "bilibiliHotKey": { "container": [ { "icon": " ", "text": "bilibiliHotKey-1" }, { "icon": " ", "text": "bilibiliHotKey-10" }, { "icon": " ", "text": "bilibiliHotKey-11" }, { "icon": " ", "text": "bilibiliHotKey-12" }, { "icon": " ", "text": "bilibiliHotKey-13" }, { "icon": " ", "text": "bilibiliHotKey-14" }, { "icon": " ", "text": "bilibiliHotKey-15" }, { "icon": " ", "text": "bilibiliHotKey-16" }, { "icon": " ", "text": "bilibiliHotKey-17" }, { "icon": " ", "text": "bilibiliHotKey-18" }, { "icon": " ", "text": "bilibiliHotKey-19" }, { "icon": " ", "text": "bilibiliHotKey-2" }, { "icon": " ", "text": "bilibiliHotKey-20" }, { "icon": " ", "text": "bilibiliHotKey-21" }, { "icon": " ", "text": "bilibiliHotKey-22" }, { "icon": " ", "text": "bilibiliHotKey-23" }, { "icon": " ", "text": "bilibiliHotKey-24" }, { "icon": " ", "text": "bilibiliHotKey-25" }, { "icon": " ", "text": "bilibiliHotKey-26" }, { "icon": " ", "text": "bilibiliHotKey-27" }, { "icon": " ", "text": "bilibiliHotKey-28" }, { "icon": " ", "text": "bilibiliHotKey-29" }, { "icon": " ", "text": "bilibiliHotKey-3" }, { "icon": " ", "text": "bilibiliHotKey-30" }, { "icon": " ", "text": "bilibiliHotKey-31" }, { "icon": " ", "text": "bilibiliHotKey-32" }, { "icon": " ", "text": "bilibiliHotKey-4" }, { "icon": " ", "text": "bilibiliHotKey-5" }, { "icon": " ", "text": "bilibiliHotKey-6" }, { "icon": " ", "text": "bilibiliHotKey-7" }, { "icon": " ", "text": "bilibiliHotKey-8" }, { "icon": " ", "text": "bilibiliHotKey-9" } ], "type": "image" } } 成果展示 我们复制到自己博客使用的 json 文件中，之后就可以引用这些表情包。下面看一下我的成果，看我“制作”了哪些表情包吧。 颜文字 Emoji Bilibili Bilibili 小电视 Bilibili 2333 娘 Bilibili 蛆音娘 Bilibili 小黄脸 Bilibili HotKey QQ 恶搞 QQ 嗷大喵小表情 酷安 酷币 微博 贴吧 Paopao 钉钉 阿鲁/aru 优化 OwO-bar 样式（尤其是移动端） 从 OwO.min.css 中获取的 css 样式中，比如 https://fastly.jsdelivr.net/npm/owo@1.0.2/dist/OwO.min.css，类 OwO-bar 定义了 height: 30px。 如果用户添加的表情包过多，将会导致用户看不到某些表情，这在移动设备上尤甚，因为移动设备宽度不够。 建议的解决办法是： 在 typecho 博客主题的 style.min.css 中添加以下自定义样式。 .OwO-bar { height: auto !important; } 添加保存后再更改主题的 functions.php 这一行（其他主题可能不一样，但类似） define('INITIAL_VERSION_NUMBER', '3.4.0'); 将版本号 “+1”，改成 3.4.1 保存，浏览器下一次加载页面时会应用最新 css 而无需考虑浏览器缓存。 上述自定义样式使用自动高度，并且利用 !important 强制覆盖从 cdn 下载的 css 文件中的样式。修改之后的便可以清晰的浏览全部表情。如上 成果展示 所示。 欢迎大家在此文评论区测试各种表情。 end.
本文参考 为 Typecho 博客添加表情评论 #1：使用 OwO.js 为 Typecho 博客添加表情评论 #2：添加 QQ，Bilibili，酷安，贴吧，微博表情包 我的补充 在上面引用的第一篇文章中，「绑定表情框与评论框」 这个步骤，将 textarea 或者 input 标签的class属性追加 OwO-textarea 然后很重要的一步就是，在上述 textarea 或者 input 标签后面增加一个 div 用来填充 OwO(className)这个容器，代码可以是下面这样 # 在你使用的typecho模板comments.php 文件中，textarea 或 input 标签后面的合适位置放下下面这个div，用来存放 OwO表情 还有一个很重要的问题。json 文件遇到内层的半角双引号需要转义，比如下面这样 "Bilibili": { "type": "image", "container": [{ "icon": " ", "text": "doge" }, ... ] } 原文中有些疏漏的地方。注意 img 标签的 style 属性要写，而且 width 属性值 32px，单位px也要写，否则可能评论区无法渲染表情（其实就是png或者gif图片）。图片来源的属性是 src 不是 data-src。 最后还有一个很关键的地方就是 typecho 后台评论设置里需要加入 的标签以及属性声明，否则typecho不会在评论区渲染表情。 入口在 【设置】➡️️【评论】➡️️【允许使用的HTML标签和属性】 后面应该就可以生效了。 最后就是优化一下样式。引用的css样式我们可能无法更改，或者自己修改后存放在自己博客或者其他对象存储中，反正只要公网可访问就行。 手机宽度不够，使用了颜文字、emoji、qq表情、微博表情、bilibili表情、酷安表情、贴吧表情，在手机上只能显示前几个，而且又无法滚动。所以需要将 OwO-bar 增加纵向滚动属性，如下 .OwO-bar { overflow-y: scroll } 另外原本的 css 中定义了小表情的 overflow: scroll，这代表横向和纵向都有滚动条，然而实际上只有纵向滚动条用得到，而横向滚动条不可滚动还出现在界面中（灰色，不可点击，也就是不能滚动但还显示在那），很丑，我们干掉他。 .OwO .OwO-body .OwO-items { overflow-y: scroll !important; overflow-x: hidden !important } 设置.OwO .OwO-body .OwO-items 这个class 的div 的属性为上面两个。横向滚动条隐藏，纵向的使用 scroll 保持滚动。另外加上 !important 覆盖原本从cdn下载的原始样式。 最后如果你的 OwO表情 和你的博客主题还有一些UI上的兼容问题，那么调下样式 改改css 吧 本文的分享结束。如果有问题，欢迎评论区交流。 end.
解决IDEA debug项目时，出现的Unable to open debugger port (127.0.0.1:55555)错误
update 直接使用 taskkill 杀掉 java.exe taskkill -f -t -im java.exe 给出几种方法： control+shift+delete 打开windows的任务管理器，选择详细信息，杀掉所有有关于“Java”的进程，然后再次运行项目，会发现IDEA不报错了。这个方法之所以凑效是因为JB家的IDEA程序不是用Java语言写的，所以你停止项目并不会导致Java的相关进程也被kill掉 重启电脑 修改tomcat server配置，更换server服务对应的端口号，如果更换的端口号没有被本地程序占用，那么更改后是可以重新启动的 若未能解决请查看其他博文（我好懒也好蔡，现在博文都懒得更新了） end.