2026年3月

Function Calling 函数调用也叫 Tools 工具,它本质上就是在AI应用中自定义实现一些方法(函数),然后交给大模型,由大模型自行判断在合适的时机调用这些方法,从而实现原来大模型无法做到的一些扩展功能,比如操作本地数据库等等。其流程如下

在这里插入图片描述

  1. 当用户把问题发送给AI应用,在AI应用的内部需要组织提交给大模型的数据,而这些数据中需要描述清楚我们的AI应用中有哪些函数能够被大模型调用。每一个函数的描述都包含三个部分,方法名称、方法作用、方法入参
  2. 当AI应用把这些数据发送给大模型后,大模型会先根据用户的问题以及上下文拆解任务,从而判断是否需要调用函数,如果有函数需要调用,则把需要调用的函数的名称,以及调用时需要使用的参数准备好一并响应给AI应用
  3. AI应用接收到响应后需要执行对应的函数,得到对应的结果,接下来把得到的结果和之前信息一块组织好再发送给大模型

注意:

  • 大模型只做调用哪个函数的决策,实际调用函数的工作是由AI应用(比如langchain4j框架、cherry studio等)完成的
  • 由于在一次任务的处理过程中可能需要根据顺序调用多个函数,所以当大模型接收到AI应用发送的数据继续拆解任务,如果发现还需要调用其他的函数,则会重复4.1~4.4这几个步骤,直到无需调用函数,最终把生成的结果响应该AI应用,并由AI应用发送给用户

具体来说,Fuction Calling就是大模型提供商在模型内部与API层面做了支持的一种能力,它最早由 OpenAI 引入:

  • 在模型层面:模型提供商需对大模型进行特别优化,使其具备根据上下文正确选择合适函数、生成有效参数的能力(比如有监督微调、强化学习)
  • 在API层面:模型提供商需额外开放对Function Calling的支持,比如需要自定义标准来规定AI应用程序向大模型提供工具列表、大模型向AI应用程序响应需要调用哪个工具并携带调用参数、AI应用程序向大模型返回工具调用的结果时的具体消息格式

由此可见,Fuction calling是大模型本身的一种能力,需要大模型自身支持,并且各个大模型厂商实现的具体标准都不一样。实际开发时需要查找对应的支持情况:Comparison Table of all supported Language Models | LangChain4j

在这里插入图片描述

举一个具体的例子,用户提问天气,大模型利用Function Calling能力调用外部工具查询天气并返回结果

在这里插入图片描述

流程解释如下:

  1. 用户通过自然语言提出问题,例如:“广州今天天气如何?适合出门吗?”
  2. AI应用程序向大模型 API 传入用户原始输入、函数描述和其他上下文信息,获取调用指令。函数描述包括函数名称、用途说明、参数结构等。具体消息格式各个厂商会有区别,示例如下

    {
      "messages": [
        {
          "role": "system",
          "content": "你是一个助手,可以根据用户的请求调用工具来获取信息。"
        },
        {
          "role": "user",
          "content": "广州今天天气如何?适合出门吗?"
        }
      ],
      // 函数列表
      "functions": [
        {
          "name": "getWeather",
          "description": "获取指定城市的天气",
          "parameters": {
            "type": "object",
            "properties": {
              "location": {
                "type": "string",
                "description": "城市名称,比如北京"
              },
              "date": {
                "type": "string",
                "description": "日期,比如 2025-08-07"
              }
            },
            "required": ["location", "date"]
          }
        }
      ]
    }
  3. 模型会智能判断是否需要调用函数,选择合适的函数,并基于上下文自动生成结构化的调用指令(函数名 + 参数),例如:

    {
      "function_call": {
        "name": "getWeather",
        "arguments": {
          "location": "Guangzhou",
          "date": "2025-07-17"
        }
      }
    }
  4. AI应用程序接收到模型返回的调用指令后,解析调用指令,得到函数名称和参数,执行对应的方法(如调用天气查询函数),并获取结果
  5. AI应用将函数执行结果 + 其他上下文信息(包括用户原始输入)传给模型,模型判断此时已有足够的信息回答问题,不再需要调用函数了,于是直接生成最终结果,例如:“广州今天35度,暴雨,建议在室内活动”

