闪星空间 https://shansing.com/ zh-CN “闪星空间”是“闪闪的星”的独立博客,主要聚焦网络、技术和思维。博主虽叫闪闪的星,但其实每个人都可以是一颗闪星。欢迎你们来访,一起闪闪发光! Sat, 28 Feb 2026 11:28:00 +0000 Sat, 28 Feb 2026 11:28:00 +0000 Kubuntu 体验 https://shansing.com/read/560/ https://shansing.com/read/560/ Sat, 28 Feb 2026 11:28:00 +0000 闪闪的星 工作原因需要一台 Linux 电脑,我新买一台 ThinkPad E14 长续航版(Intel Lunar Lake),安装 Kubuntu 主线发行版。在《Kubuntu 初体验》后,过年期间我带它回老家,既入门学习 go 语言,又观看在线视频、日常使用。我的评价不变:Linux 满意到超出预期,跟 Windows 各有千秋。现在综合说一下新旧体验。

目前痛点一个是 TrackPoint “小红点”滚动似乎有起始静止时间(应该类似于 attack time),一个是磁盘加密不能 TPM 自动解密(应该有一些复杂配置方法,先期待未来的一键配置)可以通过简单方法启用(记得看评论增强安全性)。在查阅指纹识别资料以后,我安装 Windows 才发现这台电脑竟然没有指纹传感器:Linux 下 root 密码输入场景很多,是个遗憾。硬件倒是配有红外摄像头,但我看到一个程序 howdy 很老了,它甚至会尝试安装全局 python 包,这种操作现在都被 Debian/Ubuntu apt 警告阻止。

非要再挑一个刺,就是 Ubuntu 缺乏简单的“休眠”配置(状态持久化到磁盘,抵抗断电),目前只能凑合用“睡眠”(状态停留在内存,功耗较高)。休眠似乎可以通过一系列复杂操作开启,但我还不想牺牲 Secure Boot——没错,Kubuntu/Ubuntu 竟然可以在 Secure Boot 下启动。再一个日用遗憾是缺少原生的视频超分辨率支持,不像 Windows 下 Firefox 可以调用 RTX Super Resolution。

稍微踩了几个坑。一个是输入法,某些程序可能要单独配置环境变量(按照 fcitx 推荐的方式)。然后是官方商店的 Firefox 不支持视频硬件解码,在我修改配置、安装插件、切换分支一通操作后,发现安装 Mozilla 官方 deb 包就正常——我本已告诉自己不要讨厌 snap,现在只能远离。(最终我卸载了所有 snap 包并 hold snapd 本身。)启动 ungoogled-chromium 需要手动配置 Ubuntu 对沙箱的限制,有时会掉,需要手动 reload apparmor。VirtualBox 启动虚拟机,宿主机 Secure Boot 则需要手动对内核模块签名,并在 UEFI 添加信任。有时候任务栏固定的 Firefox 图标会混乱,启动以后变成隐私模式或者 Developer 版本的独立的蓝色图标样子,删除 .cache 目录可解决,后来失灵,最后在“窗口规则”中为两个程序分别指定 .desktop 文件名似乎有效(且系统和用户目录的 applications 目录的 .desktop 文件最好不要同名)。桌面环境也卡过两次。

尽管如此还是要表扬,硬件都正常,包括 WiFi、蓝牙、摄像头等等。也可以使用 Type-C 一线通外接显示器。每个显示器可以单独设置非整数倍缩放。桌面和窗体管理器可以感知显示器颜色配置,可以选择色域限缩(不那么过饱和,不过我的某台显示器肉眼可见还是有偏色),即下图“内建”。

显示设置可以使用显示器颜色文件

内置多显示器亮度调节滑块,不需要像 Windows 那样需要另外安装软件。Fcitx 5 输入法也媲美 Windows 自带的微软拼音,还能导入搜狗在线词库。要重申,任务栏图标可以不合并/不分组,就是比 Windows 文明。外观可以自由组合:我可以用 Linux 的鼠标指针、Windows 的任务栏和图标、Mac 的窗体配色和按钮——我想 Linux 的自由包括外观配置自由,更不用说远离了遥测和强推的破烂 AI 玩意。

KDE 自由的外观调节

如果我喜欢还能换成 Windows 7 Aero 效果窗口。但是那个外观在高缩放下有点小问题,而且我怕公司认为我在用 Windows 然后开除我,我还是不用了(开玩笑)。这里插一句,我看到 Programmer Jokes 推了一张梗图。得,我被鉴定为女孩了。

男孩、女孩谈论 Linux

KDE 也并不是只有基本功能;自带的文本编辑器很强大,可以配置成 Notepad++ 那样无需保存、下次打开自动恢复。系统本身和自带组件的定制空间很大。字体也可以配置映射;感谢现在很多公司推出免费可商用字体。生态方面,IDE 和很多日常软件(包括微信)都有;Snipaste 也有,我也让 AI 写代码增加对微信 OCR 的支持。

Lular Lake 在 Kubuntu 的续航也比较满意,不插电的平衡模式日常在 10W 功耗以下,且没有明显卡顿。可以配置插电开启高性能模式,可以设置充电阈值,就像一个正常友好的操作系统应该表现的那样。

不得不说我对 Linux 桌面改观很大。以前也偶尔装过 Deepin 之类的发行版,但是没能用下去。我记得 Deepin 有时候还会进不了系统,现在也没有色彩管理。但是这里没有要“踩”它的意思,它毕竟带动了国内 Linux 生态发展。也偶尔用过 Ubuntu,有点一言难尽。我只是觉得原来其他的发行版开箱体验也很好,用最新的管线、组件能够好用又稳定。我给出“跟 Windows 各有千秋”这个评价,已经跟 Mac 平齐了。

2026-02-28 P.S.基本搞定 TPM 自动解锁加密分区。

2026-02-29 P.S.增加用户目录 .desktop 文件说明。“小红点”滚动有起始静止时间可能是我的错觉,我发现 Windows 上滚动之前(不实际向上下用力时)指针会变,这个反馈对比起来导致我认为 Linux 上反应迟钝。

]]>
0 https://shansing.com/read/560/#comments https://shansing.com/feed/
让 Snipaste 使用微信 OCR(Linux) https://shansing.com/read/559/ https://shansing.com/read/559/ Tue, 24 Feb 2026 14:20:00 +0000 闪闪的星 上一篇《让 Snipaste 使用微信 OCR》只适用于 Windows。由于工作原因,我开始使用 Linux。Snipaste 在 Linux 上的本地 OCR 方案只能依托于 Tesseract。我给 Tesseract 再一个机会尝试仍然不满意,转而跟上一次一样配置微信 OCR。其实 swigger/wechat-ocr 已经支持 Linux,也有一些衍生的 docker 镜像项目。不过我编译原项目就能得到可用的制品,思索以后决定让 AI 帮我封装。

我的环境是 Kubuntu 25.10 amd64,微信是 .deb 包安装的 4.1.0.16(当然要装 Linux 版),Snipaste 是 2.11.3(当然要是专业版)。拉 swigger/wechat-ocr 最新 commit 是 e32d4af。如果不想看下面的废话,可以直接获取 wechat-ocr-snipaste-linux.sh 文件使用。

准备工作

首先要自行编译 swigger/wechat-ocr,它添加 Linux 支持后就没有发布过 release。我不是很懂 C 程序编译,这个工程似乎既可以用 cmake 也可以用 clang。我遵循一个网友列出的命令

mkdir build
cd build 
cmake..
make

当然需要先安装 cmake、make 等套件,也需要安装 python3-dev。否则会有类似以下报错:

为 wcocr 配置了符号导出 (wechat_ocr;stop_ocr;_ZN10CWeChatOCR;_ZN9CMojoCall;_ZT?10CWeChatOCR)
CMake Error at /usr/share/cmake-3.31/Modules/FindPackageHandleStandardArgs.cmake:233 (message):
Could NOT find Python (missing: Python_INCLUDE_DIRS Development
Development.Module Development.Embed) (found suitable version "3.13.7",
minimum required is "3.8")

从中也可以看到需要保证 Python 版本至少为 3.8。似乎也用了 C 语言的先进特性,老旧操作系统/标准库可能编译失败。

封装

成功的话就可以在 build 目录得到 test_cli 程序。可以像 README 指示的那样测试:

./test_cli /opt/wechat/wxocr /opt/wechat/ /home/shansing/Pictures/Snipaste/Snipaste_2026-02-24_xx-xx-xx.png 

我本来路径依赖想写 go 代码封装为 Tesseract 格式,但是我想快点搞完这事。于是我想到用 Linux 上等公民 shell 脚本,想到最近 LLM AI 写代码很火,索性接下来交给 LLM AI。我选用的模型是 Claude Sonnet 4.5 中度思考。提示词形如:

我有一个 go 写成的 ocr 代理程序:

[这里是上一个工程的 main.go 源码,略]

现在我面临一个新问题,需要在 Linux 上使用。我决定不移植,另外用 shell 脚本封装官方的 `test_cli` 来使用。`test_cli` 的输入输出如下:

