2024-11-13 02:52:02
最近使用了 ZenFS 在浏览器中模拟文件系统,以在浏览器中像使用 node fs api 一样存储一些文件。但想要可视化的检查当前存储的文件时,却没有一个可以直观的工具来完成。所以就创建了一个 Chrome Devtools Extension ZenFS Viewer,以实现这个目标。在此过程中就遇到了如何传递 ArrayBuffer 从网页到 devtools panel 线程的问题,一些尝试如下。
首先尝试了最简单的方法,browser.devtools.inspectedWindow.eval
可以在网页执行任意代码并得到结果,例如
1 |
|
然而 inspectedWindow.eval 并不支持 Promise 返回值,例如下面的表达式无法得到 Promise 结果
1 |
|
同样的,也无法支持 ArrayBuffer。所以这个显而易见的 API 被放弃了。
1 |
|
接下来就是思想体操的时候了,一开始考虑的方案就是通过 devtools panel => background script => content-script(isolation) => content-script(main) 进行中转通信,以此在 devtools panel 中调用网页的全局变量并传递和获取 ArrayBuffer 的响应。大概示意图如下
然而在使用 chrome.runtime.sendMessage
时也遇到了和 inspectedWindow.eval
类似的问题,无法传递 ArrayBuffer,仅支持 JSON Value。当然可以序列化 ArrayBuffer 为 JSON,但在传输视频之类的大型数据时并不现实。
之后经过一些搜索和思考,找到了一种方法可以绕道 chrome.runtime.message,因为注入的 iframe 和 devtools panel 同源,所以可以使用 BroadcastChannel 通信,而 iframe 和注入的 content-script(main world) 之间尽管不同源,但仍然可以通过 postMessage/onMessage 来通信,并且两者都支持传递 ArrayBuffer,这样便可绕道成功。
网页与注入的 iframe 之间,通信可以使用基本的 postMessage/onMessage 实现,为了减少冗余代码,这里使用 comlink 来实现。
先看看注入的 content-script,它主要是负责对 iframe 暴露一些 API 的。
1 |
|
而在 iframe 中,则需要转发所有来自 BroadcastChannel 的请求通过 postMessage 传递到上层注入的 content-script 中,其中在每次传递 ArrayBuffer 时都需要使用 transfer 来转移对象到不同线程。
1 |
|
而在 Devtools 中,要做的事情有一点点多 🤏。首先需要注入两个 content-script,而其中 isolation-content.js 是用来创建 iframe 的 content-script。
1 |
|
1 |
|
接下来就可以在 devtools-panel 中获取数据了,由于 iframe 的注入完成的时机并不能确定,所以需要加个简单的通知机制。
1 |
|
1 |
|
完整代码参考: https://github.com/rxliuli/devtools-webpage-message-demo
至今为止,仍然没有简单的方法来支持 Devtools Extension 与 Webpage 之间的通信来替代 inspectedWindow.eval
,确实是有点神奇。
2024-10-16 00:26:52
之前发布 Chrome 扩展到 Chrome WebStore 时,WebStore 要求提供几张截图,而且必须是 1280x800 或者 640x400,而如果想要手动调整窗口大小为特定尺寸的话,会非常痛苦。所以一直想找到一种方法可以快速调整窗口尺寸到指定的大小。之前尝试过 AppleScript,甚至想过开发一个 Mac 原生应用来解决,但都遇到了一些问题(主要是权限问题),直到昨天看到一篇文章启发了吾辈。之前从未使用过 Shortcuts,没想到 Mac 自带的自动化工具还不错,完全解决了吾辈的问题。
在早前,吾辈曾经就该问题询问过 AI,得到的答案是创建一个 AppleScript 来自动化这个操作,看起来脚本很简单。
1 |
|
事实上,如果在 Automactor 中直接执行,也确实符合预期,可以修改窗口大小。但在吾辈将之保存为 App 后,再次运行却出现了权限错误。
1 |
|
而 System Event 也确实是给了的,不知道发生了什么。🤷
在使用简单的脚本实现受挫之后,吾辈考虑快速开发一个 Mac App 来解决这个问题,但实际上仍然遇到了一些问题。主要是对 Swift 没有任何先验知识,XCode 相比之前使用 IDE(Jetbrains/VSCode)非常难用,再加上 AI 对 Swift 代码生成和修改支持并不好,所以开发起来很痛苦,而且最终仍然遇到了与上面 AppleScript 类似的权限问题。吾辈猜测这是个愚蠢的问题,熟悉 Mac App 开发的人或许十分钟就能解决,但确实卡住了吾辈。
终于,Shortcuts 可以以低代码的方式创建一个应用。基本思路是,获取所有窗口 => 找到置顶的窗口 => 修改窗口尺寸。
另外吾辈已经把这个 Shortcut 导出放到 GitHub 上了,可以自行下载使用:https://github.com/rxliuli/mac-resize-window
The Easiest Way to Resize All Windows on Your Mac Simultaneously to the Same Dimensions
2024-09-27 16:21:46
自从九月初从广州出发继续北上旅行,刚好躲过了一次台风摩羯。虽然旅行仍然能带来一些新鲜感,但或多或少已经有些不足了,所以在江浙连续碰上两次台风之后,九月下旬便匆匆赶回来了。
第一站去往了衡山,之前前往山东时没能爬上泰山,这次顺路去爬下衡山。刚到衡山就被出租车上了一课,滴滴叫了车但司机实收很低线下想让吾辈额外付费,之前在广西阳朔、后面在江苏镇江都遇到过类似的糟心事,也许越是小地方越是不讲规则?
刚到就去了附近的万寿广场,35 度的天气下对吾辈和相机都是考验。
之后前往旁边的南岳大庙,本身吾辈并不信佛道,但拍拍风景也是好的。
晚上出来拍点夜景,偶然还看到了五岳特种兵的宣传牌,之前完全不知道这个活动。
次日一早开始爬衡山,说是爬山,也是要坐很久的区间车,在最后的山顶附近才开始爬。由于雾气很大,没能拍出来什么照片。相比于爬山爱好者,来此求神拜佛者更是络绎不绝。
影集
第二站选在了长沙,之前去了湖南的张家界,却没去省会城市,这次补上。不过前往之前刷到了一些关于臭豆腐的坏消息,所以臭豆腐一点也没尝。虽然耳边还记得那句“来长沙不吃长沙臭豆腐等于白来”的广告语。
先去城市里随便逛逛扫个街,有一说一,第一次看到把辣条作为宣传的。
大楼的阴凉处,好多正在直播的人,也许附近有“直播小区”?
途经坡子街,到处都是小吃和茶饮,尤其是茶颜悦色,非常显眼。
之后,来到湘江之畔,走在翠幕之下的河岸边,可以眺望对面的橘子洲。树荫之下,人们三三两两的坐着,或是聊天,或是休息。
途经太平老街,中午简单吃了个饭,随即前往河对岸,只是天色渐晚,遂未登岳麓山。
走了一天也有些累了,之后便回到青旅暂作休息。
次日早晨,前往湖南省博物馆,其中付费的两个场地人数不多,值得一看,尤其是数字科技展览部分。
不幸的是,下午刚出博物馆就遇上了暴雨,无奈之下只能打车回去,暂时避雨。直到傍晚,才出来去杜甫江阁附近观赏晚霞与夜景。
可能唯一留下不好印象的就是小巷之中贴的到处都是的标语了。众所周知,通常缺什么才写什么。。。
影集
第三站是南京,可以说是整个行程中最满意的一个城市,是这次旅行中照片最多的城市。
当晚就去了知名的秦淮河畔,表面看上去光鲜亮丽无比。
实则水质较差,隐有臭味,把相机吓得都变形了。
第二天前往中山陵和明孝陵,后者超级超级超级大,逛了好几个小时还没走完,直到下午 4 点钟才离开。
在离开中山陵,进入明孝陵之后,首先碰到了钟山文学馆,江南园林看起来永远都很舒服。
之后是颜真卿碑林,第一看到时突然想起来黑猴里面第四章见到二姐时的屋顶,可能最近看到黑猴的视频太多了 emmmm。
临近中午到达了紫霞湖,许多大爷大妈们在这儿野餐和游泳。
到达中午,终于抵达了明孝陵。
之后更是收集到了红粉黄橙白五种颜色的彼岸花。
里面还有非常非常多的小园子,不再一一赘述。之后打算前往明长城看看,结果下午 5 点就关闭了。
次日去了鸡鸣寺和城墙,吾辈是从电视剧《明王朝》的视频中看到过这个,实际去了之后也没什么。
在城墙上散步时,看到一个很漂亮的小园子,只是忘记记下具体的位置了。
影集
第四站是镇江,到的时候还不能办理入住,所以先去了金山寺转转。
第二天遇到了台风,天气非常非常糟糕,所以完全没有出门。第三天倒是去了附近的南山风景区,台风刚过,天气异常的好。
下山之后,附近的风景也非常棒,真的很喜欢这种蓝天白云的干净。旁边还有一处隐秘的日月潭,还有人在那里游泳。
晚上去了当地的历史文化街区,无甚新意,直接略过。
次日前往焦山,相比于自然风景似乎其历史文化意义更为丰富,不太感冒。
影集
好了,终于到达了最后一站无锡,刚经历过一场台风,下一场台风接踵而至,气的吾辈提前回广东了,可以说这是最糟糕的一站。
抵达当晚便前往惠山古镇,因为预报明天就开始有雨,在旅行中下雨是最烦人的天气了。
夜景还算不错?
探索阴暗的小巷子。
由于台风的影响,旁边的公园也没有开放。
嗯?这就结束了?
是的,由于下雨呆了一天,在下一场台风来临之前便坐上火车回去了。
影集
可以说,出去旅行排解心情的效果愈发糟糕,还是需要寻找到主线任务。
2024-09-12 22:49:12
最近为 Chrome 开发了可以直接在浏览器运行 TypeScript 的插件 TypeScript Console,需要将代码编辑器集成到 Chrome Devtools 面板。其实要在 Web 中引入代码编辑器也是类似的,下面分享一下如何实现。
首先来看看有什么问题
首先,考虑到要编写的是 TypeScript 编辑器,所以选择 Monaco Editor。它是 VSCode 的底层编辑器,所以对 TypeScript 的支持度是毋庸置疑的。来看看如何使用它
安装依赖
1 |
|
引入它,注意 MonacoEnvironment 部分,使用 TypeScript LSP 服务需要使用 WebWorker 引入对应的语言服务。
1 |
|
1 |
|
现在就有了一个基本的 TypeScript 编辑器了。
接下来如何编译和运行呢?编译 TypeScript 为 JavaScript 代码有多种多样的选择,包括 TypeScript、Babel、ESBuild、SWC 等等,这里考虑到性能和尺寸,选择 ESBuild,它提供 WASM 版本以在浏览器中使用。
安装依赖
1 |
|
基本使用
1 |
|
编译结果
接下来,如何运行编译好的代码呢?最简单的方式是直接使用 eval
执行,或者根据需要使用 WebWorker/Iframe 来运行不安全的代码。
1 |
|
或者也可以使用 WebWorker。
1 |
|
现在,结合一下上面的代码,在按下 Ctrl/Cmd+S 时触发编译执行代码。
1 |
|
1 |
|
接下来,应该看看如何支持引用 npm 包了。不使用构建工具时一般是怎么引用 npm 包呢?先看看来自 Preact 的 官方示例 吧
1 |
|
可以看到,这里借助浏览器支持 ESModule 的特性,结合上 esm.sh 这个服务,便可以引用任意 npm 包。
而关键在于这里使用了 esm 格式,而上面可以看到在构建时使用了 iife 格式,简单的解决方法是将运行时的代码修改为 esm 格式,复杂的方式是将 esm 格式转换为 iife 格式。
先说简单的方法,修改之前的代码
1 |
|
现在,可以使用 esm.sh 上的 npm 包了。
1 |
|
但实际代码中通常希望使用 import { sum } from 'lodash-es'
而非 import { sum } from 'https://esm.sh/lodash-es'
,所以还是需要转换 import
。这涉及到操作代码语法树,此处选择使用 babel,首先安装依赖。
1 |
|
还需要给 @babel/standalone
打上类型定义的补丁(已提 PR)。
1 |
|
然后获取所有的 import 并转换。
1 |
|
然后在编译代码之前先处理一下 imports 就好了。
1 |
|
现在,编译时会自动处理 npm 模块了。
1 |
|
esm 是新的标准格式,但旧的 iife 仍然有一些优势。例如不挑环境、可以直接粘贴运行等,下面将演示如何将 esm 转换为 iife。
下面两段代码是等价的,但前者无法在 Devtools Console 中运行,也无法使用 eval 执行,而后者则可以。
1 |
|
需要将下面包含 import 的代码转换为动态 import 的,参考 amd 格式可以得到
1 |
|
接下来使用 babel 提取所有 imports 并生成一个 define 函数调用,清理所有 exports,并将自定义的 define 函数追加到顶部。
首先解析每个 import,它可能在 define 中生成多个参数,例如
1 |
|
会得到
1 |
|
所以先实现解析 import
1 |
|
然后修改 transformImports
1 |
|
1 |
|
现在,代码可以正常编译和运行了,但在编辑器中引入的 npm 包仍然有类型错误提示,这又应当如何解决呢?
得益于 TypeScript 的生态发展,现在实现这个功能非常简单。首先,安装依赖
1 |
|
然后引入 @typescript/ata
1 |
|
还需要为编辑器设置一个 Model,主要是需要指定一个虚拟文件路径让 Monaco Editor 的 TypeScript 能正确找到虚拟 node_modules 下的类型定义文件。
1 |
|
1 |
|
上面的代码还有许多地方没有优化,例如在主线程直接编译代码可能会阻塞主线程、引入了 3 个 TypeScript 解析器导致 bundle 大小膨胀、没有正确处理 sourcemap 等等,但这仍然是一个不错的起点,可以在遇到需要为 Web 应用添加代码编辑器之时尝试用类似的方法完成。
2024-09-03 08:42:33
漫长的暑假终于结束,可以继续出门旅行了。这周先去了附近的海陵岛,虽然一直素有坑人的水鱼岛之称,不过吾辈还是来玩了三天。
路线: 大角湾 => 大角湾夜滩 => 北洛秘境(沙滩)=> 马尾岛 => 滨海栈道 => 观海楼 => 十里银滩
刚到这里住在了大角湾,据说是岛上最方便的地方。来的时候已经是下午,便在附近走了走,把一块海滩围起来收费并且规定只允许在那里泡水实在太蠢了。
有很多人在远方冲浪。
远远的还能看到左边的滨海栈道,不过今天没往那边走。
不过下午去旁边的大角湾夜滩,沙滩确实维护的还不错,海水看起来也非常清。
在步行的路上也拍了一些街景。
路边还拍到了一些野花?
在前往马尾岛的路上,偶然发现北洛秘境沙滩免费开放了,于是也便前往看了看。
这张照片的棕榈树真的有夏日海滩的感觉。
远方的海角伫立着一座灯塔,只是不知是否还在工作。
远方海面上依稀可见停靠的风力发电平台。
从灯塔下面向海滩望去。
遗憾的是,就在海滩的左侧,吾辈看到了污水排放口。
神奇的是,污水排放口旁边似乎有个很漂亮的拍照地点,但位于岩石之上。
之后前往马尾岛,看到了海面上停放着百舸千帆。
到达马尾岛入口,山脊的形状很奇怪。
此时已近黄昏,偏逢乌云汇聚。
待至七点,终于看到了美丽的余晖。
晚上吃了点东西便继续出门了,小摊夜市都陆陆续续出摊了,但大多数都还停留在十年前的骗人玩意上,套圈、打气球之类的。
海边可以看到涨潮已经把原本位在海边的杆子淹掉了。
有人在放烟花,但并没有动画中那么美好。
第二天刚起床便有雷雨,等到下午雨终于转小时,带着相机就出了门。昨天往右走到了马尾岛,所以今天便向左出发。
沿着海滩一直向左走,便看到了城市的排污入海口,各种垃圾沿着下水道流入海中,旁边大角湾的海水水质可想而知。
不消片刻,便抵达了附近的南海放生台(放生到污水入海口也活不了吧?)
旁边的山上可以看到隐匿于山林之中的居所。
之后就开始沿着滨海栈道出发了,总长 2.5 公里,由于栈道破损缺乏维修,花了一个半小时才走完,而且并不安全。
在一个海角可以看到部分栈道,栈道左上方便是公路。
看起来很危险,实际上一点也不安全。
山林之下。
偶遇一只鸟儿,有人知道这是什么吗?
山上便有一座妈祖庙。
山下则有各种大小的鹅卵石。
又偶遇到一只世界之王羊。
微距啊微距,好想入坑微距。
走了很久之后终于到达山的另一侧,接近十里银滩的位置有一个楼梯,上面通往已经废弃的观海楼,随处可见废弃后的破败。
行至高处,海阔天空。
终于抵达山的另一侧,可以看到十里银滩了。
海边的礁石迎接着永无止境的海浪。
海天一色,有点难。
一只渔船,不知在捕捞什么东西。
下到山脚下,看到一片野沙滩,几个小孩子正在海里玩水,于是也下去想沿着沙滩走走,却不想下来时把手机忘在了台阶上。
次日中午终于前往海里泡水,并未去往收费的海滩,而是出门即达的酒店旁边的海滩。
要说海里与水上乐园泡水的不同,应该是大海有永不停歇的浪潮在推动,让人无法在一个地方安心泡水吧。
关于海陵岛是否坑人,吾辈会说确实如此。虽然风景还算不错,但恐怕吾辈不会再去第二次。之后将会前往长沙,之后转向南京,随后便在江浙附近待到 9 月底了。
2024-08-20 02:29:48
Poe 是一个 AI 聊天机器人,它支持多种 AI 模型,包括 GPT-4o、Claude 3.5 Sonnet、Gemini Pro 等。还支持各种类型的 Bot,其中 Server Bot 是最自由的,可以自己编写 Bot 的逻辑。但是,Poe 的 Server Bot 官方仅支持 Python,而吾辈更喜欢 JavaScript,所以研究了一下怎么实现。
一开始使用 express 实现服务端,但后面发现 express 无法部署到 edge runtime,例如 Cloudflare Workers,所以改用 hono.js 实现。
首先使用 hono.js 创建一个项目并选择 cloudflare-workers 模板
1 |
|
src/index.ts 是项目入口,内容如下
1 |
|
首先在 Poe 网站上创建 Server Bot,得到一个 Name
和 Access Key
。
根据 Poe 协议规范,实现一个 Bot 需要实现一个特定的 post 请求,具体如下
API 接口传入的参数会有两个固定字段,type 是请求类型,version 是协议版本。
1 |
|
其中 query 和 settings 是必须实现的,report_feedback 和 report_error 是可选的。
下面来实现一个基本的 post 请求结构
1 |
|
Settings 请求没有额外的参数,仅要求返回这个 bot 相关的一些设置。
1 |
|
例如,实现上面的 settings 请求,这是一个简单的响应
1 |
|
参考 https://creator.poe.com/docs/poe-protocol-specification#settings
query 是关键部分,不管是请求还是响应都很复杂。
下面是请求的类型定义
1 |
|
响应要求返回 SSE 流式响应多条消息,具体也有很多类型
meta 类型,应该返回的第一条消息,主要是用来声明一些设置
1 |
|
接下来是两种消息类型,区别只在于是否替换之前已经发送的消息
1 |
|
还有两种特殊格式的消息,一种是用来返回 JSON 数据(通常给 OpenAI 这种支持函数调用的 Bot 使用),另一种是建议回复的消息,这会出现在回复消息的下方。
1 |
|
最后是结束和错误消息。
1 |
|
现在来实现一个简单的 query 请求
1 |
|
参考:https://creator.poe.com/docs/poe-protocol-specification#query
现在发布到 Cloudflare Workers 上,得到一个 URL,例如 https://xxx.workers.dev
。
1 |
|
现在在 Poe 网站上填写 Server URL,然后点击 Run check,如果成功,继续创建 Bot 就可以在 Poe 上使用了。
根据 Poe 官方的建议,还应该为 post 请求添加验证,确定是来自 Poe 的请求。首先添加环境变量
1 |
|
然后在 src/index.ts 中添加验证
1 |
|
Bot 除了可以被 Poe 调用,也可以主动调用 Poe 的 API 来实现一些功能,下面介绍其中两个。
修改 Bot Settings 的实现后,还需要主动通知 Poe 调用接口刷新设置。
例如修改了 handleSettings 函数,更新了 server_bot_dependencies
,不再使用 GPT-4o,而是使用 Claude-3.5-Sonnet
。
1 |
|
然后在项目初始化的时候主动通知 Poe 刷新设置
1 |
|
然后在浏览器中直接访问这个 URL 就可以通知 Poe 刷新设置了。
接下来,说明如何调用第三方的 Bot,这里仅以文本 => 文本的 Bot 为例(除此之外,现在的 Bot 还支持附件文件、图片、音视频等)。遗憾的是,官方文档没有记录 API 接口,只说明了使用 python 模块 fastapi_poe 来实现,所以只能分析 fastapi_poe 的源码来实现。
在其中可以找到关键接口 https://api.poe.com/bot/<botName>
,然后接口会以 SSE 流式响应多条消息,先做个测试。
1 |
|
终端打印的结果。可以消息分为三种:
1 |
|
因此需要实现一个 TransformStream 来将 SSE 文本流转换为结构化的数据,并处理 text 多条消息合并。实现本身并不复杂,但主要的问题是多个模型拆分规则可能规则会不一致,例如 GPT-4o chunk 中可能包含一条完整消息,也可能不包含,而 Claude 3.5 Sonnet 中则总是由两个 chunk 组成一条完整消息。还有一些模型会返回 ping 消息,而且 ping 消息的格式也略有不同,像是 ping
或 : ping
等。
下面是 GPT-4o 的消息示例
1 |
|
Claude 3.5 Sonnet 的消息示例
1 |
|
所以实现的 TransformStream 如下
1 |
|
修改 requestStream 来使用这个 TransformStream
1 |
|
1 |
|
现在,重新发布至 Cloudflare Workers,然后就可以向这个 Bot 聊天,并在服务端调用 GPT-4o 模型。
上面只是一个非常简单的 demo,Poe Server Bot 实际上还可以做很多事情,但对 JavaScript 缺乏官方支持,让想要尝试变得比较麻烦。吾辈发布了一个 npm 模块 fastapi-poe,来尝试像官方的 python 模块 fastapi_poe 一样使用。