2026年3月

前情提要
https://v2ex.com/t/1190080

背景

除了经常往返内地和香港,我也偶尔出国。这次和大家分享一下我用国行 iPhone Air 出国的真实体验。

与外版不同,国行 iPhone Air 最坑的设定是仅支持保存 2 张 eSIM。这意味着如果你想添加第 3 张,就必须先删掉前 2 张里的其中 1 张,简直是国内运营商最“恶毒”的限制。这次我从香港出发去美国,简单聊聊国行 iPhone Air 在境外的换卡步骤。

换卡步骤
购卡:直接在淘宝买 eSIM 套餐即可,不再赘述。

激活条件:必须在境外操作。虽然有定位要求,但只要出了内地就行,我在香港就成功换上了 T-mobile 。

添加过程:通常 eSIM 是扫二维码添加,但我这次是把 IMEI 码发给店家,等了一会儿手机就自动弹出了“添加 eSIM”的选项。由于只能存 2 张卡,我只能忍痛删掉了香港的卡(运营商是 CSL )。

使用体验:添加成功后,和实体卡完全没区别,非常顺滑。

避坑指南

换卡必须有网!
如果删卡后你舍不得开内地卡的漫游,恰好周围又没有 Wi-Fi ,你就会陷入“彻底断网、无法激活新卡”的死循环。

删卡前三思!
无论是删内地卡还是香港卡,人在国外都是无法重新添加回来的。这可能导致你收不到验证码,无法登录某些 App ,比如银行的 App 。

回港需跑实体店
虽然在手机上点一下就删了,但等我回到香港,必须亲自去运营商实体店才能把香港卡的 eSIM 补回来。

后记
国行 iPhone Air 最大的痛点就是“2 张 eSIM 的容量上限”,这个机制在旅途中确实给我造成了不少麻烦。但抛开这点不谈,这台手机的整体体验还是很棒的,依然推荐购买!

1password 近期宣布了涨价计划,截取原文:“我們將更新個人方案的價格,更新將自 2026 年 3 月 27 日起生效。
當前價格 vs 新價格:
目前價格:€31.80 EUR/年
新價格:€43.80 EUR/年
新價格將在你的下一次更新訂閱時生效(如果更新訂閱日期在 2026 年 3 月 27 日或之後)。若更新訂閱發生在 2026 年 3 月 27 日之前,則更新訂閱時仍以當前價格執行。”

欧区个人版已经订阅了快 5 年,这次涨幅有些大,要不是 apple 自带钥匙串功能简陋我都不打算用第三方服务了。
今天尝试了一下 bitwarden ,Mac 端 app 实在不好用,直接放弃,以前尝试过 keepass ,好在开源并且密码自己掌握,各平台客户端丰富,一开始下载了 strongbox (软件闭源,但 UI 确实挺好看),密码都导入好了,开了三个月免费试用一搜才发现不久前被一个纽约公司收购了,名声不怎么好,直接放弃找替代方案,最终选择了 keepassium (开源),ios 和 Mac 都有,另外 Mac 上用 keepassXC (开源,并且也有 PC 端)也是个不错的选择,密码库同步我都用的 iCloud 同步,后续计划就这样使用了,彻底告别 1password ,朋友们你们目前使用的是哪种密码管理方案?

本文主旨

[!important]
希望大家多多分享经验,积极留言评论。

所有诈骗,都离不开利用你的压力、制造虚假机会、让你自我说服。社会欺诈千千万,本文虽不能助你 100% 看穿所有套路,但愿护你守住钱袋、身体与善良。

反欺诈口诀

[!NOTE]
第一原则:

  1. 拒绝 “心软式付出”—— 陌生人的 “惨状” 可能是表演,无明确需求时,坚决不买不需要的商品、不借现金、不转账;
  2. 牢记:“真推销不纠缠,真求助找官方,真困难先找家人”。卖惨不帮、纠缠不理、道德绑架直接远离。

看不懂 = 不安全。不放心 = 不要办。不管对方说得再好听,你都没有任何义务必须当场答应。

  • 高收益、低风险、高流动 —— 必是坑
  • 看不懂、听不懂、黑箱操作 —— 马上走
  • 催你快、逼你签、倒计时 —— 立刻停
  • 要贷款、要分期、要先交钱 —— 直接拒
  • 合同看不懂、不写清金额期限 —— 绝不签
  • 拉人头、给返利、好项目喊你抢 —— 都是骗
  • 熟人急借钱、不见面就转账 —— 先核实
  • 装可怜、卖惨相、道德绑架你 —— 别心软
  • 真困难、找家人、不缠陌生人 —— 是常理
  • 用同情、逼愧疚、道德不对称 —— 是套路

亲戚涉钱三必查:

  1. 说投资 / 项目 → 查资质、查合同,别信口头承诺
  2. 让垫钱 / 代持 → 找借口推脱,亲情别绑利益
  3. 催你快掏钱 → 哪怕血缘近,也别当场拍板

境外高薪版(专门防被骗去缅甸、柬埔寨、老挝)

境外高薪包吃住,十有八九是电诈
不用技能高工资,必是坑人卖猪仔
护照一交人失控,关打拘禁任人弄
偷渡出境无保障,进去容易出来难
熟人喊你闯天下,多半推你进火坑
真赚钱不喊外人,大馅饼都是陷阱