\```
$ ./test_cli  /home/shansing/Pictures/Snipaste/Snipaste_2026-02-24_xx-xx-xx.png 
Usage: ./test_cli <wechatocr_exe> <wechat_dir> <test_png>
$ 
./test_cli /opt/wechat/wxocr /opt/wechat/ /home/user/Pictures/Snipaste/Snipaste_2026-02-24_xx-xx-xx.png 
OCR errcode=0/opt/wechat/wxocr
[86.97,53.71,601.09,130.45] r=1.000 高铁耳机寻回记
[92.08,168.82,833.85,199.51] r=0.960 Written by 闪闪的星 with❤on February 9,2026 in 生活
[86.97,250.67,1529.57,291.59] r=0.991 在高铁座位上突然想打喷嚏,顺手摘下口罩想改用纸巾遮挡口鼻,地一下两边苹果耳机飞出去。一只被我从旁
[89.52,312.05,626.66,345.30] r=1.000 边座位下方找到,另一只怎么寻也看不到。
[86.97,399.02,1529.57,437.39] r=0.995 我一会儿趴地上打手电筒,一会儿站起来看是不是飞到远处,一会儿想是不是只能买新的了。遥想曾经在豆瓣看
[84.38,455.29,1527.05,491.14] r=0.990 到过 AirPods 离异丧偶小组,但是耳机还不想收二手,于是又得耗费一千多大洋。坐回座位打算放弃了,意识到
[86.88,509.19,1529.64,555.17] r=0.963 刚刚我站起来有人好像说“这是谁的耳机”,但是当时我觉得他是在打电话,我又“社恐“没敢问,好一会儿过去了现
[86.97,575.51,1527.01,608.76] r=0.995 在也不好大声喊一句有谁捡到耳机。列车快到我的武汉站,我死马当活马医在小红书搜了遗失物品报备攻略,并
[89.52,634.34,483.43,667.59] r=0.997 且在百亿补贴下单一副新耳机。
[81.85,721.30,1498.88,759.67] r=0.972 结果我当天在武汉没待多久,自称“车长”的人就告诉我找到了。这可以列入"本来没抱期望,以为没有用结果有
[86.92,777.83,1532.17,815.99] r=0.994 用“的事迹。貌似是河南局承运,开往河南的途中按照我的要求转到另一车次回到武汉。我非常惊喜,取消新机订
[89.49,836.40,1529.59,872.24] r=0.995 单,最终返程时在站内领回。我好奇她们怎么找到的,可能是问了一句乘客就拿到了,也可能翻了座椅死角,但
[92.04,897.32,1514.24,931.07] r=0.984 这不重要。按照小红书的指示,我希望能寄送锦旗表达感谢,但对方表示不用花这个钱,拨打 12306 感谢就行。
[89.50,956.59,1457.99,994.67] r=0.973 我便克服”社恐“拨打了电话,慌慌张张说错重点,应该是寻回速度很快,我却多一遍强调“帮我省了很多钱”。
[1158.69,1051.26,1544.92,1076.84] r=0.929 hide comments ·back .home
debug play ocr DONE!
\```

其中 `<wechatocr_exe>` 对应我程序的 ocrBin,`<wechat_dir>` 对应我的 wechatDir。

你的任务是:编写一个 shell 脚本。仍然接收我原先程序的四个参数,做原先程序差不多的事(将标准输入的图片保存到临时目录,在成功或失败后删除),但是将 syscall 的 DDL 调用改成执行 `test_cli`。特别地,你需要过滤 `test_cli` 的输出,只保留 `[...] r=... ` 之后的文本,过滤无关 debug 消息行和前缀。你可以把 debug 信息输出至标准错误,确保标准输出只有 OCR 识别的最终文本。

第一个回复基本可用,赋予执行权限执行。这时我看代码发现 AI 错误理解 test_cli 的地位,于是我说:

我没看到你调用 `test_cli`,这是我另外一个程序,不是在 `OCR_BIN` `WECHAT_DIR` 里的

接着调试一下。貌似卡住了,看代码没打印文件名。应该是随机串生成阻塞住了。换便宜的 GPT 4.1 mini 让其修改随机数方法:

UUID=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 32 | head -n 1)

这个阻塞住了。帮我换一种更简单的写法

最终我选择的是:

UUID=$(cat /proc/sys/kernel/random/uuid)

写好以后手动执行一下保证成功:

cat /home/shansing/Pictures/Snipaste/Snipaste_2026-02-24_xx-xx-xx.png | /home/shansing/dev/wechat-ocr-snipaste-linux/wechat-ocr-snipaste-linux.sh stdin stdout -ocrBin /home/shansing/dev/wechat-ocr-snipaste-linux/wechat/wxocr -wechatDir /home/shansing/dev/wechat-ocr-snipaste-linux/wechat/ > output.txt

这里我仍然将微信目录复制出来了。成功之后就可以将 wechat-ocr-snipaste-linux.sh 选为 Snipaste 的 Tesseract 可执行文件。

]]>
1 https://shansing.com/read/559/#comments https://shansing.com/feed/
量子保鲜记 https://shansing.com/read/558/ https://shansing.com/read/558/ Tue, 27 Jan 2026 23:09:00 +0000 闪闪的星 一切开始于我在家吃冻干食品。虽然这种技术宣称航天级,也确实还原相当一部分食物味道,但与新鲜状态还差很远。这时我留意到旁边一本哲学教材,许多思想实验相当科幻,我突然有了灵感。而后经过数年研发,我终于能够制造并逐步量产一类“量子保鲜”食物。

我的量子保鲜法,可以在微观级别锁住食物风味,让消费者得到原汁原味的享受,并且适用于固态、液态、固液混合各种品况。保鲜过程扫描食物所有分子原子,然后打乱成一种带有叠加态的稳定状态,保存封装。固然在微观层面量子测不准,也不可复制,然而只要不操作单个或少量粒子,还是有一些理论技术可以抵消这些不利,使得宏观上可以保留一团物质的排列组合信息。事实上也只能称作保留信息,在量产时很难一模一样复制。不过好在只需要经过后面称为“解鲜”的步骤,食物就能还原得分毫不差。

若干次抽样实验中,借助某些电子显微镜或者衍射手段,在物体自然变化的误差范围内没有观察到不同。实验所用物体是合金之类的非食物,因为食物变化过快。也万万不可用生物,目前尚不知晓会发生什么事,在哲学和伦理上争议颇大。在委托第三方进行的大规模随机双盲实验中,参与者区别量子保鲜食物于现煮食物的成绩不好于瞎猜。

这种保鲜方式最好由消费者自行解鲜,类似于自热火锅需要加热。解鲜时有一个量子坍缩过程,是可控退相干,将原有的食物分子信息还原。经过研究,不可以在工厂生产时直接解鲜,良率太低。这里面可能需要意识参与。流水线工人上班起始解鲜成功率高,到饭前便显著降低。即使对于消费者,解鲜也是一种比较可能失败的游戏。

为了增加解鲜成功率,我在包装上教育消费者正确方法。我印上大大的内容物图案,比如兰州拉面就印兰州拉面,有助于消费者意识映照食物。还有一行警示语:视力不好者请在他人帮助下解鲜(含盲文版本)。尽管如此,还是会有消费者忽略说明,以为已经得心应手,意识层面没有想到相应食物,时常解鲜失败。这样就只能请求他们去经销商退换货。

我推出普通方便面、素火锅、四川麻辣烫、兰州拉面、柳州螺蛳粉等品类,价格不一。有很多博主网红挑战解鲜为另一款食物:比如买了我的普普通通方便面,心中想着高贵豪华螺蛳粉,然后进行解鲜动作。他们会拍成挑战视频,比赛自己能够做到何种程度,再做一个吃播。有一些人因此被我厂招纳进内测组,也有些人被我厂会员系统拉黑。

起初我厂仅有少数品类。尽管如此,一经推出就脱销。量子保鲜食品需要特殊包装。在我研发时军方就找上门来,要求理论保密、技术独占,一番讨价还价后才同意有条件发售,并且提供配套包装材料研发。成品十分稳固,据说 X 光也不能穿透包装层。因而上市伊始快递拒绝发货,给网售带来很大不便;大量消费者也反馈地铁安检被盘查。这种现象持续不到一周就不见了。我决定趁热打铁引入更多品类。

在低端线上,我尝试完全用复制而不烹煮的方法生产,可以将方便面价格压低至跟一般泡面无几。但是基因单一性开始引发广泛担心:完全一样的复制产品,拥有完全一样的基因序列。以前这并不是问题,也很少有人认真觉得吃进去的 DNA 会影响身体。但一些发表在媒体上的初步的动物实验,导致我的产品在铺货途中就遭下架。有证据显示其中部分实验,以及伪装成报道的文章,可能受其他食品公司资助。我计划用几十上百份食物作为原型,复制品标上序号再铺售,如此消费者可以自己避开单一基因组。但我担心即将到来的量子保鲜食品标准和法规,于是作罢。

中高端线则相当成功。我推出地理特色产品,在当地设厂给当地做贡献:柳州设螺蛳粉厂,郴州设杀猪粉厂,海口设糟粕醋厂,淄博设烧烤厂……中端线用机器烹饪,高端线请厨师于工厂端现煮。许多拥有自己熟食特产的县市政府和民众热切期盼我设分厂。也有一些争夺地理命名权的插曲。我兴致大发时也会捏造产品,像是“耒阳杀鸡粉”,果不其然后来变成全国知名:许多人真在耒阳开杀鸡粉店,很多人专程去往耒阳旅游打卡。

为了满足先前军方所提条件,我厂开始研究保鲜和解鲜的细节。参与他们的讨论,我时常感觉自己知识不足。相较起来,最开始的保鲜方法像是天上掉下、妙手偶得。研发团队最终得以在保鲜时预置参数,使得解鲜时结合所在地理环境,物理层面上限制只能在中国国内解鲜,包括港澳台和争议但主张地区。中国以外就禁止使用。我几次试图理解细节,无功而返,只觉得这真是实现了“风水”校验。但我因此损失了飞机上的生意。

我的重心开始放在对外合作上。研发团队每年都能改进量子保鲜法,也从中逆向推导出一些低级而廉价的保鲜方式。我厂承诺,所有旧代际技术最终将向社会开放。一年给出两次合作机会:一次指定合作,一次随机抽签。指定合作会考虑各方势力,结合我本人喜好:例如喜欢盛香亭转转热卤就发去邀请,也会帮助内江牛肉面预包装食品厂转型。随机抽签则讲究公平:我会考虑到报名者的实控主体,结合温湿度噪声这样的自然数值、股指人口这样的社会数值来产生随机数,最终选出中标企业。实际运行期间进行了几次黑客攻防,有一次差点败下阵来,心都提到嗓子眼了。

突然间我睁眼醒来。这里既不像医院,也不像居民楼。一位自称朋友的人不时来照顾我,我不排斥。原来这个时代量子保鲜已经稀松平常。他经常就送来量子保鲜食物。还有其他人来看望我。有人说我之前的状态让人担心,几次出现过轻生念头;有人说我是醉酒后爬进了那个机器,尽管我记得自己并不饮酒。有时我在窗边望到一个人感到好亲切,思来想去并不是家人亲戚——比我自己照镜子更感熟悉,尽管我跟他长相并不一样。倒是像明星,或者像我梦里出现过的。我朋友叮嘱我还要段时日才好外出。我只能每天读读书,看看窗边,会客时询问当今世界,独自一人就写写东西,一边期待着自由快活的日子到来。

(本文是小说,扩写自我的《假想量子保鲜食物》)

]]>
0 https://shansing.com/read/558/#comments https://shansing.com/feed/
让 Typecho 下标式超链接支持标题 https://shansing.com/read/557/ https://shansing.com/read/557/ Sun, 18 Jan 2026 00:07:30 +0000 闪闪的星 过了几年了我才发现原来 Typecho 不支持这种下标式超链接设置 title 属性:

[应该有 title 的链接][1] >-< [没 title 的链接][2]
  [1]: http://example.com "Example"
  [2]: http://example.com/2

这次偶然点了自己博客文章的链接才发现,这样会把可选的 title(即上面引号里的内容)当作链接一块解析。过程中会去除不安全的特殊符号,最终会变成指向 http://example.comexample 之类的超链接。搜了一下网上和 issue 没有人提,确定了 Markdown 是有这种语法,不想一个个改文章链接,遂自己从源码层面解决。

最开始按照我转载的《Typecho 中使 Markdown 文章的超链接在新窗口打开》,使劲改 var/CommonMark/HtmlRenderer.php,发现没用。一度以为是 Opcache 缓存原因,直到查看 GitHub 上的源码,后面我才意识到新版本 Typecho 渲染 Markdown 已经不在这了。用 IDE 搜了一下很快确定位置在 var/Utils/HyperDown.php

瞄了几眼知道几件事。图像是能解析标题的,甚至行内的超链接也能解析标题(正则表达式我让 LLM AI 读的),但下标式的就没实现。貌似 _definitions 是用来存储下标文本的数组。不过这个下标文本经过了 cleanUrl 处理。于是另外加一个变量存储原始值;后来又看到 _definitions 拢共没几处调用,索性在原函数上改,在调用的地方手动包裹 cleanUrl。仿照行内超链接的写法加一个处理标题的逻辑。如此这般向官方提交了一个 pull request

在合并之前如果你也想要这个逻辑,可以按照我的 commit 去改。

出于好奇搜了一下,我发现 GitHub 竟然能提供 .patch 和 .diff 文件,只需要在 commit url 后加对应的后缀名就能访问到,算是一个隐藏功能。于是也可以下载 commit 为 .patch 文件,然后进入 Typecho 目录用 patch 命令修补:

patch -p0 ./var/Utils/HyperDown.php < /your/d02bf06074e0fb7bbc89699b1bc2fb7a8c0317ed.patch

如果你网络环境不佳,我在这里直接贴出 patch 文件内容:

From d02bf06074e0fb7bbc89699b1bc2fb7a8c0317ed Mon Sep 17 00:00:00 2001
From: shansing <[email protected]>
Date: Wed, 14 Jan 2026 20:40:15 +0800
Subject: [PATCH] fix: optional title attribute in Markdown reference-style
 links

---
 var/Utils/HyperDown.php | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/var/Utils/HyperDown.php b/var/Utils/HyperDown.php
index b446bc617b..5d03e72e72 100644
--- a/var/Utils/HyperDown.php
+++ b/var/Utils/HyperDown.php
@@ -506,7 +506,7 @@ function ($matches) {
                 $escaped = htmlspecialchars($this->escapeBracket($matches[1]));
 
                 $result = isset($this->_definitions[$matches[2]]) ?
-                    "<img src=\"{$this->_definitions[$matches[2]]}\" alt=\"{$escaped}\" title=\"{$escaped}\">"
+                    "<img src=\"{$this->cleanUrl($this->_definitions[$matches[2]])}\" alt=\"{$escaped}\" title=\"{$escaped}\">"
                     : $escaped;
 
                 return $this->makeHolder($result);
@@ -536,10 +536,12 @@ function ($matches) {
                 $escaped = $this->parseInline(
                     $this->escapeBracket($matches[1]), '', false
                 );
-                $result = isset($this->_definitions[$matches[2]]) ?
-                    "<a href=\"{$this->_definitions[$matches[2]]}\">{$escaped}</a>"
-                    : $escaped;
-
+                $result = $escaped;
+                if (isset($this->_definitions[$matches[2]])) {
+                    [$url, $title] = $this->cleanUrl($this->_definitions[$matches[2]], true);
+                    $title = empty($title) ? '' : " title=\"{$title}\"";
+                    $result = "<a href=\"{$url}\"{$title}>{$escaped}</a>";
+                }
                 return $this->makeHolder($result);
             },
             $text
@@ -992,7 +994,7 @@ private function parseBlockFootnote(?array $block, int $key, string $line): bool
     private function parseBlockDefinition(?array $block, int $key, string $line): bool
     {
         if (preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line, $matches)) {
-            $this->_definitions[$matches[1]] = $this->cleanUrl($matches[2]);
+            $this->_definitions[$matches[1]] = $matches[2];
             $this->startBlock('definition', $key)
                 ->endBlock();

从稳定版到我提交 PR 的这段期间这个文件几乎没怎么变动,实测对于最新正式稳定版(v1.2.1)也能正常修补,不一定需要是开发版。用 patch 命令不会去校验 git 生成的文件摘要。

]]>
0 https://shansing.com/read/557/#comments https://shansing.com/feed/
我的 Windows 11 装机必做 https://shansing.com/read/555/ https://shansing.com/read/555/ Thu, 15 Jan 2026 23:05:24 +0000 闪闪的星 既然拾起这个博客了,不一会就想到不少要写的。其中就是我应该好好整理我的经验跟 SOP(Standard Operating Procedure,可以理解为标准流程),不只是让它们分散躺在我的记事本甚至 IM 收藏。说不定茫茫人海、长尾市场有人同样需要呢。这次就先描述一下我拿到手全新 Windows 11 会做的事。

部分操作可能 Windows 10 也适用。我没有实际测试。基本上我现在只用 Windows 11 x86_64 跟 arm64。

恢复 Windows 10 右键菜单

Windows 11 菜单既不华丽也不实用,切换为旧菜单更快更高效。网上随便一搜就有方法。打开 cmd 运行下列语句:

reg add HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32 /f /ve

然后重启系统或资源管理器。

Win11Debloat

有开源项目可以快速移除多余预装应用、禁用遥测,我用的一个叫 Raphire/Win11Debloat(原来好像叫 win10debloat)。有 PowerShell 一键下载运行语句。但考虑到国内网络原因,通常我会手动下载,解压运行 Run.bat。

一般用默认模式就行。

FaithType

Windows 发展到现在字体渲染有长进但不多。默认情况下汉字歪歪扭扭,甚至不在同一水平线上。即使是 4k 显示器,在大多数缩放比例下也比较难看。一些人会选择 MacType,但我不喜欢多运行一个后台程序,听说还有兼容问题。可以对字体文件本身进行修改。

通常把这个过程叫作移除 hinting。Windows 这种歪扭渲染实际是一种有意加进去适配低分屏的特性,但对我来说不需要。我先前用的是一个叫 GaspHack_Mod 的方案,现在则使用 FaithType,貌似能更彻底去除点阵字。实测效果很好。哪怕我的设备只有 1080 分辨率也会使用。

跟官方 README 建议的不同,我会选择手动处理字体后,进入高级启动命令行或者 PE 系统,将字体覆盖到原处即 C:\Windows\Fonts\。这样可能破坏系统完整性,但自我感觉兼容性更好。我也会在替换字体生效后重新调整一遍 ClearType。

Windhawk

任务栏图标合并对我来说比较低效。比如开很多个 Jetbrains IDEA 窗口要多点一遍切换。Windows 11 后期加入的不合并选项却要强制显示窗口标题。我希望保持只显示图标,同时不合并窗口。

我现在使用 Windhawk。对应的 mod 名称为“Disable grouping on the taskbar”,禁止任务栏图标分组,同时可以选择固定项的行为。还有一些可以强制开始菜单显示全部应用(Show all apps by default in start menu)。现在 Windows on arm 也可以使用,虽然 UI 是 x86 架构但是只要不实际点开就不怎么影响性能。

任务栏不合并的效果

跟 ExplorerPatcher 一类的工具一样,安装以后建议不要快速追随 Windows 更新。更新以后它们可能需要下载新符号文件,而微软可能会在更新发布若干小时之后才发布符号文件。

Windows 10 用户可以考虑 7+ Taskbar Tweaker。这是我先前用过很久且赞助过的工具,不过它不开源。

禁止商店自动更新

Windows 11 近期强制 Windows 商店自动更新应用,好在可以通过注册表禁用

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\WindowsStore]
"AutoDownload"=dword:00000002

貌似改动的是组策略。家庭版用户使用可能不合许可。

移除 Copilot

如题,没什么好说的。可能 Win11Debloat 遗漏了或者自动更新安装上了。用管理员权限打开 PowerShell 执行:

Get-AppxPackage *CoPilot* -AllUsers | Remove-AppPackage -AllUsers

调整 CPU 调度

我会用 PowerSettingsExplorer 解除一些隐藏的电源选项。比如生效的异类策略、异类线程调度策略、异类短运行线程调度策略,这些跟大小核调度有关。这个工具同样可以现场修改设置,如果觉得太复杂就只解除隐藏然后自行在高级电源选项配置。

最近还有一个选项我会调整:处理器性能核心放置最小核心数量。接通电源我会拉到 100%。都市传说这个也影响处理器性能,不要让它过度休息醒不回来。

修复开机程序启动延迟

又是一个我相信一定有道理但看表象不能理解的 Windows 机制。开机启动以后自启程序会慢慢打开。我的硬件配置并不弱还能感受到延迟。直到看到一篇 v2ex 贴子,上注册表:

Windows Registry Editor Version 5.00
        
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize]
"StartupDelayInMSec"=dword:00000000
"WaitForIdleState"=dword:00000000

实际感受确实快不少。

]]>
0 https://shansing.com/read/555/#comments https://shansing.com/feed/
借助 WSL 访问 IP 地址时转向另一 IP https://shansing.com/read/554/ https://shansing.com/read/554/ Wed, 14 Jan 2026 11:56:38 +0000 闪闪的星 工作中我有这么一种需求,想要将对某些 IP 地址的访问重定向到另外的 IP 地址,至少要包括 TCP 流量。实际场景大概是,云服务器之间能够用他们自身的 IP 地址访问,但是办公网访问这些服务需要使用 EIP(应该代表弹性 IP),偶尔我需要本地修改为测试环境的 namespace,直连云上的数据库和微服务。配置中心自然会返回给我一堆 IP 而不是 EIP,我希望不改动他们只在我本地将请求映射到 EIP。

如果是域名倒是好办,只需要简单修改 hosts 文件。这是也 LLM AI 给我的方案之一。但我不指望这些客户端蠢到 IP 地址本身(而不是域名)还需要另外解析为 IP 地址。另一个似乎是修改网卡的网关还是路由规则,看上去也不可行,没有一台对端机器来处理这些路由。可能 Windows 的网络底层不是很自由,所以我问 Linux 是不是简单很多(我印象中用 iptables 的 NAT 规则做过转发)。LLM AI 表示确实如此。

于是有一个虽然曲折但是能用的方案就出现了。我的 Windows 机器有 WSL 2 环境,也有 Proxifier 这样强制走代理的软件。那么我只需要:

  1. 先配置好 WSL 下的 IP 转发规则;
  2. 在 WSL 搭建一个代理服务器;
  3. 在 Windows 上让需要的程序走上述代理。

废话少说。鉴于新版 Debian 默认搭载 nftables 而非 iptables,我让 LLM AI 直接给出 nft 命令。然后加上启动代理服务器的命令:

sudo nft add table nat
sudo nft 'add chain nat prerouting { type nat hook prerouting priority 0; }'
sudo nft 'add chain nat output { type nat hook output priority 0; }'
# 修改下面的 IP
sudo nft add rule nat output ip daddr 10.1.0.1 dnat to 10.2.0.1
sudo nft add rule nat output ip daddr 10.1.0.2 dnat to 10.2.0.2
microsocks -p 1081

这段命令代表访问时 10.1.0.1 去访问 10.2.0.1,访问 10.1.0.2 转而访问 10.2.0.2,在 1081 端口暴露出 socks5 代理。

注意仅 WSL 2 能正确处理 iptables 类规则,WSL 1 印象中不行。其中 microsocks 可以换成喜欢的代理程序和协议,这里我随便找了一个简单、单一功能且不会引发注意的。推荐在 WSL Settings 保持“启用 localhost 转发”,然后可以直接在宿主环境(Windows)下用 localhost:1081 访问到代理;如若不行可尝试将网络模式改为 Mirrored 并重启。

这里没有持久化 nft 规则,也没有设置开机启动。我是用时再复制粘贴运行。可根据需要自行定制。

在 Windows 环境中用 Proxifier 为 10.1.0.* 指定规则走上述代理就行。

鉴于 Proxifier 是付费软件。如果只需要单一或者很少的程序走代理,也可以查看他们自己有没有类似运行参数。如 Java 开发,JRE 就有 VM 参数可以让网络请求走 HTTP 代理:

-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=1082

但是不保证所有类库都遵循。实在不行也可以尝试某程序员常备软件的 TUN 模式。或者打开内核转发配置成路由网关。

]]>
0 https://shansing.com/read/554/#comments https://shansing.com/feed/
让 Snipaste 使用微信 OCR https://shansing.com/read/549/ https://shansing.com/read/549/ Mon, 12 Jan 2026 14:33:00 +0000 闪闪的星 千呼万唤,Snipaste 这款截图贴图软件终于支持 OCR 功能。OCR 可以识别图片上的文字,跟截图软件一起工作再合适不过了。在更新日志引用的这个 issue 下,我看到 Coxxs 提供了一个改进的 OCR 组件。原来对于 Windows,Snipaste 默认的系统内置选项会调用旧版的微软组件,准确率低、识别率慢。然而我使用以后觉得新版也差强人意,尝试看看能不急集成微信 PC 版的 OCR。

答案是可以。Coxxs 的项目实际利用了 Snipaste 的第二种方式:Tesseract 对接。从 Snipaste 界面可以看出其命令行参数默认是 stdin stdout。我实际下载了 Tesseract 使用,速度不快,结果有很多空格,差点意思。但是重要的是可以用帮助命令看出参数含义,结论是 Snipaste 将图片传给标准输入 stdin,OCR 文本结果传给标准输出 stdout。那么我只要写个程序在中间调用微信 OCR 就好了。

开发过程

一般来说这里面最棘手的过程是调用微信 OCR。但我就是有印象有人开发过,所以才有了对接到 Snipaste 的想法。这个项目是 swigger/wechat-ocr,同时支持微信 PC 版 3.x 和 4.x 的版本。官方提供 C++、Python、Java、C# 接口。

那么该选择什么样的技术栈做这个中间程序呢?我第一反应想到的是 aardio。我自己写 Windows 下的小的程序经常用,印象中 aardio 跟类 C 接口的互操作性也很好。但我决定还是不趟这浑水。我工作主要语言是 Java,但是写这种小东西带个运行时也太重。最终我选择有过少许开发经验的 golang,编译方便,制品体积小,也能操作 C 的接口。但我还是有点畏难,问了 AI 大模型似乎有点繁琐,好在前述项目有人写了 go 版本的 fanchenggang/wechat-ocr-go

开发中稍微别扭的地方是需要将图片存为文件。前述 wechat-ocr 项目只接受路径作为参数,不支持文件内容。据说 Windows 下也有命名管道之类的东西可以用,但我决定从简,将从标准输入读到的图片保存到临时目录,程序运行结束或报错时删除。

为了排查问题,将调试和报错信息也输出。但是这里不能输出到 stdout。稍微看了 Coxxs/sp-oneocr,应该是输出到标准错误 stderr,即使只是调试信息。那我也这样做。

该怎么调试呢?我真没想到原来 Windows 的 cmd 也有管道。(不要问我为什么不用 PowerShell。)跟 Linux 类似,用竖线 | 操作符即可。读取文件内容用 type。即:

chcp 65001
type "C:\your\image.png" | "C:\your\wechat-ocr-snipaste\wechat-ocr-snipaste.exe" stdin stdout

其中第一句是为了让命令行提示符正确显示 UTF-8 编码。实测 Snipaste 就是需要 UTF-8 输出。

如果长期运行的话,可以后台常驻,接收到 Snipaste 请求时通过通信机制传递给运行着的实例,这样不需要重复加载 DLL 之类的。但是现在这样用一次退一次的速度就不错。后台常驻的收益可能不明显,还可能不稳定

开源仓库:https://github.com/shansing/wechat-ocr-snipaste

使用方法

注意目前只有专业版的 Snipaste 才有 OCR 支持。另外需要确保操作系统是 64 位 x86 的 Windows。实测不支持 Arm64。

首先去 Releases 页面下载我编译好的 wechat-ocr-snipaste.exe,或者自行编译。建议放到一个单独的文件夹。

确保微信 PC 版安装好。需要复制 wxocr.dll(旧版微信)或者 wechatocr.exe(4.x 版微信)的全路径,包含文件名,我们把这个参数叫作 ocrBin。找到微信本体的安装路径,通常带版本号,在我这里是 mmmojo_64.dll 所在的目录,把这个参数叫作 wechatDir。这两个参数后面会用到。

ocrBin 参数示例:

  • C:\Users\name\AppData\Roaming\Tencent\WeChat\XPlugin\Plugins\WeChatOCR\7061\extracted\WeChatOCR.exe
  • C:\Users\name\AppData\Roaming\Tencent\xwechat\XPlugin\Plugins\WeChatOcr\8075\extracted\wxocr.dll

wechatDir 参数示例:

  • C:\Program Files\Tencent\WeChat[3.9.8.25]
  • C:\Program Files\Tencent\Weixin\4.1.5.30

也很推荐把这里的 extracted 目录复制出来,跟 wechat-ocr-snipaste.exe 放在一起,重命名为 ocrBin;将版本号文件夹复制放在一起命名为 wechat。这样稍后可以用上俩参数的默认值,不必显式指明。复制出来也可以减少微信抽风风控的风险。

接着下载 wcocr.dll。可以从 swigger/wechat-ocrfanchenggang/wechat-ocr-go 得到。跟 wechat-ocr-snipaste.exe 放到一起。

现在我们的目录结构是这样的:

推荐的目录结构

接下来配置 Snipaste。右击托盘图片进入“首选项”,选择“输出”-“文字识别”。选择 OCR 引擎为 Tesseract,将可执行文件更改为选择 wechat-ocr-snipaste.exe。参数填写 -ocrBin "..." -wechatDir "...",其中 ... 替换成上述两个参数实际的值,因为引号的缘故目录不要最后加反斜线 \;如果遵循我上面推荐做法使用默认值,那么这里参数保持为空即可,如图:

推荐的配置方式

然后截图使用测试一番。正常的话结果会复制到剪贴板。

第一次使用如果不成功,可以用上一节的命令行排查一下,查看报错信息。有问题可以下面留言。

这里之所以需要自行下载这么多文件,是因为如作者说“打包他人软件是侵犯知识产权的违法行为”,所见略同。而我觉得封装 API 好像也涉及逆向有点可疑,所以这个 dll 也请你们自己下载啦。

实测效果不错。速度快,准略率又高。跟系统内置(哪怕高版本)相比抗干扰能力也强,比如不太受屏幕水印影响。

]]>
1 https://shansing.com/read/549/#comments https://shansing.com/feed/
一个阿里云通义千问工单 https://shansing.com/read/543/ https://shansing.com/read/543/ Mon, 22 Jul 2024 13:03:00 +0000 闪闪的星 这篇博客记录我在阿里云提交的一次工单,产品是通义千问,问题点是 OpenAI 兼容接口的函数调用(function call)。界面上所选产品分类是“大模型服务平台百炼”。因为早有耳闻网友吐槽阿里云客服,我提工单后,一开始回复就令我摸不着头脑、疑似 AI 生成、毫无帮助,即刻印证我的刻板印象,而最后以创建钉钉群结束,自认为有一定参考价值,值得记录。因为问题最终得到解决,我在过程中间耐心也不多,加之我的业务不重要(不是公司业务),所以特别声明我不持批评态度,而更多以现象学的宗旨提供客观文本,悬置其中的立场和争议。各位可以自行作出判断。

首先我将工单对话按原样贴上,之后给出附件内容,最后以结语总结钉钉群内容。

工单内容

我:(附件:aliyun请求响应体.txt)

qwen-max 模型,通过 openai 兼容格式调用,使用 tool 的 function call,能调用成功,但没有响应 function call 的 ID。见附件,响应的 choices[0].message.tool_calls[0].id 是空字符串。后续调用方这边后续进一步提交函数调用结果,传入相应 ID(空字符串),模型不能正确识别,导致回报通用内容(如“What would you like to know or discuss? I'm here to help with any questions you might have!”)。
此问题于昨日发现,今日仍然存在。之前是好的。

售后工程师:2024-06-05 08:02:44

已收到您提交的问题

售后工程师:

您好,关于您遇到的问题,看起来是Function Call的ID没有正确返回,这可能导致后续工具调用的整合出现错误。通常,tool_calls字段应该包含调用工具的相关信息,包括ID,以便于后续处理。由于您提到之前功能正常,现在出现异常,可能是由于服务的临时性问题或者更新导致的。

建议您检查以下几点:

确认调用的API版本是否最新,是否有更新导致的不兼容性。
检查请求的参数是否完整,特别是与tool_calls相关的部分。
如果可能,尝试使用不同的工具ID或者更新工具描述看看是否有所改善。
如果问题持续存在,您可以尝试联系阿里云的技术支持,提供具体的调用日志和错误信息,以便他们能更深入地调查。在问题解决之前,可能需要通过不依赖ID的临时解决方案来整合工具输出和模型回答。

我:

这是 AI 回复吗?我不是在联系技术支持是在联系什么?API 不返回 function call 的 ID 我这边也不能编造一个呀。

售后工程师:

您好,您的问题我们正在查询确认,需要一些时间,请您稍等。

售后工程师:

您好,麻烦您提供一下您的代码

我:

关键的请求、响应体已经在附件里。URL 就是官方文档上写的 openai 兼容地址,加上凭据就能复现我的情况了。

售后工程师:

您好,您的问题我已反馈给后端,有进展我会第一时间同步给您,请您耐心等待,感谢

售后工程师:

您好,麻烦您提供一下requestId

我:

是指响应体返回的 id 吗?响应体也附在附件了,看最后。我手机不方便复制。

售后工程师:

好的

售后工程师:

您好,您的问题我已反馈给后端,有进展我会第一时间同步给您,请您耐心等待,感谢

售后工程师:

您好,这个id一直是个空值,这个也可从官方文档中看到的。

阿里云工单图1

我:(附件:aliyun两轮对话.md)

好的,那可能是我误会了。那么现在我的问题就变成,不知为何在第二轮中我的 function call response 没有被识别,模型返回了无知的通用回复。记得6月3日还是好的;现在用同一套代码可以请求 openai 获得正常返回(模型正确回答我的问题),应该能排除格式问题。在此我将两轮请求、响应的 body 都附上。

售后工程师:

请您稍等,您的问题我正在帮您核实

售后工程师:

抱歉,您这个响应我看不出来问题

我:

问题就是没有根据我的 function call response 回复问题。并且 usage 中 prompt_tokens 也显著低于正常时候。在我的报文中,prompt_tokens 是 8,显然是没有将我的 function call response 当作输入。所以要不就是我错了,要不就是你们逻辑错了。

售后工程师:

请您稍等,您的问题我正在帮您核实

我:

在正常情况下(比如3号的时候),模型应该回复“Firefox 127 正式版预计将在2024年6月11日发布”之类的话(正如 function call response 指示的)。现在回复 What would you like to know or discuss?,怕是不仅没有看到我的 function call response,结合 prompt_tokens=8,怕是连我最开始的问题都没看见。不然起码会说“根据我的搜索,无法直接找到关于 Firefox 127 版本的具体发布信息……”或者“我没有能力访问互联网”之类的话(像没有使用 function call 那样)。

售后工程师:

请您稍等,您的问题我正在帮您核实

售后工程师:

您好,tools暂时无法与stream=True同时使用。使用tools不要用流失增量输出

我:

但是我3号及之前还用的好好的。我理解tools不能与流式输出同时使用,是因为response不好返回tool_calls;但是调用方给function call response 的时候可以流式输出,因为这时 API 的 response 跟不用 tools 一样。你看我第一轮用 function call,非流式;第二轮请求把 tools 摘掉没传了,这时候是要支持流式的,支持流式跟文档不相悖。请你们再次确认。

售后工程师:

您好,您看一下
参考链接:https://help.aliyun.com/zh/dashscope/developer-reference/compatibility-of-openai-with-dashscope/?spm=a2c4g.11186623.0.i2

阿里云工单图2

我:

我知道,但我第二轮请求已经没有传入tools,这时候是需要支持流式输出的。另一个说明很可能你们模型内部出问题的佐证是,我刚全改成非流式请求,模型在第二轮仍然不能返回期望 content 回复。第二轮响应如下:
{"choices":[{"message":{"role":"assistant","content":"What would you like to know or discuss?"},"finish_reason":"stop","index":0,"logprobs":null}],"object":"chat.completion","usage":{"prompt_tokens":8,"completion_tokens":9,"total_tokens":17},"created":1717581073,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-1cddb35d-6a10-9f4d-83d9-2a1b31fba969"}

售后工程师:

那您这边试一下,简单的function试一下呢,看一下能否正常调用

我:

既然确认我的调用符合文档,不是应该你们编写用例缩小范围排查问题吗?你们不仔细看我的附件报文,我指出来了。你们说tools跟流式不能共用,我不仅从理论上说明我的请求不属于这种情况,还实际构造了用例。现在还要我接着当debug工程师么……

售后工程师:

您好,您这边不提供您的代码确实不方便排查问题哈,还是麻烦您提供一下您的代码

我:(附件:aliyun.js)

MVP 代码见附件

售后工程师:2024-06-05 20:39:31

您好,您的问题正在帮您确认中,可能还需要一些时间,还请您先耐心等待一下,一有结果我们会第一时间工单回复您的,对您造成的不便请见谅

售后工程师:2024-06-06 09:53:03

您好,我这边看一您的代码,应该是参数类型给错了,content是字符串入参,不能是一个数组。

阿里云工单图3

我:

在你标红框往上面看,if 上面,把 object 序列化为 json 字符串了

售后工程师:

您好,建议您用纯文本格式试一下。

我:

哈?但是很明显实际数据就会很复杂啊。还是同样的,如果确认我的调用符合文档(这里也确实是字符串),应该你们编写用例缩小范围排查问题。不是要我试这试那的。我试完了,纯文本没有问题,那你们要不要改?纯文本有问题,那你们要不要改?

售后工程师:

您好,请稍等,我这边本地测试一下。

售后工程师:

您好,我这边直接使用纯文本形式是可以的。

阿里云工单图4

我:

但你们要解决的问题是我的例子,这只能说你缩小了问题范围,请解决了问题再给出反馈;如果不需要解决就说明理由——你这只反馈一半进度啊,不需要我教做事吧。在这个例子中,如果你们确实兼容openai接口,那么就要支持json序列化后的字符串。因为这是 tools[].function.parameters.type = "object" 的含义,是由openai接口定义和JSON Schema共同决定的。我又不是要 type = "string",这样才是你说的纯文本。但结论是,不管根据规范还是业务实践,函数都需要返回 object,也就是说 function call response 的 content 要支持json序列化后的字符串。

售后工程师:2024-06-06 13:44:15(群号已脱敏)

您好,这个问题拉了一个钉钉群为您更快的解决问题,请您加一下钉钉群:“【客-临时】000ETC61UP 模型兼容openai使用functioncall调用问题”群的钉钉群号: 9344xxxxxxx

附件内容

aliyun请求响应体.txt

请求JSON:
{"messages":[{"role":"user","content":"你现在有能力联 网搜索、访问网页。请用英语关键词搜索,用中文回答我的问题,并给出参考来源:\nFirefox 127正式版预计什么时候发布"}],"stream":false,"model":"qwen-max","temperature":1,"presence_penalty":0,"frequency_penalty":0,"top_p":0.99,"max_tokens":2000,"tools":[{"type":"function","function":{"name":"searchWeb","description":"Perform a web search using specific keywords. (like Google search)","parameters":{"type":"object","properties":{"query":{"type":"string","description":"Keywords for the web search."}},"required":["query"]}}},{"type":"function","function":{"name":"searchNews","description":"Search for news articles using specific keywords. (like Google News)","parameters":{"type":"object","properties":{"query":{"type":"string","description":"Keywords for the news search."}},"required":["query"]}}},{"type":"function","function":{"name":"readWebPage","description":"Fetch and parse the content of the given URL's webpage. (like Reader View of Firefox)","parameters":{"type":"object","properties":{"url":{"type":"string","description":"The URL of the webpage."}},"required":["url"]}}},{"type":"function","function":{"name":"searchAndReadFirstResult","description":"Perform a web search and read the content of the first search result. (like I'm Feeling Lucky of Google)","parameters":{"type":"object","properties":{"query":{"type":"string","description":"Keywords for the web search."}},"required":["query"]}}}]}

响应JSON:
{"choices":[{"message":{"role":"assistant","tool_calls":[{"function":{"name":"searchWeb","arguments":"{\"query\": \"Firefox 127 release date\"}"},"id":"","type":"function"}],"content":""},"finish_reason":"tool_calls","index":0,"logprobs":null}],"object":"chat.completion","usage":{"prompt_tokens":479,"completion_tokens":23,"total_tokens":502},"created":1717545113,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-c6e58eae-f4d6-968a-b88b-2e324d1c6c38"}

aliyun两轮对话.md

# 第一次请求

{"messages":[{"role":"user","content":"你现在有能力联网搜索、访问网页。请用英语关键词搜索,用中文回答我的问题,并给出参考来源:\nFirefox 127 正式版预计什么时候发布?"}],"stream":false,"model":"qwen-max","temperature":1,"presence_penalty":0,"frequency_penalty":0,"top_p":0.99,"max_tokens":2000,"tools":[{"type":"function","function":{"name":"searchWeb","description":"Perform a web search using specific keywords. (like Google search)","parameters":{"type":"object","properties":{"query":{"type":"string","description":"Keywords for the web search."}},"required":["query"]}}},{"type":"function","function":{"name":"searchNews","description":"Search for news articles using specific keywords. (like Google News)","parameters":{"type":"object","properties":{"query":{"type":"string","description":"Keywords for the news search."}},"required":["query"]}}},{"type":"function","function":{"name":"readWebPage","description":"Fetch and parse the content of the given URL's webpage. (like Reader View of Firefox)","parameters":{"type":"object","properties":{"url":{"type":"string","description":"The URL of the webpage."}},"required":["url"]}}},{"type":"function","function":{"name":"searchAndReadFirstResult","description":"Perform a web search and read the content of the first search result. (like I'm Feeling Lucky of Google)","parameters":{"type":"object","properties":{"query":{"type":"string","description":"Keywords for the web search."}},"required":["query"]}}}]}

# 第一次响应

{"choices":[{"message":{"role":"assistant","tool_calls":[{"function":{"name":"searchWeb","arguments":"{\"query\": \"Firefox 127 release date\"}"},"id":"","type":"function"}],"content":""},"finish_reason":"tool_calls","index":0,"logprobs":null}],"object":"chat.completion","usage":{"prompt_tokens":482,"completion_tokens":23,"total_tokens":505},"created":1717570659,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-a2449d01-bc30-9613-bfa2-aec1f8ee7c01"}

# 第二次请求

{"messages":[{"role":"user","content":"你现在有能力联网搜索、访问网页。请用英语关键词搜索,用中文回答我的问题,并给出参考来源:\nFirefox 127 正式版预计什么时候发布?"},{"role":"assistant","tool_calls":[{"function":{"name":"searchWeb","arguments":"{\"query\": \"Firefox 127 release date\"}"},"id":"","type":"function"}],"content":""},{"tool_call_id":"","role":"tool","name":"searchWeb","content":"{\"allSearchResults\":[{\"title\":\"Firefox 127 for developers - Mozilla | MDN\",\"url\":\"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/127\",\"snippet\":\"5 days ago ... Firefox 127 is the current Beta version of Firefox and ships on June 11, 2024. Changes for web developers. Developer Tools. HTML. data: ...\"},{\"title\":\"Release Management/Release owners - MozillaWiki\",\"url\":\"https://wiki.mozilla.org/Release_Management/Release_owners\",\"snippet\":\"May 27, 2024 ... Firefox 127, Pascal (CET), RyanVM (EST), Haik Aftandilian (PST), bhearsum (EST), gbrown (MST), RelMan (ESR 115.12), 2024-06-11. Firefox 126 ...\"},{\"title\":\"Milestones and key data for Firefox 127\",\"url\":\"https://whattrainisitnow.com/release/?version=127\",\"snippet\":\"Release day! We ship Firefox 127 at 14:00 UTC (6AM PST) at 25% rollout. June 11. In 2 weeks. Planned dot ...\"},{\"title\":\"Firefox Beta and Developer Edition Release Notes\",\"url\":\"https://www.mozilla.org/en-US/firefox/127.0beta/releasenotes/\",\"snippet\":\"May 14, 2024 ... New · You can now set Firefox to automatically launch whenever you start or restart your Windows computer. · In Firefox 127, we completed work to ...\"},{\"title\":\"Firefox version history - Wikipedia\",\"url\":\"https://en.wikipedia.org/wiki/Firefox_version_history\",\"snippet\":\"Firefox was created by Dave Hyatt and Blake Ross as an experimental branch of the Mozilla browser, first released as Firefox 1.0 on November 9, 2004.\"},{\"title\":\"Update Firefox to the latest release | Firefox Help\",\"url\":\"https://support.mozilla.org/en-US/kb/update-firefox-latest-release\",\"snippet\":\"Customize this article. Firefox. Version 128, Version 127 ... Update Firefox to the latest release. Firefox ... Firefox, download the Firefox installer, then close ...\"},{\"title\":\"Upgraded add-on signatures required for Firefox 127 : r/firefox\",\"url\":\"https://www.reddit.com/r/firefox/comments/1c4y8ey/upgraded_addon_signatures_required_for_firefox_127/\",\"snippet\":\"Apr 15, 2024 ... ... Firefox 127 : r/firefox. &nbsp;. TOPICS ... date they were uploaded and whether they are ... Release (Jun 11). Add-ons installed prior to ...\"},{\"title\":\"Firefox users on Windows 7, 8 and 8.1 moving to Extended Support ...\",\"url\":\"https://support.mozilla.org/en-US/kb/firefox-users-windows-7-8-and-81-moving-extended-support\",\"snippet\":\"Firefox version 115 ESR will be the last supported release for Windows 7, 8 and 8.1.\"},{\"title\":\"Unable to connect to host 127.0.0.1 on port 7055.Selenium 3 ...\",\"url\":\"https://stackoverflow.com/questions/41116679/unable-to-connect-to-host-127-0-0-1-on-port-7055-selenium-3-firefox-38-6-0\",\"snippet\":\"Dec 13, 2016 ... 0. See releases here. If you look at the Firefox release date and the Selenium release date, you'll see you need something that was released ...\"},{\"title\":\"[Stable Update] 2023-08-11 - Kernels, Plasma, Nvidia, Firefox ...\",\"url\":\"https://forum.manjaro.org/t/stable-update-2023-08-11-kernels-plasma-nvidia-firefox-thunderbird-pamac-pipewire-mesa/146081/127\",\"snippet\":\"Aug 11, 2023 ... Hi,. In my case the up date don't solve the issue, with htop no 100% cpu use with mariadb or obs, but ksysguard show a 100% cpu use with obs ...\"}]}"}],"stream":true,"model":"qwen-max","temperature":1,"presence_penalty":0,"frequency_penalty":0,"top_p":0.99,"max_tokens":2000,"stream_options":{"include_usage":true}}

# 第二次响应

data: {"choices":[{"delta":{"content":"","role":"assistant"},"index":0,"logprobs":null,"finish_reason":null}],"object":"chat.completion.chunk","usage":null,"created":1717570662,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-b963bdd4-d08b-9458-ad0a-d71d38117a3c"}

data: {"choices":[{"finish_reason":null,"delta":{"content":"What"},"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1717570662,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-b963bdd4-d08b-9458-ad0a-d71d38117a3c"}

data: {"choices":[{"delta":{"content":" would"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1717570662,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-b963bdd4-d08b-9458-ad0a-d71d38117a3c"}

data: {"choices":[{"delta":{"content":" you"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1717570662,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-b963bdd4-d08b-9458-ad0a-d71d38117a3c"}

data: {"choices":[{"delta":{"content":" like to know or discuss"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1717570662,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-b963bdd4-d08b-9458-ad0a-d71d38117a3c"}

data: {"choices":[{"delta":{"content":"?"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1717570662,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-b963bdd4-d08b-9458-ad0a-d71d38117a3c"}

data: {"choices":[{"finish_reason":"stop","delta":{"content":""},"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1717570662,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-b963bdd4-d08b-9458-ad0a-d71d38117a3c"}

data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":8,"completion_tokens":9,"total_tokens":17},"created":1717570662,"system_fingerprint":null,"model":"qwen-max","id":"chatcmpl-b963bdd4-d08b-9458-ad0a-d71d38117a3c"}

data: [DONE]

# 问题

模型回答 `What would you like to know or discuss?`,似乎没有识别 "role":"tool" content 给的 function call 回复。记得6月3日还是好的。6月4日发现回复 `What would you like to know or discuss?` 之类的通用回答,回报 usage 也显著减少。

aliyun.js

const baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode'
const apiKey = '???????'

async function main() {
    const userMessage = "你现在有能力联网搜索、访问网页。请用英语关键词搜索,用中文回答我的问题,并给出参考来源:\n" +
        "Firefox 127正式版预计什么时候发布"

    const requestHeader = {
        "Content-Type": "application/json",
        "Cache-Control": "no-store",
        "Authorization": "Bearer " + apiKey,
    }
    const requestBody = {
        "messages": [
            createMessage("user", userMessage)
        ],
        "stream": false,
        "model": "qwen-max",
        "temperature": 1,
        "presence_penalty": 0,
        "frequency_penalty": 0,
        "top_p": 0.99,
        "max_tokens": 2000,
        "tools": tools
    }
    const messages = requestBody.messages
    console.log('[openai]发送第一轮请求……requestBody', JSON.stringify(requestBody));
    const response = await fetch(`${baseUrl}/v1/chat/completions`, {
        method: 'POST',
        headers: requestHeader,
        body: JSON.stringify(requestBody)
    });

    let responseJson = await response.json();
    // console.log('[openai]确认解析后的 data 对象:', data);
    if (!responseJson) {
        throw Error('[openai]no response');
    }
    messages.push(responseJson.choices[0].message);
    // console.log('[openai]更新后的 messages 数组:', messages);
    // 检查是否有函数调用
    console.log('[openai]开始检查是否有函数调用');
    const availableFunctions = {
        "searchWeb": search,
    };
    let calledCustomFunction = false;
    if (responseJson.choices[0].message.tool_calls) {
        const toolCalls = responseJson.choices[0].message.tool_calls;
        const unprocessedMessages = JSON.parse(JSON.stringify(messages));
        for (const toolCall of toolCalls) {
            const functionName = toolCall.function.name;
            const functionToCall = availableFunctions[functionName];
            const functionArgs = JSON.parse(toolCall.function.arguments);
            let functionResponse;
            if (functionName === 'searchWeb') {
                functionResponse = await functionToCall(functionArgs.query);
            }
            functionResponse = JSON.stringify(functionResponse)
            if (functionResponse != null) {
                messages.push({
                    tool_call_id: toolCall.id,
                    role: "tool",
                    name: functionName,
                    content: functionResponse,
                });
                calledCustomFunction = true;
            }
        }
    }
    console.log('[openai]function call checked');

    if (!calledCustomFunction) {
        // 没有调用自定义函数,直接返回原始回复
        throw Error("这不是要测试的情况")
    } else {
        // 如果调用了自定义函数,再次向 API 发送请求
        console.log('[openai]function call needed, sending request again');
        const secondRequestBody = {
            ...requestBody,
            //这里即使改成流式、非流式,回答都不对
            // stream: true,
            // stream_options: {
            //     include_usage: true,
            // },
            //摘除tools
            tools: undefined,
            tool_choice: undefined,
            messages: messages,
        };
        try {
            console.log('[openai]sending second response...');
            console.log('[sending]secondRequestBody', JSON.stringify(secondRequestBody));
            let secondResponse = await fetch(`${baseUrl}/v1/chat/completions`, {
                method: 'POST',
                headers: requestHeader,
                body: JSON.stringify(secondRequestBody)
            });
            //强制流式

            // return {
            //     status: secondResponse.status,
            //     headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache',...corsHeaders,
            //     },
            //     body: secondResponse.body
            // };
            console.log("第二轮 secondResponse", JSON.stringify(await secondResponse.json()));
        } catch (error) {
            throw Error('[openai]API second failed:' + error.message);
        }
    }


}
function createMessage(role, content) {
    return {role, content}
}
const tools = [
    {
        type: "function",
        function: {
            name: "searchWeb",
            description: "Perform a web search using specific keywords. (like Google search)",
            parameters: {
                type: "object",
                properties: {
                    query: { type: "string","description": "Keywords for the web search."}
                },
                required: ["query"]
            }
        }
    }
]

function search(query) {
    //mock
    return {
        "allSearchResults": [
        {
            "title": "Firefox 127 for developers - Mozilla | MDN",
            "url": "https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/127",
            "snippet": "5 days ago ... Firefox 127 is the current Beta version of Firefox and ships on June 11, 2024. Changes for web developers. Developer Tools. HTML. data: ..."
        },
        {
            "title": "Release Management/Release owners - MozillaWiki",
            "url": "https://wiki.mozilla.org/Release_Management/Release_owners",
            "snippet": "May 27, 2024 ... Firefox 127, Pascal (CET), RyanVM (EST), Haik Aftandilian (PST), bhearsum (EST), gbrown (MST), RelMan (ESR 115.12), 2024-06-11. Firefox 126 ..."
        },
        {
            "title": "Milestones and key data for Firefox 127",
            "url": "https://whattrainisitnow.com/release/?version=127",
            "snippet": "Release day! We ship Firefox 127 at 14:00 UTC (6AM PST) at 25% rollout. June 11. In 2 weeks. Planned dot ..."
        },
        {
            "title": "Firefox Beta and Developer Edition Release Notes",
            "url": "https://www.mozilla.org/en-US/firefox/127.0beta/releasenotes/",
            "snippet": "May 14, 2024 ... New · You can now set Firefox to automatically launch whenever you start or restart your Windows computer. · In Firefox 127, we completed work to ..."
        },
        {
            "title": "Firefox version history - Wikipedia",
            "url": "https://en.wikipedia.org/wiki/Firefox_version_history",
            "snippet": "Firefox was created by Dave Hyatt and Blake Ross as an experimental branch of the Mozilla browser, first released as Firefox 1.0 on November 9, 2004."
        },
        {
            "title": "Update Firefox to the latest release | Firefox Help",
            "url": "https://support.mozilla.org/en-US/kb/update-firefox-latest-release",
            "snippet": "Customize this article. Firefox. Version 128, Version 127 ... Update Firefox to the latest release. Firefox ... Firefox, download the Firefox installer, then close ..."
        },
        {
            "title": "Upgraded add-on signatures required for Firefox 127 : r/firefox",
            "url": "https://www.reddit.com/r/firefox/comments/1c4y8ey/upgraded_addon_signatures_required_for_firefox_127/",
            "snippet": "Apr 15, 2024 ... ... Firefox 127 : r/firefox. &nbsp;. TOPICS ... date they were uploaded and whether they are ... Release (Jun 11). Add-ons installed prior to ..."
        },
        {
            "title": "Firefox users on Windows 7, 8 and 8.1 moving to Extended Support ...",
            "url": "https://support.mozilla.org/en-US/kb/firefox-users-windows-7-8-and-81-moving-extended-support",
            "snippet": "Firefox version 115 ESR will be the last supported release for Windows 7, 8 and 8.1."
        },
        {
            "title": "Unable to connect to host 127.0.0.1 on port 7055.Selenium 3 ...",
            "url": "https://stackoverflow.com/questions/41116679/unable-to-connect-to-host-127-0-0-1-on-port-7055-selenium-3-firefox-38-6-0",
            "snippet": "Dec 13, 2016 ... 0. See releases here. If you look at the Firefox release date and the Selenium release date, you'll see you need something that was released ..."
        },
        {
            "title": "[Stable Update] 2023-08-11 - Kernels, Plasma, Nvidia, Firefox ...",
            "url": "https://forum.manjaro.org/t/stable-update-2023-08-11-kernels-plasma-nvidia-firefox-thunderbird-pamac-pipewire-mesa/146081/127",
            "snippet": "Aug 11, 2023 ... Hi,. In my case the up date don't solve the issue, with htop no 100% cpu use with mariadb or obs, but ksysguard show a 100% cpu use with obs ..."
        }
    ]
    }
}

main();

结语

在钉钉群中,售后工程师拉上相关人员一起排查问题。我首先小结了问题及当前进展:

# 我遇到的问题
qwen-max 模型,通过 openai 兼容接口调用,使用 tool 的 function call,能调用成功,但是模型给出的回答显然不正常,没有回答我的问题。6月4号发现,记得6月3号是好的。6月5号提工单。现在用同一套代码可以请求 openai(dot)com 获得正常返回(模型正确回答我的问题),应该能排除格式问题。两轮请求、响应的 body 见于 aliyun两轮对话.txt
# 我的推测
模型给出的回复是 What would you like to know or discuss? 一类貌似无知的通用回复。在正常情况下(比如3号的时候),模型应该回复“Firefox 127 正式版预计将在2024年6月11日发布”之类的话(正如 function call response 指示的)。并且 usage 中 prompt_tokens 也显著低于正常时候。在我的报文中,prompt_tokens 是 8,显然是没有将我的 function call response 当作输入。甚至于,很可能没有“看见”我的第一条文本输入,不然起码会说“根据我的搜索,无法直接找到关于 Firefox 127 版本的具体发布信息……”或者“我没有能力访问互联网”之类的话(像没有使用 function call 那样)。
# 解决进展
最开始我怀疑是因为 API 没返回 function call 的 ID,但被售后工程师用文档指出 ID 就是空字符串,排除。
接着工程师指出“tools暂时无法与stream=True同时使用。使用tools不要用流失增量输出”。我表示3号及之前还正常。表达理解tools不能与流式输出同时使用,是因为response不好返回tool_calls;但是调用方给function call response 的时候可以流式输出,因为这时 API 的 response 跟不用 tools 一样。我第一轮用 function call,非流式;第二轮请求把 tools 摘掉没传,这时候是要支持流式的,支持流式跟文档不相悖。并且构造了一个反例,两次都使用非流式请求,同样有回答不正常的问题。
最新进展是工程师使用简单的“纯文本形式”表示没有问题,怀疑是 function call response 的 content 是 JSON 序列化字符串导致。(过程中我提供的代码见 aliyun.js)
# 我为什么仍然认为这是问题
如果阿里云确实兼容openai接口,那么就要支持json序列化后的字符串。因为这是 tools[].function.parameters.type = "object" 的含义,是由openai接口定义和JSON Schema共同决定的。我又不是要 type = "string",这样才是所谓的纯文本。但结论是,不管根据规范还是业务实践,函数都有需要返回 object,也就是说 function call response 的 content 要支持json序列化后的字符串。

群内有人询问,是否尝试过第二轮请求带上 tools 参数。几轮对话后,我尝试如此操作,结果恢复正常。尽管我翻看代码提交记录,这一块地方问题发生前后没有改动,当时请求 openai 官方接口也无问题,我修改了我的逻辑,于是问题解决。尽管从我的角度,逻辑并不等价,只是考虑到阿里云的 openai 兼容或许是大致格式兼容,优先保证能用。(从截图上看,变更后的行为也符合文档,所以无可厚非。)此时是 7 月 6 号 19 时。

“结”外生枝

本来这篇博客就这么完结,甚至我可能不会整理发表。然而几天前(7 月 17 日),我发现接口又报错了:

{
    "error": {
        "code": "invalid_type",
        "param": "messages[0].content",
        "message": "Invalid type for 'messages[0].content': expected one of a string or array of objects, but got an object instead.",
        "type": "invalid_request_error"
    }
}

而回看我的请求,对应内容是什么呢:

        {
            "tool_call_id": "",
            "role": "tool",
            "name": "readWebPage",
            "content": "{\"title\":\"The Vision for Space Exploration\",\"url\":\"https://www.nasa.gov/wp-content/uploads/2023/01/55583main_vision_space_exploration2.pdf\",\"content\":\"# National Aeronautics and Space Administration \\n\\n# The Vision for Space Exploration February 2004 “This cause of exploration and discovery is not an option we choose; it is a desire written in the human heart.” \\n\\n## President George W. Bush February 4, 2003 \\n\\n# “We leave as we came, and God willing as we shall return, with peace and hope for all mankind.” \\n\\n## Eugene Cernan (Commander of last Apollo mission) December 17, 1972 \\n\\n# “. . . America will make those words come true.” \\n\\n## President George W. Bush January 14, 2004 Message from the NASA Administrator \\n\\nDear Reader, With last year’s budget, NASA released a new Strategic Plan outlining a new approach to space exploration using a “building block” strategy to explore scientifically valuable destinations across our solar system. At the same time that we released the Strategic Plan, our Nation and the NASA family also suffered the loss of the seven brave astronauts aboard the Space Shuttle Columbia . The report of the Columbia Accident Investigation Board emphasized the need for a clearer direction from which to drive NASA’s human exploration agenda. On January 14, 2004, the President articulated a new vision for space exploration. You hold in your hands a new, bolder framework for exploring our solar system that builds upon the policy that was announced by the President after months of careful deliberations within the Administration. This plan does not undertake exploration merely for the sake of adventure, however exciting that may be, but seeks answers to profound scientific and philosophical questions, responds to recent discoveries, will put in place revolutionary technologies and capabilities for the future, and will genuinely inspire our Nation, the world, and the next generation. Our aim is to explore in a sustainable, affordable, and flexible manner. We believe the principles and roadmap set down in this document will stand the test of time. Its details will be subject to revision and expansion as new discoveries are made, new technologies are applied, and new challenges are met and over-come. This plan is guided by the Administration’s new space exploration policy, “A Renewed Spirit of Discovery: The President’s Vision for U.S. Space Exploration,” a copy of which is provided on the follow-ing pages. NASA is releasing this plan simultaneously with NASA’s FY 2005 Budget Justification. This plan is fiscally responsible, consistent with the Administration’s goal of cutting the budget deficit in half within the next five years. I cannot overstate how much NASA will change in the coming years as this plan is implemented. I also can-not overstate how profound the rewards will be on this new course. With the support of Congress, the sci-ence community, the NASA civil and contractor workforce, and most importantly, the American public, we will embark on this very exciting future. When Christopher Columbus made his voyages across the Atlantic in the 15th and 16th centuries, his ships carried the inscription “Following the light of the sun, we left the Old World.” I look forward to joining you as we follow the light of the planets and the stars into the new worlds of the 21st century. Sean O'Keefe Administrator Top image: The Mars Exploration Rover Opportunity ’s view of Mars’ Meridiani Planum, taken with the panoramic camera. Lower image: \\n\\nOpportunity looks back at its empty lander as it begins to explore Meridiani Planum. A Renewed Spirit of Discovery: The President’s Vision for U.S. Space Exploration Background \\n\\nFrom the Apollo landings on the Moon, to robotic surveys of the Sun and the planets, to the compelling images captured by advanced space telescopes, U.S. achievements in space have revolutionized humanity’s view of the universe and have inspired Americans and people around the world. These achievements also have led to the development of technologies that have widespread applications to address problems on Earth. As the world enters the second century of powered flight, it is time to articulate a new vision that will define and guide U.S. space exploration activities for the next several decades. Today, humanity has the potential to seek answers to the most fundamental questions posed about the exis-tence of life beyond Earth. Telescopes have found planets around other stars. Robotic probes have identified potential resources on the Moon, and evidence of water -- a key ingredient for life -- has been found on Mars and the moons of Jupiter. Direct human experience in space has fundamentally altered our perspective of humanity and our place in the universe. Humans have the ability to respond to the unexpected developments inherent in space travel and possess unique skills that enhance discoveries. Just as Mercury, Gemini, and Apollo challenged a gen-eration of Americans, a renewed U.S. space exploration program with a significant human component can inspire us -- and our youth -- to greater achievements on Earth and in space. The loss of Space Shuttles Challenger and Columbia and their crews are a stark reminder of the inherent risks of space flight and the severity of the challenges posed by space exploration. In preparation for future human exploration, we must advance our ability to live and work safely in space and, at the same time, develop the technologies to extend humanity’s reach to the Moon, Mars, and beyond. The new technologies required for further space exploration also will improve the Nation’s other space activities and may provide applications that could be used to address problems on Earth. Like the explorers of the past and the pioneers of flight in the last century, we cannot today identify all that we will gain from space exploration; we are confident, nonetheless, that the eventual return will be great. Like their efforts, the success of future U.S. space exploration will unfold over generations. \\n\\n# Goal and Objectives \\n\\nThe fundamental goal of this vision is to advance U.S. scientific, security, and economic interests through a robust space exploration program. In support of this goal, the United States will: • Implement a sustained and affordable human and robotic program to explore the solar system and beyond; • Extend human presence across the solar system, starting with a human return to the Moon by the year 2020, in preparation for human exploration of Mars and other destinations; • Develop the innovative technologies, knowledge, and infrastructures both to explore and to support decisions about the destinations for human exploration; and • Promote international and commercial participation in exploration to further U.S. scientific, security, and economic interests. Bringing the Vision to Reality \\n\\nThe Administrator of the National Aeronautics and Space Administration will be responsible for the plans, programs, and activities required to implement this vision, in coordination with other agencies, as deemed appropriate. The Administrator will plan and implement an integrated, long-term robotic and human explo-ration program structured with measurable milestones and executed on the basis of available resources, accu-mulated experience, and technology readiness. To implement this vision, the Administrator will conduct the following activities and take other actions as required: \\n\\nA. Exploration Activities in Low Earth Orbit Space Shuttle \\n\\n• Return the Space Shuttle to flight as soon as practical, based on the recommendations of the Columbia Accident Investigation Board; • Focus use of the Space Shuttle to complete assembly of the International Space Station; and • Retire the Space Shuttle as soon as assembly of the International Space Station is completed, planned for the end of this decade; \\n\\nInternational Space Station \\n\\n• Complete assembly of the International Space Station, including the U.S. components that support U.S. space exploration goals and those provided by foreign partners, planned for the end of this decade; • Focus U.S. research and use of the International Space Station on supporting space exploration goals, with emphasis on understanding how the space environment affects astronaut health and capabilities and developing countermeasures; and • Conduct International Space Station activities in a manner consistent with U.S. obligations contained in the agreements between the United States and other partners in the International Space Station. \\n\\nB. Space Exploration Beyond Low Earth Orbit The Moon \\n\\n• Undertake lunar exploration activities to enable sustained human and robotic exploration of Mars and more distant destinations in the solar system; • Starting no later than 2008, initiate a series of robotic missions to the Moon to prepare for and support future human exploration activities; • Conduct the first extended human expedition to the lunar surface as early as 2015, but no later than the year 2020; and • Use lunar exploration activities to further science, and to develop and test new approaches, technologies, and systems, including use of lunar and other space resources, to support sustained human space exploration to Mars and other destinations. Mars and Other Destinations \\n\\n• Conduct robotic exploration of Mars to search for evidence of life, to understand the history of the solar system, and to prepare for future human exploration; • Conduct robotic exploration across the solar system for scientific purposes and to support human exploration. In particular, explore Jupiter’s moons, asteroids and other bodies to search for evidence of life, to understand the history of the solar system, and to search for resources; • Conduct advanced telescope searches for Earth-like planets and habitable environments around other stars; • Develop and demonstrate power generation, propulsion, life support, and other key capabilities required to support more distant, more capable, and/or longer duration human and robotic exploration of Mars and other destinations; and • Conduct human expeditions to Mars after acquiring adequate knowledge about the planet using robotic missions and after successfully demonstrating sustained human exploration missions to the Moon. \\n\\nC. Space Transportation Capabilities Supporting Exploration \\n\\n• Develop a new crew exploration vehicle to provide crew transportation for missions beyond low Earth orbit; « Conduct the initial test flight before the end of this decade in order to provide an operational capability to support human exploration missions no later than 2014; • Separate to the maximum practical extent crew from cargo transportation to the International Space Station and for launching exploration missions beyond low Earth orbit; « Acquire cargo transportation as soon as practical and affordable to support missions to and from the International Space Station; and « Acquire crew transportation to and from the International Space Station, as required, after the Space Shuttle is retired from service. \\n\\nD. International and Commercial Participation \\n\\n• Pursue opportunities for international participation to support U.S. space exploration goals; and • Pursue commercial opportunities for providing transportation and other services supporting the International Space Station and exploration missions beyond low Earth orbit. \\n\\n# P R E S I D E N T G E O R G E W . B U S H J A N U A R Y 1 4 , 2 0 0 4 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .2 NASA Guiding Principles for Exploration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .3 Solar System and Beyond—Exploration Roadmap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .5 Lunar Testbeds and Missions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .7 Mars Research, Testbeds, and Missions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9 Outer Moons Research and Missions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\\n[...Content is omitted...]\\nw and cargo on different launches, a safer approach to crew transport. NASA will initiate Project Constellation to develop a new Crew Exploration Vehicle for future crew trans-port . This vehicle will be developed in stages, with the first automated test flight in 2008, more advanced test flights soon thereafter, and a fully operational capability no later than 2014. The design of the Crew Exploration Vehicle will be driv-en by the needs of the future human exploration missions described in this document. The Crew Exploration Vehicle might also supplement interna-tional partner crew transport systems to the Space Station. For cargo transport to the Space Station after 2010, NASA will rely on existing or new commercial cargo transport systems, as well as international part-ner cargo transport systems. NASA does not plan to develop new launch vehicle capabilities except where critical NASA needs—such as heavy lift—are not met by commercial or military systems. Depending on future human mission designs, NASA could decide to develop or acquire a heavy lift vehi-cle later this decade. Such a vehicle could be derived from elements of the Space Shuttle, existing commercial launch vehicles, or new designs. In the days of the Apollo program, human explo-ration systems employed expendable, single-use vehicles requiring large ground crews and careful monitoring. For future, sustainable exploration pro-grams, NASA requires cost-effective vehicles that may be reused, have systems that could be applied to more than one destination, and are highly reliable and need only small ground crews. NASA plans to invest in a number of new approaches to explo-ration, such as robotic networks, modular systems, pre-positioned propellants, advanced power and propulsion, and in-space assembly, that could enable these kinds of vehicles. These technologies will be demonstrated on the ground, at the Space Station and other locations in Earth orbit, and on the Moon start-ing this decade and into the next. Other break-through technologies, such as nuclear power and propulsion, optical communications, and potential use of space resources, will be demonstrated as part of robotic exploration missions. The challenges of designing these systems will accelerate the develop-ment of fundamental technologies that are critical not only to NASA, but also to the Nation’s econom-ic and national security. National Aeronautics and Space Administration 16 \\n\\nAbove: Mars as seen by the Hubble Space Telescope. 17 The Vision for Space Exploration NASA Transformation \\n\\n“No one regards what is before his feet; we all gaze at the stars.” \\n\\n> Quintus Ennius (239–169 BC)\\n\\nTo successfully execute the President’s vision, NASA will refocus its organization, create new offices where necessary, realign ongoing programs and personnel, experiment with new ways of doing business, and tap the great innovative and creative talents of our Nation. Prior to this plan, six Enterprises comprised NASA’s organization—Space Science, Earth Science, Biological and Physical Research, Aerospace Technology, Education, and Space Flight. To devel-op the exploration building blocks described in this document, NASA has created a new Exploration Systems Enterprise. Exploration Systems will be initially responsible for developing the solar system exploration vehicles and technologies described in this plan, including the Crew Exploration Vehicle, nuclear power and propulsion systems, and neces-sary supporting technologies. Relevant elements of the Aerospace Technology, Space Science, and Space Flight enterprises were transferred to the Exploration Systems Enterprise. The Aerospace Technology Enterprise was renamed the Aeronautics Enterprise. In the past, NASA’s human space flight programs and robotic exploration programs have largely oper-ated independently of each other. As human explor-ers prepare to join their robotic counterparts, closer coordination and integration will be necessary. The Exploration Systems Enterprise will work closely with the Space Science Enterprise to use the Moon as a testing ground for solar system exploration vehi-cles and technologies. NASA’s Space Science Enterprise will have respon-sibility for carrying out robotic testbeds on the Moon and Mars and will also demonstrate other key tech-nologies for human and robotic exploration in other missions to Mars and the outer moons. NASA’s Space Science Enterprise will eventually need to integrate human capabilities into Mars science plan-ning, and potentially deep space observatory or outer moon planning. Many other elements of the NASA organization will be focused to support this new direction. NASA’s Biological and Physical Research Enterprise will put much greater emphasis on bioastronautics research to enable human exploration of other worlds. Similarly, Space Station research will be prioritized to support human exploration. NASA’s Office of the Space Architect will be responsible for coordinating and integrating the exploration activities of NASA’s different Enterprises and for maintaining exploration roadmaps and high-level requirements. As we move outward into the solar system, NASA will rely more heavily on private sector space capa-bilities to support activities in Earth orbit and future exploration activities. In particular, NASA will seek to use existing or new commercial launch vehicles for cargo transport to the Space Station, and poten-tially to the Moon and other destinations. Building on its long history and extensive and close ties with the space and research agencies of other nations, NASA will also actively seek international partners and work with the space agencies of these partners in executing future exploration activities. Many of the technical challenges that NASA will face in the coming years will require innovative solutions. In addition to tapping creative thinking within the NASA organization, NASA will need to leverage the ideas and expertise resident in the Nation’s universities and industry. One way that NASA plans to do this is through a series of Centennial Challenges. As in the barnstorming days of early aviation, NASA plans to establish prizes for specific accomplishments that advance solar system exploration and other NASA goals. NASA will also invigorate its workforce, focus its facilities, and revitalize its field centers. Congress is reviewing human capital legislation that will provide NASA with necessary workforce tools. NASA is also planning to phase out older, underutilized build-ings and facilities. As exploration activities get underway, NASA anticipates additional planning, reviews, and changes to align and improve its infra-structure. National Aeronautics and Space Administration 18 \\n\\nAbove: The Moon as seen by the Galileo spacecraft. 19 The Vision for Space Exploration Resources \\n\\nTo confine our attention to terrestrial matters would be to limit the human spirit. \\n\\nStephen Hawking (1942– ) \\n\\nThe President’s Vision for solar system exploration is affordable in both the short-term and the long-term. NASA’s budget will increase by five percent per year over the next three years and at about one percent for the following two years. Although the budget increases are modest, NASA will be able to carry out a robust exploration pro-gram. NASA will free up resources in its budget in three ways: holding down growth in existing pro-grams that do not support the vision; retiring the Space Shuttle to free up billions of dollars in the next decade; and focusing on innovations that reduce the cost of sustained space operations. The chart above reflects the shift in funding that will occur over time as the new vision for human and robotic exploration of the solar system and beyond is implemented. \\n\\n# Exploration Strategy Based on Long-Term Affordability National Aeronautics and Space Administration 20 21 The Vision for Space Exploration National Benefits \\n\\nJust as Meriwether Lewis and William Clark could not have predicted the settlement of the American West within a hundred years of the start of their famous 19th century expedition, the total benefits of a single exploratory undertaking or discovery cannot be predicted in advance. Because the very purpose of exploratory voyages and research is to understand the unknown, exact benefits defy calculation. Nonetheless, we can define important categories of benefits to the Nation and society. Preparing for exploration and research accelerates the development of technologies that are important to the economy and national security. The space missions in this plan require advanced systems and capabilities that will accelerate the development of many critical technologies, including power, com-puting, nanotechnology, biotechnology, communica-tions, networking, robotics, and materials. These technologies underpin and advance the U.S. econo-my and help ensure national security. NASA plans to work with other government agencies and the pri-vate sector to develop space systems that can address national and commercial needs. Space exploration holds a special place in the human imagination. Youth are especially drawn to Mars rovers, astronauts, and telescopes. If engaged effec-tively and creatively, space inspires children to seek careers in math, science, and engineering, careers that are critical to our future national economic com-petitiveness. The accomplishments of U.S. space explorers are also a particularly potent symbol of American democracy, a reminder of what the human spirit can achieve in a free society. However, space explo-ration also encourages international cooperation, where spacecraft and explorers come to represent our world as well as our Nation. When the unknown becomes known, it catalyzes change, stimulating human thought, creativity and imagination. The scientific questions that this plan pursues have the potential to revolutionize whole fields of research. For example, scientists are still working to understand how similarly sized planets, such as Mars and Earth, could have developed so differently and what that could mean for our planet. If life is found beyond Earth, biological processes on other worlds may be very different from those evolved on our world. Outside the sciences, the very knowledge that life exists elsewhere in the universe may hold revelations for fields in the humanities. Exploration and discovery are key agents of growth in society—technologically, economically, socially, internationally, and intellectually. This plan sets in motion activities that will contribute to change and growth in the U.S. and the world over the next century. \\n\\n> NASA research stimulates and inspires young minds and provides critical technologies for the Nation.\\n\\n“Mankind is drawn to the heavens for the same reason we were once drawn into unknown lands and across the open sea. We choose to explore space because doing so improves our lives, and lifts our national spirit.” \\n\\nPresident George W. Bush January 14, 2004 National Aeronautics and Space Administration 22 \\n\\nAbove: An artist’s drawing of a human exploration base on Callisto, Jupiter’s second largest moon. \\n\\n# Image Credits \\n\\nPage vi An artist’s concept of a spacecraft, equipped with a centrifuge and nuclear–electric propulsion, traveling to Mars, John Frassanito and Associates. Page 6 An artist’s concept of lunar exploration, John Frassanito and Associates. Page 8 A drawing of astronauts and robots exploring Mars, John Frassanito and Associates. Page 12 A drawing of astronauts and advanced robotic assistants building a space telescope, Pat Rawlings, SAIC. Below Analytical Mechanics Associates. Editing, layout, and design by The Tauri Group, LLC, and Eileen Schramm visual communication. Above: Spiral galaxy M81, located approximately 12 million light-years from Earth, as seen by the Spitzer Space Telescope. National Aeronautics and Space Administration \\n\\nNASA Headquarters \\n\\nWashington, DC 20546 NP-2004-01-334-HQ \\n\\nhttp://www.nasa.gov\\n\"}"
        }

有点幽默,通义千问的接口认为 JSON string 不是 string。难道阿里云认真开始限制这里只能是简单字符串(“纯文本格式”)?我就不提所谓 object 对实际业务有多么重要(尽管 tools[].function.parameters.type = "object" 可能是指入参而不是出参格式)。再一次出现这样的情况:原本正常工作的代码由于接口行为变更而报错。这次,我索性从我的 function call 用例移除通义千问。

]]>
4 https://shansing.com/read/543/#comments https://shansing.com/feed/
ChatGPT Next Web 计费魔改小记 https://shansing.com/read/542/ https://shansing.com/read/542/ Thu, 27 Jun 2024 16:50:20 +0000 闪闪的星 眼看 GPT-4 系列正式开放(general available),OpenAI 还为流式(stream)输出增加 token 统计字段,我决定更新我的 GPT 网页。我一直用 chatgpt-web,自己修改代码加上的按量计费、版本切换(按量计费的 token 数还是估计的,对 GPT-4o 会高估),这时想再支持图片上传。想了几个方案,不是很妥:前端交互复杂,后端还依赖于 chatgpt-api 底层库。换一套“皮”是更好的选择。搜索 GitHub,就 ChatGPT Next Web 吧。

本来我没打算大改 ChatGPT Next Web。每个聊天对话能独立切换模型,它有了。我想要加上的图片输入,也有。我只需要加上计费。然而最终我还是动了很多地方,可以说是“魔改”吧,包括功能和外观的。我 fork 一份,提交了一个 bug 修复到上游,至今没有被合并。既然如此,我也没有后续提交的兴致。索性在这里稍微记录一下。

总的来说,我希望尽可能沿用之前我改的那套逻辑。即全站开启 HTTP Basic Auth,用于用户登录认证。自行定义 SHANSING_MODEL_CHOICES 用来配置模型单价等信息。不推荐直接使用我的项目,但如果要用请注意这些点。

ChatGPT Next Web 原生支持多种大模型,包括 OpenAI 的 GPT、Anthropic 的 Claude、Google 的 Gemini。在搭建过程中,我几乎都用过,所以下面我会顺便提到 GPT 以外的这些大模型。

为了使用 HTTP Basic Auth,需要修改 ./app/api/auth.ts 导出的 auth 函数,注释掉大部分用不到的逻辑,防止 Authorization header 冲突。

按量计费

首先当然是补上按量计费功能。如今,GPT、Claude、Gemini,官方都支持返回 token 用量,无论流式、非流式。OpenAI 的 Chat 接口,需要自行传入 stream_options,方可在流式输出时返回 usage 对象;其他两家无需额外设置。OpenAI 流式输出会在最后返回一个 trunk,这个 trunk 会在之前 finish reason 所在 trunk 的后面,实际报文如下所示:

...上面报文被省略...

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1719493000,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_sxx","choices":[{"index":0,"delta":{"content":" today"},"logprobs":null,"finish_reason":null}],"usage":null}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1719493000,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_sxx","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1719493000,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_sxx","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1719493000,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_sxx","choices":[],"usage":{"prompt_tokens":94,"completion_tokens":9,"total_tokens":103}}

data: [DONE]

在 DONE 之前的 data 段就是 usage 段。这个 usage 对象,实际上跟非流式输出时定义一样。于是可以编写一段通用的解析代码,从报文最后往前查找 "usage",然后解析后面的花括号内容。当然,这里原则上最优雅的做法是按照 HTTP 的 Server-Sent Events(SSE)去一条条解析消息,但是后面我们会看到查找字符串实现起来最简单。由于 GPT 响应 delta 报文中不会包含 "usage"(JSON 字符串里如果出现这个文本,引号会被转义),所以如此处理也不会有 bug。

在 GPT 的帮助下,解析 usage 对象的代码如下:

export function parseUsageObj(
  responseBody: string,
  key: string,
  fromStart: boolean,
) {
  const usageIndex = fromStart
    ? responseBody.indexOf('"' + key + '"')
    : responseBody.lastIndexOf('"' + key + '"');
  if (usageIndex !== -1) {
    const openBracket = responseBody.indexOf("{", usageIndex);
    const closeBracket = responseBody.indexOf("}", openBracket);
    if (openBracket !== -1 && closeBracket !== -1) {
      const jsonString = responseBody.substring(openBracket, closeBracket + 1);
      try {
        return JSON.parse(jsonString);
      } catch (e) {
        return null;
      }
    }
  }
  return null;
}

转到 ./app/api/openai/[...path]/route.ts,在 requestOpenai 调用后面,我们克隆一个 response,然后用异步的方法读取其中 body,交给上述函数解析,然后调用 pay 函数扣款:

response
      .clone()
      .text()
      .then((responseBody) => {
        //console.log("[responseBody]" + responseBody)
        const usage = parseUsageObj(responseBody, "usage", false);
        console.log(
          "[OpenAI Usage]<" + username + ">",
          JSON.stringify(usage),
        );
        if (
          usage &&
          usage.prompt_tokens != null &&
          usage.completion_tokens != null
        ) {
          return {
            promptTokenNumber: usage.prompt_tokens as number,
            completionTokenNumber: usage.completion_tokens as number,
          };
        }
        console.warn(
          "[ATTENTION][openai] unable to find usage, username=" +
            username +
            ", url=" +
            req.url +
            ", responseBody=" +
            responseBody,
        );
      })
      .then((obj) => {
        if (obj) {
          return pay(
            username,
            modelChoice,
            obj.promptTokenNumber + firstPromptTokenNumber,
            obj.completionTokenNumber + firstCompletionTokenNumber,
            config.shansingOnlineSearchSearchPrice
              .mul(searchCount + newsCount)
              .plus(config.shansingOnlineSearchCrawlerPrice.mul(crawlerCount)),
          );
        }
      });

这里需要注意的地方是,不可直接从 response 中读取 body。因为 body 只能读取一次,这边计费读了以后就不能返回给客户端了,所以我们调用 clone() 复制一份。之所以还强调异步,是因为这里的大概含义是,从 OpenAI API 读取到 response header,即可拿到 response 对象,但调用 .text().json() 这种取 body 的函数又是一个异步(promise)过程:header 读完了,body 可以慢慢读。在流式输出中,需要所有的 trunk 都接收完,才算接收完 body,后面的 .then() 才会执行;或者如果改成 await 阻塞的话,等到 body 都接收完毕才会继续执行后面的代码。显然这里不应该同步阻塞,那样流式输出就不是真流式、没有打字机效果了。而这里也可以看到,拿到作为 text 的 body 通过字符串匹配 usage 对象是最简单的,如果还交给 SSE 库的话显然会变复杂。

对于 Gemini,usage 对象名称叫 usageMetadata,其中关注 promptTokenCountcandidatesTokenCount。对于 Claude,关注 usage 对象的 input_tokensoutput_tokens。但是注意 Claude 的流式输出,input_tokens 会在报文开始不久给出,而 output_tokens 的最终值在报文结尾处。那么需要拼接两个对象。详细代码参见这里,不再赘述。

再提一嘴安全问题。GPT 流式输出如果不传 stream_options,将没有 usage 对象返回,所以应当强制要求传入。ChatGPT Next Web 的整体思路是后端聊天 API 几乎只是转发。也就是说,如果用户调用聊天接口,没有传入 stream_options,我们应该在转发时补上相应参数,而不是继续照原样转发。如果不想修改入参,也可以检测然后拒绝

眼神好、记忆棒的人会发现我这边扣费逻辑跟以前不一样。没错,我去除了预扣费。因为我是小范围分享,照预扣费的逻辑,有的用户用到最后会有一定余额不好花掉,所以索性去除。实际上我也没怎么限制并发。在意的人可以自行加上,在接口最开始时检查拥有足够余额,最保险。

流式输出动画

第二个我改动比较大的点,是流式输出动画帧。查找原项目中的 animate response to make it looks smooth,可以看到去年底作者对动画进行了修改,期望使它更平滑。然而,仔细阅读代码,发现所谓平滑的本质是强行压住已经拿到的增量消息(delta),再拆成一节节——最坏情况下,一个字一个字地——显示输出。我不是很喜欢这样。我觉得应该拿到什么 delta,就直接显示出来,看起来会更畅快,也不会一点点慢慢输出结果一会突然输出剩余字符(这是 finish 函数的作用)。

另一方面,可以看到 animateResponseText() 函数最后,再次通过 requestAnimationFrame 调用自身。我不是很懂前端,但我觉得即使函数最开始有条件判断,如果能避免这种不加间隔的类无限嵌套调用会更好。并且在使用过程中,我发现会偶然出现流式输出突然停止的问题,就好像打字机打到一半罢工了。此时查看浏览器控制台,可以看到“Maximum update depth exceeded”的未捕获报错,看着像是 Next.js 的防御机制,不排除跟嵌套调用有关。单单只是用 try catch 捕获其中 options.onUpdate 的报错,似乎就能解决这个问题,没有观察到什么副作用。

这块地方我也改了很多次,最终改成了接近引入“平滑”动画之前的样子,同时保留使用 requestAnimationFrame 这个现代方法。具体来说,接收到一个 delta,就调用一次 requestAnimationFrame 来请求动画帧,在一帧中就将所有增量文本 delta 输出显示。这样应该最直接、性能最好。

          const delta = choices[0]?.delta?.content;
          const textmoderation = json?.prompt_filter_results;

          if (delta) {
            responseText += delta;
            requestAnimationFrame(() => options.onUpdate?.(responseText));
          }

不过在使用时仍然偶发“Maximum update depth exceeded”问题,暂时不知如何进一步解决。

联网搜索

联网搜索是我一早就想加的功能。我知道可以用 function call 实现,但是想来还是有点复杂,没有底气。直到浏览国内大模型时,看到 Moonshot 的文档指示我们可以用 search2ai。看了一下,挺符合需求。

快速过一遍,search2ai 是利用大模型的 function call 工具调用搜索、爬虫,其中搜索、爬虫的核心逻辑均使用外部 API,旨在以透明方式提供 OpenAI 等接口代理。也就是说,将其独立部署,只需要修改 GPT 前端的 Base URL 就可以快速接入联网搜索。考虑过后,我决定采纳这种方式。

我对 search2ai 魔改挺多,基本只剩个架子跟搜索核心逻辑,也许以后另写一篇详谈。这里只简单说几点。一是原版爬取网页是用外部接口,我不是很欣赏,改成原生 fetch 请求,通过 @mozilla/readability 提取阅读模式一样的主体内容,再转换为 Markdown 格式。后来引入 Jina AI Reader 辅助 PDF 解析。二是自行完善、新增了 Gemini、Claude 支持。根据实际需要,我去除了关于非流式的多余分支,固定第一次请求非流式、第二次流式。而 Claude 很容易链式调用 tool,在第二次如果仍然 call function 则不能返回正确结果(貌似还会回落到非流式,我没有仔细研究),需要额外添加用户消息阻断,这里没有像 GPT 那样的 tool options。另外,计费需要统计两轮请求的费用,加和计算。实际上因为第二次是流式原样返回,简单的做法是此时将第一轮请求的 usage 放到 reponse header,由 GPT 前端加总计费。

接入 ChatGPT Next Web 时,考虑到后端 API 基本是转发,我引入一个自定义的 X-Shansing-Online-Search request header。当这个请求头存在并值为 true 时,就请求本地部署的 search2ai,将真正的 Base Url 以 X-Shansing-Base-Url 发送(因为要支持 OpenAI 兼容接口,如通义千问)。在聊天框上方加一个地球图标,给用户点击开启联网搜索。开启联网搜索就发送 X-Shansing-Online-Search: true 的 header,否则不发送。提取主题、压缩历史记录的请求总是不发送该 header。具体代码比较繁杂,可以自行拉取工程搜索关键词。

之后我还增加联网消息图标显示,用来确定一条回复消息到底有没有使用联网搜索。技术上说,就是到底大模型有没有调用 function call。需要跟 search2ai 的 response header 相配合。

Max token 参数

原项目不给大模型 API 传递 max_tokens 参数,并在注释中称其为 shit。但这样一来,前端界面的“单次回复限制”选项就形同虚设。于是我恢复传入这个参数。

然后我发现,其他地方又在使用(前端)设置的 max_tokens,并且含义跟 OpenAI 等聊天 API 不同。API 的 max_tokens 参数用来限制本次回复的 token 数(也就是输出 token 数)。但 ChatGPT Next Web 认为是总的 token 数,特别是输入 token 数,并以此来限制每次聊天所发送的上下文长度。经过权衡,我决定还是根据大模型 API 的含义修改逻辑。因为前端那个设置选项,默认值才 4000,不适应当前动不动 128K tokens 的上下文;4000 tokens 更像是单纯输出的长度。而且前端界面相应设置,标题和描述感觉自相矛盾;按照标题是控制回复(输出)的才对。

这边改动也有点大,见于 commit 067df37。先是需要理解大模型 API 的逻辑,一般需要我们自行管理上下文(先前我用的那个前端,底层库 chatgpt-api 似乎就干这个)。如果我们设置 4000 的 max token,说明我们期望这么长的输出,那对于一个 128K 上下文的模型,输入可用的就是 124000 tokens。注意输入包括系统消息(system message)、历史上下文(user、assistant)和本次需要发送的消息。统总计算,取合适轮次的上下文。我顺便修改了估计 token 数的方法 estimateTokenLength,采用一个 OpenAI 移植库真实计算 cl100k_base 的 token 数目。这是 GPT-3.5、GPT-4 采用的 tokenizer,GPT-4o 更新了使结果更小。但是没关系,历史上下文 token 计算出来宜多不宜少,宁愿携带更少的上下文也不要突然报错。事实上为了兼容其他模型,保险起见我还在结果乘了 1.1 的系数。

图片上传

所谓图片上传,ChatGPT Next Web 的实现其实是转为 base64 data url,然后传给大模型 API。因为 Local Storage 总大小极其有限,大约只有 5 MiB,所以图片会先经过本地压缩。

文首提到的我提交给上游的 pull request,是用来解决 HEIC 格式的问题。项目原本有 HEIC 支持。但用 Windows Chrome,选择 HEIC 格式的图片,file.type 值会为空导致走不到相应代码分支。用 iOS Safari,这个分支可以走到,但贡献者似乎漏写 else 导致后面会走原分支,抛出错误“上传”中止。另外,Windows Firefox 干脆在“打开”对话框选不到 .heic 文件,我自己把 fileInput.accept 的值从 MIME 类型改为扩展名就好了。

原有的图片压缩逻辑也不太行。压缩目标为 JPEG,采用 JPEG 质量等级,一步一步降低,降到一个阈值后转而缩小尺寸,直到文件大小为 256KiB。然而,根据 OpenAI 文档,GPT 支持的图片有最大尺寸限制,即短边 768px;其他模型类似。所以更好的做法是,不管三七二十一,首先缩小尺寸到短边 768px,然后才开始调降质量等级。我找到一个库,刚好可以同时设置缩放比例、目标文件大小。实测显示,甚至效率更好,肉眼可见变快很多。并且,在最初缩小尺寸之后,只调降质量等级,不再继续缩小图片,压到 256KiB 的图片也还能看。

代码可以在项目中查找 compressImage 函数,或者查看 commit #b26a9e2

通义千问

既然相比原来支持更多模型,我也看了国内的,考虑要不要新增支持。通义千问非常便宜,更重要的是支持 OpenAI 兼容格式。这样配置起来就非常方便。

不过为了共存,我没有直接修改 BaseUrl。而是仿照已有文件,新建 Alibaba 目录。参见 commit #61a1bd6

经过测试,通义千问-Max 的联网搜索效果不错。比 Gemini 之流强多了。不过中间突然改变过 API 行为,使得 tools 参数必须一直传下去,而不是在第二轮可以摘除不传(参见上面“联网搜索”章节),否则用 JSON 字符串作为 function response 会得到相当异常的回复。工单体验不太好,后面我会单独写一篇博客。

Qwen-Long 的回复质量也超出预期。像有的问题 GPT-4o 也不能很好回答,Qwen-Long 一枪命中。我前面说的“非常便宜”就是指降价后的 Qwen-Long,跟白送一样。如果我原价提供给朋友,甚至还要倒贴流量钱。Qwen-Long 这么便宜应该也是因为支持上传文件,鼓励大家多使用大文件、长 token。

文件上传支持起来比较简单,比 OpenAI 要额外调用 Assistant API 强太多。不过,貌似同时上传多个文件容易触发 429 Rate Limit,建议使用单选。有时候也会莫名触发,不是十分稳定。

小修改

接下来是一些小修改,想到哪写到哪。也不一一提供参考链接了。

自动刷新模型缓存。不知出于什么原因,ChatGPT Next Web 前端缓存了模型列表,导致服务端修改环境变量后用户很难看到更新。所以加了一个 resetModels() 函数及调用。

图片放文本前面。根据 Claude 文档,将 image 放到 text 上面有助于获得更好效果。

移除 Gemini 安全内容限制。代码传的 BLOCK HIGH。根据官方文档,相应参数指的是概率而不是严重程度。干脆改成 NONE。很多人觉得 Gemini 审查严格我估计跟这个设置有关。

正确处理 Claude 系统消息。原项目对 Claude 支持比较简略,比如没有正确传递 system 参数。这里要处理。注意这是外层参数,不是 messages / content,也不像 OpenAI 接口可以传递多个 system message。

移除 Claude 响应的 Content-Encoding header。对于非流式输出情况,Claude 似乎会错误地声明自己是 gzip 压缩,实际上并没有压缩。直接移除掉相关的 response header 就好。

所有模型都注入系统消息。虽然不确定 Claude 等模型用什么系统消息最好,但先按 ChatGPT 模板赋予一个,起码解决 LaTex 等问题。Claude 3 貌似是你不给定 LaTex 格式它就以为不支持,就不会吐出 LaTex 公式。

调用 API 时传递用户名 hash。GPT 和 Claude 支持传递用户名,未来如果发现滥用可以提醒我们。做一个 hash 后传过去就行。通义千问 OpenAI 兼容接口不支持,但传过去无害。Gemini 不支持。

为用户消息设定错误标志。相应处理逻辑本身有一句 userMessage.isError = ...,但这边 userMessage 算是一个拷贝对象,要赋给 savedUserMessage 才有用,可能是编码失误。标为错误的消息可以加个红圈圈 emoji,在之后的聊天中不会发送。用户消息也标为错误,就也不会作为后续上下文发送。比较符合直觉,也符合 Claude API 那种 user、assistant 必须一来一回的要求。

更新对话时尽量指定对话。分拆 updateCurrentSession 调用,如果不是确定要调整当前 session(对话),则需要指定。像更新对话主题(topic)的时机,实际上比最开始获取(当前)session 会延后一小段时间,不改可能出现更新到错误的对话上去的情况,因为“当前”session 已经切到另一个了。

改服务端 runtime 为默认。原项目使用 edge runtime,比较轻,但是缺乏很多 node API 支持。我部署 web 端,放心改成默认的 Node.js 运行时。这样可以继续用 fs 读写用户余额。

代理缓存 Emoji 和字体。将外部资源用 Nginx 代理并缓存,加快加载。

移除多余路径反代。可能是为打包成 app,ChatGPT Next Web 反向代理了很多接口,包括 OpenAI 聊天 API。我部署 web 端,这些看起来更像是安全漏洞,于是注释掉。我不想自己搭建一个 API 中转站。不过我建议你不要轻易试探访问,指不定有些实例背后是蜜罐。

结语

这个项目总体很好,节省很多工夫,感谢作者和贡献者们。这些修改多半是我个人需求,只是 pull request 能更积极 review、合并就好了。

在此插入一个提取错误信息的方法。从混杂 JSON 串或者 Nginx 默认错误页面中提取错误文案。对于 JSON 字符串,大概逻辑是优先取作为字符串的 messagemsg 值,如没有则取 key 包含“err”(如 errorerrMessageerror-message)的字符串类型的 value。会自动遍历嵌套对象。在这个项目的聊天、上传文件异常时能用到,算是小小万金油。由 Claude 辅助编写。代码比较长,搜索 extractErrorMessage 函数吧。

最后,尽管上面有些改动给了 commit 链接,仍然建议参考最新代码。链接:https://github.com/shansing/ChatGPT-Next-Web

]]>
4 https://shansing.com/read/542/#comments https://shansing.com/feed/
快乐星球写日记软件 https://shansing.com/read/541/ https://shansing.com/read/541/ Sun, 16 Jun 2024 13:14:00 +0000 闪闪的星 我在《我与机器学习的前缘》中写道:“《快乐星球》有一集讲述主角由于自动写日记程序造成误会的故事。来自快乐星球的法宝,输入关键词就能写日记,小学的我也想要啊。但我不知道哪里有;掂量掂量我的编程技术,我也不知道怎么做出来,只能做做白日梦。如今有 GPT 这类生成式 AI 就不是梦了。”今天,既然想要恢复博客更新,那就从这个“低垂的果实”开始,把这个“法宝”做出来吧。

为了贴近原剧,我特意去回顾了这一集。将其中多面体介绍这个软件的关键词记下,再结合东方老师对日记的要求,可以写出给 GPT 类生成式 AI 的提示词(prompts):

你是一个写日记的小学生。我会给你输进去几个单词,和一些必要的文本数据。你需要将其作为线索展开联想,串联成文,写一篇精彩的小学生日记。需要体现学生态度认真,留心观察生活,做一个有心人,贴近中国小学生生活。以记叙为主,有声有色,情景交融。内容不需要一直积极,可以展示矛盾冲突,有趣味性。用中文,字数150字左右,不超过200字。

最后的两句关于矛盾冲突的,以及字数要求,是我自己加的。因为众所周知市面上的 AI 比较倾向于生成积极的、正能量的内容,但每篇日记都很明显溢出正能量不太好。另外“串联”可能写作“串连”比较好,我因为调完才发现,就没改了。

将其作为 system 消息,再输入 user 消息,就可以使用了。在此我选择国产的通义千问 qwen-long 模型,因为便宜,并且文风比较符合中国人作品。将 max token 参数指定为 500。效果如下:

用户:同学去兄弟结拜公园钱五十送打讨厌

(这是剧中原词)

AI:今天,我的同学小明在课间偷偷告诉我,他准备周末去“兄弟结拜公园”,还神秘兮兮地说,只要在门口捐出五十元,就可以参加一个特别的仪式。我不禁觉得好笑,又有点儿疑惑,为什么会有这样的规定?小明平时可不是乱花钱的人,看起来这个公园真是别出心裁啊。

放学后,我路过那个公园,看到门口的确有个捐款箱,还有一张写着“诚心结拜,友谊长存”的标语。虽然我觉得“结拜”这种事有点儿老套,但也挺有意思的。不过,小明他们如果真这么做,可能会被老师知道,到时候免不了挨批吧。我想,要不要提醒他们呢?心里的矛盾让我犹豫不决。哎,明天再决定吧,也许这就是大人说的“成长的烦恼”吧。

还算可以。不如剧中详细、波折,但剧中那种还出现本地地名,估计得特调。再试试别的:

用户:违法ui花期五i恶化七分科学家屋内发

AI:今天是星期五,阳光明媚,本该是个愉快的日子。我在回家的路上发现了一朵不寻常的花,它的颜色绚丽,却有些违法常规,像是混合了多种色彩。我很好奇,这是否是科学家叔叔在屋内实验的结果呢?记得科学课上老师说过,自然界的规则不能轻易破坏,不然可能会带来意想不到的后果。想到这,我决定周末去找科学家叔叔问个明白。这样的发现让我既兴奋又担忧,生活中的未知总能让人深思。

用户:符文看按付款,撒地方那就阿塞飞

AI:今天,我在家里帮妈妈清理房间时,无意间发现了一个古老的符文。它刻在一个小木盒上,看起来很神秘。妈妈说这可能是爷爷留下来的,关于它的含义,我们都不清楚。我好奇地研究着这个符文,想象着它背后的故事。晚上,我坐在书桌前,一边看着窗外的星空,一边思考着这个符文可能与哪个遥远的地方或者传说有关。阿塞飞,这个名字在我脑海中闪过,它是不是某个古老国度的名字呢?我想找出答案,感觉就像是一场奇妙的冒险,我一定要解开这个谜团。

用户:苏覅和是对阿飞看见撒旦发

AI:今天放学后,我和苏覅一起走在回家的路上,突然看到阿飞正对着路边的一个奇怪石像发呆。那石像有些破损,似乎是个描绘撒旦的形象。阿飞平常总是笑嘻嘻的,今天却显得十分严肃。我靠近一看,石像的眼睛仿佛会盯人心魄。我和苏覅对视一眼,决定陪阿飞一起探寻这个石像的秘密。我们琢磨着,这个石像是从哪里来的,为何会出现在这里。夕阳下,我们的影子和石像混在一起,让人有种说不出的紧张感。这是一个不同寻常的傍晚,我们三人的心中都藏着一份探险的热情。

然后是没那么乱打的,完整一点的提示:

用户:给乞丐捐了钱,却看到他手上的钻石戒指

AI:今天放学,我看到一个衣衫褴褛的乞丐坐在街角,心里很难过,就把自己剩下的零花钱全部给了他。他的眼睛里闪烁着感激,我转身离开时却无意间瞥见他手上戴着一枚璀璨的钻石戒指。我心里一阵疑惑,忍不住回头看了一眼。那个乞丐也注意到我的目光,迅速把手藏了起来。那一刻,我感到既困惑又失落。回到家,我把这件事告诉了妈妈,她告诉我世界很复杂,不是所有事情都如表面看起来那么简单。虽然有些失望,但我知道这次的经历教会我要更加谨慎地看待周围的事物。

]]>
0 https://shansing.com/read/541/#comments https://shansing.com/feed/