diff --git a/2020/07/11/2020年中总结/index.html b/2020/07/11/2020年中总结/index.html index bb0e4a3c31..4b984aec3a 100644 --- a/2020/07/11/2020年中总结/index.html +++ b/2020/07/11/2020年中总结/index.html @@ -1 +1 @@ -2020年中总结 | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

2020年中总结

很快2020 年就过了一半了,而且是今年这么特殊的一年,很多事情都发生的出乎意料,疫情这个绕不过去的话题,之前写了点比较愤青的文字,感觉不太适合发出来就烂在草稿箱里吧,这个目前一大影响估计是今年都没办法完全摘下口罩了,前面几个月来回杭州都开车,因为彭埠大桥不通行了,实在是非常不方便,每条路都灰常堵,心累,吐槽下杭州的交通规划和交警同志,工作实在做的不咋地。

另外一件是就是蜗壳,从前不知道黝黑蜗壳是啥意思,只是经常会在他的视频里看到,大学的时候在缘网下了一个集锦,炒鸡帅气,各种空接扣篮,越来越能明白那句“你永远不知道意外和明天不知道哪个会先来,且行且珍惜”的含义,只是听了很多道理,依然活不好这一生,知易行难,王阳明真的是这方面的大师,有空可以看看这方面的书,一直想写写我跟篮球跟蜗壳的这十几年,争取能早日写好吧,不过得找个静得下来的时候写。

正事方面上半年还是挺让人失望的,没有达成一些目标,应该还是能力不足吧,技术方面分析一下还是停留在看的表面层,有些实操的,或者结合业务场景的能力不太行,算是在坚持写写 blog,主要是被这个每周一篇的目标推着走,有时会比较焦虑,内容产出也还比较差,希望能在后面有些改善,可能会降低频率,只是觉得降低了也不一定能有比较好的提升,无法战胜自己的惰性,所以暂时还是坚持下这个目标吧,还有就是 coding 能力,有时候也应该刷刷题,提升思维敏捷度,大脑用太少可能生锈了,况且本来就不是很有优势,虽然失望也只能继续努力吧,日拱一卒,来日方长,加油吧~😔

还有就是跑步减肥了,截止今天,上半年跑了 136 公里了,因为疫情影响,农历年后是从 4 月 17 号开始跑的,去年跑到了 300 公里,奖励自己了一个手表(真的挺后悔的,还不如 200 块买个手表),今年希望可以能在这个基础上再进一步,一直跟领导说,跑步算是我坚持下来的唯一一个好习惯了,618 买了个跑步机,周末回家了可以不受天气影响的多跑跑,不过如果天气好可能还是会出去跑跑,跑步机跑道多少还是有点拘束,只是感觉可能是我还是吃得太多了🤦‍♂️,效果不是很明显,还在 80 这个坎徘徊,等于浪费了大半年,可能是年初的项目太费心力,压力比较大,吃得更多,是不是可以算工伤😄,这方面也需要好好调整,可以放得开一点,虽然不太可能一下子到位,但是总要去努力下,随着年龄成长总要承担更多,也要看得开一点,没法事事如愿,尽力就好了,减肥这个事情还在结合一些俯卧撑啥的,希望也能坚持下去,加油吧,不知道原话怎么说的,意思是人类最大的勇敢就是看透了人世间的苦难,仍然热爱生活。我当然没可能让内心变得这么强大,试着去努力吧,奥力给!

0%
\ No newline at end of file +2020年中总结 | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

2020年中总结

很快2020 年就过了一半了,而且是今年这么特殊的一年,很多事情都发生的出乎意料,疫情这个绕不过去的话题,之前写了点比较愤青的文字,感觉不太适合发出来就烂在草稿箱里吧,这个目前一大影响估计是今年都没办法完全摘下口罩了,前面几个月来回杭州都开车,因为彭埠大桥不通行了,实在是非常不方便,每条路都灰常堵,心累,吐槽下杭州的交通规划和交警同志,工作实在做的不咋地。

另外一件是就是蜗壳,从前不知道黝黑蜗壳是啥意思,只是经常会在他的视频里看到,大学的时候在缘网下了一个集锦,炒鸡帅气,各种空接扣篮,越来越能明白那句“你永远不知道意外和明天不知道哪个会先来,且行且珍惜”的含义,只是听了很多道理,依然活不好这一生,知易行难,王阳明真的是这方面的大师,有空可以看看这方面的书,一直想写写我跟篮球跟蜗壳的这十几年,争取能早日写好吧,不过得找个静得下来的时候写。

正事方面上半年还是挺让人失望的,没有达成一些目标,应该还是能力不足吧,技术方面分析一下还是停留在看的表面层,有些实操的,或者结合业务场景的能力不太行,算是在坚持写写 blog,主要是被这个每周一篇的目标推着走,有时会比较焦虑,内容产出也还比较差,希望能在后面有些改善,可能会降低频率,只是觉得降低了也不一定能有比较好的提升,无法战胜自己的惰性,所以暂时还是坚持下这个目标吧,还有就是 coding 能力,有时候也应该刷刷题,提升思维敏捷度,大脑用太少可能生锈了,况且本来就不是很有优势,虽然失望也只能继续努力吧,日拱一卒,来日方长,加油吧~😔

还有就是跑步减肥了,截止今天,上半年跑了 136 公里了,因为疫情影响,农历年后是从 4 月 17 号开始跑的,去年跑到了 300 公里,奖励自己了一个手表(真的挺后悔的,还不如 200 块买个手表),今年希望可以能在这个基础上再进一步,一直跟领导说,跑步算是我坚持下来的唯一一个好习惯了,618 买了个跑步机,周末回家了可以不受天气影响的多跑跑,不过如果天气好可能还是会出去跑跑,跑步机跑道多少还是有点拘束,只是感觉可能是我还是吃得太多了🤦‍♂️,效果不是很明显,还在 80 这个坎徘徊,等于浪费了大半年,可能是年初的项目太费心力,压力比较大,吃得更多,是不是可以算工伤😄,这方面也需要好好调整,可以放得开一点,虽然不太可能一下子到位,但是总要去努力下,随着年龄成长总要承担更多,也要看得开一点,没法事事如愿,尽力就好了,减肥这个事情还在结合一些俯卧撑啥的,希望也能坚持下去,加油吧,不知道原话怎么说的,意思是人类最大的勇敢就是看透了人世间的苦难,仍然热爱生活。我当然没可能让内心变得这么强大,试着去努力吧,奥力给!

0%
\ No newline at end of file diff --git a/2021/03/31/2020-年终总结/index.html b/2021/03/31/2020-年终总结/index.html index e0bbd53100..20b763c016 100644 --- a/2021/03/31/2020-年终总结/index.html +++ b/2021/03/31/2020-年终总结/index.html @@ -1 +1 @@ -2020 年终总结 | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

2020 年终总结

拖更原因

这篇年终总结本来应该在农历过完年就出来的,结果是对没有受疫情影响的春节放假时间空闲情况预估太良好,虽然公司调了几天假,但是因为春节期间疫情状况比较好,本来酒店都不让接待聚餐什么的,后来统统放开,结果就是从初一到初六每天要不就是去亲戚家,要不就是去酒店饭店吃饭,计划很丰满,现实很骨感,时间感觉一下就没了,然后年后感觉有点犯懒了,所以才拖到现在。

生活-健身跑步