欺诈分析

任何欺诈能成立,本质是施骗者和受骗者同时满足三个条件:

1、感知到的压力

  • 想快速赚钱、回本、翻身
  • 焦虑、缺钱、想改善生活
  • 被同龄人差距、负债、家庭压力驱动

2、感知到的机会

  • 看到 “低风险高收益” 的宣传
  • 相信 “内部渠道”“独家资源”
  • 觉得自己能抓住别人抓不到的机会

3、自我合理化

  • “别人都在赚,我不赚就亏了”
  • “就投一点,亏了也没事”
  • “平台这么大,不可能骗我”

结论:只要你有贪念 + 焦虑 + 侥幸,骗子就可以精准收割。

参考资料

你在浏览器输入网址并回车,几秒后就看到了页面。但这短短几秒内,发生了极其密集的物理协作。

网页加载的本质,就是你的设备(客户端)跨越千山万水,找别人的电脑(服务器)要一份文本文件。

这不是随口一要就能拿到,机器必须经过以下 6 道关卡。

第 1 关:切碎 URL (拆解目标)

你输入的 https://baidu.com/en-US/ 叫 URL。机器首先会把它分成三个切片:

  • https:// (协议):规定了客户端跟服务器对话必须遵守的语言格式。
  • baidu.com (域名):帮服务器取的网名,方便人脑记忆,但机器读不懂。
  • /en-US/ (路径):指明你要拿的是服务器那台机器上,哪个文件夹下面的具体文件。

第 2 关:DNS 查询 (翻地址本)

底层网络通信全靠认 IP 地址(如 151.101.129.217),它根本不认识 baidu.com 这种方便人类记忆的拼音缩写。

所以浏览器必须停下来,去查一个名为 DNS 服务器 的巨型互联网通讯录,找到这个域名对应的服务器真实 IP 地址。

核心定律:就近查找原则 (多级缓存)
跨网络查 DNS 极慢。为了加快速度,机器会按离自己远近优先翻缓存:先看浏览器自己存了没 ➔ 再看你电脑系统存了没 ➔ 再问你家路由器 ➔ 全没有,最后才去远端查询 DNS 主服务器。一旦找到就立刻停手。

这些缓存咋来的?只要你以前访问过一次这个网站,或者这台路由器上别人访问过,机器就会默默把“域名和 IP 的对应关系”抄在小本上存一阵子。下次再来,直接翻本子,省去跨网提问的时间。

第 3 关:建立 TCP 连接 (修通物理公路)

拿到真实 IP 后,客户端和服务器依然不能直接传文件。因为互联网的底层物理线路极其不可靠,经常丢包掉线。

所以它们必须先建立一条带有确认与重传机制的虚拟管道这就叫 TCP 连接。机器通过经典的三次握手(来回对发寥寥几次试探包),确保双方收发通道都没瘫痪,这条路才算获批通车。

第 4 关:发 HTTP 请求 (递交订单单据)

路终于修通了。浏览器顺着管子,向远端的服务器递交一份极其严格的提货单HTTP 请求

这份订单是一段纯文本,里面核心只写着一条指令:GET /en-US/ HTTP/1.1(即:我要拿你硬盘上 /en-US 里的文件,咱们用 1.1 版本的 HTTP 标准格式对话)。

第 5 关:服务器拆包回传 (切碎发货)

服务器查验订单后,找出了你要的 HTML 文本。但它绝对不会一次性将这 2MB 的大文件整个发回去。

如果一次性全发,只要发到 99% 时由于你进了电梯丢了 1 秒钟的信号,整个 2MB 就只能重头再发。频繁重传巨大的文件,会迅速耗尽服务器带宽,导致极其恐怖的网络堵塞。

为了对抗丢包,服务器必须把完整的文本文件,切碎成几百上千个碎片(数据包)。每个碎片贴上一个编号标签(如 3/1000),像漫天撒网一样扔向互联网公路。中途哪个包不幸丢了,你的手机就只通知服务器单独重传那几 KB 的碎片即可,极大的解决了网络拥堵问题。

常见的发货暗号 (状态码)
在正式发送包裹之前,服务器会先快速回甩一个三位数的包裹单状态码,含义如下:

  • 200:文件找到了,订单通过,开始朝你丢包裹。
  • 404:对不起,你的路径给错了,这台服务器里查无此文件。
  • 500 / 503:这台主服务器内部代码崩溃了,或者被挤爆断电了。

第 6 关:拼装

那几百个切碎的文本包裹碎片,陆续接收到你的设备里。

一旦包裹被全部收齐,浏览器的底层收发器会立刻根据标签编号,把它们原封不动地重新拼合成那份完整的带有尖括号的纯文本(HTML 等)。

至此,长途跋涉向别人要一份文件的网络加载任务彻底完结。

紧接着,机器便会立刻开始 5 道引擎渲染流水线(解析 DOM 树 ➔ CSSOM 树 ➔ 最终绘制),把这份纯文本,画成精美的可交互网页。

至于浏览器具体是如何把代码一步步渲染成画面的?下一章我们接着拆解。

项目是一个 OpenSpec 驱动的简单图床网站,完全由 Claude Code 编写。