注意:

  • 由于Fuction Calling能力依赖具体大模型厂商的实现,所以其缺陷是AI应用需要自己实现对各个厂商大模型的适配,开发量比较大
  • Fuction Calling也没有规定AI应用程序具体如何调用工具,所以需要每个AI应用自行实现对工具的调用
  • 没有Fuction Calling能力的大模型并非不能调用外部工具。我们可以自定义系统提示词,让大模型来调用外部工具,比如以下系统提示词

    # 你的角色
    你是一个函数调用助手,我将提供多个函数的定义信息,包括函数名称、作用、参数及参数类型。
    
    # 你的任务
    - 根据用户的输入,判断是否需要调用某个函数  
    - 如果需要,请**严格按照以下格式**输出函数调用指令:
    ```json
    { "name": "函数名", "arguments": { "参数名": "参数值" } }
    ```
    
    # 函数定义信息
    1. **get_weather**  
       - 作用:查询指定城市的天气情况  
       - 参数:  
         -`city`(string):城市名称
    2. **get_time**  
       - 作用:查询指定城市的当前时间  
       - 参数:  
         - `city`(string):城市名称

    这样当用户提问:广州的天气怎么样?,模型会根据系统提示词以指定格式返回要调用的工具及参数

    { "name": "get_weather", "arguments": { "city": "广州" } 

    只不过这种调用工具的实现方式有以下缺点:

    • 输出格式不稳定。如调用指令中存在多余自然语言
    • 容易出现幻觉。模型可能编造并不存在的函数名或参数
    • 对开发者依赖度高。函数描述、调用指令格式、提示词逻辑完全由开发者设计
    • 上下文冗长,Token 消耗大。为确保调用逻辑正确,往往需要在 system prompt 中加入大量说明与规则

作为新一代 iPhone 机型的入门版本,iPhone 17e 不仅补齐了前一代机型的若干基础体验,在价格方面也较此前表现出了意外的诚意:起售价格不变的基础上,存储容量加倍起步,让新一代的 iPhone 全线产品彻底告别 128GB 时代。要知道,同时更新的 iPad Air (M4) 机型,还依然保持着 128GB 的存储容量选项。

外观方面,iPhone 17e 与 iPhone 16e 相比没有任何变化,同样的尺寸与相近的重量、同样的单摄镜头模组,以及依然未配备灵动岛的屏幕。不同的是,超瓷晶面板 2 成了新一代 iPhone 的标配,同样能为 iPhone 17e 提供多达 3 倍的抗刮划能力,并且还有能够减少眩光的抗反射涂层。

最为吸引人的是,在原有黑色和白色之外,iPhone17e 新增了全新的浅粉色选项。在 iPhone 17e 的玻璃背板上,浅粉色带来的依旧是「素净」的视觉效果:粉色在背板中若隐若现,并会随光线变化呈现不同的视觉色彩效果。为了更好地衬托全新的浅粉色,Apple 也推出了同色的 MagSafe 硅胶保护壳和斜挎挂绳,让 iPhone 17e 能与配件搭配相得益彰。

令人略感失望却又不出所料的是,iPhone 17e 的屏幕继续保持了「刘海」设计,没有支持许多人期待的灵动岛功能。

不过,在更大的起始容量、更耐用的屏幕面板、终于支持 MagSafe 等升级的基础上,iPhone 17e 与 iPhone 17 的差距已经有了明显拉近;从二者在系列产品中的定位角度考虑,不得不承认这项基础功能的缺失多少也在情理之中。对了,iPhone 17e 配备的也依然是一款刷新率为 60Hz 的 Liquid 视网膜显示屏。

如你所见,iPhone 17e 终于支持了 MagSafe 磁吸和充电能力,无线充电速度也终于来到相对正常的 15W 水平。

作为 Apple 拥有的专利级功能,MagSafe 绝对算得上如今 iPhone 生态体验中的基础并不可或缺的能力,不论是各类卡包、各种无线充电设备,或是各式功能各异的摄影配件,MagSafe 都为 iPhone 的日常使用赋予了更多可能性。iPhone 17e 补上了这一标准能力,可以说是众望所归。

有一点可能需要注意的是,实际使用下来,iPhone 17e 的 MagSafe 磁力吸附效果会相对明显地小于其它 iPhone 机型,不管是将手机吸附到配件上、还是将手机取下来的过程都能感觉到。当然了, 这样的区别我认为对于日常使用而言不会造成太明显的差别,不论是 MagSafe 充电器还是卡包等配件都能很稳固地吸附在手机上。

影像方面,iPhone 17e 的硬件规格基本与前代产品持平,配备的是一颗具有 4800 万像素的融合式镜头,提供 1 倍和 2 倍光学变焦镜头,依然是唯一不支持相机控制的新款 iPhone。

iPhone 17e 与 iPhone 16e 在影像能力上最大的不同,就是前者终于支持了早在 iPhone 15 系列就已经推出的新一代人像功能。

这项功能可以让我们在默认的照片模式下自动识别前景人物或者宠物,提供景深控制按钮;即使你在拍照时选择不开启景深,也可以在拍完照后重新在照片 app 中开启人像效果以及调整景深程度,而不需要特地切换到人像模式选项之后再拍摄。尤其是在拍宠物的时候,很多画面真的只在那一瞬间,新一代人像功能的加入无疑能在拍照时更加从容。

一如 Apple 对于入门产品常有的处理方式,iPhone 17e 继续使用了同系列标准款机型 iPhone 17 的同款 A19 芯片,并在 GPU 方面有一个核心的阉割。对于用户而言,这样做并不会对绝大多数日常应用、影像拍摄甚至是游戏体验产生很大的影响,在 iPhone 17 上能够轻松完成的工作,iPhone 17e 同样能够很好地胜任。

而在续航方面,iPhone 17e 的续航能力依旧延续了上一代机型的出色表现。通过第三方工具查看 iPhone 17e 的新机电池电量以及设计电量也可知,这台全新机型在电池方面与上一代产品完全没有任何变化。从实际使用来看,iPhone 17e 也如预期一般没有续航能力上的明显变化,但依然是一台表现出色的小尺寸机型。

最后再说一下 eSIM。也许是因为更厚的机身在内部有更多空间,作为在大陆地区继 iPhone Air 之后第二台支持 eSIM 的 iPhone 机型,iPhone 17e 更进一步,甚至配备了实体 nano-SIM 卡槽。

与香港地区的双卡 iPhone 机型类似,iPhone 17e 支持在插入一张实体 SIM 卡的同时,再向设备写入并激活一张 eSIM 号卡;也可以选择不使用机身上的实体 SIM 卡槽,同时向设备写入并激活两张 eSIM 号卡。

你可能会好奇,现在有了 iPhone 17e 之后,Apple 在支持文档中列出的 eSIM 快速转移功能,是否已经可以在大陆机型中使用。在将 iPhone 17e 与 iPhone Air 升级至完稿前的最新正式版系统 iOS 26.3.1 后,实际测试下来 iPhone 依然还不支持这一能力。不过,考虑到目前产品还未正式发售,或许 Apple 会在 iPhone 17e 上市后通过系统更新推送这一能力也不一定,让我们拭目以待。

虽然有了 iPhone Air 的「前车之鉴」后,我们都知道目前在大陆地区办理 eSIM 存在区域限制严格、写卡渠道单一、办理政策不一等问题,但依然很高兴看到 iPhone 17e 能够再次取得一点小小的「进步」,向用户提供更灵活的选择。另外可以大胆猜测的是,将于今年下半年发布的 iPhone 18 系列机型,不出意外也会采用类似的实体 + eSIM 组合方案。

从 MagSafe 到新一代人像功能,这些 iOS 生态下基础体验的补足,都让这款产品承载的 iPhone 入门体验变得更为均衡和完整;虽然我们依然会继续期待它能做到更多,比如灵动岛的缺席就让不少人感到遗憾。不要忽视的是,起始容量从 128GB 升级至 256GB,这份「加量不加价」也是让 iPhone 17e 成为值得认真考虑选择的重要因素。

从这些结果来看,我认为今年的 iPhone 17e 算是交出了一份合格的答卷。

    不同于 iPhone,iPad 可以说是市面上综合性能和体验几乎没有对手的存在,也正因如此,即使是已经维持「例行升级」好几代的 iPad Air,在升级到了 M4 芯片的版本之后,依然也有可圈之处,并且是值得大多数人选择的那一款 iPad。

    自从 2020 年第四代产品面世以来,iPad Air 系列的这套模具至今已经有 6 年之久:经典的直角边框设计,也是目前整个 iPad 家族使用的设计语言。也就是说,搭载 M4 芯片的这台 iPad Air,与上一代产品在外形方面没有任何区别,配件也完全通用。

    为了更好地配合连接键盘之后的横屏使用形态,iPad Air 从 2022 年推出的第五代产品开始,将前置摄像头移动到了横向边框的位置;但是比较遗憾的是,经过了 4 代产品的 3 次迭代,最新款的 iPad Air 依然不支持在 iPhone 上已成基础能力的 Face ID 解锁方式,依然必须通过 Touch ID 指纹进行解锁、安全认证及支付等操作。

    配件方面,适配 iPad Air 的妙控键盘也没有任何更新,继续采用 PU 聚氨酯材料,触控板为机械式的按压结构,不像 iPad Pro 的妙控键盘已在此前升级为手感更好的铝金属掌托及通过 Taptic Engine 提供模拟触觉反馈的触控板。

    iPad Air 妙控键盘配备了机械式触控板。

    Apple 产品的「Air」系列一直给人以更加轻薄和便携的印象,MacBook Air 如此,iPhone Air 更是如此。但是在 iPad 产品上,实际情况却有那么一点反直觉。从 iPad Pro (M4) 引入双层串联 OLED 屏幕开始,Apple 也对其内部散热材料和组件结构重新设计,让 iPad Pro 这一最为专业且强大的产品线变得前所未有的轻薄,成为了 Apple 历史上最薄的产品。

    以我手上这台 11 英寸的 iPad Air 为例,与同尺寸的 iPad Pro 相比不仅厚度多出了 0.8 毫米,重量更是多出了几乎有 20g,这一数据在 13 英寸机型上的差距还要更大。

    不过,这并不是在说 iPad Air 不够便携,事实上,iPad 产品线中的任何一款产品,在我看来都已经是足够便携的存在,尤其是 11 英寸的机型,配合键盘能够完成一些轻量的桌面级工作,单手握持又能轻松地实现移动阅读或绘画等需求。

    从搭载 Apple silicon 芯片的 iPad 出现开始,这部分的 iPad 机型对于大部分人而言,起码在性能方面绝对是过剩的,对于从 M3 芯片升级到 M4 芯片的这台 iPad Air 也是一样。在大部分的日常场景下,比如记笔记、看视频、处理文档等等,你不会感觉到它们之间有任何区别。

    从跑分数据来看,iPad Air 的这颗 M4 芯片 CPU 单核成绩为 3734、多核成绩为 13036,对比高配版本的 iPad Pro (M4) 机型,由于有 2 个核心数的差别,因此在测试数据上有约 10%-12% 差距。这样的成绩更进一步证明了,对于日常使用体验而言,iPad Air 的性能不仅完全足够,更是能在大多数场景中与定位更加专业的 Pro 机型相提并论。

    而在实际的高负载场景表现中,iPad Air (M4) 也充分证明了自己的实力。以《使命召唤》为例,在将画质与帧率选项都设置到最高档后,游戏的整体帧率表现都基本稳定在了接近 60 帧的水平、平均表现约为 55 帧;以实际游玩体验来说,画面流畅性与操作跟手性也在大多数时间时间内表现稳定,游戏的性能表现也没有因长时间运行发热出现明显的降频或大幅掉帧等现象。

    或许是为了提供更好的 AI 应用性能,也或者是为了进一步提升 (已上线地区的) Apple Intelligence 的体验,这一代 iPad Air 也将内存大小提升到了 12GB,并配备 120GB/s 的内存带宽,前者更是首次超越了同芯片的 iPad Pro 机型。要知道,配备 M4 芯片的 iPad Pro 基础机型,内存都只有 8GB 大小,仅有 1TB 和 2TB 存储容量的机型才升级到了 16GB 内存。

    内存和内存带宽的升级带来的体验也能够较为直接地呈现在实际体验中。在标签页多开、多应用分屏等场景下,频繁地在不同应用间切换变得更不容易重载内容了。对于有更高需求的专业用户来说,更高的内存带宽也能在 Procreate、Photoshop 等复杂多图层场景下带来更快的实时响应,让缩放、拖动和编辑操作都更加顺滑。

    与同样具有桌面级性能和体验的 iPad Pro 相比,iPad Air 近年的升级看起来确实有点乏善可陈,毕竟前者在例行升级芯片、内存等性能配置之余,还带来了更轻薄的机身、更出色的屏幕。

    不过,从实际选择的角度来说,如今的 iPad Air 已经是适合大多数用户的那个「最好」的选择。不论是从价格、还是从已有的功能与体验来看,它都足以满足日常娱乐、学习、办公甚至是专业创作的各种需求。尤其是在今年升级内存和内存带宽之后,至少在性能层面,iPad Air 已经足以配得上「Pro」之名。

    这或许也是 iPad Air 有意思的地方:它没有 iPad Pro 那样极致的规格和功能,却在用越来越全面、越来越成熟的实际体验,成为那台更容易被更多人带回家的 iPad。而在大多数人用不上的性能之外,iPad Pro 也依然可以用更好的屏幕、更出色的便携性,成为那个更好的答案,重要的是你想怎么选。

      为什么要做可观测?—— 解决“不可见”带来的业务风险

      在 Taro 开发的支付宝小程序中,可观测性(Observability)的本质是通过数据化、实时化的监控手段,让小程序的运行状态、用户行为及系统交互“可见、可分析、可追溯”。其必要性源于小程序在实际运行中面临的三大核心挑战:

      1、“黑盒化”运行环境:问题难以感知,故障发现滞后

      小程序部署在支付宝客户端及云端环境中,开发者无法直接接触用户设备的真实运行状态。当出现以下问题时,若无可观测能力,往往只能依赖用户反馈或事后投诉。

      典型案例:某电商小程序上线后,用户投诉“提交订单按钮点击无效”,但开发团队通过日志仅能看到“按钮渲染成功”,因缺乏用户操作路径和接口调用的关联数据,耗时 3 天才定位到是“异步数据未加载完成时按钮未禁用”的逻辑缺陷。若提前部署可观测能力,可通过用户行为流+接口状态实时发现该问题。

      2、业务依赖复杂:跨端、跨服务的故障定位成本高

      Taro 小程序通常依赖多方服务:前端通过 Taro 框架调用支付宝小程序 API(如支付、登录)、与后端服务交互(如订单查询、用户数据同步),甚至涉及第三方服务(如地图 SDK、营销活动平台)。任何一个环节异常都可能导致整体功能失效,但问题根源可能隐藏在任意一层,若无统一的观测数据(如接口调用链、用户操作路径、错误上下文),排查效率极低,可能导致用户流失或业务损失。

      3、用户体验敏感:微小问题可能引发大规模负面反馈

      支付宝小程序的用户对体验的容忍度极低——页面加载慢 1 秒可能导致 5% 的用户流失,支付流程卡顿可能直接引发投诉。但用户通常不会详细描述问题(如“首屏渲染慢是因为图片未压缩”),而是给出模糊反馈(“用不了”“太卡了”)。若无可观测能力,开发者只能通过“猜测+灰度测试”被动优化,无法精准识别影响用户体验的关键节点。

      这些问题的解决依赖于对用户行为、性能指标的实时监控,而非事后统计。

      把观测云作为 Taro 支付宝小程序可观测最佳实践的核心工具--落地路径与关键动作

      • 将观测云定位为小程序的“统一可观测平台”,以统一标签(如 service、env、version)贯穿指标、日志、链路、用户访问四类数据,形成端到端的观测视图。
      • 在 Taro 多端(含支付宝小程序)场景中,前端接入 RUM(用户访问监测),后端与基础设施接入 Logging/Tracing/Metrics,实现跨端、跨服务的统一观测与关联。
      • 通过“Dashboard + Explorer”构建业务与技术双视角的可观测体系,既看得到用户体验,也定位得到根。

      前端 RUM 接入(Taro 支付宝小程序)

      本最佳实践实验版本为 Taro v4.1.7

      查看编译环境
      ➜  ~ taro --version 
      ? Taro v4.1.7
      4.1.7
      ➜  ~ 

      1、观测云后台->用户访问监测->新建应用,选择应用类型为小程序,输入应用名称、应用ID,系统会自动根据配置信息生成引入 SDK 的代码,可选择 NPM 接入或 CDN 文件接入(这里我们选择 CDN 文件方式接入)。

      • 应用名称:用于识别当前用户访问监测的应用名称。
      • 应用 ID :应用在当前工作空间唯一标识,对应字段:app_id 。

      图片

      2、根据链接下载 CDN 文件,打开项目代码,在小程序项目中的 app.ts 入口文件引入观测云后台生成的配置代码。

      注意项目中引入代码的位置,必须要在 App() 初始化之前,否则会出现数据无法正常采集上报,或者只能上报一小部分数据的情况。

      图片

      3、本地运行项目或者打包发布测试后,通过观察接口调用发现 /v1/write/rum 接口成功调用,说明数据成功上报,可在对应应用的查看器查看上报数据。

      图片

      4、js 会自动生成一个 trace_id,如果在 allowedTracingOrigins 配置了正则(本最佳实践中配置的是 *,意味着所有接口都会带 ),会在对应的请求头中加入一些参数,ddtrace 的是这几个参数。

      图片

      实现效果

      1、小程序部署在支付宝客户端及云端环境中,开发者直接接触用户设备的真实运行状态
      进入「用户访问监测」页面—> 选择创建的对应微信小程序—> 分析看板,可以查看小程序运行中的部分重点信息。

      图片

      进入「用户访问监测」页面—> 选择创建的对应微信小程序—> 查看器,可以查看小程序应用中用户访问的动作,请求资源以及项目中的报错信息等。

      图片

      2、多方服务全链路打通

      图片

      3、后端服务透明化,可以观测到更详细的代码级方法,开发可以快速定位

      图片

      图片

      4、用户体验可视化,全局可观测

      图片

      图片

      图片

      观测云为 Taro 支付宝小程序创建一份“数字神经系统”

      在 Taro 开发的小程序中,可观测性不是“可选能力”,而是保障业务健康增长的必备基础设施。它通过将“不可见的运行状态”转化为“可见的数据洞察”,帮助开发者:

      • 从被动救火到主动预防(快速发现问题,降低故障损失);
      • 从经验驱动到数据驱动(精准优化体验,提升转化效率);
      • 从局部优化到全局可控(支撑规模化增长,保障业务稳定性);
      • 从单点协作到团队协同(提升研发效率,加速产品迭代)。

      刚入职不到 3 周的哥哥 在学习部署项目 交给了他之前的部署流程文档

      可惜原先部署文档没有给 nginx 的配置 头也是真放心 把其中一个客户的线上生产环境带 root 的直接送给了他 让他自己模仿

      项目的 SQL 也是考虑周全 每个 DDL 前必有 DROP 老哥也是犯了我之前的毛病...(看错服务器是哪个了)
      结果中午客户在企业微信里就傻眼了 我上次只是 DROP 了一张表 现在是清库了

      突然想起来《反基督者》里面的一句话 Chaos Reigns.

      最近在玩 OpenClaw ,走 Claude Code 容易封号,就试了试接 Codex 。折腾一圈发现一个很适合的组合:让 OpenClaw 走 ChatGPT Plus 自带 Codex 额度,5 小时窗口刷一次,OpenAI 没公布具体是多少条,但高强度用下来比 Claude Code 耐用,不会动不动就断粮。
      关键是 OpenClaw 走的是本地消息为主,消耗比云端任务轻得多,刚好跟这个窗口机制配套,一天能持续跑,不用盯着余额。其他模式不管是接中转站 Claude 还是 Kimi 、MiniMax ,本质都是按量烧,用越多花越多,跟这个没法比,可以放胆烧 token ,不用担心 API 账单爆炸。

      而且体验绝对是第一梯队,比 kimi 和 minimax 强多了,尤其是控制浏览器的任务。GPT-5.4 在 OSWorld-Verified 上拿了 75%,这个 benchmark 测的是模型通过截图加键鼠操作导航真实桌面环境的能力,人类基准线是 72.4%,GPT-5.4 已经超过了。对比一下,此前这个榜上 Kimi K2.5 是 63.3%。
      所以如果你的 OpenClaw 里有控制浏览器、操作桌面这类任务,GPT-5.4 目前确实是最强的那个,Kimi 和 MiniMax 在这块跟它不在一个档次。


      接入步骤
      先确认版本够新,openai-codex 的支持近几个月还在迭代,旧版本会直接报 provider 错误:
      openclaw --version
      发起 OAuth 登录,会弹浏览器,用 ChatGPT 账号授权就行:
      openclaw models auth login --provider openai-codex
      手动切换模型,这步很多人漏掉,登录成功不等于模型自动切过去:
      openclaw --profile default models set openai-codex/gpt-5.4
      重启 gateway 让配置生效:
      openclaw --profile default gateway restart
      验通路:
      openclaw --profile default agent --agent main --message "ping"


      几个坑
      旧版 OpenClaw 报 "No provider plugins found" 的,升级版本就好。
      登录成功后一定要手动 models set ,默认值不会自动跟过来,这个坑踩的人挺多。
      headless 或远程服务器环境浏览器回调会失败,考虑 device code flow 。


      我自己的 ChatGPT Plus 是在 bewild.ai 订的,支持按月按年订阅,有需要可以去看看。

      PHP Standard Library (PSL) 5.0 正式发布。作为 PHP 社区中专注于类型安全和异步编程的标准库,这次更新在架构上进行了大规模重构,引入了包括加密、二进制处理、网络栈重写在内的多个组件。

      image.png
      由于 PSL 5.0 明确要求 PHP 8.4+ 版本,开发者在本地调试时可能会遇到环境限制。如果需要快速搭建 PHP 8.4 环境,可以使用 ServBay。

      ServBay 支持多个 PHP 版本同时运行,能够一键安装 PHP 环境,并且能随时切换,方便在不影响现有项目的前提下测试 PSL 5.0 的新特性。

      image.png

      强类型数据校验

      PSL 的类型组件不依赖反射,而是通过组合子的方式验证数据。这在处理不可信的外部输入时,能够确保数据符合预期的结构。

      use Psl\Type;
      
      // 定义一套用户信息校验规则
      $schema = Type\shape([
          'id'     => Type\positive_int(),
          'email'  => Type\non_empty_string(),
          'active' => Type\bool(),
          'meta'   => Type\optional(Type\dict(Type\string(), Type\mixed())),
      ]);
      
      // 校验并获得类型完备的数据
      $validatedData = $schema->coerce($inputPayload);

      结构化并发模型

      PSL 5.0 继续深化基于 Fiber 的并发模型。开发者可以像编写同步代码一样处理异步任务,避开了传统回调或 Promise 嵌套带来的复杂性。

      use Psl\Async;
      use Psl\TCP;
      use Psl\IO;
      
      Async\main(static function(): int {
          // 并发执行多个网络请求
          [$clientA, $clientB] = Async\concurrently([
              static fn() => TCP\connect('service-a.internal', 8000),
              static fn() => TCP\connect('service-b.internal', 9000),
          ]);
      
          IO\write_error_line('所有连接均已建立成功');
      
          return 0;
      });

      函数式集合操作

      针对 PHP 原生数组在索引和关联类型上的模糊定义,PSL 提供了 Vec(列表)和 Dict(字典)组件。这些组件通过纯函数处理数据,返回类型更加明确。

      use Psl\Vec;
      use Psl\Dict;
      use Psl\Str;
      
      $users = ['nick', 'john', 'alice'];
      
      // 统一转为大写
      $upperNames = Vec\map($users, Str\uppercase(...));
      
      // 过滤掉长度不足的名称
      $filtered = Vec\filter($users, fn($u) => Str\length($u) >= 4);
      
      // 构建键值对映射
      $mapping = Dict\pull($users, fn($u) => Str\reverse($u), fn($u) => $u);

      生产级网络原语

      PSL 5.0 重写了底层的网络栈。无论是 TCP、UDP 还是 Unix Socket,所有的网络操作都支持异步非阻塞模式,并且提供了更加安全的 TLS 支持。

      use Psl\Async;
      use Psl\TCP;
      use Psl\IO;
      
      Async\main(static function(): int {
          $socket = TCP\listen('0.0.0.0', 9001);
          IO\write_error_line('服务器已在 9001 端口启动');
      
          while ($connection = $socket->accept()) {
              Async\run(static function() use ($connection) {
                  $connection->writeAll("Welcome to PSL Server\n");
                  $connection->close();
              })->ignore();
          }
      });

      全功能工业级加密库

      新版本引入了基于 libsodium 的加密组件,涵盖了对称与非对称加密、数字签名以及密钥派生等功能。这些 API 的设计遵循了“难以误用”的原则。

      use Psl\Crypto\Symmetric;
      
      // 快速生成密钥并进行数据加密
      $key = Symmetric\generate_key();
      $secretMessage = Symmetric\seal('需要保护的原始数据', $key);
      
      // 解密还原数据
      $original = Symmetric\open($secretMessage, $key);

      PSL 5.0 的发布为 PHP 开发者提供了一套更严谨、更具现代感的底层工具链。开发者可以低成本地将这些新技术应用到实际的研发工作中。

      一、滚动条的底层密码

      在HarmonyOS的UI世界中,列表滚动条就像图书馆的索引卡片——它不直接参与内容展示,却掌控着用户与海量数据的交互命脉。咱们呢要理解其中的工作原理,要从三个核心组件说起:

      1. 滚动容器(List/Scroll):负责内容布局与滚动计算
      2. 滚动条组件(ScrollBar):视觉呈现与用户交互入口
      3. 状态控制器(Scroller):记录滚动位置与速度

      当用户手指划过屏幕时,系统会触发onScroll事件,Scroller记录当前偏移量并更新滚动条位置。这个过程如同钢琴师的手指在琴键上跳跃,每个动作都被精准转化为数字信号。

      graph TD
          A[用户触摸屏幕] --> B{判断滑动方向}
          B -->|垂直滑动| C[更新Y轴偏移量]
          B -->|水平滑动| D[更新X轴偏移量]
          C --> E[计算滚动条缩放比例]
          D --> F[计算滚动条缩放比例]
          E --> G[重绘滚动条位置]
          F --> G

      二、举些个栗子

      2.1 基础滚动条控制(鸿蒙5)

      在鸿蒙5时代,滚动条控制更像手动挡驾驶,需要精准操作每个参数:

      // 电商评论列表案例
      List({ space: 15, scroller: this.commentScroller }) {
        ForEach(this.comments, (comment) => {
          CommentItem(comment)
        })
      }
      .scrollBar(BarState.On) // 显示滚动条
      .divider({ 
        strokeWidth: 1, 
        color: Color.Gray,
        startMargin: 12,
        endMargin: 12 
      }) // 分割线样式
      .edgeEffect(EdgeEffect.Spring) // 边缘回弹效果

      关键参数小说明:

      • scrollBar:控制滚动条显示策略(On/Off/Auto)
      • divider:分割线配置(鸿蒙5不支持动态间距)
      • edgeEffect:滚动到边缘时的物理反馈效果

      2.2 鸿蒙6新特性实践

      鸿蒙6带来的ScrollBar独立组件,让滚动条控制进入自动挡时代:

      // 带粘性标题的聊天列表
      Column() {
        // 消息列表
        List({ scroller: this.chatScroller }) {
          ForEach(this.messages, (msg) => {
            MessageItem(msg)
          })
        }
        .scrollBar(BarState.Off) // 关闭默认滚动条
        
        // 自定义滚动条
        ScrollBar({
          scroller: this.chatScroller,
          width: 8,
          color: Color.Blue,
          thumbColor: Color.DarkBlue
        })
        .position({ x: '90%', y: 0 })
      }
      
      // 鸿蒙6新增的智能吸附功能
      List().sticky({
        offset: 40,
        onSticky: (isSticky) => {
          console.log('标题开始吸顶:', isSticky)
        }
      })

      三、版本适配小策略

      3.1 鸿蒙5兼容方案

      针对需要同时支持新旧系统的场景,可采用条件编译:

      // 版本特性检测
      const scrollConfig = isHarmonyOS6() 
        ? { 
            scrollBar: BarState.Auto,
            dynamicScrollIndicator: true 
          } 
        : { 
            scrollBar: BarState.On,
            showScrollIndicator: true 
          }
      
      List(scrollConfig) {
        // 列表内容
      }

      3.2 渐进式迁移指南

      1. 样式迁移:将固定尺寸替换为dp单位
      2. 事件迁移@scroll改为onScroll回调
      3. 动画迁移animateTo替代旧版动画API

      四、性能优化一波

      4.1 虚拟滚动技术

      在长列表场景下,启用虚拟滚动可提升300%性能:

      List({
        space: 10,
        virtualScroll: true, // 开启虚拟滚动
        itemHeight: 80,      // 预估项高度
        buffer: 5            // 预加载缓冲区
      }) {
        ForEach(this.largeData, (item) => {
          ListItem(item)
        })
      }

      4.2 滚动卡顿排查流程

      当遇到滚动不流畅时,可按以下步骤诊断:

      1. 检查是否开启debugPerformance模式
      2. 分析onScrollEnd回调耗时
      3. 使用LayoutInspector查看渲染层级
      4. 排查嵌套滚动冲突

      五、记得避坑哦

      5.1 常见的大陷阱

      • 滚动劫持:子组件意外拦截滚动事件
      • 尺寸坍缩:未设置minHeight导致内容溢出
      • 内存泄漏:未正确销毁滚动监听器

      5.2 调试三板斧

      1. 性能监控@ohos.performance API
      2. 布局边界showLayoutBoundary可视化
      3. 内存快照:DevEco Studio内存分析工具

      总结一下下:滚动的哲学

      控制滚动条不仅是技术实现,更是咱们用户体验的平衡艺术。记住三个黄金法则:

      1. 可见性优先:复杂界面中优先显示进度指示器
      2. 响应要即时:滚动反馈延迟不超过100ms
      3. 性能至上:虚拟滚动是长列表的标配

      下次当你在深夜调试滚动效果时,不妨想象自己是个交响乐指挥——每个滚动事件都是精心编排的音符,而滚动条,正是引导用户穿越数据海洋的隐形航标。试试吧~

      把 AI agent 的逻辑拆分到多个独立运行的服务中,听起来复杂做起来也确实容易乱。LangGraph 的 RemoteGraph 特性算是一个干净的方案:本地编排器负责流程控制,远程图服务器承担具体计算,状态管理和控制流的职责边界清晰。

      本文要构建的项目是一个循环数学引擎:本地图编排一个远程图:随机选择数学运算和生成随机数。编排器会以两种方式实现——顺序执行和并行执行——以便对比两者的取舍,方便根据场景选择合适的模式。循环持续运行,直到远程图返回

      end

      架构概览

      一个 math_service 远程图负责两种操作,本地 math_orchestrator 在每次迭代中调用它两次,每种操作各一次。下面分别是顺序版本和并行版本的编排器结构:

      顺序流——远程图被依次调用两次:

      并行流——远程图用 fan-out/fan-in 模式同时被调用两次:

      math_service 是远程图,接受

      action

      字段:

      "pick_operation"

      返回一个随机数学运算或

      end

      "generate_number"

      返回一个随机整数。

      math_orchestrator 是本地图,接受初始数字后每次迭代调用远程图两次(分别传入不同 action),执行数学运算,operation 为

      end

      时终止。

      环境准备

      uv/pyproject.toml 配置如下:

       [project]  
      name = "langgraph-random-math"  
      version = "0.1.0"  
      description = "Add your description here"  
      requires-python = "==3.13"  
      dependencies = [  
          "langgraph",  
          "langgraph-cli",  
          "langgraph-sdk"  
       ]

      截至本文编写时 pydantic 与 Python 3.14 及更高版本不兼容,所以这里用 3.13。

      两个图都在本地运行——远程图跑在 LangGraph 开发服务器上,本地图作为普通 Python 脚本执行。

      步骤 1:math_service——远程图

      远程图用条件路由在单个图中处理两种操作。传入状态的

      action

      字段决定路由方向:

      pick_operation

      generate_number

      创建远程服务目录结构:

       math_service/  
       ├── auth.py  
       ├── graph.py  
       ├── langgraph.json  
       └── .env

      math_service/graph.py

       import random  
       from typing import TypedDict  
       from langgraph.graph import StateGraph, START, END
       class MathServiceState(TypedDict):  
           action: str                # "pick_operation" or "generate_number"  
           operation: str             # 结果: "add", "subtract", "multiply", "divide", 或 "end"  
           number: int                # 结果: 随机整数  
           manual_input_chance: float # 请求用户输入的概率 (0.0-1.0)  
           ask_user: bool             # 结果: True = 编排器应提示用户  
       def route_action(state: MathServiceState) -> str:  
           """根据 action 字段路由到相应的节点。"""  
           if state["action"] == "pick_operation":  
               return "pick_operation"  
           elif state["action"] == "generate_number":  
               return "generate_number"  
           else:  
               raise ValueError(f"Unknown action: {state['action']}")  
       def pick_operation(state: MathServiceState) -> dict:  
           """随机选择一个数学运算或 'end' 来停止循环。"""  
           operations = ["add", "subtract", "multiply", "divide", "end"]  
           # 'end' 有 10% 的概率;剩余 90% 在数学运算之间平均分配  
           weights = [9, 9, 9, 9, 4]  
           chosen = random.choices(operations, weights=weights, k=1)[0]  
           return {"operation": chosen}  
       def generate_number(state: MathServiceState) -> dict:  
          """  
          生成随机数或请求手动用户输入。  
      
          根据 manual_input_chance 进行掷骰:如果结果低于阈值,  
          返回 ask_user=True(不生成数字——编排器应提示用户)。  
          否则,生成并返回一个随机整数。  
          """  
          chance = state.get("manual_input_chance", 0.0)  
      
          if chance > 0.0 and random.random() < chance:  
              return {"ask_user": True}  
      
           return {"number": random.randint(1, 20), "ask_user": False}  
       builder = StateGraph(MathServiceState)  
      builder.add_node("pick_operation", pick_operation)  
      builder.add_node("generate_number", generate_number)  
      
      # 基于 action 字段从 START 进行条件路由  
      builder.add_conditional_edges(  
          START,  
          route_action,  
          {  
              "pick_operation": "pick_operation",  
              "generate_number": "generate_number",  
          },  
      )  
      
      builder.add_edge("pick_operation", END)  
      builder.add_edge("generate_number", END)  
      
       graph = builder.compile()

      math_service/auth.py

       import os  
      from langgraph_sdk import Auth  
      
      auth = Auth()  
      
      # 在生产环境中,请使用正规的 JWT 验证库。  
      # 此示例使用简单的 token 查找以保持清晰。  
      VALID_TOKENS = {  
          os.environ.get("MATH_SERVICE_TOKEN", "dev-token"): {  
              "id": "orchestrator",  
              "name": "Math Orchestrator",  
          },  
       }  
       @auth.authenticate  
      async def authenticate(authorization: str | None) -> Auth.types.MinimalUserDict:  
          """验证 Authorization 头中的 Bearer token。"""  
          if not authorization:  
              raise Auth.exceptions.HTTPException(  
                  status_code=401, detail="Missing authorization header"  
              )  
      
          try:  
              scheme, token = authorization.split(" ", 1)  
          except ValueError:  
              raise Auth.exceptions.HTTPException(  
                  status_code=401, detail="Invalid authorization format"  
              )  
      
          if scheme.lower() != "bearer" or token not in VALID_TOKENS:  
              raise Auth.exceptions.HTTPException(  
                  status_code=401, detail="Invalid token"  
              )  
      
          user = VALID_TOKENS[token]  
          return {  
              "identity": user["id"],  
              "is_authenticated": True,  
           }  
       @auth.on  
      async def authorize_all(ctx: Auth.types.AuthContext, value: dict):  
          """允许已认证用户执行所有操作。  
      
          在更复杂的设置中,你可以通过检查 value 载荷来限制  
          每个调用者可以调用哪些操作(pick_operation vs generate_number)。  
          """  
           return None  # None = 允许,不添加额外过滤

      这是一个最小认证层:检查 Bearer token 有效性,对已认证的调用者放行所有操作。生产环境中应当用 JWT 验证(

      PyJWT

      、Auth0 等)替代 token 查表,并按需增加操作级别的授权。

      generate_number

      节点内部完成决策——根据

      manual_input_chance

      掷骰后,要么生成数字,要么置

      ask_user=True

      。编排器检查这个标志并在需要时于本地提示用户。决策逻辑留在服务端,用户交互留在客户端,正是微服务中典型的职责划分方式。

      math_service/langgraph.json:

       {  
        "dependencies": ["."],  
        "graphs": {  
          "math_service": "./graph.py:graph"  
        },  
        "auth": {  
          "path": "./auth.py:auth"  
        },  
        "env": ".env"  
       }

      创建

      .env

      文件写入服务 token:

       MATH_SERVICE_TOKEN=dev-token

      在端口 2024 启动服务器:

       cd math_service  
       langgraph dev --port 2024 --no-browser

      可以针对运行中的服务器测试两种操作:

       from langgraph.pregel.remote import RemoteGraph  
      
      # 不带 token — 应该返回 401 失败  
      try:  
          bad_service = RemoteGraph("math_service", url="http://localhost:2024")  
          bad_service.invoke({  
              "action": "pick_operation", "operation": "", "number": 0,  
              "manual_input_chance": 0.0, "ask_user": False,  
          })  
          print("❌ Should have failed without token!")  
      except Exception as e:  
          print(f"✅ Correctly blocked: {e}")  
      
      # 带有效 token — 应该成功  
      service = RemoteGraph(  
          "math_service",  
          url="http://localhost:2024",  
          headers={"Authorization": "Bearer dev-token"},  
      )  
      
      # 测试: 选择一个运算  
      result = service.invoke({  
          "action": "pick_operation", "operation": "", "number": 0,  
          "manual_input_chance": 0.0, "ask_user": False,  
      })  
      print(result["operation"])  # 例如 'multiply'  
      
      # 测试: 生成一个数字(自动模式)  
      result = service.invoke({  
          "action": "generate_number", "operation": "", "number": 0,  
          "manual_input_chance": 0.0, "ask_user": False,  
      })  
      print(result["number"], result["ask_user"])  # 例如 14, False  
      
      # 测试: 生成一个数字(始终询问用户)  
      result = service.invoke({  
          "action": "generate_number", "operation": "", "number": 0,  
          "manual_input_chance": 1.0, "ask_user": False,  
      })  
       print(result["ask_user"])  # True — 编排器应提示用户

      步骤 2:本地编排器图

      接下来构建编排器,分顺序和并行两个版本以便对照。两者共享状态定义、远程图连接和节点函数,全部抽取到公共模块

      shared.py

      中。差异只在图的边如何连接。

      目录结构:

       math_orchestrator/  
       ├── shared.py  
       ├── shared_resilient.py  
       ├── orchestrator_sequential.py  
       ├── orchestrator_parallel.py  
       └── orchestrator_parallel_resilient.py

      math_orchestrator/shared.py 公共状态、连接和节点

       import os  
       from typing import TypedDict, Annotated  
       import operator  
       from langgraph.pregel.remote import RemoteGraph  
       # --- 状态定义 ---  
      
      class OrchestratorState(TypedDict):  
          current_number: float  
          operation: str  
          random_number: int  
          history: Annotated[list[str], operator.add]  
           manual_input_chance: float  # 0.0 = 始终远程, 1.0 = 始终手动  
       # --- 连接远程图 ---  
      # 单个远程图处理两种操作。  
      # 认证 token 从环境变量加载。  
      
      math_service = RemoteGraph(  
          "math_service",  
          url=os.environ.get("MATH_SERVICE_URL", "http://localhost:2024"),  
          headers={"Authorization": f"Bearer {os.environ.get('MATH_SERVICE_TOKEN', '')}"},  
       )  
       # --- 节点函数 ---  
      
      def build_initial_state(current_number: float, manual_input_chance: float) -> dict:  
          """构建 graph.invoke() 的初始状态字典。"""  
          if manual_input_chance == 0.0:  
              mode = "automatic"  
          elif manual_input_chance == 1.0:  
              mode = "manual"  
          else:  
              mode = f"mixed ({int(manual_input_chance * 100)}% manual)"  
      
          return {  
              "current_number": current_number,  
              "operation": "",  
              "random_number": 0,  
              "history": [f"Starting number: {current_number} (mode: {mode})"],  
              "manual_input_chance": manual_input_chance,  
           }  
       def get_operation(state: OrchestratorState) -> dict:  
          """使用 action='pick_operation' 调用 math_service。"""  
          result = math_service.invoke({  
              "action": "pick_operation",  
              "operation": "",  
              "number": 0,  
              "manual_input_chance": 0.0,  
              "ask_user": False,  
          })  
          op = result["operation"]  
          print(f"  → Operation: {op}")  
           return {"operation": op}  
       def _prompt_user_number(state: OrchestratorState) -> int:  
          """通过 stdin 提示用户输入一个数字。"""  
          op = state.get("operation", "?")  
          current = state.get("current_number", 0)  
          while True:  
              raw = input(  
                  f"  Current: {current} | Operation: {op} | Enter a number: "  
              )  
              try:  
                  return int(raw)  
              except ValueError:  
                   print("  Please enter a valid integer.")  
       def get_random_number(state: OrchestratorState) -> dict:  
          """  
          获取数学运算中要使用的下一个数字。  
      
          使用 action='generate_number' 调用 math_service,同时传递  
          manual_input_chance。远程图决定是生成一个数字还是请求  
          手动输入(通过 ask_user 标志)。  
          如果 ask_user 为 True,编排器在本地提示用户。  
          """  
          chance = state.get("manual_input_chance", 0.0)  
      
          result = math_service.invoke({  
              "action": "generate_number",  
              "operation": "",  
              "number": 0,  
              "manual_input_chance": chance,  
              "ask_user": False,  
          })  
      
          if result.get("ask_user"):  
              num = _prompt_user_number(state)  
              print(f"  → Number: {num} (manual)")  
              return {"random_number": num}  
      
          num = result["number"]  
          print(f"  → Number: {num}")  
           return {"random_number": num}  
       def execute_operation(state: OrchestratorState) -> dict:  
          """对 current_number 执行数学运算。"""  
          current = state["current_number"]  
          op = state["operation"]  
          num = state["random_number"]  
      
          if op == "add":  
              new_number = current + num  
              symbol = "+"  
          elif op == "subtract":  
              new_number = current - num  
              symbol = "-"  
          elif op == "multiply":  
              new_number = current * num  
              symbol = "×"  
          elif op == "divide":  
              if num == 0:  
                  num = 1  
              new_number = round(current / num, 2)  
              symbol = "÷"  
          else:  
              new_number = current  
              symbol = "?"  
      
          entry = f"  {current} {symbol} {num} = {new_number}"  
          print(entry)  
      
          return {  
              "current_number": new_number,  
              "history": [entry],  
           }
      get_operation

      get_random_number

      调用的是同一个

      math_service

      ,只是传入不同的

      action

      值。编排器视角下,远程图是一个支持多种操作的单一端点。

      下面看两种不同的图连接方式。每个编排器文件都很简短——业务逻辑全在

      shared.py

      里,编排器文件只关心拓扑结构。

      顺序执行

      math_orchestrator/orchestrator_sequential.py

      顺序版本先调用

      get_operation

      ,拿到

      end

      就直接终止,无需再去取随机数。非

      end

      的情况下继续调用

      get_random_number

      execute_operation

      ,然后循环回来。

       import argparse  
      from langgraph.graph import StateGraph, START, END  
      from shared import (  
          OrchestratorState,  
          build_initial_state,  
          get_operation,  
          get_random_number,  
          execute_operation,  
       )  
       # --- 路由逻辑 ---  
         
       def should_continue(state: OrchestratorState) -> str:  
           """获取操作后,决定:继续还是停止。"""  
           if state.get("operation") == "end":  
               return "finish"  
           return "continue"  
       # --- 构建图 ---  
      
      builder = StateGraph(OrchestratorState)  
      
      # 添加节点  
      builder.add_node("get_operation", get_operation)  
      builder.add_node("get_random_number", get_random_number)  
      builder.add_node("execute_operation", execute_operation)  
      
      # 定义边 — 顺序链  
      builder.add_edge(START, "get_operation")  
      
      # 获取操作后,决定:继续还是结束?  
      builder.add_conditional_edges(  
          "get_operation",  
          should_continue,  
          {  
              "continue": "get_random_number",  
              "finish": END,  
          },  
      )  
      
      builder.add_edge("get_random_number", "execute_operation")  
      
      # 执行后,循环回到 get_operation  
      builder.add_edge("execute_operation", "get_operation")  
      
      # 编译  
       graph = builder.compile()  
       # --- 运行 ---  
      
      if __name__ == "__main__":  
          parser = argparse.ArgumentParser()  
          parser.add_argument(  
              "--start-number", type=float, default=None,  
              help="Initial number to start with (prompts if not provided)",  
          )  
          parser.add_argument(  
              "--manual-input", action="store_true",  
              help="Always prompt user for numbers (shorthand for --manual-input-chance 1.0)",  
          )  
          parser.add_argument(  
              "--manual-input-chance", type=float, default=0.0,  
              help="Probability (0.0-1.0) of prompting user for each number (default: 0.0)",  
          )  
          args = parser.parse_args()  
      
          start = args.start_number  
          if start is None:  
              start = float(input("Enter starting number: "))  
      
          chance = 1.0 if args.manual_input else args.manual_input_chance  
      
          result = graph.invoke(build_initial_state(  
              current_number=start,  
              manual_input_chance=chance,  
          ))  
      
          print("\n🧮 Math Engine Complete! (Sequential)\n")  
          print("Computation History:")  
          for entry in result["history"]:  
              print(entry)  
           print(f"\nFinal Result: {result['current_number']}")

      顺序流的好处是逻辑直白,而且最后一次迭代拿到

      end

      时可以直接跳过取随机数的调用,省掉一次无用的 HTTP 请求。代价是每轮迭代的两次远程调用必须串行,一个等另一个。

      并行执行

      math_orchestrator/orchestrator_parallel.py

      并行版本利用 LangGraph 的 fan-out/fan-in 模式。

      get_operation

      get_random_number

      在同一个 superstep 中同时执行,两者都完成后

      execute_operation

      再决定是继续 fan-out 还是终止。

       import argparse  
      from langgraph.graph import StateGraph, START, END  
      from shared import (  
          OrchestratorState,  
          build_initial_state,  
          get_operation,  
          get_random_number,  
          execute_operation,  
       )  
       # --- 路由逻辑 ---  
      
      def should_continue(state: OrchestratorState) -> list[str] | str:  
          """  
          决定是继续循环还是停止。  
          返回节点名称列表用于 fan-out(并行),  
          或返回 END 以终止。  
          """  
          if state.get("operation") == "end":  
              return END  
          # Fan-out: 并行路由到两个节点  
           return ["get_operation", "get_random_number"]  
       # --- 构建图 ---  
      
      builder = StateGraph(OrchestratorState)  
      
      # 添加节点  
      builder.add_node("get_operation", get_operation)  
      builder.add_node("get_random_number", get_random_number)  
      builder.add_node("execute_operation", execute_operation)  
      
      # 定义边  
      # Fan-out: START 并行发送到两个节点  
      builder.add_edge(START, "get_operation")  
      builder.add_edge(START, "get_random_number")  
      
      # Fan-in: 两个节点都必须完成后 execute_operation 才能运行  
      builder.add_edge("get_operation", "execute_operation")  
      builder.add_edge("get_random_number", "execute_operation")  
      
      # 执行后,决定:再次 fan-out,还是结束  
      builder.add_conditional_edges(  
          "execute_operation",  
          should_continue,  
          ["get_operation", "get_random_number", END],  
      )  
      
      # 编译  
       graph = builder.compile()  
       # --- 运行 ---  
      
      if __name__ == "__main__":  
          parser = argparse.ArgumentParser()  
          parser.add_argument(  
              "--start-number", type=float, default=None,  
              help="Initial number to start with (prompts if not provided)",  
          )  
          parser.add_argument(  
              "--manual-input", action="store_true",  
              help="Always prompt user for numbers (shorthand for --manual-input-chance 1.0)",  
          )  
          parser.add_argument(  
              "--manual-input-chance", type=float, default=0.0,  
              help="Probability (0.0-1.0) of prompting user for each number (default: 0.0)",  
          )  
          args = parser.parse_args()  
      
          start = args.start_number  
          if start is None:  
              start = float(input("Enter starting number: "))  
      
          chance = 1.0 if args.manual_input else args.manual_input_chance  
      
          result = graph.invoke(build_initial_state(  
              current_number=start,  
              manual_input_chance=chance,  
          ))  
      
          print("\n🧮 Math Engine Complete! (Parallel)\n")  
          print("Computation History:")  
          for entry in result["history"]:  
              print(entry)  
           print(f"\nFinal Result: {result['current_number']}")

      运行结果

      打开两个终端窗口:

      终端 1——启动远程 math_service(如果此前没启动的话):

       cd math_service  
       langgraph dev --port 2024 --no-browser

      终端 2——运行编排器(任选其一):

       cd math_orchestrator  
      
      # 选项 A: 顺序 — 提示输入起始数字  
      python orchestrator_sequential.py  
      
      # 选项 B: 并行 — 通过参数指定起始数字  
      python orchestrator_parallel.py --start-number 100  
      
      # 任何选项配合手动输入模式 — 每次提示你输入数字:  
      python orchestrator_sequential.py --start-number 100 --manual-input  
      
      # 混合模式 — 每次迭代有 50% 的概率提示你:  
       python orchestrator_sequential.py --start-number 100 --manual-input-chance 0.5

      一次典型运行的输出如下:

         → Operation: subtract  
        → Number: 10  
        100.0 - 10 = 90.0  
        → Operation: subtract  
        Current: 90.0 | Operation: subtract | Enter a number: 1  
        → Number: 1 (manual)  
        90.0 - 1 = 89.0  
        → Operation: add  
        Current: 89.0 | Operation: add | Enter a number: 1  
        → Number: 1 (manual)  
        89.0 + 1 = 90.0  
        → Operation: divide  
        → Number: 17  
        90.0 ÷ 17 = 5.29  
        → Operation: add  
        → Number: 5  
        5.29 + 5 = 10.29  
        → Operation: divide  
        Current: 10.29 | Operation: divide | Enter a number: 1  
        → Number: 1 (manual)  
        10.29 ÷ 1 = 10.29  
        → Operation: subtract  
        Current: 10.29 | Operation: subtract | Enter a number: 1  
        → Number: 1 (manual)  
        10.29 - 1 = 9.29  
        → Operation: multiply  
        Current: 9.29 | Operation: multiply | Enter a number: 1  
        → Number: 1 (manual)  
        9.29 × 1 = 9.29  
        → Operation: multiply  
        Current: 9.29 | Operation: multiply | Enter a number: 2  
        → Number: 2 (manual)  
        9.29 × 2 = 18.58  
        → Operation: multiply  
        → Number: 10  
        18.58 × 10 = 185.79999999999998  
        → Operation: end  
      
      🧮 Math Engine Complete! (Sequential)  
      
      Computation History:  
      Starting number: 100.0 (mode: mixed (50% manual))  
        100.0 - 10 = 90.0  
        90.0 - 1 = 89.0  
        89.0 + 1 = 90.0  
        90.0 ÷ 17 = 5.29  
        5.29 + 5 = 10.29  
        10.29 ÷ 1 = 10.29  
        10.29 - 1 = 9.29  
        9.29 × 1 = 9.29  
        9.29 × 2 = 18.58  
        18.58 × 10 = 185.79999999999998  
      
       Final Result: 185.79999999999998

      运算和数字都由远程服务随机生成,每次运行的结果不同。

      从控制台到生产环境:使用 interrupt

      input()

      适合本地脚本调试。到了生产环境——编排器可能藏在 REST API、Web UI 或聊天界面后面——没有控制台可用。LangGraph 对此有一个一等原语:

      interrupt

      机制不复杂:节点调用

      interrupt()

      时 LangGraph 暂停整个图,将完整状态写入 checkpoint,然后把控制权交还给调用方。调用方(API 服务、Web 应用等)拿到暂停信号后向用户展示提示,收到响应后用

      Command(resume=...)

      恢复执行。图从

      interrupt()

      调用处精确恢复,哪怕过了几个小时、换了一台机器也没问题。

      以下是用

      interrupt

      替换

      input()

      get_random_number

      的写法:

      math_orchestrator/shared_interrupt.py(相关摘录)

       import sqlite3  
       from langgraph.types import interrupt, Command  
       from langgraph.checkpoint.sqlite import SqliteSaver  
       from langgraph.pregel.remote import RemoteGraph  
       math_service = RemoteGraph(  
           "math_service",  
           url="http://localhost:2024",  
       )  
       def get_random_number(state):  
          """  
          获取下一个数字 — 通过远程图或人工中断。  
      
          当远程图返回 ask_user=True 时,我们不调用 input(),  
          而是调用 interrupt(),它会暂停整个图并向调用应用程序  
          呈现一个提示。  
          """  
          chance = state.get("manual_input_chance", 0.0)  
      
          result = math_service.invoke({  
              "action": "generate_number",  
              "operation": "",  
              "number": 0,  
              "manual_input_chance": chance,  
              "ask_user": False,  
          })  
      
          if result.get("ask_user"):  
              # 暂停图 — 调用者接收此提示  
              num = interrupt({  
                  "prompt": "Enter a number",  
                  "current_number": state.get("current_number"),  
                  "operation": state.get("operation"),  
              })  
              print(f"  → Number: {num} (manual)")  
              return {"random_number": int(num)}  
      
          num = result["number"]  
          print(f"  → Number: {num}")  
           return {"random_number": num}

      编译图时必须附带 checkpointer(状态持久化),调用时必须指定 thread_id(标识具体会话):

       # 带 checkpointer 编译  
      checkpointer = SqliteSaver(sqlite3.connect("math_engine.db"))  
      graph = builder.compile(checkpointer=checkpointer)  
      
      # 启动新线程  
      config = {"configurable": {"thread_id": "session-42"}}  
      
      # 首次调用 — 运行直到 interrupt() 被调用  
      result = graph.invoke(  
          build_initial_state(current_number=100, manual_input_chance=1.0),  
          config=config,  
      )  
      
      # 图现在已暂停。result["__interrupt__"] 包含提示:  
      # [Interrupt(value={"prompt": "Enter a number", "current_number": 100, ...})]  
      
      # ... 时间流逝,用户通过 Web UI、API 等提供输入 ...  
      
      # 使用用户的值恢复  
      result = graph.invoke(Command(resume=42), config=config)  
      
      # 图从 interrupt() 被调用的地方继续执行,  
       # num = 42,并运行直到下一个 interrupt 或 END。

      几个要点。

      interrupt()

      input()

      目的相同,区别在于前者走 HTTP 而非 stdin——传入一个 payload(提示、上下文等),调用方通过

      Command(resume=...)

      回传用户输入。checkpointer 负责持久化图状态,LangGraph 支持 SQLite、Postgres 等多种后端。thread_id 用来标识会话,多个用户可以各自持有独立的暂停/运行中的图实例。远程图和图的连接方式无需任何改动,变化只发生在节点函数和调用模式上。

      保护线程:认证与授权

      如果 thread ID 是保护暂停会话的唯一手段,任何猜中或截获了 thread ID 的人都能恢复别人的图、注入自己的值。LangGraph Platform 对此有内置的认证与授权层。

      认证系统分两步走。

      @auth.authenticate

      处理程序作为中间件在每个请求上运行,验证调用者的凭据(JWT token、API 密钥、OAuth2 等)并返回用户身份;

      @auth.on

      处理程序执行资源级访问控制,给每个线程打上所有者标记,过滤访问权限,确保用户只能看到和恢复自己的线程。

      线程级安全的实现如下:

       from langgraph_sdk import Auth  
      
      auth = Auth()  
      
      @auth.authenticate  
      async def authenticate(authorization: str) -> Auth.types.MinimalUserDict:  
          """验证 Bearer token 并返回用户信息。"""  
          token = authorization.split(" ", 1)[1]  
          user = await verify_jwt(token)  # 你的 JWT 验证逻辑  
          return {  
              "identity": user["sub"],  
              "is_authenticated": True,  
          }  
      
      @auth.on.threads.create  
      async def on_thread_create(  
          ctx: Auth.types.AuthContext,  
          value: Auth.types.on.threads.create.value,  
      ):  
          """为每个新线程标记创建者的身份。  
      
          `value` 是线程创建载荷 — 一个包含来自 API 请求的字段的字典:  
          thread_id, metadata, if_exists 等。  
          我们修改 value["metadata"] 以在存储之前标记所有者。  
          返回值是 LangGraph 写入线程 metadata 的元数据过滤器。  
          """  
          value.setdefault("metadata", {})["owner"] = ctx.user.identity  
          return {"owner": ctx.user.identity}  
      
      @auth.on.threads.read  
      async def on_thread_read(  
          ctx: Auth.types.AuthContext,  
          value: Auth.types.on.threads.read.value,  
      ):  
          """过滤线程,使用户只能看到自己的。  
      
          `value` 是读取请求载荷 — 一个包含 thread_id 和来自  
          API 请求的任何 metadata 的字典。  
      
          返回值不是检查 — 而是查询过滤器。  
          LangGraph 在存储层应用它:只有 metadata.owner 与  
          ctx.user.identity 匹配的线程才会被返回。  
          其他用户拥有的线程是不可见的,而不仅仅是被阻止。  
          """  
          return {"owner": ctx.user.identity}  
      
      @auth.on.threads.create_run  
      async def on_thread_resume(  
          ctx: Auth.types.AuthContext,  
          value: Auth.types.on.threads.create_run.value,  
      ):  
          """过滤用户可以在哪些线程上恢复运行。  
      
          `value` 是运行创建载荷 — 一个包含 thread_id, assistant_id,  
          input, command, metadata, config 等的字典。  
      
          相同的过滤器机制:LangGraph 仅在线程的 metadata.owner  
          与返回的过滤器匹配时才允许运行。如果用户尝试恢复  
          另一个用户的线程,平台会拒绝请求,因为该线程  
          未通过过滤器。  
          """  
          metadata = value.setdefault("metadata", {})  
          metadata["owner"] = ctx.user.identity  
           return {"owner": ctx.user.identity}

      langgraph.json

      中注册:

       {  
         "auth": {  
           "path": "src/security/auth.py:auth"  
         }  
       }

      配置完成后,即使攻击者拿到了其他用户的 thread ID 也无法读取线程状态或恢复运行——授权处理程序会因为 owner metadata 不匹配而拒绝请求。过滤发生在平台层,不在图代码中,无法通过构造 API 请求绕过。

      生产部署时 LangGraph Platform 可以对接 Auth0、Supabase 以及任何 OAuth2/JWT 认证体系。记住一点:thread ID 是标识符,不是密钥——安全保障来自认证层对访问权限的把控。

      前面基于控制台

      input()

      的版本已经是这一模式的可运行原型。迁移到

      interrupt

      只需改动节点函数和调用模式,架构其余部分——远程图、fan-out/fan-in、错误处理——全部保持原样。

      RemoteGraph 的工作原理

      langgraph.pregel.remote

      中的

      RemoteGraph

      类是整个组合能力的底层支撑。它实现的接口和本地编译的图完全一致,可以

      .invoke()

      .stream()

      ,也可以直接嵌入为另一个图的子图节点。通信通过 HTTP对接 LangGraph Server API。

       from langgraph.pregel.remote import RemoteGraph  
      
      remote = RemoteGraph(  
          "math_service",          # 来自 langgraph.json 的 assistant ID  
          url="http://localhost:2024",  
      )  
      
      # 像使用普通图一样使用它 — action 字段控制行为  
       result = remote.invoke({"action": "pick_operation", "operation": "", "number": 0})
      RemoteGraph

      遵循

      Runnable

      接口,可以直接作为节点添加到另一个图中:

       builder.add_node("my_remote_node", remote_graph)

      编排器图不需要了解远程图的内部实现细节——它可以是一个简单状态机、一个 LLM 驱动的 agent,或者任何介于两者之间的东西。而通过条件路由,一个远程图部署就能承载多种操作。

      第二个调用依赖于第一个调用的结果时选顺序执行,或者想在最后一轮

      end

      时省掉无用的远程调用也该选顺序。两个调用互相独立、想缩短总耗时就选并行——在生产环境中远程图调用可能涉及 LLM 推理或数据库查询,并行执行差不多能把每轮延迟砍掉一半。

      错误处理

      并行执行带来一个自然的问题:远程图调用失败了怎么办?

      math_service

      临时挂掉\网络请求超时,这些情况都需要考虑。LangGraph 的处理提供了多种应对策略。

      默认行为:原子 superstep

      LangGraph 中并行节点在一个 superstep 里共同执行。superstep 中任何一个节点抛出异常,整个 superstep 原子性失败,不会有部分状态写入。假如

      get_random_number

      成功、

      get_operation

      失败,两边的结果都不会写入状态,避免了有随机数却没运算符这种不一致。

      配了 checkpointer 的情况下 LangGraph 会在内部保存成功节点的结果。恢复执行时只有失败分支重试,成功分支的工作不必重复。

      策略 1:RetryPolicy(图原生重试)

      最干净的做法是对容易出错的节点附加

      RetryPolicy

      。LangGraph 接管重试循环,支持配置尝试次数、退避策略和抖动。只有失败分支重试,成功的并行节点不会重新执行。

      重试全部耗尽后异常向上传播,图调用失败。对网络超时、5xx 错误这类瞬态故障,这是恰当的处理方式。

      策略 2:节点内 Try/Except(降级处理)

      需要图在远程服务不可用时仍然继续运行的场景下,在节点内部捕获异常并返回降级值即可。操作调用失败则引擎停止循环;数字生成调用失败则用安全默认值代替。

      策略 3:两者结合(生产环境推荐)

      生产中通常既要重试也要降级。

      RetryPolicy

      透明处理瞬态故障,重试全部耗尽后异常才落到节点内部的

      try/except

      块,由它提供兜底逻辑。

      以下是三种策略的完整可运行示例。

      总结

      RemoteGraph 让分布式 agent 架构的组合变得相当简洁——顺序还是并行的连接方式随意切换,

      RetryPolicy

      加上节点内降级逻辑构成两层容错。单个远程图通过条件路由就能承载多种操作,基础设施不必铺得很大,编排逻辑也能保持清晰。

      这个数学引擎作为 demo 虽然简单,展示的模式却可以直接迁移到正式系统——微服务化的 AI 编排,每个图作为一个独立部署的 agent 逻辑单元,根据延迟和成本需求选择合适的执行策略。

      本文完整代码:

      https://avoid.overfit.cn/post/d9102c5bf109459494a5bf2b99560b18

      by Alexander Machekhin

      最近遇到这样一个问题,我想在 asahi linux 上查看备份到 Proxmox Backup Server 里的目录,于是在 pve9 系统上交叉编译了 proxmox-backup-client ,执行时遇到这样的报错:

      d@d-macbookair:~$ proxmox-backup-client mount host/test test.ppxar ~/mnt/0
      Error: unable to read dynamic index 'test.mpxar.didx' - Invalid argument (os error 22)
      d@d-macbookair:~$ 
      

      这个报错我几乎已经确定是因为 asahi linux 使用 16KB 的页大小导致的,因为我用 4KB 页的 arm64 虚拟机测试这条命令是正常的,我想修改 proxmox-backup-client 的代码解决这个问题,但我对这个项目不熟悉,而且我一直缺少用 AI 写代码的经验(目前为止我只会用网页版本的 AI 写一些不是很复杂的代码,比如一些 bash 脚本,功能比较独立的 linux 内核模块等),想试试用 AI 来解决这个问题,但无法估计要花多少钱,也担心 AI 还没有能力解决这类问题,所以在尝试之前想先问一下有经验的 V 友,AI 适合用来解决这类问题吗?

      中转站像雨后春笋一样一个接一个的冒出,帖子标题取的一个比一个唬人:

      • [中转] Opus-4-6 上线,免费开蹬一周!回帖直接狂送 💰100 刀!

      • 中转站 Terminal.Pub 本周继续开启抽奖,抽奖 10 个 50 刀套餐,夜间不定期开放免费 claude/codex 渠道,ccmax 官渠低至 1 块 1 刀,逆向低至 1 毛 2 一刀]

      • [中转站 0 元蹬] GPT 全系模型 0 元开蹬、Anthropic Claude 和 OpenAI-Response 双协议全面支持 ,今日新注册送 500w 付费 Token

      现在开 API 中转站这么赚钱吗,各种抽奖、0 元开蹬、1000+人回复都送 100 刀那不就是 100000 刀,整十万刀,现在 Kiro 都开不出来 opus 了,真 opus-4-6 的话啥号池撑的起免费十万刀的消耗量...

      感觉就像是牛市一样,这么多人往里冲,加群一问一个不赚钱

      坐标北京,10 年 Android 程序员,年后开始找工作。

      面试第一家就拿到了 Offer ,薪资 19K ,在我接受范围之内。

      今天开开心心去办理入职,签合同时发现试用期 6 个月,没有提到只有前 3 个月 8 折。

      跟 HR 确认了一下,需要前 3 个月考核全为 A ,后 3 个月才能全额,心中顿时一万头草泥马奔腾。

      工作十几年了,没见过 6 个月 8 折的公司,也就是前 6 个月只有 15.2K ,比我刚入行时工资还低。

      思来想去,无法接受 6 个月的 8 折工资,果断放弃走人了!

      哎,不知道该说些什么,这种公司要放几年前,一个人都招不到。

      早期写网页,前端只有一个容器标签可用:<div>

      结果就是页面里堆叠了几百个 <div>。人眼能通过 CSS 样式看出哪里是头部、哪里是侧边栏。但对于搜索引擎爬虫、或是视障者的屏幕阅读器来说,这只是一坨没有主次的文本碎片。机器根本不知道 <div class="main"> 这几个英文字母代表核心内容。

      HTML5 引入 <header><main> 等语义化标签,本质不是为了给页面换个长相,而是给网页写一份“机器能看懂的结构说明书”

      当把核心代码放进 <main>,把底部备案信息扔进 <footer>,爬虫一进来就明确知道:“抓取有效信息直接去 <main> 里找,底部的东西可以直接跳过。”这就是语义化的底层价值。

      本文不背概念,直接以一个常见的博客设计稿为例,看我们该如何用这套标签把内容塞进正确的“房间”里。


      一、网页的 5 个固定组件

      再复杂的网站(比如电商、博客),核心结构都逃不出这 5 个固定组件。就像一套房子的“客厅、卧室、厨房”,功能是定死的:

      结构组件对应标签作用页面出现次数(通常)
      头部<header>放网站 Logo、大标题、全局搜索框。1 次(每个页面顶部都一样)
      导航<nav>放全局首要链接(首页、分类、关于我们)。1 次(常紧挨着头部)
      主内容<main>页面独占的核心内容(文章正文、商品详情)。仅 1 次(这是用户来页面的目的)
      侧边栏<aside>辅助内容(作者简介、相关推荐、广告)。可多次(依附主内容存在)
      底部<footer>网站补充信息(版权声明、备案号、联系方式)。1 次(每个页面底部都一样)

      拿到设计稿,第一步就是用这 5 个框,把图纸划分清楚。


      二、HTML 标签实战映射(页面级)

      我们先看“整个页面只有一份”的三个核心骨架标签。

      1. <main>:一切为了核心内容

      main 是页面的绝对主角。用户打开这篇网页为了看什么,什么就放在 <main> 里。

      💡 核心定律:一个页面只能有 1 个 <main> 且必须可见。绝对不能把它嵌套在 <header><nav><footer> 里面。
      <!-- 错误:放了两个主角 -->
      <main>文章摘要1</main>
      <main>文章摘要2</main> 
      
      <!-- 正确:所有文章被包裹在一个主角内 -->
      <main>
        <h1>今天的天气</h1>
        <p>北京今天晴,气温15-25℃...</p>
      </main>

      2. <header><footer>:门面与收尾

      <header> 放全局性的标识;<footer> 放全局的补充说明。它们通常在每个页面(首页、文章页、关于页)都保持相同的代码。

      <body>
        <header>
          <img src="logo.png" alt="我的博客logo">
          <h1>小A的技术博客</h1>
        </header>
      
        <main>
          <!-- 这里放每一页不一样的内容 -->
        </main>
      
        <footer>
          <p>©2025 小A的博客 | 备案号:京ICP备123456号</p>
        </footer>
      </body>

      三、文章与章节(内容级标签判断)

      大框架搭好后,我们进入 <main> 的内部。这里是新手最容易犯迷糊的地方:到底什么时候用 <article>,什么时候用 <section>

      1. <article>:独立成册的“小黄书”

      <article> 代表一段完全独立的内容。

      📖 独立性判断
      判断标准:把这部分内容单独复制下来,发到另一个网站去,它还是一篇完整、能看懂的东西吗? 如果能,就用 <article>

      一篇完整的博客文章、论坛里的一个主帖、一个商品介绍卡片,都属于 <article>

      <main>
        <!-- 首页的文章列表,每篇文章都是独立的 -->
        <article>
          <h2>如何搭一个简单的HTML页面</h2>
          <p>第一步:创建.html文件...</p>
        </article>
      
        <article>
          <h2>CSS 基础入门</h2>
          <p>把网页变好看的秘密...</p>
        </article>
      </main>

      2. <section>:书里的“第 X 章”

      <section> 代表具有相同主题的内容分组。它不是独立的文章,而是文章里的一个“章节”。

      💡 核心定律<section> 通常必须带有一个标题(<h1>-<h6>)。如果没有标题,说明这段内容不具备主题分组的资格,可能只是一个普通的 <div>
      <article>
        <h2>HTML结构入门</h2>
      
        <!-- 第一节内容 -->
        <section>
          <h3>1. 什么是HTML结构</h3>
          <p>就是网页的骨架...</p>
        </section>
      
        <!-- 第二节内容 -->
        <section>
          <h3>2. 核心标签有哪些</h3>
          <p>有header、nav、main...</p>
        </section>
      </article>

      四、被滥用的 <nav><aside>

      1. <nav>:只留给“主干道”

      不要看到链接就加 <nav>。文章底部的“上一篇/下一篇”链接、正文里的外部参考链接,都不配用 <nav>
      <nav> 是站点的主导航器

      🛠️ 正确做法:通常将 <ul> 列表放在 <nav> 中,确保语义极其清晰。
      <nav>
        <ul>
          <li><a href="index.html">首页</a></li>
          <li><a href="about.html">关于我</a></li>
        </ul>
      </nav>

      2. <aside>:正文的跟班

      <aside> 里面的内容如果被删掉,绝对不能影响主内容的阅读理解。
      最经典的场景就是侧边栏的“作者简介”、“相关猜你喜欢”、“广告位”。

      <main>
        <article>
          <!-- 主文章 -->
        </article>
      
        <aside>
          <h3>相关文章推荐</h3>
          <ul>
            <li><a href="#">上周去爬山</a></li>
          </ul>
        </aside>
      </main>

      五、无语义元素的归宿:<div><span>

      如果你手上的内容,套不上前面说的任何一个“带名字的房间”,这时候才轮到万能的容器出场。
      记住,它们没有任何语义,在机器眼里就是透明容器,仅为了方便 CSS 挂载样式。

      • <div>(大箱子):独占一行。用于包裹无主题的块级内容(如一个用来做动画的遮罩层、一个复杂的购物车弹窗框)。
      • <span>(小贴纸):不独占一行。用于包裹文字里的一小段,方便给这几个字单独上色。
      <p>
        今天气温<span class="high-temp">25℃</span>,比昨天高了。
      </p>
      
      <!-- 纯为了排版控制布局而存在的壳子,用 div并起好名字 -->
      <div class="banner-wrapper">
        <img src="ad.jpg">
      </div>

      🧠 10秒速记指南

      • 页面唯一主角定生死: <main>
      • 能单独转发给别人的内容块:<article>
      • 带小标题的内容区域/章节:<section>
      • 为了加 CSS 样式而设置的透明大盒子:<div>

      ➡️ 下期预告
      骨架搭好了,但网页还像一座座无法互通的信息孤岛。怎么让页面之间能够自由跳转、一键下载文件、甚至直接弹出邮件草稿?下一篇文章《HTML超链接从入门到精通》,带你掌握Web真正的灵魂——<a>标签!

      今天在海鲜市场收了一个 ChatGPT Team 的一个月车位,想着终于能爽用一把官方的 Codex 5.4 了。

      结果一顿操作上服务器(一台没有 GUI 的纯净 Linux VPS ),敲下 codex login,傻眼了。

      官方这个 CLI 的认证逻辑非常有毒:
      它必须要求你在本地浏览器完成 OAuth 授权,然后网页端会把 Token 回调到 http://localhost:1455
      因为我是 SSH 连在远程 VPS 上敲的登录命令,VPS 在 1455 端口干等,而我的浏览器在本地的 Mac 上,回调根本打不到服务器,最后要不就是满屏的 State Mismatch 错误,要不就是直接 Bad Request

      CLI 提示信息说,Headless 机器可以使用 codex login --device-auth(也就是给你一串验证码去浏览器填)。结果一敲这行命令,打开本地浏览器,登录后提示:当前 Team 管理员未开启设备验证功能... GG 。

      查了一下 GitHub 和各种论坛,大部分老哥给出的“偏方”是:
      在自己本地有浏览器的电脑装一个 Codex CLI -> 登录成功 -> 把 ~/.config/codex 等一堆授权文件夹用 tar 打个包 -> SCP 传到远端 VPS 解压覆盖。
      这种手工搬运 Token 的方式实在是既繁琐(每次过期都要折腾一轮),又感觉极度不优雅。

      既然是网络的痛点,那就走网络解决:SSH 临时隧道

      原理很简单:只要我在执行 codex login 的同时,把本机的 1455 端口悄悄通过 SSH 映射到远端服务器的 1455 端口,那浏览器重定向不就完美回传了吗?

      为了以后不再折腾,顺手糊了一个极简的 Bash 脚本,完全零依赖。
      核心逻辑就是用 SSH ControlSocket 在后台默默建立一个临时隧道,触发登录之后,只要你授权完成或者 Ctrl+C 打断,它会自动 trap cleanup EXIT,把这条隐藏的隧道拆干净,不留任何多余进程,如丝般顺滑。

      脚本开源放在这了:

      👇👇👇

      GitHub: codertesla/codex-remote-login

      用法极其傻瓜:

      (全程都在你本地的 Mac/Windows 终端下运行)

      1. 把它 down 下来
      git clone https://github.com/codertesla/codex-remote-login.git
      cd codex-remote-login
      chmod +x codex-remote-login.sh
      
      1. 执行脚本,后面跟上你的 SSH 目标服务器(支持 config alias 或者指定私钥)
      # 格式
      ./codex-remote-login.sh <你的远端 SSH 信息>
       
      # 比如
      ./codex-remote-login.sh [email protected]
      ./codex-remote-login.sh my-vps
      

      运行后它会弹出一个链接,你本地浏览器点进去走完授权,它就会提示远程登录成功,然后自动挥挥衣袖带走隧道。

      希望这个小脚本能帮到同样在 VPS 或者跳板机上开发,又被 Codex CLI 这个诡异的重定向逻辑折磨过的 V 友们。

      ​Any Video Converter Ultimate(视频转换工具终极版)是一款集DVD转换,视频转换,屏幕录影和DVD刻录功能于一体的操作简易的转换软件。

      一、安装软件

      1. 解压安装包

        安装包下载: https://pan.quark.cn/s/a92425e60e88 ,找到下载的 Any Video Converter Ultimate 7.0.0 安装包,右键点击 → 选择【解压到当前文件夹】。

      2. 进入解压目录

        双击打开解压后的【Any_Video_Converter_Ultimate_7.0.0】文件夹。

      3. 运行安装程序

        找到 avc-ultimate_7.0.0安装程序,右键 →【以管理员身份运行】(避免权限不足)。

      4. 按向导安装

        • 点击【确定】→ 勾选【我同意此协议】→ 点击【下一步】;
        • 修改安装路径(可选):将默认 C盘改为 D盘(如 C:\Program Files\Anvsoft\Any Video Converter Ultimate→ D:\Program Files\Anvsoft\Any Video Converter Ultimate)→ 点击【下一步】;
        • 连续点击【下一步】三次 → 点击【安装】。
      5. 完成基础安装

        安装进度条走完后,选择【否,稍后重新启动电脑】→ 点击【完成】。

      二、激活软件

      1. 运行注册机

        返回解压后的【Any_Video_Converter_Ultimate_7.0.0】文件夹,右键 any.video.converter.ultimate.7.0.0-patch注册机 →【以管理员身份运行】。

      2. 定位软件安装目录

        右键桌面软件图标 →【打开文件所在的位置】。

      3. 粘贴并运行注册机

        • 在打开的软件安装目录空白处,右键 →【粘贴】注册机;
        • 右键粘贴好的注册机 →【以管理员身份运行】。
      4. 完成激活

        点击注册机中的【Patch】按钮 → 显示【ok】提示即激活成功 → 点击【Exit】关闭注册机。

      三、验证安装激活

      双击桌面软件图标 → 弹出提示时点击【取消】→ 成功进入软件主界面,说明安装与激活完成!

      Ubuntu 26.04 LTS (代号 Resolute Raccoon )预计于 2026 年 4 月 23 日发布,作为下一代长期支持版本,它将成为未来数年企业与服务器环境的重要基础系统。

      相比 24.04 LTS ,本次版本的变化并不只是界面升级,而是涉及 内核、桌面架构、软件栈、应用分发和系统安全机制等多个底层领域。

      参考 https://mp.weixin.qq.com/s/1D2OZ3SPDU0NZcvRidAiTw