去年(19 年)的时候跑步突破了 300 公里,然后20 年给自己定了个 400 公里的目标,结果意料之中的没成功,原因可能疫情算一点吧,后面买了跑步机之后,基本周末回家都能跑一下,但是最后还是只跑了300 多公里,总的keep 记录跑量也没超过 1000 公里,所以跑步这个目标还是没成功的,不过还算是比去年多跑一点,这样也算后面好突破点,后面的目标就不定的太高了,每年能比前一年多一点就好,其实跑步已经从一种减肥方式变成一种习惯了,一周一次的跑步已经比较难有效减重了,但是对于保持精力和身体状态还是很有效和重要的,只是对于目前的体重还是要多减下去一些跑步才好,太重了对膝盖负担太大了,可惜还是时间呐,游泳骑车什么的都需要更苛刻的条件和时间,饮食呢控制起来比较难(贪吃
终于在 3 月底之前跑到了 1000 公里,迟了三个月,不过也总算达到了,只是体重控制还是不行,有试着走走楼梯,但是感觉对膝盖负担比较大,得再想想用什么方式

技术成长

一直提不起笔来写这篇年终总结还有个比较大的原因是觉得20 年的成长不如预期,大小目标都没怎么完成,比如深入了解 jvm,是想能有些深入的见解,而不再是某些点的比较片面的理解,系统性的归纳总结也比较少,每个方向或多或少有些看法和理解,但是不全面,一些东西看过了也会忘记,需要温故而知新,比如 AQS 的内容,第一次读其实理解比较浅,后面就强迫自己去读,去写,才有了一些比之前更深入的理解,因为很多文章都是带有作者思路的引导,适不适合自己都要看是否能从他的思路把它看懂,有些就差别很大,这个跟看书也一样,有些书大众一致推荐,一般情况下大多是经典的好的,但是也有可能是不太适合自己的,可能有时候机缘巧合看到的反而让人茅塞顿开,在 todo 里已经积攒了好多的点和面需要去学习实践,一方面是自己懒,一方面是时间也相对偏少,看看 21 年能不能有所提升,加强“时间管理”,哈哈

技术上主要是看了 mysql 的 mvcc 相关内容,rocketmq 的,redis 的代码,还有 mybatis 等,其实每一个都能写很多,也有很多值得学习的,需要全面系统学习,之前想好好画一个思维导图,将整个技术体系都梳理下,还只做了一点点,方式也有点问题,应该从大到小,而不是深度优先,细节有很多,每一个方面都有自己比较熟悉擅长的,也有不太了解的,可以做一个评分,这个也是亟待改善的,希望今年能完成。

博客

博客方面 20 年一年整是写了 53 篇,差不多是一周一篇的节奏,这个还是不错的,虽然博客质量参差不齐,但是这个更新频率还是比较好的,并且也定了个潜规则,可以一周技术一周生活,这样能缓解水文的频率,提高些技术文章的质量,虽然结果并没有好多少,不过感觉还是可以这么坚持的,能提高一些技术文章的质量那就更好了

0%
\ No newline at end of file +2020 年终总结 | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

2020 年终总结

拖更原因

这篇年终总结本来应该在农历过完年就出来的,结果是对没有受疫情影响的春节放假时间空闲情况预估太良好,虽然公司调了几天假,但是因为春节期间疫情状况比较好,本来酒店都不让接待聚餐什么的,后来统统放开,结果就是从初一到初六每天要不就是去亲戚家,要不就是去酒店饭店吃饭,计划很丰满,现实很骨感,时间感觉一下就没了,然后年后感觉有点犯懒了,所以才拖到现在。

生活-健身跑步

去年(19 年)的时候跑步突破了 300 公里,然后20 年给自己定了个 400 公里的目标,结果意料之中的没成功,原因可能疫情算一点吧,后面买了跑步机之后,基本周末回家都能跑一下,但是最后还是只跑了300 多公里,总的keep 记录跑量也没超过 1000 公里,所以跑步这个目标还是没成功的,不过还算是比去年多跑一点,这样也算后面好突破点,后面的目标就不定的太高了,每年能比前一年多一点就好,其实跑步已经从一种减肥方式变成一种习惯了,一周一次的跑步已经比较难有效减重了,但是对于保持精力和身体状态还是很有效和重要的,只是对于目前的体重还是要多减下去一些跑步才好,太重了对膝盖负担太大了,可惜还是时间呐,游泳骑车什么的都需要更苛刻的条件和时间,饮食呢控制起来比较难(贪吃
终于在 3 月底之前跑到了 1000 公里,迟了三个月,不过也总算达到了,只是体重控制还是不行,有试着走走楼梯,但是感觉对膝盖负担比较大,得再想想用什么方式

技术成长

一直提不起笔来写这篇年终总结还有个比较大的原因是觉得20 年的成长不如预期,大小目标都没怎么完成,比如深入了解 jvm,是想能有些深入的见解,而不再是某些点的比较片面的理解,系统性的归纳总结也比较少,每个方向或多或少有些看法和理解,但是不全面,一些东西看过了也会忘记,需要温故而知新,比如 AQS 的内容,第一次读其实理解比较浅,后面就强迫自己去读,去写,才有了一些比之前更深入的理解,因为很多文章都是带有作者思路的引导,适不适合自己都要看是否能从他的思路把它看懂,有些就差别很大,这个跟看书也一样,有些书大众一致推荐,一般情况下大多是经典的好的,但是也有可能是不太适合自己的,可能有时候机缘巧合看到的反而让人茅塞顿开,在 todo 里已经积攒了好多的点和面需要去学习实践,一方面是自己懒,一方面是时间也相对偏少,看看 21 年能不能有所提升,加强“时间管理”,哈哈

技术上主要是看了 mysql 的 mvcc 相关内容,rocketmq 的,redis 的代码,还有 mybatis 等,其实每一个都能写很多,也有很多值得学习的,需要全面系统学习,之前想好好画一个思维导图,将整个技术体系都梳理下,还只做了一点点,方式也有点问题,应该从大到小,而不是深度优先,细节有很多,每一个方面都有自己比较熟悉擅长的,也有不太了解的,可以做一个评分,这个也是亟待改善的,希望今年能完成。

博客

博客方面 20 年一年整是写了 53 篇,差不多是一周一篇的节奏,这个还是不错的,虽然博客质量参差不齐,但是这个更新频率还是比较好的,并且也定了个潜规则,可以一周技术一周生活,这样能缓解水文的频率,提高些技术文章的质量,虽然结果并没有好多少,不过感觉还是可以这么坚持的,能提高一些技术文章的质量那就更好了

0%
\ No newline at end of file diff --git a/2021/07/18/2021-年中总结/index.html b/2021/07/18/2021-年中总结/index.html index 76a85cc7d8..bab054648f 100644 --- a/2021/07/18/2021-年中总结/index.html +++ b/2021/07/18/2021-年中总结/index.html @@ -1 +1 @@ -2021 年中总结 | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

2021 年中总结

又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。

这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《cUrl 必知必会》,《古董局中局 1》,《古董局中局 2》,《算法图解》,《每天 5 分钟玩转 Kubernetes》《幸福了吗?》《高可用可伸缩微服务架构:基于 Dubbo、Spring Cloud和 Service Mesh》《Rust 权威指南》后面可以写个专题说说看的这些书,虽然每天打卡如果时间安排不好,并且看的书像 rust 这样比较难的话还是会有点小焦虑,不过也是个调整过程,一方面可以在白天就抽空看一会,然后也不必要每次都看很大一章,注重吸收。

技术上的成长的话,有一些比较小的长进吧,对于一些之前忽视的 synchronized,ThreadLocal 和 AQS 等知识点做了下查漏补缺了,然后多了解了一些 Java 垃圾回收的内容,但是在实操上还是比较欠缺,成型的技术方案,架构上所谓的优化也比较少,一些想法也还有考虑不周全的地方,还需要多花时间和心思去学习加强,特别是在目前已经有的基础上如何做系统深层次的优化,既不要是鸡毛蒜皮的,也不能出现一些不可接受的问题和故障,这是个很重要的课题,需要好好学习,后面考虑定一些周期性目标,两个月左右能有一些成果和总结。

另外一部分是自己的服务,因为 ucloud 的机器太贵就没续费了,所以都迁移到腾讯云的小机器上了,顺便折腾了一点点 traefik,但是还很不熟练,不太习惯这一套,一方面是 docker 还不习惯,这也加重了对这套环境的不适应,还是习惯裸机部署,另一方面就是 k8s 了,家里的机器还没虚拟化,没有很好的条件可以做实验,这也是读书打卡的一个没做好的点,整体的学习效果受限于深度和实操,后面是看都是用 traefik,也找到了一篇文章可以 traefik 转发到裸机应用,因为主仓库用的是裸机的 gogs。

还有就是运动减肥上,唉,这又是很大的一个痛点,基本没效果,只是还算稳定,昨天看到一个视频说还需要力量训练来增肌,以此可以提升基础代谢,打算往这个方向尝试下,因为今天没有疫情限制了,在 6 月底完成了 200 公里的跑步小目标,只是有些膝盖跟大腿根外侧不适,抽空得去看下医生,后面打算每天也能做点卷腹跟俯卧撑。

下半年还希望能继续多看看书,比很多网上各种乱七八糟的文章会好很多,结合豆瓣评分,找一些评价高一些的文章,但也不是说分稍低点的就不行,有些也看人是不是适合,一般 6 分以上评价比较多的就可以试试。

0%
\ No newline at end of file +2021 年中总结 | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

2021 年中总结

又到半年总结时,第一次写总结类型的文章感觉挺好写的,但是后面总觉得这过去的一段时间所做的事情,能力上的成长低于预期,但是是需要总结下,找找问题,顺便展望下未来。

这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《cUrl 必知必会》,《古董局中局 1》,《古董局中局 2》,《算法图解》,《每天 5 分钟玩转 Kubernetes》《幸福了吗?》《高可用可伸缩微服务架构:基于 Dubbo、Spring Cloud和 Service Mesh》《Rust 权威指南》后面可以写个专题说说看的这些书,虽然每天打卡如果时间安排不好,并且看的书像 rust 这样比较难的话还是会有点小焦虑,不过也是个调整过程,一方面可以在白天就抽空看一会,然后也不必要每次都看很大一章,注重吸收。

技术上的成长的话,有一些比较小的长进吧,对于一些之前忽视的 synchronized,ThreadLocal 和 AQS 等知识点做了下查漏补缺了,然后多了解了一些 Java 垃圾回收的内容,但是在实操上还是比较欠缺,成型的技术方案,架构上所谓的优化也比较少,一些想法也还有考虑不周全的地方,还需要多花时间和心思去学习加强,特别是在目前已经有的基础上如何做系统深层次的优化,既不要是鸡毛蒜皮的,也不能出现一些不可接受的问题和故障,这是个很重要的课题,需要好好学习,后面考虑定一些周期性目标,两个月左右能有一些成果和总结。

另外一部分是自己的服务,因为 ucloud 的机器太贵就没续费了,所以都迁移到腾讯云的小机器上了,顺便折腾了一点点 traefik,但是还很不熟练,不太习惯这一套,一方面是 docker 还不习惯,这也加重了对这套环境的不适应,还是习惯裸机部署,另一方面就是 k8s 了,家里的机器还没虚拟化,没有很好的条件可以做实验,这也是读书打卡的一个没做好的点,整体的学习效果受限于深度和实操,后面是看都是用 traefik,也找到了一篇文章可以 traefik 转发到裸机应用,因为主仓库用的是裸机的 gogs。

还有就是运动减肥上,唉,这又是很大的一个痛点,基本没效果,只是还算稳定,昨天看到一个视频说还需要力量训练来增肌,以此可以提升基础代谢,打算往这个方向尝试下,因为今天没有疫情限制了,在 6 月底完成了 200 公里的跑步小目标,只是有些膝盖跟大腿根外侧不适,抽空得去看下医生,后面打算每天也能做点卷腹跟俯卧撑。

下半年还希望能继续多看看书,比很多网上各种乱七八糟的文章会好很多,结合豆瓣评分,找一些评价高一些的文章,但也不是说分稍低点的就不行,有些也看人是不是适合,一般 6 分以上评价比较多的就可以试试。

0%
\ No newline at end of file diff --git a/2026/10/04/Pi-Agent-1-0-MCP集成源码解析-工具发现与提示词压缩/index.html b/2026/10/04/Pi-Agent-1-0-MCP集成源码解析-工具发现与提示词压缩/index.html index 41bbadbb18..ff8ca110f4 100644 --- a/2026/10/04/Pi-Agent-1-0-MCP集成源码解析-工具发现与提示词压缩/index.html +++ b/2026/10/04/Pi-Agent-1-0-MCP集成源码解析-工具发现与提示词压缩/index.html @@ -1 +1 @@ -Pi Agent 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩 | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

Pi Agent 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩

给 Agent 接上几个 MCP 服务器以后,很容易碰到一个问题:还没真正调用工具,请求里就已经带上了一大批工具名字、参数和使用说明。等工具开始返回数据,一份很长的 JSON 又进入上下文,后面的模型请求还要继续带着它。

前面写过 Pi Agent 的工具系统和 Pi Agent 的双层主循环。这次继续看 2026 年 10 月 1 日发布的 Pi 1.0,重点是 MCP 工具怎样进入模型请求,以及 Pi 怎样减少这部分上下文开销。

MCP(Model Context Protocol)约定了客户端怎样连接服务器、发现工具、调用工具和读取资源。接入协议以后,Agent 还要决定哪些工具提前告诉模型、工具调用怎样编排、结果保留多少。Pi 这次改动主要落在这些位置。

官方提到 Codemode 的提示词约减少 40%。这个数字很容易让人想知道:到底删了什么,原来的提示词是什么样,缩短后模型又靠什么找到工具?下文会把改前、改后的代码直接放在一起。

先确认版本:MCP 在 0.99.0 已经进入内置扩展

Pi 1.0 的发布文章把 MCP、Codemode 和延迟工具加载放在一起介绍。按具体 tag 的变更记录看,这套能力分几次进入仓库:
版本日期MCP 相关变化
0.99.02026-09-29首次内置 MCP、Codemode 和 tool_search
0.99.22026-09-30默认 MCP 工具按需发现;工具描述不再随服务器连接变化;首次请求只等待 direct 服务器
1.0.02026-10-01进一步压缩固定提示词,增加纠错提示,修复 OAuth 和恢复已加载工具的问题

版本归属可以在固定到 1.0.0 的 CHANGELOG 中核对。官方 “You Said No MCP!” 也解释了接入思路:工具需要容易发现、能够返回结构化数据,并能像函数一样组合调用。

本文的主要分析对象固定为:

1
2
3
4
5
6
7
repository: https://github.com/earendil-works/pi
tag: v1.0.0
commit: a13d35a742c6ef8462812a28fbe1d8c8b7431c32

提示词压缩的具体对照:
before: 88ff80b986e34d4fbd1fa94a4df65c60ae964516
after: 6f1072cc081f06b86a673bd142f03720d17afe15

before 是提示词压缩 commit 的父版本。它已经包含图像生成 API,因此不能拿 0.99.2 的整段提示词直接替代这个对照。本文后面的压缩代码来自该 commit 前后;其余实现以 1.0.0 为准。

本文根据源码、commit diff 和仓库测试断言分析。没有运行真实 MCP 服务或复现官方的 provider token 样本。字符数对照来自提示字符串统计,官方 token 数会单独标明。

工具已经注册,模型仍可以暂时看不到它

过去比较直接的做法,是连接服务器、拿到工具列表,然后把所有工具 schema 都交给模型。schema 在这里指输入参数及其类型、必填字段等说明。

Pi 为工具增加了不同的 exposure,也就是模型通过什么路径接触工具:
exposure默认行为适合的使用方式
codemodeMCP 默认值;工具可从脚本调用,完整声明不预先进入模型请求在脚本里组合、并行或筛选 MCP 调用
deferred工具被 tool_search 找到后,在下一次模型请求中声明按需加载后直接调用
direct工具直接声明给模型,也可以从 Codemode 调用少量经常使用的工具
hidden工具不可调用关闭某些服务器或工具能力

服务器级的 exposure 还可以被单个工具的 toolExposure 覆盖。比如把查询工具交给 Codemode,把某个常用工具直接声明,把不需要的工具隐藏。固定版本配置说明

这里还有一层名称转换:MCP 配置中的 codemode 会被映射为通用工具层的 deferred。两种 MCP 配置都不会把完整声明预先放进 Codemode description;MCP 扩展再根据配置决定激活 Codemode 还是 tool_search,供模型发现这些工具:

1
2
3
export function toToolExposure(exposure: McpExposure): ToolExposure {
return exposure === "codemode" ? "deferred" : exposure;
}

因此,下面通用工具层的 codemode 分支与 MCP 配置中的同名值处在不同层次。默认 MCP 工具在这一步落入 deferred 分支。源码见 toToolExposure。

产品层把已注册工具、脚本可调用工具和模型直接看到的工具分开。AgentSession._getCallableTools() 负责计算脚本可调用的集合:

1
2
3
4
5
6
private _getCallableTools(active: ReadonlySet<string> = new Set(this.getActiveToolNames())): AgentTool[] {
return [...this._toolRegistry.values()].filter((tool) => {
const exposure = this._getToolExposure(tool.name);
return exposure === "codemode" || exposure === "deferred" || (exposure === "direct" && active.has(tool.name));
});
}

codemode 和 deferred 工具只要注册完成,就能走脚本调用路径。direct 工具还要求它在 active tool 集合中。这样就可以把大量工具留在运行时,同时控制进入模型请求的声明数量。源码见 agent-session.ts。

searchTools 与 tool_search 做了两件不同的事

默认不把所有 schema 塞进请求以后,模型需要有地方查工具。Pi 提供了脚本内查询和模型工具加载两条路径。

searchTools() 是 Codemode 脚本中的查询函数。它搜索可调用工具,返回匹配项的名称和描述;这里的描述实际包含生成后的 TypeScript 声明。因此,它能按需拿到参数结构。调用方式可以写成下面这样,mcp__tracker 只是示例命名空间:

1
2
3
4
5
6
text(await searchTools("search issues", {
namespace: "mcp__tracker",
limit: 3,
}));

text(await describeNamespace("mcp__tracker"));

describeNamespace() 返回服务器的 instructions 和工具名,describeTool() 按具体名称返回工具描述及声明。这些数据先进入脚本,只有通过 text() 或 return 输出的部分才会进入模型上下文。源码见 createDiscoveryGlobals。

tool_search 则是模型可以直接调用的工具。它查找尚未加载的工具,将匹配项加入 active tools,从下一次模型请求开始声明。已经加载的工具会记录在会话中。源码见 searchAndLoad。

这两个接口的区别会影响上下文:脚本里查询并调用工具,可以一直把处理留在脚本中;通过 tool_search 加载以后,模型会开始直接接收匹配工具的声明。

当前内置检索器采用 BM25,即根据查询词和工具说明中的词项匹配程度排序。检索内容包括工具名、description、输入 schema 的属性名和说明,以及 namespace 的描述与 instructions。Codemode 的 searchTools() 默认返回 8 个匹配项。检索实现

Codemode 把完整结果留在脚本中

Codemode 让模型写一段 JavaScript,再在 QuickJS 沙箱中运行。QuickJS 是一个可以编译成 WebAssembly 的 JavaScript 引擎;脚本没有 Node API、文件系统、网络或计时器,需要通过注入的工具访问外部能力。

一次 MCP 调用的路径可以按下面的顺序读:

1
2
3
4
5
6
7
8
模型生成 JavaScript
→ QuickJS 运行脚本
→ tools.<name>(args)
→ ctx.executeTool()
→ MCP 服务器
→ 完整结果回到脚本
→ 脚本筛选、聚合
→ text() / return 输出给模型

executeCodemode() 没有直接绕过产品层调用 MCP 客户端。它把嵌套调用交给 ctx.executeTool()。下面是其中连续的调用片段:

1
2
3
const outcome = await ctx.executeTool(tool.name, args, { signal: callSignal });
record.id = outcome.toolCall.id;
record.durationMs = performance.now() - callStartedAt;

这让嵌套调用继续经过正常工具流水线,参数校验和扩展的 tool_call、tool_result 回调仍然生效。配置了权限扩展时,扩展也能拦截这些调用。最终发送给模型的是脚本输出,并不会自动把每个嵌套 MCP 结果全文放进对话。源码见 executeCodemode。

例如,需要查询一批 issue,再读取各自的评论,可以在脚本里并行取数据、筛选、排序,只把需要的条目交给模型。这是用于说明机制的场景,本文没有对它测量耗时或 token。

MCP 结果进入产品层以后,convertMcpResult() 会生成两种视图:模型可以看的 content,以及脚本可以拿到的完整结构化结果。下面是源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export async function convertMcpResult(
server: string,
tool: string,
result: CallToolResult,
options: ConvertMcpResultOptions = {},
): Promise<AgentToolResult<McpToolDetails>> {
// Without content blocks, toLlmContent falls back to the structured content as JSON.
const converted: (TextContent | ImageContent)[] =
result.content.length > 0 ? await toModelContent(server, result.content, options) : toLlmContent(result);
if (result.isError && textOf(converted) === "") {
converted.push({ type: "text", text: `MCP tool ${server}/${tool} returned an error` });
}
const { content, fullOutputPath } = await limitMcpContent(converted, options.saveOutput);
const { _meta: _ignored, ...scriptResult } = result;
return {
content,
details: { server, tool, ...(fullOutputPath ? { fullOutputPath } : {}) },
structuredContent: scriptResult as unknown as JsonValue,
...(result.isError ? { isError: true } : {}),
};
}

limitMcpContent() 限制直接给模型的文本,默认上限为 20KB,超出后保留首尾、把完整文本写到临时文件。structuredContent 则保留服务器的 CallToolResult,只移除 _meta。源码见 MCP 结果转换。

因此,脚本能拿到 content、structuredContent 和 isError,先处理数据再输出。服务器能提供结构化字段时,这种写法比较容易组合;如果结果只有一大段自然语言,脚本还需要自己解析它。

这里有一个实际写脚本时要留意的返回规则:

1
2
3
4
5
6
7
function toScriptValue(tool: AgentTool<any>, outcome: AgentToolCallOutcome): unknown {
const { result } = outcome;
if (tool.outputSchema && result.structuredContent !== undefined) return result.structuredContent;
const text = textOf(result);
if (outcome.isError) throw new Error(text || `Tool "${tool.name}" failed`);
return text;
}

toScriptValue() 先返回有 output schema 的 structuredContent,再判断其他结果是否失败。MCP 的 isError: true 因而仍可能正常 resolve。脚本需要检查 result.isError;Promise.allSettled() 只能接住真正的 rejection,不能替代这个判断。源码见 toScriptValue。

1.0 的提示词到底从什么缩到什么

官方给出的整体样本是:默认工具加 Codemode 开启时,GPT-5.6 请求约从 5300 降到 3300 prompt tokens。用这两个约数计算,减少约 37.7%,发布说明概括为约 40%。这个样本描述请求中的提示词,不能直接推成所有 MCP 任务的总费用或耗时都下降 40%。

对应 commit 是 6f1072c。它主要修改提示词的构造方式:合并重复解释,复用已有工具 schema,把完整 API 参考移到按需读取的文档。

执行规则与 helper:逐项说明改成分组短说明

改前的 DESCRIPTION_INTRO 包含 13 条执行规则和 11 条 helper 解释,后面还有独立的工具发现说明。以下是改前源码原文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const DESCRIPTION_INTRO = `Run JavaScript code to orchestrate/compose tool calls
- Evaluates the provided JavaScript code in a fresh QuickJS sandbox as the body of an async function: top-level \`await\` and \`return\` work.
- All nested tools are available on the global \`tools\` object, for example \`await tools.read(...)\`. Tool names are exposed as normalized JavaScript identifiers, for example \`await tools.mcp__ologs__get_profile(...)\`.
- Nested tool methods take an object as their input argument.
- Nested tools return either an object or a string, based on the description.
- A nested tool call that fails, is blocked, or gets invalid arguments rejects with an Error carrying the tool's error text.
- Runs raw JavaScript -- no Node, no file system, no network access, no timers.
- Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences.
- You may optionally start the tool input with a first line like \`// @options: {"max_output_tokens": 1000, "timeout_ms": 60000}\`.
- \`max_output_tokens\` sets the token budget for the script's output. Defaults to 10000 tokens.
- \`timeout_ms\` sets a hard deadline for the whole script. By default there is none.
- When the JS code is fully evaluated, calls that are still running are cancelled and unawaited promises are silently discarded.
- Tool calls are real and have side effects. If the script fails partway, earlier calls are not undone.
- Scripts have a 256 MB memory limit; exceeding it throws \`InternalError: out of memory\`. Filter or aggregate large data instead of accumulating it.

- Global helpers:
- \`exit()\`: Immediately ends the current script successfully (like an early return from the top level).
- \`text(value: string | number | boolean | undefined | null)\`: Appends a text item. Non-string values are stringified with \`JSON.stringify(...)\` when possible.
- \`image(imageUrlOrItem: string | { image_url: string } | ImageContent)\`: Appends an image item. \`image_url\` should be a base64-encoded \`data:\` URL. To forward an MCP tool image, pass an individual \`ImageContent\` block from \`result.content\`, for example \`image(result.content[0])\`.
- \`store(key: string, value: any)\`: stores a serializable value under a string key for later \`codemode\` calls in the same session. Storing \`undefined\` deletes the key. Writes are kept only if the script succeeds.
- \`load(key: string)\`: returns the stored value for a string key, or \`undefined\` if it is missing.
- \`ALL_TOOLS\`: metadata for the enabled nested tools as \`{ name, description }\` entries.
- \`searchTools(query: string, options?: { limit?: number; namespace?: string })\`: resolves to the nested tools that best match the query (BM25, default limit 8), as \`{ name, description }\` entries like \`ALL_TOOLS\`.
- \`describeTool(name: string)\`: resolves to the description and declaration of a nested tool, or \`undefined\`.
- \`describeNamespace(name: string)\`: resolves to \`{ name, description?, instructions?, tools }\` for a namespace of nested tools, such as an MCP server: its usage instructions and the names of its tools, or \`undefined\`.
- \`console.log(...)\` and the other \`console\` methods append a text item like \`text()\`.
- \`return value\` at the top level appends the value like \`text()\`.`;

const DEFERRED_TOOLS_GUIDANCE = `Some nested tools may be omitted from this description, such as deferred tools and MCP tools. They are still available on the global \`tools\` object and listed in \`ALL_TOOLS\`.
To find one, call \`await searchTools(query)\` (pass \`{ namespace }\` to search one namespace), or filter \`ALL_TOOLS\` by \`name\` and \`description\`. \`await describeNamespace(name)\` returns a namespace's usage instructions and the names of its tools.`;

改后,执行方式被合并成一段和两条说明,helper 合并成输出、存储和工具发现三组。启用 models 时,再加一条文档指引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const DESCRIPTION_INTRO = `Run JavaScript that calls other tools. The input is raw JavaScript (not JSON, no code fence), run as an async function body in a QuickJS sandbox: top-level \`await\` and \`return\` work. No Node, file system, network, or timers.
- \`await tools.<name>({ ...args })\` resolves to a string, or an object if the tool's declaration says so, and rejects with an Error on failure. Calls still running when the script ends are cancelled.
- Optional first line: \`// @options: {"max_output_tokens": 10000, "timeout_ms": 60000}\``;

/** One line per global. The details live in {@link CODEMODE_DOCS_PATH}. */
function describeGlobals(models: boolean): string {
const lines = [
"Globals:",
"- `text(value)`, `image(dataUrlOrImageBlock)`, `console.log(...)`, and top-level `return` add output; `exit()` ends the script.",
"- `store(key, value)` and `load(key)` keep JSON values across codemode calls.",
"- `ALL_TOOLS`, `searchTools(query, { limit?, namespace? })`, `describeTool(name)`, `describeNamespace(name)`: find unlisted tools, such as MCP tools.",
];
if (models) {
lines.push(`- \`models\`: classifiers and image generation. Read ${CODEMODE_DOCS_PATH} first.`);
}
return lines.join("\n");
}

这里可以直接看到缩短的内容:text()、image()、console.log() 和 return 合在一起,store() 与 load() 合在一起,几种工具发现方法合在一起。关于内存上限、图片格式、store 写入规则、超时默认值等细节,则放到 codemode.md 中。

对运行时说明字符串统计,改前的 intro 加独立工具发现说明为 3619 字符;改后的 intro 加不含 models 行的 globals 为 878 字符。统计包含两段之间的换行,不包含 models API、嵌套工具声明和共享 MCP 类型,也没有把源码里的反斜杠转义计作运行时字符。这个数字只说明该段文本的变化。改前源码、改后源码

models API:完整类型和方法声明改成文档入口

改前,Codemode 的常驻 description 还会注入完整模型 API。为了看清被移走的具体内容,下面直接列出原来的类型和方法元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const MODEL_TYPES = `type ModelType = "chat" | "image" | "classifier";
/** A model catalog entry. \`provider\` and \`id\` identify it; the other fields depend on the type. */
interface ModelInfo {
type?: ModelType;
provider: string;
id: string;
name: string;
api: string;
input: ("text" | "image")[];
contextWindow?: number;
[key: string]: unknown;
}
type ClassifierQuestion =
| { type: "choice"; instructions: string; criteria: Record<string, string> }
| { type: "score"; instructions: string; criteria: string[] }
| { type: "bool"; instructions: string; criteria: { true: string; false: string } };
type ClassifierAnswer =
| { type: "choice"; choice: string; probabilities: Record<string, number>; confidence: number }
| { type: "score"; score: number; confidence: number }
| { type: "bool"; probability: number };
interface ClassifierContext {
state: Record<string, unknown>;
questions: Record<string, ClassifierQuestion>;
}
/** Token counts reported by the service. Cost is in USD. */
type ModelUsage = { input: number; output: number; totalTokens: number; cost: { total: number } };
interface ClassifierResult {
api: string;
provider: string;
model: string;
answers: Record<string, ClassifierAnswer>;
usage?: ModelUsage;
stopReason: "stop" | "error" | "aborted";
errorMessage?: string;
timestamp: number;
}
type ModelTextBlock = { type: "text"; text: string };
/** \`data\` is base64. Show it with \`image(block)\`; never print \`data\` with \`text()\`, \`console\`, or \`return\`. */
type ModelImageBlock = { type: "image"; data: string; mimeType: string };
interface ImagesContext {
/** The prompt as text blocks, plus image blocks to edit or use as references. */
input: (ModelTextBlock | ModelImageBlock)[];
}
interface ImagesResult {
api: string;
provider: string;
model: string;
/** Generated images, and text blocks for models that also return text. */
output: (ModelTextBlock | ModelImageBlock)[];
usage?: ModelUsage;
stopReason: "stop" | "error" | "aborted";
errorMessage?: string;
timestamp: number;
}`;

/** Declarations of the `models` globals; codemode-execute.ts implements them. */
export const MODEL_GLOBAL_DECLARATIONS: readonly Omit<CodemodeTool, "execute">[] = [
{
name: "models.getModelsOfType",
description: "Every known model of a type, optionally for one provider.",
signature: "(type: ModelType, provider?: string): Promise<ModelInfo[]>",
},
{
name: "models.getAvailableOfType",
description: "Models of a type whose provider has working credentials.",
signature: "(type: ModelType, provider?: string): Promise<ModelInfo[]>",
},
{
name: "models.getModelOfType",
description: "One catalog entry, or undefined.",
signature: "(type: ModelType, provider: string, id: string): Promise<ModelInfo | undefined>",
},
{
name: "models.classify",
description:
"Run a classifier model on one state. Only `provider` and `id` of `model` are used. Provider errors do not throw: check `stopReason` and `errorMessage`.",
signature: "(model: ModelInfo, context: ClassifierContext): Promise<ClassifierResult>",
},
{
name: "models.generateImages",
description:
'Generate images with an image model. Only `provider` and `id` of `model` are used. Provider errors do not throw: check `stopReason` and `errorMessage`. Generation can take minutes, so do not set a short `timeout_ms`. Show results with `for (const block of result.output) if (block.type === "image") image(block);`.',
signature: "(model: ModelInfo, context: ImagesContext): Promise<ImagesResult>",
},
];

这些声明通过下面的代码进入 description:

1
2
3
4
5
6
7
if (options.models) {
const noop = () => undefined;
const models = renderDeclarations({
globals: MODEL_GLOBAL_DECLARATIONS.map((global) => ({ ...global, execute: noop })),
});
sections.push(`Model API:\n\`\`\`ts\n${MODEL_TYPES}\n\n${models}\n\`\`\``);
}

改后删除了这段常驻注入,只在 describeGlobals() 中留下:

1
2
3
if (models) {
lines.push(`- \`models\`: classifiers and image generation. Read ${CODEMODE_DOCS_PATH} first.`);
}

MODEL_TYPES 本身有 54 行,此外还有 5 个方法的签名和说明。它们从每次请求中的 API 参考,变成需要时读取的文档。运行时的 API 仍然存在。改动源码

已声明工具:完整 TypeScript 声明改成调用与返回字段速览

默认 codemode.mode = "on" 时,bash 这样的直接工具已经有正常输入 schema。旧版又在工具 description 中追加一份完整 TypeScript 声明,构造代码是:

1
descriptions[tool.name] = renderToolSample(toCodemodeDeclaration(tool));

以 bash 为例,下面是根据旧版 schema 和声明生成器还原的追加部分。共同保留的自然语言工具说明没有重复列出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// codemode tool declaration:

declare const tools: {
bash(args: {
// Shell command to execute
command: string;
// Timeout in seconds (optional, no default timeout)
timeout?: number;
}): Promise<{
exit_code: number;
// Temp file with the full output, when truncated
full_output_path?: string;
// Combined stdout and stderr, up to 1 MiB. Longer output keeps its first and last 512 KiB around an omission marker.
output: string;
// Whether `output` omits part of the command output
truncated: boolean;
wall_time_seconds: number;
}>;
};

1.0 改用 describeScriptCall(),输出类型通常只列字段名和 optional 标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function describeOutput(schema: CodemodeJsonSchema | undefined): string {
const type = renderToolOutputType(schema);
if (type === "string") return "a string";
const object = typeof schema === "object" ? schema : undefined;
const properties = object?.properties;
if (
object?.type === "object" &&
typeof properties === "object" &&
properties !== null &&
mcpStructuredContentSchema(schema) === undefined
) {
const required = new Set(Array.isArray(object.required) ? object.required : []);
const fields = Object.keys(properties).map((name) => (required.has(name) ? name : `${name}?`));
return `\`{ ${fields.join(", ")} }\``;
}
return `\`${type.replace(/\s+/g, " ")}\``;
}

/**
* A declared tool's description followed by how scripts call it and what the call resolves to. The
* arguments are the tool's declared parameters, so they are not repeated.
*/
function describeScriptCall(tool: AgentTool<any>): string {
return `${tool.description.trim()}\n\nCodemode: \`tools.${toCodemodeIdentifier(tool.name)}(args)\` resolves to ${describeOutput(toCodemodeDeclaration(tool).outputSchema)}.`;
}

生成的 bash 追加说明变成:

1
Codemode: `tools.bash(args)` resolves to `{ output, truncated, full_output_path?, exit_code, wall_time_seconds }`.

模型已经能从正常工具 schema 读到 command 和 timeout,description 只需要补充脚本调用方式及返回值概况。这省掉了重复参数和冗长输出字段注释。旧版声明构造、新版构造

codemode.mode = "only" 的情况不同:模型只通过 Codemode 调用这些工具,正常直接声明被隐藏,Codemode description 会在预算内列出非 deferred 工具的完整嵌套声明。工具创建时默认采用下面的预算:

1
2
export const DEFAULT_CODEMODE_INLINE_BUDGET = 3000;
const CHARS_PER_TOKEN = 4;

这里的 3000 是按字符数除以 4 得到的估算 tokens。声明选择按 namespace 轮流进行,每组优先放成本较低的声明;没有 namespace 的工具也单独成组。超出预算的工具继续通过发现函数查询。因此,完整声明仍然保留在需要它的路径中,但不会无条件列出全部工具。预算与选择算法、创建工具时使用默认预算

MCP 服务器段:固定解释所有路径改成按实际路径生成

旧版的服务器段开头始终说明 Codemode、工具搜索、namespace instructions 和 tool_search:

1
2
const SERVERS_SECTION_INTRO =
"MCP servers whose tools are not declared to you. Call the tools of `codemode` servers from codemode scripts: find them with `searchTools(query, { namespace })` and read a server's instructions and tool names with `describeNamespace(name)`. Load the tools of `tool_search` servers with `tool_search`.";

改后根据当前服务器实际采用的路径拼接:

1
2
3
4
5
6
function serversSectionIntro(reaches: ReadonlySet<string>): string {
let intro = "MCP servers whose tools are not declared to you.";
if (reaches.has("codemode")) intro += " Call the tools of `codemode` servers from codemode scripts.";
if (reaches.has("tool_search")) intro += " Load the tools of `tool_search` servers with `tool_search`.";
return intro;
}

固定 299 字符的说明,变成单一路径 108 字符、两种路径都有时 168 字符。工具发现方法已经在 globals 中出现,服务器段就不再重复详细用法。改前源码、改后源码

系统提示中的使用建议也缩短了

改前:

1
2
3
4
5
6
export const codemodeToolSystemPromptContribution = {
snippet: "Run JavaScript that calls other tools (chains, loops, Promise.all, filtering large results)",
guidelines: [
"Use codemode to batch or chain several tool calls, or to filter large tool output down to what you need, instead of issuing many individual tool calls. Batch independent calls in one codemode call using await Promise.allSettled([...]).",
],
} as const;

改后:

1
2
3
4
5
6
export const codemodeToolSystemPromptContribution = {
snippet: "Run JavaScript that calls other tools",
guidelines: [
"Use codemode to batch independent tool calls (Promise.allSettled), chain them, or filter large output, instead of many separate calls.",
],
} as const;

摘要从 91 字符缩到 37 字符,使用建议从 235 字符缩到 134 字符。独立调用用 Promise.allSettled()、串联调用、筛选大结果这几项建议都保留了,只减少重复解释。

这些修改共同解释了官方 5300 → 3300 prompt tokens 的样本。后续如果需要读取文档,文档内容仍会进入相应请求;真实任务的收益取决于工具规模、调用方式和查询频率。

描述保持稳定,服务器在后台连接

0.99.2 已经把默认 MCP 工具从 Codemode description 中移出。服务器连接完成或工具列表变化时,不需要重写整份 Codemode 描述,也不再把工具数量和完整 instructions 塞进去。1.0 延续了这个结构。

MCP 服务器改为列在 mcp_servers 系统提示段中。renderServersSection() 使用一行摘要,单个摘要最多 250 字符,整个段最多 4096 字符;完整 instructions 通过 describeNamespace() 查询。服务器段源码

服务器段变化时,产品层计算系统提示 section 的差异,把更新追加到会话。稳定前缀有利于缓存复用,但本文没有测量各家 provider 的实际缓存命中率。系统提示差异构造

后台连接也有明确的等待规则:会话启动时,Pi 开始连接所有启用的服务器;首次 prompt 只等待配置了 direct 工具的服务器,默认最多 10 秒。其余连接在工具需要时等待。

脚本需要哪些服务器,当前通过下面的代码判断:

1
2
3
4
function scriptNeedsServer(code: string, server: string): boolean {
if (/\b(searchTools|describeNamespace|describeTool|ALL_TOOLS)\b/.test(code)) return true;
return code.includes(mcpNamespace(server));
}

直接写出某个 mcp__<server> 命名空间,会等待对应服务器。代码中出现 searchTools、describeNamespace、describeTool 或 ALL_TOOLS 时,则等待全部尚在连接的启用服务器。即使给 searchTools() 指定 namespace,这个前置等待也依旧覆盖全部服务器。

这里使用正则和字符串匹配,没有分析 JavaScript 语法树。代码注释中出现这些名字也可能命中。因此,首次请求可以很快发出去,首次搜索工具仍可能承担服务器连接等待。启动与等待实现

工具调用时的 waitForServers() 会等到对应服务器的 ready promise 完成或 AbortSignal 取消;这次按需等待不受首次 prompt 的 10 秒上限约束。按需等待实现

1.0 的纠错提示让模型有明确的恢复路径

说明变短以后,模型可能写错工具名。1.0 为 tools 和 models 等对象加了 Proxy guard:读取不存在的成员时,立即抛出错误,并给出相近名称或可用成员列表。例如 tools.Bash 会提示 tools.bash。

这样做也改变了 JavaScript 探测行为。下面的旧式写法可能直接抛错:

1
typeof tools.some_missing_tool === "function"

需要改成成员存在性检查:

1
"some_missing_tool" in tools

相近名称匹配使用大小写和标点归一化,以及字符串包含关系。它给模型一个修正入口;工具搜索和完整文档仍是另一条恢复路径。Proxy guard 源码

OAuth:少一些反复登录,账号和权限也要存对

远程 MCP 接上以后,常见的麻烦是登录成功了,换一个操作又提示权限不足;或者工作账号和个人账号指向同一个服务,后登录的账号覆盖了前一个。1.0 的 OAuth 改动主要解决这些状态问题。

补权限时保留已经拿到的权限

假设 token 已有 issues:read,服务端返回 insufficient_scope,要求补上 issues:write。这个错误表示当前授权不够用。0.99.2 的适配器只把 challenge 中的 scope 交给下一轮授权,新的 token 可能只剩写权限,下一次读操作又要求登录。

1.0 的 stepUpScope() 合并已有权限和本次要求,再去重。下面是源码原文:

1
2
3
4
5
export function stepUpScope(granted: string | undefined, challenged: string | undefined): string | undefined {
if (!challenged) return undefined;
const scopes = [granted, challenged].flatMap((scope) => scope?.split(/\s+/).filter(Boolean) ?? []);
return [...new Set(scopes)].join(" ");
}

记录旧权限也得跟上。token 响应省略 scope 时,流程保存本次请求的 scope;refresh 响应省略它时,保存旧 token 的 scope。这分别沿用 OAuth 对授权响应和刷新的规则,见 withScope() 的调用。权限提升会跳过 refresh,重新走浏览器授权,因为刷新原来的 grant 无法补出新权限。

仓库的登录测试 明确断言,授权 URL 的 scope 从 issues:read 变成 issues:read issues:write。旧记录如果没有 scope,代码无法还原过去实际授予的权限;challenge 没给 scope 时,函数也会返回 undefined,交给流程选择默认来源。

同一个服务地址可以保留多个账号

McpOAuthCredentialStore 把 mcp-auth.json 的键从 URL 改成 server name 加 URL:

1
2
3
4
function storeKeys(name: string, serverUrl: string): { key: string; legacyKey: string } {
const legacyKey = String(new URL(serverUrl));
return { key: `${mcpNamespace(name)}|${legacyKey}`, legacyKey };
}

于是 work 和 personal 即使连接相同 URL,也有独立的 token、client registration 和刷新锁。升级时,URL-only 旧记录归第一个加载它的 server,迁移在存储锁内完成,随后删除旧键;其他同 URL 配置需要重新登录。测试 还说明,my-work 和 my_work 经 namespace 规范化后算同一个名称。由键的结构可以推断,改名也可能让配置失去原来的凭据关联。

检查授权回调来自哪个 issuer

1.0 在交换 authorization code 之前加入 RFC 9207 的 iss 检查。iss 是回调携带的授权服务器身份,防止把一个服务器签发的 code 送给另一个服务器。浏览器回调和手动粘贴 redirect URL 都会传递它。核心判断见 runFlow():

1
2
3
if (metadata && (iss !== undefined || metadata.authorization_response_iss_parameter_supported)) {
if (iss !== metadata.issuer) throw new OAuthIssuerMismatchError(metadata.issuer, iss);
}

提供了 iss,就必须严格等于 metadata 中的 issuer;服务器声明支持 iss 却没有返回,也拒绝交换。没有声明支持、回调也没有它时,保持兼容。测试 检查了这些组合,并断言错误 code 没有发到 token endpoint。这个判断以存在 metadata 为前提,旧式 fallback 没有 metadata 时,不能据此认定回调 issuer 得到了验证。

Discovery 有问题时可以指定 metadata

oauth.authServerMetadataUrl 让配置直接指向授权服务器的 metadata 文档,适合 MCP 服务报错授权地址或没有可用发现信息的情况。它下传为 authorizeMcp() 的 authorizationServerMetadataUrl。实现 信任这个配置,使用文档中的 issuer 和端点,不把 issuer 与文档 URL 做自动发现时的匹配校验。回调 iss 仍与这个 issuer 比较。

runFlow() 要求该 URL 使用 HTTPS 或 loopback,并在配置覆盖时停用 discovery 缓存,确保改 URL 后立即生效。代价是每次流程都要重新请求 metadata。指定文档返回 404 也会直接报错。这项能力把授权服务器的选择交给配置维护者,不能理解成自动跳过所有认证检查。

空字段不再破坏整条登录流程

有些服务端会返回 scope: ""、refresh_token: "" 或 expires_in: null。1.0 的解析器 将可选字段的空串和 null 按未提供处理,必需的 access_token 等字段仍要校验。过期时间解析 也避免了 Number(null) === 0 导致刚拿到 token 就被当作过期。

这些修正与恢复登录状态有关,但并发 401 共用一次 refresh、避免轮换 refresh token 被重复使用的机制,在 0.99.2 已存在。1.0 补上的是 scope 记录、账号隔离和输入兼容性。本文对照了源码及测试断言,没有执行测试,也没有据此计算登录成功率或延迟改善。

恢复会话时,先保存还没有注册的工具名

tool_search 加载的工具会记录在 transcript,也就是会话记录中。恢复会话或执行 /reload 时,读取工具列表的时间可能早于 MCP 服务器重连完成。工具尚未注册,就会被旧的 active tool 构造过程过滤掉。

1.0 增加 _pendingToolNames,让恢复的工具名先进入待处理集合:

1
2
3
4
5
6
7
8
private _restoreToolsFromTranscript(): void {
this._pendingToolNames.clear();
const current = getCurrentSystemMessage(this.sessionManager.buildSessionContext().messages);
if (!current) return;
const names = (current.toolsAdded ?? []).map((tool) => tool.name);
this._pendingToolNames = new Set(names.filter((name) => this._isAllowedTool(name)));
this._setActiveTools(names);
}

注册工具、重建运行时时,再把这些名字加入下一组 active tools:

1
2
3
4
// Pending tools that are registered now become active.
nextActiveToolNames.push(...this._pendingToolNames);

this._setActiveTools([...new Set(nextActiveToolNames)]);

这个集合有生命周期:工具注册完成后会从 pending 删除;扩展主动移除 active tool 时,会清掉原恢复集合;下一次 agent run 开始时,仍未注册的名字也会清掉。它修复了及时重连时的工具丢失,极慢连接仍可能需要重新发现。

仓库测试分别覆盖了 resume、/reload、扩展修改 loadout,以及服务器在下一次 prompt 之后才连接的情形。修复 commit、测试断言

当前实现还有哪些边界

工具搜索的分词逻辑值得直接看一眼:

1
2
3
4
5
6
7
8
9
export function tokenize(text: string): string[] {
return text
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter((term) => term.length > 0 && !STOP_WORDS.has(term))
.map(stem);
}

split(/[^a-z0-9]+/) 只保留 ASCII 字母和数字。实际传给 searchTools() 或 tool_search 的纯中文 query 会没有词项,BM25 随即返回空匹配。中文对话仍可以由模型转换成英文关键词、使用工具标识符,或改走 describeNamespace()、describeTool() 和 ALL_TOOLS。当前这两条检索路径内没有自动翻译或 embedding fallback。分词与排序源码

传输层支持 stdio 和 Streamable HTTP。stdio 是启动本地进程,通过标准输入输出交换协议消息;Streamable HTTP 用于远程服务器。1.0.0 不支持旧式 SSE transport,也不渲染 MCP Apps 的 ui:// 资源。MCP 文档、资源边界

QuickJS 隔离的是脚本运行环境。脚本仍能通过注入工具产生真实外部操作,脚本失败也不会撤销已完成的调用。参数校验和权限扩展继续留在正常工具流水线上。脚本执行说明

1.0 加入官方 MCP conformance suite 的 CI,但它按已提交的测试基线检查回归。覆盖的协议版本为 2025-03-26、2025-06-18 和 2025-11-25;1.0.0 仍有 elicitation、CIMD 等已知缺口,也没有实现 2026-07-28 的 stateless 协议。后续版本文档里的新增能力,需要单独核对 tag。测试范围说明

按使用方式选择工具曝光

接入时可以按工具使用方式配置 exposure。下面是一个示例,URL 是占位地址,工具名和通配符需要按实际服务器调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"mcpServers": {
"tracker": {
"url": "https://example.com/mcp",
"description": "Search and read issue tracker data",
"exposure": "codemode",
"toolExposure": {
"search_issues": "direct",
"list_*": "codemode",
"delete_*": "hidden"
}
}
}
}

少量、经常直接调用的工具可以提前声明;适合批量处理和筛选结果的工具可以留给 Codemode;希望发现后再直接调用的工具可以使用 deferred。hidden 控制工具在当前 Agent 中是否可达,服务器端的账号授权仍需独立配置。

阅读这套实现时,可以沿着 mcp/index.ts 看注册与等待,沿着 codemode/tool.ts 看模型能读到的说明,再沿着 codemode/execute.ts 和 mcp/tools.ts 看调用与结果转换。这样能把服务器连接、模型上下文和脚本执行三个过程对应起来。

0%
\ No newline at end of file +Pi Agent 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩 | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

Pi Agent 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩

给 Agent 接上几个 MCP 服务器以后,很容易碰到一个问题:还没真正调用工具,请求里就已经带上了一大批工具名字、参数和使用说明。等工具开始返回数据,一份很长的 JSON 又进入上下文,后面的模型请求还要继续带着它。

前面写过 Pi Agent 的工具系统和 Pi Agent 的双层主循环。这次继续看 2026 年 10 月 1 日发布的 Pi 1.0,重点是 MCP 工具怎样进入模型请求,以及 Pi 怎样减少这部分上下文开销。

MCP(Model Context Protocol)约定了客户端怎样连接服务器、发现工具、调用工具和读取资源。接入协议以后,Agent 还要决定哪些工具提前告诉模型、工具调用怎样编排、结果保留多少。Pi 这次改动主要落在这些位置。

官方提到 Codemode 的提示词约减少 40%。这个数字很容易让人想知道:到底删了什么,原来的提示词是什么样,缩短后模型又靠什么找到工具?下文会把改前、改后的代码直接放在一起。

先确认版本:MCP 在 0.99.0 已经进入内置扩展

Pi 1.0 的发布文章把 MCP、Codemode 和延迟工具加载放在一起介绍。按具体 tag 的变更记录看,这套能力分几次进入仓库:
版本日期MCP 相关变化
0.99.02026-09-29首次内置 MCP、Codemode 和 tool_search
0.99.22026-09-30默认 MCP 工具按需发现;工具描述不再随服务器连接变化;首次请求只等待 direct 服务器
1.0.02026-10-01进一步压缩固定提示词,增加纠错提示,修复 OAuth 和恢复已加载工具的问题

版本归属可以在固定到 1.0.0 的 CHANGELOG 中核对。官方 “You Said No MCP!” 也解释了接入思路:工具需要容易发现、能够返回结构化数据,并能像函数一样组合调用。

本文的主要分析对象固定为:

1
2
3
4
5
6
7
repository: https://github.com/earendil-works/pi
tag: v1.0.0
commit: a13d35a742c6ef8462812a28fbe1d8c8b7431c32

提示词压缩的具体对照:
before: 88ff80b986e34d4fbd1fa94a4df65c60ae964516
after: 6f1072cc081f06b86a673bd142f03720d17afe15

before 是提示词压缩 commit 的父版本。它已经包含图像生成 API,因此不能拿 0.99.2 的整段提示词直接替代这个对照。本文后面的压缩代码来自该 commit 前后;其余实现以 1.0.0 为准。

本文根据源码、commit diff 和仓库测试断言分析。没有运行真实 MCP 服务或复现官方的 provider token 样本。字符数对照来自提示字符串统计,官方 token 数会单独标明。

工具已经注册,模型仍可以暂时看不到它

过去比较直接的做法,是连接服务器、拿到工具列表,然后把所有工具 schema 都交给模型。schema 在这里指输入参数及其类型、必填字段等说明。

Pi 为工具增加了不同的 exposure,也就是模型通过什么路径接触工具:
exposure默认行为适合的使用方式
codemodeMCP 默认值;工具可从脚本调用,完整声明不预先进入模型请求在脚本里组合、并行或筛选 MCP 调用
deferred工具被 tool_search 找到后,在下一次模型请求中声明按需加载后直接调用
direct工具直接声明给模型,也可以从 Codemode 调用少量经常使用的工具
hidden工具不可调用关闭某些服务器或工具能力

服务器级的 exposure 还可以被单个工具的 toolExposure 覆盖。比如把查询工具交给 Codemode,把某个常用工具直接声明,把不需要的工具隐藏。固定版本配置说明

这里还有一层名称转换:MCP 配置中的 codemode 会被映射为通用工具层的 deferred。两种 MCP 配置都不会把完整声明预先放进 Codemode description;MCP 扩展再根据配置决定激活 Codemode 还是 tool_search,供模型发现这些工具:

1
2
3
export function toToolExposure(exposure: McpExposure): ToolExposure {
return exposure === "codemode" ? "deferred" : exposure;
}

因此,下面通用工具层的 codemode 分支与 MCP 配置中的同名值处在不同层次。默认 MCP 工具在这一步落入 deferred 分支。源码见 toToolExposure。

产品层把已注册工具、脚本可调用工具和模型直接看到的工具分开。AgentSession._getCallableTools() 负责计算脚本可调用的集合:

1
2
3
4
5
6
private _getCallableTools(active: ReadonlySet<string> = new Set(this.getActiveToolNames())): AgentTool[] {
return [...this._toolRegistry.values()].filter((tool) => {
const exposure = this._getToolExposure(tool.name);
return exposure === "codemode" || exposure === "deferred" || (exposure === "direct" && active.has(tool.name));
});
}

codemode 和 deferred 工具只要注册完成,就能走脚本调用路径。direct 工具还要求它在 active tool 集合中。这样就可以把大量工具留在运行时,同时控制进入模型请求的声明数量。源码见 agent-session.ts。

searchTools 与 tool_search 做了两件不同的事

默认不把所有 schema 塞进请求以后,模型需要有地方查工具。Pi 提供了脚本内查询和模型工具加载两条路径。

searchTools() 是 Codemode 脚本中的查询函数。它搜索可调用工具,返回匹配项的名称和描述;这里的描述实际包含生成后的 TypeScript 声明。因此,它能按需拿到参数结构。调用方式可以写成下面这样,mcp__tracker 只是示例命名空间:

1
2
3
4
5
6
text(await searchTools("search issues", {
namespace: "mcp__tracker",
limit: 3,
}));

text(await describeNamespace("mcp__tracker"));

describeNamespace() 返回服务器的 instructions 和工具名,describeTool() 按具体名称返回工具描述及声明。这些数据先进入脚本,只有通过 text() 或 return 输出的部分才会进入模型上下文。源码见 createDiscoveryGlobals。

tool_search 则是模型可以直接调用的工具。它查找尚未加载的工具,将匹配项加入 active tools,从下一次模型请求开始声明。已经加载的工具会记录在会话中。源码见 searchAndLoad。

这两个接口的区别会影响上下文:脚本里查询并调用工具,可以一直把处理留在脚本中;通过 tool_search 加载以后,模型会开始直接接收匹配工具的声明。

当前内置检索器采用 BM25,即根据查询词和工具说明中的词项匹配程度排序。检索内容包括工具名、description、输入 schema 的属性名和说明,以及 namespace 的描述与 instructions。Codemode 的 searchTools() 默认返回 8 个匹配项。检索实现

Codemode 把完整结果留在脚本中

Codemode 让模型写一段 JavaScript,再在 QuickJS 沙箱中运行。QuickJS 是一个可以编译成 WebAssembly 的 JavaScript 引擎;脚本没有 Node API、文件系统、网络或计时器,需要通过注入的工具访问外部能力。

一次 MCP 调用的路径可以按下面的顺序读:

1
2
3
4
5
6
7
8
模型生成 JavaScript
→ QuickJS 运行脚本
→ tools.<name>(args)
→ ctx.executeTool()
→ MCP 服务器
→ 完整结果回到脚本
→ 脚本筛选、聚合
→ text() / return 输出给模型

executeCodemode() 没有直接绕过产品层调用 MCP 客户端。它把嵌套调用交给 ctx.executeTool()。下面是其中连续的调用片段:

1
2
3
const outcome = await ctx.executeTool(tool.name, args, { signal: callSignal });
record.id = outcome.toolCall.id;
record.durationMs = performance.now() - callStartedAt;

这让嵌套调用继续经过正常工具流水线,参数校验和扩展的 tool_call、tool_result 回调仍然生效。配置了权限扩展时,扩展也能拦截这些调用。最终发送给模型的是脚本输出,并不会自动把每个嵌套 MCP 结果全文放进对话。源码见 executeCodemode。

例如,需要查询一批 issue,再读取各自的评论,可以在脚本里并行取数据、筛选、排序,只把需要的条目交给模型。这是用于说明机制的场景,本文没有对它测量耗时或 token。

MCP 结果进入产品层以后,convertMcpResult() 会生成两种视图:模型可以看的 content,以及脚本可以拿到的完整结构化结果。下面是源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export async function convertMcpResult(
server: string,
tool: string,
result: CallToolResult,
options: ConvertMcpResultOptions = {},
): Promise<AgentToolResult<McpToolDetails>> {
// Without content blocks, toLlmContent falls back to the structured content as JSON.
const converted: (TextContent | ImageContent)[] =
result.content.length > 0 ? await toModelContent(server, result.content, options) : toLlmContent(result);
if (result.isError && textOf(converted) === "") {
converted.push({ type: "text", text: `MCP tool ${server}/${tool} returned an error` });
}
const { content, fullOutputPath } = await limitMcpContent(converted, options.saveOutput);
const { _meta: _ignored, ...scriptResult } = result;
return {
content,
details: { server, tool, ...(fullOutputPath ? { fullOutputPath } : {}) },
structuredContent: scriptResult as unknown as JsonValue,
...(result.isError ? { isError: true } : {}),
};
}

limitMcpContent() 限制直接给模型的文本,默认上限为 20KB,超出后保留首尾、把完整文本写到临时文件。structuredContent 则保留服务器的 CallToolResult,只移除 _meta。源码见 MCP 结果转换。

因此,脚本能拿到 content、structuredContent 和 isError,先处理数据再输出。服务器能提供结构化字段时,这种写法比较容易组合;如果结果只有一大段自然语言,脚本还需要自己解析它。

这里有一个实际写脚本时要留意的返回规则:

1
2
3
4
5
6
7
function toScriptValue(tool: AgentTool<any>, outcome: AgentToolCallOutcome): unknown {
const { result } = outcome;
if (tool.outputSchema && result.structuredContent !== undefined) return result.structuredContent;
const text = textOf(result);
if (outcome.isError) throw new Error(text || `Tool "${tool.name}" failed`);
return text;
}

toScriptValue() 先返回有 output schema 的 structuredContent,再判断其他结果是否失败。MCP 的 isError: true 因而仍可能正常 resolve。脚本需要检查 result.isError;Promise.allSettled() 只能接住真正的 rejection,不能替代这个判断。源码见 toScriptValue。

1.0 的提示词到底从什么缩到什么

官方给出的整体样本是:默认工具加 Codemode 开启时,GPT-5.6 请求约从 5300 降到 3300 prompt tokens。用这两个约数计算,减少约 37.7%,发布说明概括为约 40%。这个样本描述请求中的提示词,不能直接推成所有 MCP 任务的总费用或耗时都下降 40%。

对应 commit 是 6f1072c。它主要修改提示词的构造方式:合并重复解释,复用已有工具 schema,把完整 API 参考移到按需读取的文档。

执行规则与 helper:逐项说明改成分组短说明

改前的 DESCRIPTION_INTRO 包含 13 条执行规则和 11 条 helper 解释,后面还有独立的工具发现说明。以下是改前源码原文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const DESCRIPTION_INTRO = `Run JavaScript code to orchestrate/compose tool calls
- Evaluates the provided JavaScript code in a fresh QuickJS sandbox as the body of an async function: top-level \`await\` and \`return\` work.
- All nested tools are available on the global \`tools\` object, for example \`await tools.read(...)\`. Tool names are exposed as normalized JavaScript identifiers, for example \`await tools.mcp__ologs__get_profile(...)\`.
- Nested tool methods take an object as their input argument.
- Nested tools return either an object or a string, based on the description.
- A nested tool call that fails, is blocked, or gets invalid arguments rejects with an Error carrying the tool's error text.
- Runs raw JavaScript -- no Node, no file system, no network access, no timers.
- Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences.
- You may optionally start the tool input with a first line like \`// @options: {"max_output_tokens": 1000, "timeout_ms": 60000}\`.
- \`max_output_tokens\` sets the token budget for the script's output. Defaults to 10000 tokens.
- \`timeout_ms\` sets a hard deadline for the whole script. By default there is none.
- When the JS code is fully evaluated, calls that are still running are cancelled and unawaited promises are silently discarded.
- Tool calls are real and have side effects. If the script fails partway, earlier calls are not undone.
- Scripts have a 256 MB memory limit; exceeding it throws \`InternalError: out of memory\`. Filter or aggregate large data instead of accumulating it.

- Global helpers:
- \`exit()\`: Immediately ends the current script successfully (like an early return from the top level).
- \`text(value: string | number | boolean | undefined | null)\`: Appends a text item. Non-string values are stringified with \`JSON.stringify(...)\` when possible.
- \`image(imageUrlOrItem: string | { image_url: string } | ImageContent)\`: Appends an image item. \`image_url\` should be a base64-encoded \`data:\` URL. To forward an MCP tool image, pass an individual \`ImageContent\` block from \`result.content\`, for example \`image(result.content[0])\`.
- \`store(key: string, value: any)\`: stores a serializable value under a string key for later \`codemode\` calls in the same session. Storing \`undefined\` deletes the key. Writes are kept only if the script succeeds.
- \`load(key: string)\`: returns the stored value for a string key, or \`undefined\` if it is missing.
- \`ALL_TOOLS\`: metadata for the enabled nested tools as \`{ name, description }\` entries.
- \`searchTools(query: string, options?: { limit?: number; namespace?: string })\`: resolves to the nested tools that best match the query (BM25, default limit 8), as \`{ name, description }\` entries like \`ALL_TOOLS\`.
- \`describeTool(name: string)\`: resolves to the description and declaration of a nested tool, or \`undefined\`.
- \`describeNamespace(name: string)\`: resolves to \`{ name, description?, instructions?, tools }\` for a namespace of nested tools, such as an MCP server: its usage instructions and the names of its tools, or \`undefined\`.
- \`console.log(...)\` and the other \`console\` methods append a text item like \`text()\`.
- \`return value\` at the top level appends the value like \`text()\`.`;

const DEFERRED_TOOLS_GUIDANCE = `Some nested tools may be omitted from this description, such as deferred tools and MCP tools. They are still available on the global \`tools\` object and listed in \`ALL_TOOLS\`.
To find one, call \`await searchTools(query)\` (pass \`{ namespace }\` to search one namespace), or filter \`ALL_TOOLS\` by \`name\` and \`description\`. \`await describeNamespace(name)\` returns a namespace's usage instructions and the names of its tools.`;

改后,执行方式被合并成一段和两条说明,helper 合并成输出、存储和工具发现三组。启用 models 时,再加一条文档指引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const DESCRIPTION_INTRO = `Run JavaScript that calls other tools. The input is raw JavaScript (not JSON, no code fence), run as an async function body in a QuickJS sandbox: top-level \`await\` and \`return\` work. No Node, file system, network, or timers.
- \`await tools.<name>({ ...args })\` resolves to a string, or an object if the tool's declaration says so, and rejects with an Error on failure. Calls still running when the script ends are cancelled.
- Optional first line: \`// @options: {"max_output_tokens": 10000, "timeout_ms": 60000}\``;

/** One line per global. The details live in {@link CODEMODE_DOCS_PATH}. */
function describeGlobals(models: boolean): string {
const lines = [
"Globals:",
"- `text(value)`, `image(dataUrlOrImageBlock)`, `console.log(...)`, and top-level `return` add output; `exit()` ends the script.",
"- `store(key, value)` and `load(key)` keep JSON values across codemode calls.",
"- `ALL_TOOLS`, `searchTools(query, { limit?, namespace? })`, `describeTool(name)`, `describeNamespace(name)`: find unlisted tools, such as MCP tools.",
];
if (models) {
lines.push(`- \`models\`: classifiers and image generation. Read ${CODEMODE_DOCS_PATH} first.`);
}
return lines.join("\n");
}

这里可以直接看到缩短的内容:text()、image()、console.log() 和 return 合在一起,store() 与 load() 合在一起,几种工具发现方法合在一起。关于内存上限、图片格式、store 写入规则、超时默认值等细节,则放到 codemode.md 中。

对运行时说明字符串统计,改前的 intro 加独立工具发现说明为 3619 字符;改后的 intro 加不含 models 行的 globals 为 878 字符。统计包含两段之间的换行,不包含 models API、嵌套工具声明和共享 MCP 类型,也没有把源码里的反斜杠转义计作运行时字符。这个数字只说明该段文本的变化。改前源码、改后源码

models API:完整类型和方法声明改成文档入口

改前,Codemode 的常驻 description 还会注入完整模型 API。为了看清被移走的具体内容,下面直接列出原来的类型和方法元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const MODEL_TYPES = `type ModelType = "chat" | "image" | "classifier";
/** A model catalog entry. \`provider\` and \`id\` identify it; the other fields depend on the type. */
interface ModelInfo {
type?: ModelType;
provider: string;
id: string;
name: string;
api: string;
input: ("text" | "image")[];
contextWindow?: number;
[key: string]: unknown;
}
type ClassifierQuestion =
| { type: "choice"; instructions: string; criteria: Record<string, string> }
| { type: "score"; instructions: string; criteria: string[] }
| { type: "bool"; instructions: string; criteria: { true: string; false: string } };
type ClassifierAnswer =
| { type: "choice"; choice: string; probabilities: Record<string, number>; confidence: number }
| { type: "score"; score: number; confidence: number }
| { type: "bool"; probability: number };
interface ClassifierContext {
state: Record<string, unknown>;
questions: Record<string, ClassifierQuestion>;
}
/** Token counts reported by the service. Cost is in USD. */
type ModelUsage = { input: number; output: number; totalTokens: number; cost: { total: number } };
interface ClassifierResult {
api: string;
provider: string;
model: string;
answers: Record<string, ClassifierAnswer>;
usage?: ModelUsage;
stopReason: "stop" | "error" | "aborted";
errorMessage?: string;
timestamp: number;
}
type ModelTextBlock = { type: "text"; text: string };
/** \`data\` is base64. Show it with \`image(block)\`; never print \`data\` with \`text()\`, \`console\`, or \`return\`. */
type ModelImageBlock = { type: "image"; data: string; mimeType: string };
interface ImagesContext {
/** The prompt as text blocks, plus image blocks to edit or use as references. */
input: (ModelTextBlock | ModelImageBlock)[];
}
interface ImagesResult {
api: string;
provider: string;
model: string;
/** Generated images, and text blocks for models that also return text. */
output: (ModelTextBlock | ModelImageBlock)[];
usage?: ModelUsage;
stopReason: "stop" | "error" | "aborted";
errorMessage?: string;
timestamp: number;
}`;

/** Declarations of the `models` globals; codemode-execute.ts implements them. */
export const MODEL_GLOBAL_DECLARATIONS: readonly Omit<CodemodeTool, "execute">[] = [
{
name: "models.getModelsOfType",
description: "Every known model of a type, optionally for one provider.",
signature: "(type: ModelType, provider?: string): Promise<ModelInfo[]>",
},
{
name: "models.getAvailableOfType",
description: "Models of a type whose provider has working credentials.",
signature: "(type: ModelType, provider?: string): Promise<ModelInfo[]>",
},
{
name: "models.getModelOfType",
description: "One catalog entry, or undefined.",
signature: "(type: ModelType, provider: string, id: string): Promise<ModelInfo | undefined>",
},
{
name: "models.classify",
description:
"Run a classifier model on one state. Only `provider` and `id` of `model` are used. Provider errors do not throw: check `stopReason` and `errorMessage`.",
signature: "(model: ModelInfo, context: ClassifierContext): Promise<ClassifierResult>",
},
{
name: "models.generateImages",
description:
'Generate images with an image model. Only `provider` and `id` of `model` are used. Provider errors do not throw: check `stopReason` and `errorMessage`. Generation can take minutes, so do not set a short `timeout_ms`. Show results with `for (const block of result.output) if (block.type === "image") image(block);`.',
signature: "(model: ModelInfo, context: ImagesContext): Promise<ImagesResult>",
},
];

这些声明通过下面的代码进入 description:

1
2
3
4
5
6
7
if (options.models) {
const noop = () => undefined;
const models = renderDeclarations({
globals: MODEL_GLOBAL_DECLARATIONS.map((global) => ({ ...global, execute: noop })),
});
sections.push(`Model API:\n\`\`\`ts\n${MODEL_TYPES}\n\n${models}\n\`\`\``);
}

改后删除了这段常驻注入,只在 describeGlobals() 中留下:

1
2
3
if (models) {
lines.push(`- \`models\`: classifiers and image generation. Read ${CODEMODE_DOCS_PATH} first.`);
}

MODEL_TYPES 本身有 54 行,此外还有 5 个方法的签名和说明。它们从每次请求中的 API 参考,变成需要时读取的文档。运行时的 API 仍然存在。改动源码

已声明工具:完整 TypeScript 声明改成调用与返回字段速览

默认 codemode.mode = "on" 时,bash 这样的直接工具已经有正常输入 schema。旧版又在工具 description 中追加一份完整 TypeScript 声明,构造代码是:

1
descriptions[tool.name] = renderToolSample(toCodemodeDeclaration(tool));

以 bash 为例,下面是根据旧版 schema 和声明生成器还原的追加部分。共同保留的自然语言工具说明没有重复列出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// codemode tool declaration:

declare const tools: {
bash(args: {
// Shell command to execute
command: string;
// Timeout in seconds (optional, no default timeout)
timeout?: number;
}): Promise<{
exit_code: number;
// Temp file with the full output, when truncated
full_output_path?: string;
// Combined stdout and stderr, up to 1 MiB. Longer output keeps its first and last 512 KiB around an omission marker.
output: string;
// Whether `output` omits part of the command output
truncated: boolean;
wall_time_seconds: number;
}>;
};

1.0 改用 describeScriptCall(),输出类型通常只列字段名和 optional 标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function describeOutput(schema: CodemodeJsonSchema | undefined): string {
const type = renderToolOutputType(schema);
if (type === "string") return "a string";
const object = typeof schema === "object" ? schema : undefined;
const properties = object?.properties;
if (
object?.type === "object" &&
typeof properties === "object" &&
properties !== null &&
mcpStructuredContentSchema(schema) === undefined
) {
const required = new Set(Array.isArray(object.required) ? object.required : []);
const fields = Object.keys(properties).map((name) => (required.has(name) ? name : `${name}?`));
return `\`{ ${fields.join(", ")} }\``;
}
return `\`${type.replace(/\s+/g, " ")}\``;
}

/**
* A declared tool's description followed by how scripts call it and what the call resolves to. The
* arguments are the tool's declared parameters, so they are not repeated.
*/
function describeScriptCall(tool: AgentTool<any>): string {
return `${tool.description.trim()}\n\nCodemode: \`tools.${toCodemodeIdentifier(tool.name)}(args)\` resolves to ${describeOutput(toCodemodeDeclaration(tool).outputSchema)}.`;
}

生成的 bash 追加说明变成:

1
Codemode: `tools.bash(args)` resolves to `{ output, truncated, full_output_path?, exit_code, wall_time_seconds }`.

模型已经能从正常工具 schema 读到 command 和 timeout,description 只需要补充脚本调用方式及返回值概况。这省掉了重复参数和冗长输出字段注释。旧版声明构造、新版构造

codemode.mode = "only" 的情况不同:模型只通过 Codemode 调用这些工具,正常直接声明被隐藏,Codemode description 会在预算内列出非 deferred 工具的完整嵌套声明。工具创建时默认采用下面的预算:

1
2
export const DEFAULT_CODEMODE_INLINE_BUDGET = 3000;
const CHARS_PER_TOKEN = 4;

这里的 3000 是按字符数除以 4 得到的估算 tokens。声明选择按 namespace 轮流进行,每组优先放成本较低的声明;没有 namespace 的工具也单独成组。超出预算的工具继续通过发现函数查询。因此,完整声明仍然保留在需要它的路径中,但不会无条件列出全部工具。预算与选择算法、创建工具时使用默认预算

MCP 服务器段:固定解释所有路径改成按实际路径生成

旧版的服务器段开头始终说明 Codemode、工具搜索、namespace instructions 和 tool_search:

1
2
const SERVERS_SECTION_INTRO =
"MCP servers whose tools are not declared to you. Call the tools of `codemode` servers from codemode scripts: find them with `searchTools(query, { namespace })` and read a server's instructions and tool names with `describeNamespace(name)`. Load the tools of `tool_search` servers with `tool_search`.";

改后根据当前服务器实际采用的路径拼接:

1
2
3
4
5
6
function serversSectionIntro(reaches: ReadonlySet<string>): string {
let intro = "MCP servers whose tools are not declared to you.";
if (reaches.has("codemode")) intro += " Call the tools of `codemode` servers from codemode scripts.";
if (reaches.has("tool_search")) intro += " Load the tools of `tool_search` servers with `tool_search`.";
return intro;
}

固定 299 字符的说明,变成单一路径 108 字符、两种路径都有时 168 字符。工具发现方法已经在 globals 中出现,服务器段就不再重复详细用法。改前源码、改后源码

系统提示中的使用建议也缩短了

改前:

1
2
3
4
5
6
export const codemodeToolSystemPromptContribution = {
snippet: "Run JavaScript that calls other tools (chains, loops, Promise.all, filtering large results)",
guidelines: [
"Use codemode to batch or chain several tool calls, or to filter large tool output down to what you need, instead of issuing many individual tool calls. Batch independent calls in one codemode call using await Promise.allSettled([...]).",
],
} as const;

改后:

1
2
3
4
5
6
export const codemodeToolSystemPromptContribution = {
snippet: "Run JavaScript that calls other tools",
guidelines: [
"Use codemode to batch independent tool calls (Promise.allSettled), chain them, or filter large output, instead of many separate calls.",
],
} as const;

摘要从 91 字符缩到 37 字符,使用建议从 235 字符缩到 134 字符。独立调用用 Promise.allSettled()、串联调用、筛选大结果这几项建议都保留了,只减少重复解释。

这些修改共同解释了官方 5300 → 3300 prompt tokens 的样本。后续如果需要读取文档,文档内容仍会进入相应请求;真实任务的收益取决于工具规模、调用方式和查询频率。

描述保持稳定,服务器在后台连接

0.99.2 已经把默认 MCP 工具从 Codemode description 中移出。服务器连接完成或工具列表变化时,不需要重写整份 Codemode 描述,也不再把工具数量和完整 instructions 塞进去。1.0 延续了这个结构。

MCP 服务器改为列在 mcp_servers 系统提示段中。renderServersSection() 使用一行摘要,单个摘要最多 250 字符,整个段最多 4096 字符;完整 instructions 通过 describeNamespace() 查询。服务器段源码

服务器段变化时,产品层计算系统提示 section 的差异,把更新追加到会话。稳定前缀有利于缓存复用,但本文没有测量各家 provider 的实际缓存命中率。系统提示差异构造

后台连接也有明确的等待规则:会话启动时,Pi 开始连接所有启用的服务器;首次 prompt 只等待配置了 direct 工具的服务器,默认最多 10 秒。其余连接在工具需要时等待。

脚本需要哪些服务器,当前通过下面的代码判断:

1
2
3
4
function scriptNeedsServer(code: string, server: string): boolean {
if (/\b(searchTools|describeNamespace|describeTool|ALL_TOOLS)\b/.test(code)) return true;
return code.includes(mcpNamespace(server));
}

直接写出某个 mcp__<server> 命名空间,会等待对应服务器。代码中出现 searchTools、describeNamespace、describeTool 或 ALL_TOOLS 时,则等待全部尚在连接的启用服务器。即使给 searchTools() 指定 namespace,这个前置等待也依旧覆盖全部服务器。

这里使用正则和字符串匹配,没有分析 JavaScript 语法树。代码注释中出现这些名字也可能命中。因此,首次请求可以很快发出去,首次搜索工具仍可能承担服务器连接等待。启动与等待实现

工具调用时的 waitForServers() 会等到对应服务器的 ready promise 完成或 AbortSignal 取消;这次按需等待不受首次 prompt 的 10 秒上限约束。按需等待实现

1.0 的纠错提示让模型有明确的恢复路径

说明变短以后,模型可能写错工具名。1.0 为 tools 和 models 等对象加了 Proxy guard:读取不存在的成员时,立即抛出错误,并给出相近名称或可用成员列表。例如 tools.Bash 会提示 tools.bash。

这样做也改变了 JavaScript 探测行为。下面的旧式写法可能直接抛错:

1
typeof tools.some_missing_tool === "function"

需要改成成员存在性检查:

1
"some_missing_tool" in tools

相近名称匹配使用大小写和标点归一化,以及字符串包含关系。它给模型一个修正入口;工具搜索和完整文档仍是另一条恢复路径。Proxy guard 源码

OAuth:少一些反复登录,账号和权限也要存对

远程 MCP 接上以后,常见的麻烦是登录成功了,换一个操作又提示权限不足;或者工作账号和个人账号指向同一个服务,后登录的账号覆盖了前一个。1.0 的 OAuth 改动主要解决这些状态问题。

补权限时保留已经拿到的权限

假设 token 已有 issues:read,服务端返回 insufficient_scope,要求补上 issues:write。这个错误表示当前授权不够用。0.99.2 的适配器只把 challenge 中的 scope 交给下一轮授权,新的 token 可能只剩写权限,下一次读操作又要求登录。

1.0 的 stepUpScope() 合并已有权限和本次要求,再去重。下面是源码原文:

1
2
3
4
5
export function stepUpScope(granted: string | undefined, challenged: string | undefined): string | undefined {
if (!challenged) return undefined;
const scopes = [granted, challenged].flatMap((scope) => scope?.split(/\s+/).filter(Boolean) ?? []);
return [...new Set(scopes)].join(" ");
}

记录旧权限也得跟上。token 响应省略 scope 时,流程保存本次请求的 scope;refresh 响应省略它时,保存旧 token 的 scope。这分别沿用 OAuth 对授权响应和刷新的规则,见 withScope() 的调用。权限提升会跳过 refresh,重新走浏览器授权,因为刷新原来的 grant 无法补出新权限。

仓库的登录测试 明确断言,授权 URL 的 scope 从 issues:read 变成 issues:read issues:write。旧记录如果没有 scope,代码无法还原过去实际授予的权限;challenge 没给 scope 时,函数也会返回 undefined,交给流程选择默认来源。

同一个服务地址可以保留多个账号

McpOAuthCredentialStore 把 mcp-auth.json 的键从 URL 改成 server name 加 URL:

1
2
3
4
function storeKeys(name: string, serverUrl: string): { key: string; legacyKey: string } {
const legacyKey = String(new URL(serverUrl));
return { key: `${mcpNamespace(name)}|${legacyKey}`, legacyKey };
}

于是 work 和 personal 即使连接相同 URL,也有独立的 token、client registration 和刷新锁。升级时,URL-only 旧记录归第一个加载它的 server,迁移在存储锁内完成,随后删除旧键;其他同 URL 配置需要重新登录。测试 还说明,my-work 和 my_work 经 namespace 规范化后算同一个名称。由键的结构可以推断,改名也可能让配置失去原来的凭据关联。

检查授权回调来自哪个 issuer

1.0 在交换 authorization code 之前加入 RFC 9207 的 iss 检查。iss 是回调携带的授权服务器身份,防止把一个服务器签发的 code 送给另一个服务器。浏览器回调和手动粘贴 redirect URL 都会传递它。核心判断见 runFlow():

1
2
3
if (metadata && (iss !== undefined || metadata.authorization_response_iss_parameter_supported)) {
if (iss !== metadata.issuer) throw new OAuthIssuerMismatchError(metadata.issuer, iss);
}

提供了 iss,就必须严格等于 metadata 中的 issuer;服务器声明支持 iss 却没有返回,也拒绝交换。没有声明支持、回调也没有它时,保持兼容。测试 检查了这些组合,并断言错误 code 没有发到 token endpoint。这个判断以存在 metadata 为前提,旧式 fallback 没有 metadata 时,不能据此认定回调 issuer 得到了验证。

Discovery 有问题时可以指定 metadata

oauth.authServerMetadataUrl 让配置直接指向授权服务器的 metadata 文档,适合 MCP 服务报错授权地址或没有可用发现信息的情况。它下传为 authorizeMcp() 的 authorizationServerMetadataUrl。实现 信任这个配置,使用文档中的 issuer 和端点,不把 issuer 与文档 URL 做自动发现时的匹配校验。回调 iss 仍与这个 issuer 比较。

runFlow() 要求该 URL 使用 HTTPS 或 loopback,并在配置覆盖时停用 discovery 缓存,确保改 URL 后立即生效。代价是每次流程都要重新请求 metadata。指定文档返回 404 也会直接报错。这项能力把授权服务器的选择交给配置维护者,不能理解成自动跳过所有认证检查。

空字段不再破坏整条登录流程

有些服务端会返回 scope: ""、refresh_token: "" 或 expires_in: null。1.0 的解析器 将可选字段的空串和 null 按未提供处理,必需的 access_token 等字段仍要校验。过期时间解析 也避免了 Number(null) === 0 导致刚拿到 token 就被当作过期。

这些修正与恢复登录状态有关,但并发 401 共用一次 refresh、避免轮换 refresh token 被重复使用的机制,在 0.99.2 已存在。1.0 补上的是 scope 记录、账号隔离和输入兼容性。本文对照了源码及测试断言,没有执行测试,也没有据此计算登录成功率或延迟改善。

恢复会话时,先保存还没有注册的工具名

tool_search 加载的工具会记录在 transcript,也就是会话记录中。恢复会话或执行 /reload 时,读取工具列表的时间可能早于 MCP 服务器重连完成。工具尚未注册,就会被旧的 active tool 构造过程过滤掉。

1.0 增加 _pendingToolNames,让恢复的工具名先进入待处理集合:

1
2
3
4
5
6
7
8
private _restoreToolsFromTranscript(): void {
this._pendingToolNames.clear();
const current = getCurrentSystemMessage(this.sessionManager.buildSessionContext().messages);
if (!current) return;
const names = (current.toolsAdded ?? []).map((tool) => tool.name);
this._pendingToolNames = new Set(names.filter((name) => this._isAllowedTool(name)));
this._setActiveTools(names);
}

注册工具、重建运行时时,再把这些名字加入下一组 active tools:

1
2
3
4
// Pending tools that are registered now become active.
nextActiveToolNames.push(...this._pendingToolNames);

this._setActiveTools([...new Set(nextActiveToolNames)]);

这个集合有生命周期:工具注册完成后会从 pending 删除;扩展主动移除 active tool 时,会清掉原恢复集合;下一次 agent run 开始时,仍未注册的名字也会清掉。它修复了及时重连时的工具丢失,极慢连接仍可能需要重新发现。

仓库测试分别覆盖了 resume、/reload、扩展修改 loadout,以及服务器在下一次 prompt 之后才连接的情形。修复 commit、测试断言

当前实现还有哪些边界

工具搜索的分词逻辑值得直接看一眼:

1
2
3
4
5
6
7
8
9
export function tokenize(text: string): string[] {
return text
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter((term) => term.length > 0 && !STOP_WORDS.has(term))
.map(stem);
}

split(/[^a-z0-9]+/) 只保留 ASCII 字母和数字。实际传给 searchTools() 或 tool_search 的纯中文 query 会没有词项,BM25 随即返回空匹配。中文对话仍可以由模型转换成英文关键词、使用工具标识符,或改走 describeNamespace()、describeTool() 和 ALL_TOOLS。当前这两条检索路径内没有自动翻译或 embedding fallback。分词与排序源码

传输层支持 stdio 和 Streamable HTTP。stdio 是启动本地进程,通过标准输入输出交换协议消息;Streamable HTTP 用于远程服务器。1.0.0 不支持旧式 SSE transport,也不渲染 MCP Apps 的 ui:// 资源。MCP 文档、资源边界

QuickJS 隔离的是脚本运行环境。脚本仍能通过注入工具产生真实外部操作,脚本失败也不会撤销已完成的调用。参数校验和权限扩展继续留在正常工具流水线上。脚本执行说明

1.0 加入官方 MCP conformance suite 的 CI,但它按已提交的测试基线检查回归。覆盖的协议版本为 2025-03-26、2025-06-18 和 2025-11-25;1.0.0 仍有 elicitation、CIMD 等已知缺口,也没有实现 2026-07-28 的 stateless 协议。后续版本文档里的新增能力,需要单独核对 tag。测试范围说明

按使用方式选择工具曝光

接入时可以按工具使用方式配置 exposure。下面是一个示例,URL 是占位地址,工具名和通配符需要按实际服务器调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"mcpServers": {
"tracker": {
"url": "https://example.com/mcp",
"description": "Search and read issue tracker data",
"exposure": "codemode",
"toolExposure": {
"search_issues": "direct",
"list_*": "codemode",
"delete_*": "hidden"
}
}
}
}