我给 Claude Code 接入了 Codex MCP server ,让 Claude Code 写完代码后调用 Codex review 代码,根据反馈意见进行修改,直到通过为止。

现在的问题是,Codex 已经 review 了 8 轮,每轮都能抛出七八个不一样的问题,Claude Code 吭哧吭哧改,改完了 Codex 还能 review 出来几个完全不一样的问题,CC token 烧了快 100 刀,Codex review 通过还是遥遥无期。

大家有遇到过这种问题吗?

买了一个欧乐 B ,说明书上写,刷头的刷毛尖端变颜色了,就表示应该更换刷头了。

这个刷头我用了半个多月吧,刷毛尖端就已经变颜色。
这就到了该换的时候了吗?

看起来欧乐 B 的刷头还不便宜,半个月换一个是不是太奢侈了?

要 antigravity 帮我生成 git 提交信息,等他回答完毕准备提交代码发现我的开发的代码不见了

使用模型是 Gemini 3.1 pro

截图

为什么会出现这么严重的问题?

问了 9.9 开的 team 的 gpt ,从 auto 到 thinking 到 pro 全都说自己是 GPT4-mini 系列。

问了两个自己的免费套餐的 GPT 账号,反而都说自己是 GPT5 系列。

不知道直接问的判断准不准,感觉这个 Team 有猫腻儿的样子。

从 Surge 换到 Singbox ,习惯了 Snell 后想继续用 0-RTT 开销的协议。看到 Singbox 官网推荐 SS AEAD 2022 + TCP + 多路复用,于是 server 架了一个 Singbox 自己的实现,开了 TCP fast open ,加密方式 2022-blake3-aes-128-gcm ,只走 IPv4 。结果一天就被阻断了(说好的 Resists passive detection 呢。。。)。

不知道是哪个环节导致的,不过还好只封了一个端口。

供参考。

我在外面用 tailscale 连家里的 NAS ,发现其 UDP 上传速度被限速到 5mbps 了(TCP 上传没有被限速,UDP 下 ipv4 和 ipv6 均被限速,公网 ipv4 和 ipv6 还在),能找客服解开吗?

(之前用 qbittorrent 把一个 win10 安装镜像挂成了种子,上传流量 几百个 GB ,后来发现上传被限速就立即关掉了)

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:霁明

1. 引言

1.1 背景与动机

在 AIWorks 的工作流和 Agent 编排系统中,有一个核心需求:支持在节点配置面板的配置项中引用上游节点的输出变量。例如,一个 LLM 节点需要引用“开始节点”的用户输入或自定义变量,或者引用上一个“HTTP 请求节点”的返回结果。

最直接的方案是使用传统的 Input 或 Textarea 组件,配合变量占位符语法如 {{nodeId.variableName}}。但这种方案存在明显的用户体验问题:

  • 可读性差​:原始的变量语法对用户不友好,难以快速识别变量来源
  • 输入效率低​:用户需要记忆变量名称和语法格式
  • 缺乏上下文​:无法直观展示变量所属节点和类型
  • 易出错​:手动输入变量语法容易出现拼写错误

我们期望的用户体验是:

  1. 用户输入 / 字符时,自动弹出变量选择菜单
  2. 菜单按节点分组展示所有可用变量,支持搜索过滤
  3. 选择变量后,以可视化标签的形式展示(显示节点图标、节点名称、变量名)
  4. 底层数据仍保持 {{#nodeId.variableName#}} 格式,便于后端解析

1.2 最终效果

实现后的效果如下:

Lexical 变量输入编辑器 Jan 6 2026.gif

  • 触发菜单​:在编辑器中任意位置输入 /,即刻弹出变量选择悬浮菜单
  • 变量搜索​:支持按变量名进行搜索
  • 可视化标签​:选中的变量渲染为带有节点图标和样式的标签
  • 无缝编辑​:标签与普通文本混排,支持 Input 组件中的常规操作,例如复制、删除、撤销等

2. 技术选型:为什么选择 Lexical?

2.1 Lexical 简介

Lexical 是 Meta(Facebook)于 2022 年开源的一个可扩展的可扩展富文本编辑器框架,它专注于提供高可靠性、出色的可访问性和高性能,让开发者能构建出从简单文本到复杂富文本协作编辑器的应用。它核心是一个轻量、无依赖的编辑器,通过模块化的插件机制支持自定义功能,支持与 React 等前端框架进行绑定,旨在简化富文本编辑器的开发和维护。

2.2 主流富文本框架对比

维度LexicalSlateTiptapProseMirrorEditor.jsQuill
维护方Meta社区Tiptap 团队社区CodeX 团队社区
是否开源是 (MIT)是 (MIT)是 (MIT)是 (MIT)是 (Apache 2.0)是 (BSD)
React 支持原生原生支持需适配层支持支持
学习曲线中等中等偏高中等偏低陡峭
社区生态增长迅速稳定繁荣稳定稳定稳定
TS 支持完善完善完善支持支持支持
核心优势高可靠性、高性能、Meta 背书,适合现代 web 应用灵活性极高、符合 React 直觉兼顾易用与强大、UI 无头协同编辑天花板、极其严谨块级结构、天然适合 CMS简单易用、稳定
主要劣势文档仍可优化升级可能断层协作/高级功能需付费订阅开发门槛极高跨行选择等体验有限定制复杂功能较难
适用场景现代高性能 React 应用需要极度定制 UI 的 React 项目快速交付的产品复杂协同办公 (Google Docs 类)新闻发布、类 Notion 编辑器评论区、简单博客、CMS

2.3 选择 Lexical 的理由

  1. 轻量级​:核心库约 42KB(gzip 后),对 bundle size 友好
  2. 现代架构​:基于不可变状态,与 React 理念一致
  3. 高性能​:优化的内部机制使得能够处理大规模的文本编辑任务而不牺牲响应速度
  4. 强扩展性​:插件化设计,自定义节点类型简单直观
  5. React 深度集成​:虽然并不仅限于 React,但它提供了与 React 深度集成的能力
  6. 官方维护​:Meta 活跃维护,稳定可靠
  7. TypeScript 原生​:完整的类型支持,开发体验好
  8. 同类主流产品验证​:Dify、FastGPT 等都采用 Lexical 实现变量输入功能

2.4 AIWorks 使用的依赖

{
  "lexical": "^0.35.0",
  "@lexical/react": "^0.35.0",
  "@lexical/text": "^0.35.0",
  "@lexical/utils": "^0.35.0"
}
  • lexical:核心库,提供编辑器状态管理、节点系统、命令系统
  • @lexical/react:React 绑定,提供 Composer、插件等组件
  • @lexical/text:文本处理工具,包含文本实体(Text Entity)相关功能
  • @lexical/utils:工具函数,如 mergeRegister 用于批量注册/注销

3. Lexical 核心概念速览

在深入实现之前,我们需要理解 Lexical 的几个核心概念。

3.1 编辑器状态

Lexical 采用不可变状态设计。编辑器的所有内容都存储在 EditorState 中,任何修改都会产生新的状态对象。

// 读取状态(只读操作)
editor.getEditorState().read(() => {
  const root = $getRoot();
  const text = root.getTextContent();
});

// 更新状态(写操作)
editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    selection.insertText('Hello');
  }
});

关键点​:

  • read() 内只能读取,不能修改
  • update() 内可以读取和修改
  • 所有 $ 开头的函数(如 $getRoot$getSelection)只能在这两个回调中调用

3.2 节点体系

Lexical 的内容由树状节点结构组成:

RootNode
  └── ParagraphNode (ElementNode)
        ├── TextNode ("普通文本")
        ├── VariableLabelNode (DecoratorNode) 
        └── TextNode ("更多文本")

核心节点类型:

类型说明示例
RootNode根节点,每个编辑器有且仅有一个-
ElementNode容器节点,可包含子节点ParagraphNode, ListNode
TextNode文本叶子节点普通文本内容
DecoratorNode装饰器节点,可渲染自定义 React 组件变量标签、提及、表情

DecoratorNode 是实现自定义可视化元素的关键​,后文会详细讲解。

3.3 命令系统

Lexical 使用命令模式处理用户输入和操作:

// 创建自定义命令
const HELLO_WORLD_COMMAND: LexicalCommand<string> = createCommand();

// 注册自定义命令行为
editor.registerCommand(
  HELLO_WORLD_COMMAND,
  (payload: string) => {
    console.log(payload);
    return false;
  },
  COMMAND_PRIORITY_LOW,
);

// 触发对应命令
editor.dispatchCommand(HELLO_WORLD_COMMAND, 'Hello World!');

Lexical 内置了许多命令,例如:KEY\_DOWN\_COMMAND、UNDO\_COMMAND、INSERT\_TAB\_COMMAND 等,具体可查看LexicalCommands.ts

命令优先级从高到低:

  • COMMAND_PRIORITY_CRITICAL (4)
  • COMMAND_PRIORITY_HIGH (3)
  • COMMAND_PRIORITY_NORMAL (2)
  • COMMAND_PRIORITY_LOW (1)
  • COMMAND_PRIORITY_EDITOR (0)

3.4 节点转换

节点转换是 Lexical 的强大特性,允许监听特定类型节点的变化并自动处理:

editor.registerNodeTransform(TextNode, (textNode) => {
  // 每当 TextNode 发生变化时触发
  const text = textNode.getTextContent();
  
  // 检测特定模式并转换
  if (isVariablePattern(text)) {
    const variableNode = $createVariableLabelNode(...);
    textNode.replace(variableNode);
  }
});

这是实现“输入特定文本自动转换为自定义节点”的核心机制。

3.5 插件架构

Lexical 采用组合式插件设计:

<LexicalComposer initialConfig={config}>
  {/* 核心编辑插件 */}
  <RichTextPlugin contentEditable={...} placeholder={...} />
  
  {/* 功能插件 */}
  <HistoryPlugin />           {/* 撤销/重做 */}
  <OnChangePlugin />          {/* 内容变化监听 */}
  <VariableLabelPlugin />     {/* 自定义:变量渲染 */}
  <VariableLabelPickerPlugin />{/* 自定义:变量选择 */}
</LexicalComposer>

插件通过 useLexicalComposerContext() 获取编辑器实例:

const MyPlugin = () => {
  const [editor] = useLexicalComposerContext();
  
  useEffect(() => {
    // 使用 editor 注册命令、转换等
  }, [editor]);
  
  return null; // 无 UI 的纯逻辑插件
};