少量、经常直接调用的工具可以提前声明;适合批量处理和筛选结果的工具可以留给 Codemode;希望发现后再直接调用的工具可以使用 deferred。hidden 控制工具在当前 Agent 中是否可达,服务器端的账号授权仍需独立配置。

阅读这套实现时,可以沿着 mcp/index.ts 看注册与等待,沿着 codemode/tool.ts 看模型能读到的说明,再沿着 codemode/execute.ts 和 mcp/tools.ts 看调用与结果转换。这样能把服务器连接、模型上下文和脚本执行三个过程对应起来。

0%
\ No newline at end of file diff --git a/atom.xml b/atom.xml index 9966f1f118..657db8c039 100644 --- a/atom.xml +++ b/atom.xml @@ -64,10 +64,10 @@ - - + + diff --git a/categories/linked-list/index.html b/categories/linked-list/index.html index 09c1eff4be..b57987c25c 100644 --- a/categories/linked-list/index.html +++ b/categories/linked-list/index.html @@ -1 +1 @@ -分类: linked list | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

linked list 分类

2020
0%
6.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" class=octo-body fill=currentColor> \ No newline at end of file +分类: Linked List | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

0%
\ No newline at end of file diff --git a/css/main.css b/css/main.css index 147ce215f5..9824c19e1e 100644 --- a/css/main.css +++ b/css/main.css @@ -18,4 +18,4 @@ Updated for @stackoverflow/stacks v0.64.0 Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less -*/.hljs{color:#fff;background:#1c1b1b}.hljs-subst{color:#fff}.hljs-comment{color:#999}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag{color:#88aece}.hljs-attribute{color:#c59bc1}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type{color:#f08d49}.hljs-selector-class{color:#88aece}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#b5bd68}.hljs-meta,.hljs-selector-pseudo{color:#88aece}.hljs-built_in,.hljs-literal,.hljs-title{color:#f08d49}.hljs-bullet,.hljs-code{color:#ccc}.hljs-meta .hljs-string{color:#b5bd68}.hljs-deletion{color:#de7176}.hljs-addition{color:#76c490}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;text-shadow:0 -.1em .2em #000;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}:not(pre)>code[class*=language-],pre[class*=language-]{background:#4c3f33}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border:.3em solid #7a6651;border-radius:.5em;box-shadow:1px 1px .5em #000 inset}:not(pre)>code[class*=language-]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid #7a6651;box-shadow:1px 1px .3em -.1em #000 inset;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#997f66}.token.namespace,.token.punctuation{opacity:.7}.token.boolean,.token.constant,.token.number,.token.property,.token.symbol,.token.tag{color:#d1939e}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#bce051}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f4b73d}.token.atrule,.token.attr-value,.token.keyword{color:#d1939e}.token.important,.token.regex{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red}}.code-container:hover .copy-btn,.highlight:hover .copy-btn{opacity:1}.code-container{position:relative}.code-lang{font-size:40px;line-height:1;opacity:.1;pointer-events:none;position:absolute;right:5px}.copy-btn{color:#333;cursor:pointer;line-height:1.6;opacity:0;padding:2px 6px;position:absolute;transition:opacity .2s ease-in-out;background-color:#eee;background-image:linear-gradient(#fcfcfc,#eee);border:1px solid #d5d5d5;border-radius:3px;font-size:.8125em;right:4px;top:8px}code,figure.highlight,kbd,pre{background:var(--highlight-background);color:var(--highlight-foreground)}figure.highlight,pre{line-height:1.6;margin:0 auto 20px}figure.highlight figcaption,pre .caption{background:var(--highlight-gutter-background);color:var(--highlight-foreground);display:flow-root;font-size:.875em;line-height:1.2;padding:.5em}figure.highlight figcaption a,pre .caption a{color:var(--highlight-foreground);float:right}figure.highlight figcaption a:hover,pre .caption a:hover{border-bottom-color:var(--highlight-foreground)}code,pre{font-family:consolas,Menlo,monospace,'PingFang SC','Microsoft YaHei'}code{border-radius:3px;font-size:.875em;padding:2px 4px;overflow-wrap:break-word}kbd{border:2px solid #ccc;border-radius:.2em;box-shadow:.1em .1em .2em rgba(0,0,0,.1);font-family:inherit;padding:.1em .3em;white-space:nowrap}figure.highlight{overflow:auto;position:relative;color-scheme:light}@media (prefers-color-scheme:dark){figure.highlight{color-scheme:dark}}figure.highlight pre{border:0;margin:0;padding:10px 0}figure.highlight table{border:0;margin:0;width:auto}figure.highlight td{border:0;padding:0}figure.highlight .gutter{-webkit-user-select:none;user-select:none}figure.highlight .gutter pre{background:var(--highlight-gutter-background);color:var(--highlight-gutter-foreground);padding-left:10px;padding-right:10px;text-align:right}figure.highlight .code pre{padding-left:10px;width:100%}figure.highlight .marked{background:rgba(0,0,0,.3)}pre .caption{margin-bottom:10px}.gist table{width:auto}.gist table td{border:0}pre code{background:0 0;padding:0;text-shadow:none}.blockquote-center{border-left:0;margin:40px 0;padding:0;position:relative;text-align:center}.blockquote-center::after,.blockquote-center::before{left:0;line-height:1;opacity:.6;position:absolute;width:100%}.blockquote-center::before{border-top:1px solid #ccc;text-align:left;top:-20px;content:'\f10d';font-family:'Font Awesome 7 Free';font-weight:900}.blockquote-center::after{border-bottom:1px solid #ccc;bottom:-20px;text-align:right;content:'\f10e';font-family:'Font Awesome 7 Free';font-weight:900}.blockquote-center div,.blockquote-center p{text-align:center}.group-picture{margin-bottom:20px}.group-picture .group-picture-row{display:flex;gap:3px;margin-bottom:3px}.group-picture .group-picture-column{flex:1}.group-picture .group-picture-column img{height:100%;margin:0;object-fit:cover;width:100%}.post-body .label{color:#555;padding:0 2px}.post-body .label.default{background:#f0f0f0}.post-body .label.primary{background:#efe6f7}.post-body .label.info{background:#e5f2f8}.post-body .label.success{background:#e7f4e9}.post-body .label.warning{background:#fcf6e1}.post-body .label.danger{background:#fae8eb}.post-body .link-grid{display:grid;grid-gap:1.5rem;gap:1.5rem;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));margin-bottom:20px;padding:1rem}.post-body .link-grid .link-grid-container{border:solid #ddd;box-shadow:1rem 1rem .5rem rgba(0,0,0,.5);min-height:5rem;min-width:0;padding:.5rem;position:relative;transition:background .3s}.post-body .link-grid .link-grid-container:hover{animation:.5s next-shake;background:var(--card-bg-color)}.post-body .link-grid .link-grid-container:active{box-shadow:.5rem .5rem .25rem rgba(0,0,0,.5);transform:translate(.2rem,.2rem)}.post-body .link-grid .link-grid-container .link-grid-image{border:1px solid #ddd;border-radius:50%;box-sizing:border-box;height:5rem;padding:3px;position:absolute;width:5rem}.post-body .link-grid .link-grid-container p{margin:0 1rem 0 6rem}.post-body .link-grid .link-grid-container p:first-of-type{font-size:1.2em}.post-body .link-grid .link-grid-container p:last-of-type{font-size:.8em;line-height:1.3rem;opacity:.7}.post-body .link-grid .link-grid-container a{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}.post-body .note{border-radius:3px;margin-bottom:20px;padding:1em;position:relative;border:1px solid #eee;border-left-width:5px}.post-body .note summary{cursor:pointer;outline:0}.post-body .note summary p{display:inline}.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6{border-bottom:initial;margin:0;padding-top:0}.post-body .note :first-child{margin-top:0}.post-body .note :last-child{margin-bottom:0}.post-body .note.default{border-left-color:#777}.post-body .note.default h2,.post-body .note.default h3,.post-body .note.default h4,.post-body .note.default h5,.post-body .note.default h6{color:#777}.post-body .note.primary{border-left-color:#6f42c1}.post-body .note.primary h2,.post-body .note.primary h3,.post-body .note.primary h4,.post-body .note.primary h5,.post-body .note.primary h6{color:#6f42c1}.post-body .note.info{border-left-color:#428bca}.post-body .note.info h2,.post-body .note.info h3,.post-body .note.info h4,.post-body .note.info h5,.post-body .note.info h6{color:#428bca}.post-body .note.success{border-left-color:#5cb85c}.post-body .note.success h2,.post-body .note.success h3,.post-body .note.success h4,.post-body .note.success h5,.post-body .note.success h6{color:#5cb85c}.post-body .note.warning{border-left-color:#f0ad4e}.post-body .note.warning h2,.post-body .note.warning h3,.post-body .note.warning h4,.post-body .note.warning h5,.post-body .note.warning h6{color:#f0ad4e}.post-body .note.danger{border-left-color:#d9534f}.post-body .note.danger h2,.post-body .note.danger h3,.post-body .note.danger h4,.post-body .note.danger h5,.post-body .note.danger h6{color:#d9534f}.post-body .tabs{margin-bottom:20px}.post-body .tabs,.tabs-comment{padding-top:10px}.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{background:var(--content-bg-color);display:flex;display:flex;flex-wrap:wrap;justify-content:center;margin:0;padding:0;position:sticky;top:0;z-index:5}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid #ddd;border-left:1px solid transparent;border-right:1px solid transparent;border-radius:0;border-top:3px solid transparent;flex-grow:1;list-style-type:none}@media (max-width:413px){.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:block;margin-bottom:5px}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid transparent;border-left:3px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-radius:0}}.post-body .tabs ul.nav-tabs li.tab a,.tabs-comment ul.nav-tabs li.tab a{border-bottom:initial;display:block;line-height:1.8;padding:.25em .75em;text-align:center;transition:.2s ease-out}.post-body .tabs ul.nav-tabs li.tab a i[class^=fa],.tabs-comment ul.nav-tabs li.tab a i[class^=fa]{width:1.285714285714286em}.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-color:#fc6423 #ddd transparent}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-color:#ddd #ddd #ddd #fc6423}}.post-body .tabs ul.nav-tabs li.tab.active a,.tabs-comment ul.nav-tabs li.tab.active a{cursor:default}.post-body .tabs .tab-content,.tabs-comment .tab-content{border:1px solid #ddd;border-radius:0;border-top-color:transparent}@media (max-width:413px){.post-body .tabs .tab-content,.tabs-comment .tab-content{border-radius:0;border-top-color:#ddd}}.post-body .tabs .tab-content .tab-pane,.tabs-comment .tab-content .tab-pane{padding:20px 20px 0}.post-body .tabs .tab-content .tab-pane:not(.active),.tabs-comment .tab-content .tab-pane:not(.active){display:none}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{display:inline-block;margin:-1px 10px 0;padding:0 10px}.pagination .page-number.current{background:#ccc;border-color:#ccc;color:var(--content-bg-color)}.pagination{border-top:1px solid #eee;margin:120px 0 0;text-align:center}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:0;border-top:1px solid #eee;transition:border-color .2s ease-in-out}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-top-color:var(--link-hover-color)}@media (max-width:767px){.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{margin:0 5px}.pagination{border-top:0}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:1px solid #eee;border-top:0}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-bottom-color:var(--link-hover-color)}.site-meta{text-align:center}}.pagination .space{margin:0;padding:0}.comments{margin-top:60px;overflow:hidden}.comment-button-group{display:flex;display:flex;flex-wrap:wrap;justify-content:center;justify-content:center;margin:1em 0}.comment-button-group .comment-button{margin:.1em .2em}.comment-button-group .comment-button.active{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.comment-position{display:none}.comment-position.active{display:block}.tabs-comment{margin-top:4em;padding-top:0}.tabs-comment .comments{margin-top:0;padding-top:0}.headband{background:var(--theme-color);height:3px}@media (max-width:991px){.headband{display:none}}.site-brand-container{display:flex;flex-shrink:0;padding:0 10px}.use-motion .column,.use-motion .site-brand-container .toggle{opacity:0}.site-meta{flex-grow:1;text-align:center}.custom-logo-image{margin-top:20px}@media (max-width:991px){.custom-logo-image{display:none}}.brand{border-bottom:0;color:var(--brand-color);display:inline-block;padding:0}.brand:hover{color:var(--brand-hover-color)}.site-title{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.375em;font-weight:400;line-height:1.5;margin:0}.site-subtitle{color:#ddd;font-size:.8125em;margin:10px 10px 0}.use-motion .custom-logo-image,.use-motion .site-subtitle,.use-motion .site-title{opacity:0;position:relative;top:-10px}.site-nav-right,.site-nav-toggle{display:none}.site-nav-right .toggle,.site-nav-toggle .toggle{color:var(--text-color);padding:10px;width:22px}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:var(--text-color);border-radius:1px}@media (max-width:767px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height);visibility:unset}}.menu{margin:0;padding:1em 0;text-align:center}.menu-item{display:inline-block;list-style:none;margin:0 10px}@media (max-width:767px){.menu-item{display:block;margin-top:10px}.menu-item.menu-item-search{display:none}}.menu-item a{border-bottom:0;display:block;font-size:.8125em;transition:border-color .2s ease-in-out}.menu-item a.menu-item-active,.menu-item a:hover{background:var(--menu-item-bg-color)}.menu-item i[class^=fa]{margin-right:8px}.menu-item .badge{background:#ccc;border-radius:10px;color:var(--content-bg-color);font-weight:700;line-height:1;margin-left:.35em;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.use-motion .menu-item{visibility:hidden}.github-corner :hover .octo-arm{animation:560ms ease-in-out octocat-wave}.github-corner svg{color:#fff;fill:var(--theme-color);position:absolute;right:0;top:0;z-index:5}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:991px){.github-corner{display:none}.github-corner svg{color:var(--theme-color);fill:#fff}.github-corner .github-corner:hover .octo-arm{animation:none}.github-corner .github-corner .octo-arm{animation:560ms ease-in-out octocat-wave}.sidebar{left:-320px;background:#222;bottom:0;box-shadow:inset 0 2px 6px #000;max-height:100vh;overflow-y:auto;position:fixed;top:0;transition:left .2s ease-out,right .2s ease-out;width:320px;z-index:20}.sidebar-active .sidebar{left:0}.sidebar a{border-bottom-color:#555;color:#999}.sidebar a:hover{border-bottom-color:#eee;color:#eee}.links-of-author:not(:first-child){margin-top:15px}.links-of-author a{border-bottom-color:#555;display:inline-block;margin-bottom:10px;margin-right:10px;vertical-align:middle}.links-of-author a::before{background:#20d0ff;display:inline-block;margin-right:3px;transform:translateY(-2px);border-radius:50%;content:' ';height:4px;width:4px}.links-of-blogroll-item{padding:0 5px}.popular-posts .popular-posts-item .popular-posts-link:hover{background:0 0}.sidebar-dimmer{background:#000;height:100%;left:0;opacity:0;position:fixed;top:0;transition:visibility .4s,opacity .4s;visibility:hidden;width:100%;z-index:10}.sidebar-active .sidebar-dimmer{opacity:.7;visibility:visible}}.sidebar-inner{color:#999;padding:18px 10px;text-align:center;display:flex;flex-direction:column;justify-content:center}.sidebar-toggle{bottom:61px;height:16px;padding:5px;width:16px;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;left:30px}.sidebar-toggle:hover{opacity:.8}@media (max-width:991px){.sidebar-toggle{left:20px;opacity:.8}}.sidebar-toggle:hover .toggle-line{background:#fc6423}@media (any-hover:hover){body:not(.sidebar-active) .sidebar-toggle:hover :first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}body:not(.sidebar-active) .sidebar-toggle:hover :last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}}.sidebar-active .sidebar-toggle :nth-child(2){opacity:0}.sidebar-active .sidebar-toggle :first-child{top:6px;transform:rotate(45deg)}.sidebar-active .sidebar-toggle :last-child{top:-6px;transform:rotate(-45deg)}.sidebar-nav{font-size:.875em;height:0;margin:0;overflow:hidden;padding-left:0;pointer-events:none;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}.sidebar-nav-active .sidebar-nav{height:calc(2em + 1px);pointer-events:unset;visibility:unset}.sidebar-nav li{border-bottom:1px solid transparent;color:var(--text-color);cursor:pointer;display:inline-block;transition:border-bottom-color .2s ease-in-out,color .2s ease-in-out}.sidebar-nav li.sidebar-nav-overview{margin-left:10px}.sidebar-nav li:hover{color:#fc6423}.sidebar-overview-active .sidebar-nav-overview,.sidebar-toc-active .sidebar-nav-toc{border-bottom-color:#fc6423;color:#fc6423;transition-delay:0.2s}.sidebar-overview-active .sidebar-nav-overview:hover,.sidebar-toc-active .sidebar-nav-toc:hover{color:#fc6423}.sidebar-panel-container{align-items:start;display:grid;flex:1;overflow-x:hidden;overflow-y:auto;padding-top:0;transition:padding-top .2s ease-in-out}.sidebar-nav-active .sidebar-panel-container{padding-top:20px}.sidebar-panel{animation:.2s ease-in-out deactivate-sidebar-panel;grid-area:1/1;height:0;opacity:0;overflow:hidden;pointer-events:none;transform:translateY(0);transition:.2s ease-in-out;transition-property:opacity,transform,visibility;visibility:hidden}.sidebar-nav-active .sidebar-panel,.sidebar-overview-active .sidebar-panel.post-toc-wrap{transform:translateY(-20px)}.sidebar-overview-active:not(.sidebar-nav-active) .sidebar-panel.post-toc-wrap{transition-delay:0s,0.2s,0s}.sidebar-overview-active .sidebar-panel.site-overview-wrap,.sidebar-toc-active .sidebar-panel.post-toc-wrap{animation-name:activate-sidebar-panel;height:auto;opacity:1;pointer-events:unset;transform:translateY(0);transition-delay:0.2s,0.2s,0s;visibility:unset}.sidebar-panel.site-overview-wrap{display:flex;flex-direction:column;justify-content:center;gap:10px;justify-content:flex-start}@keyframes deactivate-sidebar-panel{from{height:var(--inactive-panel-height,0)}to{height:var(--active-panel-height,0)}}@keyframes activate-sidebar-panel{from{height:var(--inactive-panel-height,auto)}to{height:var(--active-panel-height,auto)}}.post-toc{font-size:.875em}.post-toc ol{list-style:none;margin:0;padding:0 2px 0 10px;text-align:left}.post-toc ol>:last-child{margin-bottom:5px}.post-toc ol>ol{padding-left:0}.post-toc ol a{transition:.2s ease-in-out}.post-toc .nav-item{line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.post-toc .nav .nav-child{--height:0;height:0;opacity:0;overflow:hidden;transition:.2s ease-in-out;visibility:hidden}.post-toc .nav .active>.nav-child{height:var(--height,auto);opacity:1;visibility:unset}.post-toc .nav .active>a{border-bottom-color:#fc6423;color:#fc6423}.post-toc .nav .active-current>a,.post-toc .nav .active-current>a:hover{color:#fc6423}.site-author-image{border:1px solid #eee;max-width:120px;padding:2px}.site-author-name{color:var(--text-color);font-weight:600;margin:0}.site-description{color:#999;font-size:.8125em;margin-top:0}.site-state{display:flex;flex-wrap:wrap;justify-content:center;line-height:1.4}.site-state-item{padding:0 15px}.site-state-item a{border-bottom:0;display:block}.site-state-item-count{display:block;font-size:1em;font-weight:600}.site-state-item-name{color:#999;font-size:.8125em}.sidebar .sidebar-button:not(:first-child){margin-top:15px}.sidebar .sidebar-button button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:1px solid #fc6423;border-radius:4px}.sidebar .sidebar-button button:hover{background:#fc6423;color:#fff}.sidebar .sidebar-button button i[class^=fa]{margin-right:5px}.links-of-author a{font-size:.8125em}.links-of-author i[class^=fa]{margin-right:2px}.cc-license .cc-opacity{border-bottom:0;opacity:.7}.cc-license .cc-opacity:hover{opacity:.9}.cc-license img{display:inline-block}.links-of-blogroll{font-size:.8125em}.links-of-blogroll-title{font-size:.875em;font-weight:600}.links-of-blogroll-list{list-style:none;gap:5px;margin:5px 0 0;padding:0;display:flex;flex-wrap:wrap;justify-content:center;flex-direction:column}.links-of-blogroll-item{max-width:100%}.links-of-blogroll-item a{box-sizing:border-box;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-post-related{font-size:.8125em;padding:18px 0 0}.popular-posts{margin:0;padding:1em 0;text-align:left}.popular-posts .popular-posts-item{display:block}.popular-posts .popular-posts-item .popular-posts-link{border-bottom:0;display:block;padding:5px 20px;transition:background .2s ease-in-out}.popular-posts .popular-posts-item .popular-posts-link:hover{background:var(--menu-item-bg-color)}.popular-posts .popular-posts-item .popular-posts-time{color:#999}.footer{color:#999;font-size:.875em;padding:20px 0;transition:left .2s ease-in-out,right .2s ease-in-out}.footer.footer-fixed{bottom:0;left:0;position:absolute;right:0}.footer-inner{box-sizing:border-box;text-align:center;display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.footer-inner{width:auto}}@media (min-width:1200px){.footer-inner{width:1160px}}@media (min-width:1600px){.footer-inner{width:73%}}.use-motion .footer{opacity:0}.languages{display:inline-block;font-size:1.125em;position:relative}.languages .lang-select-label span{margin:0 .5em}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:red;display:inline-block;margin:0 5px}.beian img{display:inline-block;margin:0 3px;vertical-align:middle}@keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,50%,60%,70%,80%{transform:scale(1.1)}}@media (max-width:567px){.main-inner{padding:initial!important}.posts-expand .post-header{margin-bottom:10px!important}.post-block{margin-top:initial!important;padding:8px 18px!important}.post-body h1,.post-body h2,.post-body h3,.post-body h4,.post-body h5,.post-body h6{margin:20px 0 8px}.post-body .note h1,.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6,.post-body .tabs .tab-content .tab-pane h1,.post-body .tabs .tab-content .tab-pane h2,.post-body .tabs .tab-content .tab-pane h3,.post-body .tabs .tab-content .tab-pane h4,.post-body .tabs .tab-content .tab-pane h5,.post-body .tabs .tab-content .tab-pane h6{margin:0 5px}.post-body>p{margin:0 0 10px}.post-body .note>p,.post-body .tabs .tab-content .tab-pane>p{padding:0 5px}.post-body img,.post-body video{margin-bottom:10px!important}.post-body figure:not(.highlight) figcaption{margin:-5px auto 15px!important}.post-body .note{margin-bottom:10px!important;padding:10px!important}.post-body .tabs .tab-content .tab-pane{padding:10px 10px 0!important}.post-eof{margin:40px auto 20px!important}.pagination{margin-top:40px}}.back-to-top{font-size:12px;align-items:center;bottom:-100px;color:#fff;display:flex;height:26px;transition:bottom .2s ease-in-out;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;left:30px}.back-to-top span{margin-right:8px;display:none}.back-to-top .fa{text-align:center;width:26px}.back-to-top:hover{opacity:.8;color:#fc6423}.back-to-top.back-to-top-on{bottom:30px}.rtl.post-body a,.rtl.post-body h1,.rtl.post-body h2,.rtl.post-body h3,.rtl.post-body h4,.rtl.post-body h5,.rtl.post-body h6,.rtl.post-body li,.rtl.post-body ol,.rtl.post-body p,.rtl.post-body ul{direction:rtl;font-family:UKIJ Ekran}.rtl.post-title{font-family:UKIJ Ekran}.post-button{margin-top:40px;text-align:center}.use-motion .collection-header,.use-motion .comments,.use-motion .pagination,.use-motion .post-block,.use-motion .post-body,.use-motion .post-header{visibility:hidden}.posts-collapse .post-content{margin-bottom:35px;margin-left:35px;position:relative}@media (max-width:767px){.posts-collapse .post-content{margin-left:0;margin-right:0}}.posts-collapse .post-content .collection-title{font-size:1.125em;position:relative}.posts-collapse .post-content .collection-title::before{background:#999;border:1px solid #fff;margin-left:-6px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:10px;width:10px}.posts-collapse .post-content .collection-year{font-size:1.5em;font-weight:700;margin:60px 0;position:relative}.posts-collapse .post-content .collection-year .collection-year-count{font-size:.75em;background:#ccc;border-radius:10px;color:var(--content-bg-color);font-weight:700;line-height:1;margin-left:.35em;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.posts-collapse .post-content .collection-year::before{background:#bbb;margin-left:-4px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:8px;width:8px}.posts-collapse .post-content .collection-header{display:block;margin-left:20px}.posts-collapse .post-content .collection-header small{color:#bbb;margin-left:5px}.posts-collapse .post-content .post-header{border-bottom:1px dashed #ccc;margin:30px 2px 0;padding-left:15px;position:relative;transition:border .2s ease-in-out}.posts-collapse .post-content .post-header::before{background:#bbb;border:1px solid #fff;left:-6px;position:absolute;top:.75em;transition:background .2s ease-in-out;border-radius:50%;content:' ';height:6px;width:6px}.posts-collapse .post-content .post-header:hover{border-bottom-color:#666}.posts-collapse .post-content .post-header:hover::before{background:#222}.posts-collapse .post-content .post-meta-container{display:inline;font-size:.75em;margin-right:10px}.posts-collapse .post-content .post-title{display:inline}.posts-collapse .post-content .post-title a{border-bottom:0;color:var(--link-color)}.posts-collapse .post-content .post-title .fa{font-size:.875em;margin-left:5px}.posts-collapse .post-content::before{background:#f5f5f5;content:' ';height:100%;margin-left:-2px;position:absolute;top:1.25em;width:4px}.post-body{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;overflow-wrap:break-word}@media (min-width:1200px){.post-body{font-size:1.125em}}@media (min-width:992px){.post-body{text-align:justify}}.post-body h1 .header-anchor,.post-body h1 .headerlink,.post-body h2 .header-anchor,.post-body h2 .headerlink,.post-body h3 .header-anchor,.post-body h3 .headerlink,.post-body h4 .header-anchor,.post-body h4 .headerlink,.post-body h5 .header-anchor,.post-body h5 .headerlink,.post-body h6 .header-anchor,.post-body h6 .headerlink{border-bottom-style:none;color:inherit;float:right;font-size:.875em;margin-left:10px;opacity:0}.post-body h1 .header-anchor::before,.post-body h1 .headerlink::before,.post-body h2 .header-anchor::before,.post-body h2 .headerlink::before,.post-body h3 .header-anchor::before,.post-body h3 .headerlink::before,.post-body h4 .header-anchor::before,.post-body h4 .headerlink::before,.post-body h5 .header-anchor::before,.post-body h5 .headerlink::before,.post-body h6 .header-anchor::before,.post-body h6 .headerlink::before{content:'\f0c1';font-family:'Font Awesome 7 Free';font-weight:900}.post-body h1:hover .header-anchor,.post-body h1:hover .headerlink,.post-body h2:hover .header-anchor,.post-body h2:hover .headerlink,.post-body h3:hover .header-anchor,.post-body h3:hover .headerlink,.post-body h4:hover .header-anchor,.post-body h4:hover .headerlink,.post-body h5:hover .header-anchor,.post-body h5:hover .headerlink,.post-body h6:hover .header-anchor,.post-body h6:hover .headerlink{opacity:.5}.post-body h1:hover .header-anchor:hover,.post-body h1:hover .headerlink:hover,.post-body h2:hover .header-anchor:hover,.post-body h2:hover .headerlink:hover,.post-body h3:hover .header-anchor:hover,.post-body h3:hover .headerlink:hover,.post-body h4:hover .header-anchor:hover,.post-body h4:hover .headerlink:hover,.post-body h5:hover .header-anchor:hover,.post-body h5:hover .headerlink:hover,.post-body h6:hover .header-anchor:hover,.post-body h6:hover .headerlink:hover{opacity:1}.post-body .exturl .fa{font-size:.875em;margin-left:4px}.post-body figure:not(.highlight){margin:0 auto 20px}.post-body figure:not(.highlight) figcaption{color:#999;font-size:.875em;font-weight:700;line-height:1;margin:-15px auto 15px;text-align:center}.post-body embed,.post-body iframe,.post-body img,.post-body video{margin-bottom:20px}.post-body .video-container{height:0;margin-bottom:20px;overflow:hidden;padding-top:75%;position:relative;width:100%}.post-body .video-container embed,.post-body .video-container iframe,.post-body .video-container object{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.post-gallery{display:flex;min-height:200px}.post-gallery .post-gallery-image{flex:1}.post-gallery .post-gallery-image:not(:first-child){clip-path:polygon(40px 0,100% 0,100% 100%,0 100%);margin-left:-20px}.post-gallery .post-gallery-image:not(:last-child){margin-right:-20px}.post-gallery .post-gallery-image img{height:100%;object-fit:cover;opacity:1;width:100%}.posts-expand .post-gallery{margin-bottom:60px}.posts-collapse .post-gallery{margin:15px 0}.posts-expand .post-header{font-size:1.125em;margin-bottom:60px;text-align:center}.posts-expand .post-title{font-size:1.5em;font-weight:400;margin:initial;overflow-wrap:break-word}.posts-expand .post-title-link{border-bottom:0;color:var(--link-color);display:inline-block;max-width:100%;position:relative}.posts-expand .post-title-link::before{background:var(--link-color);bottom:0;content:'';height:2px;left:0;position:absolute;transform:scaleX(0);transition:transform .2s ease-in-out;width:100%}.posts-expand .post-title-link:hover::before{transform:scaleX(1)}.posts-expand .post-title-link .fa{font-size:.875em;margin-left:5px}.post-sticky-flag{display:inline-block;margin-right:8px;transform:rotate(30deg)}.posts-expand .post-meta-container{color:#999;font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:.75em;margin-top:3px}.posts-expand .post-meta-container .post-description{font-size:.875em;margin-top:2px}.posts-expand .post-meta-container time{border-bottom:1px dashed #999}.post-meta{display:flex;flex-wrap:wrap;justify-content:center}:not(.post-meta-break)+.post-meta-item::before{content:'|';margin:0 .5em}.post-meta-item-icon{margin-right:3px}@media (max-width:991px){.back-to-top{left:20px;opacity:.8}.post-body{text-align:justify}.post-meta-item-text{display:none}}.post-meta-break{flex-basis:100%;height:0}.post-nav{border-top:1px solid #eee;display:flex;gap:30px;justify-content:space-between;margin-top:1em;padding:10px 5px 0}.post-nav-item{flex:1}.post-nav-item a{border-bottom:0;display:block;font-size:.875em;line-height:1.6}.post-nav-item a:active{top:2px}.post-nav-item .fa{font-size:.75em}.post-nav-item:first-child .fa{margin-right:5px}.post-nav-item:last-child{text-align:right}.post-nav-item:last-child .fa{margin-left:5px}.post-footer{display:flex;flex-direction:column;justify-content:center}.post-eof{background:#ccc;height:1px;margin:80px auto 60px;width:8%}.post-block:last-of-type .post-eof{display:none}.post-copyright ul{list-style:none;overflow:hidden;padding:.5em 1em;position:relative;background:var(--card-bg-color);border-left:3px solid #ff2a2a;margin:1em 0 0}.post-copyright ul::after{content:'\f25e';font-family:'Font Awesome 7 Brands';font-size:200px;opacity:.1;position:absolute;right:-50px;top:-150px}.post-tags{margin-top:40px;text-align:center}.post-tags a{display:inline-block;font-size:.8125em}.post-tags a:not(:last-child){margin-right:10px}.social-like{border-top:1px solid #eee;font-size:.875em;margin-top:1em;padding-top:1em;display:flex;flex-wrap:wrap;justify-content:center}.social-like a{border-bottom:none}.reward-container{margin:1em 0 0;padding:1em 0;text-align:center}.reward-container button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:2px solid #fc6423;border-radius:2px;outline:0;vertical-align:text-top}.reward-container button:hover{background:#fc6423;color:#fff}.post-reward{display:none;padding-top:20px}.post-reward.active{display:block}.post-reward div{display:inline-block}.post-reward div span{display:block}.post-reward img{display:inline-block;margin:.8em 2em 0;max-width:100%;width:180px}@keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}.category-all-page .category-all-title{text-align:center}.category-all-page .category-all{margin-top:20px}.category-all-page .category-list{list-style:none;margin:0;padding:0}.category-all-page .category-list-item{margin:5px 10px}.category-all-page .category-list-count{font-size:.75em;background:#ccc;border-radius:10px;color:var(--content-bg-color);font-weight:700;line-height:1;margin-left:.35em;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.category-all-page .category-list-child{padding-left:10px}.event-list hr{background:#222;margin:20px 0 45px}.event-list hr::after{background:#222;color:#fff;content:'NOW';display:inline-block;font-weight:700;padding:0 5px}.event-list .event{--event-background:#222;--event-foreground:#bbb;--event-title:#fff;background:var(--event-background);padding:15px}.event-list .event .event-summary{border-bottom:0;color:var(--event-title);margin:0;padding:0 0 0 35px;position:relative}.event-list .event .event-summary::before{animation:1s ease-in-out infinite alternate dot-flash;background:var(--event-title);left:0;margin-top:-6px;position:absolute;top:50%;border-radius:50%;content:' ';height:12px;width:12px}.event-list .event:nth-of-type(odd) .event-summary::before{animation-delay:.5s}.event-list .event:not(:last-child){margin-bottom:20px}.event-list .event .event-relative-time{color:var(--event-foreground);display:inline-block;font-size:12px;font-weight:400;padding-left:12px}.event-list .event .event-details{color:var(--event-foreground);display:block;line-height:18px;padding:6px 0 6px 35px}.event-list .event .event-details::before{color:var(--event-foreground);display:inline-block;margin-right:9px;width:14px;font-family:'Font Awesome 7 Free';font-weight:900}.event-list .event .event-details.event-location::before{content:'\f041'}.event-list .event .event-details.event-duration::before{content:'\f017'}.event-list .event .event-details.event-description::before{content:'\f024'}.event-list .event-past{--event-background:#f5f5f5;--event-foreground:#999;--event-title:#222}@keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}ul.breadcrumb{font-size:.75em;list-style:none;margin:1em 0;padding:0 2em;text-align:center}ul.breadcrumb li{display:inline}ul.breadcrumb li:not(:first-child)::before{content:'/\00a0';font-weight:400;padding:.5em}ul.breadcrumb li:last-child{font-weight:700}.tag-cloud{text-align:center}.tag-cloud a{display:inline-block;margin:10px}.tag-cloud-0{border-bottom-color:#aaa;color:#aaa}.tag-cloud-1{border-bottom-color:#9a9a9a;color:#9a9a9a}.tag-cloud-2{border-bottom-color:#8b8b8b;color:#8b8b8b}.tag-cloud-3{border-bottom-color:#7c7c7c;color:#7c7c7c}.tag-cloud-4{border-bottom-color:#6c6c6c;color:#6c6c6c}.tag-cloud-5{border-bottom-color:#5d5d5d;color:#5d5d5d}.tag-cloud-6{border-bottom-color:#4e4e4e;color:#4e4e4e}.tag-cloud-7{border-bottom-color:#3e3e3e;color:#3e3e3e}.tag-cloud-8{border-bottom-color:#2f2f2f;color:#2f2f2f}.tag-cloud-9{border-bottom-color:#202020;color:#202020}.tag-cloud-10{border-bottom-color:#111;color:#111}.search-active{overflow:hidden;margin-right:var(--dialog-scrollgutter,0)}.search-pop-overlay{background:rgba(0,0,0,0);display:flex;height:100%;left:0;position:fixed;top:0;transition:visibility .4s,background .4s;visibility:hidden;width:100%;z-index:40}.search-active .search-pop-overlay{background:rgba(0,0,0,.3);visibility:visible}.search-popup{background:var(--card-bg-color);border-radius:5px;height:80%;margin:auto;transform:scale(0);transition:transform .4s;width:700px}.search-active .search-popup{transform:scale(1)}@media (max-width:767px){.search-popup{border-radius:0;height:100%;width:100%}}.search-popup .popup-btn-close,.search-popup .search-icon{color:#999;font-size:18px;padding:0 10px}.search-popup .popup-btn-close{cursor:pointer}.search-popup .popup-btn-close:hover .fa{color:#222}.search-popup .search-header{background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;padding:5px}@media (prefers-color-scheme:dark){.tag-cloud-0{border-bottom-color:#555;color:#555}.tag-cloud-1{border-bottom-color:#646464;color:#646464}.tag-cloud-2{border-bottom-color:#737373;color:#737373}.tag-cloud-3{border-bottom-color:#828282;color:#828282}.tag-cloud-4{border-bottom-color:#929292;color:#929292}.tag-cloud-5{border-bottom-color:#a1a1a1;color:#a1a1a1}.tag-cloud-6{border-bottom-color:#b0b0b0;color:#b0b0b0}.tag-cloud-7{border-bottom-color:silver;color:silver}.tag-cloud-8{border-bottom-color:#cfcfcf;color:#cfcfcf}.tag-cloud-9{border-bottom-color:#dedede;color:#dedede}.tag-cloud-10{border-bottom-color:#eee;color:#eee}.search-popup .search-header{background:#666}}.search-popup input.search-input{background:0 0;border:0;outline:0;width:100%}.search-popup input.search-input::-webkit-search-cancel-button{display:none}.search-popup .search-result-container{display:flex;flex-direction:column;height:calc(100% - 55px);overflow:auto;padding:5px 25px}.search-popup .search-result-container hr{flex-shrink:0;margin:5px 0 10px}.search-popup .search-result-container hr:first-child{display:none}.search-popup .search-result-list{margin:0 5px;padding:0}.search-popup a.search-result-title{font-weight:700}.search-popup p.search-result{border-bottom:1px dashed #ccc;margin:0 0 10px;padding:5px 0}.search-popup .search-input-container{flex-grow:1;padding:2px}.search-popup .search-result-icon{color:#ccc;margin:auto}mark.search-keyword{background:0 0;border-bottom:1px dashed #ff2a2a;color:#ff2a2a;font-weight:700}.search-stats{align-items:center;display:flex;justify-content:space-between}.search-stats img{height:1em;margin:0}.pagination.algolia-pagination{margin-top:auto;padding:40px 0;visibility:visible}.use-motion .animated{animation-fill-mode:none;visibility:inherit}.use-motion .sidebar .animated{animation-fill-mode:both}header.header{background:var(--content-bg-color);border-radius:initial;box-shadow:initial}.main{align-items:stretch;display:flex;justify-content:space-between;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.main{width:auto}}@media (min-width:1200px){.main{width:1160px}}@media (min-width:1600px){.main{width:73%}}@media (max-width:991px){header.header{border-radius:initial}.main{display:block;width:auto}}.main-inner{border-radius:initial;box-sizing:border-box;width:calc(100% - 252px)}.footer-inner{padding-left:252px}@media (max-width:991px){.main-inner{border-radius:initial;width:100%}.footer-inner{padding-left:0;padding-right:0;width:auto}}.column{width:240px}.site-brand-container{background:var(--theme-color)}.site-meta{padding:20px 0}.site-nav-right .toggle,.site-nav-toggle .toggle{color:#fff}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:#fff}@media (min-width:768px) and (max-width:991px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height);visibility:unset}}.menu .menu-item{display:block;margin:0}.menu .menu-item a{padding:5px 20px;position:relative;transition-property:background-color;display:flex;align-items:center}.menu .menu-item a .badge{margin-left:auto}@media (max-width:991px){.column{width:auto}.site-nav-on .site-brand-container{box-shadow:0 0 16px rgba(0,0,0,.5)}.menu .menu-item.menu-item-search{display:none}}.main-menu .menu-item-active::after{background:#bbb;border-radius:50%;content:' ';height:6px;margin-top:-3px;position:absolute;right:15px;top:50%;width:6px}.sub-menu{margin:0;padding:6px 0}.sub-menu .menu-item{display:inline-block}.sub-menu .menu-item a{background:0 0;margin:5px 10px;padding:initial}.sub-menu .menu-item a:hover{background:0 0;color:#fc6423}.sub-menu .menu-item-active{border-bottom-color:#fc6423;color:#fc6423}.sub-menu .menu-item-active:hover{border-bottom-color:#fc6423}@media (min-width:992px){.sidebar{position:sticky;top:12px}.sidebar-toggle{display:none}.sidebar-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;box-sizing:border-box;color:var(--text-color);margin-top:12px;max-height:calc(100vh - 24px)}.site-state-item{padding:0 10px}.sidebar .sidebar-button{border-bottom:1px dotted #ccc;border-top:1px dotted #ccc}.sidebar .sidebar-button button{border:0;color:#fc6423;display:block;width:100%}.sidebar .sidebar-button button:hover{background:0 0;border:0;color:#e34603}.links-of-author{display:flex;flex-wrap:wrap;justify-content:center}.links-of-author-item{margin:5px 0 0;width:50%}.links-of-author-item a{border-bottom:0;border-radius:4px;padding:0 5px;box-sizing:border-box;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.links-of-author-item a:hover{background:var(--body-bg-color)}.links-of-blogroll-item a{padding:0 5px}}.main-inner{background:var(--content-bg-color);box-shadow:initial;padding:40px}@media (max-width:991px){.main-inner{padding:20px}}.sub-menu{border-bottom:1px solid #ddd}.post-block:first-of-type{padding-top:40px}@media (max-width:767px){.pagination{margin-bottom:10px}}.sidebar-tag-cloud{line-height:1.8;margin-top:12px;text-align:center}.sidebar-tag-cloud .tag-cloud-tags a{display:inline-block;margin:0 5px;white-space:nowrap}.tool-directory,.tool-workspace{--tool-ink:#18212b;--tool-muted:#66717e;--tool-line:#dce2e7;--tool-soft:#f4f6f7;--tool-primary:#166a5b;color:var(--tool-ink);margin:0 auto;max-width:1120px}.tool-workspace [hidden]{display:none!important}.tool-directory-header{border-bottom:1px solid var(--tool-line);padding:18px 0 34px}.tool-directory-header h1{font-size:34px;line-height:1.25;margin:8px 0 12px}.tool-directory-header p{color:var(--tool-muted);font-size:15px;line-height:1.75;margin:0;max-width:720px}.tool-eyebrow{color:#16705f;display:block;font-size:11px;font-weight:700;letter-spacing:0}.tool-privacy{align-items:center;color:#3e6159;display:flex;font-size:12px;gap:7px;margin-top:16px}.tool-grid{display:grid;gap:14px;grid-template-columns:repeat(2,minmax(0,1fr));padding:28px 0}.tool-group{border-bottom:1px solid var(--tool-line);padding-top:28px}.tool-group .tool-grid{padding:16px 0 28px}.tool-group-heading{align-items:baseline;display:flex;gap:12px}.tool-group-heading h2{font-size:18px;margin:0}.tool-group-heading span{color:var(--tool-muted);font-size:11px}.tool-card{align-items:center;background:#fff;border:1px solid var(--tool-line);border-left:4px solid #55616d;border-radius:6px;color:var(--tool-ink);display:flex;gap:15px;min-height:94px;padding:16px 18px;text-decoration:none;transition:border-color .16s,box-shadow .16s,transform .16s}.tool-card:hover{border-color:#aeb8c0;box-shadow:0 7px 20px rgba(24,33,43,.08);color:var(--tool-ink);transform:translateY(-2px)}.tool-card>i{color:#87919a;margin-left:auto}.tool-accent-red{border-left-color:#bc4c3c}.tool-accent-blue{border-left-color:#326fa8}.tool-accent-green{border-left-color:#27805f}.tool-accent-amber{border-left-color:#b7791f}.tool-accent-violet{border-left-color:#71569b}.tool-card-mark{align-items:center;background:#eef1f3;border-radius:4px;color:#33404b;display:flex;flex:0 0 44px;font-family:monospace;font-size:14px;font-weight:700;height:44px;justify-content:center}.tool-card-copy{min-width:0}.tool-card-copy small,.tool-card-copy strong{display:block}.tool-card-copy strong{font-size:16px;line-height:1.45}.tool-card-copy small{color:var(--tool-muted);font-size:12px;line-height:1.55;margin-top:4px}.tool-directory-note{border-top:1px solid var(--tool-line);padding:24px 0 12px}.tool-directory-note h2{font-size:18px;margin:0 0 8px}.tool-directory-note p{color:var(--tool-muted);font-size:13px;line-height:1.75;margin:0}.tool-breadcrumb{font-size:12px;margin:4px 0 22px}.tool-breadcrumb a{color:#596572;text-decoration:none}.tool-breadcrumb i{margin-right:6px}.tool-header{align-items:flex-start;border-bottom:1px solid var(--tool-line);display:flex;gap:28px;justify-content:space-between;margin-bottom:24px;padding-bottom:24px}.tool-header h1{font-size:30px;line-height:1.3;margin:7px 0 10px}.tool-header p{color:var(--tool-muted);font-size:14px;line-height:1.7;margin:0;max-width:740px}.tool-local-badge{align-items:center;background:#edf6f2;border:1px solid #cce4da;border-radius:5px;color:#1d6353;display:flex;flex:0 0 auto;gap:10px;padding:9px 12px}.tool-local-badge>i{font-size:16px}.tool-local-badge span{font-size:12px;font-weight:700;line-height:1.25}.tool-local-badge small{display:block;font-size:10px;font-weight:400;margin-top:2px}.tool-panel{border:1px solid var(--tool-line);border-radius:6px;padding:22px}.tool-input-panel{border-top:3px solid #1f6e60}.tool-panel-heading,.tool-section-heading{align-items:flex-start;display:flex;gap:20px;justify-content:space-between}.tool-panel-heading h2,.tool-section-heading h2{font-size:18px;line-height:1.4;margin:0}.tool-panel-heading p,.tool-section-heading p{color:var(--tool-muted);font-size:12px;line-height:1.6;margin:4px 0 0}.tool-section-heading{border-bottom:1px solid var(--tool-line);margin-bottom:20px;padding-bottom:16px}.tool-section-heading span{color:#16705f;font-size:10px;font-weight:700}.tool-section-heading h2{margin-top:3px}.tool-dropzone{align-items:center;background:#f7f9fa;border:1px dashed #aeb9c2;border-radius:5px;cursor:pointer;display:flex;gap:12px;margin:18px 0 14px;padding:14px 16px;transition:background .15s,border-color .15s}.tool-dropzone.is-dragging,.tool-dropzone:hover{background:#edf6f2;border-color:#2c7b69}.tool-dropzone>i{color:#2c7162;font-size:20px}.tool-dropzone small,.tool-dropzone span,.tool-dropzone strong{display:block}.tool-dropzone strong{font-size:13px}.tool-dropzone small{color:var(--tool-muted);font-size:11px;margin-top:2px}.tool-dropzone input{height:1px;opacity:0;position:absolute;width:1px}.tool-field{display:block;min-width:0}.tool-field>span{color:#48535f;display:block;font-size:11px;font-weight:700;margin-bottom:6px}.tool-field input,.tool-field select,.tool-field textarea{background:#fff;border:1px solid #cfd6dc;border-radius:4px;box-sizing:border-box;color:var(--tool-ink);font-family:inherit;font-size:13px;outline:0;width:100%}.tool-field input:focus,.tool-field select:focus,.tool-field textarea:focus{border-color:#247766;box-shadow:0 0 0 2px rgba(36,119,102,.12)}.tool-field textarea{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.55;min-height:180px;padding:12px;resize:vertical}.tool-field input,.tool-field select{height:38px;padding:0 10px}.tool-field-wide{margin-top:12px}.tool-form-row{display:grid;gap:14px;grid-template-columns:repeat(2,minmax(0,1fr));margin:18px 0 10px}.tool-actions{align-items:center;display:flex;gap:9px;margin-top:16px;min-height:36px}.tool-actions-between{justify-content:space-between}.tool-button{align-items:center;border:1px solid transparent;border-radius:4px;cursor:pointer;display:inline-flex;font-size:12px;font-weight:600;gap:7px;height:36px;justify-content:center;padding:0 14px}.tool-button:disabled{cursor:wait;opacity:.55}.tool-button-primary{background:#176b5c;border-color:#176b5c;color:#fff}.tool-button-primary:hover{background:#11594c}.tool-button-secondary{background:#edf4f8;border-color:#c6d8e3;color:#28566f}.tool-button-secondary:hover{background:#e1edf3}.tool-button-quiet{background:#fff;border-color:#cfd6dc;color:#3c4854}.tool-button-quiet:hover{background:#f4f6f7}.tool-hint,.tool-progress{color:var(--tool-muted);font-size:11px;margin-left:4px}.tool-results{border-top:3px solid #2e6fa3;margin-top:28px;padding-top:22px}.tool-empty{align-items:center;color:#7b8690;display:flex;flex-direction:column;min-height:210px;justify-content:center;text-align:center}.tool-empty i{color:#a4adb5;font-size:29px;margin-bottom:13px}.tool-empty strong{color:#5a6570;font-size:14px}.tool-empty span{font-size:11px;margin-top:5px}.tool-metrics{display:grid;gap:1px;grid-template-columns:repeat(5,minmax(0,1fr));margin-bottom:22px;overflow:hidden}.tool-metric{background:#f1f4f5;min-width:0;padding:14px}.tool-metric span{color:#6b7680;display:block;font-size:10px}.tool-metric strong{display:block;font-size:20px;line-height:1.3;margin-top:4px;overflow-wrap:anywhere}.tool-chart-panel{border-bottom:1px solid var(--tool-line);border-top:1px solid var(--tool-line);margin:0 0 22px;padding:18px 0}.tool-chart-panel canvas{display:block;height:220px;max-width:100%;width:100%}.tool-chart-title{display:flex;justify-content:space-between;margin-bottom:14px}.tool-chart-title strong{font-size:13px}.tool-chart-title span{color:var(--tool-muted);font-size:10px}.tool-split{display:grid;gap:30px;grid-template-columns:minmax(250px,0.8fr) minmax(0,1.4fr)}.tool-split h3{font-size:14px;margin:0 0 12px}.tool-findings{display:grid;gap:8px}.tool-finding{border-left:3px solid #6d7a85;color:#4c5762;font-size:12px;line-height:1.6;padding:7px 10px}.tool-finding strong{color:#27323c;display:block;font-size:12px}.tool-finding--danger{background:#fff4f2;border-left-color:#bf4d3e}.tool-finding--warn{background:#fff8e9;border-left-color:#bd7a1d}.tool-finding--ok{background:#eff8f4;border-left-color:#26785e}.tool-finding--info{background:#f0f6fa;border-left-color:#3777a7}.tool-table-wrap{overflow-x:auto}.tool-table{border-collapse:collapse;font-size:11px;margin:0;table-layout:auto;width:100%}.tool-table td,.tool-table th{border-bottom:1px solid #e4e8eb;padding:9px 8px;text-align:left;vertical-align:top}.tool-table th{color:#66727d;font-size:10px;white-space:nowrap}.tool-table td code{font-size:10px;overflow-wrap:anywhere}.tool-state-bars{display:grid;gap:9px;margin-bottom:24px}.tool-state-row{display:grid;font-size:10px;gap:8px;grid-template-columns:86px 1fr 34px}.tool-state-track{background:#edf0f2;height:8px;margin-top:3px;overflow:hidden}.tool-state-fill{background:#3978a8;height:100%;min-width:2px}.tool-state-row b{text-align:right}.tool-stack-groups{display:grid;gap:10px}.tool-stack-group{border-bottom:1px solid var(--tool-line);padding-bottom:10px}.tool-stack-group strong{font-size:12px}.tool-stack-group span{color:var(--tool-muted);float:right;font-size:10px}.tool-stack-group code{color:#4d5964;display:block;font-size:10px;line-height:1.5;margin-top:5px;overflow-wrap:anywhere}.tool-segments{background:#eef1f3;border-radius:4px;display:flex;padding:2px}.tool-segments button{background:0 0;border:0;border-radius:3px;color:#63707b;cursor:pointer;font-size:11px;height:30px;padding:0 12px}.tool-segments button.is-active{background:#fff;box-shadow:0 1px 3px rgba(20,30,40,.12);color:#23303b}.tool-cause-chain{display:flex;display:flex;flex-wrap:wrap;justify-content:center;gap:6px;margin-bottom:14px}.tool-cause-chain span{background:#f3f5f6;border:1px solid #dce2e7;border-radius:3px;font-family:monospace;font-size:10px;padding:5px 7px}.tool-output{background:#20272e;border-radius:5px;color:#e7ecef;font-size:11px;line-height:1.65;margin:0;max-height:620px;overflow:auto;padding:18px;white-space:pre-wrap;word-break:break-word}.tool-workspace[data-tool=json] .tool-output{background:#182027;border:1px solid #303c46;color:#f4f7f9;font-size:12px;tab-size:2}.tool-workspace[data-tool=json] .tool-output code{background:0 0;border:0;color:inherit;font-size:inherit;line-height:inherit;padding:0;text-shadow:none}.tool-inline-field{align-items:center;color:var(--tool-muted);display:inline-flex;font-size:11px;gap:7px}.tool-inline-field select{background:#fff;border:1px solid #cfd6dc;border-radius:4px;height:36px;padding:0 8px}.tool-utility-status{color:#28725e;font-size:12px;margin-bottom:12px}.tool-utility-status.is-error{color:#b33e34}.tool-metrics-compact{grid-template-columns:repeat(4,minmax(0,1fr))}.tool-config-section{border-top:1px solid var(--tool-line);margin-top:20px;padding-top:18px}.tool-config-section h3{font-size:13px;margin:0 0 13px}.tool-config-grid{display:grid;gap:14px;grid-template-columns:repeat(4,minmax(0,1fr))}.tool-llm-verdict{align-items:center;border:1px solid #cfd9d5;border-left:4px solid #27805f;border-radius:5px;display:grid;gap:3px 14px;grid-template-columns:auto 1fr;margin-bottom:18px;padding:13px 15px}.tool-llm-verdict span{background:#e8f3ee;border-radius:3px;color:#236852;font-size:10px;grid-row:1/3;padding:5px 7px}.tool-llm-verdict strong{font-size:16px}.tool-llm-verdict small{color:var(--tool-muted);font-size:10px}.tool-llm-verdict--warn{border-left-color:#b7791f}.tool-llm-verdict--warn span{background:#fbf2df;color:#8a5b13}.tool-llm-verdict--danger{border-left-color:#bc4c3c}.tool-llm-verdict--danger span{background:#faeae7;color:#973a30}.tool-llm-output{align-items:start}.tool-llm-output h3{font-size:13px;margin:0 0 10px}.tool-assumptions{border-top:1px solid var(--tool-line);margin-top:20px;padding-top:15px}.tool-assumptions strong{font-size:12px}.tool-assumptions p{color:var(--tool-muted);font-size:11px;line-height:1.65;margin:5px 0 0}.tool-reference-links{align-items:center;display:flex;display:flex;flex-wrap:wrap;justify-content:center;gap:8px 16px;margin-top:13px}.tool-reference-links span{color:var(--tool-muted);font-size:10px;font-weight:700}.tool-reference-links a{color:#2b617b;font-size:11px;text-decoration:none}.tool-memory-form{display:grid;gap:14px;grid-template-columns:repeat(4,minmax(0,1fr));margin-top:20px}.tool-unit-input{position:relative}.tool-unit-input input{padding-right:42px}.tool-unit-input em{color:#7a858e;font-size:9px;font-style:normal;position:absolute;right:9px;top:12px}.tool-budget{align-items:center;display:flex;gap:20px;margin-bottom:20px}.tool-budget-ring{align-items:center;border:8px solid #dfe7e4;border-radius:50%;display:flex;flex:0 0 94px;flex-direction:column;height:94px;justify-content:center}.tool-budget-ring strong{font-size:20px}.tool-budget-ring span{color:var(--tool-muted);font-size:9px}.tool-budget-summary strong{font-size:17px}.tool-budget-summary p{color:var(--tool-muted);font-size:12px;line-height:1.6;margin:5px 0 0}.tool-memory-bar{display:flex;height:18px;margin:0 0 20px;overflow:hidden}.tool-memory-segment{min-width:1px}.tool-memory-segment:first-child{background:#3978a8}.tool-memory-segment:nth-child(2){background:#27805f}.tool-memory-segment:nth-child(3){background:#b7791f}.tool-memory-segment:nth-child(4){background:#71569b}.tool-memory-segment:nth-child(5){background:#8d5964}.tool-memory-segment:nth-child(6){background:#63717d}.tool-memory-segment:nth-child(7){background:#dfe5e8}.tool-more{align-items:center;border-top:1px solid var(--tool-line);display:flex;display:flex;flex-wrap:wrap;justify-content:center;gap:8px 18px;margin-top:34px;padding-top:18px}.tool-more span{color:#79838d;font-size:10px;font-weight:700}.tool-more a{color:#3b586f;font-size:11px;text-decoration:none}@media (max-width:767px){.tool-directory,.tool-workspace{margin-left:16px;margin-right:16px;width:auto}.tool-directory-header h1,.tool-header h1{font-size:25px}.tool-grid{grid-template-columns:1fr}.tool-header{display:block}.tool-local-badge{display:inline-flex;margin-top:15px}.tool-panel{padding:16px}.tool-panel-heading{display:block}.tool-panel-heading .tool-button{margin-top:10px}.tool-form-row,.tool-split{grid-template-columns:1fr}.tool-config-grid,.tool-memory-form,.tool-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.tool-actions-between{align-items:flex-start;flex-direction:column}.tool-chart-title{display:block}.tool-chart-title span{display:block;margin-top:4px}}@media (max-width:420px){.tool-config-grid,.tool-memory-form{grid-template-columns:1fr}.tool-actions{align-items:flex-start;display:flex;flex-wrap:wrap;justify-content:center}.tool-card{padding:14px}.tool-group-heading{align-items:flex-start;display:block}.tool-group-heading span{display:block;margin-top:4px}}.affiliate-creative{margin:14px 0;padding:0 12px;position:relative}.affiliate-creative a{display:block;line-height:0;position:relative}.affiliate-creative img{border-radius:4px;display:block;height:auto;max-width:100%;width:100%}.affiliate-copy{color:#fff;left:8%;line-height:1.35;max-width:48%;position:absolute;text-align:left;text-shadow:0 1px 3px rgba(0,0,0,.8);top:50%;transform:translateY(-50%)}.affiliate-copy small,.affiliate-copy strong{display:block;letter-spacing:0}.affiliate-copy strong{font-size:14px}.affiliate-copy small{font-size:10px;margin-top:5px}.affiliate-creative--post-end{display:none;margin:22px auto;max-width:690px}@media (max-width:991px){.affiliate-creative--post-end{display:block}}.affiliate-disclosure{background:rgba(0,0,0,.62);border-radius:2px;bottom:4px;color:#fff;font-size:10px;line-height:1.4;padding:1px 4px;pointer-events:none;position:absolute;right:16px}.topic-hub{background:#f5f7f8;border:1px solid #e1e5e8;border-radius:4px;border-top:3px solid #222;margin-bottom:28px;padding:24px 28px 26px}.topic-hub-header{align-items:flex-end;display:flex;gap:24px;justify-content:space-between;margin-bottom:22px}.topic-hub-header h2{font-size:24px;line-height:1.35;margin:4px 0 6px}.topic-hub-header p{color:#666;font-size:14px;line-height:1.7;margin:0;max-width:620px}.topic-hub-eyebrow{color:#777;font-size:12px;font-weight:600}.topic-entry-link,.topic-hub-all{color:#333;flex:0 0 auto;font-size:13px;font-weight:600;text-decoration:none}.topic-hub-list{display:grid;gap:14px;grid-template-columns:repeat(3,minmax(0,1fr))}.topic-entry{background:#fff;border:1px solid #d9dee3;border-radius:4px;border-top:3px solid #5b6573;box-shadow:0 2px 8px rgba(30,40,50,.06);min-width:0;padding:18px}.topic-entry>p{color:#555;font-size:13px;line-height:1.7;margin:14px 0 16px}.topic-entry--java{border-top-color:#c74634}.topic-entry--java .topic-entry-icon{color:#c74634}.topic-entry--llm{border-top-color:#237a57}.topic-entry--llm .topic-entry-icon{color:#237a57}.topic-entry--agent{border-top-color:#326da8}.topic-entry--agent .topic-entry-icon{color:#326da8}.topic-entry-heading{align-items:flex-start;display:flex;gap:11px}.topic-entry-heading h2,.topic-entry-heading h3{font-size:16px;line-height:1.4;margin:0 0 5px}.topic-entry-heading a{color:#222;text-decoration:none}.topic-entry-heading span{color:#888;display:block;font-size:11px;line-height:1.5}.topic-entry-icon{flex:0 0 18px;font-size:17px;line-height:22px;text-align:center}.topic-hub--full{background:0 0;border:0;margin:0;padding:0}.topic-hub--full .topic-hub-header{border-bottom:1px solid #eee;margin-bottom:6px;padding-bottom:22px}.topic-hub--full .topic-hub-list{display:block}.topic-hub--full .topic-entry{border:0;border-bottom:1px solid #eee;border-radius:0;padding:30px 0;scroll-margin-top:24px}.topic-reading-list{list-style:none;margin:20px 0 0;padding:0}.topic-reading-list li{border-top:1px solid #eee;padding:12px 0}.topic-reading-list a{color:#222;display:block;font-size:14px;font-weight:600;line-height:1.55}.topic-reading-list span{color:#777;display:block;font-size:12px;line-height:1.55;margin-top:3px}.post-series{border-left:3px solid #237a57;margin:30px 0 8px;padding:4px 0 4px 16px}.post-series>span{color:#777;display:block;font-size:12px}.post-series>a{display:inline-block;font-size:16px;font-weight:600;margin-top:3px}.post-series p{color:#666;font-size:13px;line-height:1.65;margin:5px 0 0}@media (max-width:767px){.topic-hub{padding:20px 18px}.topic-hub-header{align-items:flex-start;display:block}.topic-hub-header h2{font-size:21px}.topic-hub-all{display:inline-block;margin-top:12px}.topic-hub-list{grid-template-columns:1fr}.topic-hub--full{padding:0}} \ No newline at end of file +*/.hljs{color:#fff;background:#1c1b1b}.hljs-subst{color:#fff}.hljs-comment{color:#999}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag{color:#88aece}.hljs-attribute{color:#c59bc1}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type{color:#f08d49}.hljs-selector-class{color:#88aece}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#b5bd68}.hljs-meta,.hljs-selector-pseudo{color:#88aece}.hljs-built_in,.hljs-literal,.hljs-title{color:#f08d49}.hljs-bullet,.hljs-code{color:#ccc}.hljs-meta .hljs-string{color:#b5bd68}.hljs-deletion{color:#de7176}.hljs-addition{color:#76c490}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;text-shadow:0 -.1em .2em #000;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}:not(pre)>code[class*=language-],pre[class*=language-]{background:#4c3f33}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border:.3em solid #7a6651;border-radius:.5em;box-shadow:1px 1px .5em #000 inset}:not(pre)>code[class*=language-]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid #7a6651;box-shadow:1px 1px .3em -.1em #000 inset;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#997f66}.token.namespace,.token.punctuation{opacity:.7}.token.boolean,.token.constant,.token.number,.token.property,.token.symbol,.token.tag{color:#d1939e}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#bce051}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f4b73d}.token.atrule,.token.attr-value,.token.keyword{color:#d1939e}.token.important,.token.regex{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red}}.code-container:hover .copy-btn,.highlight:hover .copy-btn{opacity:1}.code-container{position:relative}.code-lang{font-size:40px;line-height:1;opacity:.1;pointer-events:none;position:absolute;right:5px}.copy-btn{color:#333;cursor:pointer;line-height:1.6;opacity:0;padding:2px 6px;position:absolute;transition:opacity .2s ease-in-out;background-color:#eee;background-image:linear-gradient(#fcfcfc,#eee);border:1px solid #d5d5d5;border-radius:3px;font-size:.8125em;right:4px;top:8px}code,figure.highlight,kbd,pre{background:var(--highlight-background);color:var(--highlight-foreground)}figure.highlight,pre{line-height:1.6;margin:0 auto 20px}figure.highlight figcaption,pre .caption{background:var(--highlight-gutter-background);color:var(--highlight-foreground);display:flow-root;font-size:.875em;line-height:1.2;padding:.5em}figure.highlight figcaption a,pre .caption a{color:var(--highlight-foreground);float:right}figure.highlight figcaption a:hover,pre .caption a:hover{border-bottom-color:var(--highlight-foreground)}code,pre{font-family:consolas,Menlo,monospace,'PingFang SC','Microsoft YaHei'}code{border-radius:3px;font-size:.875em;padding:2px 4px;overflow-wrap:break-word}kbd{border:2px solid #ccc;border-radius:.2em;box-shadow:.1em .1em .2em rgba(0,0,0,.1);font-family:inherit;padding:.1em .3em;white-space:nowrap}figure.highlight{overflow:auto;position:relative;color-scheme:light}@media (prefers-color-scheme:dark){figure.highlight{color-scheme:dark}}figure.highlight pre{border:0;margin:0;padding:10px 0}figure.highlight table{border:0;margin:0;width:auto}figure.highlight td{border:0;padding:0}figure.highlight .gutter{-webkit-user-select:none;user-select:none}figure.highlight .gutter pre{background:var(--highlight-gutter-background);color:var(--highlight-gutter-foreground);padding-left:10px;padding-right:10px;text-align:right}figure.highlight .code pre{padding-left:10px;width:100%}figure.highlight .marked{background:rgba(0,0,0,.3)}pre .caption{margin-bottom:10px}.gist table{width:auto}.gist table td{border:0}pre code{background:0 0;padding:0;text-shadow:none}.blockquote-center{border-left:0;margin:40px 0;padding:0;position:relative;text-align:center}.blockquote-center::after,.blockquote-center::before{left:0;line-height:1;opacity:.6;position:absolute;width:100%}.blockquote-center::before{border-top:1px solid #ccc;text-align:left;top:-20px;content:'\f10d';font-family:'Font Awesome 7 Free';font-weight:900}.blockquote-center::after{border-bottom:1px solid #ccc;bottom:-20px;text-align:right;content:'\f10e';font-family:'Font Awesome 7 Free';font-weight:900}.blockquote-center div,.blockquote-center p{text-align:center}.group-picture{margin-bottom:20px}.group-picture .group-picture-row{display:flex;gap:3px;margin-bottom:3px}.group-picture .group-picture-column{flex:1}.group-picture .group-picture-column img{height:100%;margin:0;object-fit:cover;width:100%}.post-body .label{color:#555;padding:0 2px}.post-body .label.default{background:#f0f0f0}.post-body .label.primary{background:#efe6f7}.post-body .label.info{background:#e5f2f8}.post-body .label.success{background:#e7f4e9}.post-body .label.warning{background:#fcf6e1}.post-body .label.danger{background:#fae8eb}.post-body .link-grid{display:grid;grid-gap:1.5rem;gap:1.5rem;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));margin-bottom:20px;padding:1rem}.post-body .link-grid .link-grid-container{border:solid #ddd;box-shadow:1rem 1rem .5rem rgba(0,0,0,.5);min-height:5rem;min-width:0;padding:.5rem;position:relative;transition:background .3s}.post-body .link-grid .link-grid-container:hover{animation:.5s next-shake;background:var(--card-bg-color)}.post-body .link-grid .link-grid-container:active{box-shadow:.5rem .5rem .25rem rgba(0,0,0,.5);transform:translate(.2rem,.2rem)}.post-body .link-grid .link-grid-container .link-grid-image{border:1px solid #ddd;border-radius:50%;box-sizing:border-box;height:5rem;padding:3px;position:absolute;width:5rem}.post-body .link-grid .link-grid-container p{margin:0 1rem 0 6rem}.post-body .link-grid .link-grid-container p:first-of-type{font-size:1.2em}.post-body .link-grid .link-grid-container p:last-of-type{font-size:.8em;line-height:1.3rem;opacity:.7}.post-body .link-grid .link-grid-container a{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}.post-body .note{border-radius:3px;margin-bottom:20px;padding:1em;position:relative;border:1px solid #eee;border-left-width:5px}.post-body .note summary{cursor:pointer;outline:0}.post-body .note summary p{display:inline}.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6{border-bottom:initial;margin:0;padding-top:0}.post-body .note :first-child{margin-top:0}.post-body .note :last-child{margin-bottom:0}.post-body .note.default{border-left-color:#777}.post-body .note.default h2,.post-body .note.default h3,.post-body .note.default h4,.post-body .note.default h5,.post-body .note.default h6{color:#777}.post-body .note.primary{border-left-color:#6f42c1}.post-body .note.primary h2,.post-body .note.primary h3,.post-body .note.primary h4,.post-body .note.primary h5,.post-body .note.primary h6{color:#6f42c1}.post-body .note.info{border-left-color:#428bca}.post-body .note.info h2,.post-body .note.info h3,.post-body .note.info h4,.post-body .note.info h5,.post-body .note.info h6{color:#428bca}.post-body .note.success{border-left-color:#5cb85c}.post-body .note.success h2,.post-body .note.success h3,.post-body .note.success h4,.post-body .note.success h5,.post-body .note.success h6{color:#5cb85c}.post-body .note.warning{border-left-color:#f0ad4e}.post-body .note.warning h2,.post-body .note.warning h3,.post-body .note.warning h4,.post-body .note.warning h5,.post-body .note.warning h6{color:#f0ad4e}.post-body .note.danger{border-left-color:#d9534f}.post-body .note.danger h2,.post-body .note.danger h3,.post-body .note.danger h4,.post-body .note.danger h5,.post-body .note.danger h6{color:#d9534f}.post-body .tabs{margin-bottom:20px}.post-body .tabs,.tabs-comment{padding-top:10px}.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{background:var(--content-bg-color);display:flex;display:flex;flex-wrap:wrap;justify-content:center;margin:0;padding:0;position:sticky;top:0;z-index:5}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid #ddd;border-left:1px solid transparent;border-right:1px solid transparent;border-radius:0;border-top:3px solid transparent;flex-grow:1;list-style-type:none}@media (max-width:413px){.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:block;margin-bottom:5px}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid transparent;border-left:3px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-radius:0}}.post-body .tabs ul.nav-tabs li.tab a,.tabs-comment ul.nav-tabs li.tab a{border-bottom:initial;display:block;line-height:1.8;padding:.25em .75em;text-align:center;transition:.2s ease-out}.post-body .tabs ul.nav-tabs li.tab a i[class^=fa],.tabs-comment ul.nav-tabs li.tab a i[class^=fa]{width:1.285714285714286em}.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-color:#fc6423 #ddd transparent}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-color:#ddd #ddd #ddd #fc6423}}.post-body .tabs ul.nav-tabs li.tab.active a,.tabs-comment ul.nav-tabs li.tab.active a{cursor:default}.post-body .tabs .tab-content,.tabs-comment .tab-content{border:1px solid #ddd;border-radius:0;border-top-color:transparent}@media (max-width:413px){.post-body .tabs .tab-content,.tabs-comment .tab-content{border-radius:0;border-top-color:#ddd}}.post-body .tabs .tab-content .tab-pane,.tabs-comment .tab-content .tab-pane{padding:20px 20px 0}.post-body .tabs .tab-content .tab-pane:not(.active),.tabs-comment .tab-content .tab-pane:not(.active){display:none}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{display:inline-block;margin:-1px 10px 0;padding:0 10px}.pagination .page-number.current{background:#ccc;border-color:#ccc;color:var(--content-bg-color)}.pagination{border-top:1px solid #eee;margin:120px 0 0;text-align:center}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:0;border-top:1px solid #eee;transition:border-color .2s ease-in-out}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-top-color:var(--link-hover-color)}@media (max-width:767px){.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{margin:0 5px}.pagination{border-top:0}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:1px solid #eee;border-top:0}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-bottom-color:var(--link-hover-color)}.site-meta{text-align:center}}.pagination .space{margin:0;padding:0}.comments{margin-top:60px;overflow:hidden}.comment-button-group{display:flex;display:flex;flex-wrap:wrap;justify-content:center;justify-content:center;margin:1em 0}.comment-button-group .comment-button{margin:.1em .2em}.comment-button-group .comment-button.active{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.comment-position{display:none}.comment-position.active{display:block}.tabs-comment{margin-top:4em;padding-top:0}.tabs-comment .comments{margin-top:0;padding-top:0}.headband{background:var(--theme-color);height:3px}@media (max-width:991px){.headband{display:none}}.site-brand-container{display:flex;flex-shrink:0;padding:0 10px}.use-motion .column,.use-motion .site-brand-container .toggle{opacity:0}.site-meta{flex-grow:1;text-align:center}.custom-logo-image{margin-top:20px}@media (max-width:991px){.custom-logo-image{display:none}}.brand{border-bottom:0;color:var(--brand-color);display:inline-block;padding:0}.brand:hover{color:var(--brand-hover-color)}.site-title{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.375em;font-weight:400;line-height:1.5;margin:0}.site-subtitle{color:#ddd;font-size:.8125em;margin:10px 10px 0}.use-motion .custom-logo-image,.use-motion .site-subtitle,.use-motion .site-title{opacity:0;position:relative;top:-10px}.site-nav-right,.site-nav-toggle{display:none}.site-nav-right .toggle,.site-nav-toggle .toggle{color:var(--text-color);padding:10px;width:22px}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:var(--text-color);border-radius:1px}@media (max-width:767px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height);visibility:unset}}.menu{margin:0;padding:1em 0;text-align:center}.menu-item{display:inline-block;list-style:none;margin:0 10px}@media (max-width:767px){.menu-item{display:block;margin-top:10px}.menu-item.menu-item-search{display:none}}.menu-item a{border-bottom:0;display:block;font-size:.8125em;transition:border-color .2s ease-in-out}.menu-item a.menu-item-active,.menu-item a:hover{background:var(--menu-item-bg-color)}.menu-item i[class^=fa]{margin-right:8px}.menu-item .badge{background:#ccc;border-radius:10px;color:var(--content-bg-color);font-weight:700;line-height:1;margin-left:.35em;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.use-motion .menu-item{visibility:hidden}.github-corner :hover .octo-arm{animation:560ms ease-in-out octocat-wave}.github-corner svg{color:#fff;fill:var(--theme-color);position:absolute;right:0;top:0;z-index:5}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:991px){.github-corner{display:none}.github-corner svg{color:var(--theme-color);fill:#fff}.github-corner .github-corner:hover .octo-arm{animation:none}.github-corner .github-corner .octo-arm{animation:560ms ease-in-out octocat-wave}.sidebar{left:-320px;background:#222;bottom:0;box-shadow:inset 0 2px 6px #000;max-height:100vh;overflow-y:auto;position:fixed;top:0;transition:left .2s ease-out,right .2s ease-out;width:320px;z-index:20}.sidebar-active .sidebar{left:0}.sidebar a{border-bottom-color:#555;color:#999}.sidebar a:hover{border-bottom-color:#eee;color:#eee}.links-of-author:not(:first-child){margin-top:15px}.links-of-author a{border-bottom-color:#555;display:inline-block;margin-bottom:10px;margin-right:10px;vertical-align:middle}.links-of-author a::before{background:#2e1f20;display:inline-block;margin-right:3px;transform:translateY(-2px);border-radius:50%;content:' ';height:4px;width:4px}.links-of-blogroll-item{padding:0 5px}.popular-posts .popular-posts-item .popular-posts-link:hover{background:0 0}.sidebar-dimmer{background:#000;height:100%;left:0;opacity:0;position:fixed;top:0;transition:visibility .4s,opacity .4s;visibility:hidden;width:100%;z-index:10}.sidebar-active .sidebar-dimmer{opacity:.7;visibility:visible}}.sidebar-inner{color:#999;padding:18px 10px;text-align:center;display:flex;flex-direction:column;justify-content:center}.sidebar-toggle{bottom:61px;height:16px;padding:5px;width:16px;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;left:30px}.sidebar-toggle:hover{opacity:.8}@media (max-width:991px){.sidebar-toggle{left:20px;opacity:.8}}.sidebar-toggle:hover .toggle-line{background:#fc6423}@media (any-hover:hover){body:not(.sidebar-active) .sidebar-toggle:hover :first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}body:not(.sidebar-active) .sidebar-toggle:hover :last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}}.sidebar-active .sidebar-toggle :nth-child(2){opacity:0}.sidebar-active .sidebar-toggle :first-child{top:6px;transform:rotate(45deg)}.sidebar-active .sidebar-toggle :last-child{top:-6px;transform:rotate(-45deg)}.sidebar-nav{font-size:.875em;height:0;margin:0;overflow:hidden;padding-left:0;pointer-events:none;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}.sidebar-nav-active .sidebar-nav{height:calc(2em + 1px);pointer-events:unset;visibility:unset}.sidebar-nav li{border-bottom:1px solid transparent;color:var(--text-color);cursor:pointer;display:inline-block;transition:border-bottom-color .2s ease-in-out,color .2s ease-in-out}.sidebar-nav li.sidebar-nav-overview{margin-left:10px}.sidebar-nav li:hover{color:#fc6423}.sidebar-overview-active .sidebar-nav-overview,.sidebar-toc-active .sidebar-nav-toc{border-bottom-color:#fc6423;color:#fc6423;transition-delay:0.2s}.sidebar-overview-active .sidebar-nav-overview:hover,.sidebar-toc-active .sidebar-nav-toc:hover{color:#fc6423}.sidebar-panel-container{align-items:start;display:grid;flex:1;overflow-x:hidden;overflow-y:auto;padding-top:0;transition:padding-top .2s ease-in-out}.sidebar-nav-active .sidebar-panel-container{padding-top:20px}.sidebar-panel{animation:.2s ease-in-out deactivate-sidebar-panel;grid-area:1/1;height:0;opacity:0;overflow:hidden;pointer-events:none;transform:translateY(0);transition:.2s ease-in-out;transition-property:opacity,transform,visibility;visibility:hidden}.sidebar-nav-active .sidebar-panel,.sidebar-overview-active .sidebar-panel.post-toc-wrap{transform:translateY(-20px)}.sidebar-overview-active:not(.sidebar-nav-active) .sidebar-panel.post-toc-wrap{transition-delay:0s,0.2s,0s}.sidebar-overview-active .sidebar-panel.site-overview-wrap,.sidebar-toc-active .sidebar-panel.post-toc-wrap{animation-name:activate-sidebar-panel;height:auto;opacity:1;pointer-events:unset;transform:translateY(0);transition-delay:0.2s,0.2s,0s;visibility:unset}.sidebar-panel.site-overview-wrap{display:flex;flex-direction:column;justify-content:center;gap:10px;justify-content:flex-start}@keyframes deactivate-sidebar-panel{from{height:var(--inactive-panel-height,0)}to{height:var(--active-panel-height,0)}}@keyframes activate-sidebar-panel{from{height:var(--inactive-panel-height,auto)}to{height:var(--active-panel-height,auto)}}.post-toc{font-size:.875em}.post-toc ol{list-style:none;margin:0;padding:0 2px 0 10px;text-align:left}.post-toc ol>:last-child{margin-bottom:5px}.post-toc ol>ol{padding-left:0}.post-toc ol a{transition:.2s ease-in-out}.post-toc .nav-item{line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.post-toc .nav .nav-child{--height:0;height:0;opacity:0;overflow:hidden;transition:.2s ease-in-out;visibility:hidden}.post-toc .nav .active>.nav-child{height:var(--height,auto);opacity:1;visibility:unset}.post-toc .nav .active>a{border-bottom-color:#fc6423;color:#fc6423}.post-toc .nav .active-current>a,.post-toc .nav .active-current>a:hover{color:#fc6423}.site-author-image{border:1px solid #eee;max-width:120px;padding:2px}.site-author-name{color:var(--text-color);font-weight:600;margin:0}.site-description{color:#999;font-size:.8125em;margin-top:0}.site-state{display:flex;flex-wrap:wrap;justify-content:center;line-height:1.4}.site-state-item{padding:0 15px}.site-state-item a{border-bottom:0;display:block}.site-state-item-count{display:block;font-size:1em;font-weight:600}.site-state-item-name{color:#999;font-size:.8125em}.sidebar .sidebar-button:not(:first-child){margin-top:15px}.sidebar .sidebar-button button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:1px solid #fc6423;border-radius:4px}.sidebar .sidebar-button button:hover{background:#fc6423;color:#fff}.sidebar .sidebar-button button i[class^=fa]{margin-right:5px}.links-of-author a{font-size:.8125em}.links-of-author i[class^=fa]{margin-right:2px}.cc-license .cc-opacity{border-bottom:0;opacity:.7}.cc-license .cc-opacity:hover{opacity:.9}.cc-license img{display:inline-block}.links-of-blogroll{font-size:.8125em}.links-of-blogroll-title{font-size:.875em;font-weight:600}.links-of-blogroll-list{list-style:none;gap:5px;margin:5px 0 0;padding:0;display:flex;flex-wrap:wrap;justify-content:center;flex-direction:column}.links-of-blogroll-item{max-width:100%}.links-of-blogroll-item a{box-sizing:border-box;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-post-related{font-size:.8125em;padding:18px 0 0}.popular-posts{margin:0;padding:1em 0;text-align:left}.popular-posts .popular-posts-item{display:block}.popular-posts .popular-posts-item .popular-posts-link{border-bottom:0;display:block;padding:5px 20px;transition:background .2s ease-in-out}.popular-posts .popular-posts-item .popular-posts-link:hover{background:var(--menu-item-bg-color)}.popular-posts .popular-posts-item .popular-posts-time{color:#999}.footer{color:#999;font-size:.875em;padding:20px 0;transition:left .2s ease-in-out,right .2s ease-in-out}.footer.footer-fixed{bottom:0;left:0;position:absolute;right:0}.footer-inner{box-sizing:border-box;text-align:center;display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.footer-inner{width:auto}}@media (min-width:1200px){.footer-inner{width:1160px}}@media (min-width:1600px){.footer-inner{width:73%}}.use-motion .footer{opacity:0}.languages{display:inline-block;font-size:1.125em;position:relative}.languages .lang-select-label span{margin:0 .5em}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:red;display:inline-block;margin:0 5px}.beian img{display:inline-block;margin:0 3px;vertical-align:middle}@keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,50%,60%,70%,80%{transform:scale(1.1)}}@media (max-width:567px){.main-inner{padding:initial!important}.posts-expand .post-header{margin-bottom:10px!important}.post-block{margin-top:initial!important;padding:8px 18px!important}.post-body h1,.post-body h2,.post-body h3,.post-body h4,.post-body h5,.post-body h6{margin:20px 0 8px}.post-body .note h1,.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6,.post-body .tabs .tab-content .tab-pane h1,.post-body .tabs .tab-content .tab-pane h2,.post-body .tabs .tab-content .tab-pane h3,.post-body .tabs .tab-content .tab-pane h4,.post-body .tabs .tab-content .tab-pane h5,.post-body .tabs .tab-content .tab-pane h6{margin:0 5px}.post-body>p{margin:0 0 10px}.post-body .note>p,.post-body .tabs .tab-content .tab-pane>p{padding:0 5px}.post-body img,.post-body video{margin-bottom:10px!important}.post-body figure:not(.highlight) figcaption{margin:-5px auto 15px!important}.post-body .note{margin-bottom:10px!important;padding:10px!important}.post-body .tabs .tab-content .tab-pane{padding:10px 10px 0!important}.post-eof{margin:40px auto 20px!important}.pagination{margin-top:40px}}.back-to-top{font-size:12px;align-items:center;bottom:-100px;color:#fff;display:flex;height:26px;transition:bottom .2s ease-in-out;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;left:30px}.back-to-top span{margin-right:8px;display:none}.back-to-top .fa{text-align:center;width:26px}.back-to-top:hover{opacity:.8;color:#fc6423}.back-to-top.back-to-top-on{bottom:30px}.rtl.post-body a,.rtl.post-body h1,.rtl.post-body h2,.rtl.post-body h3,.rtl.post-body h4,.rtl.post-body h5,.rtl.post-body h6,.rtl.post-body li,.rtl.post-body ol,.rtl.post-body p,.rtl.post-body ul{direction:rtl;font-family:UKIJ Ekran}.rtl.post-title{font-family:UKIJ Ekran}.post-button{margin-top:40px;text-align:center}.use-motion .collection-header,.use-motion .comments,.use-motion .pagination,.use-motion .post-block,.use-motion .post-body,.use-motion .post-header{visibility:hidden}.posts-collapse .post-content{margin-bottom:35px;margin-left:35px;position:relative}@media (max-width:767px){.posts-collapse .post-content{margin-left:0;margin-right:0}}.posts-collapse .post-content .collection-title{font-size:1.125em;position:relative}.posts-collapse .post-content .collection-title::before{background:#999;border:1px solid #fff;margin-left:-6px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:10px;width:10px}.posts-collapse .post-content .collection-year{font-size:1.5em;font-weight:700;margin:60px 0;position:relative}.posts-collapse .post-content .collection-year .collection-year-count{font-size:.75em;background:#ccc;border-radius:10px;color:var(--content-bg-color);font-weight:700;line-height:1;margin-left:.35em;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.posts-collapse .post-content .collection-year::before{background:#bbb;margin-left:-4px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:8px;width:8px}.posts-collapse .post-content .collection-header{display:block;margin-left:20px}.posts-collapse .post-content .collection-header small{color:#bbb;margin-left:5px}.posts-collapse .post-content .post-header{border-bottom:1px dashed #ccc;margin:30px 2px 0;padding-left:15px;position:relative;transition:border .2s ease-in-out}.posts-collapse .post-content .post-header::before{background:#bbb;border:1px solid #fff;left:-6px;position:absolute;top:.75em;transition:background .2s ease-in-out;border-radius:50%;content:' ';height:6px;width:6px}.posts-collapse .post-content .post-header:hover{border-bottom-color:#666}.posts-collapse .post-content .post-header:hover::before{background:#222}.posts-collapse .post-content .post-meta-container{display:inline;font-size:.75em;margin-right:10px}.posts-collapse .post-content .post-title{display:inline}.posts-collapse .post-content .post-title a{border-bottom:0;color:var(--link-color)}.posts-collapse .post-content .post-title .fa{font-size:.875em;margin-left:5px}.posts-collapse .post-content::before{background:#f5f5f5;content:' ';height:100%;margin-left:-2px;position:absolute;top:1.25em;width:4px}.post-body{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;overflow-wrap:break-word}@media (min-width:1200px){.post-body{font-size:1.125em}}@media (min-width:992px){.post-body{text-align:justify}}.post-body h1 .header-anchor,.post-body h1 .headerlink,.post-body h2 .header-anchor,.post-body h2 .headerlink,.post-body h3 .header-anchor,.post-body h3 .headerlink,.post-body h4 .header-anchor,.post-body h4 .headerlink,.post-body h5 .header-anchor,.post-body h5 .headerlink,.post-body h6 .header-anchor,.post-body h6 .headerlink{border-bottom-style:none;color:inherit;float:right;font-size:.875em;margin-left:10px;opacity:0}.post-body h1 .header-anchor::before,.post-body h1 .headerlink::before,.post-body h2 .header-anchor::before,.post-body h2 .headerlink::before,.post-body h3 .header-anchor::before,.post-body h3 .headerlink::before,.post-body h4 .header-anchor::before,.post-body h4 .headerlink::before,.post-body h5 .header-anchor::before,.post-body h5 .headerlink::before,.post-body h6 .header-anchor::before,.post-body h6 .headerlink::before{content:'\f0c1';font-family:'Font Awesome 7 Free';font-weight:900}.post-body h1:hover .header-anchor,.post-body h1:hover .headerlink,.post-body h2:hover .header-anchor,.post-body h2:hover .headerlink,.post-body h3:hover .header-anchor,.post-body h3:hover .headerlink,.post-body h4:hover .header-anchor,.post-body h4:hover .headerlink,.post-body h5:hover .header-anchor,.post-body h5:hover .headerlink,.post-body h6:hover .header-anchor,.post-body h6:hover .headerlink{opacity:.5}.post-body h1:hover .header-anchor:hover,.post-body h1:hover .headerlink:hover,.post-body h2:hover .header-anchor:hover,.post-body h2:hover .headerlink:hover,.post-body h3:hover .header-anchor:hover,.post-body h3:hover .headerlink:hover,.post-body h4:hover .header-anchor:hover,.post-body h4:hover .headerlink:hover,.post-body h5:hover .header-anchor:hover,.post-body h5:hover .headerlink:hover,.post-body h6:hover .header-anchor:hover,.post-body h6:hover .headerlink:hover{opacity:1}.post-body .exturl .fa{font-size:.875em;margin-left:4px}.post-body figure:not(.highlight){margin:0 auto 20px}.post-body figure:not(.highlight) figcaption{color:#999;font-size:.875em;font-weight:700;line-height:1;margin:-15px auto 15px;text-align:center}.post-body embed,.post-body iframe,.post-body img,.post-body video{margin-bottom:20px}.post-body .video-container{height:0;margin-bottom:20px;overflow:hidden;padding-top:75%;position:relative;width:100%}.post-body .video-container embed,.post-body .video-container iframe,.post-body .video-container object{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.post-gallery{display:flex;min-height:200px}.post-gallery .post-gallery-image{flex:1}.post-gallery .post-gallery-image:not(:first-child){clip-path:polygon(40px 0,100% 0,100% 100%,0 100%);margin-left:-20px}.post-gallery .post-gallery-image:not(:last-child){margin-right:-20px}.post-gallery .post-gallery-image img{height:100%;object-fit:cover;opacity:1;width:100%}.posts-expand .post-gallery{margin-bottom:60px}.posts-collapse .post-gallery{margin:15px 0}.posts-expand .post-header{font-size:1.125em;margin-bottom:60px;text-align:center}.posts-expand .post-title{font-size:1.5em;font-weight:400;margin:initial;overflow-wrap:break-word}.posts-expand .post-title-link{border-bottom:0;color:var(--link-color);display:inline-block;max-width:100%;position:relative}.posts-expand .post-title-link::before{background:var(--link-color);bottom:0;content:'';height:2px;left:0;position:absolute;transform:scaleX(0);transition:transform .2s ease-in-out;width:100%}.posts-expand .post-title-link:hover::before{transform:scaleX(1)}.posts-expand .post-title-link .fa{font-size:.875em;margin-left:5px}.post-sticky-flag{display:inline-block;margin-right:8px;transform:rotate(30deg)}.posts-expand .post-meta-container{color:#999;font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:.75em;margin-top:3px}.posts-expand .post-meta-container .post-description{font-size:.875em;margin-top:2px}.posts-expand .post-meta-container time{border-bottom:1px dashed #999}.post-meta{display:flex;flex-wrap:wrap;justify-content:center}:not(.post-meta-break)+.post-meta-item::before{content:'|';margin:0 .5em}.post-meta-item-icon{margin-right:3px}@media (max-width:991px){.back-to-top{left:20px;opacity:.8}.post-body{text-align:justify}.post-meta-item-text{display:none}}.post-meta-break{flex-basis:100%;height:0}.post-nav{border-top:1px solid #eee;display:flex;gap:30px;justify-content:space-between;margin-top:1em;padding:10px 5px 0}.post-nav-item{flex:1}.post-nav-item a{border-bottom:0;display:block;font-size:.875em;line-height:1.6}.post-nav-item a:active{top:2px}.post-nav-item .fa{font-size:.75em}.post-nav-item:first-child .fa{margin-right:5px}.post-nav-item:last-child{text-align:right}.post-nav-item:last-child .fa{margin-left:5px}.post-footer{display:flex;flex-direction:column;justify-content:center}.post-eof{background:#ccc;height:1px;margin:80px auto 60px;width:8%}.post-block:last-of-type .post-eof{display:none}.post-copyright ul{list-style:none;overflow:hidden;padding:.5em 1em;position:relative;background:var(--card-bg-color);border-left:3px solid #ff2a2a;margin:1em 0 0}.post-copyright ul::after{content:'\f25e';font-family:'Font Awesome 7 Brands';font-size:200px;opacity:.1;position:absolute;right:-50px;top:-150px}.post-tags{margin-top:40px;text-align:center}.post-tags a{display:inline-block;font-size:.8125em}.post-tags a:not(:last-child){margin-right:10px}.social-like{border-top:1px solid #eee;font-size:.875em;margin-top:1em;padding-top:1em;display:flex;flex-wrap:wrap;justify-content:center}.social-like a{border-bottom:none}.reward-container{margin:1em 0 0;padding:1em 0;text-align:center}.reward-container button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:2px solid #fc6423;border-radius:2px;outline:0;vertical-align:text-top}.reward-container button:hover{background:#fc6423;color:#fff}.post-reward{display:none;padding-top:20px}.post-reward.active{display:block}.post-reward div{display:inline-block}.post-reward div span{display:block}.post-reward img{display:inline-block;margin:.8em 2em 0;max-width:100%;width:180px}@keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}.category-all-page .category-all-title{text-align:center}.category-all-page .category-all{margin-top:20px}.category-all-page .category-list{list-style:none;margin:0;padding:0}.category-all-page .category-list-item{margin:5px 10px}.category-all-page .category-list-count{font-size:.75em;background:#ccc;border-radius:10px;color:var(--content-bg-color);font-weight:700;line-height:1;margin-left:.35em;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.category-all-page .category-list-child{padding-left:10px}.event-list hr{background:#222;margin:20px 0 45px}.event-list hr::after{background:#222;color:#fff;content:'NOW';display:inline-block;font-weight:700;padding:0 5px}.event-list .event{--event-background:#222;--event-foreground:#bbb;--event-title:#fff;background:var(--event-background);padding:15px}.event-list .event .event-summary{border-bottom:0;color:var(--event-title);margin:0;padding:0 0 0 35px;position:relative}.event-list .event .event-summary::before{animation:1s ease-in-out infinite alternate dot-flash;background:var(--event-title);left:0;margin-top:-6px;position:absolute;top:50%;border-radius:50%;content:' ';height:12px;width:12px}.event-list .event:nth-of-type(odd) .event-summary::before{animation-delay:.5s}.event-list .event:not(:last-child){margin-bottom:20px}.event-list .event .event-relative-time{color:var(--event-foreground);display:inline-block;font-size:12px;font-weight:400;padding-left:12px}.event-list .event .event-details{color:var(--event-foreground);display:block;line-height:18px;padding:6px 0 6px 35px}.event-list .event .event-details::before{color:var(--event-foreground);display:inline-block;margin-right:9px;width:14px;font-family:'Font Awesome 7 Free';font-weight:900}.event-list .event .event-details.event-location::before{content:'\f041'}.event-list .event .event-details.event-duration::before{content:'\f017'}.event-list .event .event-details.event-description::before{content:'\f024'}.event-list .event-past{--event-background:#f5f5f5;--event-foreground:#999;--event-title:#222}@keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}ul.breadcrumb{font-size:.75em;list-style:none;margin:1em 0;padding:0 2em;text-align:center}ul.breadcrumb li{display:inline}ul.breadcrumb li:not(:first-child)::before{content:'/\00a0';font-weight:400;padding:.5em}ul.breadcrumb li:last-child{font-weight:700}.tag-cloud{text-align:center}.tag-cloud a{display:inline-block;margin:10px}.tag-cloud-0{border-bottom-color:#aaa;color:#aaa}.tag-cloud-1{border-bottom-color:#9a9a9a;color:#9a9a9a}.tag-cloud-2{border-bottom-color:#8b8b8b;color:#8b8b8b}.tag-cloud-3{border-bottom-color:#7c7c7c;color:#7c7c7c}.tag-cloud-4{border-bottom-color:#6c6c6c;color:#6c6c6c}.tag-cloud-5{border-bottom-color:#5d5d5d;color:#5d5d5d}.tag-cloud-6{border-bottom-color:#4e4e4e;color:#4e4e4e}.tag-cloud-7{border-bottom-color:#3e3e3e;color:#3e3e3e}.tag-cloud-8{border-bottom-color:#2f2f2f;color:#2f2f2f}.tag-cloud-9{border-bottom-color:#202020;color:#202020}.tag-cloud-10{border-bottom-color:#111;color:#111}.search-active{overflow:hidden;margin-right:var(--dialog-scrollgutter,0)}.search-pop-overlay{background:rgba(0,0,0,0);display:flex;height:100%;left:0;position:fixed;top:0;transition:visibility .4s,background .4s;visibility:hidden;width:100%;z-index:40}.search-active .search-pop-overlay{background:rgba(0,0,0,.3);visibility:visible}.search-popup{background:var(--card-bg-color);border-radius:5px;height:80%;margin:auto;transform:scale(0);transition:transform .4s;width:700px}.search-active .search-popup{transform:scale(1)}@media (max-width:767px){.search-popup{border-radius:0;height:100%;width:100%}}.search-popup .popup-btn-close,.search-popup .search-icon{color:#999;font-size:18px;padding:0 10px}.search-popup .popup-btn-close{cursor:pointer}.search-popup .popup-btn-close:hover .fa{color:#222}.search-popup .search-header{background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;padding:5px}@media (prefers-color-scheme:dark){.tag-cloud-0{border-bottom-color:#555;color:#555}.tag-cloud-1{border-bottom-color:#646464;color:#646464}.tag-cloud-2{border-bottom-color:#737373;color:#737373}.tag-cloud-3{border-bottom-color:#828282;color:#828282}.tag-cloud-4{border-bottom-color:#929292;color:#929292}.tag-cloud-5{border-bottom-color:#a1a1a1;color:#a1a1a1}.tag-cloud-6{border-bottom-color:#b0b0b0;color:#b0b0b0}.tag-cloud-7{border-bottom-color:silver;color:silver}.tag-cloud-8{border-bottom-color:#cfcfcf;color:#cfcfcf}.tag-cloud-9{border-bottom-color:#dedede;color:#dedede}.tag-cloud-10{border-bottom-color:#eee;color:#eee}.search-popup .search-header{background:#666}}.search-popup input.search-input{background:0 0;border:0;outline:0;width:100%}.search-popup input.search-input::-webkit-search-cancel-button{display:none}.search-popup .search-result-container{display:flex;flex-direction:column;height:calc(100% - 55px);overflow:auto;padding:5px 25px}.search-popup .search-result-container hr{flex-shrink:0;margin:5px 0 10px}.search-popup .search-result-container hr:first-child{display:none}.search-popup .search-result-list{margin:0 5px;padding:0}.search-popup a.search-result-title{font-weight:700}.search-popup p.search-result{border-bottom:1px dashed #ccc;margin:0 0 10px;padding:5px 0}.search-popup .search-input-container{flex-grow:1;padding:2px}.search-popup .search-result-icon{color:#ccc;margin:auto}mark.search-keyword{background:0 0;border-bottom:1px dashed #ff2a2a;color:#ff2a2a;font-weight:700}.search-stats{align-items:center;display:flex;justify-content:space-between}.search-stats img{height:1em;margin:0}.pagination.algolia-pagination{margin-top:auto;padding:40px 0;visibility:visible}.use-motion .animated{animation-fill-mode:none;visibility:inherit}.use-motion .sidebar .animated{animation-fill-mode:both}header.header{background:var(--content-bg-color);border-radius:initial;box-shadow:initial}.main{align-items:stretch;display:flex;justify-content:space-between;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.main{width:auto}}@media (min-width:1200px){.main{width:1160px}}@media (min-width:1600px){.main{width:73%}}@media (max-width:991px){header.header{border-radius:initial}.main{display:block;width:auto}}.main-inner{border-radius:initial;box-sizing:border-box;width:calc(100% - 252px)}.footer-inner{padding-left:252px}@media (max-width:991px){.main-inner{border-radius:initial;width:100%}.footer-inner{padding-left:0;padding-right:0;width:auto}}.column{width:240px}.site-brand-container{background:var(--theme-color)}.site-meta{padding:20px 0}.site-nav-right .toggle,.site-nav-toggle .toggle{color:#fff}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:#fff}@media (min-width:768px) and (max-width:991px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height);visibility:unset}}.menu .menu-item{display:block;margin:0}.menu .menu-item a{padding:5px 20px;position:relative;transition-property:background-color;display:flex;align-items:center}.menu .menu-item a .badge{margin-left:auto}@media (max-width:991px){.column{width:auto}.site-nav-on .site-brand-container{box-shadow:0 0 16px rgba(0,0,0,.5)}.menu .menu-item.menu-item-search{display:none}}.main-menu .menu-item-active::after{background:#bbb;border-radius:50%;content:' ';height:6px;margin-top:-3px;position:absolute;right:15px;top:50%;width:6px}.sub-menu{margin:0;padding:6px 0}.sub-menu .menu-item{display:inline-block}.sub-menu .menu-item a{background:0 0;margin:5px 10px;padding:initial}.sub-menu .menu-item a:hover{background:0 0;color:#fc6423}.sub-menu .menu-item-active{border-bottom-color:#fc6423;color:#fc6423}.sub-menu .menu-item-active:hover{border-bottom-color:#fc6423}@media (min-width:992px){.sidebar{position:sticky;top:12px}.sidebar-toggle{display:none}.sidebar-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;box-sizing:border-box;color:var(--text-color);margin-top:12px;max-height:calc(100vh - 24px)}.site-state-item{padding:0 10px}.sidebar .sidebar-button{border-bottom:1px dotted #ccc;border-top:1px dotted #ccc}.sidebar .sidebar-button button{border:0;color:#fc6423;display:block;width:100%}.sidebar .sidebar-button button:hover{background:0 0;border:0;color:#e34603}.links-of-author{display:flex;flex-wrap:wrap;justify-content:center}.links-of-author-item{margin:5px 0 0;width:50%}.links-of-author-item a{border-bottom:0;border-radius:4px;padding:0 5px;box-sizing:border-box;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.links-of-author-item a:hover{background:var(--body-bg-color)}.links-of-blogroll-item a{padding:0 5px}}.main-inner{background:var(--content-bg-color);box-shadow:initial;padding:40px}@media (max-width:991px){.main-inner{padding:20px}}.sub-menu{border-bottom:1px solid #ddd}.post-block:first-of-type{padding-top:40px}@media (max-width:767px){.pagination{margin-bottom:10px}}.sidebar-tag-cloud{line-height:1.8;margin-top:12px;text-align:center}.sidebar-tag-cloud .tag-cloud-tags a{display:inline-block;margin:0 5px;white-space:nowrap}.tool-directory,.tool-workspace{--tool-ink:#18212b;--tool-muted:#66717e;--tool-line:#dce2e7;--tool-soft:#f4f6f7;--tool-primary:#166a5b;color:var(--tool-ink);margin:0 auto;max-width:1120px}.tool-workspace [hidden]{display:none!important}.tool-directory-header{border-bottom:1px solid var(--tool-line);padding:18px 0 34px}.tool-directory-header h1{font-size:34px;line-height:1.25;margin:8px 0 12px}.tool-directory-header p{color:var(--tool-muted);font-size:15px;line-height:1.75;margin:0;max-width:720px}.tool-eyebrow{color:#16705f;display:block;font-size:11px;font-weight:700;letter-spacing:0}.tool-privacy{align-items:center;color:#3e6159;display:flex;font-size:12px;gap:7px;margin-top:16px}.tool-grid{display:grid;gap:14px;grid-template-columns:repeat(2,minmax(0,1fr));padding:28px 0}.tool-group{border-bottom:1px solid var(--tool-line);padding-top:28px}.tool-group .tool-grid{padding:16px 0 28px}.tool-group-heading{align-items:baseline;display:flex;gap:12px}.tool-group-heading h2{font-size:18px;margin:0}.tool-group-heading span{color:var(--tool-muted);font-size:11px}.tool-card{align-items:center;background:#fff;border:1px solid var(--tool-line);border-left:4px solid #55616d;border-radius:6px;color:var(--tool-ink);display:flex;gap:15px;min-height:94px;padding:16px 18px;text-decoration:none;transition:border-color .16s,box-shadow .16s,transform .16s}.tool-card:hover{border-color:#aeb8c0;box-shadow:0 7px 20px rgba(24,33,43,.08);color:var(--tool-ink);transform:translateY(-2px)}.tool-card>i{color:#87919a;margin-left:auto}.tool-accent-red{border-left-color:#bc4c3c}.tool-accent-blue{border-left-color:#326fa8}.tool-accent-green{border-left-color:#27805f}.tool-accent-amber{border-left-color:#b7791f}.tool-accent-violet{border-left-color:#71569b}.tool-card-mark{align-items:center;background:#eef1f3;border-radius:4px;color:#33404b;display:flex;flex:0 0 44px;font-family:monospace;font-size:14px;font-weight:700;height:44px;justify-content:center}.tool-card-copy{min-width:0}.tool-card-copy small,.tool-card-copy strong{display:block}.tool-card-copy strong{font-size:16px;line-height:1.45}.tool-card-copy small{color:var(--tool-muted);font-size:12px;line-height:1.55;margin-top:4px}.tool-directory-note{border-top:1px solid var(--tool-line);padding:24px 0 12px}.tool-directory-note h2{font-size:18px;margin:0 0 8px}.tool-directory-note p{color:var(--tool-muted);font-size:13px;line-height:1.75;margin:0}.tool-breadcrumb{font-size:12px;margin:4px 0 22px}.tool-breadcrumb a{color:#596572;text-decoration:none}.tool-breadcrumb i{margin-right:6px}.tool-header{align-items:flex-start;border-bottom:1px solid var(--tool-line);display:flex;gap:28px;justify-content:space-between;margin-bottom:24px;padding-bottom:24px}.tool-header h1{font-size:30px;line-height:1.3;margin:7px 0 10px}.tool-header p{color:var(--tool-muted);font-size:14px;line-height:1.7;margin:0;max-width:740px}.tool-local-badge{align-items:center;background:#edf6f2;border:1px solid #cce4da;border-radius:5px;color:#1d6353;display:flex;flex:0 0 auto;gap:10px;padding:9px 12px}.tool-local-badge>i{font-size:16px}.tool-local-badge span{font-size:12px;font-weight:700;line-height:1.25}.tool-local-badge small{display:block;font-size:10px;font-weight:400;margin-top:2px}.tool-panel{border:1px solid var(--tool-line);border-radius:6px;padding:22px}.tool-input-panel{border-top:3px solid #1f6e60}.tool-panel-heading,.tool-section-heading{align-items:flex-start;display:flex;gap:20px;justify-content:space-between}.tool-panel-heading h2,.tool-section-heading h2{font-size:18px;line-height:1.4;margin:0}.tool-panel-heading p,.tool-section-heading p{color:var(--tool-muted);font-size:12px;line-height:1.6;margin:4px 0 0}.tool-section-heading{border-bottom:1px solid var(--tool-line);margin-bottom:20px;padding-bottom:16px}.tool-section-heading span{color:#16705f;font-size:10px;font-weight:700}.tool-section-heading h2{margin-top:3px}.tool-dropzone{align-items:center;background:#f7f9fa;border:1px dashed #aeb9c2;border-radius:5px;cursor:pointer;display:flex;gap:12px;margin:18px 0 14px;padding:14px 16px;transition:background .15s,border-color .15s}.tool-dropzone.is-dragging,.tool-dropzone:hover{background:#edf6f2;border-color:#2c7b69}.tool-dropzone>i{color:#2c7162;font-size:20px}.tool-dropzone small,.tool-dropzone span,.tool-dropzone strong{display:block}.tool-dropzone strong{font-size:13px}.tool-dropzone small{color:var(--tool-muted);font-size:11px;margin-top:2px}.tool-dropzone input{height:1px;opacity:0;position:absolute;width:1px}.tool-field{display:block;min-width:0}.tool-field>span{color:#48535f;display:block;font-size:11px;font-weight:700;margin-bottom:6px}.tool-field input,.tool-field select,.tool-field textarea{background:#fff;border:1px solid #cfd6dc;border-radius:4px;box-sizing:border-box;color:var(--tool-ink);font-family:inherit;font-size:13px;outline:0;width:100%}.tool-field input:focus,.tool-field select:focus,.tool-field textarea:focus{border-color:#247766;box-shadow:0 0 0 2px rgba(36,119,102,.12)}.tool-field textarea{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.55;min-height:180px;padding:12px;resize:vertical}.tool-field input,.tool-field select{height:38px;padding:0 10px}.tool-field-wide{margin-top:12px}.tool-form-row{display:grid;gap:14px;grid-template-columns:repeat(2,minmax(0,1fr));margin:18px 0 10px}.tool-actions{align-items:center;display:flex;gap:9px;margin-top:16px;min-height:36px}.tool-actions-between{justify-content:space-between}.tool-button{align-items:center;border:1px solid transparent;border-radius:4px;cursor:pointer;display:inline-flex;font-size:12px;font-weight:600;gap:7px;height:36px;justify-content:center;padding:0 14px}.tool-button:disabled{cursor:wait;opacity:.55}.tool-button-primary{background:#176b5c;border-color:#176b5c;color:#fff}.tool-button-primary:hover{background:#11594c}.tool-button-secondary{background:#edf4f8;border-color:#c6d8e3;color:#28566f}.tool-button-secondary:hover{background:#e1edf3}.tool-button-quiet{background:#fff;border-color:#cfd6dc;color:#3c4854}.tool-button-quiet:hover{background:#f4f6f7}.tool-hint,.tool-progress{color:var(--tool-muted);font-size:11px;margin-left:4px}.tool-results{border-top:3px solid #2e6fa3;margin-top:28px;padding-top:22px}.tool-empty{align-items:center;color:#7b8690;display:flex;flex-direction:column;min-height:210px;justify-content:center;text-align:center}.tool-empty i{color:#a4adb5;font-size:29px;margin-bottom:13px}.tool-empty strong{color:#5a6570;font-size:14px}.tool-empty span{font-size:11px;margin-top:5px}.tool-metrics{display:grid;gap:1px;grid-template-columns:repeat(5,minmax(0,1fr));margin-bottom:22px;overflow:hidden}.tool-metric{background:#f1f4f5;min-width:0;padding:14px}.tool-metric span{color:#6b7680;display:block;font-size:10px}.tool-metric strong{display:block;font-size:20px;line-height:1.3;margin-top:4px;overflow-wrap:anywhere}.tool-chart-panel{border-bottom:1px solid var(--tool-line);border-top:1px solid var(--tool-line);margin:0 0 22px;padding:18px 0}.tool-chart-panel canvas{display:block;height:220px;max-width:100%;width:100%}.tool-chart-title{display:flex;justify-content:space-between;margin-bottom:14px}.tool-chart-title strong{font-size:13px}.tool-chart-title span{color:var(--tool-muted);font-size:10px}.tool-split{display:grid;gap:30px;grid-template-columns:minmax(250px,0.8fr) minmax(0,1.4fr)}.tool-split h3{font-size:14px;margin:0 0 12px}.tool-findings{display:grid;gap:8px}.tool-finding{border-left:3px solid #6d7a85;color:#4c5762;font-size:12px;line-height:1.6;padding:7px 10px}.tool-finding strong{color:#27323c;display:block;font-size:12px}.tool-finding--danger{background:#fff4f2;border-left-color:#bf4d3e}.tool-finding--warn{background:#fff8e9;border-left-color:#bd7a1d}.tool-finding--ok{background:#eff8f4;border-left-color:#26785e}.tool-finding--info{background:#f0f6fa;border-left-color:#3777a7}.tool-table-wrap{overflow-x:auto}.tool-table{border-collapse:collapse;font-size:11px;margin:0;table-layout:auto;width:100%}.tool-table td,.tool-table th{border-bottom:1px solid #e4e8eb;padding:9px 8px;text-align:left;vertical-align:top}.tool-table th{color:#66727d;font-size:10px;white-space:nowrap}.tool-table td code{font-size:10px;overflow-wrap:anywhere}.tool-state-bars{display:grid;gap:9px;margin-bottom:24px}.tool-state-row{display:grid;font-size:10px;gap:8px;grid-template-columns:86px 1fr 34px}.tool-state-track{background:#edf0f2;height:8px;margin-top:3px;overflow:hidden}.tool-state-fill{background:#3978a8;height:100%;min-width:2px}.tool-state-row b{text-align:right}.tool-stack-groups{display:grid;gap:10px}.tool-stack-group{border-bottom:1px solid var(--tool-line);padding-bottom:10px}.tool-stack-group strong{font-size:12px}.tool-stack-group span{color:var(--tool-muted);float:right;font-size:10px}.tool-stack-group code{color:#4d5964;display:block;font-size:10px;line-height:1.5;margin-top:5px;overflow-wrap:anywhere}.tool-segments{background:#eef1f3;border-radius:4px;display:flex;padding:2px}.tool-segments button{background:0 0;border:0;border-radius:3px;color:#63707b;cursor:pointer;font-size:11px;height:30px;padding:0 12px}.tool-segments button.is-active{background:#fff;box-shadow:0 1px 3px rgba(20,30,40,.12);color:#23303b}.tool-cause-chain{display:flex;display:flex;flex-wrap:wrap;justify-content:center;gap:6px;margin-bottom:14px}.tool-cause-chain span{background:#f3f5f6;border:1px solid #dce2e7;border-radius:3px;font-family:monospace;font-size:10px;padding:5px 7px}.tool-output{background:#20272e;border-radius:5px;color:#e7ecef;font-size:11px;line-height:1.65;margin:0;max-height:620px;overflow:auto;padding:18px;white-space:pre-wrap;word-break:break-word}.tool-workspace[data-tool=json] .tool-output{background:#182027;border:1px solid #303c46;color:#f4f7f9;font-size:12px;tab-size:2}.tool-workspace[data-tool=json] .tool-output code{background:0 0;border:0;color:inherit;font-size:inherit;line-height:inherit;padding:0;text-shadow:none}.tool-inline-field{align-items:center;color:var(--tool-muted);display:inline-flex;font-size:11px;gap:7px}.tool-inline-field select{background:#fff;border:1px solid #cfd6dc;border-radius:4px;height:36px;padding:0 8px}.tool-utility-status{color:#28725e;font-size:12px;margin-bottom:12px}.tool-utility-status.is-error{color:#b33e34}.tool-metrics-compact{grid-template-columns:repeat(4,minmax(0,1fr))}.tool-config-section{border-top:1px solid var(--tool-line);margin-top:20px;padding-top:18px}.tool-config-section h3{font-size:13px;margin:0 0 13px}.tool-config-grid{display:grid;gap:14px;grid-template-columns:repeat(4,minmax(0,1fr))}.tool-llm-verdict{align-items:center;border:1px solid #cfd9d5;border-left:4px solid #27805f;border-radius:5px;display:grid;gap:3px 14px;grid-template-columns:auto 1fr;margin-bottom:18px;padding:13px 15px}.tool-llm-verdict span{background:#e8f3ee;border-radius:3px;color:#236852;font-size:10px;grid-row:1/3;padding:5px 7px}.tool-llm-verdict strong{font-size:16px}.tool-llm-verdict small{color:var(--tool-muted);font-size:10px}.tool-llm-verdict--warn{border-left-color:#b7791f}.tool-llm-verdict--warn span{background:#fbf2df;color:#8a5b13}.tool-llm-verdict--danger{border-left-color:#bc4c3c}.tool-llm-verdict--danger span{background:#faeae7;color:#973a30}.tool-llm-output{align-items:start}.tool-llm-output h3{font-size:13px;margin:0 0 10px}.tool-assumptions{border-top:1px solid var(--tool-line);margin-top:20px;padding-top:15px}.tool-assumptions strong{font-size:12px}.tool-assumptions p{color:var(--tool-muted);font-size:11px;line-height:1.65;margin:5px 0 0}.tool-reference-links{align-items:center;display:flex;display:flex;flex-wrap:wrap;justify-content:center;gap:8px 16px;margin-top:13px}.tool-reference-links span{color:var(--tool-muted);font-size:10px;font-weight:700}.tool-reference-links a{color:#2b617b;font-size:11px;text-decoration:none}.tool-memory-form{display:grid;gap:14px;grid-template-columns:repeat(4,minmax(0,1fr));margin-top:20px}.tool-unit-input{position:relative}.tool-unit-input input{padding-right:42px}.tool-unit-input em{color:#7a858e;font-size:9px;font-style:normal;position:absolute;right:9px;top:12px}.tool-budget{align-items:center;display:flex;gap:20px;margin-bottom:20px}.tool-budget-ring{align-items:center;border:8px solid #dfe7e4;border-radius:50%;display:flex;flex:0 0 94px;flex-direction:column;height:94px;justify-content:center}.tool-budget-ring strong{font-size:20px}.tool-budget-ring span{color:var(--tool-muted);font-size:9px}.tool-budget-summary strong{font-size:17px}.tool-budget-summary p{color:var(--tool-muted);font-size:12px;line-height:1.6;margin:5px 0 0}.tool-memory-bar{display:flex;height:18px;margin:0 0 20px;overflow:hidden}.tool-memory-segment{min-width:1px}.tool-memory-segment:first-child{background:#3978a8}.tool-memory-segment:nth-child(2){background:#27805f}.tool-memory-segment:nth-child(3){background:#b7791f}.tool-memory-segment:nth-child(4){background:#71569b}.tool-memory-segment:nth-child(5){background:#8d5964}.tool-memory-segment:nth-child(6){background:#63717d}.tool-memory-segment:nth-child(7){background:#dfe5e8}.tool-more{align-items:center;border-top:1px solid var(--tool-line);display:flex;display:flex;flex-wrap:wrap;justify-content:center;gap:8px 18px;margin-top:34px;padding-top:18px}.tool-more span{color:#79838d;font-size:10px;font-weight:700}.tool-more a{color:#3b586f;font-size:11px;text-decoration:none}@media (max-width:767px){.tool-directory,.tool-workspace{margin-left:16px;margin-right:16px;width:auto}.tool-directory-header h1,.tool-header h1{font-size:25px}.tool-grid{grid-template-columns:1fr}.tool-header{display:block}.tool-local-badge{display:inline-flex;margin-top:15px}.tool-panel{padding:16px}.tool-panel-heading{display:block}.tool-panel-heading .tool-button{margin-top:10px}.tool-form-row,.tool-split{grid-template-columns:1fr}.tool-config-grid,.tool-memory-form,.tool-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.tool-actions-between{align-items:flex-start;flex-direction:column}.tool-chart-title{display:block}.tool-chart-title span{display:block;margin-top:4px}}@media (max-width:420px){.tool-config-grid,.tool-memory-form{grid-template-columns:1fr}.tool-actions{align-items:flex-start;display:flex;flex-wrap:wrap;justify-content:center}.tool-card{padding:14px}.tool-group-heading{align-items:flex-start;display:block}.tool-group-heading span{display:block;margin-top:4px}}.affiliate-creative{margin:14px 0;padding:0 12px;position:relative}.affiliate-creative a{display:block;line-height:0;position:relative}.affiliate-creative img{border-radius:4px;display:block;height:auto;max-width:100%;width:100%}.affiliate-copy{color:#fff;left:8%;line-height:1.35;max-width:48%;position:absolute;text-align:left;text-shadow:0 1px 3px rgba(0,0,0,.8);top:50%;transform:translateY(-50%)}.affiliate-copy small,.affiliate-copy strong{display:block;letter-spacing:0}.affiliate-copy strong{font-size:14px}.affiliate-copy small{font-size:10px;margin-top:5px}.affiliate-creative--post-end{display:none;margin:22px auto;max-width:690px}@media (max-width:991px){.affiliate-creative--post-end{display:block}}.affiliate-disclosure{background:rgba(0,0,0,.62);border-radius:2px;bottom:4px;color:#fff;font-size:10px;line-height:1.4;padding:1px 4px;pointer-events:none;position:absolute;right:16px}.topic-hub{background:#f5f7f8;border:1px solid #e1e5e8;border-radius:4px;border-top:3px solid #222;margin-bottom:28px;padding:24px 28px 26px}.topic-hub-header{align-items:flex-end;display:flex;gap:24px;justify-content:space-between;margin-bottom:22px}.topic-hub-header h2{font-size:24px;line-height:1.35;margin:4px 0 6px}.topic-hub-header p{color:#666;font-size:14px;line-height:1.7;margin:0;max-width:620px}.topic-hub-eyebrow{color:#777;font-size:12px;font-weight:600}.topic-entry-link,.topic-hub-all{color:#333;flex:0 0 auto;font-size:13px;font-weight:600;text-decoration:none}.topic-hub-list{display:grid;gap:14px;grid-template-columns:repeat(3,minmax(0,1fr))}.topic-entry{background:#fff;border:1px solid #d9dee3;border-radius:4px;border-top:3px solid #5b6573;box-shadow:0 2px 8px rgba(30,40,50,.06);min-width:0;padding:18px}.topic-entry>p{color:#555;font-size:13px;line-height:1.7;margin:14px 0 16px}.topic-entry--java{border-top-color:#c74634}.topic-entry--java .topic-entry-icon{color:#c74634}.topic-entry--llm{border-top-color:#237a57}.topic-entry--llm .topic-entry-icon{color:#237a57}.topic-entry--agent{border-top-color:#326da8}.topic-entry--agent .topic-entry-icon{color:#326da8}.topic-entry-heading{align-items:flex-start;display:flex;gap:11px}.topic-entry-heading h2,.topic-entry-heading h3{font-size:16px;line-height:1.4;margin:0 0 5px}.topic-entry-heading a{color:#222;text-decoration:none}.topic-entry-heading span{color:#888;display:block;font-size:11px;line-height:1.5}.topic-entry-icon{flex:0 0 18px;font-size:17px;line-height:22px;text-align:center}.topic-hub--full{background:0 0;border:0;margin:0;padding:0}.topic-hub--full .topic-hub-header{border-bottom:1px solid #eee;margin-bottom:6px;padding-bottom:22px}.topic-hub--full .topic-hub-list{display:block}.topic-hub--full .topic-entry{border:0;border-bottom:1px solid #eee;border-radius:0;padding:30px 0;scroll-margin-top:24px}.topic-reading-list{list-style:none;margin:20px 0 0;padding:0}.topic-reading-list li{border-top:1px solid #eee;padding:12px 0}.topic-reading-list a{color:#222;display:block;font-size:14px;font-weight:600;line-height:1.55}.topic-reading-list span{color:#777;display:block;font-size:12px;line-height:1.55;margin-top:3px}.post-series{border-left:3px solid #237a57;margin:30px 0 8px;padding:4px 0 4px 16px}.post-series>span{color:#777;display:block;font-size:12px}.post-series>a{display:inline-block;font-size:16px;font-weight:600;margin-top:3px}.post-series p{color:#666;font-size:13px;line-height:1.65;margin:5px 0 0}@media (max-width:767px){.topic-hub{padding:20px 18px}.topic-hub-header{align-items:flex-start;display:block}.topic-hub-header h2{font-size:21px}.topic-hub-all{display:inline-block;margin-top:12px}.topic-hub-list{grid-template-columns:1fr}.topic-hub--full{padding:0}} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index e3e8558754..56096df3f1 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -3748,7 +3748,7 @@ https://nicksxs.me/ - 2026-10-12 + 2026-10-16 daily 1.0 diff --git a/tags/linked-list/index.html b/tags/linked-list/index.html index 25e10146ac..53be7051c3 100644 --- a/tags/linked-list/index.html +++ b/tags/linked-list/index.html @@ -1 +1 @@ -标签: Linked List | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

0%
\ No newline at end of file +标签: linked list | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

linked list 标签

2020
0%
6.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" class=octo-body fill=currentColor> \ No newline at end of file diff --git a/tags/mysql/index.html b/tags/mysql/index.html index a45119023a..3036aed087 100644 --- a/tags/mysql/index.html +++ b/tags/mysql/index.html @@ -1 +1 @@ -标签: Mysql | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

0%
pan>
0%
\ No newline at end of file +标签: MySQL | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

0%
\ No newline at end of file diff --git a/tags/php/index.html b/tags/php/index.html index e6d5da7702..ecfe490cae 100644 --- a/tags/php/index.html +++ b/tags/php/index.html @@ -1 +1 @@ -标签: PHP | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

0%
\ No newline at end of file +标签: PHP | Nicksxs's Blog

Nicksxs's Blog

Java 运行时、生产诊断与 AI 工程实践

0%
an>
0%
\ No newline at end of file