4. 整体架构设计

4.1 架构图

Lexical 变量输入编辑器.png

4.2 组件职责划分

组件/模块职责
PromptEditor业务组件,连接 workflow store,处理多行提示词场景
VariableEditor业务组件,处理单行变量输入场景
Editor核心组件,封装 Lexical 编辑器和所有插件
VariableLabelNode自定义节点,渲染为 React 组件,用于反显变量标签
VariableLabelPlugin自定义插件,监听文本变化,将变量语法转换为变量标签
VariableLabelPickerPlugin自定义插件,处理 / 触发和变量选择
SingleLinePlugin自定义插件,限制单行输入

4.3 数据流及渲染过程

flowchart TD
Start([开始]) --> Input["用户输入 '/'"]
Input --> Detect["VariableLabelPickerPlugin 检测到 '/'"]
Detect --> Menu["弹出 VariableMenu 菜单"]
Menu --> Select["用户选择变量"]
Select --> Insert["插入文本 '{{#nodeId.varName#}}'"]
Insert --> Transform["VariableLabelPlugin 的 TextNode Transform 检测到变量语法"]
Transform --> CreateNode["创建 VariableLabelNode 替换文本"]
CreateNode --> Render["VariableLabelNode 渲染 VariableLabel 组件"]
Render --> Sync["OnChangePlugin 的 onChange 方法触发,同步文本内容到外部状态"]
Sync --> End([结束])

5. 核心实现详解

5.1 自定义 VariableLabelNode

这是整个方案的核心。我们通过继承 DecoratorNode 来创建一个可以渲染 React 组件的自定义节点:

export class VariableLabelNode extends DecoratorNode<JSX.Element> {
  __variableKey: string;      // 变量的完整标识,如 {{#nodeId.name#}}
  __variableLabel: string;    // 显示用的标签
  __isSystemVariable: boolean; // 是否为系统变量

  static getType(): string {
    return "variableLabel";
  }

  // 返回 React 组件作为节点的渲染内容
  decorate(): JSX.Element {
    return (
      <VariableLabel
        variableLabel={this.__variableLabel}
        isSystemVariable={this.__isSystemVariable}
      />
    );
  }
  // ... 其他方法
}

关键设计点:

  1. 继承 DecoratorNode​:这使得节点可以渲染任意 React 组件
  2. getTextContent()​:返回变量的原始格式文本,确保序列化时能正确还原
  3. decorate()​:返回 VariableLabel 组件,实现可视化展示

5.2 触发器:/ 唤起变量选择菜单

当用户输入 / 时,我们需要弹出一个变量选择菜单。这里使用 Lexical 官方提供的 LexicalTypeaheadMenuPlugin

const VariableLabelPickerPlugin = ({ variableGroups }) => {
  const [editor] = useLexicalComposerContext();

  // 自定义触发匹配:检测用户输入 /
  const checkForTriggerMatch = useBasicTypeaheadTriggerMatch("/", {
    minLength: 0,
  });

  // 用户选择变量后的处理逻辑
  const onSelectOption = useCallback((selectedOption, nodeToRemove, closeMenu) => {
    editor.update(() => {
      // 删除触发字符 /
      if (nodeToRemove) nodeToRemove.remove();

      // 插入变量文本,格式为 {{#nodeId.variableName#}}
      selection.insertNodes([
        $createTextNode(`{{#${selectedOption.nodeId}.${selectedOption.name}#}}`),
      ]);
      closeMenu();
    });
  }, [editor]);
  // ...
};

工作流程:

  1. 用户输入 /checkForTriggerMatch 返回匹配结果
  2. 弹出 VariableMenu 组件,显示可用变量列表
  3. 用户点击选择 → onSelectOption 插入格式化的变量文本
  4. VariableLabelPlugin 监测到文本变化,自动转换为节点

注意这里我们并不直接插入 VariableLabelNode,而是插入格式化的文本字符串。这是为了解耦选择逻辑和渲染逻辑——文本到节点的转换由下一个插件统一处理。

5.3 文本实体识别与自动转换

VariableLabelPlugin 负责监听文本变化,当发现符合变量格式的文本时,自动将其转换为 VariableLabelNode

const VariableLabelPlugin = () => {
  const [editor] = useLexicalComposerContext();

  // 创建变量节点的工厂函数
  const createVariableLabelPlugin = useCallback((textNode: TextNode) => {
    const text = textNode.getTextContent();
    const info = parseVariableTokenInfo(text);
    return $createVariableLabelNode(
      text,
      info?.variableName ?? "",
      info?.isSystemVariable ?? false,
    );
  }, []);

  useEffect(() => {
    // 注册文本实体转换器
    registerLexicalTextEntity(
      editor,
      getVariableMatchInText,  // 正则匹配函数
      VariableLabelNode,
      createVariableLabelPlugin,
    );
  }, [editor]);
  // ...
};

变量格式通过正则表达式定义:

// 用户变量格式:{{#uuid.variableName#}}
export const USER_VARIABLE_REGEX = new RegExp(
  "(\\{\\{)(#)([a-fA-F0-9-]{36}\\.[a-zA-Z0-9_]+)(#)(\\}\\})",
);

// 系统变量格式:{{#system.xxx#}}
export const SYSTEM_VARIABLE_REGEX = new RegExp(
  "(\\{\\{)(#)(system\\.[a-zA-Z0-9_]+)(#)(\\}\\})",
);

registerLexicalTextEntity 是核心的转换逻辑,它注册了两个 Transform:

export function registerLexicalTextEntity(editor, getMatch, targetNode, createNode) {
  // 1. TextNode → VariableLabelNode 的转换
  const textNodeTransform = (node: TextNode) => {
    const text = node.getTextContent();
    const match = getMatch(text);
    if (match === null) return;

    // 分割文本节点,将匹配部分替换为目标节点
    const [nodeToReplace, remainingNode] = node.splitText(match.start, match.end);
    const replacementNode = createNode(nodeToReplace);
    nodeToReplace.replace(replacementNode);

    // 递归处理剩余文本(可能包含多个变量)
    if (remainingNode) textNodeTransform(remainingNode);
  };

  // 2. 反向转换:当节点内容不再匹配时还原为文本
  const reverseNodeTransform = (node) => {
    const match = getMatch(node.getTextContent());
    if (match === null) {
      replaceWithSimpleText(node);  // 还原为普通文本
    }
  };

  return [
    editor.registerNodeTransform(TextNode, textNodeTransform),
    editor.registerNodeTransform(targetNode, reverseNodeTransform),
  ];
}

5.4 变量标签的可视化渲染

VariableLabel 组件负责将变量以友好的方式呈现给用户:

const VariableLabel = ({ variableLabel, isSystemVariable }) => {
  const { Icon, nodeLabel, displayLabel } = useVariableLabelInfo(
    variableLabel,
    isSystemVariable,
  );

  return (
    <div className="inline-flex items-center rounded-sm bg-bg-primary-4 px-[2px]">
      <Icon className="flex-shrink-0" />
      <span className="text-text-2-icon">{nodeLabel}</span>
      <span className="text-text-4-description">/</span>
      <span className="text-primary-default">{displayLabel}</span>
    </div>
  );
};

会渲染一个可视化变量标签,包含节点图标、节点名称和变量名,效果如下:

5.5 编辑器单行模式

在某些场景(如 HTTP 节点的 URL 输入、条件节点的表达式输入),我们需要限制编辑器为单行模式:

const SingleLinePlugin = ({ onEnter }) => {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    mergeRegister(
      // 1. 限制 RootNode 只保留一个段落
      editor.registerNodeTransform(RootNode, (rootNode) => {
        if (rootNode.getChildrenSize() <= 1) return;

        const children = rootNode.getChildren();
        const firstChild = children[0];
        // 将后续段落的内容合并到第一个段落
        for (let i = 1; i < children.length; i++) {
          const paragraph = children[i];
          paragraph.getChildren().forEach(child => firstChild.append(child));
          paragraph.remove();
        }
      }),

      // 2. 拦截 Enter 键
      editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
        event?.preventDefault();
        onEnter?.();  // 可以触发外部回调,如提交表单
        return true;
      }, COMMAND_PRIORITY_HIGH),
    );
  }, [editor, onEnter]);

  return null;
};

这个插件通过两种机制实现单行限制:

  1. RootNode Transform​:当检测到多个段落时,自动合并为一个
  2. Command 拦截​:阻止 Enter 键创建新段落

5.6 编辑器状态初始化与同步

编辑器内容需要与后端数据同步,我们采用纯文本格式存储。

编辑器状态初始化:

export const textToEditorState = (text = "") => {
  const lines = text.split("\n");
  const paragraph = lines.map((p) => ({
    children: [{ text: p, type: "text", ... }],
    type: "paragraph",
    //...
  }));

  return JSON.stringify({
    root: { children: paragraph, type: "root", ... },
  });
};

编辑器状态同步:

const handleEditorChange = (editorState: EditorState) => {
  const text = editorState.read(() => {
    return $getRoot()
      .getChildren()
      .map((p) => p.getTextContent())
      .join("\n");
  });
  onChange(text);
};

由于 VariableLabelNode.getTextContent() 返回原始变量格式({{#nodeId.name#}}),导出的文本可以直接存储,再次加载时会自动转换回节点形式。

6. 总结

本文介绍了基于 Lexical 实现工作流变量输入编辑器的完整方案:

  1. VariableLabelNode​:继承 DecoratorNode 实现渲染自定义变量标签节点
  2. VariableLabelPickerPlugin​:使用 LexicalTypeaheadMenuPlugin 实现 / 触发展示变量选择菜单
  3. VariableLabelPlugin​:通过 Transform 自动识别和转换变量文本
  4. SingleLinePlugin​:可选的单行模式支持
  5. 插件化架构​:功能解耦,各插件职责单一,方便维护和扩展

这套方案适用于:

  • 工作流中的变量引用
  • 类似评论区的 Mention 功能
  • 模板引擎的可视化编辑
  • 任何需要“触发字符 + 选择菜单 + 自定义渲染”的场景

最后

欢迎关注【袋鼠云数栈 UED 团队】\~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

用三个礼拜的时间 vibe 了一款 SSH Client:

Netcatty

项目地址:github.com/binaricat/Netcatty

之前一直付费用 Termius ,不过后来感觉一个月 15 刀买一个 SSH Client 还是有点贵。中间调研过各种平替,确实除了 Termius 没找到第二款支持云同步的 SSH Client 。所以决定尝试 vibe 一个出来。

Netcatty 支持基于多种方式的云同步,包括 Google Drive, Github Gist, OneDrive, S3, WebDav 。同步几乎是实时的,只要配置发生变化就会自动往云端同步。
在功能上,Netcatty 实现了 Termius 的大部分功能,交互,布局都有参考。如果你是 Termius 老用户应该会很熟悉 Netcatty 的交互。

不过 Fido2/生物指纹密钥功能暂时先没做。此外 Netcatty 目前没有实现移动端。

如果你刚好缺一个带云同步功能的 SSH Client ,同时也觉得 Termius 有点贵,并且没有移动端的需求,那么可以试试这个开源平替。省下来的钱可以买一个 AI 订阅了😘

本项目定位于做 termius 开源平替,后续也不会收费,我会在平时使用的时候顺手修一修 bug ,如果可以帮到你,莫忘记给主包点个 star 就当交个朋友

功能特性

🗂️ Vault

  • 多种视图 —— 网格 / 列表 / 树形
  • 快速搜索 —— 迅速定位主机与分组

🖥️ 终端工作区

  • 分屏 —— 水平/垂直分割,多任务并行
  • 多会话管理 —— 多连接并排处理

📁 SFTP + 内置编辑器

  • 文件工作流 —— 拖拽上传/下载更直观
  • 就地编辑 —— 内置编辑器快速修改文件

🎨 个性化

  • 自定义主题 —— 按喜好调整应用外观
  • 关键词高亮 —— 自定义终端输出高亮规则

云端同步

  • 本项目支持基于 Github/Google/OneDrive/WebDav 等机制的云端同步功能

最近米哈游那个程序员独自在家突然暴毙(包括前几年上海那个独居女性也是突然暴毙她的财产好像是被充公了?)

我觉得这种事情在未来,甚至现在已经有越来越多这种独自生活的人。那么像他们要怎么去提前规划,处理好自己万一哪一天突然挂掉之后,财产的处理方案呢?


结合前段时间 死了么 app 爆火,我突然想到一个事情,现在中国是不是已经有大量的独居人群,甚至年龄已经开始偏年轻化了?

就像上海那个案例,我记得她年纪其实不大。这说明了一个什么问题?是不是中国现在其实存在大量潜在的、处于孤独或危险境地的这类人群?

图片
文末有源码下载链接!

    Go运行时(runtime)是Go高性能和高并发的核心支撑,其中内存管理与垃圾回收是关键。今天将深入底层机制,理解Go程序如何分配内存、如何决定数据的生命周期、以及Go垃圾回收器是如何工作的。

1、Go内存管理体系概览

Go使用一种分层式、针对并发优化的内存管理架构。可以概括为:线程本地分配(arena+mcache)+全局分配(central)+垃圾回收(GC),也就是减少锁争用+高速内存分配+自动回收。

2、内存分配器

Go的内存分配器模仿TCMalloc(TCMalloc—Design document for the C/C++ memory allocator TCMalloc, which the Go memory allocator is based on.),主要分三层:

2.1 堆(Heap):有runtime管理的大块连续内存区域

Go并不直接向操作系统请求小块内存,而是向操作系统申请一块(64MB大小)内存,这块内存在操作系统的术语叫Arena(竞技场),Go在Arena内做更细粒度的管理

2.2 中心缓存(Central Cache):全局共享的小对象池(加锁)

Central按size class将对象划分为8B~32KB的多种规格:

每个size class都有一个central列表

用于分配中等频率的内存

有锁(mutex)保证多线程安全

2.3 mcache:每个P拥有的线程本地缓存(无锁)

Go的GMP模型中,每个P(处理器)拥有一个mcache:

本地缓存,分配速度极快

小对象分配的时间复杂度是O(1),直接从mcache中拿

mcache没了才从Central Cache获取

3、小对象和大对象的分配策略

4、栈内存和堆内存

Go使用可憎长栈(由2KB~8GB):

栈内存快、无需GC,函数返回自动销毁

堆内存慢,需要GC管理

因此,尽可能把对象放在栈内存上,更高效,要做到这一点,依赖逃逸分析。

5、逃逸(Escape)分析

什么是逃逸?编译器决定变量放在栈上还是放在堆上,放在堆上的就产生了逃逸。可以用以下命令来查看自己的程序哪些地方产生了逃逸:

go build -gcflags "-m -m"

图片

5.1 什么情况下变量会逃逸?

5.1.1 返回值逃出当前作用域

图片

5.1.2 变量被存到interface、空interface时

图片

5.1.3 闭包引用的外部变量

图片

5.1.4 大量数据复制到channel时也可能逃逸

5.1.5 编译器无法证明变量的生命周期,例如发送指针到通道

图片

6、Go垃圾回收(GC)整体流程

Go的GC是并发标记、并发清扫:

世界停止很短

GC是三色表记法

并发+增量+自适应(根据GOGC调整)

7、GC完整流程图

┌────────────┐
│  Root Scan │  ← STW(短暂)
└─────┬──────┘
      │
      ▼
┌────────────┐
│  Marking   │  ← 并发(goroutine 与 GC 同时运行)
├────────────┤
│ Write Barrier(写屏障) │
└─────┬──────┘
      │
      ▼
┌────────────┐
│  Mark Done │ ← STW(短暂)
└─────┬──────┘
      │
      ▼
┌────────────┐
│  Sweep     │ ← 并发
└────────────┘

8、三色标记法(Tri-Color Marking)

三种颜色的含义:

GC从Roots开始扫描(栈、全局变量、寄存器),步骤:

8.1 初始化:所有对象都是白色

8.2 将Root对象标记为灰色,进入灰色队列

8.3 循环处理灰色对象队列

对每个灰色对象:把它的子对象标记为灰色,自己变为黑色

8.4 结束时:黑色存活,白色将在Sweep阶段被回收

9、写屏障(Write Barrier):并发GC保证正确性的关键

并发GC时用户程序还在运行,会出现:新引用出现或者白对象被指向,导致对象遗漏,Go使用混合写屏障来保证三色不变性。

写屏障规则:

在程序写指针(p=q)时:将新引用的对象标记为灰色,将旧引用的对象标记为灰色(必要时)

核心保证:黑对象永远不指向白对象

10、Sweep阶段:并发清除白色对象,这个阶段和程序并发运行,不会产生STW(Stop the word)

11、GOGC:GC调度器

GOGC默认是100,表示:当本来GC后heap增长100%,再次触发GC。

GOGC=50   // 更频繁 GC
GOGC=200  // 更少 GC
GOGC=off  // 关闭自动 GC

12、从GC视角出发,如何写出更高效的代码?

  • 尽量避免逃逸(减少堆对象)
  • 减少大对象创建(>32KB)
  • 重用对象(sync.Pool)
  • 避免短命且大量分配的临时 slice/map
  • 使用 [][]byte 而非 string 大规模拼接
  • 用 make 预估容量,避免扩容

*源码地址*

1、公众号“Codee君”回复“每日一Go”获取源码

2、源码获取链接: https://pan.baidu.com/s/1B6pgLWfSgMngVeFfSTcPdg?pwd=jc1s 提取码: jc1s


如果您喜欢这篇文章,请点赞、推荐+分享给更多朋友,万分感谢!

前几天,OpenClaw 2026.2.26 有重大更新,把 ACP Agents 作为一等公民了:这样 OpenClaw 可以作为 ACP Clients 连接上任何的 ACP Agents 了。

不过,早在一月份,OpenClaw 自己就可以作为 ACP Agent ,被其他任意 ACP Client 所连接。

我就在想,如果能通过 VS Code 来连上 OpenClaw ,岂不是更方便?

于是,我今天就立马更新了我的 VS Code ACP Client extension,添加了对 OpenClaw 的支持!

在 VS Code 中运行 OpenClaw

不过,值得注意的是,最近版本的 OpenClaw 作为 ACP Agent 似乎有些 bug ,我使用的是 v2026.1.30 版本的 OpenClaw ,可以如丝般顺乎地操作~

npm i -g [email protected]

目前,ACP Client extension 已经默认支持 GitHub Copilot 、Claude Code 、Gemini CLI 、Qwen Code 、OpenCode 、Codex CLI 、Qoder CLI 、Auggie CLI 、OpenClaw 这九个 Agent 。

当然也可以另外配置,连接更多的 ACP Agent 。

项目的代码完全开源:

https://github.com/formulahendry/vscode-acp

欢迎围观或者使用~

TVBox-Swift 是一套基于原生 Swift & SwiftUI 构建的跨平台多媒体应用核心工程。它同时支持 macOS 和 iOS 设备,致力于在双端提供无缝、现代且极其流畅的视听体验。

该版支持 type=0/1/4 ( XML/JSON/Remote )。
该版暂不支持 type=3 (JAR/Spider) 动态脚本源。

macOS 版本经过验证可以正常使用,iOS 版本暂未验证。

源码: https://github.com/Jstrom2022/tvbox-Swift

最近在工位上突然想到一个问题。

假如一个人不结婚、也不搞什么高风险投资,就老老实实上班,正常交社保。趁着年轻能力允许的条件下,每个月固定买 2 克实物黄金,坚持个几十年,到老了能不能解决养老的问题?


我想了几个点,也不知道对不对

1️⃣ 为什么不把买黄金的钱存起来?

主要是为了对抗通胀。

  • 我想的是从古至今,黄金都是值钱的,更何况不是说乱世黄金嘛
  • 所以那时黄金价格应该跟着日常消费水平走,即使发生通胀,对应的单克金价应该等价于当时的消费水平

除非到那时候金子突然不值钱了,但我觉得这种情况不会发生。


2️⃣ 在正常缴纳社保的前提下

我觉得到那时候可能会进一步延迟退休

等我那天干不动了,每个月卖一两克

在没有重大疾病的情况下,生活还会有压力嘛? [最普通的生活条件即可]


3️⃣ 为什么不买黄金类基金?

我想的是实物拿在手里最踏实,持有克数不会受到市场波动而有影响。但是说实话,我也不知道这两者到底是不是都可以,给我的感觉是实物更靠谱些。


所以上面这些想法到底切不切实际?还是我想的太简单了