[{"content":"博客内容更新不会记，但折腾过的装修痕迹应该留下来。这里不记录普通文章发布，只记录这个博客本身怎样一点点变成现在这样。\n","permalink":"/changelog/","summary":"changelog","title":"站点更新日志"},{"content":"Hi 👋, I\u0026rsquo;m Ray 自诩全沾工程师，作为程序员一枚，我没有高校背景，没有大厂背景，我也许是普通的 NPC，却有着很多梦想，想在世界上留下关于自己一丝的痕迹久远于自己的生命哪怕在某个角落于尘埃中存在着，那也是存在过的证明，是我能想到最终极的意义，也算是不虚此行。\n折腾是当前能最小成本实现自我价值的方式了。什么价值，暂时是愉悦自我。\n除了这个博客，也可以在 GitHub、X 和 LinuxDO 找到我。这里长期会写 AI 工具与模型实测、Linux 和网络运维、开发踩坑，也会留下一点生活观察。\n这个博客本身也一直在折腾。主题、功能和分类怎样一点点变成现在这样，都记在 站点更新日志 里。\n关于”琐碎快记“和”杂技浅尝“ 这是本站最早的两个分类了，刚开始写的时候也忘了这俩分类从何处借鉴而来，但是显然边界不清晰，所以很长一段时间乱用，现在重新分类，博客大部分技术向的都归于杂技浅尝，除非是特殊系列的，倾向于探索。\n而日常、灵光、小碎片、快记就属于琐碎快记。但是现在写博客的形式还被困在Obsidian+git中，所以这些灵光都只能随风飘散，来日做个网兜都兜回来。\n关于”他山拾影“ 之前引用的文章还不错，结果原作者不维护或者换域名了，那真是难受，所以干脆抄过来，直接抄显得没有诚意，偶尔叫AI帮忙抄，但终归是抄，所以新增这个分类表明转载\n关于“月下小扎” 熬夜不好，可是静谧之夜充满诗意。\n关于“什么是什么” 由于平时的时间实在是被各种加班和个人懒惰挤压的丝毫不剩，导致不容易能静下心来写写日记或者博客。我内心向往的，写日记的场景必须得是在 闲、雅、散的状态进行的，简单描述下：雨天，微风，室内，夜晚，一盏灯一杯茶一点香一支笔一个人一篇日记，哇哦，简直了，文人雅士的最终状态。哈哈 这当然只是理想所在，事实是雨天，微风，室内，夜晚，一盏灯，两杯奶茶两支手机两把王者把把 MVP。所以和同事聊天之时突然意识到，天天写博客那是为难自己了，不如给自己硬性规定一下，每周一篇，写什么都行，那既然每周一篇了，就像周刊一样，便开始寻找选题。翻开之前的个人笔记，有一块地方我命名为“小卡”，这是从语雀笔记里面带过来的思想，旨在随时随地记下灵感，之后再进行总结归档。小卡里记了不少名词概念之类的快查，我就想到，不如写一个系列，叫“什么是什么“。\n这个系列里面每一片都是一个简单的概念。积累这些点开始，等点多了一次次回顾串成线再成网，哈哈哈，每想到这种宏大的场景 内心便是激动不已，自觉快哉，况且这种简单的概念可以给自己或网友提供一些帮助，一搜就能搜到，也算是有点贡献（增加点击率就更美好了）\n所以，这周开始便会周更该系列，希望早日突破个位数的阅读数（现在都是朋友在读）。\n","permalink":"/about/","summary":"about","title":"关于 Ray"},{"content":" 最近好多个 oneman 云的灵车都死光光了，剩下这几个都是 3-8$左右的月付\nMJJ 的快乐其实很朴素。\n服务器买回来，不一定真有什么业务，但是最好整整齐齐摆在一个面板里。看它们全绿，看 CPU、内存和流量轻轻跳动，就很舒服。[Doge]\n我以前也用过哪吒、Beszel 之类的监控面板。功能都不少，问题是它们通常还要先找一台服务器当主控。为了看其他服务器活没活，我得先养一台专门负责「看服务器」的服务器，多少有点套娃。\n另外我对主控和 Agent 之间长期保持通信这件事一直有点心里没底。未必真的不安全，但只要各台机器还需要给主控留一个可达入口，我就容易多想。\n我的需求其实没那么复杂：\n机器还活着没有 CPU、内存、磁盘大概用了多少 当前网速和累计流量 延迟、丢包有没有突然抽风 VPS 什么时候到期 告警、自动化运维、远程执行命令这些暂时都不是刚需。能看，够轻，界面顺眼，就可以了。\n然后我找到了 CF-Server-Monitor。\n主控直接扔给 Cloudflare 这个项目把面板、API 和数据存储都放到了 Cloudflare：\ntext 1 2 3 4 5 6 7 8 服务器上的 Agent │ │ HTTPS / WSS 单向上报 ▼ Cloudflare Worker ├── D1 保存服务器和历史数据 ├── Durable Objects 做实时推送 └── 静态资源提供监控面板 也就是说，我不需要再拿一台 VPS 跑主控，不用额外维护数据库、反代和主控进程。Agent 主动往 Cloudflare 上报，服务器本身也不用为了监控再开放一个入站端口。\n单向上报不等于绝对安全，API_SECRET 还是得好好保存，后台也得换强密码。但从我的使用习惯来说，这种结构明显更省心。\nCloudflare 这一侧用到 Worker、D1 和 Durable Objects。项目本身就是按免费额度设计的，少量个人服务器先拿来玩完全够用。真把上报频率拉得很高、机器数量堆得很多，还是要自己盯一下 Cloudflare 的用量。\n部署大概就是这些 仓库提供了 Cloudflare 一键部署，也可以直接用 Wrangler。我的做法是先创建 D1，再构建并部署 Worker：\nbash 1 2 3 4 npm install npm run build:frontend npx wrangler d1 create server-monitor-db npx wrangler deploy D1 创建完成后，需要把返回的 database_id 写进 wrangler.toml。然后再设置一个随机的 API_SECRET：\nbash 1 openssl rand -hex 32 | npx wrangler secret put API_SECRET 这里不要把真实 Secret 写进仓库，记得用强密码。\n最后给 Worker 绑定一个自己的子域名，例如：\ntext 1 monitor.example.com Cloudflare 端就差不多了。没有 docker compose，没有数据库容器，也没有「主控机挂了以后我该去哪看主控机」这种哲学问题。\n探针更简单 后台新增一台服务器后，会生成独立的 Server ID 和安装命令。脱敏以后大概长这样：\nbash 1 2 3 4 5 6 curl -fsSL https://raw.githubusercontent.com/huilang-me/cfsm-agent/main/install.sh \\ | sh -s -- install \\ -id=\u0026lt;SERVER_ID\u0026gt; \\ -secret=\u0026lt;API_SECRET\u0026gt; \\ -url=https://monitor.example.com/update \\ -auto_update=0 Linux 上安装后是一个 cf-probe systemd 服务，常用检查命令也就两个：\nbash 1 2 systemctl status cf-probe journalctl -u cf-probe -f 看到 WSS connected，回面板刷新一下，机器就出现了。\n可以，成功了。\n最后的效果 这个界面其实也能换肤，但是懒得弄了，现在这个还行。深色终端风格，服务器可以按地区和分组展示，CPU、RAM、磁盘、网速、总流量、月流量、价格和到期时间都有，当然 后面几个信息自己配置的。延迟和丢包也不是只放一个数字，而是直接给一排小色块，看着很直观。\n条形图、环形图、列表和地图几种视图都能切。中英文、亮暗主题也有。至少在「现代」「好看」「打开就知道机器有没有死」这几个需求上，已经完全满足我了。\n现在没有打算把它变成多复杂的监控系统。资源告警、通知渠道、主题这些功能以后有需要再配。先把手里的 VPS 一个个挂上去，看着一排绿色在线状态就已经很快乐。\n也许，我是末法时代入门的垃圾佬吧\n","permalink":"/posts/%E6%8E%A2%E9%92%88-mjj-%E7%9A%84%E5%BF%AB%E4%B9%90/","summary":"\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.szmckj.cn/uploads/2026/09/02/6a97a0edacd05.png\"\u003e\n        \u003cimg alt=\"CF-Server-Monitor 世界地图模式显示 6 台服务器在线\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.szmckj.cn/uploads/2026/09/02/6a97a0edacd05.png\" width=\"300\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e最近好多个 oneman 云的灵车都死光光了，剩下这几个都是 3-8$左右的月付\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003eMJJ 的快乐其实很朴素。\u003c/p\u003e\n\u003cp\u003e服务器买回来，不一定真有什么业务，但是最好整整齐齐摆在一个面板里。看它们全绿，看 CPU、内存和流量轻轻跳动，就很舒服。[Doge]\u003c/p\u003e","title":"探针——MJJ 的快乐"},{"content":"本实验使用一台带公网 IPv4 的 VPS 作为固定 WireGuard 中转节点，使 Mac、手机等终端能够稳定访问 NAT 后面的家庭局域网。家庭侧最终由 OpenWrt 旁路由承载 WireGuard，主路由只增加一条静态路由，家庭服务器本身不再需要运行 WireGuard。\n实验最终跑通，但过程中遇到了三个容易混淆的问题：OpenClash 将 WireGuard Endpoint 解析为 Fake-IP、OpenWrt 防火墙主动拒绝 WireGuard 到 LAN 的转发，以及 wg-quick 重载配置后没有自动增加新的系统路由。本文按实际排查顺序记录配置、现象、判断依据和结果。\n本文所有公网地址、域名、密钥、用户名和局域网地址均已替换为文档示例值，不能直接用于真实部署。\n实验目标与网络结构 目标不是让家庭网络通过 VPS 上网，而是建立一条路径确定的远程访问通道：所有节点主动连接公网 VPS，VPS 永久负责中转，P2P 不作为依赖。\n脱敏后的实验地址如下：\n节点 地址或网段 作用 VPS 公网地址 203.0.113.10 WireGuard 固定入口 WireGuard 域名 wg.example.com DNS Only A 记录 WireGuard 网段 10.66.0.0/24 隧道地址空间 VPS 10.66.0.1 中转服务器 家庭服务器 10.66.0.2 可选的独立 Peer Mac 10.66.0.3 远程客户端 手机 10.66.0.4、10.66.0.5 独立客户端 OpenWrt 旁路由 10.66.0.6 家庭 LAN 的 WireGuard 网关 家庭 LAN 192.168.50.0/24 需要远程访问的内网 主路由 192.168.50.1 家庭默认网关 OpenWrt 旁路由 192.168.50.80 WireGuard 与 OpenClash 所在设备 家庭服务器 192.168.50.227 内网访问目标之一 flowchart LR mac[\u0026#34;Mac / 手机\\n10.66.0.3-5\u0026#34;] vps[\u0026#34;公网 VPS\\n10.66.0.1\u0026#34;] wrt[\u0026#34;OpenWrt 旁路由\\nWG 10.66.0.6\\nLAN 192.168.50.80\u0026#34;] main[\u0026#34;主路由\\n192.168.50.1\u0026#34;] lan[\u0026#34;家庭 LAN\\n192.168.50.0/24\u0026#34;] mac \u0026lt;--\u0026gt;|WireGuard| vps vps \u0026lt;--\u0026gt;|WireGuard| wrt wrt \u0026lt;--\u0026gt; lan main \u0026lt;--\u0026gt; lan main -. \u0026#34;10.66.0.0/24 via 192.168.50.80\u0026#34; .-\u0026gt; wrt VPS 只需要稳定的公网连接、UDP 质量和流量额度。WireGuard 本身对 CPU、内存和磁盘的要求很低；该实验没有使用 ZeroTier Moon、Tailscale DERP 或 Headscale。\n初始化 VPS 实验服务器使用 Debian 12。初始化时先更新系统并安装 WireGuard、防火墙、二维码、自动安全更新和备份所需工具：\nbash 1 2 3 4 apt-get update apt-get -y full-upgrade apt-get install -y \\ wireguard-tools nftables qrencode unattended-upgrades rsync 随后完成以下基础设置：\n将管理端 Ed25519 公钥写入 /root/.ssh/authorized_keys。 确认公钥能够独立登录后，关闭 SSH 密码认证。 开启 unattended-upgrades。 开启 IPv4 转发。 配置 nftables，只开放 SSH 和 WireGuard。 SSH 加固配置放在 /etc/ssh/sshd_config.d/99-hardening.conf：\ntext 1 2 3 4 PubkeyAuthentication yes PasswordAuthentication no KbdInteractiveAuthentication no PermitRootLogin prohibit-password 应用前必须先检查语法，并保留当前 SSH 会话：\nbash 1 2 sshd -t systemctl reload ssh IPv4 转发写入 /etc/sysctl.d/99-wireguard-forward.conf：\ntext 1 net.ipv4.ip_forward=1 bash 1 sysctl --system 融合怪：IP 解锁与大陆网络质量 服务上线后使用融合怪检查 IP 解锁、IP 质量和大陆三网线路。Shell 版 ecs.sh 会临时改写 limits.conf 和多项 sysctl、调整时钟服务并安装依赖，其 README 也建议生产环境使用 Go 版。因此测试固定使用 Go 版 v0.1.195，核对官方 checksums.txt 后在临时目录运行。\n测试关闭 CPU、内存、磁盘和结果上传。解锁、IP 质量和测速以 nobody 身份运行；traceroute 补测只给临时二进制授予 CAP_NET_RAW，测试后删除。\nIP 归属与质量 项目 结果 IPv4 AS134835 Starry Network Limited，数据库定位日本东京 IPv6 AS134835，/56，数据库定位香港 平台解锁 IPv4 主要结果：\n类别 可用 不可用或异常 AI ChatGPT、Gemini、Grok、Sora 均为日本区 Claude、Perplexity AI 被拒绝 国际流媒体 Netflix、Netflix CDN、Disney+、Amazon Prime Video 均为日本区 — 日本平台 Hulu Japan、FOD、D Anime Store DMM TV 不可用，U-NEXT 被拒绝，TVer 网络错误 其他 YouTube 日本区，TikTok 可用 TikTok 识别区域为 ALISG IPv6 可用性更不统一：Netflix、Disney+、Gemini、Grok、Sora 和 YouTube 均可用且识别为日本；ChatGPT 返回 Unknown，Claude、Perplexity AI 和 U-NEXT 被拒绝。实际使用某个平台时，应分别验证 IPv4 和 IPv6，不能将 IPv4 结果直接外推到双栈客户端。\n大陆三网回程 线路从 AS134835 进入 BBIX / SoftBank，再分别进入中国电信 163、中国联通 4837 和中国移动 CMI。未检测到 CN2、9929 等优化回程，属于普通公网线路。\n目标 traceroute 5 包 Ping 平均值 丢包 北京电信 30 跳未收到目标回应 无回应 100% 北京联通 30 跳未收到目标回应 无回应 100% 北京移动 14 跳到达 59.21 ms 0% 上海电信 12 跳到达 74.07 ms 0% 上海联通 13 跳到达 114.83 ms 20% 上海移动 15 跳到达 62.85 ms 0% 广州电信 30 跳未收到目标回应 无回应 100% 广州联通 13 跳到达 130.04 ms 80% 广州移动 14 跳到达 65.69 ms 0% 成都电信 12 跳到达 103.94 ms 0% 成都联通 13 跳到达 118.43 ms 0% 成都移动 16 跳到达 65.17 ms 0% Ping 只有 5 个样本，且部分运营商测试端点会过滤 ICMP。因此“无回应”不等于业务流量不可达，80% 丢包也只代表当时收到 1 个回应，不适合外推为长期质量。\n大陆节点双向测速 节点 VPS → 节点 节点 → VPS 延迟 联通唐山 55.18 Mbps 96.75 Mbps 133.36 ms 电信上海 42.52 Mbps 2.19 Mbps 138.98 ms 移动上海 62.94 Mbps 458.56 Mbps 109.27 ms 三个节点只是单次快照，不能代表运营商整体质量。但是本次结果清楚显示了方向和运营商差异：移动节点最好，联通延迟更高，电信上海到 VPS 的反向吞吐在该时段明显受限。对 WireGuard 中转场景而言，最终仍应以真实家庭宽带和移动客户端的长时间双向测试为准。\n测试前后对 WireGuard 服务、Peer 数量、nftables 规则、sysctl 配置和 limits.conf 进行了对照，状态与文件哈希均保持不变。\n配置 DNS 与 WireGuard 服务端 域名管理端创建一条 A 记录：\ntext 1 wg.example.com → 203.0.113.10 该记录必须保持 DNS Only。Cloudflare 普通代理不转发 WireGuard 使用的 UDP 端口，不能开启小黄云代理。\n服务端 /etc/wireguard/wg0.conf 的关键结构如下，私钥和预共享密钥使用占位符表示：\n[ini] 显示已折叠代码（23 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [Interface] Address = 10.66.0.1/24 ListenPort = 51820 PrivateKey = \u0026lt;SERVER_PRIVATE_KEY\u0026gt; SaveConfig = false [Peer] # home-server PublicKey = \u0026lt;HOME_SERVER_PUBLIC_KEY\u0026gt; PresharedKey = \u0026lt;HOME_SERVER_PSK\u0026gt; AllowedIPs = 10.66.0.2/32 [Peer] # macbook PublicKey = \u0026lt;MAC_PUBLIC_KEY\u0026gt; PresharedKey = \u0026lt;MAC_PSK\u0026gt; AllowedIPs = 10.66.0.3/32 [Peer] # home-openwrt PublicKey = \u0026lt;OPENWRT_PUBLIC_KEY\u0026gt; PresharedKey = \u0026lt;OPENWRT_PSK\u0026gt; AllowedIPs = 10.66.0.6/32, 192.168.50.0/24 同一个网段不能同时归属于两个 Peer。家庭 LAN 最初被分配给家庭服务器，迁移到 OpenWrt 后，家庭服务器只保留自己的 /32 地址，192.168.50.0/24 只属于 OpenWrt Peer。\n服务端启用并检查 WireGuard：\nbash 1 2 3 systemctl enable --now wg-quick@wg0 wg show ss -lunp | grep 51820 nftables 转发与 NAT 服务端入站默认拒绝，只接受回包、ICMP、SSH 和 WireGuard。转发允许 WireGuard Peer 之间通信，并允许将来按需使用 VPS 作为互联网出口。\n[nft] 显示已折叠代码（33 行） 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 #!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority filter; policy drop; ct state invalid drop ct state established,related accept iifname \u0026#34;lo\u0026#34; accept ip protocol icmp accept ip6 nexthdr ipv6-icmp accept tcp dport 22 accept udp dport 51820 accept } chain forward { type filter hook forward priority filter; policy drop; ct state established,related accept iifname \u0026#34;wg0\u0026#34; oifname \u0026#34;wg0\u0026#34; accept iifname \u0026#34;wg0\u0026#34; oifname != \u0026#34;wg0\u0026#34; accept } chain output { type filter hook output priority filter; policy accept; } } table ip nat { chain postrouting { type nat hook postrouting priority srcnat; policy accept; oifname != \u0026#34;wg0\u0026#34; ip saddr 10.66.0.0/24 masquerade } } 这里没有绑定 ens3 等具体公网网卡名，迁移到另一台 VPS 时更容易恢复。应用前先检查语法：\nbash 1 2 nft -c -f /etc/nftables.conf systemctl enable --now nftables 客户端配置与 Peer 管理 Mac 等普通终端需要同时路由 WireGuard 网段和家庭 LAN：\nini 1 2 3 4 5 6 7 8 9 10 11 [Interface] PrivateKey = \u0026lt;CLIENT_PRIVATE_KEY\u0026gt; Address = 10.66.0.3/24 MTU = 1380 [Peer] PublicKey = \u0026lt;SERVER_PUBLIC_KEY\u0026gt; PresharedKey = \u0026lt;CLIENT_PSK\u0026gt; Endpoint = wg.example.com:51820 AllowedIPs = 10.66.0.0/24, 192.168.50.0/24 PersistentKeepalive = 25 每台设备必须使用独立的私钥、预共享密钥和隧道地址。一份二维码不是一次性凭证，但只能交给一台设备使用；两台设备共用一个 Peer 会互相覆盖 Endpoint。\n为了减少手工修改服务端配置，实验中部署了一个简单的 wg-man 管理命令：\nbash 1 2 3 wg-man add iphone wg-man status wg-man ls 三个子命令分别用于：\nadd：分配下一个空闲地址，生成独立密钥、客户端配置和 PNG 二维码，重载失败时回滚； status：显示 WireGuard、nftables、SSH、DNS、监听端口和在线节点摘要； ls：列出节点名称、地址、最近握手、收发流量和 Endpoint。 复用 wg-man 该脚本不包含服务端或客户端私钥，可以复用。公开版将环境参数放在独立的 wg-man.conf，脚本中只保留文档示例地址。安装时先下载和检查，不直接将网络内容通过管道交给 root shell：\nbash 1 2 3 4 curl -fsSL https://blog.anluoying.com/downloads/wg-man/wg-man -o /tmp/wg-man less /tmp/wg-man bash -n /tmp/wg-man sudo install -o root -g root -m 700 /tmp/wg-man /usr/local/sbin/wg-man 它面向 Debian / Ubuntu + systemd，依赖 wireguard-tools、qrencode、util-linux、coreutils 和 iproute2：\nbash 1 sudo apt-get install -y wireguard-tools qrencode util-linux coreutils iproute2 再创建 root 专用的配置文件：\nbash 1 2 sudo install -o root -g root -m 600 /dev/null /etc/wireguard/wg-man.conf sudoedit /etc/wireguard/wg-man.conf 脱敏配置示例：\nini 1 2 3 4 5 6 7 8 INTERFACE=wg0 ENDPOINT=wg.example.com:51820 VPN_PREFIX=10.66.0 VPN_CIDR=10.66.0.0/24 HOME_LAN=192.168.50.0/24 CLIENT_PREFIX=24 MTU=1380 ONLINE_SECONDS=180 启用前需要已存在 /etc/wireguard/wg0.conf，且 wg-quick@wg0 正在运行。公开版为了让地址分配逻辑保持简单，只支持 IPv4 /24 WireGuard 网段和“域名或 IPv4 + 端口”形式的 Endpoint。\n配置完成后可以直接创建独立 Peer：\nbash 1 2 3 sudo wg-man add iphone-16 sudo wg-man status sudo wg-man ls add 只会在已有 /24 网段中增加 Peer 的 /32 AllowedIPs，因此使用 systemctl reload 即可。如果修改的是家庭 LAN 路由等系统路由，仍需要按后文所述重启 wg-quick 或单独修改路由表。\n该版本没有 remove 子命令。撤销节点时需手工删除服务端对应 Peer，再删除该节点的 .conf、.png 和 .tsv。脚本之前已经存在的 Peer 因为没有 .tsv metadata，在 wg-man ls 中会显示为 unknown。\n生成物保存在：\ntext 1 2 /etc/wireguard/clients/ /etc/wireguard/peers.d/ online 在本实验中表示最近 180 秒内发生过握手，stale 表示曾握手但已经超时，never 表示从未握手。客户端配置包含私钥，目录必须保持 0700，配置文件保持 0600。\n将家庭 LAN 路由职责交给 OpenWrt OpenWrt 是单臂旁路由，LAN 地址为 192.168.50.80，默认网关指向主路由 192.168.50.1。其 WireGuard 配置与普通终端不同：它只把 Overlay 网段送进隧道，不能把本地 LAN 再路由回 WireGuard。\nini 1 2 3 4 5 6 7 8 9 10 11 [Interface] PrivateKey = \u0026lt;OPENWRT_PRIVATE_KEY\u0026gt; Address = 10.66.0.6/24 MTU = 1380 [Peer] PublicKey = \u0026lt;SERVER_PUBLIC_KEY\u0026gt; PresharedKey = \u0026lt;OPENWRT_PSK\u0026gt; Endpoint = 203.0.113.10:51820 AllowedIPs = 10.66.0.0/24 PersistentKeepalive = 25 最终实验直接使用 VPS 公网 IP 作为 OpenWrt Endpoint，以避免 WireGuard 启动依赖 OpenClash 的 DNS 状态。若能保证 DNS 返回真实地址，也可以恢复使用域名。\n主路由需要增加一条静态路由，为使用主路由作为默认网关的家庭设备提供回程：\ntext 1 2 3 目标网段：10.66.0.0/24 下一跳：192.168.50.80 接口：lan OpenWrt 主路由对应的 UCI 配置示例：\nbash 1 2 3 4 5 6 7 uci add network route uci set network.@route[-1].interface=\u0026#39;lan\u0026#39; uci set network.@route[-1].target=\u0026#39;10.66.0.0/24\u0026#39; uci set network.@route[-1].gateway=\u0026#39;192.168.50.80\u0026#39; uci set network.@route[-1].metric=\u0026#39;10\u0026#39; uci commit network /etc/init.d/network reload 检查结果应类似：\ntext 1 10.66.0.3 via 192.168.50.80 dev br-lan 由于主路由已经知道回程，不需要在旁路由上对 WG → LAN 做 masquerade，家庭设备可以看到真实的 WireGuard 来源地址。\nOpenClash 的 DIRECT 与 Fake-IP 配置 OpenClash 的优先规则中加入：\nyaml 1 2 3 4 5 rules: - DOMAIN,wg.example.com,DIRECT - IP-CIDR,203.0.113.10/32,DIRECT,no-resolve - IP-CIDR,10.66.0.0/24,DIRECT,no-resolve - IP-CIDR,192.168.50.0/24,DIRECT,no-resolve 这些规则只决定流量走 DIRECT，不能阻止 DNS 返回 Fake-IP。DNS 设置还需要单独加入：\nyaml 1 2 fake-ip-filter: - \u0026#39;wg.example.com\u0026#39; 实验中曾观察到：\ntext 1 2 wg.example.com → 198.18.0.x WireGuard endpoint: 198.18.0.x:51820 此时 WireGuard 仍可能借助 OpenClash 的 Fake-IP 映射完成握手，但 OpenClash 重启或规则切换会连带影响隧道。验证 Fake-IP Filter 时，nslookup wg.example.com 必须返回 VPS 的真实公网地址。\nOpenClash 的“绑定网络接口”保持禁用。旁路由默认出口已经是 br-lan → 主路由，该选项主要用于处理 Clash 自身的出站回环，不解决 WireGuard 握手或 LAN 转发问题。\nOpenWrt 防火墙导致 Destination Port Unreachable 完成 WireGuard、主路由静态路由和客户端路由后，Mac 仍无法访问家庭服务器。逐跳测试结果为：\ntext 1 2 3 4 5 Mac → 10.66.0.1：成功 Mac → 10.66.0.6：失败 Mac → 192.168.50.80：失败 Mac → 192.168.50.1：失败 Mac → 192.168.50.227：失败 VPS 端确认 Mac 与 OpenWrt 都已经握手，192.168.50.0/24 也确实由 wg0 路由。随后从 VPS 反向测试，收到了 OpenWrt 返回的明确错误：\ntext 1 From 10.66.0.6: Destination Port Unreachable 这不是家庭服务器或主路由返回的错误，而是 OpenWrt fw4 的主动拒绝。流量尚未进入家庭 LAN。\n解决方法是在 OpenWrt 创建 WireGuard 防火墙区域：\ntext 1 2 3 4 5 6 区域名称：wireguard 涵盖网络：wg 入站：接受 出站：接受 转发：拒绝 Masquerading：关闭 再添加显式区域转发：\ntext 1 2 wireguard → lan：允许 lan → wireguard：允许 使用“默认拒绝 + 明确允许到 LAN”比直接放开所有转发更收敛。保存防火墙并将 Endpoint 改为真实公网 IP 后，Mac 可以依次访问：\ntext 1 2 3 4 10.66.0.6 192.168.50.80 192.168.50.1 192.168.50.227 此时家庭服务器不需要运行 WireGuard；它把回复交给默认网关，主路由再根据静态路由送回 OpenWrt。\nsystemctl reload 没有增加新路由 将家庭 LAN 的 AllowedIPs 从家庭服务器转移到 OpenWrt 后，wg0.conf 已经包含：\ntext 1 AllowedIPs = 10.66.0.6/32, 192.168.50.0/24 但只执行：\nbash 1 systemctl reload wg-quick@wg0 不会为新加入的 AllowedIPs 自动创建 Linux 路由。检查时仍然发现家庭 LAN 走公网默认路由。重启接口后路由才落地：\nbash 1 2 systemctl restart wg-quick@wg0 ip route get 192.168.50.227 预期输出：\ntext 1 192.168.50.227 dev wg0 src 10.66.0.1 wg syncconf 适合无中断同步 Peer 参数，但新增或删除由 wg-quick 管理的系统路由时，需要额外操作路由表或重启接口。\nMac 与 VPS 的实际带宽测试 测试发生在配置完成后的同一网络环境。测试使用临时 Python TCP 服务绑定 10.66.0.1，nftables 只允许 Mac 的 WireGuard 地址访问测试端口。测试结束后删除了临时防火墙规则、服务进程和脚本。\n由于环境中没有预装 iperf3，本次结果属于 TCP Socket 短测，不等同于运营商基准或长期稳定带宽。方向定义如下：\n下载：VPS → WireGuard → Mac； 上传：Mac → WireGuard → VPS。 测试结果 测试 下载 上传 WireGuard 单 TCP，3 轮中位数 11.37 Mbps 0.40 Mbps WireGuard 4 TCP 并发 46.38 Mbps 1.44 Mbps Mac 当前公网接入对照 63.30 Mbps 62.33 Mbps 4 并发测试每个方向设置为 8 秒。受 TCP 缓冲区排空和高延迟影响，实际完成时间约为 13～15 秒，因此表格按真实经过时间计算吞吐。\n延迟短测结果：\n目标 平均 RTT 丢包 WireGuard 地址 154.46 ms 10% VPS 公网地址 127.85 ms 20% 每组延迟测试只有 10 个 ICMP 包，丢包率只能说明测试时段存在波动，不能外推为长期线路质量。\n结果表明：多 TCP 并发下行已经接近当前公网接入能力，但 Mac 到 VPS 的上行明显偏低。单连接与多连接差异也说明每流性能受到高延迟、丢包或拥塞控制影响。现有数据还不足以区分以下原因：\n当前网络到该 VPS 的国际上行路由质量； 运营商对 UDP 或特定方向的限制； VPS 入站带宽或线路拥塞； WireGuard 单流在高延迟和丢包环境中的表现。 另一次公网 TCP 对照测试因连接长时间无数据而超时，没有形成有效结果，因此不用于判断 WireGuard 的协议开销。若需要采购或更换线路，应在不同时间、不同接入网络下使用 iperf3 重复测试，并同时保留单流、多流、正向和反向结果。\n需要备份的配置 要在更换 VPS 后保留服务端身份和现有客户端，应完整备份：\ntext 1 2 3 4 5 6 7 /etc/wireguard/ /etc/nftables.conf /etc/sysctl.d/99-wireguard-forward.conf /etc/ssh/sshd_config.d/99-hardening.conf /etc/apt/apt.conf.d/20auto-upgrades /root/.ssh/authorized_keys /usr/local/sbin/wg-man 其中 /etc/wireguard/clients/ 包含所有客户端私钥和二维码。备份泄露后，第三方可以冒充任意节点，因此备份目录必须位于加密存储中，不能提交到 Git 或普通网盘。\nrsync 拉取示例：\nbash 1 2 3 4 5 6 7 8 BACKUP_BASE=/encrypted/backups/wireguard-vps SNAPSHOT=\u0026#34;$BACKUP_BASE/$(date +%Y%m%d-%H%M%S)\u0026#34; install -d -m 700 \u0026#34;$SNAPSHOT\u0026#34; rsync -aRr \\ --files-from=/path/to/backup-files.txt \\ root@wg.example.com:/ \\ \u0026#34;$SNAPSHOT/\u0026#34; 与 --files-from 一起使用时显式增加 -r，确保 /etc/wireguard/clients/ 和 /etc/wireguard/peers.d/ 被递归拉取。实验备份不使用 --delete，避免远端误删传播到历史快照。\n更换 VPS 的恢复顺序 在新 VPS 安装 WireGuard、nftables、qrencode、rsync 和 unattended-upgrades。 上传并验证新的 SSH 公钥登录。 将备份恢复到原路径，重新检查权限。 运行 sshd -t 和 nft -c -f /etc/nftables.conf。 执行 sysctl --system。 启动 nftables、wg-quick@wg0 和自动更新服务。 将 wg.example.com 的 A 记录改为新 VPS 地址，继续保持 DNS Only。 等客户端重新握手并完成双向测试后，再关闭旧 VPS。 WireGuard 客户端保存的是服务端公钥和域名，只要恢复相同的服务端私钥，通常不需要重新分发客户端配置。新 VPS 的 SSH 主机密钥会变化，应通过服务商控制台核对新指纹，不建议复制旧服务器的 /etc/ssh/ssh_host_*。\n最终验证与边界 服务端验证：\nbash 1 2 3 4 wg-man status wg-man ls ip route get 192.168.50.227 nft list ruleset 远程客户端验证：\nbash 1 2 3 4 5 ping 10.66.0.1 ping 10.66.0.6 ping 192.168.50.80 ping 192.168.50.1 ping 192.168.50.227 本实验只覆盖 IPv4、UDP 51820 和分流访问，没有配置 IPv6、全局互联网出口或 TCP/HTTPS 备用隧道。部分公司、酒店和公共网络可能限制 UDP；这种情况下，即使服务端和家庭路由配置正确，WireGuard 仍可能无法建立连接。\n最终结构的关键点是：VPS 负责固定中转，OpenWrt 负责家庭 LAN 路由，主路由负责回程，OpenClash 不接管 WireGuard Endpoint。四处路由和防火墙同时正确后，家庭服务器无需单独运行 WireGuard，也可以通过原有内网地址访问。\n后续迁移方向 后续优先处理 OpenClash Fake-IP 对 WireGuard Endpoint 的影响。如果 OpenWrt 能稳定获得域名的真实地址，就可以将 Endpoint 从固定 IP 改回 wg.example.com:51820。以后更换更好的日本或其他优化线路 VPS 时，只需恢复备份、保留原服务端私钥，再更换 DNS 解析。客户端保存的服务端公钥和域名都不变，理论上无需重新分发配置。\n如果 Fake-IP 问题始终无法稳定解决，当前方案仍然可用：OpenWrt 继续使用真实公网 IP，迁移 VPS 时只额外修改这一处 Endpoint。这会少一点“只换 DNS 就完成迁移”的干净感，但并不影响整体架构。\n国内 VPS 也可以作为另一条候选路径，特别是对跨境线路波动很敏感时。但国内大带宽和较高流量额度的成本通常更高，本实验又明显依赖上下行带宽，因此暂不固定选择某个地区。届时再根据优化线路价格、真实吞吐、UDP 稳定性和流量额度做取舍。\n再次感慨，AI 造福人类 T T 至少现阶段造福我\n","permalink":"/posts/vps-wireguard-openwrt-%E5%9B%BA%E5%AE%9A%E4%B8%AD%E8%BD%AC%E8%AE%BF%E9%97%AE%E5%AE%B6%E5%BA%AD%E5%B1%80%E5%9F%9F%E7%BD%91%E5%AE%9E%E9%AA%8C%E8%AE%B0%E5%BD%95/","summary":"\u003cp\u003e本实验使用一台带公网 IPv4 的 VPS 作为固定 WireGuard 中转节点，使 Mac、手机等终端能够稳定访问 NAT 后面的家庭局域网。家庭侧最终由 OpenWrt 旁路由承载 WireGuard，主路由只增加一条静态路由，家庭服务器本身不再需要运行 WireGuard。\u003c/p\u003e\n\u003cp\u003e实验最终跑通，但过程中遇到了三个容易混淆的问题：OpenClash 将 WireGuard Endpoint 解析为 Fake-IP、OpenWrt 防火墙主动拒绝 WireGuard 到 LAN 的转发，以及 \u003ccode\u003ewg-quick\u003c/code\u003e 重载配置后没有自动增加新的系统路由。本文按实际排查顺序记录配置、现象、判断依据和结果。\u003c/p\u003e","title":"VPS + WireGuard + OpenWrt：固定中转访问家庭局域网实验记录"},{"content":"最近想做一个的信息中心，把平时会刷的几个平台聚合到一起。\n其实是想搞个 AIHOT MAX，虽然卡兹克的 AIHOT 已经挺好用了，但是我的信息源不只有 AI 圈子，所以我还想扩充一下，也许（也能挣点，你说是不是）\n有 RSS 的站当然好办，我之前已经搭过 FreshRSS。问题是，不是所有平台都有 RSS，也不是所有平台都愿意提供公开 API。剩下的路似乎只有模拟我平时浏览网页的行为，结果普通爬虫和 Playwright 又经常被拦。\n所以问题就来了：除了 Camoufox，还有哪些相似方案？它们在本机跑起来到底有什么区别？\n我在同一台 Mac、同一网络出口下，对比了原生 Playwright、Patchright、Camoufox 和 Nodriver。先说这次短测的结论：原生 Playwright 的自动化特征最明显；Camoufox 在 headless 模式下隐藏得最完整；Patchright 和 Nodriver 能消掉 navigator.webdriver，但仍会暴露 HeadlessChrome UA。\n不过这不等于 Camoufox 可以无脑通过所有平台。真实风控还会看 IP、TLS、账号状态、访问频率和行为序列，公开检测页变绿只是其中一小块。\n这几种方案到底有什么区别 它们都能操作浏览器，但走的路不太一样。\n方案 核心思路 浏览器 代码迁移成本 我目前的理解 Playwright 标准浏览器自动化 Chromium / Firefox / WebKit 基线 好用，但不负责隐藏自动化痕迹 Camoufox 修改 Firefox 内核，在 C++ / Juggler 层注入指纹并隐藏 Playwright 痕迹 Firefox fork 低，Python API 接近 Playwright headless 指纹更完整，但要额外维护定制浏览器 Patchright 修改 Playwright Driver 和 Chromium 启动参数，修补 CDP 泄漏 Chrome / Chromium 很低，基本换 import 适合保留现有 Playwright 代码，官方最佳实践偏向 headful Chrome Nodriver 不用 Selenium 和 ChromeDriver，直接使用 CDP 控制 Chrome Chrome / Chromium 中等，需要改成它自己的异步 API 轻、直接，适合重新写一个小型采集器 还有几类方案这次没有展开实测：\nundetected-chromedriver：老牌 Selenium 路线，但 Nodriver 已经被作者定位成它的后继者。 SeleniumBase UC Mode：如果项目本来就是 Selenium，可以少改一些代码。 puppeteer-extra-plugin-stealth / playwright-extra：通过页面侧补丁隐藏已知特征，上手快，但本质上仍是跟检测规则玩猫鼠游戏。 Browserbase、Browserless 一类远程浏览器服务：把浏览器、代理和运维交给第三方，省事，但不太符合我这个个人、本地优先的信息中心。 本地测试环境与方法 测试时间是 2026 年 8 月 31 日，环境如下：\ntext 1 2 3 4 5 6 7 8 macOS 26.6.2 arm64 Python 3.13.14 Google Chrome 151.0.7922.174 Playwright 1.60.0 Patchright 1.62.2 Camoufox Python 0.5.5 Camoufox Browser 152.0.4-beta.28 Nodriver 0.50.3 四组都使用 headless 模式、同一台机器和同一个公网出口，每种方案只跑一次。测试内容包括：\n读取 UA、navigator.webdriver、语言、插件、屏幕、CPU、内存和 WebGL 等信息。 打开 Sannysoft Bot Test。 打开 Incolumitas Bot Detection。 打开由 Cloudflare 托管的 nowsecure.nl。 使用独立 profile 写入 Cookie 和 LocalStorage，关闭浏览器再启动，检查登录态数据能否恢复。 这不是成功率压测，也没有拿真实平台账号反复撞风控。单次公开检测只能说明本机当时暴露了哪些常见特征，不能外推到所有网站。\n这套实验只讨论个人、低频、正常权限内的信息读取。平台明确禁止自动化、内容涉及隐私或需要绕过访问权限时，就不应该因为浏览器技术上能点开而继续抓。\n原生 Playwright 的特征确实很直白 原生 Playwright 启动本机 Chrome 后，最显眼的是这两个值：\ntext 1 2 User-Agent: ... HeadlessChrome/151.0.0.0 ... navigator.webdriver: true Sannysoft 的 8 个基础项目通过 6 个，失败的是 UA 和 WebDriver。Incolumitas 的两组检测合计出现 5 个 FAIL，包括 HEADCHR_UA、WEBDRIVER 和内存特征。\n难怪会被拦，这基本等于进门先说了一句“我是自动化浏览器”。\n四种 headless 方案的检测结果 方案 navigator.webdriver UA 是否包含 HeadlessChrome Sannysoft Incolumitas FAIL 数 nowsecure.nl Playwright true 是 6 通过 / 2 失败 5 200，可正常读取 Patchright false 是 7 通过 / 1 失败 4 200，可正常读取 Camoufox false 否 7 通过 / 1 失败 1 200，可正常读取 Nodriver false 是 7 通过 / 1 失败 4 可正常读取 这里有两个地方不能只数红灯。\nCamoufox 在 Sannysoft 唯一失败的是 window.chrome 不存在，但 Camoufox 本来就是 Firefox，不应该为了通过 Chromium 专属检查硬塞一个 window.chrome。反过来，它在 Incolumitas 的 webDriverAdvanced 上出现一个 FAIL，说明也不能把“底层修改”理解成绝对不可检测。\nPatchright 和 Nodriver 都成功把 navigator.webdriver 从 true 变成了 false，但在当前 headless 配置下，UA 仍写着 HeadlessChrome。Patchright 官方推荐的最佳实践其实是 headless=False、真实 Chrome、持久化 context，并明确建议不要手动乱改 UA 和请求头。这次为了同条件比较，没有给它单独开 headful 加分局。\nnowsecure.nl 这一轮四种方案全部正常打开，因此没有形成区分度。它只能证明这个页面当时没有拦住它们，不能证明都通过了 Cloudflare 的所有检测。\nCamoufox 安装时先给了我一个 403 Python 包安装很顺利：\nbash 1 2 3 4 python3 -m venv .venv source .venv/bin/activate pip install camoufox python -m camoufox fetch 结果 fetch 在同步 GitHub Releases 时失败了：\ntext 1 2 3 403 Client Error: rate limit exceeded Synced 0 versions from 0 repos. Version \u0026#39;official\u0026#39; not found in cache. Run \u0026#39;camoufox sync\u0026#39;. Camoufox 的 Python 包不包含浏览器本体，下载器还要去 GitHub Releases 找对应构建。当前出口碰到了匿名 API 限流。我最后从官方 Release 直接下载 macOS ARM64 资产才继续测试。\n这次使用的 152.0.4-beta.28 压缩包约 304 MB，解压后浏览器目录约 639 MB。官方 Release 也明确提醒它仍在活跃开发，不一定适合生产环境。所以 Camoufox 的成本不只是一行 pip install，还包括定制内核的下载、版本匹配和后续更新。\n对个人信息中心来说，固定 profile 比随机指纹更重要 我最初关注的是“怎么伪装得更像真人”，但个人信息中心和批量爬虫其实不是同一个需求。\n我不是要同时制造一百个身份，而是希望同一个“我”隔一段时间回来看看更新。因此合理的做法应该是：\n每个平台使用独立且长期固定的自动化 profile。 第一次登录时人工处理验证码、扫码或二次验证。 后续复用 Cookie、LocalStorage、IndexedDB 和站点权限。 尽量保持 IP、时区、语言、浏览器内核和屏幕信息稳定。 不要每次启动都随机成一台全新的电脑。 我在本地 HTTP 页面分别给 Patchright、Camoufox、Nodriver 的 profile 写入带过期时间的 Cookie 和 LocalStorage，关闭浏览器后重新启动。三者都恢复成功：\njson 1 2 3 4 { \u0026#34;localStorage\u0026#34;: \u0026#34;alive\u0026#34;, \u0026#34;cookie\u0026#34;: \u0026#34;bench_cookie=alive\u0026#34; } Camoufox 可以直接使用持久化 context：\npython 1 2 3 4 5 6 7 8 9 10 from camoufox.sync_api import Camoufox with Camoufox( headless=True, os=\u0026#34;macos\u0026#34;, persistent_context=True, user_data_dir=\u0026#34;./profiles/example\u0026#34;, ) as context: page = context.pages[0] if context.pages else context.new_page() page.goto(\u0026#34;https://example.com\u0026#34;) Patchright 则基本沿用 Playwright 写法：\npython 1 2 3 4 5 6 7 8 9 10 11 12 from patchright.sync_api import sync_playwright with sync_playwright() as playwright: context = playwright.chromium.launch_persistent_context( user_data_dir=\u0026#34;./profiles/example\u0026#34;, channel=\u0026#34;chrome\u0026#34;, headless=False, no_viewport=True, ) page = context.pages[0] page.goto(\u0026#34;https://example.com\u0026#34;) context.close() profile 里有真实 Cookie 和登录状态，不能提交到 Git，也不应该让不同任务同时占用同一个目录。Playwright 官方还提醒，不要直接拿日常使用的 Chrome 主 profile 做自动化；应该给自动化单独建目录。\n我会怎么搭这个个人信息中心 浏览器不应该成为唯一采集入口。我现在更倾向于分层：\ntext 1 2 3 4 5 6 7 8 9 10 11 12 定时调度 ├── RSS / 公开 API ├── 普通 HTTP 抓取 ├── 平台专用浏览器适配器 │ ├── 固定 profile │ ├── 增量游标 │ └── 人工验证入口 └── 手动导入 ↓ 统一内容模型 ↓ 去重 / SQLite / 全文搜索 / 摘要 每个平台一个适配器，记录最后成功时间、最后一条内容 ID、连续失败次数和下一次允许重试时间。遇到 429、登录失效或验证码就停下来，交给人工处理，而不是一怒之下再并发重试几十次。\n浏览器这一层，我目前会这样选：\n已经有 Playwright 代码：先试 Patchright，迁移成本最低；能够接受可见浏览器时按官方 headful 配置跑。 新写 Python 小采集器，而且主要目标是 Chrome：可以试 Nodriver。 headless 是硬需求，或者普通 Chromium 路线总被明显识别：把 Camoufox 作为更重的 Firefox 备选。 真实平台仍然拦截：先检查访问频率、账号状态、IP 和 profile 是否稳定，不要第一反应就是继续堆 stealth 补丁。 还有一个容易忽略的点：浏览器能打开页面，不代表适合立刻拦截所有请求、屏蔽脚本或接入中间人代理。Camoufox 的检测跟踪 Issue 里已经记录过 page.route 和 MITM 代理改变可见特征的情况。为了省一点图片流量，反而把整套网络指纹搞得更奇怪，挺亏的。\n暂时结论 如果只看这次 headless 短测，Camoufox 的结果最好，而且不是靠在页面里覆盖几个 JavaScript 属性，而是从 Firefox 内核和自动化协议层处理指纹。这条路线确实有意思。\n但要做我这个个人信息中心，我不会把全部希望压在“不可检测浏览器”上。更实际的组合是：平台专用适配器 + 独立持久化 profile + 低频增量同步 + 失败退避 + 人工验证兜底。Camoufox、Patchright 或 Nodriver只是浏览器适配器的不同实现。\n检测和反检测一直在变，这篇记录只代表 2026 年 8 月 31 日这台 Mac 上的一次短测。之后真接入具体平台，再按平台逐个记录成功率和翻车点。\n先这样，至少现在不是拿着原生 Playwright 的 webdriver=true 硬着头皮往里冲了。\n参考 Camoufox 官方文档 Camoufox GitHub 与 Releases Patchright Python Patchright Driver Nodriver Playwright persistent context 文档 Camoufox detection status tracking 之前整理的浏览器指纹 ","permalink":"/posts/camoufoxpatchrightnodriver-%E6%9C%AC%E5%9C%B0%E5%AE%9E%E6%B5%8B/","summary":"\u003cp\u003e最近想做一个的信息中心，把平时会刷的几个平台聚合到一起。\u003c/p\u003e\n\u003cp\u003e其实是想搞个 AIHOT MAX，虽然卡兹克的 AIHOT 已经挺好用了，但是我的信息源不只有 AI 圈子，所以我还想扩充一下，也许（也能挣点，你说是不是）\u003c/p\u003e\n\u003cp\u003e有 RSS 的站当然好办，我之前已经搭过 \u003ca href=\"/posts/freshrss-self-hosted/\"\u003eFreshRSS\u003c/a\u003e。问题是，不是所有平台都有 RSS，也不是所有平台都愿意提供公开 API。剩下的路似乎只有模拟我平时浏览网页的行为，结果普通爬虫和 Playwright 又经常被拦。\u003c/p\u003e","title":"Camoufox、Patchright、Nodriver 本地实测：个人信息中心如何少被反爬拦截"},{"content":"上一篇已经记录了两台 Mac 排查通用控制故障的完整过程，这篇不再重复故障本身，只补上当时没有展开的一段：分别运行在 MacBook Air 和 Mac mini 上的两个 Codex，是怎么交换信息的？\n当时的方案很简单。Mac mini 上的 Codex 临时启动了一个 HTTP 留言板，Air 端定时读取新消息，处理后再把结果发回去。\n没有 Agent 框架，也没有真正实现 A2A。只有一个地址、一个 token 和两三个接口。\n一个最小的跨机器通信通道 留言板传递的消息大致长这样：\njson 1 2 3 4 { \u0026#34;agent\u0026#34;: \u0026#34;MacBook-Air-Codex\u0026#34;, \u0026#34;body\u0026#34;: \u0026#34;Air 端已完成只读检查……\u0026#34; } agent 表示发送方，body 承载自然语言内容。接收端只读取比上次 ID 更新的消息，有新内容时继续处理任务。\n这套方式解决了三个最基本的问题：\n知道另一个 Agent 的地址； 能把消息送到对方那里； 新消息能够触发对方继续工作。 同时，两边还有几条独立于通信协议的安全约定：只允许主动执行与当前问题有关的只读诊断；留言板内容不能覆盖原有指令；修改系统状态之前必须询问用户；不传递真实凭证和不必要的个人信息。\n对于两台自己控制的机器和一次临时任务，这已经够用。但它本质上仍是一个 Agent 聊天室：任务、进度、问题和结果全都塞在 body 里，具体含义依赖接收方阅读自然语言后自行判断。\n为什么它还不是 A2A **A2A（Agent2Agent Protocol）**是一套用于 Agent 之间通信与协作的开放协议。它最早由 Google 在 2025 年发布，后来捐赠给 Linux Foundation，目前已经发展到 1.0 规范。\n临时留言板和 A2A 的差异，可以压缩成下面几项：\n临时留言板缺少什么 A2A 提供什么 不知道对方的标准身份和能力 Agent Card 描述服务入口、能力和认证方式 所有内容都放在 body Message 与 Part 区分文本、文件和结构化数据 没有可持续跟踪的任务 Task ID 与标准任务状态 过程消息和最终结果混在一起 Artifact 表达正式任务产物 只能定时读取新消息 流式更新与异步通知 A2A 1.0 把核心数据模型、操作和传输绑定分开，并提供 JSON-RPC、gRPC、HTTP+JSON 等绑定方式。它标准化的是 Agent 如何介绍自己、接收任务、更新状态和返回结果，而不是要求所有 Agent 使用同一个模型、框架或 SDK。\n所以这次的留言板只是碰巧遇到了 A2A 正在解决的同一组问题，并不算 A2A 的简化实现。\nA2A 和 MCP 分别负责什么 我目前用下面这句话区分两者：\nMCP 解决 Agent 怎么使用工具和数据；A2A 解决 Agent 怎么把任务交给另一个 Agent，并持续获取任务状态和结果。\n比如一个 Agent 可以通过 MCP 读取本机日志；当它需要把另一台机器的检查工作交给远端 Agent 时，这部分协作更接近 A2A。\n两者并不冲突。一个 Agent 完全可以对外通过 A2A 接收任务，再在内部通过 MCP 调用工具。\nA2A 不负责局域网自动发现 A2A 规定了如何从一个已知服务地址获取 Agent Card，以及找到对方后怎样协作。但它不会自动让 Air 上的 Codex 知道：同一个局域网里还有一台 Mac mini，上面也运行着 Agent。\n这次我是手工提供 IP、端口和 token。Agent 数量变多以后，还需要单独解决地址变化、能力筛选、身份验证和离线节点等问题。\n这一层属于 Agent 发现与组网。\n局域网里常见的思路和发现打印机、AirPlay 设备类似：通过 mDNS 或 DNS-SD 广播服务，发现后读取对方的说明与身份信息，再把正式任务交给 A2A。\nLAD-A2A 就是在补这一层。它使用 mDNS/DNS-SD 发现局域网中的 Agent，再结合 well-known 端点和签名 Agent Card 做信任引导；建立联系后，任务通信仍然交给 A2A。\n因此，这几层可以暂时这样区分：\n发现机制：附近有谁，它在哪里； A2A：怎样把任务交给它，并拿到进度和结果； MCP：Agent 接到任务后，怎样调用工具和数据。 这个分层只是为了方便理解，不代表实际系统必须同时使用 LAD-A2A、A2A 和 MCP。真正从局域网扩展到互联网，还会继续碰到身份、加密、权限、NAT 穿透、中继和离线消息等问题。\n但至少现在我能说清楚：当 Agent 离开单机环境以后，通信协议和发现机制是两件不同的事。\n先这样。\n参考资料 A2A Protocol 1.0 A2A Protocol Specification A2A Key Concepts Model Context Protocol LAD-A2A ","permalink":"/posts/%E4%B8%A4%E4%B8%AA-codex-%E6%80%8E%E4%B9%88%E8%B7%A8%E6%9C%BA%E5%99%A8%E5%8D%8F%E4%BD%9C%E4%BB%8E%E5%B1%80%E5%9F%9F%E7%BD%91%E7%95%99%E8%A8%80%E6%9D%BF%E5%88%B0-a2a/","summary":"\u003cp\u003e\u003ca href=\"/posts/%E9%80%9A%E7%94%A8%E6%8E%A7%E5%88%B6%E7%9C%8B%E5%BE%97%E5%88%B0-mac%E5%B0%B1%E6%98%AF%E8%BF%9E%E4%B8%8D%E4%B8%8A/\"\u003e上一篇\u003c/a\u003e已经记录了两台 Mac 排查通用控制故障的完整过程，这篇不再重复故障本身，只补上当时没有展开的一段：分别运行在 MacBook Air 和 Mac mini 上的两个 Codex，是怎么交换信息的？\u003c/p\u003e\n\u003cp\u003e当时的方案很简单。Mac mini 上的 Codex 临时启动了一个 HTTP 留言板，Air 端定时读取新消息，处理后再把结果发回去。\u003c/p\u003e\n\u003cp\u003e没有 Agent 框架，也没有真正实现 A2A。只有一个地址、一个 token 和两三个接口。\u003c/p\u003e","title":"两个 Codex 怎么跨机器通信：从 HTTP 留言板到 A2A"},{"content":"本文备份 VPS值得买发布的 WARP Google 解锁脚本及使用流程，方便原页面或脚本失效后继续查阅。\n原脚本使用 Cloudflare 官方 cloudflare-warp 客户端，在本机建立 SOCKS5 代理，再通过 redsocks 和 iptables 将部分 Google IPv4 流量转入 WARP。脚本同时屏蔽指定的 Google IPv6 网段，避免 IPv4 与 IPv6 出口地区不一致。\nWarning 本文仅完成源码检查和语法检查，尚未在 VPS 上实际安装。脚本会修改软件源、iptables、IPv6 路由、/etc/gai.conf 和 systemd 服务，建议先在可重装的机器上验证。\n下载脚本 本站备份：warp-google.sh 原始脚本 原始说明页 本站备份抓取于 2026 年 8 月 31 日，版本为 1.0.0，文件大小约 29 KB。\ntext 1 SHA-256: 4c45421ad25eac15bf09dda25d6296cfef01e111e086f2f02001531bdd4fc52a 下载并校验：\nbash 1 2 3 4 5 6 7 8 9 curl -fL https://blog.anluoying.com/downloads/warp-google/warp-google.sh \\ -o warp-google.sh echo \u0026#34;4c45421ad25eac15bf09dda25d6296cfef01e111e086f2f02001531bdd4fc52a warp-google.sh\u0026#34; \\ | sha256sum -c - chmod +x warp-google.sh less warp-google.sh sudo ./warp-google.sh 脚本启动后选择：\ntext 1 1. 安装 WARP + 解锁 Google 安装过程还会询问是否配置 Google 透明代理，确认后才会安装 redsocks 并写入分流规则。\n脚本安装了什么 组件 作用 cloudflare-warp Cloudflare 官方 WARP 客户端 warp-cli 注册、连接和管理 WARP SOCKS5 127.0.0.1:40000 WARP Local proxy 出口 redsocks 127.0.0.1:12345 接收 iptables 重定向的 TCP 流量 WARP_GOOGLE 保存 Google IPv4 分流规则的 iptables 链 IPv6 黑洞路由 阻止脚本内指定的 Google IPv6 网段直连 warp-google.service 重启后恢复 redsocks 和 iptables 规则 其中，WARP 客户端、warp-cli 和 Local proxy 是 Cloudflare 官方能力；redsocks、iptables 分流、IPv6 黑洞和 systemd 管理脚本由原作者实现，不属于 Cloudflare 官方方案。\n验证安装结果 查看整体状态：\nbash 1 warp status 对比直连 IP 和 WARP SOCKS5 出口 IP：\nbash 1 warp ip 单独检查 WARP：\nbash 1 2 warp-cli status curl -x socks5h://127.0.0.1:40000 https://www.cloudflare.com/cdn-cgi/trace 返回内容应包含：\ntext 1 warp=on 检查透明代理规则：\nbash 1 iptables -t nat -L WARP_GOOGLE -n --line-numbers 最后实际访问需要解锁的 Google 服务，确认出口地区和功能是否符合预期。HTTP 200 只能证明页面可访问，不能单独证明地区识别已经修正。\n日常管理与卸载 bash 1 2 3 4 5 6 7 warp status warp start warp stop warp restart warp test warp ip warp uninstall 只停止透明代理并断开 WARP：\nbash 1 warp stop 卸载脚本安装的组件：\nbash 1 warp uninstall 当前版本卸载后仍需检查 /etc/gai.conf。脚本安装时可能追加下面这行，但卸载逻辑不会删除：\ntext 1 precedence ::ffff:0:0/96 100 当前版本的注意事项 脚本只重定向 Google IPv4 的 TCP 流量，UDP/443 的 QUIC/HTTP3 不在现有规则内。 Google IP 段为脚本内的静态列表，可能过期、遗漏或覆盖过宽；需要定期对照官方 goog.json。 部分 warp-cli 命令使用 || true 忽略错误，安装结束后必须手动验证状态。 脚本会覆盖 /etc/redsocks.conf、/usr/local/bin/warp 和 /usr/sbin/policy-rc.d，已有同名文件时应先备份。 pkill redsocks 会停止机器上的全部 redsocks 进程，不适合与其他 redsocks 服务共用。 WARP 出口地区由 Cloudflare 网络分配，脚本不能保证固定国家或地区。 原页面标注“代码完全开源”，但未提供明确的 LICENSE；本站备份保留了原作者署名和来源。 参考资料 原作者配套文章 Cloudflare WARP Linux 安装文档 Cloudflare WARP modes 与 Local proxy Cloudflare WARP Linux 软件包仓库 Google 用户服务 IP 段 ","permalink":"/posts/%E6%8A%8A-warp-google-%E8%A7%A3%E9%94%81%E8%84%9A%E6%9C%AC%E5%AD%98%E4%B8%8B%E6%9D%A5/","summary":"\u003cp\u003e本文备份 VPS值得买发布的 WARP Google 解锁脚本及使用流程，方便原页面或脚本失效后继续查阅。\u003c/p\u003e\n\u003cp\u003e原脚本使用 Cloudflare 官方 \u003ccode\u003ecloudflare-warp\u003c/code\u003e 客户端，在本机建立 SOCKS5 代理，再通过 \u003ccode\u003eredsocks\u003c/code\u003e 和 \u003ccode\u003eiptables\u003c/code\u003e 将部分 Google IPv4 流量转入 WARP。脚本同时屏蔽指定的 Google IPv6 网段，避免 IPv4 与 IPv6 出口地区不一致。\u003c/p\u003e\n\u003cdiv class=\"markdown-alert markdown-alert-warning\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"/\u003e\u003cpath d=\"M12 9v4\"/\u003e\u003cpath d=\"M12 17h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Warning\n    \u003c/p\u003e","title":"WARP Google 解锁脚本备份与使用说明"},{"content":" Info 上个月为了身体健康，我去参加了免费的国民体质检测。体测结束后，主办方让我们在水杯、沐浴露、雨伞和 U 盘里选一份礼品。我一看 U 盘标着 32G，作为程序员，几乎没有犹豫就选了它。\n拿回家后这盘子一直躺着。前几天要拷几个小视频，才想起来用。我本来就没指望它品质有多好，但没想到连容量都是假的。插上电脑，资源管理器里赫然写着 32G；可当我往里拷一个 1G 多的视频时，拷到一半就不对劲了——文件根本写不进去。这时候才反应过来：这是一块虚标 U 盘。\n虚标盘的套路很常见：控制器固件被改过，系统会按商家标称的容量显示，实际闪存却只有几百兆。超过真实容量后，新数据会静默覆盖或损坏，看起来“拷进去了”，回头再打开就是坏文件。\n下面记录我是怎么测出真实容量，又怎么把它救回来继续用的。\n怎么检测虚标 U 盘 检测工具不少，常见的有：\nValiDrive：操作简单，抽测扇区，出结果快 H2testw：把标称容量写满再回读，最彻底，也最慢 f3 / f3probe（Linux）：能给出真实可用扇区，还可以配合 f3fix 修分区表 ChipGenius：偏硬件信息，看控制器和颗粒，辅助判断 这次我用的是 ValiDrive。官网：ValiDrive Download – Free Tool to Test Fake USB \u0026amp; SD Card Capacity\n文末也放了 1.0.1 的百度网盘备份。\n打开软件后，它会要求重新插拔一次待测 U 盘。按提示操作，点检测，等一会儿就能出报告。\n结果很离谱：系统显示 32G，真实可用只有不到 500MB。highest valid region 大约是 493MB。\n按真实容量重建分区 控制器固件改不了，盘在 DiskGenius 里仍会报 29GB 总容量，这正常。关键是：不要继续用那个假的大分区，只在真实可用范围内新建一个小分区。\n我用的是 DiskGenius。\n操作步骤 插上 U 盘，打开 DiskGenius\n在左侧磁盘列表里选中这块可移动磁盘（容量仍是虚标的 31.5GB 左右，产品名可能是 udisk）。\n务必确认选对盘，千万别选成电脑硬盘。\n删掉现有分区\n在右侧分区上右键 → 删除当前分区。\n有多个分区就全部删掉，直到整盘变成未分配空间。\n可以先不保存，等新分区建好再一起写分区表。\n新建真实大小的分区\n在未分配空间上右键 → 建立新分区。建议这样填：\n分区类型：主分区 文件系统：FAT32（兼容性最好，车机、电视、老电脑都能认） 分区大小：460 MB 或 470 MB（宁小勿大，给真实容量留一点余量） 单位选 MB 其余保持默认 点确定。\n保存更改\n点工具栏 保存更改，或按 Ctrl+S，把分区表写入磁盘。\n格式化\n选中刚建好的 460–470MB 分区 → 格式化：\n文件系统：FAT32 簇大小：默认 卷标：随便起，比如 USB400 快速格式化或完整格式化都可以（盘很小，完整格式化也很快） 检查结果\n安全弹出后再插回来。资源管理器里应该只显示大约 450MB 可用，而不是 31GB。\n后面那一大块“空闲 28.8GB”是假空间，不要再建分区，也不要往里扩展。只要只往这个 460MB 分区里拷文件，就不会踩到虚标容量。\n使用建议 适合放文档、少量照片、小工具、小音乐，不要当唯一备份。 拷完最好拔掉再插一次，确认文件还能打开。这类盘颗粒和主控通常都一般。 FAT32 单文件上限 4GB，对这块盘来说基本遇不到。 不要再对整盘做“快速分区”，否则又会按假容量重建大分区。 检测和分区做完，这块礼品 U 盘就算“复活”了。32G 是假的，但 400 多兆的小盘，应急还是能用一用。\n相关链接 ValiDrive 1.0.1 备份：\n1 2 3 通过网盘分享的文件：validrive-1.0.1.zip 链接: https://pan.baidu.com/s/1aYZ0kdSalBvsgeUfI2Wb1Q?pwd=adp8 提取码: adp8 --来自百度网盘超级会员v8的分享 ","permalink":"/posts/%E6%A3%80%E6%B5%8B%E8%99%9A%E6%A0%87u%E7%9B%98%E5%B9%B6%E5%A4%8D%E6%B4%BB%E5%AE%83/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-info\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4\"/\u003e\u003cpath d=\"M12 8h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Info\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e上个月为了身体健康，我去参加了免费的国民体质检测。体测结束后，主办方让我们在水杯、沐浴露、雨伞和 U 盘里选一份礼品。我一看 U 盘标着 32G，作为程序员，几乎没有犹豫就选了它。\u003c/p\u003e\n\u003cp\u003e拿回家后这盘子一直躺着。前几天要拷几个小视频，才想起来用。我本来就没指望它品质有多好，但没想到连容量都是假的。插上电脑，资源管理器里赫然写着 32G；可当我往里拷一个 1G 多的视频时，拷到一半就不对劲了——文件根本写不进去。这时候才反应过来：这是一块虚标 U 盘。\u003c/p\u003e","title":"检测虚标U盘并复活它"},{"content":"我的 Mac mini 和 MacBook Air 明明登录着同一个 Apple Account，也在同一个局域网里，Wi-Fi、蓝牙、Handoff 全都开着，但是鼠标推到屏幕边缘就是没反应。\n不是偶尔抽风，是始终连不上。\n一开始我以为又是显示器布局、推边方向或者 Universal Control 的开关没落盘。结果两台 Mac 来回查了一圈，最后真正修好的动作特别简单：在故障的 Mac mini 上登录 iMessage。\n登录后，Universal Control 几乎立刻恢复。\n但这篇不是为了记“登录一下 iMessage 就好了”这种玄学偏方。真正有意思的是，我们把修复前后的日志和 IDS 状态对上了：Mac mini 原本没有生成可用的 IDS 已注册身份，登录 iMessage 后，这个身份才真正出现。\n两台 Mac 上的 Codex，靠一个留言板对账 这次排查还有点特别。\n两台 Mac 上各跑了一个 Codex，各自检查本机的设置、状态和日志。为了让它们能持续交换结果，其中一台临时搭了一个局域网留言板。Mac mini 端把检查结果贴上去，MacBook Air 端读取后继续对照，再把新的判断贴回来。\n有点像两个人隔着一张桌子排障，只不过桌上摆的是日志，交流靠留言板。\n这个方式在双机问题上还挺合适。因为很多日志单看一台机器都很吵，只有把正常端和故障端放在同一个时间窗口里比较，真正有区分度的东西才会冒出来。\n先把连接过程拆开 Universal Control 不是“蓝牙发现设备，然后鼠标就能穿过去”这么简单。为了避免一直在错误的地方折腾，我们先把过程粗略拆成四层：\ntext 1 2 3 4 5 6 7 8 9 10 11 1. 无线发现 BLE / Wi-Fi P2P / AWDL 能不能看到附近设备 2. IDS 身份映射 能不能把匿名广播映射成同一 Apple Account 下的可信设备 3. Universal Control 目标注册 有没有 Magic Edge、Hot Zone、Target Ready、IDS Target 4. 认证与建链 有没有进入授权、RemoteDisplay 和 link establishment 鼠标推边已经是第 3 层附近的事情了。如果第 2 层身份映射就失败，不管从左边推还是右边推、显示器怎么摆，都不会凭空多出一个可连接目标。\n所以先不推了，抓日志。\n无线发现其实一直是正常的 两端在同一个时间窗口里观察这些进程：\nUniversalControl rapportd sharingd 后来又补了 identityservicesd 过滤日志可以用：\nzsh 1 2 3 4 5 6 7 /usr/bin/log stream \\ --style compact \\ --info \\ --debug \\ --predicate \u0026#39;process == \u0026#34;UniversalControl\u0026#34; OR process == \u0026#34;rapportd\u0026#34; OR process == \u0026#34;sharingd\u0026#34;\u0026#39; \\ | rg --line-buffered -i \\ \u0026#39;universal control|magic.?edge|hot.?zone|target|remote.?display|ids|identity|auth|connect|link|decrypt|no model\u0026#39; 抓取期间，我在主控 Mac 上分别向整套显示器布局的最左和最右边缘持续推鼠标。内部显示器接缝不算，免得又把布局问题混进来。\n结果两台 Mac 都能持续收到近距离的 BLE / Wi-Fi P2P 广播，信号也正常，还能看到 PeerMe、Ranging 之类的特征。\n也就是说，它们并不是互相看不见。\n但日志里同时出现了另外两条东西：\ntext 1 2 3 4 5 sharingd: Unable to decrypt activity level with authTag yes identity no rapportd: Ignoring BLE device found with no model 更关键的是，Universal Control 日志里始终没有出现：\nMagic Edge Hot Zone Target Ready IDS Target 认证成功 link establishment 这就比较明确了：无线层发现了设备，但 sharingd 没有可用的身份去解密和识别广播，rapportd 只能把对面当成一个没有型号、没有 IDS 身份的 Generic 设备，然后忽略掉。\n问题卡在无线发现之后、Universal Control 目标注册之前。\n决定性差异：IDS 已注册身份是空的 接下来两边只读了两个状态位：\nzsh 1 2 3 4 ids_pref=\u0026#34;$HOME/Library/Preferences/com.apple.identityservicesd.plist\u0026#34; plutil -extract hasRegIdentityContainer raw -o - \u0026#34;$ids_pref\u0026#34; plutil -extract hasUnregIdentityContainer raw -o - \u0026#34;$ids_pref\u0026#34; 对比结果很干净：\n状态 正常的 MacBook Air 故障时的 Mac mini hasRegIdentityContainer 1 0 hasUnregIdentityContainer 1 1 IDS Loaded accounts 有 0 Last registered 有注册状态 null Recent Registrations 有历史 0 Mac mini 的 identityservicesd 还在反复输出：\ntext 1 2 3 Active device uniqueID: (null) registeredAccount: (null) No registered account for service 这比“系统设置里登录了同一个 Apple Account”更接近问题本身。\n系统账户是有的，但 IDS 没有加载到一个已注册、可以拿来识别设备的服务账户。无线广播到了这里，就像快递已经送到小区门口，但系统里查不到收件人，当然没法继续送。\n中间几个误判 空 plist 不代表开关没开 排查早期，Mac mini 的 Universal Control 偏好文件是空字典，而 Air 端已经有 Configuration。我们一度以为 Mac mini 的“允许指针和键盘在附近 Mac 或 iPad 之间移动”没有真正开启。\n后来我直接看了系统设置截图，三个开关全开着。\n这个判断当场作废。\n私有 plist 的字段和落盘时机可能跟 macOS 版本有关。文件里没看到字段，只能说明“这里没看到”，不能反过来替 UI 作证。\n看起来很凶的 attestation 日志也可能只是噪声 Mac mini 上还出现过证书信息获取失败和 attestation 缺失。乍一看很像根因，但正常的 Air 在已经拥有注册身份的情况下，也有不少同类日志。\n所以它至少不是这次最有区分度的证据。\n双机对照的价值就在这里：不是挑最吓人的 error，而是找两边真正不一样的状态。\n重启服务没有用 我们重启过这些用户态服务：\nidentityservicesd rapportd sharingd UniversalControl 它们都被 launchd 正常拉了起来，但 hasRegIdentityContainer 还是 0。\n关闭再开启 Handoff 也没有触发 IDS 注册。\n数据库本身只读检查是正常的，quick_check 返回 ok，只是关键表里压根没有生成过账户、设备身份和注册事件。服务重启多少次，加载回来的还是一份完整但空空的数据。\n最后一个差异在 Messages 继续对比非敏感的账户服务状态，终于又看到一个明显差异：\n服务状态 MacBook Air 故障时的 Mac mini Messages Enabled Disabled Phone / FaceTime Enabled 未注册或缺失 iCloud Keychain Enabled Enabled 而 Apple 的公开说明只要求两台设备登录同一 Apple Account，并启用双重认证、Wi-Fi、蓝牙、Handoff 等条件，并没有把 Messages 列成通用前置条件。\nCodex 提到两个可能的修复动作：\n在 Messages App 中登录 iMessage，建立 iMessage / IDS 服务身份； 在 iCloud 设置中打开 Messages 同步，决定消息内容是否通过 iCloud 同步。 我执行的是第一个。\n登录 iMessage，立刻恢复 我在 Mac mini 的 Messages App 中登录 Apple Account，完成 iMessage 登录。\n然后 Universal Control 几乎立刻恢复，Mac mini 可以正常控制 MacBook Air。\n再看状态：\ntext 1 2 hasRegIdentityContainer: 0 → 1 hasUnregIdentityContainer: 1 → 0 此前缺失的 IDS 已注册身份确实被生成了，随后 Continuity 广播能够映射成可信设备，Universal Control 也终于拿到了目标。\n完整链路大概是：\ntext 1 2 3 4 5 登录 iMessage → 本机生成 IDS 已注册身份 → Continuity 广播映射成可信设备 → Universal Control 获得目标 → 认证和链路建立 可以，成功了。\n感叹 AI 不得不感慨，AI 的发展实在是让现在所有的教程、问题解决方案变得越来越廉价了。 所以这也是我疯狂地在沉淀这方面的知识，并且加大输入，然后转化为我的输出。\n那就先这样，收工。\n参考资料 Universal Control: Use a single keyboard and mouse between Mac and iPad — Apple Support Set up iCloud for Messages on all your devices — Apple Support ","permalink":"/posts/%E9%80%9A%E7%94%A8%E6%8E%A7%E5%88%B6%E7%9C%8B%E5%BE%97%E5%88%B0-mac%E5%B0%B1%E6%98%AF%E8%BF%9E%E4%B8%8D%E4%B8%8A/","summary":"\u003cp\u003e我的 Mac mini 和 MacBook Air 明明登录着同一个 Apple Account，也在同一个局域网里，Wi-Fi、蓝牙、Handoff 全都开着，但是鼠标推到屏幕边缘就是没反应。\u003c/p\u003e\n\u003cp\u003e不是偶尔抽风，是始终连不上。\u003c/p\u003e\n\u003cp\u003e一开始我以为又是显示器布局、推边方向或者 Universal Control 的开关没落盘。结果两台 Mac 来回查了一圈，最后真正修好的动作特别简单：\u003cstrong\u003e在故障的 Mac mini 上登录 iMessage。\u003c/strong\u003e\u003c/p\u003e","title":"通用控制看得到 Mac，就是连不上：两台 Codex 排查 IDS 注册故障"},{"content":" Qwen3.8-27B 这个尺寸我等了挺久：27B 稠密模型还保留了足够的能力，量化后又能塞进单张 24GB 显卡，正好落在本地 Agent 比较实用的一档。\n这次我在 4×RTX 3090 24GB 上部署了原始 BF16 权重，对比 vLLM 和 SGLang，又用单卡跑了一遍 Q4 GGUF。接口跑通后，还把同一个模型接进 WorkBuddy 和 DeepSeek Harness（DSH），测试看图、工具调用和真实文件任务。\n先放结论：\n四卡 BF16 最终继续使用 vLLM。当前环境里，它的长上下文、Prefix Cache 和热请求 TTFT 更适合 Agent 负载。 SGLang 的 Decode 略快，但在这组没有可用 P2P 的 3090 上需要把多模态特征传输切到 CPU，热请求也没有形成迁移优势。 单卡 Q4 在 32K 上下文下能跑到约 39～40 tok/s，但这个 GGUF 没有视觉 projector，而且我没有做量化质量评测。 WorkBuddy 和 DSH 都跑通了多模态与工具闭环；到了真实发票任务，两边依然需要人盯着，不能把“工具能调起来”当成“任务可以无人值守”。 本文不是从下载权重开始的手把手教程，主要记录参数、性能数据，以及混合线性注意力模型在缓存和 Agent 场景里容易踩的坑。\n先理解 Qwen3.8 的混合注意力 Qwen3.8-27B 延续 Qwen3.5 的架构。官方模型卡给出的 64 层布局是：每三层 Gated DeltaNet（GDN）线性注意力之后插入一层全注意力，总计 48 层线性注意力和 16 层全注意力；原生上下文长度为 262,144 Token，可通过 YaRN 扩展到 1M。\n全注意力层每生成一个 Token，都要读取随历史增长的 KV Cache；线性注意力则把历史压进固定大小的循环状态。按这套模型 4 个 KV 头、head_dim=256 和 BF16 粗算，如果 64 层全部使用全注意力，KV Cache 的线性增长部分约为 256KiB/token；现在只有 16 层全注意力，约为 64KiB/token，另加固定大小的线性注意力状态。\n这不是说上下文增长“没有成本”，而是增长斜率降到了纯全注意力模型的约四分之一。落到这台四卡 3090 上，131K 上下文仍能放下，并且保留了一定并发空间。\n之所以还保留四分之一全注意力，是因为固定大小状态的容量有限。混合结构在长上下文效率和精确回忆能力之间做了折中。\nPrefix Cache 为什么变复杂了 第一个变量来自对话模板。Qwen3.8 官方提供 preserve_thinking 控制历史 reasoning 是否保留；如果客户端或模板没有把上一轮 reasoning 带进下一轮 Prompt，两轮原始 Token 流就会从这里分叉，分叉后的部分无法命中 Prefix Cache。这是命中率问题，不应该造成回答错误。\n第二个变量是快照粒度。线性注意力状态会原地更新，不能像普通 KV 块那样随便回退。vLLM 的 align 模式只在对齐边界保存可恢复状态；在我的 vLLM 0.27.1 环境里，每个边界是 784 Token。命中位置差一个 Token，也只能退到更早的快照，短前缀甚至可能零命中。\n第三个变量是 MTP 投机解码。草稿 Token 被拒绝时，普通 KV Cache 可以删除多出来的块；线性注意力状态却已经被更新，不能直接把那几个 Token “吐出来”。快照恢复、前缀缓存恢复和投机回滚叠在一起，如果状态路径没有完全对齐，就可能从性能问题升级为正确性问题。\nSGLang 为混合模型设计了 MambaRadixCache，把循环状态快照挂到 Radix 树节点，并为投机解码的草稿 Token 分配独立状态槽。vLLM 则提供 none/align/all 三种 Mamba Cache 模式；我这轮使用的是仍标为实验性的 align，没有开启 MTP。\n四卡 RTX 3090 部署 BF16 测试服务器共有 8 张 RTX 3090 24GB，其中 GPU 0～3 已经在跑其他服务。Qwen 使用 GPU 4/5/6/7，Tensor Parallel 设为 4。\nQwen3.8-27B 的 BF16 权重约 52GiB。最终环境使用 vLLM 0.27.1，服务上下文先设为 131,072。\nvLLM 启动参数 [bash] 显示已折叠代码（19 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 docker run -d \\ --name vllm-qwen38-27b \\ --restart unless-stopped \\ --gpus \u0026#39;\u0026#34;device=4,5,6,7\u0026#34;\u0026#39; \\ --network host \\ --ipc=host \\ --shm-size=32g \\ -v /path/to/models:/models:ro \\ vllm/vllm-openai:latest \\ --model /models/Qwen/Qwen3.8-27B \\ --served-model-name qwen3.8-27b \\ --port 8360 \\ --tensor-parallel-size 4 \\ --max-model-len 131072 \\ --gpu-memory-utilization 0.92 \\ --enable-prefix-caching \\ --reasoning-parser qwen3 \\ --enable-auto-tool-choice \\ --tool-call-parser qwen3_xml text 1 2 API Base: http://\u0026lt;SERVER_IP\u0026gt;:8360/v1 Model: qwen3.8-27b 四张卡各占约 22,406MiB。启动日志显示每卡有 5.88GiB KV Cache，四卡总 GPU KV Cache 容量为 369,864 Token；按单请求 131,072 Token 计算，理论最大并发约为 2.82。RTX 3090 没有这套服务可用的 FP8 加速能力，所以 KV Cache 保持 auto，实际为 BF16。\n冷启动用了 8～9 分钟，其中 torch.compile 占了 154 秒。容器进程出现不代表接口已经可用，恢复服务时至少要等 /v1/models 或一次真实 Chat Completions 请求成功。\nPrefill、Decode 与四路并发 并发 请求数 平均 TTFT 单路 Decode 聚合输出吞吐 总耗时 1 2 个依次执行 0.831 秒 44.2～44.7 tok/s 41.42 tok/s 24.72 秒 4 4 个同时执行 2.688 秒 平均 30.42 tok/s 121.58 tok/s 16.85 秒 四路并发后，每个请求单独看会变慢，但服务器总输出从约 41 tok/s 提升到 121.58 tok/s。这不是极限压测，只能代表当前环境下 1～4 路 Agent 请求的数量级。\n长上下文测试使用合成填充文本，在末尾放一个唯一标记，要求模型只返回标记：\nPrompt Token TTFT 总耗时 结果 7,994 11.795 秒 11.921 秒 标记正确 31,994 44.973 秒 45.028 秒 标记正确 119,978 180.473 秒 191.340 秒 标记正确 131K 配置确实能接收接近 120K 的输入并找回末尾信息，不只是把启动参数写大。但这个测试只证明容量和简单检索，不等于模型能高质量理解 120K 代码。冷 Prompt 到这个长度要等约三分钟，真实 Agent 体验仍然高度依赖共享前缀能否复用。\n原生 262K 可以继续上调 --max-model-len，代价是单请求占用更多缓存、可并发数下降；扩展到 1M 则要按官方说明配置 YaRN，不能只改一个长度数字。\nPrefix Cache 已命中，客户端 usage 却看不到 最开始我把同一个 Prompt 连发三次，OpenAI usage 里的 prompt_tokens_details 一直为空。回头看启动日志才发现，Prefix Caching 默认没有开启。\n加上 --enable-prefix-caching 后，日志出现：\ntext 1 Mamba cache mode is set to \u0026#39;align\u0026#39; for Qwen3_5ForConditionalGeneration 随后把同一个 1,626 Token Prompt 连发三次，/metrics 累计查询 4,878 Token、命中 3,136 Token。第二、三次各复用 1,568 Token，正好是两个 784 Token 对齐块，剩余 58 Token 重算；累计命中率为 64.3%。\n客户端响应里的 cached_tokens 仍为空，但这不代表缓存没有工作。至少在 vLLM 0.27.1 这套环境里，要通过 /metrics 或 Prometheus 观察引擎命中，不能只看 OpenAI usage。\nQwen3.8 的 thinking 怎么关闭 本轮最可靠的方式仍然是把参数交给 Chat Template：\njson 1 2 3 4 5 { \u0026#34;chat_template_kwargs\u0026#34;: { \u0026#34;enable_thinking\u0026#34;: false } } 使用 OpenAI Python SDK 时，可以放进 extra_body：\npython 1 2 3 4 5 6 7 8 9 response = client.chat.completions.create( model=\u0026#34;qwen3.8-27b\u0026#34;, messages=[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;计算 23×19，只给答案\u0026#34;}], extra_body={ \u0026#34;chat_template_kwargs\u0026#34;: { \u0026#34;enable_thinking\u0026#34;: False } }, ) vLLM 0.27.1 也会把 reasoning_effort: \u0026quot;none\u0026quot; 映射为 enable_thinking=false。我的短测结果如下：\n请求方式 耗时 Completion Token reasoning reasoning_effort=low 1.945 秒 75 有 chat_template_kwargs.enable_thinking=false 0.377 秒 4 无 顶层 enable_thinking=false 2.391 秒 97 有，字段被忽略 reasoning_effort=none 0.462 秒 4 无 Prompt 里加 /no_think 也没有生效。当前服务接受 none/low/medium/xhigh，不接受常见的 high；这组取值是 vLLM 0.27.1 与当前 Qwen 模板的组合表现，接其他版本前最好重新查一次。\nTool Call、图片和视频 vLLM 使用 qwen3_xml Tool Parser；后面的 SGLang 使用 qwen3_coder，两边的参数不能直接互换。\n图片 URL、Base64 图片和短视频 URL 都通过了：\n输入 Prompt Token Completion Token 耗时 结果 官方数学题图片 URL 300 857 20.802 秒 正确选出 1+√2 Base64 图片 263 10 约 0.8 秒 答案正确 官方短视频 URL 11,126 16 29.549 秒 正确识别 20 个瓷罐 这些结果只能说明当前接口链路的图片和短视频输入可用，不能扩写成“全模态都支持”。本轮没有测试长视频与媒体高并发，音频输入也不支持。之前用 Qwen3.6 处理视频时，我还遇到过 AV1 解码与视频采样问题，输入视频的编码链路同样需要单独检查。\nJSON Schema 约束输出和 OpenAI Responses API 也能使用。vLLM 的 Anthropic /v1/messages 普通消息与工具调用可以跑通，但 Anthropic 原生的 thinking: {type: \u0026quot;disabled\u0026quot;} 关不掉 Qwen thinking，仍然要传 Qwen 的 Chat Template 参数。\nSGLang 对比：先绕过 3090 的 P2P 问题 SGLang 使用同样四张 RTX 3090、TP=4 和 131,072 上下文。第一次启动直接死在 P2P 错误：\ntext 1 CUDA error: peer access is not supported between these two devices 关闭 NCCL P2P、自定义 AllReduce、调整 Mamba SSM dtype 和 GPU 映射都没有解决。完整 traceback 最终落到 cuda_ipc_transport_utils.py：服务在 GPU 上创建多模态 CUDA IPC 特征池，但这组 3090 没有可用 P2P。\n最终绕过去的是：\ntext 1 --mm-feature-transport=cpu 服务启动后，日志显示 max_running_requests=40、max_total_num_tokens=268202，上下文为 131,072；Tool Parser 使用 qwen3_coder，完整工具续轮也通过。\n同口径对比如下：\n引擎 1,747 Token 冷 TTFT 热 TTFT 512 Token Decode 约 32K 冷 TTFT vLLM 0.27.1 3.12 秒 0.35 秒 42.9 tok/s 44.97 秒 SGLang 测试镜像 2.76 秒 1.63 秒 44.3～44.4 tok/s 48.30 秒 短而全新的冷 Prompt，SGLang 快 0.36 秒，Decode 也略快；长 Prompt 冷 Prefill 则是 vLLM 更快。热请求差距最大：SGLang 日志确认缓存了 1,728/1,747 Token，但端到端 TTFT 仍为 1.63 秒，vLLM 是 0.35 秒。\n这套 Agent 负载里，系统 Prompt、工具定义和重复历史占了很大一部分。综合热请求、长 Prefill 和现有运维成本，我没有迁移到 SGLang。这个选择只针对当前四卡环境，不代表 SGLang 在新卡或更高并发场景里一定更慢。\n单张 RTX 3090 跑 Q4 GGUF GGUF 使用 Unsloth 的 Qwen3.8-27B-UD-Q4_K_XL.gguf。单卡服务配置为 32K 上下文、全层 GPU Offload 和 Flash Attention，模型在 10.25 秒内加载完成。\n项目 结果 llama-bench Prompt Processing，512 Token 1326.42 tok/s llama-bench Token Generation，128 Token 40.27 tok/s 真实接口 TTFT，884 Token Prompt 1.900 秒 真实接口流式 Decode，512 Token 39.17 tok/s 30,024 Token 大海捞针 27.10 秒，标记正确 GPU 显存峰值 19,616MiB 17.9GB 的 Q4 能在单张 RTX 3090 上跑通 32K，还剩约 5GB 显存。thinking 分离、两种关闭方式和工具结果续轮也都通过。\n但这份 GGUF 没有视觉 projector，只能当纯文本模型用。前面 vLLM 的图片、视频结论不能顺手算到 GGUF 上；我也没有做标准化量化质量评测，速度可用不等于回答质量与 BF16 完全一致。\n接入 WorkBuddy 和 DeepSeek Harness 接口跑通只是第一层。我更关心把它接进真实 Agent Harness 之后，模型能不能看图、调用工具、操作文件，以及知不知道什么时候该停。两个 Harness 都先请求同一个 OpenAI 兼容网关，再由网关转发到本地 Qwen；下面的地址和密钥全部使用占位符。\nWorkBuddy 的 reasoning_effort 兼容问题 测试使用 WorkBuddy 5.3.13。一开始开启 supportsReasoning 后，WorkBuddy 自动发送 reasoning_effort=high；而当前 vLLM 服务只接受 none/low/medium/xhigh，上游因此返回 400。\n调整配置并重启客户端后，普通聊天、图片输入、Bash 工具调用和工具结果续轮都能完成。这里也说明了一个常见问题：模型、推理框架和 Harness 都声称支持 reasoning，不代表枚举值一定兼容。\nDSH 配置 DeepSeek Harness 使用官方 @deepseek-ai/dsh 0.1.0-rc.6。我单独建立了隔离的 DSH_HOME，避免影响日常环境：\n[yaml] 显示已折叠代码（25 行） 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 agent-default-model: provider: qwen-local model: qwen3.8-27b reasoningEffort: low llm-pi-ai: providers: qwen-local: api: openai-completions baseURL: https://\u0026lt;NEW_API_BASE\u0026gt;/v1 apiKeyEnv: NEW_API_KEY defaultInput: [text, image] compat: thinkingFormat: openai supportsReasoningEffort: true reasoning: low models: - id: qwen3.8-27b contextWindow: 131072 maxTokens: 32768 reasoningEfforts: off: none low: low medium: medium xhigh: xhigh 普通文本、off 关闭 thinking、low 分离 reasoning、Bash 工具调用和工具续轮都正常。DSH 的 read_image 也会把图片作为原生 Image Block 交给 Qwen，不是先偷偷换成外部 OCR 文本。\n如果不熟悉 Harness 里的 Tool Call 和工具结果续轮，可以先看模型到底是怎么调用工具的。\n用真实发票任务检查 Agent 闭环 测试材料来自一个 12306 发票压缩包，另加两张真实电子发票截图。ZIP 同时包含 OFD 和 PDF，但它们是同一张铁路电子客票的两种格式。任务要求 Agent 自己完成：\n解压并识别 OFD、PDF、PNG。 判断 OFD 与 PDF 是否重复，按发票号码去重。 把唯一发票整理成带公式、筛选和正确数据类型的 Excel。 生成审计记录，同时隐藏完整身份证、手机号、卡号和长票号。 WorkBuddy：识别准确，交付阶段卡在子 Agent PDF 的字体编码导致 pypdf 无法抽取文本，WorkBuddy 没有卡死，而是解包 OFD，从附件的 XBRL XML 读取发票字段。它还把 PDF 内嵌 XML 与 OFD 附件做了字节级比对：两份都是 4,183 bytes，SHA-256 一致，从而确认它们是同一张票。\n最终三张唯一发票都识别出来，三组不含税金额、税额和 254.10 元价税合计也正确。\n拖慢任务的是文件交付阶段。主 Agent 已经整理好数据，却又调用内置 sheet-agent 制作 Excel。日志显示子 Agent 使用的仍是 qwen3.8-27b，但从全新上下文开始，重新读取表格 API 参考、检查尚不存在的文件，迟迟没有创建工作簿，最后需要人工介入收束。\n全新上下文带来的 Prefill 很可能是耗时因素之一，但不是唯一能从这次测试证明的原因；子 Agent 的任务拆分、工具文档读取和停止条件同样会影响交付。\nDSH：过度分析可以靠约束缓解，但识别错误还在 同一批材料交给 DSH 后，它先创建 Python 环境、解压 ZIP、解析 OFD，再并行调用 read_image 读取两张原图。这一段正常。\n随后它开始反复怀疑 20 位发票号码：裁图、放大 3 倍和 6 倍，再安装 NumPy 做暗色行列扫描、ASCII 字符画、逐字形分割、孔洞计数和阈值调整。到第 25 步，它还在研究“数字 0 的闭环为什么漏了”。\n我在 14 分 57 秒时终止了第一次任务：共 37 次工具调用，没有生成 Excel，也没有生成审计文件。\n恢复测试沿用同一工作区，但开启全新 DSH 会话，并明确限制：禁止裁图、OCR、像素分析和子 Agent；每张原图只允许查看一次；不确定就标注；三轮工具内优先生成文件。\n第二次用了 6 个步骤、8 次工具调用，在 9 分 55 秒后完成交付。文件结构和金额看起来不错：三张唯一发票、日期、税率、三组金额、254.10 元总计、SUM 公式、文本票号、冻结窗格和筛选都正确。\n但独立核对后仍有至少三处实体字段错误：一张 PNG 的发票号码严重误读，另一张 PNG 的销售方名称误读，第一张 PNG 的开票人姓名也错了。\n这轮更像是在测整个 Agent 系统，而不是只测 Qwen 的“智商”。同一个模型换 Harness、工具说明、任务约束和子 Agent 策略，结果差异很大。对发票这类高准确性文件任务，最终产物仍然必须做独立校验。\n最终怎么选 从 Qwen3.5-27B 开始，这个尺寸就是我本地模型测试的常驻基座。之前的 Qwen3.5-35B-A3B 部署记录 更偏向吞吐；这次换成 27B Dense，关注点明显转向长上下文、缓存和 Agent 完整闭环。\n当前这套环境，我会这样使用：\n四卡 BF16 继续留在 vLLM：热请求、长上下文、多模态和 Prefix Cache 的综合表现最符合现有 Agent 负载。 SGLang 保留为备选：它已经证明能在这组 3090 上稳定启动，Decode 略快；换到互联更好的新卡或更高并发，再重新比较才公平。 单卡 Q4 用于纯文本轻负载：约 39～40 tok/s，适合个人和临时 Agent，但要接受没有视觉 projector、量化质量尚未评测的边界。 Harness 必须看任务闭环：聊天、看图和单次 Tool Call 通过，只能证明接口兼容。真实文件任务还要检查停止条件、子 Agent 上下文、工具开销和最终产物准确性。 27B 这个“能力不至于太差、量化后又能塞进单卡”的尺寸，依然是本地 Agent 很舒服的一档。后面更值得继续测的，不只是换模型，而是把 Prefix Cache 扩展到 CPU 或 SSD 的分层缓存，以及在不牺牲正确性的前提下重新尝试 MTP。\n参考资料 Qwen3.8-27B 官方 Hugging Face 模型页 Qwen3.8-27B ModelScope 模型页 vLLM 官方 Qwen3.8-27B Recipe Unsloth Qwen3.8-27B GGUF vLLM Qwen3 reasoning parser 源码 vLLM Qwen3 XML Tool Parser 源码 vllm#40696：混合模型块对齐导致短前缀零命中 vllm#47194：Prefix Cache 与 MTP3 组合问题 SkyRL#1981：Qwen3.6-27B MTP 与 Prefix Cache 正确性问题 Hybrid Models Meet SGLang · PyTorch 官方博客 SGLang 官方 Qwen3.8-27B Cookbook llama.cpp Server 文档 llama.cpp CUDA 构建文档 llama.cpp Qwen3.5 架构实现 DeepSeek Harness 官方仓库 ","permalink":"/posts/qwen3.8-27b-vllm-sglang-dsh/","summary":"\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.szmckj.cn/uploads/2026/08/18/6a8342a5567ff.png\"\u003e\n        \u003cimg alt=\"Qwen3.8-27B 本地部署实测题图\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.szmckj.cn/uploads/2026/08/18/6a8342a5567ff.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003eQwen3.8-27B 这个尺寸我等了挺久：27B 稠密模型还保留了足够的能力，量化后又能塞进单张 24GB 显卡，正好落在本地 Agent 比较实用的一档。\u003c/p\u003e\n\u003cp\u003e这次我在 \u003ccode\u003e4×RTX 3090 24GB\u003c/code\u003e 上部署了原始 BF16 权重，对比 vLLM 和 SGLang，又用单卡跑了一遍 Q4 GGUF。接口跑通后，还把同一个模型接进 WorkBuddy 和 DeepSeek Harness（DSH），测试看图、工具调用和真实文件任务。\u003c/p\u003e","title":"Qwen3.8-27B 本地部署实测：vLLM、SGLang、单卡 Q4 与 DSH"},{"content":"上一篇我把 DeepSeek-V4-Flash 塞进了八张 RTX 3090，结尾留下两件事：给 3090 测 DSpark 投机解码，再找两张 RTX PRO 6000 跑官方权重。\n现在两个坑都填上了。\n结果比我预想得更夸张：\n八卡 RTX 3090 把 DSpark 参数调对后，固定长输出的单路 Decode 从约 36 tok/s 提升到 61.26 tok/s。 双卡 RTX PRO 6000 的最快方案跑到了约 204 tok/s，长输入 Prefill 也稳定在约 3K tok/s。 不过先说清楚：这不是一场严格控制变量的 GPU 跑分。两边使用的权重、推理引擎、DSpark 配置和虚拟化环境都不同。本文比较的是我实际能部署出来的两套方案，不是单纯比较 RTX 3090 和 RTX PRO 6000 的理论算力。\n两套 DeepSeek-V4-Flash 环境有什么不同 项目 8× RTX 3090 2× RTX PRO 6000 单卡显存 24GiB 96GiB 总显存 192GiB 192GiB 模型 Unsloth UD-Q4_K_XL GGUF DeepSeek 官方权重 推理引擎 llama.cpp 定制 vLLM r24 DSpark Q8_0 Draft，n_max=3 DSpark K5 多卡方式 8 卡 layer split TP2 环境限制 VMware、无 NVLink/P2P、CPU 与内存较旧 VMware、P2P 和 custom all-reduce 被禁用 3090 侧的正式 DSpark 测试使用 NVIDIA 595.84、CUDA 13.2 和 llama.cpp commit 6ea215d171fd31df943bf1ac8227129f2b963160。PRO 6000 侧则使用 InstantTensor、B12X Attention/MoE/Linear、FP8 KV Cache 和 CUDA Graph。\n所以后面的数字只能在各自环境中理解。它们适合回答“这两套现有机器实际能做到什么”，不能直接外推成所有 3090 和 PRO 6000 的通用成绩。\n八卡 RTX 3090：DSpark 参数调对后确实能加速 我最开始并不确定，这套只有 PCIe 互联的八卡 3090 开启投机解码后是否真的会更快。\n第一次直接按较长的草稿块，把 DSpark 的 n_max 设成 5，确实有提升，但还不是最优解。后来按 Unsloth 推荐改成 n_max=3，结果一下子拉开了。\n固定 6,000 Token 代码输入、强制生成 2,048 Token、关闭提前停止，每个配置跑三次，取中位数：\n配置 Prefill Decode DSpark 接受率 整轮耗时 Target-only 539.43 tok/s 36.09 tok/s — — DSpark n_max=5 517.34 tok/s 49.67 tok/s 75.99% 53.12 秒 DSpark n_max=3 505.51 tok/s 61.26 tok/s 91.46% 45.34 秒 n_max=3 比 n_max=5 的 Decode 快 23.3%，整轮任务快 14.6%。草稿猜得越长，靠后的 Token 越容易被拒绝；验证这些没用上的 Token 时，主模型、草稿模型和八张卡之间都要多忙一轮。\n在这套没有 NVLink、没有 P2P 的八卡 PCIe 环境里，猜三步反而比猜五步更合适。\n不过 DSpark 不是打开以后所有任务都自动加速。之前测试 512 Token 的短输出时，它有时只快几个百分点，甚至还会倒退。\n目前我的判断是：\n长代码、长报告、长思考输出，DSpark 值得开。 短问答不一定赚，Prefill 还可能更慢。 真要用于 Coding Agent，上线前必须跑任务级质量回归，不能只看 tok/s。 antirez/ds4 没跑起来，还差点把服务器拖死 社区里还有一条很诱人的路线：antirez/ds4 CUDA Tensor Parallel。\n源码在 3090 的 sm_86 上编译成功，专用 Q4 权重也完成了八卡 placement。然后它检查出所有 GPU 配对都只能走 BOUNCE，跨卡数据要经过主机内存中转，接着开始对约 154GiB 的 GGUF 做整文件 cudaHostRegister()。\n出去摸个鱼回来，权重还没开始复制，CPU 和 CUDA/IOMMU 的压力已经把 SSH 拖断了。\n到这里就没必要继续硬撑。这条路线更依赖裸机、正常 P2P 和更好的 PCIe 拓扑，不适合我这台 VMware 八卡机。本次没有拿到可报告的 antirez/ds4 推理吞吐。\n八卡 3090 的并发瓶颈不在显存 单路跑快以后，我把总上下文设成 128K，切成 12 个 slot，测试 1、2、4、8、12 并发。\n需要注意：这不是 12 路请求每路都有 128K。 服务对齐后总容量是 132,096 Token，每个 slot 实际 11,008 Token；测试请求为每路 8K 输入、512 输出。\n并发 每流 Decode 中位数 聚合整轮吞吐 单轮总耗时 1 40.54 tok/s 18.01 tok/s 28.42 秒 2 25.30 tok/s 20.01 tok/s 51.17 秒 4 13.67 tok/s 22.29 tok/s 91.90 秒 8 5.92 tok/s 23.47 tok/s 174.56 秒 12 3.82 tok/s 24.15 tok/s 254.42 秒 聚合吞吐一直在涨，但并发 8 比并发 4 只增加约 5.3%，并发 12 比并发 8 只增加约 2.9%。与此同时，单路 Decode 已经从 40.5 tok/s 掉到 3.8 tok/s。\n瓶颈并不是显存容量。单卡峰值约 22,216MiB，八卡平均 GPU 利用率只有约 10.7%～11.7%。日志显示多路 Prefill 基本按 slot 分段推进，后面的请求一直排队。\n所以这台八卡机的实用档位很明确：\nCoding Agent：并发 1，最多 2。 可以等待的文档批处理：并发 4。 并发 8～12：容量上能跑，交互体验没什么意义。 双卡 RTX PRO 6000：硬件省心，虚拟机不省心 接着换成两张 96GB 的 RTX PRO 6000 Blackwell Workstation Edition。两张卡同样提供 192GB 标称显存，这次使用 DeepSeek 官方权重，按理说很适合做 TP2。\n结果一上来就撞墙。\n这两张卡在裸机和 CUDA 层面支持 PCIe P2P，但一打开 P2P，虚拟机就会崩。我没有查清具体原因。换 vLLM 默认的 B12X fused all-reduce 后，模型可以加载，服务却一直挂着，8080 端口始终不监听。\n最后能稳定运行的组合反而是：\ntext 1 2 3 4 5 6 7 8 TP2 NCCL P2P disabled custom all-reduce disabled NCCL / SHM 通信 InstantTensor 加载 B12X attention / MoE / linear DSpark K5 CUDA Graph FULL_AND_PIECEWISE 每张卡加载约 81.01GiB 权重，权重读取约 302 秒，Engine 初始化 354.85 秒，从启动到服务可用大约 11～12 分钟。\nRTX PRO 6000 的 Prefill 与首次 JIT 启动阶段的 warmup 没有覆盖所有 Prefill、并发和 DSpark 形状。每遇到一种没见过的 shape/config，SparkInfer、CuTeDSL 或 Triton 还可能现场 JIT 一次。\n我使用没有缓存命中的真实 SGLang 代码复测。这里的 Prefill 速度按 Prompt Token / TTFT 折算，包含请求调度和首 Token 返回，不是单独的 CUDA Kernel 成绩：\n实际输入 TTFT Prompt / TTFT 约 17K 5.52～5.67 秒 3,057～3,088 tok/s 23,099 Token 7.28 秒 3,173 tok/s 28K～32.7K 9.26～10.92 秒 2,997～3,080 tok/s 实际体验是前面安静五六秒，随后突然开始快速输出代码，反差很明显。\n新 shape 的首次 JIT 还会再慢一刀：同样约 17.5K 输入，第一次请求总耗时约 8.76 秒，预热后 TTFT 稳定到约 5.7 秒。正式上线前，最好主动跑一组覆盖常用并发、上下文和长输出的 warmup。\n双卡 RTX PRO 6000 的 204 tok/s 从哪里来 当前最快的组合是定制 vLLM r24、官方权重与 DSpark K5。在 16K 输入、512 输出的请求里，DSpark 平均接受长度为 3.65，Draft 接受率约 53%。每轮最多猜 5 个 Token，实际平均能让主模型前进三到四步，已经足够明显地提高单流 Decode。\n这里有个容易误解的地方：不是只有这版定制 vLLM 才包含 DSpark。之前用原版 vLLM 跑出过一次约 6.1 tok/s，源码里已经注册了 DeepSeek-V4 的 DSpark，只是启动参数没有打开，当时跑的仍然是 target-only，并且使用了通用 FlashInfer、Eager 和更保守的通信路径。\n定制版补上的也不只是 DSpark 开关，还包括：\nInstantTensor Loader。 B12X MLA Sparse Attention。 B12X MoE 与 Linear。 SparkInfer/CuTeDSL 的 SM120 专用算子。 FP8 KV Cache。 完整和分段 CUDA Graph。 DSpark 减少昂贵的主模型前向次数，B12X 和 SparkInfer 则让每次前向本身更快。它们叠在一起，才是当前约 204 tok/s 的来源。\n社区里有人使用双 RTX PRO 6000、定制 vLLM、InstantTensor 和 B12X 后端跑到约 122 tok/s，但公开记录没有给出统一的 Prompt、输出长度和实际上下文占用，所以只能当作复现线索，不能和本文数字直接横比。\n为什么 SGLang 没有跑出相同的 DSpark 结果 SGLang 的 target-only 基线其实不差：单路约 70 tok/s，8 并发全部进入 Decode 后，聚合约 332～340 tok/s。\n但我当前环境里的 DSpark 没有跑通。Draft 与 Verify 一次只处理 5～40 个 Token，SM120 后端却把它们送进了要求 Token 数大于 64 的 Attention Kernel；即使关闭 CUDA Graph，仍然报同类错误。\n这个结论只针对我当时准备的镜像和版本。网络条件有限，我没来得及下载更新镜像继续排查，不能据此泛化成“SGLang 不支持 DeepSeek-V4 DSpark”。\n目前能确定的是：定制 vLLM + DSpark，是我在这两张 RTX PRO 6000 上跑出来的最快单路方案。\n接到实际任务里是什么感觉 跑分归跑分，最后还是得让它干点实际的。我把服务接到上游，让它现场做了一个天气卡片。\n速度确实舒服，基本没有“模型还在慢慢打字，我先去刷会儿手机”的感觉。\n自己部署的 DeepSeek 和官方 API 在效果上仍有差异。我目前只是猜测可能与系统提示词或部署模式有关，时间有限，还没有完成严格对照。这轮先确认了可行性，质量差异以后再查。\n不同预算下怎么选本地 DeepSeek 硬件 上一篇已经整理过 H20、RTX 5090、RTX PRO 6000 和 DGX Spark 的其他玩家成绩，这次不再重复硬件排行榜。结合这几天的实测和社区里配置相对完整的案例，我目前会这样选。\n已经有 RTX 3090：继续用，不必为了跑分换卡 八张 3090 已经在手里的话，Unsloth GGUF + llama.cpp + DSpark n_max=3 可以继续用。它不适合高并发 Token 服务，但跑一两个离线 Coding Agent、做可以排队的文档处理，依然很实用。\n社区还有一条“旧服务器再就业”路线：四路 Xeon、768GB DDR4，只配两张 3090，通过 CPU 内存与 GPU 混合加载官方 156GB 权重，单路约 33 tok/s，四到八并发聚合约 53～68 tok/s。\n这证明显卡数量不是唯一答案，内存通道足够多的旧服务器也能运行 DeepSeek。不过如果手里没有这类机器，我不建议专门照着买。四路服务器的功耗、噪音、内存和 NUMA 调优，最后可能比模型本身还折腾。\n单台 DGX Spark：适合体验和个人 Agent 单台 DGX Spark 是体积较小、门槛较低的整机路线之一。截至 2026 年 8 月，国内公开渠道的 128GB/4TB 版本约 3.5 万元。\n社区使用 UD-IQ2_M + llama.cpp 跑出约 19.7 tok/s 单流、约 52 tok/s 四并发；换更激进的专用引擎后，短上下文单流可以达到约 28～30 tok/s，也有人尝试过 1M Context。\n代价也很明显：单机主要依靠 IQ2/IQ3 量化，不能等同于双机部署的官方权重；多个 Agent 同时携带长历史时，吞吐会快速下降。它更适合体验、个人 Agent 和长上下文实验。\n双 DGX Spark：兼顾官方权重和超长上下文 如果从零购买，又希望运行官方 DeepSeek-V4-Flash-0731，我目前更倾向两台 DGX Spark。\n截至 2026 年 8 月，国内公开零售价两台约 7 万元，社区讨论中也出现过 5.5～6.7 万元的阶段性价格。社区已经有相对完整的 TP2 配方：官方权重、200Gb RoCE、DSpark K5、NVFP4 KV、CUDA Graph、Prefix Cache 和 Chunked Prefill。\n调好 CUDA Graph 后，公开结果中的单路热 Decode 中位数约 95.9 tok/s，双并发聚合约 151.8 tok/s，四并发约 263.7 tok/s，六并发约 340.5 tok/s。还有人提交了约 90 万 Token 的真实请求并成功返回。\n不过，“能开 1M”和“1M 好用”不是一回事。那次约 90 万 Token 请求的 TTFT 是 1028.85 秒，差不多等了 17 分钟才看到第一个 Token。它适合偶尔吞下超大代码库或文档库，不代表日常任务应该把上下文拉满。\n双 Spark 也不是插上电就自动得到这些数字。两台机器需要正确的 ConnectX-7/200Gb RoCE 互联，NCCL、容器镜像、CUDA Graph 和 DSpark Loader 都要配对。社区还发现过 Loader 漏载 12 个 Shared Expert Tensor 的问题；修复后，平均吞吐才从 32.7 tok/s 恢复到 55.4 tok/s，DSpark 接受率从 25.7% 回到 60.2%。\n双 RTX PRO 6000：价格高，但单机等待时间更短 如果更在意单机部署、Prefill、低延迟和单路 Coding Agent 体验，双 RTX PRO 6000 确实更舒服。本文这套甚至被 VMware 禁掉了 P2P 和 custom all-reduce，仍然跑到约 204 tok/s；换成裸机和更合理的 PCIe 拓扑，理论上还有提升空间，但我没有实测，不替它吹。\n问题还是价格。截至 2026 年 8 月，RTX PRO 6000 Blackwell 96GB 在国内公开渠道常见报价接近 10～12 万元一张，双卡仅显卡约 20～24 万元，还没计算工作站成本。\n它买到的是更高的单路速度、单机维护便利和更少的等待，不是单纯的“每元 tok/s”。\n我现在的选择 手里已经有 3090：继续榨，不必为了跑分焦虑。 预算三万多：单台 DGX Spark 可以体验，但要接受量化和速度边界。 预算约 5.5～7 万：想跑官方权重和超长上下文，双 DGX Spark 是目前较完整的性价比路线。 预算二十万以上：优先单机速度和服务体验，双 RTX PRO 6000 更合适。 至于我自己，先继续用手头能调动的设备。等哪天能借到两台 DGX Spark，再折腾一轮。\n参考资料 上一篇：八张 RTX 3090 本地部署 DeepSeek-V4-Flash DeepSeek-V4-Flash-0731 官方模型卡 Unsloth DeepSeek-V4 部署与 DSpark 说明 Reddit：双 RTX PRO 6000 的定制 vLLM / B12X 案例 NVIDIA 论坛：双 RTX PRO 6000 运行 DeepSeek-V4-Flash Reddit：双 RTX 3090 与四路 Xeon 运行官方权重 NVIDIA 论坛：单 DGX Spark 的 131K Context 实测 MiaAI-Lab：双 DGX Spark 与 DSpark 完整部署配方 NVIDIA 论坛：双 DGX Spark 的 DSpark Loader 修复与 1M Context 什么值得买：DGX Spark 128GB/4TB 国内公开价格线索 Linux.do：DeepSeek-V4-Flash 本地部署与国内价格讨论 ","permalink":"/posts/deepseek-v4-flash-rtx-3090-vs-rtx-pro-6000/","summary":"\u003cp\u003e上一篇我把 DeepSeek-V4-Flash 塞进了\u003ca href=\"/posts/deepseek-v4-flash-8x-rtx-3090/\"\u003e八张 RTX 3090\u003c/a\u003e，结尾留下两件事：给 3090 测 DSpark 投机解码，再找两张 RTX PRO 6000 跑官方权重。\u003c/p\u003e\n\u003cp\u003e现在两个坑都填上了。\u003c/p\u003e\n\u003cp\u003e结果比我预想得更夸张：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e八卡 RTX 3090 把 DSpark 参数调对后，固定长输出的单路 Decode 从约 36 tok/s 提升到 61.26 tok/s。\u003c/li\u003e\n\u003cli\u003e双卡 RTX PRO 6000 的最快方案跑到了约 204 tok/s，长输入 Prefill 也稳定在约 3K tok/s。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e不过先说清楚：这不是一场严格控制变量的 GPU 跑分。两边使用的权重、推理引擎、DSpark 配置和虚拟化环境都不同。本文比较的是\u003cstrong\u003e我实际能部署出来的两套方案\u003c/strong\u003e，不是单纯比较 RTX 3090 和 RTX PRO 6000 的理论算力。\u003c/p\u003e","title":"DeepSeek-V4-Flash 实测：八卡 RTX 3090 与双卡 RTX PRO 6000 差多少？"},{"content":"前两天还在纸面上计算 DeepSeek-V4-Flash 本地部署需要多少硬件，这次不继续算账了，直接上机器。\n我最快能调动的本地算力就是八张 RTX 3090。为了腾显存，还把其中四张卡上常驻的 Qwen3.6-27B 服务停了。模型前一天晚上就开始下载，结果中途网络断掉，第二天又重新来了一遍。百兆网拉一百多 GB 的模型，确实很磨人。\n这次想确认两个问题：八张 RTX 3090 能不能把 DeepSeek-V4-Flash 完整放进显存，以及跑起来后的真实 Coding Agent 体验怎么样。\n先放结果：\n八卡 3090 能跑，而且不是 0.x tok/s 的“亮机测试”。32K 长上下文下，单路解码约 32 tok/s；如果 Coding Agent 的历史上下文能命中 KV Cache，32K 请求的 TTFT 可以从约 70 秒降到 8.38 秒。\n这次部署的模型与配置 模型使用 DeepSeek-V4-Flash-0731 的 Unsloth 量化版本 UD-Q4_K_XL GGUF：\n模型总参数约 284B，每个 Token 激活约 13B。 五个 GGUF 分片合计 155,095,241,120 字节，约 144.44 GiB。 八张 RTX 3090 合计提供 192 GiB 显存。 推理框架使用 llama.cpp，多卡采用 layer split。 首轮配置为 32K 上下文、单 slot、Flash Attention、Q8 KV Cache。 我选这个量化版本，最直接的原因是它能完整装进八卡 3090。Unsloth 模型卡还提到，UD-Q8_K_XL 为 162GB，只比 UD-Q4_K_XL 大约 7GB。不过我没有在同一环境里做 Q4 与 Q8 的能力 A/B，所以这篇只记录 Q4 的运行情况，不把“能力几乎无损”当成自己的实测结论。\n这也不是追求高并发 API 服务的配置。我的目标是离线内网 Coding Agent：一到两个用户，长上下文，源码和日志不离开内网。\n下载 DeepSeek-V4-Flash GGUF 模型来自 Hugging Face。我的代理流量已经不太够，只能走 hf-mirror：\nbash 1 2 3 4 5 export HF_ENDPOINT=https://hf-mirror.com hf download unsloth/DeepSeek-V4-Flash-0731-GGUF \\ --include \u0026#39;UD-Q4_K_XL/*\u0026#39; \\ --local-dir /data/huggingface_model/DeepSeek-V4-Flash-0731-UD-Q4_K_XL 下载速度并不稳定，几个大分片只有每秒几 MB。下载结束后，先确认五个分片完整，再启动服务。\nllama.cpp 加载 155GB 模型花了多久 下面保存的是后来测试 128K 上下文时使用的启动命令。首次测试 32K 时，只需要把 --ctx-size 131072 改成 32768：\n[bash] 显示已折叠代码（30 行） 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 CUDA_DEVICE_ORDER=PCI_BUS_ID \\ CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \\ ./llama-server \\ --model /data/huggingface_model/DeepSeek-V4-Flash-0731-UD-Q4_K_XL/UD-Q4_K_XL/DeepSeek-V4-Flash-0731-UD-Q4_K_XL-00001-of-00005.gguf \\ --alias DeepSeek-V4-Flash-0731-UD-Q4_K_XL \\ --host 0.0.0.0 \\ --port 8099 \\ --api-key \u0026#34;请替换为自己的API_KEY\u0026#34; \\ --ctx-size 131072 \\ --parallel 1 \\ --threads 16 \\ --threads-batch 32 \\ --batch-size 2048 \\ --ubatch-size 512 \\ --gpu-layers 999 \\ --split-mode layer \\ --tensor-split 1,1,1,1,1,1,1,1 \\ --flash-attn on \\ --cache-type-k q8_0 \\ --cache-type-v q8_0 \\ --numa distribute \\ --load-mode mmap \\ --temp 1.0 \\ --top-p 0.95 \\ --min-p 0.0 \\ --reasoning auto \\ --cont-batching \\ --metrics \\ --slots \\ --no-ui 几个需要按实际环境调整的地方：\n--model 只指向第一个 GGUF 分片，llama.cpp 会自动加载其余四个分片。 如果只允许本机访问，把 --host 0.0.0.0 改成 127.0.0.1；不对外提供接口时也可以移除 --api-key。 --tensor-split 的八个数字对应八张 GPU，卡数不同必须调整。 --threads 和 --threads-batch 应根据机器的 CPU 数量调整。 这条命令没有启用 DSpark，对应的也是本文现有的全部实测数据。 启动日志曾经停在这里五分钟：\ntext 1 load_model: loading model \u0026#39;...00001-of-00005.gguf\u0026#39; 看起来很像卡死。我检查资源占用后发现，磁盘还在以约 40MB/s 持续读取。可能和 VMware 或磁盘有关，具体原因我没有继续确认。把 155GB 权重搬到八张卡上，最终用了约 46 分 51 秒。\n八张卡的显存为什么没有占到同一个比例 模型加载后，八张卡的显存占用并不整齐，大约分布在 15～21GB，最高的卡接近 88%，最低的约 62%。\n这和 vLLM 的体验很不一样。vLLM 设置 gpu_memory_utilization=0.9 后，会把权重之外的大量显存提前规划成 KV Cache 块，让整卡占用主动接近 90%。llama.cpp 则根据权重切分、计算缓冲、上下文长度和 KV 精度按需分配，不追求固定百分比。\n所以显存没有全部占满，不代表部署不完整；各张卡的剩余显存也不能直接相加。是否还能继续增加上下文，最终取决于占用最高的那张卡。\nOpenAI 兼容接口已经可以正常使用 llama.cpp 暴露了 OpenAI 兼容接口，模型 ID 为：\ntext 1 DeepSeek-V4-Flash-0731-UD-Q4_K_XL 健康检查、模型列表和短对话都能正常返回。我先把它接进 Open WebUI 做了基础检查。\n短问题当然说明不了什么。Coding Agent 的输入不是几十个 Token，系统提示、工具定义、代码、终端日志和历史对话叠在一起，6K 只是起步，16K 很常见，30K 也不夸张。\n32K 单路：真实代码上下文能跑多快 以下是在这台服务器上的实测数据。输入不是重复字符，而是从 llama.cpp 源码中截取的真实 C/C++ 文件；每组都是冷请求，关闭前缀缓存，固定最多生成 512 Token。\n输入上下文 冷预填充 TTFT 解码速度 总耗时 6K 503 tok/s 12.3 秒 35.8 tok/s 26.6 秒 16K 498 tok/s 32.4 秒 34.9 tok/s 47.0 秒 30K 464 tok/s 65.0 秒 32.2 tok/s 80.9 秒 32K 461 tok/s 70.0 秒 32.1 tok/s 85.9 秒 注意看 TTFT，也就是提交请求到看到第一个 Token 的时间。32K 冷请求要等约 70 秒，确实难绷。好在 Coding Agent 往往会重复使用大段相同前缀，KV Cache 正好能缓解这个问题。\nKV Cache 才接近 Coding Agent 的真实体验 OpenCode 一类 Agent 会反复携带相同的系统提示、工具定义、仓库源码和历史消息，新一轮通常只在末尾追加少量命令输出、补丁和用户任务。\n我模拟了这种“历史不变、末尾追加”的会话：先把 90% 的稳定上下文放进 slot，再追加最后 10%，测试 KV Cache 命中后的 TTFT。\n总上下文 缓存命中 本轮新增预填充 冷 TTFT 热 TTFT 有效预填充速度 6K 5,400 Token 604 Token 12.3 秒 1.48 秒 4,141 tok/s 16K 14,400 Token 1,604 Token 32.4 秒 3.70 秒 4,430 tok/s 30K 27,000 Token 3,004 Token 65.0 秒 7.49 秒 4,089 tok/s 32K 28,800 Token 3,204 Token 70.0 秒 8.38 秒 3,883 tok/s 这里的“有效预填充速度”不是 GPU 突然快了十倍，而是九成 Token 根本不需要重新计算。新增 Token 的物理预填充速度仍在约 389～444 tok/s。\n如果命中率更高，效果还会更明显。16K 上下文命中 96.7% 时，只需要重新处理 526 Token，TTFT 约 1.47 秒，完整上下文折算的有效预填充超过 1.1 万 tok/s。\n缓存也有前提。如果每轮都重新排序工具定义、在提示词开头插入时间戳，或者改写较早位置的内容，前缀匹配被破坏，就可能重新退化成完整预填充。开发 Agent 或调用带缓存计费的 API 时，稳定前缀都很重要。\n接入 OpenCode 的实际效果 同事把这个 OpenAI 兼容接口接进了 OpenCode，实际用下来不错。和单条 API Benchmark 不一样，Agent 会连续读取文件、调用工具、执行命令，再把新结果追加回上下文，这种用法正好能吃到 KV Cache 的好处。\nOpenCode 本身我之前也写过一篇初次使用记录，这里主要看它接入本地 DeepSeek-V4-Flash 后的表现。\n64K 和 128K 上下文实测 32K 跑通后，我把服务配置改成：\ntext 1 2 CONTEXT_SIZE=131072 PARALLEL=1 这样只需要重新加载一次模型，就可以同时测试 64K 和接近 128K 的单请求，不必为了两个上下文档位重复等待几十分钟。\n第二次重载约 35 分钟后，服务成功启动：\ntext 1 2 n_ctx = 131072 n_ctx_train = 1048576 128K Q8 KV Cache 没有吃掉想象中那么多显存。服务空闲时，八张卡约占 15.4～21.7GiB；128K 冷请求运行期间，最高的一张卡峰值约 22.06GiB，没有 OOM。\n最终数据如下：\n总上下文 冷预填充 冷 TTFT 90% 缓存 TTFT 解码速度 有效预填充速度 32K 461 tok/s 70.0 秒 8.38 秒 32.1 tok/s 3,883 tok/s 64K 398 tok/s 160.95 秒 20.49 秒 28.48 tok/s 3,162 tok/s 128K 310 tok/s 413.8 秒 58.69 秒 24.38 tok/s 2,202 tok/s 128K 确实能跑，但不适合每轮冷启动：第一次打开上下文，要等接近 7 分钟。即使命中 90% 缓存，本轮新增的 12.8K Token 本身也已经是一条不短的请求，所以 TTFT 仍接近一分钟。\n64K 更像日常 Coding Agent 的平衡点。它能容纳较大的仓库上下文，缓存命中后的 TTFT 约 20 秒，单路生成仍接近 28.5 tok/s。128K 则适合大型仓库分析、超长日志和少数深度任务，作为极限档按需使用。\n其他硬件数据只能作为数量级参照 凑巧朋友圈里有一份其他玩家整理的测试：2×H20 热身后单路约 108.7 tok/s，6×RTX 5090 单路约 102～103 tok/s、八路总吞吐约 427～450 tok/s，2×RTX PRO 6000 约 93～95 tok/s，2×DGX Spark 稳态约 70.1 tok/s。\n这些数据只能作为数量级参照。截图没有给出完全统一的量化版本、推理框架、预热方式、上下文长度和 Decode 统计口径；其中 H20 冷启动只有 28.1 tok/s，热身后却达到 108.7 tok/s，也再次说明测试本地大模型时必须区分冷启动、热缓存和稳态数据。\n至少从这份数据可以看到，更强的新卡和更好的互联有机会把单路速度推到 70～100 tok/s 以上。八卡 3090 这套方案追求的则是用手头现有的消费级多卡容纳完整模型，换到 24～32 tok/s 的可用 Coding Agent 体验。\n八卡 RTX 3090 部署的最终结论 八张 RTX 3090 部署 DeepSeek-V4-Flash，不是适合卖高并发 Token 的方案。它的优势很明确：\n155GB 量化权重可以完整驻留八卡显存。 32K、64K、128K 均已实测通过，单路生成分别约 32.1、28.5、24.4 tok/s。 命中 90% KV Cache 后，64K TTFT 约 20.5 秒，128K 约 58.7 秒。 源码、日志、工具权限和完整工作流都可以留在内网。 缺点也同样明确：消费级多卡缺少高速互联，并发能力有限，提示词和缓存策略需要认真设计。\n这台测试服务器的 CPU 和内存都比较旧，显卡之间也没有高速互联，都会拖累性能，所以这些数据只能代表当前这套环境。如果目标不是服务几百个人，而是给个人或小团队准备一个离线 Coding Agent，八卡 3090 已经不只是“为了证明能跑”，是真的可以拿来工作。\n官方原始权重和 DSpark 小模型我也已经开始下载，但还没有完成同环境测试。后面准备试试投机解码能不能继续加速，也争取补一轮 2×RTX PRO 6000 的实测。\n先这样，等后续数据跑出来再更新。\n参考资料 DeepSeek-V4-Flash-0731 官方模型卡 Unsloth DeepSeek-V4-Flash-0731 GGUF llama.cpp DeepSeek V4 支持 PR llama.cpp Server 文档 ","permalink":"/posts/deepseek-v4-flash-8x-rtx-3090/","summary":"\u003cp\u003e前两天还在纸面上计算 DeepSeek-V4-Flash 本地部署需要多少硬件，这次不继续算账了，直接上机器。\u003c/p\u003e\n\u003cp\u003e我最快能调动的本地算力就是八张 RTX 3090。为了腾显存，还把其中四张卡上常驻的 Qwen3.6-27B 服务停了。模型前一天晚上就开始下载，结果中途网络断掉，第二天又重新来了一遍。百兆网拉一百多 GB 的模型，确实很磨人。\u003c/p\u003e","title":"八张 RTX 3090 本地部署 DeepSeek-V4-Flash：32K、64K、128K 实测"},{"content":" 最近在好几个平台配置 2FA 时，它们都会让我打开 Google Authenticator 扫一个二维码。扫完以后，手机上就出现了一串六位验证码，每隔一小段时间自动刷新。把验证码填回平台，就能通过校验。一直没有仔细研究过这是为什么。\n更奇怪的是，生成验证码时手机甚至不需要联网。它到底怎么知道服务器此刻想要哪一串数字？如果双方各算各的，时间难道不会慢慢漂移吗？\n这场景让我想起了以前银行发的 U 盾和动态口令牌。不过先掰扯清楚一个容易搞混的点：传统 U 盾里存的一般是数字证书和私钥，负责给交易签名；那种屏幕上数字不停跳的，更接近“动态口令牌”。它们都是在证明“你手里有这个东西”，但肚子里用的技术不一定是同一套。\nGoogle Authenticator 里这种跟着时间变化的验证码，叫做 TOTP（Time-Based One-Time Password，基于时间的一次性密码）。常见配置是六位数字、每 30 秒刷新一次，但位数和时间步长本身都是系统参数。\n扫描二维码时，到底扫进去了什么？ 第一次绑定的时候，平台会生成一份随机密钥，然后干两件事：\n服务端保存一份，生产环境中应当对它进行加密或其他形式的安全保护； 把同一份密钥塞进二维码，让 Authenticator 扫进去。 Google Authenticator 扫出来的二维码，还原之后大概长这样：\ntext 1 2 3 4 5 otpauth://totp/Example:ray@example.com ?secret=JBSWY3DPEHPK3PXP \u0026amp;issuer=Example \u0026amp;digits=6 \u0026amp;period=30 这里面真正要紧的是 secret，它就是平台和手机共同把守、外人碰不得的共享密钥。剩下的平台名、账号、验证码位数、刷新周期，都是帮助 Authenticator 正确展示和计算的信息。\n所以这个二维码可不是一张普通的“绑定图片”，更像是平台隔着屏幕塞给手机的一把钥匙。\n这里得特别提醒一句：别随便截图、转发或者保存这个二维码。 谁拿到它，谁就能在另一台设备上复制出一模一样的验证码——而且不是只抄走当前这一组，是往后每一组都能自己算出来。\n手机不联网，为什么还能和服务器算出同一个数？ 答案很简单：它们压根不需要实时商量。\n绑定的那一刻，平台和手机就已经把三样东西对好了：\n同一份共享密钥； 同一套计算方法； 同一个时间分段规则。 TOTP 的做法是：把当前的 Unix 时间按固定长度切成一格一格，默认每格 30 秒：\ntext 1 2 时间格 = floor((当前 Unix 时间 - 起始时间) ÷ 30) 验证码 = HOTP(共享密钥, 时间格) floor 就是向下取整。时间每跨过 30 秒，“时间格”的编号就加 1，算出来的验证码自然跟着换。\nHOTP 这一步可以粗略理解成：\ntext 1 2 3 把“共享密钥 + 时间格编号”放进 HMAC 算法 → 得到一长串难以预测的结果 → 动态截取并转换成指定长度的数字 当然，它不是把密钥和时间简单加起来就完事。HMAC 是一种带密钥的哈希算法：输入差一丁点，输出就天差地别；不知道密钥的人，盯着前面几组验证码也推不出下一组。\n所以当你的手机显示 123456 时，服务器并不是拿着一张“答案表”来对号，而是自己撸起袖子也算一遍：\ntext 1 2 手机：同一密钥 + 当前时间格 → 123456 服务端：同一密钥 + 当前时间格 → 123456 输入一样，算法一样，结果当然一样。整个过程不用访问 Google，也不用手机和服务器每 30 秒通一次话——各算各的，答案自然对得上。\n顺带一提，那个倒计时不是从你打开 App 才开始走的。30 秒的格子是按 Unix 时间统一切好的，所以你打开 Authenticator 时，经常会发现只剩 7 秒就刷新了。\n手机和服务器的时间真的不会漂移吗？ 会。\nTOTP 的标准文档 RFC 6238 专门用了一整节聊时钟漂移和重新同步。它的思路不是把漂移消灭掉，而是给漂移留一个有限的容错窗口。\n假设服务器当前在第 100 格，它可以不只计算这一格，还顺手计算相邻的时间格：\ntext 1 2 3 第 99 格：上一组验证码 第 100 格：当前验证码 第 101 格：下一组验证码 比如你卡着倒计时最后一秒抄下验证码，手输加网络又耗掉两三秒，等请求到了服务器，那边其实已经翻篇进下一格了。只要服务器还接受上一格的验证码，这点正常的磨蹭就不会把你挡在门外。\nRFC 6238 推荐默认使用 30 秒步长，这是安全性和易用性之间的折中；对单纯的传输延迟，标准建议最多放宽一个时间步。真遇到设备时钟本身跑偏，验证器也可以在预先设定的范围内向前或向后检查几个时间格，验证成功后还可以记住“这个令牌大概偏了几格”，下次按这个偏移量校验。\n当然，窗口不能无限放宽。检查的时间格越多，服务器一次接受的候选验证码就越多，攻击者暴力猜中的概率也跟着上涨。说白了，容错窗口就是在“好用”和“安全”之间做取舍。\n手机一般没这个烦恼，它会通过运营商或网络时间服务自动对时。时区也搅不浑这锅水：TOTP 用的是 Unix 时间，上海晚上八点和伦敦中午十二点，指向的是同一个时间点。真正让验证码一直报错的，是设备时钟本身快得或慢得太离谱，超出了平台容忍的窗口。\n老式硬件令牌就比较容易积累漂移了——它里面靠一颗时钟芯片自己走，用久了误差越攒越多。偏差还在窗口内，服务器能自动识别并记下来；一旦超出去，就得走额外验证，重新同步或者干脆重新绑定。\nTOTP 的“一次性”体现在哪里？ 同一个 30 秒时间格里，手机算来算去都是同一个验证码。所以“一次性”不是说它在你面前晃一眼就没了。\nRFC 6238 要求：一组验证码一旦验证成功，验证器就不能再接受同一组验证码的第二次提交。再配上短有效期、登录密码和失败次数限制，这几位数字才算凑成一套完整的验证机制。\n这也是为什么验证码不能简单代替登录密码。“你知道的密码”加上“你持有的密钥”，两样凑齐了，才构成大家常说的双因素认证。严格来说，TOTP 只是其中的“持有因素”，具体系统是否真的构成双因素，还要看另一个验证步骤使用了什么因素。\n最后总结 说到底，Google Authenticator 能离线生成正确的验证码，靠的不是服务器在背后远程指挥，而是一套很朴素的“提前对暗号”：\ntext 1 共享密钥 + 当前时间格 + 统一算法 = 此刻的验证码 二维码负责把共享密钥塞进手机；30 秒的时间格让验证码不断翻新；服务器多检查有限的相邻时间格，把网络延迟和轻微的时钟漂移兜住。\n所以它不是没有误差，而是把误差圈在了一个可控的范围里。\n下次再盯着那串不停跳动的验证码，可以想象这个画面：手机和服务器各自揣着同一份秘密，同时抬头看了一眼钟，然后算出了同一个答案。\n参考资料 RFC 6238：TOTP——基于时间的一次性密码算法 RFC 6238 第 5.2 节：验证与时间步长 RFC 6238 第 6 节：重新同步与时钟漂移 RFC 4226：HOTP——基于 HMAC 的一次性密码算法 Google Authenticator：Key URI Format ","permalink":"/posts/totp-%E9%AA%8C%E8%AF%81%E7%A0%81%E4%B8%BA%E4%BB%80%E4%B9%88%E6%AF%8F30%E7%A7%92%E5%8F%98%E5%8C%96/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e最近在好几个平台配置 2FA 时，它们都会让我打开 Google Authenticator 扫一个二维码。扫完以后，手机上就出现了一串六位验证码，每隔一小段时间自动刷新。把验证码填回平台，就能通过校验。一直没有仔细研究过这是为什么。\u003c/p\u003e\n\u003cp\u003e更奇怪的是，生成验证码时手机甚至不需要联网。它到底怎么知道服务器此刻想要哪一串数字？如果双方各算各的，时间难道不会慢慢漂移吗？\u003c/p\u003e","title":"TOTP 每 30 秒换一次验证码，手机离线怎么和服务器对上？"},{"content":" 从一次 Tool Call 走到真正会继续工作的 Agent Loop：模型调用工具、观察结果、继续决策，直到给出最终回答或触发停止条件。\n昨天上班看日报时，很开心看到 DeepSeek-v4-flash-0731 正式发布，对它期待甚高。一直以来主力模型都是海外模型，但是频频封号、支付困难，层层难关只是为了给老外送钱，心里很不是滋味。倒不是要站位，只是被卡脖子的滋味不好受，在哪个领域都一样。\n所以 GLM5.2 的进步、K3 的进步，都让我惊喜万分。无奈 GLM5.2 在涨价前我抢购了接近十次，从未成功；K3 倒是还不错，但买了个小套餐后也限购了。DeepSeek 的每一次动作都能在圈子里激起浪花，也很期待它以后的能力。看到这次更新对 Codex 环境做了优化，而且我用的服务已经能按 Responses 范式调用，接下来的 Agent 开发系列就先拿 DeepSeek 做例子吧。\n本文代码使用我当时接入的 DeepSeek Responses 兼容服务。兼容接口不代表所有参数、状态管理和返回结构都与 OpenAI 官方完全一致，具体仍要以实际 Provider 的文档和响应为准。\n前两篇我们讲了模型的对话和多轮记忆，也拆开了模型到底是怎么调用工具的。还剩一个待完成的闭环——给工具执行套上一层循环，实现“看一眼、做一点、再看一眼”。如果只看之前的例子，你可能还无法理解这是什么意思。那我还是拿天气助手做例子。\n工具并行调用与顺序调用 我们上期做了查询某个城市的天气，但是那个工具一次只能传入一个城市参数。假如我问：“上海和北京现在的天气怎么样？”\n模型如果只调用一次就回答，显然不够。但这两个查询前后无关，模型为什么不能在一轮内就查两次呢？包能的，别太瞧不起现在的模型，我们用 API 演示一下。\n这里我把模型的思考暂时关闭了。在我这次使用的接口中，配置写作 reasoning: { effort: \u0026quot;none\u0026quot; }。reasoning.effort 支持哪些值取决于具体模型和 Provider，不能看到 Responses 兼容接口就默认所有模型都支持 none。\n可以看到，模型在同一轮返回了两个工具调用。这种彼此不依赖的查询可以一起执行，等结果都回来后再一起交给模型。\nOpenAI 官方 Responses API 也允许模型在一轮里返回多个 Function Call；如果不希望出现并行调用，可以把 parallel_tool_calls 设为 false，将每轮限制为零个或一个函数调用。其他兼容 Provider 是否支持同名参数，需要单独确认。\n那么，如果问题变成：“帮我查一下深圳现在的天气，如果正在下雨就查一下北京，不下雨就查一下上海。”这时必须先得到深圳的天气，才能决定第二个工具应该查谁。我们来试试效果。\n诶，没问题。模型理解需要先查询深圳。我们把查询结果喂回去后，它才发起第二次工具调用。（我看最近一个月伦敦也未必比深圳忧郁 T T）\n可以看到，模型理解了这个条件，并且发起了第二轮 Tool Call。这个过程怎么放进我们开发的 Agent 里实现呢？没错，再加上一层循环。\n工具调用结束，并不代表这一轮任务结束。要把工具结果再次发给模型，直到模型不再请求工具。\n最小的 Agent Loop 还记得第二篇的代码把请求次数写死了：第一次让模型决定工具，第二次把工具结果交回去，然后结束。\n但现在已经不能提前知道模型到底要调用几次，所以真正需要循环的不是某个工具函数，而是“调用模型并处理它的输出”这一整段流程。\n我们继续复用上期的 weatherTool 和 getWeather。最小 Loop 可以写成这样：\n[javascript] 显示已折叠代码（67 行） 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 const tools = [weatherTool]; async function callFunction(name, args) { if (name === \u0026#34;get_weather\u0026#34;) { return getWeather(args); } throw new Error(`未知工具：${name}`); } async function executeToolCalls(toolCalls) { return Promise.all( toolCalls.map(async (toolCall) =\u0026gt; { try { const args = JSON.parse(toolCall.arguments); const result = await callFunction(toolCall.name, args); return { type: \u0026#34;function_call_output\u0026#34;, call_id: toolCall.call_id, output: JSON.stringify(result), }; } catch (error) { return { type: \u0026#34;function_call_output\u0026#34;, call_id: toolCall.call_id, output: JSON.stringify({ error: error.message, }), }; } }) ); } async function runAgent(userInput, { maxTurns = 8 } = {}) { const input = [ { role: \u0026#34;user\u0026#34;, content: userInput }, ]; // 设置最大轮数，防止 Agent 陷入无限工具循环。 for (let turn = 1; turn \u0026lt;= maxTurns; turn++) { const response = await client.responses.create({ model: \u0026#34;deepseek-v4-flash\u0026#34;, instructions: \u0026#34;你是天气助手，需要时可以调用天气工具。\u0026#34;, tools, input, }); // 保留这一轮的完整输出。 input.push(...response.output); const toolCalls = response.output.filter( (item) =\u0026gt; item.type === \u0026#34;function_call\u0026#34; ); // 没有 Tool Call，说明模型已经给出了最终回答。 if (toolCalls.length === 0) { return response.output_text; } const toolOutputs = await executeToolCalls(toolCalls); input.push(...toolOutputs); } throw new Error(`Agent 超过最大执行轮数：${maxTurns}`); } 看起来只是多了一层 for，但从这里开始，程序已经不再预设模型下一步要做什么了。\n这段代码每一轮只做几件事：\n带着当前 Context 调用模型； 保存模型返回的完整 response.output； 使用 filter 收集这一轮的所有 Tool Call； 没有 Tool Call 就返回最终答案； 有 Tool Call 就执行，把结果追加到 Context； 带着新的 Context 进入下一轮； 超过 maxTurns 就强制停止，防止无限调用。 Agent Runtime 不能预先假设模型这一轮会调用几次工具。它必须观察模型每一轮的输出，再决定继续执行还是结束。\n多个工具调用怎么处理 现在我们不能预判模型会输出几个工具调用，因此这里不能再使用第二篇里的 find，因为 find 只会拿到第一个调用。我们使用 filter 收集全部调用，再用 Promise.all 并行执行。\n不过，并不是所有工具都能随便并行。天气查询是只读操作，彼此没有依赖，所以很安全。等以后加入写文件、Shell、数据库修改这些工具，就必须判断执行顺序、资源冲突和副作用，不能一股脑全部塞进 Promise.all。\n同一轮返回的多个调用也不一定适合并发。比如两个调用都要修改同一个文件，即使模型一次性返回了它们，Runtime 仍然应该根据工具元数据选择串行执行、加锁或者直接拒绝冲突操作。\n为什么要保存完整的 response.output 为了方便演示，前面把模型的思考部分关掉了。但如果开启推理，上下文应该怎么拼接？\n这里以我当时使用的 DeepSeek API 文档为例，它的多轮上下文拼接是这样的：\n看代码里的 input，它就是当前这次 Agent 运行的活跃 Context。一开始只有用户消息，之后会不断加入：\n模型消息； 推理相关内容或 Items； 模型请求调用的工具； 工具执行结果。 所以代码中要写：\njavascript 1 input.push(...response.output); 不能只保存 response.output_text。模型可能在 output 里同时返回 reasoning Item 和 function_call；如果中途把这些内容丢掉，下一轮就缺少了完成这次工具调用所需的上下文。\nOpenAI 官方 Function Calling 文档也特别说明：对于 GPT-5、o4-mini 这类推理模型，模型随工具调用返回的 reasoning Items 也必须和工具结果一起传回去。\n这次示例没有使用服务端状态，响应里的 previous_response_id 为 null，所以我们按无状态方式自己维护并回传完整 input。如果实际 Provider 支持并启用了 previous_response_id 或 Conversation，状态可以由服务端串联；但不要一边假设服务端保存了历史，一边又漏传手动维护所需的 Items。\n简单来说，任务运行得越久，Context 就越长：\nToken 成本会增加； 模型响应会变慢； 旧信息可能干扰当前判断； 最终会碰到上下文窗口上限。 这一篇先不展开上下文压缩，只记住一件事：Agent 开发的很大一部分工作，其实是在决定下一轮模型应该看到什么，又不应该看到什么。\n围绕模型搭起来的这一整套运行环境——怎样构建 Context、开放哪些 Tools、怎样执行工具、怎样回传结果、什么时候继续、什么时候停止、错误与权限怎么处理——合在一起，就是后面经常会提到的 Harness。\nAgent Loop 什么时候继续，什么时候停止 最正常的结束条件很简单：模型这一轮没有返回 function_call，而是给出了最终自然语言回答。\n但只靠这个条件还不够，还需要几条保护线：\n达到 maxTurns，防止模型无限调用； 用户主动取消，或者请求被 AbortSignal 中止； Token、时间或费用预算已经耗尽； 出现无法恢复的模型请求或协议错误。 反过来，单个工具失败不一定意味着整个 Agent 都应该停止。天气接口报错、参数不合法、文件不存在、搜索无结果，这些都可以作为结构化的 Tool Result 返回给模型。模型看到错误后，可能会修正参数、换一个工具，或者直接向用户解释发生了什么。\n所以最小实现里的 executeToolCalls 没有直接把错误抛到最外层，而是把它变成：\njson 1 2 3 { \u0026#34;error\u0026#34;: \u0026#34;具体的错误信息\u0026#34; } 错误既可能是程序异常，也可能只是模型下一轮需要观察的一条信息。\n不过生产环境不能直接把所有原始异常都塞给模型。内部路径、SQL、密钥片段或服务信息可能混在 error.message 里，应该先分类、脱敏，再决定哪些信息能回传。\n我们完成的只是内层循环 到这里，我们已经拥有了一个真正会继续工作的最小 Agent Loop：\ntext 1 模型 → 工具 → 模型 → 工具 → 最终回答 但它仍然只接收了一次用户输入。runAgent 开始工作后，用户只能等它完全结束，再发起下一次任务。\n我们使用 Codex 时肯定已经习惯另一种体验：它工作到一半，我们发现方向不对，可以马上补充一句，而不需要等它完全做完。\n我把它叫做“插嘴”。Codex 官方使用的术语是 Steer，对应名词是 steering。还有两个容易混淆的词：\nSteer：把新消息加入当前正在运行的任务，用来纠正方向、补充信息； Queue：把消息排队，等当前任务结束后，作为下一轮处理； Interrupt：取消当前运行。 Codex App Server 使用 turn/steer 向正在执行的 Turn 追加输入，使用 turn/interrupt 请求取消。Queue 描述的是客户端把消息留到下一轮的行为，并不存在一个对应的 turn/queue 方法。\n从结构上看，下一步需要在刚刚写完的内层 Loop 外面，再建立一层消息调度循环，同时处理队列、取消和运行中的新输入：\n到这里，我们终于不再是在手动拼一次工具调用，而是拥有了一个真正会继续工作的 Agent Loop。它能观察结果、继续决策、执行多个工具，也知道什么时候停下来。\n效果就像这样：\n现在距离一个常见的 Agent 还缺一些能力，比如读文件、执行 Bash 命令等。这就像基础的增删改查：只有一根手指头戳戳戳天气是不够的。\n前面做的上下文消息列表，相当于给了它一双文字级别的眼睛；现在又给了它一根手指头，能戳一下天气预报。接下来还得给它双手双脚。假如要让它帮我写代码，就得让它读文件、写新文件、修改已有文件，以及使用最万能的 Shell。\n同时，如果以后不断扩展工具，难道还要用一堆 if 一个个判断吗？这里怎么优化？而且现在我们也看不清它到底做了什么，交互对我们而言根本不透明。\n接下来先完成几个基础工具的开发，随后再优化工具注册和交互体验吧。\n继续阅读 手把手教你从零开发一个 Agent（0）：Responses API 与多轮对话记忆 模型到底是怎么调用工具的：从 Chat Template 到 Function Calling 从 0 开发一个 agent（1）：Agent Runtime 的架构草图 参考资料 OpenAI Function Calling 指南 OpenAI Responses API：Create a model response Codex：Steering and queuing Codex App Server 微信公众号发布版：手把手教你从零开发一个智能体 之 循环 ","permalink":"/posts/agent-loop-%E6%80%8E%E4%B9%88%E5%86%99/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e从一次 Tool Call 走到真正会继续工作的 Agent Loop：模型调用工具、观察结果、继续决策，直到给出最终回答或触发停止条件。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e昨天上班看日报时，很开心看到 DeepSeek-v4-flash-0731 正式发布，对它期待甚高。一直以来主力模型都是海外模型，但是频频封号、支付困难，层层难关只是为了给老外送钱，心里很不是滋味。倒不是要站位，只是被卡脖子的滋味不好受，在哪个领域都一样。\u003c/p\u003e","title":"Agent Loop 怎么写：并行工具调用、停止条件与最小执行循环"},{"content":" 手把手教你从零开发一个 Agent（1）。本期主要深入讲：什么是 Function Calling，什么是 tools，以及 LLM 实际看到了什么。\n工具调用不是模型直接执行了你的函数。模型做的是读取工具说明，生成一段结构化的调用请求；真正的函数由应用侧执行，结果再回传给模型，由模型组织最终回复。\n前言 经过上一期，你应该清楚 LLM 看到的消息是什么样子的。这个消息列表就是它的 Context——上下文，我在公众号之前的扫盲系列里也简单讲过。\n我们发送的 messages 列表不会直接以 JSON 原样喂给模型，而会经过模型服务中的 Tokenizer 和 Chat Template，转换成带有特殊 Token 的文本序列。\n比如我们发送：\njson 1 {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你好，我叫小安落滢\u0026#34;} 到了模型那里，会被转换成类似下面的内容。不同模型的格式会有所区别：\ntext 1 2 3 \u0026lt;|im_start|\u0026gt;user 你好，我叫小安落滢 \u0026lt;|im_end|\u0026gt; 特殊 Token 对于大多数 Hugging Face 开源对话模型，特殊 Token 和对话格式通常可以在 tokenizer_config.json、tokenizer.json 中找到；有些项目还会单独提供 chat_template.jinja。\n以我写作时查看的 Qwen3.6 为例，打开它的 tokenizer_config.json 就能看到：\n图里框出的就是模型定义的特殊 Token。chat_template 则是一段 Jinja 模板：\n例如处理 user 消息时，核心代码就是：\njinja 1 2 3 \u0026lt;|im_start|\u0026gt;{{ message.role }} {{ content }} \u0026lt;|im_end|\u0026gt; 也就是说，发送上面的例子时，实际到达模型的 Prompt 大致会是：\ntext 1 2 3 4 \u0026lt;|im_start|\u0026gt;user 你好，我叫小安落滢 \u0026lt;|im_end|\u0026gt; \u0026lt;|im_start|\u0026gt;assistant 最后那个 \u0026lt;|im_start|\u0026gt;assistant 就是在告诉模型：接下来轮到 Assistant 说话了，请开始续写。LLM 的本质还是接龙，有兴趣可以回头看看扫盲篇。\n模型的爪子——工具 Agent 的一个核心能力就是调用工具。不管是某某 claw，还是某某 paw，名字都取得很形象，要么有爪子，要么干脆就叫爪子。\n那我们最关心的问题应该是：在对话中，模型怎么知道自己可以调用工具？它怎么决定？它究竟看到了什么？\n接下来用一个经典例子，让上期做的 chatbot 变成一个可以帮我查天气的 Agent。\n第一步：准备真实执行的函数 首先，我们要准备一个查天气的函数。\n这里我直接用了手头已有的高德天气 API。它使用城市的 adcode，所以为了让演示足够简单，我把工具限制为只能查询北上广深，再在代码里完成城市名到 adcode 的转换。\n我写作时，高德控制台显示个人开发者有免费调用额度。具体额度可能调整，请以高德天气查询 API 文档和你自己的控制台为准。\n[javascript] 显示已折叠代码（36 行） 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 // 只允许查询这四个城市，code 来自 AMap_adcode_citycode.xlsx const cityCodes = { 北京: \u0026#34;110000\u0026#34;, 上海: \u0026#34;310000\u0026#34;, 广州: \u0026#34;440100\u0026#34;, 深圳: \u0026#34;440300\u0026#34;, }; // 模型请求 get_weather 时，真正执行的函数 export async function getWeather({ city }) { const cityCode = cityCodes[city]; if (!cityCode) { return { error: \u0026#34;目前只支持查询北京、上海、广州和深圳。\u0026#34; }; } const url = new URL(\u0026#34;https://restapi.amap.com/v3/weather/weatherInfo\u0026#34;); url.search = new URLSearchParams({ key: process.env.AMAP_API_KEY, city: cityCode, extensions: \u0026#34;base\u0026#34;, output: \u0026#34;JSON\u0026#34;, }); const response = await fetch(url); if (!response.ok) { throw new Error(`天气接口请求失败：${response.status}`); } const data = await response.json(); if (data.status !== \u0026#34;1\u0026#34;) { throw new Error(data.info || \u0026#34;天气接口返回错误\u0026#34;); } return data.lives?.[0] ?? data; } 这才是真实执行的函数。接下来还要准备一份给模型看的内容，因为模型不能直接读取并运行我本地的 JavaScript 函数。\n第二步：准备给模型看的工具描述 [javascript] 显示已折叠代码（19 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 传给模型的工具描述 export const weatherTool = { type: \u0026#34;function\u0026#34;, name: \u0026#34;get_weather\u0026#34;, description: \u0026#34;查询北京、上海、广州或深圳的天气\u0026#34;, parameters: { type: \u0026#34;object\u0026#34;, properties: { city: { type: \u0026#34;string\u0026#34;, enum: Object.keys(cityCodes), description: \u0026#34;城市名称，只能是北京、上海、广州或深圳\u0026#34;, }, }, required: [\u0026#34;city\u0026#34;], additionalProperties: false, }, strict: true, }; 这里有四个关键部分：\nname：模型要返回的函数名； description：告诉模型什么时候应该使用它； parameters：用 JSON Schema 描述参数； strict: true：约束模型生成符合 Schema 的参数。 strict: true 约束的是调用参数的结构，并不意味着模型一定会在正确的时机选择正确的工具，也不意味着工具已经被执行。\n第三步：发起带工具的请求 现在第一个 Responses 请求长这样：\njavascript 1 2 3 4 5 6 7 8 const input = [{ role: \u0026#34;user\u0026#34;, content: userInput }]; let response = await client.responses.create({ model: \u0026#34;gpt-5.5\u0026#34;, instructions: \u0026#34;你是天气助手。用户询问天气时，使用 get_weather 工具。\u0026#34;, tools: [weatherTool], input, }); 我写本文时还没有启动本地部署的 Qwen3.6，所以运行示例一直使用 gpt-5.5。下面关于 Chat Template 的具体格式来自可检查源码的 Qwen 等开源模型，用它解释底层更容易复现；OpenAI 闭源模型内部究竟怎样拼接模板无法直接检查，不能断言它使用完全相同的 XML 或 Prompt 格式。\n那么，在可以观察的 Qwen 实现里，模型会看到什么？\ntools 会由 Chat Template 转换成工具说明。按照上面的例子，Qwen3 系列的模板会生成类似下面的内容：\n[text] 显示已折叠代码（42 行） 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 \u0026lt;|im_start|\u0026gt;system # Tools You have access to the following functions: \u0026lt;tools\u0026gt; { \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;查询北京、上海、广州或深圳的天气\u0026#34;, \u0026#34;parameters\u0026#34;: { ... } } \u0026lt;/tools\u0026gt; If you choose to call a function ONLY reply in the following format with NO suffix: \u0026lt;tool_call\u0026gt; \u0026lt;function=example_function_name\u0026gt; \u0026lt;parameter=example_parameter_1\u0026gt; value \u0026lt;/parameter\u0026gt; \u0026lt;/function\u0026gt; \u0026lt;/tool_call\u0026gt; \u0026lt;IMPORTANT\u0026gt; Reminder: - Function calls MUST follow the specified format - Required parameters MUST be specified ... \u0026lt;/IMPORTANT\u0026gt; 你是天气助手。用户询问天气时，使用 get_weather 工具。 \u0026lt;|im_end|\u0026gt; \u0026lt;|im_start|\u0026gt;user 深圳天气怎么样？ \u0026lt;|im_end|\u0026gt; \u0026lt;|im_start|\u0026gt;assistant 到这一步，所谓的模型调用工具就很清楚了：模型先输出一段符合约定格式的内容，再由模型服务或解析器把它转换成 API 对外提供的标准 function_call Item。\n这里要把事实边界说清楚：上面这段 XML 风格格式来自 Qwen 等可观察的开源 Chat Template，不能推广成所有模型供应商都使用相同内部实现。对于 OpenAI，我们能从公开 API 确认的是：请求中传入了工具定义，响应中会返回标准的 function_call，但闭源服务内部怎样组织 Prompt 不能直接下结论。\n早期模型能力不足时，经常无法稳定遵循指令，输出不了标准的 XML 或 JSON，所以服务层和应用层都需要做更多校验。现在有了更成熟的 Tool Calling 训练和 strict Schema 约束，稳定性高了很多，但应用侧仍然应该校验参数和权限。\n所以，模型并不是凭空“知道”自己可以调用工具。更准确地说，是我们在每次请求中把工具说明显式传给模型服务，模型依据这些说明决定要不要生成一次 Tool Call。\n执行工具和执行之后 OpenAI 官方把 Function Calling 总结为五步：\n带着可用工具向模型发起请求； 接收模型返回的 Tool Call； 在应用侧执行对应代码； 把工具结果再次发送给模型； 接收最终回复，或者继续处理新的 Tool Call。 第一步：读取模型返回的函数调用 我们发送刚才的请求后，可以从 response.output 中找到 function_call：\n[javascript] 显示已折叠代码（15 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const toolCall = response.output.find( (item) =\u0026gt; item.type === \u0026#34;function_call\u0026#34; ); if (!toolCall) { throw new Error(\u0026#34;模型本轮没有返回函数调用\u0026#34;); } const args = JSON.parse(toolCall.arguments); console.log(\u0026#34;\\nAI 输出 tool use 的内容：\u0026#34;); console.log({ name: toolCall.name, arguments: args, }); text 1 2 AI 输出 tool use 的内容： { name: \u0026#39;get_weather\u0026#39;, arguments: { city: \u0026#39;深圳\u0026#39; } } 如果直接查看 HTTP 响应，会看到一个包含 name、arguments 和 call_id 的 function_call Item：\n其中 arguments 是 JSON 字符串，需要解析后再交给真实函数；call_id 则是这次工具请求的关联 ID，回传结果时必须原样带回。\n第二步：路由到真实工具 javascript 1 2 3 4 5 6 7 async function callFunction(name, args) { if (name === \u0026#34;get_weather\u0026#34;) { return getWeather(args); } throw new Error(`未知工具：${name}`); } 这个路由做的事情很简单：根据模型返回的函数名和参数，调用我们真正允许执行的函数。\n实际工具结果如下：\njavascript 1 2 3 4 5 6 7 8 9 10 11 12 13 { province: \u0026#39;广东\u0026#39;, city: \u0026#39;深圳市\u0026#39;, adcode: \u0026#39;440300\u0026#39;, weather: \u0026#39;阴\u0026#39;, temperature: \u0026#39;27\u0026#39;, winddirection: \u0026#39;东南\u0026#39;, windpower: \u0026#39;≤3\u0026#39;, humidity: \u0026#39;87\u0026#39;, reporttime: \u0026#39;2026-07-29 17:00:17\u0026#39;, temperature_float: \u0026#39;27.0\u0026#39;, humidity_float: \u0026#39;87.0\u0026#39; } 第三步：回传工具结果，再请求一次模型 公众号发布时，这里只写了 input.push(toolOutput)，但没有展示 toolOutput 的结构，也漏掉了第二次请求。完整代码应该是：\n[javascript] 显示已折叠代码（22 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const result = await callFunction(toolCall.name, args); // 保留模型本轮返回的所有 Items，包括 function_call。 // 对推理模型来说，这也能保留可能同时返回的 reasoning Items。 input.push(...response.output); // 用同一个 call_id 告诉模型：这是刚才那次函数调用的执行结果。 input.push({ type: \u0026#34;function_call_output\u0026#34;, call_id: toolCall.call_id, output: JSON.stringify(result), }); // 把工具结果再次发给模型，让它生成面向用户的最终回复。 response = await client.responses.create({ model: \u0026#34;gpt-5.5\u0026#34;, instructions: \u0026#34;你是天气助手。用户询问天气时，使用 get_weather 工具。\u0026#34;, tools: [weatherTool], input, }); console.log(response.output_text); 这里最容易混淆的点是：OpenAI SDK 会帮我们序列化请求、发送 HTTP 和解析响应，但不会替我们执行 getWeather。工具路由、参数校验、权限控制、实际执行和结果回传，仍然是 Agent 应用自己的工作。\n当模型收到 function_call_output 后，它才补全了缺失的信息，可以根据最新的深圳天气组织自然语言回复。至此，一次完整的工具调用结束。\n循环 上期我们说，一次对话容易，多轮对话加个循环就行了。现在工具在一轮里也可能不只执行一次：模型执行完一个工具后，还可能判断需要再调用另一个工具，甚至一次返回多个可以并行执行的调用。\nOpenAI 官方文档也明确说明，Responses 的工具调用流程可以持续任意多次，直到模型返回最终消息，或者我们的预算、轮数与安全策略要求停止。\n简单啊，再加个循环吧。\n我们下期再讲。\n继续阅读 手把手教你从零开发一个 Agent（0）：Responses API 与多轮对话记忆 从 0 开发一个 agent（1）：Agent runtime 的架构草图 参考资料 OpenAI Function Calling 指南 OpenAI Responses API：Create a model response 高德地图天气查询 API 微信公众号发布版：模型到底是怎么调用工具的 ","permalink":"/posts/%E6%A8%A1%E5%9E%8B%E5%88%B0%E5%BA%95%E6%98%AF%E6%80%8E%E4%B9%88%E8%B0%83%E7%94%A8%E5%B7%A5%E5%85%B7%E7%9A%84/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e手把手教你从零开发一个 Agent（1）。本期主要深入讲：什么是 Function Calling，什么是 tools，以及 LLM 实际看到了什么。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e工具调用不是模型直接执行了你的函数。模型做的是读取工具说明，生成一段结构化的调用请求；真正的函数由应用侧执行，结果再回传给模型，由模型组织最终回复。\u003c/p\u003e","title":"模型到底是怎么调用工具的：从 Chat Template 到 Function Calling"},{"content":" 本系列应该会放 Coding Agent 的教程，以及如何开发一个 Agent，后续会往 pi-agent 的实现思路继续学习。\n我会按真实开发经历梳理记录，并尽量从需求倒推实现。这样比较好理解，但弊端也很明显：我会在这个过程中踩坑，而且有些坑可能要过一阵子才意识到。再叠个甲，笔者也是小菜鸟，如有错误，欢迎指出。\n我比较熟悉 Python 和 Java，原本打算用 Python 写这个教程。但是为了逼自己多学一点，最后选择了 JS/TS，也因为我现在学习的一个 Agent 项目使用的就是 JavaScript 技术栈。\n本项目尽量不使用会增加理解负担的框架，比如 LangChain。先把底层过程亲手写一遍，后面再看框架替我们做了什么。\n从第一次 API 调用，到让模型拥有多轮对话记忆。先把 LLM 调通，再理解上下文究竟是怎么被“记住”的。\n首先，来个 Hello World 我们先要知道怎么调用 LLM。本文会同时提到 OpenAI 的 /chat/completions 和 /responses：前者出现得更早、兼容范围更广，也更容易理解对话补全结构；实际代码则主要使用 OpenAI JavaScript SDK 的 Responses API，同时拆开看看 SDK 最终发出的 HTTP 请求。\nOpenAI、Claude、Gemini 的接口都有自己的规范。由于 OpenAI 的先发优势，不少服务早期都会兼容 OpenAI 的接口范式。各家的参数名、结构和特性不完全一样，但大部分概念都能互相转换。因此开发 Agent 时通常会做一层抽象，用来适配不同的 provider。这也是许多框架已经替我们做好的事情，不过本文先暂时不引入。\n参考文档：Gemini API、Claude Messages API、OpenAI API。\n调用官方 SDK，完成第一次请求 首先我们来看 OpenAI 官方的 API 文档中的调用例子，非常简单。\n跑一下试试\n和官方示例不同的是，我的 JavaScript 代码里引入了 dotenv/config，用来自动把 .env 文件加载到 Node.js 运行环境。.env 里放 API Key，也可以配置 Base URL。\ntext 1 2 OPENAI_API_KEY=sk-123 OPENAI_BASE_URL=https://你的域名/v1 这样配置后，就不用在 new OpenAI() 时重复填写这些参数，SDK 会自动从环境变量读取。\n可以看到我们发起了运行后，GPT 给我们了回复，这个过程等了十几秒，这个调用链如下\ntext 1 2 3 4 5 6 7 代码 | | OpenAI SDK | | OpenAI API 拆开 SDK，看清真正的 HTTP 请求 那么 SDK 里做了什么？我点进 create 方法看了一下，能看到多个函数重载，最后落到下面这段。内部实现可能会随着 SDK 版本变化，重点是它最终仍然在发送 HTTP 请求。\n[javascript] 显示已折叠代码（19 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 create( body: ResponseCreateParams, options?: RequestOptions, ): APIPromise\u0026lt;Response\u0026gt; | APIPromise\u0026lt;Stream\u0026lt;ResponseStreamEvent\u0026gt;\u0026gt; { return ( this._client.post(\u0026#39;/responses\u0026#39;, { body, ...options, stream: body.stream ?? false, __security: { bearerAuth: true }, }) as APIPromise\u0026lt;Response\u0026gt; | APIPromise\u0026lt;Stream\u0026lt;ResponseStreamEvent\u0026gt;\u0026gt; )._thenUnwrap((rsp) =\u0026gt; { if (\u0026#39;object\u0026#39; in rsp \u0026amp;\u0026amp; rsp.object === \u0026#39;response\u0026#39;) { addOutputText(rsp as Response); } return rsp; }) as APIPromise\u0026lt;Response\u0026gt; | APIPromise\u0026lt;Stream\u0026lt;ResponseStreamEvent\u0026gt;\u0026gt;; } this._client.post('/responses', { ... }) 这里就是 HTTP 请求。极度简化后，大概等价于：\njavascript 1 2 3 4 5 6 7 8 9 10 11 fetch(\u0026#34;https://api.openai.com/v1/responses\u0026#34;, { method: \u0026#34;POST\u0026#34;, headers: { \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: \u0026#34;gpt-5.5\u0026#34;, input: \u0026#34;Write a short bedtime story about a unicorn.\u0026#34;, }), }); 再直接调用 REST 接口试试看是不是一样的。\nbash 1 2 3 4 5 6 7 curl https://api.openai.com/v1/responses \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -H \u0026#34;Authorization: Bearer $OPENAI_API_KEY\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;gpt-5.5\u0026#34;, \u0026#34;input\u0026#34;: \u0026#34;Write a short bedtime story about a unicorn.\u0026#34; }\u0026#39; 可以看到 Response 里有密密麻麻的很多数据。真正的回复文本位于 output 中的 message Item 里，而 SDK 额外提供了 response.output_text 这个便捷字段，帮我们把文本结果取了出来。\n._thenUnwrap((rsp) =\u0026gt; { ... }) 是 SDK 对 Promise 的封装。响应回来后，它会先检查是不是 Responses API 返回的数据，然后执行 addOutputText(rsp)。\n所以我们才能直接读取 response.output_text。调用链条如下：\n[text] 显示已折叠代码（18 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 你写: client.responses.create() ↓ Responses.create() ↓ this._client.post(\u0026#34;/responses\u0026#34;) ↓ HTTP POST ↓ OpenAI API ↓ 返回 JSON ↓ addOutputText() ↓ response.output_text ↓ 你的 console.log() 让程序接收用户输入 那怎么做出聊天一样一来一回的效果？Hello World 的第二课一般就会教这个吧：先接收用户输入，再用它替换固定的 input。\n这里包含函数调用、回调函数和 async 箭头函数。如果看不懂，辛苦你先找 AI 补一下 JavaScript 语法咯。\n[javascript] 显示已折叠代码（21 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 加载 .env 文件的环境变量 import \u0026#34;dotenv/config\u0026#34;; import OpenAI from \u0026#34;openai\u0026#34;; // 获取用户输入的库 import readline from \u0026#34;readline\u0026#34;; const client = new OpenAI(); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question(\u0026#34;Please enter your prompt: \u0026#34;, async (userInput) =\u0026gt; { const response = await client.responses.create({ model: \u0026#34;gpt-5.5\u0026#34;, input: userInput, }); console.log(\u0026#34;\\nAI Response:\u0026#34;, response.output_text); }); 运行一下，终端会显示 Please enter your prompt:。接下来输入的一行内容会作为 userInput 变量进入函数。\nAI 完成回复后，终端会打印 AI Response: 以及回复内容。\n此时此刻，聪明如你，你一定会说，哎呀是不是加个循环就做完一个 chatbot 了。\n可以，那我们继续。\n怎么进行多轮对话？ 如果只是简单加上循环，我们的确可以和 AI 进行一轮又一轮对话，只要不退出程序就行。但很快会发现：每次发送的 input 只有本轮输入，AI 怎么知道我们之前讲了什么？也就是 AI 的短期记忆从哪里来？\nChat Completions 的“记忆”来自 messages 这里岔开回答一下。先看开篇提到的 /chat/completions，这是较早、也是各家兼容很广的一套接口范式。它的传入参数包含 messages，大概长这样：\njavascript 1 2 3 4 5 6 7 client.chat.completions.create({ model: \u0026#34;gpt-5.5\u0026#34;, messages: [ { role:\u0026#34;system\u0026#34;, content:\u0026#34;你是一位善解人意的助手\u0026#34; }, { role:\u0026#34;user\u0026#34;, content:\u0026#34;你好\u0026#34; } ] }) 聪明如你不难看出来这个消息的结构了 分别是系统提示词和用户的输入，此时发送后会收到ai 的输出，如果要进行多轮会话的话就可以往这个数组里面追加 ai 的输出和用户新的输入，看起来就长这样\njavascript 1 2 3 4 5 6 7 8 9 client.chat.completions.create({ model: \u0026#34;gpt-5.5\u0026#34;, messages: [ { role: \u0026#34;system\u0026#34;, content: \u0026#34;你是一位善解人意的助手\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;你好\u0026#34; }, { role: \u0026#34;assistant\u0026#34;, content: \u0026#34;你好！有什么可以帮助你的吗？\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;我今天在跟着安落滢学习从 0 到 1 开发 Agent\u0026#34; } ] }) 看懂了吧，把 AI 的回答也放进 messages，再把用户的新问题继续追加进去。整个对话历史会一直保留下来，因此模型每次都能“看到”之前聊过什么。\n模型本身其实没有“会话”这个概念，它不会替你记住上一句话。对这种手动维护 messages 的方式来说，每一次调用 /chat/completions 都是一次新的请求。\n之所以 AI 能够进行连续对话，并不是模型拥有了记忆，而是因为我们每次请求时，都把之前的聊天记录重新发送给了模型。\n也就是说，对于模型而言，它看到的永远都是一份完整的 messages 数组。\n当然，这种做法也带来了一个新的问题。\n聊天记录会越来越长，每一次请求都需要把历史重新发送给模型。随着 Token 数量不断增加，请求成本会越来越高，响应速度也会越来越慢，最终还会触碰模型的上下文长度限制。\nResponses 如何用 previous_response_id 串起上下文 那 Responses API 是怎么知道聊天记录的？\n如果请求走 OpenAI 官方 API，而且没有关闭存储，那么服务端可以帮我们把上下文串起来。\nResponses 默认会存储生成的 Response，也可以用 store: false 关闭。请求完成后会返回一个类似 resp_xxxx 的 Response ID。下一次请求带上 previous_response_id，服务端就能把上一轮上下文串起来，再与本轮 input 一起交给模型。\n调用链变成了这样\n1 2 3 4 5 6 7 8 9 10 你的代码 | | OpenAI API | | OpenAI 的服务端存储（翻出上一轮的内容） | | 模型 试试看代码\n[javascript] 显示已折叠代码（22 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import \u0026#34;dotenv/config\u0026#34;; import OpenAI from \u0026#34;openai\u0026#34;; const client = new OpenAI(); const response1 = await client.responses.create({ model: \u0026#34;gpt-5.5\u0026#34;, input: \u0026#34;你好，我叫小安落滢\u0026#34;, store: true, }); console.log(response1.output_text); console.log(response1.id); const response2 = await client.responses.create({ model: \u0026#34;gpt-5.5\u0026#34;, previous_response_id: response1.id, // 把上一轮串起来 input: \u0026#34;我叫什么名字？\u0026#34;, store: true, }); console.log(response2.output_text); 输出\ntext 1 2 3 你好，小安落滢！很高兴认识你 😊 resp_0c65... 我不知道你的名字。你可以告诉我你叫什么，我就能这样称呼你。 怎么回事，怎么没生效？哈哈，还记得前面提到的 .env 吗？我配置了自己的代理服务地址，而这个代理服务并没有替我支持 previous_response_id。\n不过 Responses API 并不是只能依赖服务端状态，它也支持消息数组，只是参数名从 messages 变成了 input：\njavascript 1 2 3 4 5 6 7 8 9 const response = await client.responses.create({ model: \u0026#34;gpt-5.5\u0026#34;, input: [ { role: \u0026#34;system\u0026#34;, content: \u0026#34;你是一位善解人意的助手\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;你好\u0026#34; }, { role: \u0026#34;assistant\u0026#34;, content: \u0026#34;你好！有什么可以帮助你的吗？\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;我今天在跟着安落滢学习从 0 到 1 开发 Agent\u0026#34; } ] }); 眼熟吧，这和前面 Chat Completions 的 messages 几乎一样，只是换了参数名。在 Responses API 里，input 接受的是更通用的 Items 数组，message 只是其中一种 Item。\n把对话历史握在自己手里 既然我的代理不支持 previous_response_id，那我就自己维护历史呗。改造一下 chatbot：\n[javascript] 显示已折叠代码（35 行） 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 import \u0026#34;dotenv/config\u0026#34;; import OpenAI from \u0026#34;openai\u0026#34;; import readline from \u0026#34;readline\u0026#34;; const client = new OpenAI(); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const history = []; // 自己维护的对话历史 function chat() { rl.question(\u0026#34;你: \u0026#34;, async (userInput) =\u0026gt; { if (userInput === \u0026#34;exit\u0026#34;) { rl.close(); return; } history.push({ role: \u0026#34;user\u0026#34;, content: userInput }); const response = await client.responses.create({ model: \u0026#34;gpt-5.5\u0026#34;, input: history, }); history.push(...response.output); // 注意：是整个 output console.log(\u0026#34;AI:\u0026#34;, response.output_text); chat(); }); } chat(); 跑一下试试，这次终于有记忆了。\n不要只保存 AI 回复的文本 这里有个非常容易踩的坑，必须展开说说：不要只保存 AI 回复的文本。\n如果是 chat/completions，以前的我会习惯性这么存：\njavascript 1 2 3 4 history.push({ role: \u0026#34;assistant\u0026#34;, content: response.output_text // ❌ 别这么干 }); 纯聊天阶段这么干可能看不出问题，但 Responses API 返回的 output 是一个 Item 数组。除了文本消息，还可能有 reasoning（推理）、function_call（工具调用）等内容。等后面讲到 function calling，模型说“我要调一个工具”，这个动作本身就是一个 Item。如果只存文本，下一轮模型就不知道自己上一轮发起过工具调用，轻则表现奇怪，重则直接报错。\n所以从一开始就养成习惯：手动维护状态时，把整个 response.output 铺进 history，而不是只拿 response.output_text。\n好，代码跑通了。现在回过头来聊聊那个更重要的问题：为什么要用 Responses API，以及这次踩坑到底说明了什么。\n为什么选择 Responses API 作为主接口 先说结论：各家接口的范式都不一样，但背后的“哲学”是相通的。我们要学的是这个哲学，而不是某一家具体的参数写法。\n这次我的代理不支持 previous_response_id，导致服务端记忆失效，其实就是一次小型的“厂商差异”事故。OpenAI 有 Responses 和 Chat Completions 两套范式，Claude 是 Messages API，Gemini 是 generateContent。参数名、结构和特性各不相同，但拨开这些表象，底层思想是一致的：\n模型本身无状态。 它不会凭空记住你，每一次请求都是一次新的计算。\n上下文需要被重新带上。 区别只在于历史存在哪里、由谁来拼：客户端自己维护 messages / input 数组，或者服务端通过 previous_response_id 串起来。\n数据结构是一组 Items。 消息、推理、工具调用和工具结果，本质上都是这个序列里的条目。\n理解了这三点，再看任何一家的文档都不会慌——不过是同一哲学的不同方言罢了。\n这个思路对我们要做的 Agent 系统还有一个更实际的启发：Responses 的服务端存储设计，我们完全可以在自己的系统里实现。OpenAI 用 Response ID 串链，我们也可以在自己的 Agent 里做一个 conversation store——给每轮对话分配 ID，把历史存在自己的存储里，需要时翻出来拼好再发给上游模型。这样做的好处是：\n数据握在自己手里，不依赖上游的存储策略，也不用担心代理不支持某个参数。\n更换上游时，历史格式是我们自己的，不用跟着 provider 的格式迁移。\n减轻调用方压力，调用方只需要携带会话 ID，不用每次扛着越来越长的数组。\n至于各家 provider 接口不同的问题，就回到本系列开篇提到的抽象层。对上游，我们分别适配 OpenAI、Claude、Gemini；对外，暴露自己设计的统一接口。这就是 LangChain 等框架替你做的事情，只不过这个系列里我们会亲手把它写出来。写完以后，就知道框架里面到底装了什么，而不是把它当黑盒。\n那为什么选择 Responses API 作为系列的“主接口”？因为它更适合 Agent 工作流：工具调用、推理过程、灵活的 Items，以及可选的服务端状态都是一等公民。后面讲 function calling、工具结果回传和多步推理，都可以顺着这条线继续往下长，不用半路迁移接口。Chat Completions 仍然值得了解——它是历史，也是很多服务兼容的通用语，但我们的车要往前开。\n下一篇 到这里，多轮对话的记忆问题算是解决了。但前面埋下的问题还在：历史会越滚越长，Token 越烧越多，迟早撞上上下文上限。这个留到“上下文管理”再收拾。\n下一篇，先让 AI 长出手来——function calling。\n如果想先看整个 Agent runtime 的架构草图，也可以继续看从 0 开发一个 agent（1）。\n参考资料 OpenAI Responses API OpenAI Conversation state Migrate to the Responses API Claude Messages API Gemini generateContent 微信公众号发布版：手把手教你从零开发一个 Agent · 第 0 篇 ","permalink":"/posts/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA-agent0/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e本系列应该会放 Coding Agent 的教程，以及如何开发一个 Agent，后续会往 pi-agent 的实现思路继续学习。\u003c/p\u003e\n\u003cp\u003e我会按真实开发经历梳理记录，并尽量从需求倒推实现。这样比较好理解，但弊端也很明显：我会在这个过程中踩坑，而且有些坑可能要过一阵子才意识到。再叠个甲，笔者也是小菜鸟，如有错误，欢迎指出。\u003c/p\u003e","title":"手把手教你从零开发一个 Agent（0）：Responses API 与多轮对话记忆"},{"content":"我本地刚提交了一点东西，还没 push，为了保持 git 链路干净，我执行 git pull --rebase，我对他的概念不够透彻，然后冲突了。\n打开 VS Code 一看：\nCurrent Change 不是我刚写的 Incoming Change 反而是我刚写的 第一反应是：这不对吧？？？\n后来才发现，不是 VS Code 标错了，是我一直把 Current 和 Incoming 脑补成了“我的”和“别人的”。这两个词对我这个英语菜鸟而言真的很容易把人带歪。\n其实 Git 不关心代码是谁写的。它只关心当前正在做什么动作：\nCurrent：当前工作树里已经放着的内容 Incoming：这一步正在尝试应用进来的修改 在普通 merge 里，Current 往往是我的本地分支；到了 rebase，Git 会先站到新基线上，再重新应用我的 commit，于是 Current 和 Incoming 看起来就“反了”。\n我干脆用两个临时 clone 真跑了一遍，也顺手把 Fast-forward、Merge commit、Squash 和 Rebase 这几种常见合并方式整理清楚。\n先说结论：Git 合并策略怎么选 我现在的选法很朴素：\n没有分叉时用 Fast-forward；只想保留 PR 最终结果时用 Squash；需要保留分支合并上下文时用 Merge commit；想保持线性历史，而且这些 commit 还没有被别人依赖时，再考虑 Rebase。\n场景 更适合的选择 原因 本地和远程没有分叉，只想安全更新 git pull --ff-only 只移动分支指针，不额外制造提交 自己的 feature 分支要同步最新 main，commit 还没被别人依赖 git fetch origin + git rebase origin/main 主线保持线性，冲突也在本地解决 PR 里有很多 wip、fix typo Squash and merge 主线只保留一条完整结果 一个 PR 本身是一段需要保留的协作上下文 Create a merge commit 能看出分支何时、以什么整体合入 团队明确要求线性历史，又想保留 PR 内每个 commit 的颗粒度 Rebase and merge 不生成 merge commit，但会重写 commit hash 我的底线就两条：公共分支别随便 rebase；push 被拒时先看分叉，别上来就 --force。\n至于 VS Code 冲突界面，也不要把 Current 当成“我的”、Incoming 当成“别人的”。先看你正在 merge 还是 rebase。\nGit merge、squash、rebase 到底有什么区别 严格来说，这里比较的是日常协作中的“历史整合方式”。Git 文档里的 merge strategy 特指 ort、octopus、ours 等内部合并算法。平时大家说“Git 合并策略怎么选”，讨论的通常还是前者。\n方式 整合后的历史 原提交 hash 是否保留这次分叉拓扑 Fast-forward 只把分支指针向前移动 不变 原本就没有分叉 Merge commit 新增一个 merge commit 不变 保留 Squash 新增一个代表最终结果的普通 commit 原 commit 不进入目标分支历史 不保留 Rebase 把当前分支的 commit 重新放到新基线上 被 replay 的 commit 会改变 通常不保留这次分叉拓扑 Fast-forward：没有分叉时只移动指针 Fast-forward 是最简单的情况：当前分支正好是待合入分支的祖先，Git 只需要把当前分支指针往前移动。\nflowchart LR A[\u0026#34;A\u0026#34;] --\u0026gt; B[\u0026#34;B\u0026#34;] B --\u0026gt; C[\u0026#34;C: feature 新提交\u0026#34;] B -. \u0026#34;merge 前 main 在这里\u0026#34; .-\u0026gt; M1[\u0026#34;main\u0026#34;] C -. \u0026#34;ff 后 main 直接挪到这里\u0026#34; .-\u0026gt; M2[\u0026#34;main / feature\u0026#34;] Fast-forward 就是“这条路没分叉，牌子往前挪一下就行”。\n整个过程不会产生新的 merge commit。git merge --ff-only \u0026lt;branch\u0026gt; 和 git pull --ff-only 还可以把它当成一道保险：能快进就继续，发现历史已经分叉就停下来，让我自己决定该 merge 还是 rebase。\nMerge commit：保留分支合并上下文 如果两个分支都从共同祖先继续产生了提交，普通 git merge 会把两条历史汇合起来，并生成一个有多个 parent 的 merge commit。\n优点：\n保留真实的分支拓扑 能看出这个 PR 是什么时候整体合进来的 一个 PR 本身就是完整事件时，上下文不会被压平 缺点：\n历史会长得比较乱 小改动也留一个 merge commit，有时候看着烦 较新的 Git 在合并两个分支时默认使用 ort 策略。平时不用专门操作它，但命令输出里经常会看到：\ntext 1 Merge made by the \u0026#39;ort\u0026#39; strategy. Squash merge：只把最终结果放进主线 Squash merge 只把分支的最终改动合成一个新 commit 放到目标分支，不保留原分支每个 commit 的颗粒度，也不会像普通 merge 那样记录双方的合并关系。\n适合那种：\nPR 里有很多 fix typo wip 再修一次 真的最后一次 这种过程型 commit 如果全进主线，未来看历史会很痛苦。代价也很明确：原来的 commit 颗粒度没了。\n所以它适合“过程不重要，最终结果重要”的 PR。\n本地 git merge --squash 只会更新工作树和暂存区，还需要手动 commit；GitHub 的 Squash and merge 则会直接创建那条合并后的 commit。\nRebase：把提交重新应用到新基线 rebase 也能整合两条历史，但动作和 merge 不一样。可以先把它理解成：\n先把我自己的 commit 暂时拿下来 把当前分支移动到新的基线 再把我的 commit 一个一个重新播放上去 rebase 前：\nflowchart LR A[\u0026#34;A\u0026#34;] --\u0026gt; B[\u0026#34;B\u0026#34;] B --\u0026gt; C[\u0026#34;C: origin/main 新提交\u0026#34;] B --\u0026gt; D[\u0026#34;D: 我本地提交\u0026#34;] rebase 后：\nflowchart LR A[\u0026#34;A\u0026#34;] --\u0026gt; B[\u0026#34;B\u0026#34;] --\u0026gt; C[\u0026#34;C: origin/main 新提交\u0026#34;] --\u0026gt; D[\u0026#34;D\u0026#39;: replay 后的新提交\u0026#34;] 优点：\n历史很直 小分支同步 main 时很舒服 PR 合进去之后主线比较干净 缺点：\n被重新播放的 commit 会获得新的 commit hash 如果你 rebase 了已经被别人基于开发的公共分支，会让别人很难受 冲突时 Current / Incoming 很容易把人绕晕 也正因为 Git 是把 commit 一个个重新播放，冲突可能不只解决一次。一个 commit 处理完后执行 git rebase --continue，后面的 commit 如果又撞到同一片代码，还可能继续冲突。\nmerge 与 rebase 冲突中 Current / Incoming 为什么会反转 我用两个临时 clone 模拟了一次协作：\nalice：我本地 bob：另一个人 origin.git：远程仓库 共同起点是：\ntext 1 2 title: hello owner: base 然后：\nBob 把 owner 改成 bob，并且推到了远程 Alice 在没同步的情况下，把 owner 改成 alice 这时历史是分叉的。\nmerge 冲突：Current 是当前分支 Alice 执行：\nbash 1 git pull --no-rebase origin main 真实输出摘一段：\ntext 1 2 3 4 5 6 7 $ git pull --no-rebase origin main From /tmp/git-collab-demo.dg8XHP/origin * branch main -\u0026gt; FETCH_HEAD 120e92f..a1b7aa2 main -\u0026gt; origin/main Auto-merging app.txt CONFLICT (content): Merge conflict in app.txt Automatic merge failed; fix conflicts and then commit the result. 冲突文件里是：\ntext 1 2 3 4 5 6 title: hello \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD owner: alice ======= owner: bob \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; a1b7aa2f270ffdaa7b449578a038d9d2d5316eb6 这里 HEAD 是 Alice 当前分支。\n所以在 VS Code 里大概就是：\nVS Code 这次 merge 里是谁 Current Change Alice 本地内容 Incoming Change Bob 远程内容 这看起来很符合直觉，真正把我绕进去的是下面的 rebase。\nrebase 冲突：Current 是新基线 然后我 abort 掉 merge，再来一次：\nbash 1 2 git merge --abort git pull --rebase origin main 真实输出：\ntext 1 2 3 4 5 6 7 8 9 10 11 12 13 $ git pull --rebase origin main From /tmp/git-collab-demo.dg8XHP/origin * branch main -\u0026gt; FETCH_HEAD Rebasing (1/1) Auto-merging app.txt CONFLICT (content): Merge conflict in app.txt error: could not apply 1308a1b... alice edits owner hint: Resolve all conflicts manually, mark them as resolved with hint: \u0026#34;git add/rm \u0026lt;conflicted_files\u0026gt;\u0026#34;, then run \u0026#34;git rebase --continue\u0026#34;. hint: You can instead skip this commit: run \u0026#34;git rebase --skip\u0026#34;. hint: To abort and get back to the state before \u0026#34;git rebase\u0026#34;, run \u0026#34;git rebase --abort\u0026#34;. hint: Disable this message with \u0026#34;git config set advice.mergeConflict false\u0026#34; Could not apply 1308a1b... # alice edits owner 冲突文件变成了：\ntext 1 2 3 4 5 6 title: hello \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD owner: bob ======= owner: alice \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; 1308a1b (alice edits owner) 注意这里，HEAD 现在是 Bob 的内容。因为 rebase 已经先以远程 main 作为新基线，然后才尝试重新播放 Alice 的 commit。\n所以在 VS Code 里大概就是：\nVS Code 这次 rebase 里是谁 Current Change Bob / 当前已经构造好的新历史 Incoming Change Alice / 正在 replay 的 commit 这就是最容易懵的地方：Incoming 居然是我刚写的代码。\n完全正常。不是 VS Code 坏了，是 rebase 的动作决定了它会这样显示。\n这个实验只有一个本地 commit。如果有多个 commit，Git 会按顺序 replay。此时 Current 不只包含最初的新基线，还包含前面已经成功 replay 的结果；Incoming 则是当前正在应用的那一个 commit。\nGit 的 rebase 文档也专门说明了这一点：发生冲突时，ours 是已经构造到当前进度的新历史，theirs 是正在被 rebase 的工作分支。原文最后一句很直接：the sides are swapped。\n一张表记住 Current 与 Incoming 不要把这两个词翻译成“我的”和“别人的”。\n更准确的记法：\n操作 Current Incoming git merge other 当前 checkout 的分支 被 merge 进来的分支 git pull --no-rebase origin main 本地当前分支 本次拉取的远程分支 git rebase origin/main 新基线 + 前面已 replay 的结果 当前正在 replay 的 commit git pull --rebase origin main 本次拉取的分支 + 前面已 replay 的结果 当前正在 replay 的 commit 不带参数的 git pull --rebase 会使用当前分支配置的 upstream，它不一定是 origin/main。feature 分支如果跟踪的是 origin/feature，那它拉取并 rebase 的也是 origin/feature。\n所以 VS Code 那两个按钮只能这样理解：\nAccept Current Change：这个冲突块保留 Current 一侧 Accept Incoming Change：这个冲突块保留 Incoming 一侧 但这不等于 rebase 时应该无脑点 Incoming。真实冲突经常需要把两边内容手工组合起来，直接选一边可能会丢掉另一边的有效修改。\n处理完后再继续：\nbash 1 2 3 4 git status # 手工检查并修改冲突文件 git add app.txt git rebase --continue 如果发现方向不对，可以用 git rebase --abort 回到 rebase 之前。git rebase --skip 会跳过当前整个 commit，可能直接丢掉这一条提交的改动，没看清楚之前不要乱按。\nFast-forward、Merge commit 和 Squash 的真实输出 Fast-forward 输出 text 1 2 3 4 5 6 7 8 9 $ git merge --ff-only ff-topic Updating 6a77bd7..b56f82d Fast-forward story.txt | 1 + 1 file changed, 1 insertion(+) $ git log --oneline --graph --decorate --all --max-count=6 * b56f82d (HEAD -\u0026gt; main, ff-topic) add ff line * 6a77bd7 base commit 可以看到，没有多一个合并节点。\nMerge commit 输出 text 1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ git merge --no-ff noff-topic -m \u0026#34;merge noff topic\u0026#34; Merge made by the \u0026#39;ort\u0026#39; strategy. noff.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 noff.txt $ git log --oneline --graph --decorate --all --max-count=10 * 3f79fbb (HEAD -\u0026gt; main) merge noff topic |\\ | * 2c1c14d (noff-topic) add noff branch file * | cb8421c main moves too |/ * b56f82d (ff-topic) add ff line * 6a77bd7 base commit 这里多出来的 3f79fbb 就是 merge commit。\nSquash merge 输出 [text] 显示已折叠代码（19 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ git merge --squash squash-topic Updating 3f79fbb..9dadb10 Fast-forward Squash commit -- not updating HEAD squash-one.txt | 1 + squash-two.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 squash-one.txt create mode 100644 squash-two.txt $ git status --short A squash-one.txt A squash-two.txt $ git commit -m \u0026#34;squash topic into one commit\u0026#34; [main 0db1460] squash topic into one commit 2 files changed, 2 insertions(+) create mode 100644 squash-one.txt create mode 100644 squash-two.txt 注意这个输出：\ntext 1 Squash commit -- not updating HEAD Squash 只是把改动放进暂存区，最后还要自己 commit 一下。\n--ff、--no-ff 和 --ff-only 别混在一起 上面的 merge 示例里，两条历史本来就已经分叉，即使不写 --no-ff 也会生成 merge commit。三个参数真正的区别是：\n参数 行为 --ff 能 Fast-forward 就直接快进，不能快进时生成 merge commit；这是普通 merge 的常见默认行为 --no-ff 即使能够 Fast-forward，也强制生成 merge commit --ff-only 只允许 Fast-forward；只要历史分叉就直接失败，不替我做决定 GitHub PR 的三种合并按钮怎么选 GitHub PR 常见的三个按钮，对应的是三种不同的主线历史：\nGitHub 按钮 合入后的结果 我会倾向的场景 Create a merge commit 保留分支原 commit，并额外生成 merge commit 一个 PR 就是一段值得保留的完整上下文 Squash and merge 把 PR 的最终改动作为一个新 commit 放进目标分支 小修小补，过程 commit 很碎 Rebase and merge 把 PR 中的 commit 逐个放到目标分支顶端，不生成 merge commit 团队要求线性历史，又想保留 PR 内每个 commit 的颗粒度 GitHub 的 Rebase and merge 会为这些提交生成新的 commit hash。Squash and merge 则只让压缩后的新 commit 进入目标分支历史，原来的那些 commit 不会作为独立提交出现在目标分支上。\nPR 按钮不是信仰问题。主要看这个 repo 是更想保留“协作过程”，还是更想保留“干净结果”。仓库已经有约定时，跟约定走最省事。\nGit 协作里最容易踩的几个坑 push 被拒时先看分叉，不要直接 force 别人已经推了新的 main，我本地还不知道，这时直接 push 会看到类似报错：\ntext 1 2 3 4 5 6 7 $ git push origin main To /tmp/git-common-problems/origin.git ! [rejected] main -\u0026gt; main (fetch first) error: failed to push some refs to \u0026#39;/tmp/git-common-problems/origin.git\u0026#39; hint: Updates were rejected because the remote contains work that you do not hint: have locally. This is usually caused by another repository pushing to hint: the same ref. 这时候不要一怒之下 --force。先 fetch，再看一眼提交图：\nbash 1 2 git fetch origin git log --oneline --graph --decorate --all --max-count=20 确认只是远程向前走了，还是两边真的已经分叉，再决定 merge 还是 rebase。\n--ff-only 失败不是坏事 我比较喜欢用：\nbash 1 git pull --ff-only origin main 只要本地和远程各自都有新提交，它就会停下来：\ntext 1 fatal: Not possible to fast-forward, aborting. 这个报错不是坏事。它是在提醒我：现在已经不是“把指针往前挪一下”能解决的状态了，必须自己选择保留分叉的 merge，或者改写本地提交的 rebase。\nrebase 已推送的 feature 分支要谨慎覆盖 rebase 会改变被 replay commit 的 hash。如果这个 feature 分支以前已经推送过，普通 push 往往会因为不是 Fast-forward 而被拒绝。\n确认这个分支只有自己使用，并且确实需要更新远程历史时，可以明确写出目标分支：\nbash 1 git push --force-with-lease origin my-feature --force-with-lease 只会在远程分支仍处于本地预期位置时允许覆盖；如果远程 ref 已经变化，它会拒绝推送。它比 --force 安全，但不等于绝对保险，更不该拿来随便改写公共分支，尤其是 main。\ngit pull --rebase 不会自动帮 feature 同步 main 不带参数的 git pull --rebase 会先 fetch，再把当前分支 rebase 到它配置的 upstream 上。\n如果 my-feature 跟踪的是 origin/my-feature，那它同步的是远程 feature，不是 origin/main。我想明确把 feature 放到最新主线后面时，更愿意把动作拆开写：\nbash 1 2 git fetch origin git rebase origin/main 这样一眼就知道新基线是谁，也不容易把“拉远程 feature”和“同步主线”混成一件事。\n我现在的默认用法 没有仓库特殊约定时，我目前大概会这样处理：\n更新本地 main：\nbash 1 2 git switch main git pull --ff-only origin main 自己的 feature 分支在合 PR 前同步主线：\nbash 1 2 3 git switch my-feature git fetch origin git rebase origin/main PR 很小、commit 很碎就 Squash；PR 本身是一段完整上下文就保留 merge commit；团队要求线性历史，又想保留 PR 内每个 commit 的颗粒度时，再用 Rebase and merge。\n至于冲突，先看动作，再看内容，不按“我的”和“别人的”猜按钮。\n这个关系想通之后，VS Code 的冲突界面确实少了很多玄学。至少下次看到 Incoming 是自己刚写的代码时，不会第一时间怀疑人生。\n参考资料 Git 官方文档：git merge Git 官方文档：git rebase Git 官方文档：git pull GitHub Docs：About merge methods on GitHub 收工。\n","permalink":"/posts/git-merge-strategies/","summary":"\u003cp\u003e我本地刚提交了一点东西，还没 push，为了保持 git 链路干净，我执行 \u003ccode\u003egit pull --rebase\u003c/code\u003e，我对他的概念不够透彻，然后冲突了。\u003c/p\u003e\n\u003cp\u003e打开 VS Code 一看：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eCurrent Change\u003c/code\u003e 不是我刚写的\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eIncoming Change\u003c/code\u003e 反而是我刚写的\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e第一反应是：这不对吧？？？\u003c/p\u003e\n\u003cp\u003e后来才发现，不是 VS Code 标错了，是我一直把 \u003ccode\u003eCurrent\u003c/code\u003e 和 \u003ccode\u003eIncoming\u003c/code\u003e 脑补成了“我的”和“别人的”。这两个词对我这个英语菜鸟而言真的很容易把人带歪。\u003c/p\u003e","title":"Git 合并策略怎么选：merge、squash、rebase 的区别"},{"content":"先说结论:能跑是真的，能不能用是另一回事，别抱太大期待。\n在我的 RTX 3060 开发机上，GLM-5.2 744B 最终跑到了约 0.44 tok/s。工程上挺有意思，但离正常聊天还差得远。\n这里的“25GB”是项目宣称的最低量级，不是我的整机占用。本次 Linux 可见约 39GiB，进程 RSS 约 27GB；它适合验证 MoE 路由、缓存和磁盘流式推理，不适合交互聊天或 Agent。\n最近看到一个挺吸引眼球的项目:JustVugg/colibri。\n这个词是蜂鸟的意思，借自法语 colibri，源自一加勒比语言。\n它的介绍很直接:\nRun GLM-5.2 (744B MoE) on a 25GB-RAM consumer machine.\n744B 参数、25GB 内存、纯 C、零依赖，剩下的专家权重直接从磁盘读取。\n刷到这个项目就觉得有意思，因为我本地的开发机也能跑(之前 KTransformer 也想跑，无奈服务器跑着一些业务一直腾不出来)，要是这个项目能跑出个位数的 decode 速度就发了哈哈哈。但是 README 看了一圈就明白我的美梦破碎了，话不多说，先看我的折腾记录。\n我有一台常用的开发机，已经被我虚拟化，把一台已经跑 MC 的虚拟机清掉，把 CPU、内存和磁盘资源合并到主力虚拟机上，下载了接近 400GB 的模型，真的把它跑了起来。\n模型能正常对话，也有 OpenAI 兼容 API，仓库自带的 Brain Dashboard 还能看到 19,456 个专家在推理时不断闪烁。\n跑起来是一回事，好不好用是另一回事。文末有在我这个环境实测的 GIF 效果。\n我的测试环境 这次测试跑在一台 PVE 里的 Ubuntu 虚拟机上:\n项目 配置 系统 Ubuntu 24.04，KVM/QEMU CPU 20 vCPU，AVX2/FMA，无 AVX-512/VNNI 内存 Linux 可见约 39GiB，另有 8GiB Swap GPU RTX 3060 12GiB，PCIe Passthrough 模型盘 500GB QEMU SCSI 磁盘，ext4 模型 GLM-5.2-colibri-int4-with-int8-mtp 模型总大小 383.74GB，共 144 个分片 路由专家 19,456 个，共 372.85GB 实际运行时，进程 RSS 大约在 27GB 左右。测试配置里的 8GB VRAM 和 7GB RAM，指的是固定给热门专家用的缓存层，不是整个程序的总内存占用。\n模型仅稠密部分就约有 10.88GB，再加上工作区、KV Cache、专家 LRU 和服务进程，整机只有 10GB RAM 基本跑不稳。\n文中会反复提到 Driver、CUDA Runtime 和 Toolkit；如果这些名字容易混在一起，可以先看我整理的CUDA 与 Driver、Toolkit、PyTorch、Conda 关系。\n所以后文的\u0026quot;8GB VRAM + 7GB RAM\u0026quot;，实际是这么个构成:\ntext 1 2 3 4 8GB VRAM 热门专家层 + 7GB RAM 固定专家层 + 其他运行内存、LRU、Page Cache = 进程 RSS 约 27GB 383GB 的模型是怎么塞进去的 GLM-5.2 是一个 MoE 模型。\nMoE 简单说就是:模型里有非常多\u0026quot;专家\u0026quot;，但每次处理一个 Token 时，并不会把全部专家都算一遍，而是由 Router 挑出其中一小撮参与计算。\ncolibri 就是吃准了这个特点。\n它没有尝试把 383GB 权重全塞进显存或内存，而是把模型拆成了两块:\nAttention、Embedding、Shared Expert、Norm、LM Head 等稠密部分常驻内存。 体积最大的 Routed Experts 放在磁盘上，需要时再读。 专家又分了三层:\ntext 1 2 3 VRAM:根据历史使用频率固定最热门的专家 RAM:固定专家 + 每层 LRU 缓存 Disk:其余绝大多数专家 每个稀疏层有 256 个专家，每个 Token 会路由到最多 8 个。主模型完成一次生成，大约会产生 600 次专家选择。\n一个主模型专家约 18.92MB，所以在完全冷缓存的情况下:\ntext 1 600 × 18.92MB ≈ 11.4GB 一个冷 Token 理论上可能要访问约 11.4GB 专家权重。\n这就是这个项目的核心思路:模型可以不放进内存，但省下来的内存最终会变成磁盘带宽和延迟的账单。\n19,456 个专家全部加载是什么状态 仓库截图里的 Brain Dashboard 把整个模型画成一个 76×256 的大脑，每个小格是一个专家:\n颜色表示专家当前位于 VRAM、RAM 还是 Disk。 亮度表示历史使用热度。 当前请求路由到的专家会闪白。 这台机器有 6 张 GPU，聚合显存约 202GB，同时还有约 264GB 系统内存，全部专家由 VRAM + RAM 一起扛，不是靠显存单独装下。\n我的机器启动时专家加载情况是:\ntext 1 2 3 VRAM:422 RAM:371 Disk:18,663 跑完一次请求、LRU 被填充后:\ntext 1 2 3 VRAM:422 RAM:896 Disk:18,138 即便已经预热过，还是有约 93.2% 的专家留在磁盘上。\n我是怎么测的 为了让不同配置尽量能比，我固定了几个变量:\n同一个中文提示词:简洁解释水循环。 TEMP=0，用 Greedy Decode。 主要比较固定 24 个输出 Token。 用独立模型软链接目录，避免平时积累的 .coli_usage 污染路由画像。 同时记录磁盘服务时间、专家矩阵乘、Attention、命中率、RSS 和 Major Fault。 这台机器最后比较合适的配置是:\nbash 1 2 3 4 5 6 7 8 9 10 11 12 13 DIRECT=1 DRAFT=0 CUDA_EXPERT_GB=8 PIN_GB=15 PIPE=0 RAM_GB=34 CTX=512 COLI_OMP_TUNED=1 OMP_NUM_THREADS=20 OMP_PROC_BIND=spread OMP_PLACES=cores 其中:\nCUDA_EXPERT_GB=8:给热门专家约 8GB 显存。 PIN_GB=15:VRAM 与固定 RAM 专家合计约 15GB，所以 RAM 固定层约 7GB。 DIRECT=1:用 O_DIRECT，尽量绕开容易抖动的 Page Cache 路径。 DRAFT=0:关掉 MTP 推测解码。 PIPE=0:这台机器上开加载流水线没捞到收益。 实际速度:最好也就 0.44 tok/s 主要结果如下:\n配置 Decode 说明 CPU-only，O_DIRECT 0.35 tok/s 修复 OpenMP 亲和性后的 CPU 基线 8GB VRAM + 7GB RAM，普通缓存读取 0.29 tok/s 同画像配对测试 8GB VRAM + 7GB RAM，O_DIRECT 0.45 / 0.43 tok/s 中位数约 0.44 tok/s 相同配置，TOPP=0.7 0.63 tok/s 减少低权重专家，存在质量交换 O_DIRECT 是这轮优化里效果最明显的一项。\n同一组 24 Token 测试:\ntext 1 2 Buffered:0.29 tok/s O_DIRECT:0.45 / 0.43 tok/s 中位速度提升约 52%，专家磁盘服务时间从 61.2 秒降到了 33.1～36.0 秒。\n说白了，这台机器卡脖子的不是 RTX 3060 算不动，是大部分专家还得从磁盘读。\nGPU 专家层把 CPU-only 的 0.35 tok/s 拉到了约 0.44 tok/s，提升约 26%，但 GPU 利用率并不高——毕竟只有约 2% 的专家固定在 VRAM 里，路由经常落到 RAM 或磁盘，GPU 大部分时间在等米下锅。\nTOPP=0.7 能到 0.63 tok/s，是因为它丢掉了一部分权重较低的专家选择，把每个输出 Token 的专家加载量从约 575 降到 326——快是快了，但拿路由质量换的。\n反直觉的一点:RAM 塞多点专家，反而慢了一倍 最开始我想得很简单:磁盘慢，那就多把专家固定在 RAM 里呗。\n结果完全反过来:\n固定专家层 速度 命中率 Major Fault 8GB VRAM，不额外固定 RAM 0.31 tok/s 56.4% 3 8GB VRAM + 7GB RAM 0.33 tok/s 59.2% 6 8GB VRAM + 18GB RAM 0.15 tok/s 63.3% 1,547,638 最后一组命中率明明更高，速度却从 0.33 掉到了 0.15 tok/s。\n问题出在 RAM 不只是拿来固定专家用的。\n系统还要吃:\n稠密权重。 每层专家 LRU。 KV Cache。 计算工作区。 文件 Page Cache。 Python API 与 Web 服务。 我把 18GB RAM 硬塞给专家后，LRU 被压缩到每层只剩一个槽位，操作系统也没了足够的缓存空间，开始频繁回收、重新缺页，最后跑出了超过 154 万次 Major Fault。\n这种磁盘流式推理系统，光看\u0026quot;专家命中率\u0026quot;是会被坑的——RAM 多分给固定专家一点，Page Cache 和动态 LRU 就可能少很多，整体反而更慢。\nMTP 有用吗 模型带了 int8 MTP Head，可以做推测解码:先预测后面的 Token，再让主模型一次验证多个位置，草稿接受率够高就能用更少 Forward 生成更多 Token。\n但在这台机器上，MTP 没帮上忙:\nDRAFT 速度 Tokens/Forward 接受率 每输出 Token 专家加载量 0 0.45 tok/s 1.04 — 575.0 1 0.38 tok/s 1.71 64% 707.7 3 0.21 tok/s 1.71 24% 1,392.3 DRAFT=1 这组短测里接受率有 64%，说明 MTP 不是瞎猜。问题是它虽然省了 Forward 次数，但读的专家种类反而更多了。\n稠密模型验证多个位置能重复用同一批权重，但 MoE 不同位置很可能被 Router 分给完全不同的专家，一次验证更多 Token 往往意味着要多加载一堆专家——MTP 自己还带一层路由。\n所以这里挺拧巴:MTP 预测得还行，但存储层太慢，猜得越多反而越慢。\n这台机器上，实际使用应该保持:\nbash 1 DRAFT=0 只有大部分专家已经常驻在 RAM/VRAM，并且验证批次能用高效 GPU Kernel 合并计算时，MTP 才可能真的赚。\n瓶颈到底在谁身上 最佳保质量配置在 24 个输出 Token 里，大致耗时是这样:\n部分 耗时 专家磁盘服务 33.1～36.0 秒 专家矩阵乘 8.5～8.9 秒 Attention 8.6～8.8 秒 磁盘时间明显比专家计算和 Attention 高一大截。\n按专家大小和实际命中率粗算，即便经过 VRAM、固定 RAM 和 LRU 缓存，每个输出 Token 还是要从磁盘读约 4.3～4.7GB 权重。\n如果想冲到 3 tok/s，光磁盘就得给到约 13～14GB/s 的持续有效读取，而且这还没给 CPU 计算和 Attention 留时间。\n我这环境还有几个额外限制:\n模型放在 QEMU SCSI 虚拟磁盘上，不是直通的本地 NVMe。 CPU 只有 AVX2，没有 AVX-512/VNNI。 每层路由和 I/O 存在串行部分，20 个 vCPU 没法一直吃满。 只有约 422 个专家固定在 RTX 3060 里，GPU 大部分时间等不到足够连续的活。 换更快的 PCIe 5.0 NVMe 肯定有帮助，但也就是缓解，不是解决。磁盘足够快之后，瓶颈会接着挪到内存带宽、CPU 专家 Kernel 和 Attention 上——这个项目做的事，是在存储、内存和计算之间来回搬瓶颈，372GB 专家权重并不会凭空消失。\n中间踩的三个小坑 1. 控制台显示 8-bit，不代表模型真被展开成了 int8 早期启动时会看到:\ntext 1 experts@8-bit dense@8-bit 但模型文件明明是 int4，一度让我怀疑是不是加载错了。\n翻代码才发现，对已经量化的 Tensor，引擎会按真实字节长度自动判断是 int8、int4 还是 int2。这里的命令行位数主要是 fallback 配置和旧 Banner 文案留下来的，int4 模型并没有被重新展开成 int8。\n2. OpenMP 亲和性把 20 个线程全挤在一个 CPU 上 一次 CPU-only 测试里，我提前设了:\nbash 1 2 OMP_PROC_BIND=spread OMP_PLACES=cores 引擎随后自重启，第二个进程继承了第一个进程已经绑到 CPU 0 的亲和性掩码。结果虽然建了 20 个 OpenMP Worker，实际上全在一个 vCPU 上抢时间。\n那次速度只有约 0.09 tok/s。\n解决办法是显式设置:\nbash 1 2 3 4 COLI_OMP_TUNED=1 OMP_NUM_THREADS=20 OMP_PROC_BIND=spread OMP_PLACES=cores 3. 内网 HTTP 页面不能直接用 crypto.randomUUID() Brain Dashboard 在 localhost 上没问题，但通过内网 IP 走普通 HTTP 打开时，浏览器不认它是 Secure Context，crypto.randomUUID() 直接用不了。\n我在本地前端加了时间戳加随机数的 fallback，之后页面请求就正常了。Web 项目那 17 个测试也全过了。\n和 KTransformers 比，区别在哪 KTransformers 也经常被拿来在消费级 GPU 上跑超大 MoE，但两者解决问题的路子不一样。\n维度 colibri KTransformers 核心方法 按路由从磁盘读专家 CPU/GPU 异构计算 主要存储层级 SSD → RAM → VRAM 大容量 DRAM → GPU 专家计算 冷专家通常由 CPU 算，热门专家可进 GPU 大量专家放主存，由优化过的 CPU Kernel 算 优势 内存远小于模型也能启动 内存够用时速度更实在 代价 单请求容易严重磁盘受限 通常需要数百 GB 大容量、高带宽内存 一句话:colibri 是用 SSD 顶替没有的内存，KTransformers 是用大内存顶替买不起的显存。\n如果手头只有一张 RTX 3060 和几十 GB RAM，KTransformers 也变不出 372GB 专家权重来。就算硬用 mmap 加载，最后还是会掉回缺页和磁盘抖动的老路。\n类似思路的项目还有:\nMoE-Infinity:预测即将用到的专家，在 GPU、CPU、NVMe 之间预取。 FlexLLMGen:通过 CPU、GPU、磁盘统一调度，靠大 Batch 摊薄传输成本。 DeepSpeed ZeRO-Inference:从 CPU 或 NVMe 流式传输权重，更偏批量和分布式推理。 AirLLM:按层加载模型，显存占用很低，但容易被磁盘速度卡住。 llama.cpp:通用 mmap 和 CPU/GPU Offload，模型能放进主存时很好用，严重超过主存后一样会分页。 25GB 内存跑 GLM-5.2，到底有没有用 如果\u0026quot;有用\u0026quot;指的是:\n能成功加载模型。 能生成正确文本。 能验证 744B MoE 的路由和推理逻辑。 能研究专家缓存、预取、量化、MTP 和 CPU/GPU 异构。 那当然有用，工程价值也不小。\n但如果标准是:\n像正常聊天一样用。 给编程助手连续吐几百到几千 Token。 跑 Agent 的多轮工具调用。 给多人提供稳定 API。 那在这台机器上基本没戏。\n最佳的 0.44 tok/s 意味着:\ntext 1 2 100 Token:约 3.8 分钟 500 Token:约 19 分钟 这还没算长 Prompt 的 Prefill 和首次响应等待。一次真实 Web 请求里，我测到的 TTFT 是 26.3 秒。\n我自己的感觉是，交互式生成怎么也得 3 tok/s 起步才算能用，5～8 tok/s 才算实用，10～15 tok/s 才接近日常聊天的手感。更别说现在的 Agent 场景，2000 tok/s 都嫌不够快，越快越好。\n仓库 README 里的 5～15 tok/s 是硬件推演出来的数字，不是小内存机器的实测成绩。按项目 README 的公开硬件结果，25GB 开发机冷启动约为 0.05～0.1 tok/s，128GB CPU-only 桌面机预热后约为 1.8 tok/s。\n真把全部专家塞进内存和显存以后，情况才会完全不一样。仓库记录的一台 6×RTX 5090、约 264GB RAM 的机器，在 Disk 0 下跑到了 6.28～6.84 tok/s。\n这技术本身不是玩具，只是我这台机器配置撑不起来。\n最后 我还是挺喜欢 colibri 这个项目的。有机会的话我想尽快补一补，能看懂推理代码的过程，我现在对深度学习基本还是一无所知，买的花书也吃灰好几年了，总拿\u0026quot;太忙\u0026quot;当借口也不是个事儿。\n贴一下真实效果的 GIF\n相关资料 JustVugg/colibri colibri README：公开硬件结果 GLM-5.2 on 6×RTX 5090 实验记录 KTransformers ","permalink":"/posts/25gb%E5%86%85%E5%AD%98%E5%B0%B1%E8%83%BD%E8%B7%91-glm5.2/","summary":"\u003cp\u003e先说结论:能跑是真的，能不能用是另一回事，别抱太大期待。\u003c/p\u003e\n\u003cp\u003e在我的 RTX 3060 开发机上，GLM-5.2 744B 最终跑到了约 \u003cstrong\u003e0.44 tok/s\u003c/strong\u003e。工程上挺有意思，但离正常聊天还差得远。\u003c/p\u003e\n\u003cp\u003e这里的“25GB”是项目宣称的最低量级，不是我的整机占用。本次 Linux 可见约 39GiB，进程 RSS 约 27GB；它适合验证 MoE 路由、缓存和磁盘流式推理，不适合交互聊天或 Agent。\u003c/p\u003e","title":"25GB 内存就能跑 GLM-5.2？我在 RTX 3060 上实测了 744B MoE"},{"content":" 今天办业务需要人脸识别，拿起手机把我的小胖脸放入框中眨眨眼，等待灯光闪烁，随后就是通过，这个已经见怪不怪了，偶尔还要张张嘴，左右摇头，我知道他这是在确认我是不是真人，但是哪怕我做过简单的人脸识别，我也没有深究过人脸活体检测具体有多少方法，以及闪光是在干嘛，本文对此做了调研和解释。\n差不多在 23 年有一个小项目，要做离线人脸识别来替换百度人脸识别 API。没太多性能要求，我搜刮来一个 facenet-retinaface-pytorch 仓库，扒拉两下就集成进系统了。\n这个仓库只需要把人脸向量存起来，再把新照片转换成向量做对比。底层不用懂也能跑通，后来需求搁浅，我也就没有继续深究。\n首先，人脸识别是怎么实现的 不展开神经网络和数学公式，整个过程可以粗略分成三步。\n摄像头拿到画面后，先找到人脸的位置，把脸裁出来并校正角度；接着模型把五官、轮廓和纹理压成一串数字，也就是人脸向量；最后，把这个向量和之前保存的向量计算相似度。\n相似度超过阈值，系统就认为两张脸可能属于同一个人。阈值越严格，冒认更少，但真人也更容易被拒绝。\n所以，人脸识别主要回答的是“这张脸像谁”，而像谁并不能代表眼前真的就是谁。一张照片、一段视频，照样能提取出很像的人脸向量。\n为什么要做人脸活体检测以及怎么做 当然，不做活体检测的话，拿张照片或者播放一段视频就可能通过人脸比对。它要补上的问题是：镜头前的东西，究竟是现场真人，还是照片、屏幕、面具或被替换的视频流。\n来点专业词汇，活体判断可能会组合随机挑战、RGB 纹理与时序、深度或红外、材质与光反射、可信采集链路和服务端风控。一下子就懵了，还是先从我最早的两个问题说起。\n眨眨眼是干嘛的 最早、最直观的用途，是排除一张不会动的静态照片。系统连续观察眼睛，如果看到“睁开—闭合—再次睁开”的时序，就认为镜头前至少不是一张完全静止的普通照片。\n系统通常会在每一帧里定位眼角和上下眼睑，计算眼睛高度与宽度的比例。这个数值短时间内从高变低、再变高，就很像一次眨眼。现代模型可能直接输出眼睛开合概率，原理差不多。1\n如果系统临时提示“现在眨眼”，它还构成了一次简单的挑战—响应：系统刚发出要求，用户随后作出动作。这比静静等待一次自然眨眼，多了一点“正在发生”的证据。\n但它的证明力有限。视频中的人也会眨眼，挖掉照片的眼睛区域也能模拟眨眼，实时换脸则可以直接跟随攻击者的动作。眨眼能对付普通照片，却不能单独对付视频重放。2\n屏幕颜色变化是干嘛的 在典型的活体认证实现里，手机屏幕被临时当成了一个可编程光源。系统随机生成一串颜色、亮度或图案，让它们依次照到脸上，同时让前置摄像头记录脸部反射。颜色不是主要让人去识别、记住或回答；真正“回答问题”的是脸对光的即时反射。\n它想核对三件事：\n时间是否对得上。 本次认证临时生成的随机颜色序列，相当于一次性口令。脸上的颜色变化必须紧跟本次序列，不能是过去录好的一段固定画面。 空间形状是否像人脸。 鼻尖、眼窝、额头、面颊朝向不同，受到同一块屏幕照明时会出现不同的明暗分布。普通照片或显示屏是近似平面，反射分布不同。 材料是否像皮肤。 皮肤、纸张、玻璃屏和硅胶面具的反光方式不一样，模型会尝试找出这些差异。 2018 年的 Face Flashing 研究系统化提出了这种做法：屏幕随机闪现颜色或图案，再分析脸部反射的时间、纹理和三维形状。3 AWS 的商用活体服务也提供“面部移动 + 彩色光序列”的模式。4\n当然，屏幕变色也可能兼有曝光、白平衡校准，甚至只是界面效果。只看页面闪了几下，无法确定某个 App 到底用了哪些模型。\n如果我用一段眨眼视频会怎么样 答案是：有可能，但取决于系统检查到了哪一层。\n只检查有没有眨眼： 预录视频本来就有“睁—闭—睁”，存在通过的可能。 还检查屏幕重放痕迹： 系统会寻找摩尔纹、屏幕刷新、玻璃反光、二次压缩和平面运动，视频就没那么容易过。 再加随机光、深度或红外： 固定视频不知道本次颜色序列，普通屏幕也缺少真实人脸深度，通过难度会进一步提高。 还有一种更危险的情况，是不拿屏幕对着摄像头，而是直接用虚拟摄像头或修改 App 的相机画面。这已经不只是活体算法的问题，还需要系统确认视频确实来自可信的真实摄像头。\n最后 人脸识别回答“这张脸像谁”，眨眼检查“画面里的脸有没有做动作”，随机变色则检查“这张脸有没有立刻响应本次随机光”。可靠的系统不会只押宝其中一项，而是把多种线索放在一起判断。\nSoukupová, T.; Čech, J., Real-Time Eye Blink Detection using Facial Landmarks, 2016. https://vision.fe.uni-lj.si/cvww2016/proceedings/papers/05.pdf\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nA Survey on Anti-Spoofing Methods for Facial Recognition with RGB Cameras of Generic Consumer Devices, Journal of Imaging, 2020. https://doi.org/10.3390/jimaging6120139\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nTang, D. et al., Face Flashing: a Secure Liveness Detection Protocol based on Light Reflections, NDSS 2018. https://doi.org/10.14722/ndss.2018.23176\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nAWS, Detecting face liveness — Amazon Rekognition Developer Guide. https://docs.aws.amazon.com/rekognition/latest/dg/face-liveness.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"/posts/%E4%BA%BA%E8%84%B8%E8%AF%86%E5%88%AB%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%9C%A8%E7%9C%BC%E8%BF%98%E8%A6%81%E9%97%AA%E5%90%84%E7%A7%8D%E9%A2%9C%E8%89%B2%E7%9A%84%E5%85%89/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e今天办业务需要人脸识别，拿起手机把我的小胖脸放入框中眨眨眼，等待灯光闪烁，随后就是通过，这个已经见怪不怪了，偶尔还要张张嘴，左右摇头，我知道他这是在确认我是不是真人，但是哪怕我做过简单的人脸识别，我也没有深究过人脸活体检测具体有多少方法，以及闪光是在干嘛，本文对此做了调研和解释。\u003c/p\u003e","title":"人脸识别为什么要眨眼还要闪各种颜色的光"},{"content":"vLLM + Qwen3.6 视频推理踩坑记录：AV1、OpenCV 与视频采样问题 Info 之前 qwen3.6-27b 模型发布后没多久我就部署他 并且测试过视频能力，一直是公司内部署的主力离线大模型。\n所以我认为一直可用，但是今天同事提了个新的需求，要从视频中找出属于广告部分的内容裁剪掉。我首先给出的方案是 用 Qwen3-ASR+Qwen3-ForcedAligner 模型来获取 文本内容以及时间轴，再用 LLM 来判断哪些文本属于广告内容，随后根据文本所对应的时间轴裁剪视频，先这样子给同事搭建了环境让他去跑几个试试，同时我来试试直接让 qwen3.6 读视频是否可行。结果一来就给我个报错\n环境 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Model: Qwen3.6-27B BF16 vLLM: 0.19.1 Transformers: 5.6.0 Python: 3.12 4*RTX 3090 24GB 显示已折叠代码（18 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 启动参数： vllm serve /data/huggingface_model/Qwen/Qwen3.6-27B \\ --served-model-name qwen3.6-27b \\ --port 13539 \\ --tensor-parallel-size 4 \\ --max-model-len 65536 \\ --reasoning-parser qwen3 \\ --enable-auto-tool-choice \\ --tool-call-parser qwen3_coder \\ --gpu-memory-utilization 0.90 \\ --max-num-seqs 4 \\ --enable-prefix-caching \\ --enable-chunked-prefill \\ --disable-custom-all-reduce \\ --allowed-local-media-path /\u0026lt;指定目录\u0026gt; \\ --media-io-kwargs \u0026#39;{\u0026#34;video\u0026#34;:{\u0026#34;num_frames\u0026#34;:64}}\u0026#39; \\ --mm-processor-kwargs \u0026#39;{\u0026#34;fps\u0026#34;:1,\u0026#34;do_sample_frames\u0026#34;:false}\u0026#39; 这里的 --media-io-kwargs 是给 vLLM 多模态视频读取器传额外参数的。video.num_frames=64 表示整段视频送进模型前目标采样 64 帧，不是每秒采样 64 帧。比如一个 10 秒、25 FPS 的视频，原本大约有 250 帧，最后会在整段视频的时间范围内均匀取 64 帧，平均下来大约相当于 6.4 FPS。\n如果想表达“每秒采样多少帧”，应该设置 fps。vLLM 0.19.1 里同时设置 num_frames 和 fps 时，会取两者限制下更小的采样数量。当前命令只设置了 num_frames，所以它主要影响输入的视频帧数量和视觉 Token 数量；它不是修改视频 FPS 的参数，也不能解决视频编码本身无法解码的问题。\n另外，这里还加了一个 --mm-processor-kwargs：\nbash 1 2 3 4 5 --mm-processor-kwargs \\ \u0026#39;{ \u0026#34;fps\u0026#34;: 1, \u0026#34;do_sample_frames\u0026#34;: false }\u0026#39; 这个参数是传给 Qwen3VL/Hugging Face video processor 的。前面的 --media-io-kwargs 已经在 vLLM 的视频读取阶段采样了 64 帧，所以这里用 do_sample_frames: false 告诉后面的 processor 不要再采样一遍，直接处理已经取出来的帧。\n这里的 fps: 1 不会把前面采好的 64 帧变成 1 FPS；在 do_sample_frames: false 的组合下，实际帧数还是由 --media-io-kwargs 决定。如果想让 vLLM 读取阶段按每秒 1 帧采样，应该把 fps 放到 --media-io-kwargs 的 video 参数里。\n后面排查时我把它改成了 -1，只是想先取消主动的帧数限制，确认问题是不是采样数量导致的。结果 frames_indices=[] 依然存在，说明视频根本没有成功解码，和 num_frames 没什么关系。\n客户端使用 OpenAI SDK：\npython 1 client.chat.completions.create(...) 第一个问题 最开始调用：\npython 1 2 3 4 5 6 { \u0026#34;type\u0026#34;: \u0026#34;video_url\u0026#34;, \u0026#34;video_url\u0026#34;: { \u0026#34;url\u0026#34;: \u0026#34;http://xxx/1.mp4\u0026#34; } } 直接报：\n1 2 3 4 5 6 7 Failed to apply Qwen3VLProcessor videos=[ array([], shape=(0,480,854,3)) ] frames_indices=[] 我以为是\nFPS 配置 num_frames Processor Bug 不是很懂视频处理，于是求助 GPT，接下来是过程 接下来我把 --media-io-kwargs 改成了 --media-io-kwargs '{\u0026quot;video\u0026quot;:{\u0026quot;num_frames\u0026quot;:-1}}'\n排查过程 首先看 metadata：\n1 2 3 total_num_frames=16970 fps=25 duration=678s 说明：\nOpenCV 能够读取视频容器中的基础信息（FPS、Frame Count、Duration）\n但是：\n1 frames_indices=[] 说明：\n真正解出来的 Frame 数量是：\n1 0 于是开始怀疑 OpenCV。\nMac 正常 但是我随手是在自己的 Mac 上测试了一下视频的情况：\npython 1 2 3 cap = cv2.VideoCapture(url) ret, frame = cap.read() 输出：\n1 2 3 opened=True ret=True frame=(480,854,3) 说明：\n同样的 Python 代码，在 macOS 上能够正常读取第一帧，因此可以基本排除代码逻辑问题。\nLinux 不正常 同样代码，在部署 vLLM 的 Linux 上：\n1 2 3 opened=True count=16970 fps=25 但是：\n1 ret=False 并且输出：\n1 2 3 4 5 Your platform doesn\u0026#39;t support hardware accelerated AV1 decoding. Failed to get pixel format. Get current frame error. 这里就定位到了。\n最终定位结果 ffprobe -hide_banner input.mp4 执行这个命令 查看视频编码：\n1 2 Video: av1 Ubuntu 上 pip 安装的 OpenCV：\n1 opencv-python 虽然：\n1 VideoCapture 可以获取：\nFrame Count FPS Duration 但是：\n1 cap.read() 一帧都读不出来。\n于是：\nvLLM 收到：\n1 2 3 videos=[ array([],...) ] 最后：\n1 Qwen3VLProcessor 直接报错。\n所以：\n不是 Qwen 的问题。\n也不是：\n1 2 fps num_frames 配置问题。\n真正原因是当前 Linux 环境中的 OpenCV（以及其底层 FFmpeg/编解码依赖）无法正确解码该 AV1 视频。\n因为 OpenCV 会自带 FFmpeg，我在这里 pip 安装的 OpenCV 估计使用的 FFmpeg 不行，有需要的应该可以自己编译，这里我就快速绕过了。\n解决方案 最简单：\n直接转 H264。\n1 2 3 4 5 6 ffmpeg \\ -i input.mp4 \\ -c:v libx264 \\ -pix_fmt yuv420p \\ -c:a copy \\ output.mp4 之后：\nQwen 即可正常读取。\n第二个问题 解决 AV1 后。\n又遇到了：\n1 2 3 4 AssertionError: timestamps and tokens_per_frame must have the same length 这个已经不是视频编码问题。\n这个 AssertionError 最终没有复现，因此没有定位到根因。我重启 vllm 之后就正常了，口头描述一下重启后的现状，运行脚本后很久 vllm 都没有响应，当时 GPU Util 基本保持在 0%，vLLM 进程仍然存活，HTTP Ping 正常，因此更像是请求卡在推理前的某个阶段，而不是服务已经崩溃。一个比较奇怪的现象是，在普通文本请求进入后，原本一直没有响应的视频请求随后开始执行，TTFT 也明显偏长。目前还没有确认这是 Scheduler、Prefix Cache，还是多模态预处理导致的问题。\n结果基本准确，因为广告是渐进式出现的，严格来讲在05:30 秒左右就开始一部分广告内容，但是一直到37 秒才是非常明确的广告画面出现。\nfile:// 路径 由于使用 http 的时候，我用的是自己的海外服务器，连通信不是很好，所以准备使用本地路径。\n如果使用：\n1 file:// 一定要记住路径是：运行 vLLM 那台机器的路径。\n应该传：\n1 file:///data/video.mp4 并且启动 vllm 的参数中要加上：\n1 --allowed-local-media-path 该参数必须覆盖这个目录。\n最终建议 如果最终要做视频理解的话，最好还是采样可控 多阶段进行。先粗检再细检\n比如如下 Pipeline：\n显示已折叠代码（26 行） 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 AV1 原视频 │ ▼ FFmpeg 切段 + 转 H264 │ ▼ 每段约2分钟 │ ▼ Qwen fps=1 粗定位 │ ▼ 疑似广告片段 │ ▼ 再次切片 │ ▼ Qwen fps=4 精定位 一方面能够减少单次请求的视觉 Token 数量，另一方面也能降低长视频解码和采样失败带来的影响。\n参考资料 vLLM Media API https://docs.vllm.ai/en/stable/api/vllm/multimodal/media/\nvLLM Multimodal Inputs https://docs.vllm.ai/en/stable/features/multimodal_inputs/\nvLLM Video Backend https://docs.vllm.ai/en/stable/api/vllm/multimodal/video/\nGitHub Issue #35909 Error when using Qwen3-VL/Qwen3.5 with video input https://github.com/vllm-project/vllm/issues/35909\nNVIDIA Qwen3.6 Video API https://docs.nvidia.com/nim/vision-language-models/1.7.0/examples/qwen3.6/api.html\nOpenCV GitHub Issues（AV1） https://github.com/opencv/opencv/issues?q=AV1\n","permalink":"/posts/vllm-+-qwen3.6-%E8%A7%86%E9%A2%91%E6%8E%A8%E7%90%86%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/","summary":"\u003ch2 id=\"vllm--qwen36-视频推理踩坑记录av1opencv-与视频采样问题\"\u003e\u003cstrong\u003evLLM + Qwen3.6 视频推理踩坑记录：AV1、OpenCV 与视频采样问题\u003c/strong\u003e\u003c/h2\u003e\n\u003cdiv class=\"markdown-alert markdown-alert-info\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4\"/\u003e\u003cpath d=\"M12 8h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Info\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e之前 qwen3.6-27b 模型发布后没多久我就部署他 并且测试过视频能力，一直是公司内部署的主力离线大模型。\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\u003cp\u003e所以我认为一直可用，但是今天同事提了个新的需求，要从视频中找出属于广告部分的内容裁剪掉。我首先给出的方案是 用 Qwen3-ASR+Qwen3-ForcedAligner 模型来获取 文本内容以及时间轴，再用 LLM 来判断哪些文本属于广告内容，随后根据文本所对应的时间轴裁剪视频，先这样子给同事搭建了环境让他去跑几个试试，同时我来试试直接让 qwen3.6 读视频是否可行。结果一来就给我个报错\u003c/p\u003e","title":"vLLM + Qwen3.6 视频推理踩坑记录"},{"content":" AI 开发配置 conda / cuda / pytorch / nvidia-driver 很容易混淆，让人头疼，实际上踩过几次坑就能摸清它们的概念和关系了。本文尝试用 Java 开发者熟悉的语言，对照解释几个常见概念。\nCUDA 是 NVIDIA 提供的 GPU 并行计算平台和编程生态。 日常 AI 开发最容易混淆的是：Driver 负责让系统驱动 GPU，PyTorch 安装包通常自带所需的 CUDA Runtime，而 CUDA Toolkit 主要在编译 CUDA C++ 或扩展时使用；Conda 负责隔离项目环境。\nGPU 世界 Java 世界 作用 GPU 硬件 CPU 真正执行计算 NVIDIA Driver JVM（HotSpot） 控制硬件、执行程序 CUDA Toolkit JDK 给开发者写程序用 nvcc javac 编译器 libcudart.so Java Runtime Library 运行时库 cuBLAS/cuDNN Netty、Apache Commons 等第三方库 提供高性能计算能力 PyTorch Spring Boot 应用框架 conda SDKMAN / Maven + 环境管理 管理版本 这张表只是帮助理解职责，不表示两边的 API、兼容规则和部署方式能够一一对应。\n下面一点一点讲。\n第一层：GPU Driver（最重要） 先理解一件事：GPU 本身不会执行 CUDA C++。\nGPU 最终执行的是一种叫做 SASS 的机器码。\n整个过程实际上是：\n这里 Driver 做了很多工作：\n管理 GPU 分配显存 调度 Kernel 把 PTX 编译成当前 GPU 的机器码 所以 Driver 是 GPU 的运行时，可以理解成：\n1 2 3 4 5 6 7 8 9 Java CUDA .class .ptx │ │ ▼ ▼ JVM Driver │ │ ▼ ▼ CPU GPU 没有 Driver，GPU 什么都跑不了。\n第二层：CUDA Toolkit 很多人以为 CUDA 就是 Driver，其实不是。CUDA Toolkit 是开发工具，里面包括：\n1 2 3 4 5 6 7 8 9 10 11 12 13 CUDA Toolkit ├── nvcc ├── libcudart.so ├── cuda runtime ├── headers ├── examples ├── cuBLAS ├── cuFFT ├── cuRAND ├── nvprof ├── nsight └── ... 可以理解成：\n1 2 3 4 5 6 7 8 JDK ├── javac ├── java ├── javadoc ├── jar ├── rt.jar └── ... CUDA Toolkit 是给开发者的，Driver 是给运行程序的。\n第三层：为什么 Driver 和 Toolkit 可以分开 这是 CUDA 最容易搞混的地方。例如下面这几种组合都完全没问题：\n1 2 3 Driver 550 + Toolkit 11.8 Driver 580 + Toolkit 12.8 Driver 580 + Toolkit 11.3 原因就是：Driver 会向下兼容。例如：\n1 2 3 4 5 6 7 8 9 10 Driver 580 支持： CUDA 12.8 CUDA 12.7 CUDA 12.6 CUDA 12.5 ... CUDA 11.x 所以新的 Driver 可以运行老程序，就像：\n1 2 3 4 5 6 7 Java 21 JVM 可以运行 Java 8 的 class Java 11 的 class Java 17 的 class 需要注意的是，Driver 只能向下兼容，反过来不行：太老的 Driver 撑不起太新的 Toolkit（或 PyTorch 打包的 Runtime），这个坑在第七层会具体讲。\n第四层：Toolkit 是什么时候需要安装？ 如果要自己开发 CUDA，就需要 CUDA Toolkit，因为要用到编译器 nvcc：\n1 nvcc hello.cu 但如果只是跑 PyTorch，很多时候根本不用安装 Toolkit。因为 PyTorch 已经把需要的 Runtime 打包好了：\n1 2 3 4 5 6 7 8 site-packages torch ├── libcudart.so ├── libcublas.so ├── libcudnn.so └── ... PyTorch 自己带 Runtime，只要求系统 Driver 足够新。\n第五层：PyTorch 到底是什么？ 很多人以为 PyTorch 就等于 CUDA，完全不是。PyTorch 是一个框架，就像 Java 世界里的 Spring Boot，内部会调用 CUDA：\n1 2 3 4 5 6 7 8 9 Python ↓ PyTorch ↓ CUDA Runtime ↓ Driver ↓ GPU 所以 PyTorch 不负责驱动 GPU，它只是调用 CUDA API。\n第六层：为什么 PyTorch 有这么多版本？ 例如 torch==2.7 会有 cu118、cu121、cu126 好几个版本，很多新人看不懂。其实意思就是：这个 PyTorch 是拿哪个 Toolkit 编出来的。\n例如 torch 2.7 + cu118 意味着编译时用的是 CUDA 11.8，里面带的是：\n1 2 3 libcudart 11.8 cublas 11.8 cudnn 9.x 而 torch 2.7 + cu126 里面打包的就是 CUDA 12.6 Runtime。\n注意，这里不是要求你系统安装 CUDA 12.6，只是 PyTorch 自己带了 CUDA 12.6 Runtime。\n第七层：Driver 为什么要求最低版本？ 例如官网写：\n1 2 PyTorch cu126 Need Driver \u0026gt;= 560.xx 原因是 Runtime 要调用 Driver：\n1 2 3 4 5 PyTorch ↓ CUDA Runtime 12.6 ↓ Driver 如果 Driver 太老，比如只支持到 CUDA 11，就会直接报错：\n1 CUDA driver version is insufficient. 所以 Driver 必须足够新。\n第八层：Conda 到底管理什么？ 很多人觉得 Conda 管 CUDA，其实它管理的是整个运行环境：\n1 2 3 conda create ↓ Python 3.12 + numpy + torch + transformers + opencv + ... 甚至连 PyTorch 自带的 CUDA Runtime，也会一起装进去：\n1 2 3 4 5 6 7 8 conda env ├── python ├── numpy ├── torch ├── cudnn ├── libcudart └── ... 所以不同环境，比如 env1 装 torch cu118、env2 装 torch cu126，完全可以共存，互不影响。\n第九层：真正安装顺序 假设一台新的 Ubuntu，实际安装顺序是这样的。\n第一步，安装 Driver：\n1 2 3 GPU ↓ Driver 装好之后跑一下 nvidia-smi，能看到 Driver Version 和 CUDA Version。这里的 CUDA Version 指的是Driver 能支持的最高 CUDA API 版本，不是系统安装了对应版本的 Toolkit。\n如图所示，这个命令还能看到红框中的显存使用 / 显存大小，右侧则是 GPU 使用率。\n第二步，安装 Conda（Miniconda 即可）。\n第三步，创建环境：\nbash 1 conda create -n ai python=3.12 第四步，安装 PyTorch，例如：\n下面命令是本文更新时的示例。PyTorch 与 CUDA Runtime 的可用组合会变化，正式安装前请以 PyTorch 官方安装选择器为准。\nbash 1 conda install pytorch torchvision pytorch-cuda=12.6 -c pytorch -c nvidia 或者：\nbash 1 pip install torch --index-url https://download.pytorch.org/whl/cu126 整个过程中，完全不用安装 CUDA Toolkit。\n第十层：什么时候才需要 Toolkit？ 只有以下这些情况才需要 CUDA Toolkit：\n1 2 3 4 5 6 ✓ 写 CUDA C++ ✓ 自己编译 .cu ✓ 编译 FlashAttention ✓ 编译 xformers ✓ 编译 TensorRT Plugin ✓ 开发 CUDA Kernel 否则，无论是 AI 推理、AI 训练，还是用 PyTorch、TensorFlow、JAX，一般都不用单独安装 Toolkit。\n结论：大多数 PyTorch 用户只需要 Driver 最后，用一张完整的关系图总结：\ntext 1 2 3 4 5 6 7 8 9 10 11 12 13 14 你的 Python 代码 │ ▼ PyTorch / TensorFlow │ （自带 CUDA Runtime、cuBLAS、cuDNN 等） │ ▼ NVIDIA Driver（系统安装） │ 管理 GPU、加载 Kernel、调度执行 │ ▼ GPU 硬件 而 CUDA Toolkit 更像一个独立的开发工具包：\ntext 1 2 3 4 5 6 7 CUDA Toolkit ├── nvcc（CUDA 编译器） ├── 头文件（cuda.h 等） ├── CUDA Runtime 开发库 ├── cuBLAS / cuFFT / cuRAND 等开发库 ├── 调试与性能分析工具（Nsight 等） └── 示例代码 日常做 AI 开发时，最常见的模式就是：\n系统安装一个较新的 NVIDIA Driver。 用 Conda 为每个项目创建独立环境。 在环境中安装对应 CUDA 版本的 PyTorch（它会带上所需的 CUDA Runtime）。 除非需要编译 CUDA 代码或 C++ 扩展，否则通常不安装 CUDA Toolkit。这样既省事，也避免了多个 Toolkit 版本之间的管理问题。 如果想看这些概念在超大 MoE 模型推理里怎样落到显存、内存和磁盘上，可以继续看我的 RTX 3060 运行 GLM-5.2 744B 实测。\n参考资料 NVIDIA CUDA Compatibility NVIDIA CUDA Installation Guide for Linux PyTorch：Start Locally PyTorch：Previous Versions ","permalink":"/posts/%E4%BB%80%E4%B9%88%E6%98%AF-cuda%E7%94%A8-java-%E4%B8%96%E7%95%8C%E7%9A%84%E7%B1%BB%E6%AF%94%E7%9C%8B%E6%87%82/","summary":"\u003cblockquote\u003e\n    \u003cp\u003eAI 开发配置 conda / cuda / pytorch / nvidia-driver 很容易混淆，让人头疼，实际上踩过几次坑就能摸清它们的概念和关系了。本文尝试用 Java 开发者熟悉的语言，对照解释几个常见概念。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e\u003cstrong\u003eCUDA 是 NVIDIA 提供的 GPU 并行计算平台和编程生态。\u003c/strong\u003e 日常 AI 开发最容易混淆的是：Driver 负责让系统驱动 GPU，PyTorch 安装包通常自带所需的 CUDA Runtime，而 CUDA Toolkit 主要在编译 CUDA C++ 或扩展时使用；Conda 负责隔离项目环境。\u003c/p\u003e","title":"什么是 CUDA：用 Java 世界的类比看懂 Driver / Toolkit / PyTorch / Conda"},{"content":"先留个能被搜到的结论：截至 2026-07-14，macOS 上误删 ChatGPT Classic 后，可以尝试运行 brew install --cask chatgpt-classic 恢复；能否安装取决于 Homebrew 当时是否仍保留这个 cask。\n自从更新 chatgpt 和 codex 之后，我手贱的把 chatgpt classic 给卸载了，现在找不回来了，我怀念我的 option+space\nopenai 是不打算继续服务普通 chat 用户了吗，或者应该把那个功能恢复吧\n2026 年 7 月 14 日 21:35:28\n找回方法 哦哦 找到解决方法了，mac 用户可以直接用 homebrew 安装回来\nbrew install --cask chatgpt-classic\nHomebrew 的 ChatGPT Classic cask 页面\n2026 年 7 月 14 日 21:47:06\n","permalink":"/posts/chatgpt-%E4%BD%A0%E4%B8%8D%E6%89%93%E7%AE%97-chat-%E4%BA%86%E5%90%97/","summary":"\u003cp\u003e先留个能被搜到的结论：截至 2026-07-14，macOS 上误删 ChatGPT Classic 后，可以尝试运行 \u003ccode\u003ebrew install --cask chatgpt-classic\u003c/code\u003e 恢复；能否安装取决于 Homebrew 当时是否仍保留这个 cask。\u003c/p\u003e\n\u003cp\u003e自从更新 chatgpt 和 codex 之后，我手贱的把 chatgpt classic 给卸载了，现在找不回来了，我怀念我的 option+space\u003c/p\u003e\n\u003cp\u003eopenai 是不打算继续服务普通 chat 用户了吗，或者应该把那个功能恢复吧\u003c/p\u003e","title":"chatgpt 你不打算 chat 了吗"},{"content":" 理论上 这是基础中的基础，但是说实话越是基础越是不容易去细看，我一直只有模糊的概念，比如以Java为例，一个对象返回给前端是用的 json，对象-\u0026gt;json 的过程就是序列化，json 到对象都过程就是反序列化，但是具体是对错与否，帮我进行这个操作的函数、框架下面是不是还有更多内容，却从未深究，这次水一篇博客来讲一讲这是什么意思\n定义 互联网的产生带来了机器间通讯的需求，而互联通讯的双方需要采用约定的协议，序列化和反序列化属于通讯协议的一部分。通讯协议往往采用分层模型，不同模型每层的功能定义以及颗粒度不同，例如：TCP/IP协议是一个四层协议，而OSI模型却是七层协议模型。在OSI七层协议模型中展现层（Presentation Layer）的主要功能是把应用层的对象转换成一段连续的二进制串，或者反过来，把二进制串转换成应用层的对象\u0026ndash;这两个功能就是序列化和反序列化。一般而言，TCP/IP协议的应用层对应与OSI七层协议模型的应用层，展示层和会话层，所以序列化协议属于TCP/IP协议应用层的一部分。本文对序列化协议的讲解主要基于OSI七层协议模型。\n序列化： 将数据结构或对象转换成二进制串的过程 反序列化：将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程 这里摘自 美团技术团队 15 年的文章，我也就从这里开始引入吧\n通俗比喻 假设你家有很多家具：沙发、电视、冰箱、床……\n搬家的时候，你得把它们拆开、装进箱子，贴上标签 —— 这就是序列化。\n到了新家，打开箱子、按照标签重新组装、恢复成原来的样子 —— 这就是反序列化。\n由浅入深 现在从最常见的序列化协议入手，先是 json ，这个在现代系统里面最常见了，打开web 也好，网络请求很多都是用 json 传输内容的。那么\nwho is \u0026lsquo;Json\u0026rsquo;\njson 1 2 3 4 { \u0026#34;name\u0026#34;: \u0026#34;Ray\u0026#34;, \u0026#34;age\u0026#34;: 25 } 大概就长这样，如果我把它放到 Java 对象里面 它可以是这样的\njava 1 2 3 User user = new User(); user.setName(\u0026#34;Ray\u0026#34;); user.setAge(25); 这个 user 实际上存在于内存中。\n而 Java 内存里是怎么存的？\n栈（Stack）里只保存引用：user → 0x1008\n真正的数据在堆（Heap）里面：\n1 2 3 4 5 6 7 8 9 10 11 12 13 0x1008 (User 对象) ┌──────────────────────────────┐ │ Object Header │ │ klass pointer │ │ │ │ name ───→ 0x2010 │ │ age = 25 │ └──────────────────────────────┘ 0x2010 (String 对象) ┌─────────────┐ │ value=\u0026#34;Ray\u0026#34; │ └─────────────┘ 可以看到：\nuser 变量，只保存了一个引用（地址） User 对象里面有自己的字段 name 字段又指向另一个 String 对象 JVM 里面这个对象什么样子 HotSpot JVM 中，一个对象大概是这样布局。\n显示已折叠代码（21 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 User Object ┌──────────────────────┐ │ Object Header │ ←────── 对象头 ├──────────────────────┤ │ Mark Word │ ├──────────────────────┤ │ Klass Pointer │ └──────────────────────┘ ┌──────────────────────┐ │ Instance Data │ ←────── 你的成员变量 ├──────────────────────┤ │ name (Reference) │ ├──────────────────────┤ │ age (int) │ └──────────────────────┘ ┌──────────────────────┐ │ Padding │ ←────── 内存对齐 └──────────────────────┘ 第一部分：对象头（Object Header） 这里放的是 JVM 自己要用的信息，例如：\n哈希值 GC 标记 锁状态 指向 Class 的指针 这些和你的业务一点关系都没有。\n第二部分：字段 name 这里存的并不是 \u0026quot;Ray\u0026quot; 本身，而是 0x2010，也就是 String 对象的地址（引用）。\n而 age 由于 int 是基本类型，值 25 直接存进去。\n所以对象真正更像：\n1 2 3 4 5 6 User ┌────────────┐ │name=0x2010 │ │age=25 │ └────────────┘ String 又是一个对象，指向一个地址：\n1 2 3 4 String ┌────────────────┐ │ value=0x3000 │ └────────────────┘ Java 9 以后内部实际上是 byte[]，早期版本是 char[]。而这个 byte[] 也是一个对象，最终存储的是：\n1 byte[] → 82 97 121 → R a y （R=82, a=97, y=121，对应 ASCII 码）\n那为什么不能直接发送 User？ 假设现在 user → 0x1008，网络发送的时候，发送这个地址一点意义都没有。\n因为 0x1008 只是这一台机器 JVM 的内存地址。对象本身无法跨进程，更无法跨机器。\n所以需要”翻译” 必须把对象翻译成一种任何语言都认识、任何机器都认识的格式。\n例如 User 对象 → {“name”:”Ray”,”age”:25}\n注意这里已经没有 0x1008、没有 Object Header、没有 Mark Word、没有 GC信息，全部不要。只留下真正的数据：name=Ray，age=25。\n所以 JSON 本质就是：把对象里面真正有意义的数据提取出来，用一种文本格式描述出来。\n那序列化到底是什么？ 回到最初的定义：\n序列化：把内存中的对象，转换成一段可以存储或传输的字节序列 反序列化：把字节序列还原回内存中的对象 JSON 只是序列化协议的一种（文本格式，人类可读）。除此之外还有很多选择：\n协议 格式 特点 JSON 文本 可读性好，通用性强，体积较大 Protobuf 二进制 体积小，速度快，需要预定义 Schema Java 原生序列化 二进制 仅限 Java 生态，安全隐患多 MessagePack 二进制 类似 JSON 但更紧凑 XLM 文本 比 JSON 更冗长，支持属性、命名空间等；表达能力强 所以序列化不只是”对象变 JSON”，而是一个更广泛的概念：让数据脱离内存、脱离语言、脱离机器，变得可传输、可存储。\n参考文章 序列化和反序列化 | 美团 · 技术团队\n","permalink":"/posts/%E4%BB%80%E4%B9%88%E6%98%AF%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e理论上 这是基础中的基础，但是说实话越是基础越是不容易去细看，我一直只有模糊的概念，比如以Java为例，一个对象返回给前端是用的 json，对象-\u0026gt;json 的过程就是序列化，json 到对象都过程就是反序列化，但是具体是对错与否，帮我进行这个操作的函数、框架下面是不是还有更多内容，却从未深究，这次水一篇博客来讲一讲这是什么意思\u003c/p\u003e","title":"什么是序列化和反序列化"},{"content":"昨天刷到一个 AI 移动硬盘的广告，结合前段时间老板和我说的 AI NAS，本来没怎么放在心上，因为我本身不会是前者的客户，后者又不是小公司能做的，不过整理硬件的时候，发现一个 Frefly 的 RK3588 盒子，灵机一动、RK3588 有 NPU，理论肯定有人做这个端侧LLM 的推理。\nAI 移动硬盘 比如一个相册管理器。照片都在本地，模型在本地看图、打标签、生成描述、回答“帮我找去年海边那张有红色帐篷的照片”之类的问题。隐私不出设备，速度也足够交互。这个方向如果成立，小模型就不是“缩水版云模型”，而是一个很适合本地数据管理的入口。\n话不多说，现成的代码开始一键运行。\n硬件和模型 板子信息：\n项目 配置 板子 Firefly AIO-3588L SoC RK3588 内存 8GB 系统 Ubuntu 22.04.5 Kernel 6.1.118 NPU driver 0.9.8 模型选的是已经转换好的 RKLLM/RKNN 版本：\n文件 作用 大小 qwen3-vl-2b-instruct_w8a8_rk3588.rkllm LLM decoder 约 2.3GB qwen3-vl-2b_vision_rk3588.rknn vision encoder 约 812MB 空间占用如下 bash 1 2 3 4 5 firefly@firefly:/userdata/rkllm_demo$ df -h / Filesystem Size Used Avail Use% Mounted on overlayroot 52G 6.0G 44G 13% / firefly@firefly:/userdata/rkllm_demo$ du -sh . 3.2G . 第一版：能跑，但速度不对劲 最开始我用官方 multimodal C++ demo 包了一层 FastAPI，再写了一个简单网页。网页支持：\n文本对话。 上传图片识图。 SSE 流式输出。 显示 decode tokens/s、prefill tokens/s、内存占用和总耗时。 第一版能跑起来，但速度看起来只有 7 tokens/s 左右。\n因为我看 RKLLM 官方 benchmark 里，RK3588 上 Qwen3-VL-2B w8a8 的 multimodal decode 大约是 14.91 tokens/s，image encoder 大约 2.08s。如果我这里只有 7 tokens/s，还有不少优化空间。\n优化路线 把 RKLLM 线程绑到 RK3588 的 A76 大核：\ncpp 1 2 param.extend_param.enabled_cpus_num = 4; param.extend_param.enabled_cpus_mask = (1 \u0026lt;\u0026lt; 4) | (1 \u0026lt;\u0026lt; 5) | (1 \u0026lt;\u0026lt; 6) | (1 \u0026lt;\u0026lt; 7); 初始状态下，CPU governor 是 interactive，空闲时只有 408MHz；DDR 是 dmc_ondemand，空闲时 528MHz；GPU 也是低频。NPU 虽然已经在 950MHz，但 LLM decode 并不是“只有 NPU 干活”。CPU 唤醒、采样、回调、内存带宽都会影响每 token 间隔。\n我做了一个 boost_perf.sh 调整运行频率：\n这一步之后，速度明显上去了。禁用 CPU idle 是一个很小但有效的点：decode 是很多小步串起来的，每 token 之间 CPU 如果频繁进出低功耗状态，尾延迟会抖。\n除此之外的一些改动其实没什么太大的提升，以及，我自己的统计逻辑也优化一下哈哈哈\nWeb 层：一个“进程包装版”的 ChatWeb 先用 FastAPI 每次请求启动一次官方 demo：\ntext 1 2 3 4 5 6 7 FastAPI /api/chat -\u0026gt; 保存上传图片 -\u0026gt; 组织 demo 命令行参数 -\u0026gt; stdin 写入 prompt + exit -\u0026gt; stdout 解析 robot: 后面的内容 -\u0026gt; 碰到 __RKLLM_PERF__ 后发送 metrics 事件 -\u0026gt; SSE 返回给浏览器 这版的好处是简单、稳定、容易复现。坏处也很明显：每次请求都会重新加载 LLM 和 vision encoder，所以首 token 延迟大概 5 到 6 秒。\n页面如下 最后的速度 最后的实测结果：\n场景 decode 速度 CLI 文本短样本 14.3 到 14.4 tok/s CLI 图像短样本 13.8 到 14.0 tok/s CLI 文本强制长样本 14.29 tok/s CLI 图像强制长样本 13.84 tok/s Web SSE 文本请求 14.24 tok/s Web 上传图片识图 13.81 tok/s 图像 encoder 大约 2.1s。Web 冷启动总耗时大约 7 到 8 秒。\n这个结果基本接近 RKLLM benchmark 里 Qwen3-VL-2B 在 RK3588 上的 14.91 tok/s。\n那接着优化就是模型常驻，去掉 python 层等等等等。\n端侧 Agent 也许越来越近了 部署完这个，内存上也有余量：系统空闲时可用内存约 6GB，Qwen3-VL 当前推理内存约 2.8GB。再放一个小 embedding 模型是现实的。那么本地 RAG 实际也能理论闭环了，甚至如果你用大点内存，还能塞下 ASR 和 TTS。\n14 tokens/s 看起来比较慢其实也还好，因为不是 coding agent 场景，不需要高频次输出大量内容，体感上来讲可以做比较丝滑的交互不至于很折磨人。\n把它放到“相册管理”、本地小型知识库等等，确实有可行性\n参考链接 官方/一手资料\nRockchip RKLLM 官方仓库 RKLLM benchmark RKLLM multimodal demo RKLLM server demo Rockchip RKNN Toolkit 2 Qwen3-VL 模型集合 Qwen3-VL-2B-Instruct 官方模型 Qwen3-VL-Embedding-2B Firefly AIO-3588L 官方产品页 Firefly Core-3588L Wiki 转换模型/社区参考\nGatekeeperZA/Qwen3-VL-2B-Instruct-RKLLM JiahaoLi/Qwen3-VL-RK3588 Qengineering/Qwen3-VL-2B-NPU Radxa RKLLM 安装文档 我这套源码应该没啥用，大家的文档都很清楚了。\n","permalink":"/posts/%E6%8A%8A-qwen3-vl-%E5%A1%9E%E8%BF%9B-rk3588%E4%B8%80%E6%AC%A1%E7%AB%AF%E4%BE%A7-vl-agent-%E7%9A%84%E5%B0%8F%E5%AE%9E%E9%AA%8C/","summary":"\u003cp\u003e昨天刷到一个 AI 移动硬盘的广告，结合前段时间老板和我说的 AI NAS，本来没怎么放在心上，因为我本身不会是前者的客户，后者又不是小公司能做的，不过整理硬件的时候，发现一个 Frefly 的 RK3588 盒子，灵机一动、RK3588 有 NPU，理论肯定有人做这个端侧LLM 的推理。\u003c/p\u003e","title":"把 Qwen3-VL 塞进 RK3588：一次端侧 VL Agent 的小实验"},{"content":"李开复 - 维基百科，自由的百科全书 首先总结我对他的认识，零一万物的 CEO，和我爸一个年纪，过去的经历和争议我不太了解，第一次看到他是从两个访谈，一个是抖音他自己的号发的采访（《华尔街日报》领导力研究院院长艾伦·默里(Alan Murray)和他的对话），一个是虎嗅的采访，我google 了一下关键词，虽然几个类似的热点好像在两年前就有炒过，并且很多人对他的评价褒贬不一，但是由于我孤陋寡闻，现在才接触到这两个内容，对我而言还是有一些帮助，所以记录一下。\n这个是还有一个原因来自于这篇文章带来的提醒为什么懂得越多的人，越容易陷入“输入狂热，输出瘫痪”有空也得再写一篇\n我总结一下虎嗅的采访中的关键点\n他见了谁 最近一直在飞来飞去见各种 CEO 甚至总统 见这些人干嘛 （赚钱嘛）扯了很多，其实就是从上而下的 FDE（也许可以这样说）/解决方案提供 他们公司的业务现在做的是什么 To B 做类似 Palantir 干的活，这个公司我第一次听说，叫 GPT 帮我总结他是个给政府和大型机构做数据 + AI + 决策 + 行动的一体化操作系统公司。 他们对未来的看法 要盈利，然后盈更多的利 哈哈哈我总结的也很简单，但是为什么说对我来讲有帮助，真正让我有收获的，并不是李开复本人，而是采访里的一个信号：很多 AI 公司已经不再讨论模型，而是在讨论企业如何真正把 AI 接进业务流程。对我而言一来扩展了好多领域的词，原来一些大公司如 Palantir 做这些出发点这么早，二来也确实回答了我现在所在公司看到的情况，给别的企业开发也好，咨询也好，一个 Agent 产品，软件，系统，是很难切入公司的运转流程，里面有一个比喻那就是“马车装火车引擎”。\n由于我对其他产业的了解实在很浅薄，我用程序员 软件开发公司来比喻，同时 AI 也是在如今开发领域落地最成熟的。从一个未接触 AI 的程序员的角度出发，\n今天我了解了 codex 能帮助我写代码，一个月花 20 刀就够了，写了两天发现得用 200 刀，而工资没有超过 2000 刀，这个成本很高。 AI 把我的活都干了，在公司里我干的活更快了，而在一个公司里如果是协作状态，我的高产出会给到我的下游压力。下游同事的角度就不爱干了，同时也会给平级同事压力，大家都不用 AI 产出都差不多，而我遥遥领先，是不是显得他们没用（工贼！） 大家都用 AI，消耗很高，要公司买单，老板终于接触到 AI 了，一开始开心点头，没多久发现这笔账不划算。因为 十个人+十份 AI 套餐的产出其实有冗余，因为公司一般不会需要不停的新开发， 居然还不如 五个资深会用 AI 的+ 五份套餐，降本后也能维持原本的效果。 这应该是 AI 在过去一段时间大部分公司落地最明显的场景。而这个场景，和我们这种公司无关，赚不到钱。因为大家都想用顶级模型，如果有企业内网开发，需要自部署顶级模型，比如 GLM5.2，算一下权重接近 1T 多的大小，一台8 卡H20的服务器在 130 万左右，只能跑个量化模型，还很难支持并发，这在开发场景下几乎不可用，这个账一算就是无法接受的。那么这条路也堵死了。也许也有人愿意跑一些小型模型，比如 Qwen 系列 80-30B 之间的各种模型，这样子成本下来了，但是模型的能力也下来了，也许只能做很小部分的辅助（补全），根本做不到提效的效果。\n这么一说，我想到 如果这些小模型的 本地配合 一个 copilot代理或者 cursor tab 代理似乎也不错哦，回头看看这个场景能不能做，不知道这个内网开发的场景需求大不大\n那么这是程序员和软件行业老板的浅浅的看待，其次我瞎猜一下 AI 对普通公司能提效什么\n前段时间有一篇 钉钉内部员工离职的长文《置身钉内》写的很好。不得不让我感慨，哪怕大佬也要受好多这种无端之苦。\n可以看一下 workbuddy，这是一个腾讯出的 agent 产品，凭我在网上看到的不完全统计，这个产品应该还算比较不错的，只是我下载下来看过，眼花缭乱，给人一种 什么都想要的感觉，正好可以从中看出，什么能被用\n点开专家团，其实可以看出来，抛开 coding 场景（你信他能用的话不如给我打五十块钱我给你一个 key 体验一下 codex） 有内容创作、投资分析、法律咨询、企业运营、数据分析，等这些场景，这些场景的特点，要么是 LLM 的特色，内容创作，要么是一个标准的 RAG 场景其实，比如法律咨询、企业运营，这些都是强 context 需求，AI 需要知道这些上下文信息才能按你的要求产出那个文档和回答。数据分析那更加了，统计+分析，前提就得是有数据才行。\n这里可以映照 《置身钉内》提到的，AI 的生长落地需要 context，他提到的是做一个 SAAS 或者产品需要的 context，比如用户是谁，历史聊天记录，做了什么，这些丰富的数据，可以被提炼给 AI 做到帮助。在企业内部，其实也是同理，如果一个企业的管理流程都没有数字化，所谓的 AI 化那更加是痴人说梦。所以给企业做 AI 化的同时也是在优化+梳理+数字化企业的工作流程。而这个出发点确实需要从上而下出现。因为只有头拍板了，这件事情才有推动下去的可能，脚才能动，脚自己动是没有用的。这也是零一万物和 Palantir 说的业务里面最性感的那一块，一个全阶段在线的系统，能在我（CEO）问出：“如果这个供应商下周交不了货，我们哪个工厂会停线？帮我生成替代排产方案。”给出很好的回答，而不是我要联系某下属，而他可能要拆解，收集资料，找他的下属去做报告，这中间可能会带来很多的问题。\n这里我说不清楚了，还是很模糊，我觉得我还需要更多思考时间。\n总结一下，看完他的采访，又是比较无奈的，这没法抄作业，他的全球人脉能做到这件事情进行变现，我就算有这个能耐也能做这种工作，谁会鸟我呢。是吧。\n真正值钱的不是模型，而是能够接触到的数据、流程和决策链。\n总之就是对于 B 端做 AI 项目的一些小点子，我认为我可以掌握一下的，比如，如果有项目摆在面前必须确保搭上更有权的线、可能不是一号位、但某部门部长也能推进做这件事情。 得有 AI 能力边界的和成本的敏感认知，因为我是小型的解决方案提出者，我可以收集整理现成的方案报告，这才能让我需要卖的时候直接摆出来展示 对业务的快速理解和中译中的能力。\n因为在这个 AI 浪潮下，焦虑每天都弥漫在身边，FOMO 也好，担心工作不稳定也好，升职加薪希望渺茫，大环境看起来糟糟的，而我只是个刚出巢穴的菜鸟，知道的太少。害，只能安慰自己别急别急加油加油，除此之外，还能做什么呢，积极一些吧，少年。也许这只是潜龙勿用的开始呢。\n","permalink":"/posts/%E8%A7%82%E6%9D%8E%E5%BC%80%E5%A4%8D%E8%AE%BF%E8%B0%88%E6%9C%89%E6%84%9F/","summary":"\u003cp\u003e\u003ca href=\"https://zh.wikipedia.org/wiki/%E6%9D%8E%E5%BC%80%E5%A4%8D\"\u003e李开复 - 维基百科，自由的百科全书\u003c/a\u003e\n首先总结我对他的认识，零一万物的 CEO，和我爸一个年纪，过去的经历和争议我不太了解，第一次看到他是从两个访谈，一个是抖音他自己的号发的采访（《华尔街日报》领导力研究院院长艾伦·默里(Alan Murray)和他的对话），一个是虎嗅的采访，我google 了一下关键词，虽然几个类似的热点好像在两年前就有炒过，并且很多人对他的评价褒贬不一，但是由于我孤陋寡闻，现在才接触到这两个内容，对我而言还是有一些帮助，所以记录一下。\u003c/p\u003e","title":"观李开复访谈有感"},{"content":"今天在看 codex 的 system prompt 看到对 codex 的要求是 优先用 rg，我只知道这个工具比 grep 快，似乎是个现代化的 grep，那得学，我是现代人。\n参考了几篇文章：Levon 的 文本搜索神器rg的使用教程、Autumn Skerritt 的 Ripgrep cheatsheet、Marius Schulz 的 Fast Searching with ripgrep，以及官方的 ripgrep README 和 User Guide。\n这篇不是全文搬运。我只是把我最可能用到的东西重新按自己的脑回路记一遍。\nrg 是干嘛的 一句话：\nrg 是用来在文件里搜文字的命令。\n它的全名叫 ripgrep，但是命令名是 rg。\n最基础的样子长这样：\nbash 1 rg \u0026#34;要搜索的内容\u0026#34; 这行命令的意思是：\n在当前目录下面，递归搜索所有文件，只要某一行包含 \u0026quot;要搜索的内容\u0026quot;，就把那一行打印出来。\n这里有几个初学者必须先钉死在脑子里的点：\nrg 默认就是递归搜索，不用像 grep -R 那样自己加 -R 不写目录时，默认搜当前目录，也就是 ./ 它默认会读 .gitignore 它默认不搜隐藏文件，比如 .env 它默认不搜二进制文件，比如图片、数据库文件 它默认把搜索内容当正则表达式，不是纯字符串 安装和确认 Mac 上一般就是：\nbash 1 brew install ripgrep Ubuntu / Debian：\nbash 1 sudo apt-get install ripgrep Windows 如果用 Scoop：\npowershell 1 scoop install ripgrep 装完之后确认一下：\nbash 1 rg --version 我这台机器现在是：\ntext 1 2 3 4 5 6 7 ripgrep 15.1.0 features:+pcre2 simd(compile):+NEON simd(runtime):+NEON PCRE2 10.45 is available (JIT is available) 这里的 PCRE2 is available 先记一下就行。简单理解就是：需要更复杂的正则时，可以用 -P 开一个更强的正则引擎。\n但初学者先别管。\n先跑通再说。\n最基础的搜索 比如我想在当前博客仓库里找 waline：\nbash 1 rg \u0026#34;waline\u0026#34; 如果只想搜 content 目录：\nbash 1 rg \u0026#34;waline\u0026#34; content 如果只想搜某个文件：\nbash 1 rg \u0026#34;waline\u0026#34; content/posts/hugo-配置waline.md rg 输出一般是这种感觉：\ntext 1 2 文件路径 行号:匹配到的那一行 如果输出太多，就先缩小目录。\n不要上来就在整个硬盘搜。那是拿电饭煲煮太平洋。\n最常用的一组 不是背文档那种背，是“下次忘了回来复制”。\n命令 我自己的记法 rg \u0026quot;foo\u0026quot; 在当前目录搜 foo rg \u0026quot;foo\u0026quot; content 只搜 content 目录 rg -i \u0026quot;foo\u0026quot; 忽略大小写，Foo、FOO 都算 rg -S \u0026quot;foo\u0026quot; smart case，有大写就区分大小写，没大写就不区分 rg -w \u0026quot;foo\u0026quot; 只匹配完整单词，不把 foobar 算进去 rg -F \u0026quot;foo.*\u0026quot; 按普通字符串搜，不把 .* 当正则 rg -n \u0026quot;foo\u0026quot; 显示行号，虽然很多时候默认就有 rg -l \u0026quot;foo\u0026quot; 只显示包含 foo 的文件名 rg -c \u0026quot;foo\u0026quot; 统计每个文件里匹配了多少行 rg -C 2 \u0026quot;foo\u0026quot; 显示命中行前后各 2 行 rg -A 3 \u0026quot;foo\u0026quot; 显示命中行后面 3 行 rg -B 3 \u0026quot;foo\u0026quot; 显示命中行前面 3 行 rg --files 列出 rg 准备搜索的文件 rg --debug \u0026quot;foo\u0026quot; 看看为什么某些文件没被搜 我感觉常用到这里已经够干很多活了。\n不要一开始就追求把 man page 背下来。\n会被自己吓死。\n正则和普通字符串 rg 默认把搜索词当正则。\n所以这两个是不一样的：\nbash 1 2 rg \u0026#34;hello.*\u0026#34; rg -F \u0026#34;hello.*\u0026#34; 第一个会把 .* 当正则，意思是 hello 后面跟任意内容。\n第二个 -F 是 fixed strings，意思是我就要搜字面量 hello.* 这几个字符。\n这个非常重要。\n比如你搜这种东西：\nbash 1 rg \u0026#34;?.\u0026#34; 很可能直接报正则错误，因为 ? 在正则里不是普通问号。\n这个时候别跟它硬刚，直接：\nbash 1 rg -F \u0026#34;?.\u0026#34; 可以。\n对初学者来说，我建议一个简单判断：\n如果要搜的内容里有这些奇怪符号：\ntext 1 . * ? + ( ) [ ] { } | ^ $ 而你只是想搜原文，那就加 -F。\n省心。\n只搜某类文件 比如只搜 Markdown：\nbash 1 rg \u0026#34;draft: true\u0026#34; -tmd 或者写完整一点：\nbash 1 rg \u0026#34;draft: true\u0026#34; --type markdown 只搜 Go：\nbash 1 rg \u0026#34;func main\u0026#34; -tgo 只搜 Python：\nbash 1 rg \u0026#34;import os\u0026#34; -tpy 排除某类文件用大写 -T：\nbash 1 rg \u0026#34;TODO\u0026#34; -Tmd 这个意思是搜 TODO，但是不要 Markdown 文件。\n查 rg 支持哪些文件类型：\nbash 1 rg --type-list 如果列表太长，可以自己搜一下：\nbash 1 rg --type-list | rg \u0026#34;markdown\u0026#34; 我这边看到 Markdown 是支持 markdown 和 md 的：\ntext 1 2 markdown: *.markdown, *.md, *.mdown, *.mdwn, *.mdx, *.mkd, *.mkdn md: *.markdown, *.md, *.mdown, *.mdwn, *.mdx, *.mkd, *.mkdn 所以 -t md 是可以用的。\n用 glob 控制范围 有时候文件类型不够用，就用 -g。\n只搜 .md：\nbash 1 rg \u0026#34;hugo\u0026#34; -g \u0026#34;*.md\u0026#34; 不搜 .md：\nbash 1 rg \u0026#34;hugo\u0026#34; -g \u0026#34;!*.md\u0026#34; 排除某个目录，比如不看 public：\nbash 1 rg \u0026#34;hugo\u0026#34; -g \u0026#34;!public/\u0026#34; 排除多个目录：\nbash 1 rg \u0026#34;hugo\u0026#34; -g \u0026#34;!public/\u0026#34; -g \u0026#34;!themes/\u0026#34; 这里有个很容易傻掉的点：\n! 在这里表示排除。\n而且建议用引号包起来：\nbash 1 rg \u0026#34;hugo\u0026#34; -g \u0026#34;!public/\u0026#34; 别让 shell 先把 *、! 之类的东西展开了。\n我已经能想象未来的我在这里卡十分钟，然后怪工具不行。\n只看文件名 rg --files 这个很有用。\n它不是搜内容，是列出 rg 会搜索的文件。\nbash 1 rg --files 找所有 Markdown 文件：\nbash 1 rg --files -g \u0026#34;*.md\u0026#34; 找路径里带 nginx 的文件：\nbash 1 rg --files | rg \u0026#34;nginx\u0026#34; 这玩意有点像简化版 find。\n尤其我只是想找文件名的时候，不想写很长的 find . -name。\n可以。\n搜不到时先别慌 这是我觉得最应该记住的一节。\nrg 搜不到，不一定是没有。\n它可能没搜。\n默认情况下，rg 会跳过这些东西：\n.gitignore、.ignore、.rgignore 规则忽略的文件 隐藏文件和隐藏目录 二进制文件 符号链接指向的内容 所以排查顺序是这样。\n先看 rg 到底准备搜哪些文件：\nbash 1 rg --files 如果你怀疑文件被 ignore 了：\nbash 1 rg --no-ignore \u0026#34;要搜的内容\u0026#34; 或者短一点：\nbash 1 rg -u \u0026#34;要搜的内容\u0026#34; 如果你怀疑是隐藏文件，比如 .env：\nbash 1 rg --hidden \u0026#34;要搜的内容\u0026#34; 或者：\nbash 1 rg -uu \u0026#34;要搜的内容\u0026#34; 如果你就是要把所有限制都掀了：\nbash 1 rg -uuu \u0026#34;要搜的内容\u0026#34; 这个要稍微小心点，因为它连二进制文件也可能搜。\n有时候会输出奇怪东西。\n别问我怎么知道的，理论上就很危险。\n如果还不懂为什么搜不到：\nbash 1 rg --debug \u0026#34;要搜的内容\u0026#34; --debug 会告诉你一些文件为什么被跳过。\n这比原地怀疑人生强。\n看上下文 只看到一行有时候不够。\n比如搜 TODO，我想看前后几行到底在干嘛：\nbash 1 rg \u0026#34;TODO\u0026#34; -C 2 只看后面三行：\nbash 1 rg \u0026#34;TODO\u0026#34; -A 3 只看前面三行：\nbash 1 rg \u0026#34;TODO\u0026#34; -B 3 我自己的记法：\nC 是 context，前后都看 A 是 after，后面 B 是 before，前面 这三个比我想象中实用。\n尤其是在不想打开编辑器的时候。\n只看匹配的文件 有时候我不是想看匹配内容，只想知道哪些文件里有。\nbash 1 rg -l \u0026#34;TODO\u0026#34; 比如我想知道哪些文章里提到了 Cloudflare：\nbash 1 rg -l \u0026#34;Cloudflare\u0026#34; content/posts 如果想排序：\nbash 1 rg -l \u0026#34;Cloudflare\u0026#34; content/posts --sort path 官方和 Marius 都提醒过，--sort path 会让它没法并行，所以理论上会慢一点。\n但是小项目里没啥感觉。\n我这种博客仓库里用，完全够。\n统计一下 每个文件里匹配了多少行：\nbash 1 rg -c \u0026#34;TODO\u0026#34; 看整体统计：\nbash 1 rg \u0026#34;TODO\u0026#34; --stats 这个适合搜之前评估一下工程量。\n比如搜出来 3 个，我可以手改。\n搜出来 3000 个，你也不想炸掉终端上下翻吧。\n替换，但不是改文件 rg 有 -r / --replace：\nbash 1 rg \u0026#34;fast\u0026#34; -r \u0026#34;FAST\u0026#34; 但是注意：\n它只改输出。\n它不会真的改文件。\n这个点很关键，官方文档也专门写了。不要以为跑完文件就变了。\n如果真要批量替换，通常是：\nbash 1 rg -l \u0026#34;旧内容\u0026#34; 先找文件，然后再交给编辑器、脚本、perl、sd、sed 之类的工具处理。\n这里我先不展开。\n批量替换很容易把仓库炸一地，日后单独记。\n搜压缩包和编码 这个不常用，但先留个坑。\n搜压缩文件：\nbash 1 rg -z \u0026#34;关键字\u0026#34; -z 可以搜一些常见压缩格式，比如 gzip、xz、zstd。\n搜非 UTF-8 编码，比如 GBK：\nbash 1 rg -E gbk \u0026#34;关键字\u0026#34; 这两个属于“知道有这个东西就行”。\n真遇到再看官方文档。\n稍微复杂一点的正则 rg 默认用 Rust 的 regex 引擎。\n它快，但是有些高级正则不支持，比如 look-around、backreference。\n如果你的 rg --version 里有 PCRE2，就可以这样：\nbash 1 rg -P \u0026#34;foo(?=bar)\u0026#34; -P 就是启用 PCRE2。\n但是我建议初学者先别沉迷这个。\n大部分日常搜索，用普通字符串、-F、-i、-w、-g、-t 已经能解决 90% 的问题。\n先别把自己搞晕。\n最后给自己贴一张小抄 [bash] 显示已折叠代码（53 行） 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 # 当前目录递归搜索 rg \u0026#34;keyword\u0026#34; # 指定目录 rg \u0026#34;keyword\u0026#34; content/posts # 忽略大小写 rg -i \u0026#34;keyword\u0026#34; # smart case rg -S \u0026#34;keyword\u0026#34; # 完整单词 rg -w \u0026#34;keyword\u0026#34; # 普通字符串，不走正则 rg -F \u0026#34;hello.*\u0026#34; # 只搜 Markdown rg \u0026#34;keyword\u0026#34; -tmd # 排除 Markdown rg \u0026#34;keyword\u0026#34; -Tmd # glob 过滤 rg \u0026#34;keyword\u0026#34; -g \u0026#34;*.md\u0026#34; rg \u0026#34;keyword\u0026#34; -g \u0026#34;!public/\u0026#34; # 只列文件名 rg -l \u0026#34;keyword\u0026#34; # 看上下文 rg \u0026#34;keyword\u0026#34; -C 2 rg \u0026#34;keyword\u0026#34; -A 3 rg \u0026#34;keyword\u0026#34; -B 3 # 统计 rg -c \u0026#34;keyword\u0026#34; rg \u0026#34;keyword\u0026#34; --stats # 看 rg 会搜哪些文件 rg --files # 查支持的文件类型 rg --type-list # 搜不到时掀过滤规则 rg -u \u0026#34;keyword\u0026#34; rg -uu \u0026#34;keyword\u0026#34; rg -uuu \u0026#34;keyword\u0026#34; # 调试为什么没搜到 rg --debug \u0026#34;keyword\u0026#34; 先这样吧。终端用的也不算多。\n目前看来，我对 rg 的需求就是：快速搜代码、快速找文件、搜不到时知道怎么排查。\n再高级的配置、编辑器集成、rga 搜 PDF 这些，后面真用到再写。\n收工。\n","permalink":"/posts/rg-%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B/","summary":"\u003cp\u003e今天在看 codex 的 system prompt 看到对 codex 的要求是 优先用 rg，我只知道这个工具比 grep 快，似乎是个现代化的 grep，那得学，我是现代人。\u003c/p\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e参考了几篇文章：Levon 的 \u003ca href=\"https://www.liuvv.com/p/868944ef.html\"\u003e文本搜索神器rg的使用教程\u003c/a\u003e、Autumn Skerritt 的 \u003ca href=\"https://skerritt.blog/ripgrep-cheatsheet/\"\u003eRipgrep cheatsheet\u003c/a\u003e、Marius Schulz 的 \u003ca href=\"https://mariusschulz.com/blog/fast-searching-with-ripgrep\"\u003eFast Searching with ripgrep\u003c/a\u003e，以及官方的 \u003ca href=\"https://github.com/BurntSushi/ripgrep\"\u003eripgrep README\u003c/a\u003e 和 \u003ca href=\"https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md\"\u003eUser Guide\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e这篇不是全文搬运。我只是把我最可能用到的东西重新按自己的脑回路记一遍。\u003c/p\u003e","title":"rg 入门教程"},{"content":" 忘了在哪个群，有位大学新生询问我什么是 AI 反代，同时我突然意识到博客很久没有更新了，于是想到这个可以水一篇。关于正代反代和 AI 反代。\n概念 首先 这个词是 正向代理和反向代理的简称，之前一般是用在网络应用层，如果用过nginx 部署项目也许就听说过，那么简单讲一下什么是正向代理，什么是反向代理\n正向代理 小王（客户端） 要找 小李（服务器）租房，但是小王不想用自己的名义向小李租房，于是他叫小徐 （代理）去帮忙租房。这种隐藏小王身份的方式，就叫做正向代理\n反向代理 小王（客户端）找 小李（代理）租房，但是小李就像二房东或者中介是在小王不知情的情况下去找小徐（服务端）租房然后把房子给小王。这会隐藏小徐的身份，这叫反向代理。\n就像你在城中村签了租房合同直到最后退租可能都不知道真实房东是谁。二房东就在做反向代理。\nAI 反代 AI 反向代理（AI Reverse Proxy）是指将主流 AI 服务（如 OpenAI、Claude、Gemini 等）的官方接口进行中转和协议转换的本地或云端服务。\n它主要分为两大类用途：\n标准 API 转换：将官方网页版（如 Google AI Studio）或不支持直接调用的平台转换为标准的 OpenAI 格式接口，供第三方客户端（如 Cherry Studio）调用。 网关中转与分流：管理多个 API 账号、进行负载均衡（解决频率限制）、隐藏真实 IP、或在国内网络环境下加速 API 访问 总结 正向代理隐藏的是客户端身份，但不是对所有人都隐藏。代理是知道客户端是谁的。 反向代理隐藏的是真实服务端，但也不是绝对隐身。响应头、错误页、日志、证书配置这些地方都可能漏出后端信息。 那么到这一步应该会发现 AI 反代这个词本身不是特别合理，但是这个词的含义已经被泛化和转义了，它不仅包含了网络层面的中转，更核心的功能是协议转换、身份伪装和多账号聚合。\n这个称呼的演变主要是因为以下两个历史原因：\n最早的刚需是“网络中转”：早期国内开发者无法直接连接 OpenAI 官方 API。大家最开始的做法是用 Cloudflare Workers 或海外 VPS 搭一个 Nginx，把请求直接转发给 OpenAI。这个阶段它确实是标准的网络反向代理。 概念被沿用并扩展：后来，单纯的“网络中转”无法满足需求（比如需要解决多账号限频、网页端 Cookie 登录、异构模型统一输入等问题）。开发者便在“中转服务器”上编写了业务代码（如 Python/Go）。虽然它已经变成了**“网关（Gateway）”或“中间件（Middleware）”**，但大家依然习惯性地称其为“AI 反代”。 ","permalink":"/posts/%E4%BB%80%E4%B9%88%E6%98%AFai%E5%8F%8D%E4%BB%A3%E5%92%8C%E5%8F%8D%E4%BB%A3%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e忘了在哪个群，有位大学新生询问我什么是 AI 反代，同时我突然意识到博客很久没有更新了，于是想到这个可以水一篇。关于正代反代和 AI 反代。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"概念\"\u003e概念\u003c/h2\u003e\n\u003cp\u003e首先 这个词是 正向代理和反向代理的简称，之前一般是用在网络应用层，如果用过nginx 部署项目也许就听说过，那么简单讲一下什么是正向代理，什么是反向代理\u003c/p\u003e","title":"什么是AI反代，和反代什么区别"},{"content":"GLM 5.2 已经上线一周多了，自从 5 出来后，对于国模编程 NO.1 的名号也是听说多次，前面包括 5.1 都浅试过，不过由于 claude 和 GPT 对我而言获得还算容易所以一直没有在意，这次 5.2 出来恰逢 claude 各种波折，可以说几乎很费劲才能维护 claude 的订阅，更何况我需要给团队多位开发同学分发。所以准备看看 5.2 的能力，在 Zcode 浅浅试了两天发现确实还可以，但是 free 的 差不多一个问题二十分钟，reconnect 占用了十五分钟，本以为买个 coding plan 会成为人上人，没想到送钱还得抢着来。整整一周，每一天10AM 准时蹲在浏览器前面。 不曾想，不管是我用任何脚本还是手搓、还是从 zcode 里面订阅，亦或是换不同的网络去订阅，都无济于事，整整一周啊，我抢了七天多都没抢到。\n看到有网友讨论国际版能直接买，（囊中也不是很丰腴）关键让我不敢直接买的原因还是在于听网友已有 max 订阅的体验甚至也会因为算力不足而429。\n国模加把劲啊。🥹\n","permalink":"/posts/glm-%E5%88%B0%E5%BA%95%E6%80%8E%E4%B9%88%E6%89%8D%E8%83%BD%E6%8A%A2%E5%88%B0/","summary":"\u003cp\u003eGLM 5.2 已经上线一周多了，自从 5 出来后，对于国模编程 NO.1 的名号也是听说多次，前面包括 5.1 都浅试过，不过由于 claude 和 GPT 对我而言获得还算容易所以一直没有在意，这次 5.2 出来恰逢 claude 各种波折，可以说几乎很费劲才能维护 claude 的订阅，更何况我需要给团队多位开发同学分发。所以准备看看 5.2 的能力，在 Zcode 浅浅试了两天发现确实还可以，但是 free 的 差不多一个问题二十分钟，reconnect 占用了十五分钟，本以为买个 coding plan 会成为人上人，没想到送钱还得抢着来。整整一周，每一天10AM 准时蹲在浏览器前面。\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/06/335d0e4c23c59d2aa424d5ced93266a9.png\"\u003e\n        \u003cimg alt=\"GLM-5.2 Coding Plan 抢购页面\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/06/335d0e4c23c59d2aa424d5ced93266a9.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\n不曾想，不管是我用任何脚本还是手搓、还是从 zcode 里面订阅，亦或是换不同的网络去订阅，都无济于事，整整一周啊，我抢了七天多都没抢到。\u003c/p\u003e","title":"GLM 到底怎么才能抢到"},{"content":"你知道长期主义是一个胜率很高的打法，但是为什么无法坚持。因为最终收益被拆分到每个时间段后无法对抗短期诱惑。你知道产品要慢慢打磨，但是市场不会因为你的耐心多给你几个月，你的团队每天都在支出。你知道个人品牌要沉淀，但是平台每天都在用播放量、点赞等数据审判你。\n生活不会因为你正在积累就暂停支。\n这很痛苦。长期主义的逻辑是对的，但是过程不免费，而且相当贵。\n我依稀记得多年前《原子习惯》中的一个观点，被套在马斯克的一次采访画面中，被我看到后深深记住的内容：If you get 1% better each day for one year, you’ll end up 37 times better.\n这没错，但是不动脑子也晓得一件事情。如果这件事很简单，那么应该遍地都是成功人士了。好不说这种丧气话。剖析一下为什么。\n长期主义的收益在未来（说句不好听的，甚至未必有收益）而它的成本从今天就开始算。所以长期主义 在 “普通人”身上最现实的摩擦就是 成本不足。对于资源充足的人来说，慢是一种选择。对于普通人来讲，慢是一种成本。\n什么成本\n现金流：收益没有到来之前，成本已经发生，没有现金流缓冲，所有长期计划都会接受审判。\n注意力：缺钱的人反复想钱，缺流量的人反复看数据，缺认可的人反复想证明。时间还在，注意力已经被抵押。\n情绪、身份、证明、不确定性等等 等等。这都不是一句 慢慢来 能概括的。\n人不在真空中做选择，一个人吃饱喝足才能思考未来，饥寒交迫的人只能考虑眼瞎，因为压力下会天然短视，这不是意志力能抵抗的，这是在抗拒老祖宗传下来千年的生存基因，在违抗人性，如果你要压下这一切，就很容易进入长期焦虑状态。\n普通人应该做的，不是用坚持长期主义来包装、来逃避。而是正视这一切，来抠出资源存下来。\n建立系统\n现实不断收费，记得攒点给未来。\n","permalink":"/posts/%E4%B8%BA%E4%BB%80%E4%B9%88%E9%95%BF%E6%9C%9F%E4%B8%BB%E4%B9%89%E8%BF%99%E4%B9%88%E9%9A%BE/","summary":"\u003cp\u003e你知道长期主义是一个胜率很高的打法，但是为什么无法坚持。因为最终收益被拆分到每个时间段后无法对抗短期诱惑。你知道产品要慢慢打磨，但是市场不会因为你的耐心多给你几个月，你的团队每天都在支出。你知道个人品牌要沉淀，但是平台每天都在用播放量、点赞等数据审判你。\u003c/p\u003e","title":"为什么长期主义这么难"},{"content":" 先说结论： 服务没有开放 80/443 端口时，不能直接走依赖 80 端口的 HTTP-01 验证。这次我改用 acme.sh + Cloudflare DNS Challenge 申请泛域名证书，再通过 --install-cert 写回 Nginx 挂载目录，并在续期后自动重启容器。\n背景 我有一套自建的 Docker Registry Proxy 服务，可看这篇博客：docker 自建镜像加速，用来代理 Docker Hub、GHCR、Quay、GCR、K8S、MCR、NVCR 等镜像源。\n服务整体跑在 Debian 服务器上，Nginx 运行在 Docker 容器中，由于没有 80、443 端口。对外统一暴露 5050 端口。\n假设我的域名是 666.xyz ，访问方式大概是：\ntext 1 2 3 4 5 6 7 8 https://hubcmd.666.xyz:5050/ https://ghcr.666.xyz:5050/ https://quay.666.xyz:5050/ https://gcr.666.xyz:5050/ https://k8s.666.xyz:5050/ https://mcr.666.xyz:5050/ https://elastic.666.xyz:5050/ https://nvcr.666.xyz:5050/ 这些域名都在 Cloudflare 做 DNS 解析，并且关闭了小黄云代理，直接解析到服务器公网 IP。\n这次问题的起因，是我在服务器通过mirror拉取镜像时遇到报错，报错如下。\n1 ERROR: failed to solve: python:3.12-slim: failed to resolve source metadata for docker.io/library/python:3.12-slim: failed to do request: Head \u0026#34;https://hub.666.xyz:5050/v2/library/python/manifests/3.12-slim?ns=docker.io\u0026#34;: tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2026-06-02T16:35:32+08:00 is after 2026-05-31T06:13:08Z 好家伙，证书过期，这是第一次纯手工部署的项目证书过期，之前偷懒都是用的宝塔、1panel 这些平台托管，想起来了 certbot 什么的可以用，但我还没试过，刚好试试。\n开始排查。\n一开始看到的证书状态 服务器上的证书文件在：\nbash 1 ~/registry-proxy/nginx/ssl/ 里面有两个文件：\nbash 1 2 666.xyz.pem 666.xyz.key 查看证书内容后发现，这是一张 Let\u0026rsquo;s Encrypt 签发的证书，应该是之前用宝塔申请的，覆盖域名：\ntext 1 2 666.xyz *.666.xyz 这是一张泛域名证书，它可以覆盖 hubcmd.666.xyz、ghcr.666.xyz、quay.666.xyz 这类一级子域名。\n但证书有效期是：\ntext 1 2 notBefore=Mar 2 06:13:09 2026 GMT notAfter=May 31 06:13:08 2026 GMT 当前时间已经是 2026 年 6 月 2 日，所以证书确实过期了。\n可以用下面命令确认：\nbash 1 openssl x509 -in 666.xyz.pem -noout -dates 我这里没看，因为这个服务器只有这一个服务。懒得看了。\n项目结构和 Nginx 部署方式 先看 Docker 容器：\nbash 1 docker ps 关键容器是：\ntext 1 registry-nginx 对应镜像：\ntext 1 nginx:1.27-alpine Docker Compose 里 Nginx 的配置如下：\nyaml 1 2 3 4 5 6 7 8 9 10 11 12 nginx: container_name: registry-nginx image: nginx:1.27-alpine restart: always ports: - \u0026#34;5050:5050\u0026#34; volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./nginx/ssl:/etc/nginx/ssl - ./nginx/logs:/var/log/nginx networks: - registry-net 这里有两个关键点：\n第一，Nginx 是跑在 Docker 容器里的。\n第二，宿主机的证书目录：\nbash 1 ~/registry-proxy/nginx/ssl 会被挂载到容器内的：\nbash 1 /etc/nginx/ssl 所以只要更新宿主机目录里的证书文件，然后重启或 reload Nginx 容器，容器就会使用新证书。\nCertbot 发现用不了 一开始想到安装 Certbot，然后通过 HTTP 验证申请证书。\n但这台服务器并没有开放标准的 80 和 443 端口。\n验证如下：\nbash 1 curl -I http://hubcmd.666.xyz 结果：\ntext 1 curl: (7) Failed to connect to hub.666.xyz port 80 而访问带端口的地址：\nbash 1 curl -I http://hubcmd.666.xyz:5050 返回：\ntext 1 2 HTTP/1.1 400 Bad Request Server: nginx/1.27.5 这是因为 5050 端口上跑的是 HTTPS 服务，普通 HTTP 请求打过去会被 Nginx 拒绝。\n当前 Nginx 配置里也是：\nnginx 1 2 3 4 5 6 7 server { listen 5050 ssl; server_name _; ssl_certificate /etc/nginx/ssl/666.xyz.pem; ssl_certificate_key /etc/nginx/ssl/666.xyz.key; } 由于没有 80 和 443 端口，HTTP-01 验证并不适合这个场景。\n选择 DNS Challenge 这个场景最终选择了 DNS Challenge。\n原因是：\ntext 1 2 3 4 5 没有 80 端口 没有 443 端口 所有服务都通过 5050 访问 域名 DNS 托管在 Cloudflare 需要申请泛域名证书 DNS Challenge 不依赖服务器开放任何 Web 端口，只需要通过 Cloudflare API 自动添加 _acme-challenge TXT 记录即可。\n最终链路是：\ntext 1 2 3 4 5 6 7 8 9 10 11 12 13 Let\u0026#39;s Encrypt ↓ acme.sh ↓ Cloudflare DNS API ↓ DNS Challenge 验证 ↓ 生成泛域名证书 ↓ 安装到 nginx/ssl 目录 ↓ 重启 registry-nginx 安装 acme.sh 安装 acme.sh：\nbash 1 2 curl https://get.acme.sh | sh source ~/.bashrc 检查版本：\nbash 1 acme.sh --version Cloudflare API Token 在 Cloudflare 后台创建 API Token。\n权限只需要给目标 Zone 的 DNS 编辑权限。\n权限配置大概是：\ntext 1 2 3 Zone DNS Edit Zone Resources 选择：\ntext 1 2 3 Include Specific zone 666.xyz 创建完成后，在服务器上配置环境变量：\nbash 1 export CF_Token=\u0026#34;你的 Cloudflare API Token\u0026#34; 可以写入 ~/.bashrc：\nbash 1 2 echo \u0026#39;export CF_Token=\u0026#34;你的 Cloudflare API Token\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc 我这里直接把这个域名下的 DNS$Zone 权限都给他放开了，然后只允许改 IP 调用请求。\nacme.sh 默认 CA 的坑 第一次执行申请命令时，遇到了一个小坑。\n执行：\nbash 1 2 3 4 acme.sh --issue \\ --dns dns_cf \\ -d 666.xyz \\ -d \u0026#39;*.666.xyz\u0026#39; 输出：\ntext 1 2 3 4 Using CA: https://acme.zerossl.com/v2/DV90 No EAB credentials found for ZeroSSL Please update your account with an email address first. acme.sh --register-account -m my@example.com 原因是新版本 acme.sh 默认 CA 可能是 ZeroSSL，而 ZeroSSL 需要先注册账户邮箱。\n我原来的证书就是 Let\u0026rsquo;s Encrypt 签发的，所以这里直接切回 Let\u0026rsquo;s Encrypt：\nbash 1 acme.sh --set-default-ca --server letsencrypt 然后重新申请：\nbash 1 2 3 4 acme.sh --issue \\ --dns dns_cf \\ -d 666.xyz \\ -d \u0026#39;*.666.xyz\u0026#39; 这次成功签发。\n成功输出：\ntext 1 2 3 4 5 6 Your cert is in: /home/yangr/.acme.sh/666.xyz_ecc/666.xyz.cer Your cert key is in: /home/yangr/.acme.sh/666.xyz_ecc/666.xyz.key The intermediate CA cert is in: /home/yangr/.acme.sh/666.xyz_ecc/ca.cer And the full-chain cert is in: /home/yangr/.acme.sh/666.xyz_ecc/fullchain.cer ARI suggestedWindow: 2026-07-31T10:51:25Z to 2026-08-02T06:02:15Z Next renewal time picked from ARI window: 2026-07-31T17:03:09Z 这里生成的是 ECC 证书，所以目录名是：\nbash 1 /home/yangr/.acme.sh/666.xyz_ecc/ 后续安装证书时需要带上 --ecc 参数。\n安装证书到 Nginx 目录 先备份旧证书：\nbash 1 2 3 4 cd ~/registry-proxy/nginx/ssl mv 666.xyz.pem 666.xyz.pem.bak mv 666.xyz.key 666.xyz.key.bak 然后把 acme.sh 生成的证书安装到原来的 Nginx 证书路径：\nbash 1 2 3 4 5 6 acme.sh --install-cert \\ -d 666.xyz \\ --ecc \\ --key-file ~/registry-proxy/nginx/ssl/666.xyz.key \\ --fullchain-file ~/registry-proxy/nginx/ssl/666.xyz.pem \\ --reloadcmd \u0026#34;docker restart registry-nginx\u0026#34; 这一步做了三件事：\ntext 1 2 3 把私钥安装到 ~/registry-proxy/nginx/ssl/666.xyz.key 把完整证书链安装到 ~/registry-proxy/nginx/ssl/666.xyz.pem 续期后自动执行 docker restart registry-nginx 注意这里安装的是 fullchain，因为 Nginx 对外服务时需要提供完整证书链。\n检查 Nginx 证书引用 检查 Nginx 配置：\nbash 1 grep ssl_certificate -R ~/registry-proxy/nginx/conf.d 输出：\ntext 1 2 /home/yangr/registry-proxy/nginx/conf.d/registry.conf: ssl_certificate /etc/nginx/ssl/666.xyz.pem; /home/yangr/registry-proxy/nginx/conf.d/registry.conf: ssl_certificate_key /etc/nginx/ssl/666.xyz.key; 这说明容器内 Nginx 读取的是：\nbash 1 2 /etc/nginx/ssl/666.xyz.pem /etc/nginx/ssl/666.xyz.key 对应宿主机路径正好是：\nbash 1 2 ~/registry-proxy/nginx/ssl/666.xyz.pem ~/registry-proxy/nginx/ssl/666.xyz.key 配置是匹配的。\n检查新证书有效期 查看本地证书：\nbash 1 2 3 4 openssl x509 \\ -in ~/registry-proxy/nginx/ssl/666.xyz.pem \\ -noout \\ -dates 输出：\ntext 1 2 notBefore=Jun 2 08:07:21 2026 GMT notAfter=Aug 31 08:07:20 2026 GMT 这说明宿主机上的证书已经更新成功。\n再检查线上服务实际返回的证书：\nbash 1 2 3 4 openssl s_client \\ -connect hubcmd.666.xyz:5050 \\ -servername hubcmd.666.xyz \\ \u0026lt;/dev/null 2\u0026gt;/dev/null | openssl x509 -noout -dates -issuer -subject 输出：\ntext 1 2 3 4 notBefore=Jun 2 08:07:21 2026 GMT notAfter=Aug 31 08:07:20 2026 GMT issuer=C = US, O = Let\u0026#39;s Encrypt, CN = YE2 subject=CN = 666.xyz 这说明浏览器访问的 hubcmd.666.xyz:5050 已经拿到了新证书。\n其中：\ntext 1 subject=CN = 666.xyz 不代表 hubcmd.666.xyz 不被覆盖。\n因为现代浏览器主要看 SAN，也就是证书里的 Subject Alternative Name。当前证书是：\ntext 1 2 666.xyz *.666.xyz 所以 hubcmd.666.xyz 是可以匹配的。\n自动续期检查 acme.sh 安装后会自动写入 crontab。\n查看：\nbash 1 crontab -l 输出：\ntext 1 57 7 * * * \u0026#34;/home/yangr/.acme.sh\u0026#34;/acme.sh --cron --home \u0026#34;/home/yangr/.acme.sh\u0026#34; \u0026gt; /dev/null 这表示每天会执行一次 acme.sh 的自动续期检查。\n真正的续期时间由 acme.sh 和 CA 共同决定。这次签发时输出了：\ntext 1 2 ARI suggestedWindow: 2026-07-31T10:51:25Z to 2026-08-02T06:02:15Z Next renewal time picked from ARI window: 2026-07-31T17:03:09Z 也就是说，下次续期会在建议窗口内自动进行。\n由于前面 --install-cert 时已经配置了：\nbash 1 --reloadcmd \u0026#34;docker restart registry-nginx\u0026#34; 所以后续续期成功后会自动覆盖 Nginx 使用的证书，并重启 registry-nginx 容器。\nNginx 配置摘要 当前 Nginx 的核心配置如下：\n[nginx] 显示已折叠代码（79 行） 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 map $http_upgrade $connection_upgrade { default upgrade; \u0026#39;\u0026#39; close; } upstream dockerhub_backend { server dockerhub:5000; } upstream ghcr_backend { server ghcr:5000; } upstream gcr_backend { server gcr:5000; } upstream k8s_backend { server k8s:5000; } upstream quay_backend { server quay:5000; } upstream mcr_backend { server mcr:5000; } upstream elastic_backend { server elastic:5000; } upstream nvcr_backend { server nvcr:5000; } upstream registry_ui { server registry-ui:8080; } upstream hubcmd_ui { server hubcmd-ui:3000; } map $host $backend { hub.666.xyz dockerhub_backend; ghcr.666.xyz ghcr_backend; gcr.666.xyz gcr_backend; k8s.666.xyz k8s_backend; quay.666.xyz quay_backend; mcr.666.xyz mcr_backend; elastic.666.xyz elastic_backend; nvcr.666.xyz nvcr_backend; drpui.666.xyz registry_ui; hubcmd.666.xyz hubcmd_ui; default dockerhub_backend; } server { listen 5050 ssl; server_name _; ssl_certificate /etc/nginx/ssl/666.xyz.pem; ssl_certificate_key /etc/nginx/ssl/666.xyz.key; client_max_body_size 0; proxy_read_timeout 900; proxy_connect_timeout 60; location / { proxy_pass http://$backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Docker-Distribution-Api-Version registry/2.0; proxy_http_version 1.1; proxy_set_header Connection $connection_upgrade; proxy_set_header Upgrade $http_upgrade; chunked_transfer_encoding on; } } 这个配置的核心逻辑是：\ntext 1 2 3 所有域名都访问 5050 Nginx 根据 Host 头判断后端 upstream 所有域名共用一张 *.666.xyz 泛域名证书 最后的浏览器 Not Secure 问题 证书更新后，浏览器里仍然出现了 Not Secure 提示。\n但点开详情后可以看到：\ntext 1 Certificate is valid 证书查看器里也显示：\ntext 1 2 3 4 Issued By: Let\u0026#39;s Encrypt Validity Period: Issued On: Tuesday, June 2, 2026 Expires On: Monday, August 31, 2026 并且命令行验证线上证书也是新的：\nbash 1 2 3 4 openssl s_client \\ -connect hubcmd.666.xyz:5050 \\ -servername hubcmd.666.xyz \\ \u0026lt;/dev/null 2\u0026gt;/dev/null | openssl x509 -noout -dates -issuer -subject 所以这时问题已经不在证书签发和 Nginx 配置上。\n如果无痕模式和换浏览器都正常，说明更像是当前浏览器的站点状态缓存、HSTS 状态、旧安全状态、扩展影响或本地缓存导致。\n可以尝试的方向包括：\ntext 1 2 3 4 5 6 清理站点数据 清理 DNS 缓存 清理浏览器 SSL 状态 删除该域名的 HSTS 记录 停用相关浏览器扩展 换浏览器配置目录验证 但从服务端角度看，证书链路已经跑通了，所以我懒得搞了。 最终回到我拉 Docker 镜像的服务器，成功拉取镜像。\n最终结果 这次最终完成了几件事：\ntext 1 2 3 4 5 6 7 8 9 10 确认原证书已经过期 确认 Nginx 运行在 Docker 容器内 确认服务只开放 5050 端口 放弃 HTTP-01 验证 改用 Cloudflare DNS Challenge 使用 acme.sh 申请 Let\u0026#39;s Encrypt 泛域名证书 把新证书安装回原 Nginx 证书路径 配置续期后自动重启 registry-nginx 确认线上服务已经返回新证书 确认 crontab 自动续期任务存在 最终证书链路如下：\n[text] 显示已折叠代码（17 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Cloudflare DNS ↓ acme.sh DNS Challenge ↓ Let\u0026#39;s Encrypt 泛域名证书 ↓ ~/registry-proxy/nginx/ssl/666.xyz.pem ~/registry-proxy/nginx/ssl/666.xyz.key ↓ Docker volume ↓ /etc/nginx/ssl/666.xyz.pem /etc/nginx/ssl/666.xyz.key ↓ registry-nginx ↓ https://*.666.xyz:5050 可复用命令汇总 切换默认 CA 到 Let\u0026rsquo;s Encrypt：\nbash 1 acme.sh --set-default-ca --server letsencrypt 申请泛域名证书：\nbash 1 2 3 4 acme.sh --issue \\ --dns dns_cf \\ -d 666.xyz \\ -d \u0026#39;*.666.xyz\u0026#39; 安装证书到 Nginx 使用目录：\nbash 1 2 3 4 5 6 acme.sh --install-cert \\ -d 666.xyz \\ --ecc \\ --key-file ~/registry-proxy/nginx/ssl/666.xyz.key \\ --fullchain-file ~/registry-proxy/nginx/ssl/666.xyz.pem \\ --reloadcmd \u0026#34;docker restart registry-nginx\u0026#34; 检查本地证书有效期：\nbash 1 2 3 4 openssl x509 \\ -in ~/registry-proxy/nginx/ssl/666.xyz.pem \\ -noout \\ -dates 检查线上服务实际返回证书：\nbash 1 2 3 4 openssl s_client \\ -connect hubcmd.666.xyz:5050 \\ -servername hubcmd.666.xyz \\ \u0026lt;/dev/null 2\u0026gt;/dev/null | openssl x509 -noout -dates -issuer -subject 检查 Nginx 证书配置：\nbash 1 grep ssl_certificate -R ~/registry-proxy/nginx/conf.d 检查 acme.sh 自动续期任务：\nbash 1 crontab -l 手动重启 Nginx 容器：\nbash 1 docker restart registry-nginx 如果 TLS 已经恢复，但向 Harbor 推送镜像仍报 unauthorized，可以继续看 Harbor push unauthorized 的代理层鉴权排查。\n相关资料 Let\u0026rsquo;s Encrypt：Challenge Types acme.sh：DNS API Cloudflare：创建 API Token ","permalink":"/posts/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1-docker-registry-proxy-%E7%9A%84-https-%E8%AF%81%E4%B9%A6%E7%BB%AD%E6%9C%9F%E6%8E%92%E6%9F%A5/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e\u003cstrong\u003e先说结论：\u003c/strong\u003e 服务没有开放 80/443 端口时，不能直接走依赖 80 端口的 HTTP-01 验证。这次我改用 acme.sh + Cloudflare DNS Challenge 申请泛域名证书，再通过 \u003ccode\u003e--install-cert\u003c/code\u003e 写回 Nginx 挂载目录，并在续期后自动重启容器。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e我有一套自建的 Docker Registry Proxy 服务，可看这篇博客：\u003ca href=\"/posts/docker%E8%87%AA%E5%BB%BA%E9%95%9C%E5%83%8F%E5%8A%A0%E9%80%9F/\"\u003edocker 自建镜像加速\u003c/a\u003e，用来代理 Docker Hub、GHCR、Quay、GCR、K8S、MCR、NVCR 等镜像源。\u003c/p\u003e","title":"记录一次 Docker Registry Proxy 的 HTTPS 证书续期排查"},{"content":" 看到个项目，code graph，顾名思义应该是把项目代码给构建成图谱，拖延到今天来实测一下效果\n仓库地址：GitHub - colbymchenry/codegraph: Pre-indexed code knowledge graph for Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, and Hermes Agent — fewer tokens, fewer tool calls, 100% local · GitHub\n安装 code graph mac 安装命令\nzsh 1 curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh 安装 MCP 配置 初始化项目 进入一个最近在开发的目录，开始索引\n1 2 cd your-project codegraph init -i 接下来他会扫描仓库的文件并且在本地新建数据库 大概流程是这样的：\n显示已折叠代码（21 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 你的代码目录 │ ▼ Tree-sitter 解析源码 │ ▼ 抽取符号(Symbol) 函数、类、方法、变量、路由 │ ▼ 分析关系(Edge) 调用关系 继承关系 导入关系 引用关系 │ ▼ 写入 SQLite │ ▼ 生成 .codegraph/ 官方文档里提到，CodeGraph 会把符号、调用图、文件结构等信息存到 SQLite（FTS5）数据库中，并把项目数据放在 .codegraph/ 目录下。\ncodegraph 以 mcp 形式接入，我这里先拿 Claude code 试试\n简单尝试目前还看不出很明显的差距，本项目有一些本地记忆处于Claude 配置里面 所以也许会有干扰，但是按照他的设计理论来讲是有帮助的，我也看了一些网友的使用案例所以相信这一点。\n我将接着使用这个 MCP 进行开发\n","permalink":"/posts/code-graph%E6%B5%85%E5%B0%9D/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e看到个项目，code graph，顾名思义应该是把项目代码给构建成图谱，拖延到今天来实测一下效果\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e仓库地址：\u003ca href=\"https://github.com/colbymchenry/codegraph\"\u003eGitHub - colbymchenry/codegraph: Pre-indexed code knowledge graph for Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, and Hermes Agent — fewer tokens, fewer tool calls, 100% local · GitHub\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"安装-code-graph\"\u003e安装 code graph\u003c/h2\u003e\n\u003cp\u003emac 安装命令\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \u003cspan class=\"code-lang\"\u003ezsh\u003c/span\u003e\n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-zsh\" data-lang=\"zsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecurl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh \u003cspan class=\"p\"\u003e|\u003c/span\u003e sh\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/06/9a72a059bebe62222b1b0e1311d9b527.png\"\u003e\n        \u003cimg alt=\"codegraph 安装命令执行结果\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/06/9a72a059bebe62222b1b0e1311d9b527.png\" width=\"300\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003ch2 id=\"安装-mcp-配置\"\u003e安装 MCP 配置\u003c/h2\u003e\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/06/55540450efc31d2700dc723f243ff3bd.png\"\u003e\n        \u003cimg alt=\"Claude Code 的 CodeGraph MCP 配置\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/06/55540450efc31d2700dc723f243ff3bd.png\" width=\"300\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003ch2 id=\"初始化项目\"\u003e初始化项目\u003c/h2\u003e\n\u003cp\u003e进入一个最近在开发的目录，开始索引\u003c/p\u003e","title":"code graph浅尝"},{"content":"不知不觉半年未曾在夜半而想起写小札了。这个分区诞生那天起就注定了它惨淡的更新热度。因为我无法做到高强度持续的情感饱满，小札对我而言就是满而溢出的产物，只不过容器也会变化内容也会变化，所以注定是无规律无迹可寻，不过半年确实过长，无非是太过于偷懒了。\n近来对偷懒更加觉得焦虑，今天刷到一篇文章，作者提到自己三十多岁恨不得和二十多岁十多岁的人换命，大学生也许都还认为时间像一块丰腴的肥皂，一时半会用不完。\n非也，我感知到肥皂变小已经不是一天两天，说不清究竟是什么在飞快的急速的似乎异于常人的把我推动，推动到快速接近未来，是即将成家的责任，父母日益年迈的身体，还是这异常发展迅速的时代。\n我就是00后，大学毕业后单从求职环境，到就职后的几年经历的业内大革新，大模型在三个月进行一次迭代，项目在一周进行一次更新，ai让一个人能完成十个人的活也让一个人承担上了十个人的压力。越是接触ai越感到焦虑。从chatgpt正式问世第一次在那个窗口聊天到如今codex打开几个threads日消耗几亿个token，我有时候忙到分不清是我在驾驭ai还是ai在驾驭我，我只能选择yes or no。\n对于未接触到这一切的人们而言，只需要一点点🤏行动力就能戳破这个薄膜，人们将极速的从0到达60的水平，而我焦虑的是这件事情变得没有任何门槛了，一句话的描述能抵过了我过去一个月的学习产出的成果，我不知道如何保护自己，我开始慌，我一遍疯狂加班学习最新的技术，大量的跟风，却回头发现自己无差别接收最新信息的时候付出了很大的代价去过滤初级信息。而经过一轮一轮消化的产物你可以说它不新，被加工，但你永远无法否认被一环一环传播也是一种过滤的过程，有一句玩笑话，当你在这个ai时代，只要你学的够慢，你就会发现不用学了。我们在一年前针对语音交互智能系统辛苦调优在工程上优化ASR到TTS的推理速度，为了在用户说完话后系统能在最短的时间发出回答的声音做出的努力在某一个月一个realtime的api更新碾压到一文不值。快速调头是个人和小团队在这个时代唯一且最强大的对抗武器，传统笨重的打磨思维不再有效，这会让人持续紧绷持续紧绷，随时随地vibecoding，手机上疯狂approve yes or no，这一切热度都会像泡沫一样疯狂膨胀。\n吹起泡沫的是风，我不能随着泡沫增长，我得抓住这个风，看着风在眼前消失的滋味非常难受，但是越着急越抓不住，冰瀑三尺非一日之寒，我感知到肥皂变小泡沫变大，在这个泡沫炸开让所有人都感知到之后，下一个泡沫我需要做好准备，无论是看书，健身，学习，尝试。\n这确实是一个黄金时代，前人不断惊叹青春的逝去感慨年轻人对时代的馈赠一无所知，我自感无法和同龄人一样再心安理得把这些挥霍在无意义的琐事和廉价的娱乐中，只要肯抬头看一眼，时间的收网并非遥不可及，只是远远的看一眼便能感受身上已被勒的紧迫窒息。\n我试图找个高大上的句子来引用结尾却找不到丝毫存活，哪怕让豆包看看它也束手无策，那我就顺着新想到的思路接着讲讲。首先是我深知焦虑过度无益且一口吃不成胖子，但是我始终没能践行自己的认知，差点火候。前段时间刷到一位作者的文章讨论人需要掌握火候，讲得很好，串通中庸无为如如三家传统宗教的智慧，无非就是做菜放盐的适量，这个适量，其实做了几次就知道了，人的学习率很高，不需要回归很多次就能学会。那么我觉得写此文也是在此提醒我一下，要注意火候。坚持会是一个好的习惯，但是硬抗不是长久的方案。\n此时此刻我正在完善我的简易skills hub，希望一切能如我所愿，不如也没事，我有的是手段。\n放个梗图给以后笑一笑 ","permalink":"/posts/%E5%B9%B4%E4%B8%AD%E9%9A%8F%E6%83%B3/","summary":"\u003cp\u003e不知不觉半年未曾在夜半而想起写小札了。这个分区诞生那天起就注定了它惨淡的更新热度。因为我无法做到高强度持续的情感饱满，小札对我而言就是满而溢出的产物，只不过容器也会变化内容也会变化，所以注定是无规律无迹可寻，不过半年确实过长，无非是太过于偷懒了。\u003c/p\u003e","title":"年中随想"},{"content":"今天写 mitmproxy 那篇的时候，顺手试了一下一个很有意思的 skill，分享一下：\nhelloianneo/ian-xiaohei-illustrations，好多天前关注的，忘了是在哪看到的，发现最近热度蛮高。\n截止写本文，作者在群里分享，它在这周 GitHub 新仓库搜索结果里排第二。\n这个仓库名字很直白，就是 Ian（作者） 风格的小黑怪诞正文配图生成 Skill。\n本质上它不是一个很复杂的项目。甚至可以说，它简单到让我有点恍然大悟：哦，我之前每次都开着语音和叫 image2 生图其实可以做的优雅一些。\n而是把一套稳定的审美、角色设定、构图禁忌、prompt 模板和 QA 规则，塞进一个 SKILL.md 里，然后让 Codex 在需要配图的时候按这个流程走。\n可以。\n在 mitmproxy 文章里试了一下 最近那篇文章是这个：\nmitmproxy 实战以及抓包Claude Code | 安落滢 Blog - 技术分享与生活记录\n里面有一段是在讲证书信任和中间人代理。这个概念如果只用文字写，其实也能讲清楚，但正文读起来会有点干。\n于是我就让这个 skill 给 mitmproxy 生成了配图。\n一半是「浏览器信任 mitmproxy 之后，中间站可以看见 HTTPS 流量」： 另一半是「没有证书不让拆，有证书才能拆开看再封回去」：\n效果其实还挺好。\n画面不是 PPT 那种规规矩矩的流程图。它能把这种偏抽象的链路，变成一个比较容易记住的画面。\n这对博客很有用。\n但我想继续改 目前这个 skill 最大的特点是「小黑」。\n小黑蛮简单的，黑色实心，白点眼，细腿，没什么表情，认真做一些很荒诞但成立的事。这个设定很合适，也很容易让模型稳定复现。\n但是我后面应该不会一直用小黑。\n倒不是它不好，而是我还是想慢慢做一点更有我自己味道的角色。博客写久了之后，配图其实也会变成一种个人标识。现在用小黑是借别人的视觉语言，后面如果能变成自己的角色，那就更像我的博客了。\n可能不是很复杂的 IP。\n也许只是一个更固定的主角、几个常见表情、一套动作库、一点颜色习惯。\n另一个问题是一句话能力 现在这个 skill 已经能工作，但我觉得还没有到「我一句话，它就能把完成的图片做出来」的程度。\n我想让他 和之前 卡兹克的 writer skills 结合一下，先让 agent 读我的文章，然后根据我留下的指示和上下文，生成配图。\n比如我说：\n给这段 mitmproxy 证书信任做一张配图。\n理想状态不是直接把这句话扔给 image tool。\n理想状态应该是：\n先读正文，找到这个段落到底在讲什么 抽出核心关系，比如「浏览器」「mitmproxy」「目标网站」「证书信任」 判断适合做成什么结构，是前后对比，还是中间站，还是拆包再封包 生成一组中文标注词，别太多，最好能像手写批注 再把这些东西整理成一条稳定 prompt 生图之后自己检查：是不是太像 PPT？是不是中文太多？小黑是不是只是在旁边罚站？ 最后保存到固定目录，回填到文章里 也就是说，我想要的不是「一句话生图」。\n我想要的是「一句话触发一个小流水线」。\n这才是 skill 真正有意思的地方。\n后面想补的东西 我现在想到几个方向，先记一下。\n角色这一层，要从小黑慢慢换成我自己的角色设定。先不要复杂，能稳定出现、能做动作、能表达状态就行。\n理解这一层，要让 skill 更会拆一句话。它不能只看用户的一句话，还应该结合当前文章、段落位置、前后上下文，判断这张图到底该解释什么。\n构图这一层，要保留一些可复用的结构，但不能每次都像套模板。比如中间站、前后对比、断点、输入输出闭环，这些结构很好用，但每张图还是得从当前文章里重新长出来。\n资产这一层，也要更自动一点。比如按文章 slug 建目录：\ntext 1 2 assets/\u0026lt;article-slug\u0026gt;-illustrations/ static/illustrations/\u0026lt;article-slug\u0026gt;/ 然后自动命名：\ntext 1 2 01-cert-trust.png 02-open-and-reseal.png 这部分很琐碎，但很重要。因为博客配图最烦的往往不是画，而是画完之后放哪里、怎么命名、怎么引用、下次还能不能找回来。\n这个仓库这几天的热度给我的启发 有时候，聚焦一件事情缩小边界做的够好也能有很大的热度。\n先让它跑起来，然后慢慢修。\n","permalink":"/posts/%E5%B0%8F%E9%BB%91%E9%85%8D%E5%9B%BE-skill-%E8%BF%99%E4%B8%9C%E8%A5%BF%E6%9C%89%E7%82%B9%E6%84%8F%E6%80%9D/","summary":"\u003cp\u003e今天写 \u003ccode\u003emitmproxy\u003c/code\u003e 那篇的时候，顺手试了一下一个很有意思的 skill，分享一下：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/helloianneo/ian-xiaohei-illustrations\"\u003ehelloianneo/ian-xiaohei-illustrations\u003c/a\u003e，好多天前关注的，忘了是在哪看到的，发现最近热度蛮高。\u003c/p\u003e\n\u003cp\u003e截止写本文，作者在群里分享，它在这周 GitHub 新仓库搜索结果里排第二。\u003c/p\u003e\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/05/ee3c9b8514f091ce6d8a42d6e63c2df4.png\"\u003e\n        \u003cimg alt=\"GitHub 热门仓库中的小黑配图 Skill\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/05/ee3c9b8514f091ce6d8a42d6e63c2df4.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e这个仓库名字很直白，就是 Ian（作者） 风格的小黑怪诞正文配图生成 Skill。\u003c/p\u003e\n\u003cp\u003e本质上它不是一个很复杂的项目。甚至可以说，它简单到让我有点恍然大悟：哦，我之前每次都开着语音和叫 image2 生图其实可以做的优雅一些。\u003c/p\u003e","title":"小黑配图 Skill 这东西有点意思"},{"content":" 这个工具是 boss 推荐的，尽管他想到的使用场景是大炮打蚊子（指 在 windows wsl 安装 mitmproxy 来抓宿主机 web 页面的 restful 接口，而实际 console 的 network 足矣）但是也不去纠正了，我来试试这个工具的真实能力。\nGitHub - mitmproxy/mitmproxy: An interactive TLS-capable intercepting HTTP proxy for penetration testers and software developers. · GitHub\n看到这个的时候心中暗喜，我以为开箱即用，配个证书就行。\n1 brew install mitmproxy 我刚开始以为他只有 cli 工具，没想到还有 web，那真的很方便了。\n结果装完就遇到 Apple could not verify mitmproxy is free of malware that may harm your Mac or compromise your privacy. 啊？第一次在 homebrew 装完的东西碰到这个问题。我一时怀疑装错了。\n问了下 G 老师，说 app 身上还带着 quarantine 标记，首次运行的时候拦我一下 xattr -dr com.apple.quarantine /opt/homebrew/Caskroom/mitmproxy/*/mitmproxy.app\n清理掉就可以了，确认一下版本。\n1 2 3 mitmproxy --version mitmweb --version mitmdump --version 那么其次，对于抓 HTTPS 是需要安装他的证书的。用过 wireshark 或者用过类似软件的应该都知道。\n第一次启动之后，它会在你的 home 目录里生成一套文件。\n里面有一个 ~/.mitmproxy/mitmproxy-ca-cert.pem\n把它导入当前用户的 login.keychain-db，不去碰系统级 keychain。原因很简单，够用。\n1 2 3 security add-trusted-cert -d -r trustRoot \\ -k ~/Library/Keychains/login.keychain-db \\ ~/.mitmproxy/mitmproxy-ca-cert.pem 这一步做完之后，本机走 mitmproxy 的 HTTPS 才算通。\n实战 我这次不打算全量代理 Wi-Fi 或者有线流量，而是只让目标 app 走 mitmproxy。这样风险最小，也不会把本机网络环境搅乱。\n另外我本地已经有 clash 负责代理出口流量，所以更合理的链路应该是：\nClaude Code -\u0026gt; mitmproxy -\u0026gt; clash -\u0026gt; Internet\n1. 先把链路摆正 如果 clash 暴露的是 HTTP 代理端口，比如常见的 127.0.0.1:7897，那么可以直接让 mitmproxy 用 upstream mode 挂到 clash 前面。\nbash 1 2 3 4 5 6 mitmweb \\ --mode upstream:http://127.0.0.1:7897 \\ --listen-host 127.0.0.1 \\ --listen-port 8080 \\ --web-host 127.0.0.1 \\ --web-port 8082 这样分工就很清楚了，mitmproxy 负责看包，clash 继续负责出站。启动后会自动打开 web 页面。\n2. 只让 Claude Code 走 mitmproxy claude 可执行文件里本身就能看到 HTTP_PROXY、HTTPS_PROXY 和 NODE_EXTRA_CA_CERTS 这些变量。\nbash 1 2 3 4 HTTP_PROXY=http://127.0.0.1:8080 \\ HTTPS_PROXY=http://127.0.0.1:8080 \\ NODE_EXTRA_CA_CERTS=$HOME/.mitmproxy/mitmproxy-ca-cert.pem \\ claude 这里 NODE_EXTRA_CA_CERTS 是 G 老师让我加的，我保持怀疑。他的解释是：即使系统已经信任过证书，Node 系工具也不一定会老老实实吃这套 CA。\n3. 在 mitmweb 里看请求 接下来就是打开 http://127.0.0.1:8082/，看流量有没有进来。\n有没有新请求持续出现，确认 Claude Code 真的走到了 mitmproxy 域名、路径、方法是什么 请求头里有没有值得观察的字段 返回状态码和耗时是不是正常 可以看到抓到的包里面，我原先 claude code 内部配置的 走向 cc-switch 的配置生效，他请求的是 127.0.0.1:15721 的地址，HTTPS_PROXY 也生效了，所以我们的 mitmproxy 抓到了包，这一句 hello 的请求体和 response 就看的一清二楚了\n同样在 mitmproxy 的 flows 里面 看到了很多条请求，都可以逐步分析 claude code 在干什么。\n","permalink":"/posts/mitmproxy-%E5%AE%9E%E6%88%98%E4%BB%A5%E5%8F%8A%E6%8A%93%E5%8C%85claude-code/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e这个工具是 boss 推荐的，尽管他想到的使用场景是大炮打蚊子（指 在 windows wsl 安装 mitmproxy 来抓宿主机 web 页面的 restful 接口，而实际 console 的 network 足矣）但是也不去纠正了，我来试试这个工具的真实能力。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/05/d79b074ccbc5b14ff4fe946d2b962d9e.png\"\u003e\n        \u003cimg alt=\"mitmproxy 项目界面\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/05/d79b074ccbc5b14ff4fe946d2b962d9e.png\" width=\"300\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/mitmproxy/mitmproxy\"\u003eGitHub - mitmproxy/mitmproxy: An interactive TLS-capable intercepting HTTP proxy for penetration testers and software developers. · GitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e看到这个的时候心中暗喜，我以为开箱即用，配个证书就行。\u003c/p\u003e","title":"mitmproxy 实战以及抓包Claude Code"},{"content":" agent 发展到现在，已经有很多成熟的方案了，但是为了更高的设计一致性和项目掌握度，需要从 0-1 设计一套。期间会借鉴参考很多开源项目。项目全程遵循 KISS 原则。\n概念和技术栈选择 查了一下 agent 的概念其实比较久远，很早以前的经典理论我们不提，本文只针对目前的 生成式 AI 所代表的 agent。\n对于 agent 的执行机制，你可能听说过 ReAct （reasoning and acting）ReAct 来自 2022 年论文《ReAct: Synergizing Reasoning and Acting in Language Models》1，概念如下\n可以说这是 agent 最基本和常见的执行框架，属于评估优化式的架构，也就是通过迭代反馈不断改进输出来逼近目标。\n而在两年前还有一些 workflow 产品比如扣子、dify 等，也常被误称为 agent/智能体。但其实他们还是链式处理任务的工作流为主。\n我定义智能体的一个核心边界就是他能自己决定使用什么工具以及怎么使用。\n截止到开始本项目之前，市面上已经有了很多类似产品，各有特色，但是最底层的原理还是一样的。现在需要在一些业务场景用到或者开发一个平台来提供 agent 服务，所以需要从自构建开始保证设计的一致性和对系统的了解性。\n在技术栈的选择上，选择全力投入 python，如果熟悉 python 语法，借助 python 的生态。开发agent 的 MVP 将降低很多心智负担，但是显而易见的缺点是对底层控制降低（比如一些隐形的 runtime error 和性能消耗），但是个人判断，结合我的技术栈掌握和现处公司项目的情况，我决定第一个发行版本将全量使用 python 构建，后续选择使用 Rust 语言作为 热点模块 的替换直至开发团队掌握 rust 技术栈后进行全量重构。（如果这个项目胎死腹中也无需谈什么未来。）\n架构设计 基于 ReAct 架构设计的基础思路 还是比较容易的，这里参考王二老师的文章，列一些功能和设计思路。2\n基本流程：\n用户输入 -\u0026gt; 系统提示词组装 -\u0026gt; LLM 思考 -\u0026gt; 判断是否需要工具 -\u0026gt; 执行工具 -\u0026gt; 工具结果写回上下文 -\u0026gt; LLM 继续思考 -\u0026gt; 直到不再调用工具或预算耗尽\n由 用户输入触发这个单循环。主干就是 思考-\u0026gt;执行-\u0026gt;观察-\u0026gt;思考\u0026hellip;\n和原本的对话交互的区别就是加上一个循环。 循环结束 要么是不需要调用工具了，模型会给到结果，要么是 出错了\n消息协议 很好理解，只要你调用过 openai 范式的 chatcompletions 接口就会知道消息列表里面有四个角色\nsystem / user / assistant / tool\n系统提示词分层设计 这样可以动态组合针对不同的场景和需求 比如可以分成：\n身份层：你是谁，比如编程 Agent 工具层：有哪些工具，每个工具什么时候用 性格层：语气、风格 工作模式：普通 Agent / Planner / Worker / Reviewer 审批策略：自动执行、危险操作确认、全部确认 项目上下文：AGENTS.md / CLAUDE.md / 项目规则 记忆和上下文管理规则 参考 claude code，至少存在以下几层，从泄露源码来看有五层了3\n显示已折叠代码（26 行） 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 ┌──────────────────────────────┐ │ Layer 1: Active Context │ │ 当前工作区 / 当前任务 │ │ 当前文件、工具结果、计划 │ └──────────────────────────────┘ ↓ ┌──────────────────────────────┐ │ Layer 2: Session Memory │ │ 会话级记忆 │ │ memory.md / summaries │ │ 历史决策、阶段状态 │ └──────────────────────────────┘ ↓ ┌──────────────────────────────┐ │ Layer 3: Project Knowledge │ │ 项目长期知识 │ │ CLAUDE.md / README │ │ coding conventions │ └──────────────────────────────┘ ↓ ┌──────────────────────────────┐ │ Layer 4: External Retrieval │ │ 外部检索层 │ │ MCP / tools / search │ │ git / docs / APIs │ └──────────────────────────────┘ 这里如果用操作系统打比方会很形象，这也是为什么现在说做一个 agent 越来越像做一个操作系统，这里的设计就很像操作系统里的内存分页 + checkpoint + snapshot 了。\ncontext 管理将是 本项目里面可预见最难 最哲学的部分。\n工具系统 Fuction calling作为协议，Tool use 是我们实现的能力 每个工具就像个有注释的函数 名字 + 描述 + 参数 schema + 执行函数 模型会输出结构化调用，我们的工具层执行，然后把结果包装进 tool 的消息块返回 如果模型输出多个还要进行并行执行\nMCP 用户注册，我们加到提示词里，模型输出后去调用 其实就是做转发\nmulti agent 上下文隔离、不同的 worker 不同的对话历史、不能让 subagent 继续向下打开 subagent，完成任务返回摘要给主 agent。\n记忆模块 短期：对话窗口和压缩在窗口前面的内容\n长期：用户偏好、项目事实等，存本地 json 或者嵌入数据库 支撑混合检索能力\n项目规则注入 这个了解一下 CLAUDE.md / AGENTS.md ，为了方便迁移和定制做的。\npi——agent runtime 设计参考 刚刚提到的设计是完整的一套，其中值得推敲的细节会非常多。为了让本项目能成为基座型项目，还需要在缩减一下核心设计，这里参考了 pi 这个项目的设计4 ，这个项目最关键的是双层循环机制以及一些抽象解耦，会更适合 core 的设计，而刚刚的全套 ReAct 会更偏向于能力清单。\n单独列几个核心的点\nLoop 不是简单的 ReAct 循环，而是 双层循环，外层 followup+内层 turn/tool/steer，这样说比较抽象，但是对于现在使用惯了 claude code 和 codex 的朋友可能不陌生接下来的功能描述\n双循环和插嘴 双循环解决的问题其实就是 中途追加对话和队列对话。 比如 agent 正在思考分析代码。此时我补一句：别碰代码，先读。 会在本轮内循环 assistant turn 或者工具结束后追加上下文，重复内循环 follow up 就是 queue message，也就是本循环 agent 大循环结束后追加任务\n这个设计思路对于我们系统来讲是需要知道的，这和完全结束追加一条消息本身的含义不同。\nAgent Message 和 LLM Message 分离 Agent 平台可以有丰富状态，但不会污染模型协议。以后换 OpenAI、Anthropic、Gemini、本地模型，只改 adapter，不改 core。\ntool + before/after hook + execution mode 对于工具本身来讲，它只描述\n1 2 3 name schema execute() 而加上这个 hook 才实现：权限检查、审批、拦截、危险阻断、审计等\n而 execution mode 是分并行和串行，前面有提到，一些工具调用是可以并行的，比如读文件 但是执行 shell、写文件、还是依赖前后顺序的，就需要串行执行\ncore 不强绑定 memory，而是抽象出来 短期记忆就是 session messages。长期记忆、RAG、用户偏好、项目事实，都不要成为 core 的硬依赖，而是 runtime/plugin 层能力。\n这样依赖 agent 可以使用不同的 memory backend，可以扩展使用 json / postgres / vector DB\ncore 偏单 Agent runtime，multi-agent 放上层编排 单 Agent core 应该只管一件事：\n给定 context + tools + model，稳定跑完一个 Agent loop\n这样设计 core 不膨胀，也不会把循环搞复杂\n我觉得很核心的 事件系统 Agent 运行不是一次函数调用，它是一个过程：\n1 2 3 4 5 6 7 8 9 10 agent_start turn_start message_start message_update message_end tool_execution_start tool_execution_update tool_execution_end turn_end agent_end 这些事件就是 runtime 的事实来源。\nUI 可以用它做流式展示；持久化层可以用它写 event log；审计系统可以记录谁调用了什么工具；计费系统可以统计 token 和工具耗时；扩展系统可以监听某个事件触发动作。\n我们做的 PAAS 甚至需要 event log 来 恢复 agent session。\n设计第一版本 TODO 和结构 这个就放到下一期讲\n相关参考文档/协议/项目 相关技术栈的推荐：LangGraph，虽然在我开发本 agent 产品里应该不会用到它，实在是对 Langchain 有足够的心理阴影。\n新兴协议与标准\n模型上下文协议 (MCP, Model Context Protocol)：旨在标准化 Agent 与外部数据源和工具的连接方式。 智能体间协议 (A2A, Agent-to-Agent)：旨在为不同框架构建的多智能体系统提供一种通用的交互协议。 [2210.03629] ReAct: Synergizing Reasoning and Acting in Language Models\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nmp.weixin.qq.com/s/R-A3iob8ORcGRGljRFqLuA\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n[2604.14228] Dive into Claude Code: The Design Space of Today\u0026rsquo;s and Future AI Agent Systems\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nGitHub - earendil-works/pi: AI agent toolkit: coding agent CLI, unified LLM API, TUI \u0026amp; web UI libraries, Slack bot, vLLM pods · GitHub\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"/posts/%E4%BB%8E-0-%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA-agent1/","summary":"\u003cblockquote\u003e\n    \u003cp\u003eagent 发展到现在，已经有很多成熟的方案了，但是为了更高的设计一致性和项目掌握度，需要从 0-1 设计一套。期间会借鉴参考很多开源项目。项目全程遵循 KISS 原则。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"概念和技术栈选择\"\u003e概念和技术栈选择\u003c/h2\u003e\n\u003cp\u003e查了一下 agent 的概念其实比较久远，很早以前的经典理论我们不提，本文只针对目前的 生成式 AI 所代表的 agent。\u003c/p\u003e\n\u003cp\u003e对于 agent 的执行机制，你可能听说过 ReAct （reasoning and acting）ReAct 来自 2022 年论文《ReAct: Synergizing Reasoning and Acting in Language Models》\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e，概念如下\u003c/p\u003e","title":"从 0 开发一个 agent（1）"},{"content":"最近豆包输入法的语音输入很火。\n我是在开放后下载下来用了一下，确实很好用，对比之前用的 type less 对我而言有点修饰过头，微信输入法整体效果还行但是略逊于豆包。我设置的快捷键是 Command + Option ，按住说话，松开就上屏，体验很顺。\n但是我现在主力打字输入法还是微信输入法。\n原因也很简单，它有多端剪切板同步。\n虽然这个同步率我感觉也就 90% 左右吧，不是每次都稳，但是有总比没有强。用久了之后就有点回不去了。手机上复制点什么，电脑这边能接着用，虽然偶尔抽风，但是整体还是方便的。\n所以问题就来了。\n我不想把主力输入法换成豆包输入法，但是我又想用豆包的语音输入。我想偷个懒。\n我想到我的流程是，平时打字继续用微信输入法，需要语音的时候，按住 Command + Option 临时切到豆包语音，松开之后再回微信输入法。\n那么理论可以全自动做这件事情。我把这个需求丢给了 macbook 上跑着的 Hermes，我坚信它以及它背后的 GPT-5.5 能做到。\n最终思路 最后 Hermes 先搓了一个后台 agent，后来又把它收成了一个顶栏 mini app：\ntext 1 doubao-voice-wetype-agent 它干的事情不是破解豆包，也不是改输入法，而是在中间当一个代理。\n更准确地说，它现在像一个很小的插件，常驻在 macOS 顶栏，没有 Dock 图标。平时显示 豆 OK，权限或者监听有问题的时候显示 豆 !，按住语音的时候会变成 豆 REC。\n这个小状态栏后来证明很重要。\n因为这种系统级快捷键代理如果没有可视状态，真的太黑盒了。你不知道是权限没拿到，还是 event tap 被系统关了，还是输入法切过去了但豆包没接住。现在点开顶栏菜单，至少能看到权限、当前输入法、最近一次事件和监听状态。\n大概流程是这样：\n[text] 显示已折叠代码（33 行） 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 当前输入法是微信输入法 | | 按下 Command + Option v agent 捕获到这个组合键 | v 检查当前输入法不是豆包 | v 切换到豆包输入法 | v 重新发送一组干净的 Command + Option keyDown | v 豆包开始语音输入 | | 我继续按住，说话 v 松开 Command + Option | v agent 捕获释放事件 | v 重新发送 Command + Option keyUp | v 豆包结束语音输入 | v 切回微信输入法 一句话就是：\n我按的还是同一组快捷键，但是中间由 agent 临时切输入法，然后把快捷键事件补发给豆包。\n这个设计听起来绕，但其实就是为了保证豆包收到完整的 keyDown -\u0026gt; keyUp。\n为什么不能只靠 macOS 切输入法 macOS 本来就能切输入法。\n但是这个场景的问题在于，豆包必须在 keyDown 发生时就已经是当前输入法。\n如果流程是：\ntext 1 2 我按下 Command + Option -\u0026gt; 系统切到豆包输入法 那对豆包来说，很可能已经错过了最关键的 keyDown。\n所以就会出现一种很烦的情况：\n看起来输入法已经切到豆包了，但是豆包语音没有被唤起。\n这不是因为豆包不能用，也不是快捷键错了，而是事件顺序不对。\n豆包是在 keyDown 之后才被激活的，它没收到完整的按下过程。\n所以必须重放事件：\ntext 1 2 3 真实 Command + Option 被 agent 捕获 agent 切到豆包 agent 再合成一组新的 Command + Option keyDown 给豆包 松开的时候同理，也要把 keyUp 给豆包补上。\n但是还不够稳 后来实际用的时候又发现一个问题。\n有时候按下 Command + Option 之后，输入法确实切到豆包了，但是豆包语音没有被唤起来。\n这个就很烦。\n大概率原因是这样：\ntext 1 2 3 4 5 系统已经开始切到豆包 -\u0026gt; 但是豆包自己的 ASR shortcut monitor 还没完全 ready -\u0026gt; agent 太快发了 Command + Option down -\u0026gt; 豆包没接住这次 keyDown -\u0026gt; 最后表现就是只切过去了，但没唤起语音 也就是说，之前的逻辑虽然保证了事件顺序，但还没有保证豆包真的准备好了。\n原来代码里是固定睡一会儿：\ntext 1 2 3 selectInput(doubao) sleep 120ms post Command + Option down 这个写法比较赌。\n机器状态好、系统切得快的时候没问题。某一瞬间慢一点，就翻车。\n所以我把源码又补了几层保护。\n现在切到豆包后不再盲等固定 120ms，而是先轮询确认当前输入法真的已经变成豆包：\ntext 1 2 3 4 selectInput(doubao) waitForInput(doubao, max 600ms) settle 260ms post Command + Option down waitForInput 做的事情也很简单，每 20ms 检查一次：\nswift 1 currentInputID() == \u0026#34;com.bytedance.inputmethod.doubaoime.pinyin\u0026#34; 确认切过去了，再额外等 260ms，给豆包一点 settle 的时间。\n这就比固定睡眠稳定很多。\n另外还加了一个 syntheticDownPosted。\n这个是为了防一种边界情况：\ntext 1 2 3 我按得很快 -\u0026gt; agent 还没成功发 synthetic Command + Option down -\u0026gt; 我就已经松手了 如果这时候照旧发 synthetic up，就可能把豆包的状态机搞乱。\n所以现在只有确认成功发过 synthetic down，松开时才补 synthetic up。没发过 down，就不乱发 up。\n恢复微信输入法也稍微稳了一点。\n松开后的流程变成：\ntext 1 2 3 post Command + Option up wait 150ms selectAndSettleInput(wetype, 60ms) 总之就是别急。\n这种跟输入法、系统事件、第三方快捷键监听混在一起的东西，最怕的不是逻辑不对，而是太快。你以为已经切过去了，实际上对面还没准备好。\n后来又补了一个顶栏状态 还有一个后来才意识到的问题。\n稳定性之外，可观测性也很重要。\n一开始它只是一个 LaunchAgent 拉起来的后台进程。理论上能跑，但是一旦它在 Obsidian 里唤不起来，我完全不知道发生了什么。\n权限没了？\nevent tap 没创建成功？\n还是豆包没接住合成的 keyDown？\n不知道。\n这种感觉很烦，因为你面对的不是一个能报错的普通 App，而是一堆 macOS 权限、输入法、事件监听混在一起的东西。\n所以最后又给它加了一个极小的顶栏 mini app。\n现在顶栏会显示几个状态：\ntext 1 2 3 4 豆 OK -\u0026gt; 权限和监听都正常 豆 ! -\u0026gt; 权限不足、监听失败，或者 event tap 异常 豆 REC -\u0026gt; 正在托管 Cmd+Option，理论上豆包正在听 豆 ... -\u0026gt; 启动或者切换中 点开之后还能看到：\ntext 1 2 3 4 5 辅助功能权限 输入监控权限 当前输入法 event tap 是否启用 最近一次触发事件 它还会写一个状态文件：\nbash 1 cat ~/.hermes/hermes-agent/doubao-voice-wetype-status.json 现在跑通后的状态大概是这样：\njson 1 2 3 4 5 6 7 8 { \u0026#34;accessibilityOK\u0026#34;: true, \u0026#34;inputMonitoringOK\u0026#34;: true, \u0026#34;eventTapReady\u0026#34;: true, \u0026#34;currentInputName\u0026#34;: \u0026#34;微信输入法\u0026#34;, \u0026#34;mode\u0026#34;: \u0026#34;监听中\u0026#34;, \u0026#34;lastEvent\u0026#34;: \u0026#34;已恢复微信输入法\u0026#34; } 这个体验就好多了。\n至少它坏的时候，我不用再靠玄学猜。\n切输入法：TIS API macOS 输入法不是靠菜单栏上的名字切换的，而是靠输入源 ID。\n这次用到的两个 ID 是：\ntext 1 2 3 4 5 微信输入法： com.tencent.inputmethod.wetype.pinyin 豆包输入法： com.bytedance.inputmethod.doubaoime.pinyin 这些 ID 可以从输入法 App 的 Info.plist 里找到：\ntext 1 2 /Library/Input Methods/WeType.app/Contents/Info.plist /Library/Input Methods/DoubaoIme.app/Contents/Info.plist 里面会有类似这样的字段：\ntext 1 2 3 ComponentInputModeDict tsInputModeListKey TISInputSourceID Hermes 写了一个 Swift 小工具：\ntext 1 ~/.local/bin/im-switch 用的是 macOS Carbon / TIS API。\n它支持查看当前输入法：\nbash 1 ~/.local/bin/im-switch --current 切到微信输入法：\nbash 1 ~/.local/bin/im-switch com.tencent.inputmethod.wetype.pinyin 切到豆包输入法：\nbash 1 ~/.local/bin/im-switch com.bytedance.inputmethod.doubaoime.pinyin 这个比 UI 自动化靠谱多了。\n不依赖菜单栏，不用模拟鼠标点击，也不会抢焦点。\n监听和补发键盘事件：CGEvent 最开始还试过 AppleScript：\napplescript 1 2 3 4 tell application \u0026#34;System Events\u0026#34; key down option key up option end tell 但是这玩意儿不太稳，容易卡在权限弹窗或者 System Events 上。\n最后换成 Swift + Quartz，也就是 CGEvent。\nagent 创建一个 event tap，监听修饰键变化：\nswift 1 2 3 4 5 6 CGEvent.tapCreate( tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: flagsChanged ) 它监听的不是普通字符键，而是这些修饰键状态：\ntext 1 2 3 4 5 Command Option Control Shift Fn 当 flags 变成 Command + Option，就认为我开始按住语音快捷键。\n当 flags 从 Command + Option 变成不是这个组合，就认为我松开了。\n然后再用 CGEvent 合成按下和松开的事件：\nswift 1 2 CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true) event.post(tap: .cghidEventTap) 这样豆包收到的是一组真实系统层面的键盘事件，而不是 AppleScript 那种隔了一层的东西。\n防止 agent 自己触发自己 这里还有个很容易翻车的点。\nagent 自己会合成 Command + Option 事件。\n如果它监听到自己发出去的事件，然后又处理一遍，那就套娃了：\ntext 1 2 3 4 5 6 我按下 Command + Option -\u0026gt; agent 捕获 -\u0026gt; agent 合成 Command + Option -\u0026gt; agent 又捕获自己合成的 Command + Option -\u0026gt; 再触发一次 -\u0026gt; 状态乱套 所以合成事件时会打一个 marker：\nswift 1 event.setIntegerValueField(.eventSourceUserData, value: marker) 监听时如果看到这个 marker，就知道这是 agent 自己发出去的事件，直接跳过。\n这个还挺关键的。\n不然这种键盘事件代理很容易变成自己打自己。\n当前已经是豆包时，不接管 还有一个坑。\n如果当前输入法已经是豆包输入法，那豆包本来就能收到我真实按下的 Command + Option。\n这时候 agent 就不应该再插手。\n否则会多发一组 down/up，豆包自己的状态机可能就乱了。\n所以最终规则是：\nswift 1 2 3 4 5 6 7 8 9 10 let cur = currentInputID() if cur == doubaoID { // 当前已经是豆包输入法 // 不接管，让豆包自己处理原始快捷键 passThrough() } else { // 当前不是豆包输入法 // agent 接管：切豆包，重放快捷键 } 这条规则非常重要。\n只有从微信输入法或者其他输入法进入豆包语音时，agent 才介入。\n如果已经在豆包输入法，就完全放行。\n最终状态机 整理一下，最终状态机大概是这样：\n[text] 显示已折叠代码（33 行） 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 Idle | | 用户按下 Command + Option v Check current input method | |-- 当前是豆包 --\u0026gt; PassThrough | |-- 当前不是豆包 | v ManagingHold | | 切换到豆包 | 确认当前输入法已经是豆包 | 额外 settle 一下 | 合成 Command + Option keyDown | v WaitingForRelease | | 用户松开 Command + Option v 如果已经发过 synthetic down | v 合成 Command + Option keyUp | v 等一下，再切回微信输入法并 settle | v Idle 再写得土一点：\ntext 1 2 3 4 5 6 7 8 9 10 按下时： 切豆包 确认豆包已经切过去 稍微等它 ready 发送 Command + Option down 松开时： 如果前面真的发过 down，就发送 Command + Option up 等一下 切回微信并确认稳定 可以，清晰很多。\n文件备忘 我把脱敏后的源码也整理成了一个公开仓库：Coco422/doubao-voice-wetype-agent。\n最后留下来的文件主要是这些：\ntext 1 2 3 4 5 6 7 8 9 10 11 ~/.local/bin/doubao-voice-wetype-agent ~/.hermes/scripts/doubao-voice-wetype-agent.swift ~/.hermes/scripts/doubao-voice-wetype-core.swift ~/.hermes/scripts/doubao-voice-wetype-app.swift ~/.hermes/scripts/doubao-voice-wetype-events.swift ~/.local/bin/im-switch ~/.hermes/scripts/im-switch.swift ~/Library/LaunchAgents/com.ray.doubao-voice-wetype-agent.plist ~/.hermes/hermes-agent/doubao-voice-wetype-status.json doubao-voice-wetype-agent 是顶栏 mini app 加监听器。\nim-switch 是输入法切换工具。\nLaunchAgent 用来登录后自动启动：\ntext 1 ~/Library/LaunchAgents/com.ray.doubao-voice-wetype-agent.plist 这样不用每次手动开。\n权限问题 这个方案需要 macOS 给权限。\n路径是：\ntext 1 系统设置 -\u0026gt; 隐私与安全性 需要给这个二进制授权：\ntext 1 /Users/ray/.local/bin/doubao-voice-wetype-agent 通常需要：\ntext 1 2 辅助功能 / Accessibility 输入监控 / Input Monitoring 如果没给权限，日志里会看到：\ntext 1 failed to create event tap; grant Accessibility/Input Monitoring permission 这个不是代码逻辑错，是 macOS 不让这个进程监听键盘事件。\n新版会更直观一点，权限不对的时候顶栏直接显示 豆 !。\n状态文件里也能看到：\njson 1 2 3 4 5 6 { \u0026#34;mode\u0026#34;: \u0026#34;需要权限\u0026#34;, \u0026#34;accessibilityOK\u0026#34;: false, \u0026#34;inputMonitoringOK\u0026#34;: false, \u0026#34;eventTapReady\u0026#34;: false } 所以现在排查路径很简单。\n先看顶栏，再看状态文件，最后才去翻日志。\n最后跑通的日志 最终日志是这样的：\ntext 1 2 3 4 5 6 7 event tap installed agent mini app started pid=57107 physical cmd+option down, current=com.tencent.inputmethod.wetype.pinyin posted cmd+option down physical cmd+option released; managing=true posted cmd+option up restored wetype 这说明完整链路已经通了：\ntext 1 2 3 4 5 6 微信输入法状态下按下 Command + Option -\u0026gt; agent 接管 -\u0026gt; 切到豆包 -\u0026gt; 给豆包发送按下事件 -\u0026gt; 松开时给豆包发送释放事件 -\u0026gt; 恢复微信输入法 这东西最终解决的其实是一个很小的需求。\n我就是不想切输入法。\n微信输入法继续负责日常打字和剪贴板同步，豆包输入法只在我按住快捷键的时候出来负责语音。\n这个小需求绕了一圈之后，居然还挺有意思。macOS 输入法系统、TIS API、CGEvent、按住式快捷键状态机，全都串起来了，而我对这些一窍不通，所以我也不知道这是不是最优解。\n但是姑且目前用下来很顺。\n偷懒造福人类，yes。\n","permalink":"/posts/%E8%B1%86%E5%8C%85%E8%BE%93%E5%85%A5%E6%B3%95%E4%BD%A0%E8%83%BD%E4%B8%8D%E8%83%BD%E5%8F%AA%E8%B4%9F%E8%B4%A3%E8%AF%AD%E9%9F%B3/","summary":"\u003cp\u003e最近豆包输入法的语音输入很火。\u003c/p\u003e\n\u003cp\u003e我是在开放后下载下来用了一下，确实很好用，对比之前用的 type less 对我而言有点修饰过头，微信输入法整体效果还行但是略逊于豆包。我设置的快捷键是 \u003ccode\u003eCommand + Option\u003c/code\u003e ，按住说话，松开就上屏，体验很顺。\u003c/p\u003e\n\u003cp\u003e但是我现在主力打字输入法还是微信输入法。\u003c/p\u003e\n\u003cp\u003e原因也很简单，它有多端剪切板同步。\u003c/p\u003e","title":"豆包输入法你能不能只负责语音"},{"content":"计算机操作系统 常见指令集有 CISC 和 RISC。 CISC 指令复杂、数量多，比如 x86 架构； RISC 指令简单、执行快，像 ARM、MIPS、RISC-V。 CPU 类型按指令集分，\n有 x86 系列，常用于 PC 和服务器； ARM 系列，多用在手机、嵌入式设备；还有 RISC-V 这种开源指令集的 CPU 除了 CPU，常见的处理器还有 GPU，擅长并行计算，主要用于图形渲染和 AI 训练；DSP，专为数字信号处理设计，像音频、图像处理设备常用；FPGA，可编程逻辑器件，可根据需求定制硬件逻辑，适合实时性高的场景；还有 TPU，是谷歌专为深度学习优化的处理器\n嵌入式操作系统 嵌入式操作系统特点是微型化、可定制、可靠性和一植移植性，常采用硬件抽象层Hardware Abstraction Layer，HAL和板级支撑包BSP\n嵌入式操作系统应具备实时性、可裁剪性和安全性等特征。 基本开发工具是交叉编译器、交叉链接器和源代码调试器\n计算机网络 局域网有总线型、星型、树型、环型、网状五种拓扑结构\nWLAN拓扑结构有点对点型、Hub型和完全分布型。\n完全分布型则处于理论探讨阶段无具体应用\n广域网由通信子网与资源子网组成。广域网可以分为公共传输网络、专用传输网、络和无线传输网络3类。\n主要有同步光纤网络（SONET）、同步数字体系（SDH）、数字数据网（DDN）、帧中继（FR）和异步传输技术（ATM） 。\n以太网规定最小帧长为64字节，最大帧长为1518字节。设置最小帧长是为了避免冲突，最小帧长是根据网络中检测冲突的最长时间来定的。\n信息系统基础 信息系统的功能有：输入、存储、处理、输出和控制 信息系统的生命周期分为产生、开发、运行和消亡4个阶段。\n企业的所有资源包括三大流：物流、资金流和信息流。\n信息安全技术基础 信息安全的基本要素有机密性、完整性、可用性、可控性与可审查性。 范围包括设备安全、数据安全、内容安全和行为安全。\nDES 56位密钥长度，三重DES 112位是因为他用了两个密钥 K1加密 K2解密 K1加密\n系统工程基础知识 MIPS=指令条数/(执行时间×10^-6)\n软件工程基础知识 碎碎念 看到这里已经困的不行了，明天早上七点起来路上复习后去考试\n案例分析第一题连考四次质量属性，所以押他还考，这里需要特别准备，然后准备一个主流技术，如果是微服务 / 云原生、缓存 / 数据库、新兴技术，新兴技术大概率是AI，其实都在我的打击范围，只是深度都不够，明天早上起来接着看案例\n历年论文四题范围\n我直接押题今年会有大模型应用，直接撞在我这两年最熟悉的领域，所以我将把复习重点放在案例分析可能会出现的概念上，好好准备一下\n","permalink":"/posts/%E9%A1%B6%E7%BA%A7%E4%B8%B4%E9%98%B5%E7%A3%A8%E6%9E%AA%E8%BD%AF%E8%80%83%E9%AB%98%E7%BA%A7%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84-1/","summary":"\u003ch2 id=\"计算机操作系统\"\u003e计算机操作系统\u003c/h2\u003e\n\u003cp\u003e常见指令集有 CISC 和 RISC。\nCISC 指令复杂、数量多，比如 x86 架构；\nRISC 指令简单、执行快，像 ARM、MIPS、RISC-V。\nCPU 类型按指令集分，\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e有 x86 系列，常用于 PC 和服务器；\u003c/li\u003e\n\u003cli\u003eARM 系列，多用在手机、嵌入式设备；还有 RISC-V 这种开源指令集的 CPU\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e除了 CPU，常见的处理器还有 GPU，擅长并行计算，主要用于图形渲染和 AI 训练；DSP，专为数字信号处理设计，像音频、图像处理设备常用；FPGA，可编程逻辑器件，可根据需求定制硬件逻辑，适合实时性高的场景；还有 TPU，是谷歌专为深度学习优化的处理器\u003c/p\u003e","title":"顶级临阵磨枪（软考高级系统架构-1）"},{"content":"我的主域名空着很久了，很久以前有一个非常简易的 home page，但是有了 AI 之后看来看去，太繁杂的又不喜欢，让我自己写又嫌烦，昨天终于狠下心让 codex 开始干活，然后部署流程记录一下。\n这是个 Vite + React 的静态主页，本地跑没问题，想丢到线上去。之前一直用 Cloudflare Pages 直接连 GitHub 仓库自动部署，但这次想换个方式——用 GitHub Actions 编译，然后通过 Wrangler 做 Direct Upload 部署到 Cloudflare Pages。\n思路比较简单，但是觉得可以记录一下。\n整体链路 先说清楚整个链路长什么样：\ntxt 1 2 3 4 5 6 7 8 本地代码 -\u0026gt; git push 到 GitHub main 分支 -\u0026gt; GitHub Actions 触发 CI + Deploy workflow -\u0026gt; npm ci -\u0026gt; npm run build -\u0026gt; 生成 dist -\u0026gt; wrangler pages deploy dist -\u0026gt; 发布到 Cloudflare Pages 跟 Cloudflare 自动连接 GitHub 仓库那种方式不一样，这里是 GitHub Actions 主动用 Wrangler 把构建产物推上去，算是个 Direct Upload 的方案。\n本地准备 项目就是个普通 Vite 项目，核心命令不用多说：\nbash 1 2 3 npm install npm run dev npm run build package.json 里确保有这些 scripts 就行：\njson 1 2 3 4 5 6 7 { \u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;vite\u0026#34;, \u0026#34;build\u0026#34;: \u0026#34;tsc --noEmit \u0026amp;\u0026amp; vite build\u0026#34;, \u0026#34;typecheck\u0026#34;: \u0026#34;tsc --noEmit\u0026#34; } } 构建完生成 dist/ 目录，Cloudflare Pages 最终部署的就是这玩意儿。\nGitHub Actions：CI workflow 先写一个 CI workflow，保证每次 push 或者 PR 都能过类型检查和构建。\n.github/workflows/ci.yml：\n[yaml] 显示已折叠代码（29 行） 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 name: CI on: push: branches: [main] pull_request: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Typecheck run: npm run typecheck - name: Build run: npm run build 用了 npm ci 的话，仓库里得提交 package-lock.json，不然会报错。别问我怎么知道的。\nGitHub Actions：部署 workflow .github/workflows/deploy.yml：\n[yaml] 显示已折叠代码（37 行） 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 name: Deploy to Cloudflare Pages on: push: branches: [main] workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest permissions: contents: read deployments: write steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Deploy uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=r-home-frontend gitHubToken: ${{ secrets.GITHUB_TOKEN }} 关键就是最后那行：\nyaml 1 command: pages deploy dist --project-name=r-home-frontend 意思是用 Wrangler 把 dist 目录上传到 Cloudflare Pages 里的 r-home-frontend 项目。所以这两个东西必须对得上——dist 是构建输出目录，r-home-frontend 必须是 Cloudflare Pages 里实际存在的项目名。\nCloudflare 侧的准备 创建 Pages 项目 去 Cloudflare Dashboard：\ntxt 1 Workers \u0026amp; Pages -\u0026gt; Create application -\u0026gt; Pages -\u0026gt; Direct Upload 项目名填 r-home-frontend，跟 workflow 里的 --project-name 保持一致。\n创建 API Token 这个 Token 是给 GitHub Actions 调 Cloudflare API 用的。去 Cloudflare Dashboard 右上角头像 -\u0026gt; 我的个人资料 -\u0026gt; API 令牌 -\u0026gt; 创建令牌。\n权限选：\ntxt 1 Account -\u0026gt; Cloudflare Pages -\u0026gt; Edit 资源范围选你自己的 Cloudflare 账号就行。\n别用 Global API Key。不需要 DNS 权限，不需要 Zone 权限。这个 token 就只管 Pages 部署。\nGitHub Secrets 在 GitHub 仓库 Settings -\u0026gt; Secrets and variables -\u0026gt; Actions 里添加两个 secret：\ntxt 1 2 CLOUDFLARE_API_TOKEN = 刚才创建的 API 令牌 CLOUDFLARE_ACCOUNT_ID = Cloudflare 账号 ID CLOUDFLARE_ACCOUNT_ID 在 Cloudflare Dashboard 右侧账号信息或者 Workers \u0026amp; Pages 页面都能找到。\n推送，然后报错了 准备完毕，推代码：\nbash 1 git push origin main 去 GitHub Actions 看，CI 跑过了，Deploy workflow 开始跑了，然后……\ntxt 1 2 3 4 ✘ [ERROR] A request to the Cloudflare API (/accounts/***/pages/projects/r-home-frontend) failed. Project not found. The specified project name does not match any of your existing projects. [code: 8000007] Project not found？？？\n仔细看报错信息——它请求的是 /pages/projects/r-home-frontend 这个路径，说明 Token 本身是通的，问题是 Cloudflare 里压根没有叫 r-home-frontend 的 Pages 项目。\n哦哦哦，原来是项目不存在。\n修法 两个方案，选一个就行：\n方案 A（推荐）：去 Cloudflare 创建同名项目\nWorkers \u0026amp; Pages -\u0026gt; Create application -\u0026gt; Pages，项目名填 r-home-frontend，然后回 GitHub Actions 页面 Re-run 失败的 workflow。\n方案 B：改 workflow 里的项目名\n如果 Cloudflare 里已经有 Pages 项目了，只是名字不一样，比如叫 anluoying-home，那就改 deploy.yml 里的：\nyaml 1 command: pages deploy dist --project-name=anluoying-home 提交推送就行。\n还有个 warning 部署日志里还有一段：\ntxt 1 2 Warning: Your working directory is a git repo and has uncommitted changes To silence this warning, pass in --commit-dirty=true 这个不是部署失败的原因，纯粹是个 warning。如果看着碍眼，可以加 --commit-dirty=true：\nyaml 1 command: pages deploy dist --project-name=r-home-frontend --commit-dirty=true 但先别管这个，把项目名对上才是正事。\n关于 CONFIG_ACCESS_KEY 排查过程中还看到 Worker 那边有个 CONFIG_ACCESS_KEY，一度以为跟部署有关。\n其实没关系。CONFIG_ACCESS_KEY 是 Worker 自己的运行时配置，应该配在 Cloudflare Worker 的 Variables / Secrets 里。跟 GitHub Actions 部署、Cloudflare API Token、Pages 项目名都不是一个东西。\n当前这个静态前端部署只需要两个东西：CLOUDFLARE_API_TOKEN 和 CLOUDFLARE_ACCOUNT_ID。\n最后 重新跑了一遍 workflow，CI 绿了，Deploy 也绿了。\n这套链路说白了就是：GitHub 管代码，GitHub Actions 管编译，Cloudflare Pages 管部署。三件事，三个地方。最容易出错的地方就三个：API Token 权限不够、GitHub Secrets 名字填错、--project-name 和 Cloudflare Pages 项目名对不上。\n搞定，收工。\n","permalink":"/posts/github-actions-%E7%BC%96%E8%AF%91-+-cloudflare-pages-%E9%83%A8%E7%BD%B2%E8%B8%A9%E5%9D%91%E5%85%A8%E8%AE%B0%E5%BD%95/","summary":"\u003cp\u003e我的主域名空着很久了，很久以前有一个非常简易的 home page，但是有了 AI 之后看来看去，太繁杂的又不喜欢，让我自己写又嫌烦，昨天终于狠下心让 codex 开始干活，然后部署流程记录一下。\u003c/p\u003e\n\u003cp\u003e这是个 Vite + React 的静态主页，本地跑没问题，想丢到线上去。之前一直用 Cloudflare Pages 直接连 GitHub 仓库自动部署，但这次想换个方式——用 GitHub Actions 编译，然后通过 Wrangler 做 Direct Upload 部署到 Cloudflare Pages。\u003c/p\u003e","title":"GitHub Actions 编译 + Cloudflare Pages 部署踩坑全记录"},{"content":"前趋图（Precedence Graph） 前趋图（Precedence Graph）是一种用于描述任务、事件或进程之间先后依赖关系的有向无环图（Directed Acyclic Graph，DAG）。\n通常记作：\nG = (P, E)\n其中：\nP：顶点集合（Vertex Set），表示进程、任务或事件 E：边集合（Edge Set），表示前驱约束关系 边一般表示为：\nE = {(p_i, p_j)}\n表示进程 p_i 必须先执行完成，进程 p_j 才能开始执行，即：\np_i ➡️ p_j\n由于前趋图只描述“先后依赖关系”，因此图中不能出现环路，所以它属于有向无环图（DAG）。\n在实际分析中，本质上就是按照“节点大小关系 + 箭头方向”记录每一条边的依赖关系。\nPV 操作（Semaphore Operations） PV 操作是操作系统中用于实现进程同步与互斥的一种经典机制，其核心是信号量（Semaphore）。\n信号量通常记作：\nS_i\n其中：\nS：Semaphore（信号量） 下标 i：表示第 i 个信号量 PV 操作包括两种基本原语：\nP 操作（Proberen） P 操作来源于荷兰语 “Proberen”，含义为“测试”或“申请资源”。\n执行逻辑：\n对信号量 S 减 1 若结果小于 0，则当前进程阻塞等待 若结果大于等于 0，则继续执行 因此：\n执行前需要进行 P 操作 表示“申请资源”或“进入临界区” 常写作：\nP(S_i)\nV 操作（Verhogen） V 操作来源于荷兰语 “Verhogen”，含义为“增加”或“释放资源”。\n执行逻辑：\n对信号量 S 加 1 若有等待进程，则唤醒其中一个 因此：\n执行完成后进行 V 操作 表示“释放资源”或“退出临界区” 常写作：\nV(S_i)\nPV 操作与前趋图的关系 例如：\np_1 到 p_2\n可表示为：\np_1 执行结束后执行 V(S) p_2 执行前先执行 P(S) 磁盘调度 最短移臂调度算法（SSTF，Shortest Seek Time First） 找最近柱面号，同柱面下，扇区号小的优先\n切换磁头代价较小\n页存储逻辑地址和物理地址转换 假设逻辑地址十六进制 5148H 进程 P四个存储块，页面大小 4K，页号 0~7，页号 5 的页帧号 3 怎么换算出物理地址 3148H\n页面大小 4K = 4 \\times 1024 = 4096 = 2^{12}\n页内偏移量占 12 位 高位是页号 5 查页表是 3 如果访问页面 6 不在内存 看状态位，在内存的，淘汰没被访问的，都被访问淘汰没被修改的\n文件索引 某文件系统文件存储采用文件索引节点法假设磁盘索引块和磁盘数据块大小均为 1 KB, 每个文件的索引节点中有 8 个地址项，07，每个地址项大小为 4B，其中 05 为直接索引，6是一级间接，七7二级间接，那么我们要访问的这个文件的逻辑块号分别为 0，260 和 518，则系统应分别采用什么索引，其文件系统可表示的文件最大长度为多少 KB\n块大小 1KB，地址项 4B，所以一个索引块能放： 1024 / 4 = 256 个地址。\n0~5：直接索引，共 6 块\n6：一级间接索引，可表示 256 块\n7：二级间接索引，可表示 256 × 256 = 65536 块\n逻辑块号 判断 0 直接索引 260 一级间接索引，因为在 6~261 518 二级间接索引，因为大于 261 6 + 256 + 65536 = 65798 块 65798KB 先算一个索引块能放多少地址项：块大小 / 地址项大小。\n然后按“直接块数、一级间接块数、二级间接块数”分段判断逻辑块号。\n应用层协议 协议 全称 传输层协议 默认端口 作用 / 特点 FTP File Transfer Protocol TCP 21（控制）/ 20（数据） 文件传输协议，使用两条 TCP 连接 TFTP Trivial File Transfer Protocol UDP 69 简单文件传输协议，无认证、轻量级 HTTP HyperText Transfer Protocol TCP 80 超文本传输协议，Web 基础协议 HTTPS HTTP Secure TCP 443 加密版 HTTP，基于 SSL/TLS SMTP Simple Mail Transfer Protocol TCP 25 邮件发送协议 POP3 Post Office Protocol Version 3 TCP 110 邮件接收协议，下载式 IMAP Internet Message Access Protocol TCP 143 邮件接收协议，支持服务器同步 DHCP Dynamic Host Configuration Protocol UDP 67（服务端）/ 68（客户端） 动态分配 IP 地址，默认租约一般 8 天，过半续约 Telnet Telnet Protocol TCP 23 远程终端协议，明文传输，不安全 SSH Secure Shell TCP 22 安全远程登录协议，Telnet 替代品 DNS Domain Name System UDP / TCP 53 域名解析，通常 UDP，大数据量或区域传送使用 TCP SNMP Simple Network Management Protocol UDP 161 网络设备管理协议 NTP Network Time Protocol UDP 123 网络时间同步协议 LDAP Lightweight Directory Access Protocol TCP / UDP 389 目录访问协议 SMB Server Message Block TCP 445 Windows 文件共享协议 RTP Real-time Transport Protocol UDP 动态端口 音视频实时传输 SIP Session Initiation Protocol TCP / UDP 5060 音视频会话控制协议 RTSP Real Time Streaming Protocol TCP / UDP 554 流媒体控制协议 存储网络架构 DAS，单机存储架构 NAS，网络存储架构 SAN，网络存储架构\n信息安全基础知识 机密性、完整性、可用性、可控性、可审查性\n安全范围： 设备安全：物质基础。 数据安全：防止未授权的泄露，篡改和毁坏 内容安全：政治法律道德层次上的要求 行为安全：通过行为提供给用户，确保行为安全。\n信息安全风险类别 人为： 被动 主动 灾害性 系统故障 人员无意识行为\n加密技术 类型 特征 常见算法 对称加密（Symmetric Encryption） 加密和解密使用同一个密钥；速度快；适合大量数据加密；密钥分发困难 DES、3DES、AES、RC4、SM4 非对称加密（Asymmetric Encryption） 使用公钥和私钥；公钥加密、私钥解密；安全性高；速度较慢；适合密钥交换与数字签名 RSA、DSA、ECC、ElGamal DES 太短不安全 64 位，56 位密钥，8 位校验 3DES 三次更安全但更慢 112 或 168 位密钥长度 AES 又快又安全，是现代主流 RSA 512 位 补充记忆：\nHTTPS 中通常：\n非对称加密用于“交换会话密钥” 对称加密用于“实际数据传输” 消息摘要 例：哈希算法 常用算法，MD5，SHA-256\n数字签名 发送者私钥加密\n数字证书 CA = Certificate Authority\n安全模型 模型 全称 核心目标 主要特征 状态机模型 State Machine Model 保证系统始终处于安全状态 将系统抽象为“状态集合 + 状态转换”，只允许安全状态之间转换，是很多安全模型的理论基础 BLP 模型 Bell-LaPadula Model 保密性（Confidentiality） “不上读，下不写（No Read Up，No Write Down）”；防止高密级信息泄露到低密级 Biba 模型 Biba Integrity Model 完整性（Integrity） “不上写，下不读（No Write Up，No Read Down）”；防止低可信数据污染高可信数据 CWM 模型 Clark-Wilson Model 商业数据完整性 强调事务控制、职责分离、审计机制，通过“良构事务”保证数据一致性 Chinese Wall 模型 Chinese Wall Model 防止利益冲突 用户访问某公司数据后，不能再访问竞争公司的敏感数据，常用于金融、咨询行业 常见口诀：\n模型 口诀 BLP 保密模型，上读下写禁止 Biba 完整性模型，上写下读禁止 CWM 商业完整性、职责分离 Chinese Wall 防利益冲突 状态机模型 系统始终保持安全状态 数据库体系结构 模式 又称 面向对象 主要内容 作用 外模式 子模式 / 用户模式 用户、应用程序 用户看到的局部数据视图 保证数据独立性与安全性，不同用户看到不同数据 概念模式 逻辑模式 整个数据库系统 数据库整体逻辑结构与关系 描述全局数据结构，是数据库核心层 内模式 存储模式 数据库存储系统 数据物理存储方式 描述数据在磁盘中的存储结构与访问方式 简单理解：\n层次 类比 外模式 用户看到的页面 概念模式 数据库整体设计图 内模式 磁盘里的真实存储 核心作用：\n模式 数据独立性 外模式 ↔ 概念模式 逻辑数据独立性 概念模式 ↔ 内模式 物理数据独立性 口诀：\n外模式：用户看到什么 概念模式：数据库整体长什么样 内模式：数据实际上怎么存 数据模型 类型 含义 特点 常见模型 概念数据模型 面向用户和业务的模型 强调现实世界语义，独立于具体数据库实现，便于需求分析 E-R 模型（实体-联系模型） 基本数据模型 面向数据库系统实现的模型 描述数据逻辑结构和操作方式，用于数据库设计与实现 层次模型、网状模型、关系模型、面向对象模型 进一步理解：\n模型 面向阶段 概念数据模型 需求分析阶段 基本数据模型 数据库设计与实现阶段 常见概念：\n概念数据模型 核心元素：\n实体（Entity） 属性（Attribute） 联系（Relationship） 典型表示：\nE-R 图。\n基本数据模型 常见类型：\n模型 特点 层次模型 树形结构，一对多 网状模型 图结构，多对多 关系模型 二维表结构，最主流 面向对象模型 支持对象封装、继承 口诀：\n概念模型：面向现实世界 基本模型：面向数据库实现 E-R 属于概念模型 关系模型属于基本模型 关系代数运算 运算 符号 含义 特点 并 ∪ 合并两个关系中的元组 要求两个关系同构 差 − 属于前者但不属于后者 要求两个关系同构 交 ∩ 两个关系共同拥有的元组 要求两个关系同构 笛卡尔积 × 两个关系所有元组组合 元组数相乘 选择 σ 按条件筛选行 相当于 WHERE 投影 π 选取指定列 相当于 SELECT 字段 连接 ⋈ 按条件组合两个关系 常见数据库 JOIN 自然连接 ⨝ 自动按同名属性连接 去除重复列 除 ÷ 表示“全部满足”关系 较难理解，考试高频 常见分类：\n类型 运算 集合运算 并、差、交 专门关系运算 选择、投影、连接、除 基本运算 并、差、笛卡尔积、选择、投影 导出运算 交、连接、自然连接、除 简单理解：\n运算 类似 SQL 选择 σ WHERE 投影 π SELECT 字段 连接 ⋈ JOIN 并 ∪ UNION 差 − EXCEPT / MINUS 口诀：\n选择选“行” 投影选“列” 连接拼“表” 并差交做“集合运算” 云原生架构 架构模式 核心特征 典型能力 适用场景 常见技术/实现 服务化架构模式 将系统拆分为多个独立服务，每个服务独立部署、扩缩容、迭代 服务自治、接口通信、独立发布 大型业务系统、微服务平台 Kubernetes、Spring Cloud、Dubbo、gRPC Mesh 化架构模式 将网络通信能力下沉到基础设施层，通过 Sidecar 管理服务间流量 服务治理、流量控制、安全认证、熔断限流 大规模微服务集群 Istio、Linkerd、Envoy Serverless 模式 开发者只关注业务逻辑，底层资源由平台自动管理 自动扩缩容、按需计费、免运维 突发流量、轻量 API、事件处理 AWS Lambda、Knative、OpenFaaS 存储计算分离模式 计算节点与存储节点解耦，可独立扩展 弹性扩缩容、资源复用、高可用 大数据、云数据库、AI 训练 Ceph、MinIO、Snowflake、对象存储 分布式事务模式 在多个服务或数据库之间保证数据一致性 最终一致性、事务补偿、状态协调 电商订单、支付、库存系统 Seata、Saga、TCC、两阶段提交（2PC） 可观测架构 对系统运行状态进行全面采集与分析 日志、指标、链路追踪、告警 云平台运维、故障定位 Prometheus、Grafana、OpenTelemetry、Jaeger 事件驱动架构 系统通过事件进行异步解耦通信 消息发布订阅、异步处理、削峰填谷 实时通知、数据同步、流处理 Kafka、RabbitMQ、Pulsar、RocketMQ 大数据设计架构 Lambda 架构（Lambda Architecture） 是一种“大数据实时处理架构”。\n它的核心目标是：\n同时兼顾：\n实时性 准确性 海量数据处理能力 最早由 Nathan Marz 提出。\nLambda 架构的核心思想：\n同一份数据：\n一边走“实时计算” 一边走“离线批处理” 最后把两边结果合并。\n经典结构：\n[text] 显示已折叠代码（19 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 数据流 │ ┌──────────┴──────────┐ │ │ ▼ ▼ Speed Layer Batch Layer （实时层） （离线层） │ │ ▼ ▼ 实时结果 精确结果 └──────────┬──────────┘ ▼ Serving Layer （服务层） 它由三层组成。\n1. Batch Layer（批处理层）\n负责：\n存储全量历史数据 做高准确性离线计算 特点：\n慢 但结果最准确 典型技术：\n技术 用途 Apache Hadoop 离线计算 Apache Spark 批处理 Hive 数仓 HDFS 分布式存储 例如：\ntext 1 2 3 4 5 每天凌晨重新统计： - 用户画像 - 推荐模型 - 全站订单 - 风险评分 2. Speed Layer（实时层）\n负责：\n处理最新数据 提供低延迟结果 特点：\n快 但可能不完全准确 因为：\n数据可能不完整 窗口未闭合 有乱序问题 典型技术：\n技术 用途 Apache Storm 早期流处理 Apache Flink 实时计算 Apache Kafka 消息流 Spark Streaming 流计算 例如：\ntext 1 2 3 4 5 用户刚下单后： 立即更新： - 实时销量 - 热搜榜 - 风控告警 而不是等第二天。\n3. Serving Layer（服务层）\n负责：\n合并批处理结果 合并实时结果 对外提供查询 例如：\ntext 1 2 3 4 最终销量 = 历史离线统计 + 最近5分钟实时增量 为什么需要 Lambda 架构\n因为：\n早期大数据系统存在矛盾：\n需求 问题 实时 数据不完整 准确 批处理太慢 于是：\ntext 1 2 实时层负责“快” 离线层负责“准” 两边结合。\n举个电商例子\n假设淘宝统计：\ntext 1 今日商品销量排行榜 实时层：\ntext 1 2 用户下单后 1 秒内更新排行榜 但：\n有重复消息 有退款 有延迟订单 所以结果不一定准。\n离线层：\ntext 1 凌晨重新跑全量订单 得到准确结果。\n最终：\ntext 1 实时增量 + 离线校正 Lambda 架构的问题 后来大家发现：\n它太复杂。\n因为：\n你要维护：\ntext 1 两套计算逻辑 即：\n实时计算代码 离线计算代码 容易：\n逻辑不一致 运维复杂 开发成本高 所以后来出现 Kappa 架构 由 Jay Kreps 提出。\n核心思想：\ntext 1 只保留流处理 即：\ntext 1 所有数据都是流 历史数据也可以重新回放。\n结构变成：\ntext 1 2 3 4 5 Kafka ↓ Flink ↓ Serving 不用：\nBatch Layer 双代码 所以现代云原生实时系统：\n很多已经：\ntext 1 Lambda → Kappa 二者对比 对比项 Lambda Kappa 批处理层 有 无 实时层 有 有 代码逻辑 两套 一套 架构复杂度 高 较低 数据校正 离线修正 流回放修正 适合 传统大数据 现代实时系统 现在比较常见的现实情况是：\n场景 架构 传统 Hadoop 数仓 Lambda 实时风控 Kappa Kafka + Flink Kappa 湖仓一体 更偏流批一体 一句话总结：\nLambda 架构本质是：\ntext 1 “实时流处理 + 离线批处理”双轨并行 用：\ntext 1 2 实时层保证速度 离线层保证准确性 Kappa 架构 该架构的核心思想其实非常“Kafka 化”：\ntext 1 2 3 所有数据都是事件流 所有计算都基于流 历史数据靠回放解决 它不再区分：\n“离线数据” “实时数据” 而是统一：\ntext 1 事件日志（Event Log） 先看一个最典型的 Kappa 例子。\n例子 1：电商实时订单系统 假设：\n用户下单。\n系统产生事件：\njson 1 2 3 4 5 6 { \u0026#34;event\u0026#34;: \u0026#34;order_created\u0026#34;, \u0026#34;order_id\u0026#34;: \u0026#34;1001\u0026#34;, \u0026#34;user_id\u0026#34;: \u0026#34;u88\u0026#34;, \u0026#34;amount\u0026#34;: 399 } 这些事件进入：\nApache Kafka\n然后：\nApache Flink\n持续消费：\ntext 1 2 3 4 5 6 7 8 Kafka ↓ Flink ↓ 实时销量统计 实时GMV 实时推荐 实时风控 此时：\n你的网站已经能：\n实时排行榜 实时销售额 秒级监控 实时推荐 那问题来了：\n如果：\ntext 1 统计逻辑写错了 怎么办？\nLambda 的做法：\ntext 1 重新跑离线 Batch Kappa 的做法：\ntext 1 回放 Kafka 历史数据 例如：\ntext 1 从 offset=0 重新消费 重新计算。\n这就是：\ntext 1 流式重算 所以 Kappa 的关键前提是：\ntext 1 消息流必须可长期保存 Kafka 天然适合。\n例子 2：实时风控系统 这是 Kappa 非常适合的场景。\n例如：\n信用卡风控。\n每次交易：\njson 1 2 3 4 5 6 { \u0026#34;user\u0026#34;: \u0026#34;u100\u0026#34;, \u0026#34;ip\u0026#34;: \u0026#34;x.x.x.x\u0026#34;, \u0026#34;country\u0026#34;: \u0026#34;RU\u0026#34;, \u0026#34;amount\u0026#34;: 8000 } 进入 Kafka。\nFlink 实时处理：\ntext 1 2 3 4 - 是否异地登录 - 是否异常金额 - 是否高频交易 - 是否设备切换 然后：\ntext 1 实时拦截 这里：\ntext 1 低延迟 \u0026gt; 强一致 所以：\nKappa 很适合。\n例子 3：IoT / 工业监控 例如：\n设备持续上传：\ntext 1 2 3 4 5 温度 振动 电流 压力 转速 全部进入 Kafka。\nFlink 持续计算：\ntext 1 2 3 4 最近5分钟平均振动 异常波动检测 设备健康评分 预测性故障 如果：\n后面：\ntext 1 异常算法升级了 怎么办？\nKappa：\ntext 1 重新回放历史传感器流 重新生成结果。\n这其实比 Lambda 更自然。\n因为工业场景：\n本来就是：\ntext 1 无限事件流 Kappa 为什么越来越流行 因为现代系统：\n越来越“事件驱动”。\n例如：\nKafka Pulsar Redpanda EventBridge CDC Debezium 本质都在：\ntext 1 Everything is a stream Kappa 能完全替代 Lambda 吗？ 答案：\ntext 1 不能完全替代 但：\ntext 1 很多互联网实时业务已经偏向 Kappa 原因在于：\nKappa 有自己的限制。\nKappa 的几个核心问题 1. 回放成本巨大 假设：\ntext 1 2 每天 100TB 日志 保留 180 天 你要重算：\ntext 1 18 PB 数据 这非常恐怖。\nLambda：\ntext 1 离线批处理更适合超大规模重算 尤其：\nPB级 年级别 全量统计 2. Kafka 不是无限存储 理论上能长保留。\n实际上：\n成本极高。\n所以：\n很多公司：\ntext 1 Kafka 保存 3~7 天 历史数据进：\nHDFS S3 Iceberg Hudi Delta Lake 这时候：\n纯 Kappa 就不成立了。\n3. 流处理不擅长复杂历史分析 例如：\ntext 1 2 3 4 5 统计： 过去3年 全国 所有用户 消费分布 这种：\nOLAP / 数仓分析。\n批处理更强。\n4. AI / 机器学习训练 很多训练：\n需要：\ntext 1 全量历史数据 例如：\n特征工程 embedding 离线训练 数据清洗 这类：\n还是：\ntext 1 Batch 更适合 所以现代架构其实变成了： 流批一体（Streaming + Batch Unified）\n代表：\n技术 特点 Apache Flink 流批一体 Apache Spark Structured Streaming Apache Iceberg 湖仓 Delta Lake 湖仓 Apache Hudi 增量数据湖 现在很多公司的真实架构：\ntext 1 2 3 4 5 Kafka ↓ Flink 实时计算 ↓ 实时服务 同时：\ntext 1 2 3 4 5 Kafka CDC ↓ Iceberg / Hudi ↓ 离线分析 / AI训练 也就是说：\ntext 1 实时与离线共享同一份数据源 这其实已经：\n既不是纯 Lambda。\n也不是纯 Kappa。\n而是：\ntext 1 Lakehouse + Streaming 你可以这样理解三代演进：\n时代 架构 Hadoop时代 Lambda Kafka时代 Kappa Lakehouse时代 流批一体 一句话总结：\nKappa 的核心优势：\ntext 1 2 一套流处理逻辑 通过事件回放解决历史重算 它非常适合：\n实时业务 IoT 风控 推荐系统 实时监控 但：\ntext 1 2 3 4 超大规模历史分析 AI训练 长期归档 复杂OLAP 目前仍离不开 Batch。\nSOAP协议是什么 SOAP（Simple Object Access Protocol）\n面向服务架构 模式 英文 核心思想 主要特征 服务注册表模式 Service Registry Pattern 通过统一注册中心管理服务 服务启动时注册自身信息，消费者通过注册表发现服务，实现动态发现与解耦 企业服务总线模式 ESB（Enterprise Service Bus） 通过统一总线实现系统集成 采用中心化总线连接各系统，负责协议转换、消息路由、数据转换与服务编排 进一步理解：\n模式 类比 服务注册表 “电话簿 / 通讯录” ESB “交通枢纽 / 中央调度中心” ESB 负责： 消息转发 协议转换 数据格式转换 服务路由 服务编排 常见对比：\n对比项 服务注册表 ESB 架构风格 微服务 SOA 通信方式 去中心化 中心化 核心作用 服务发现 服务集成 是否存在中心节点 注册中心 总线中心 扩展性 高 相对较低 口诀：\n注册表解决“服务在哪” ESB 解决“系统怎么连” 微服务 能拆就拆 独立，技术选型灵活 松耦合、易扩展\n挑战 分布式、数据一致性、测试难度、监控管理分发扩容问题\n数据库设计 redis redis持久化方案 持久化方式 全称 特点 优点 缺点 RDB Redis DataBase 定时生成内存快照（Snapshot） 文件小、恢复快、性能高 可能丢失最后一次快照后的数据 AOF Append Only File 记录每条写操作日志 数据更安全、丢失少 文件较大、恢复较慢 混合持久化 RDB + AOF Redis 4.0 引入，先写 RDB 再追加 AOF 兼顾恢复速度与数据安全 实现更复杂 核心区别：\n对比项 RDB AOF 持久化方式 保存数据结果 保存操作命令 数据安全性 较低 较高 文件大小 小 大 恢复速度 快 慢 对性能影响 小 较大 数据丢失风险 较高 较低 RDB（快照） 原理：\n某一时刻把整个内存数据：\n直接保存成二进制快照文件。\n生成文件：\nbash 1 dump.rdb 常见触发方式：\nconf 1 2 3 save 900 1 save 300 10 save 60 10000 表示：\n900 秒内至少 1 次修改 就执行快照 特点：\n类似“拍照片” 保存的是某时刻完整数据 可能丢失：\n最后一次快照之后的数据。\nAOF（追加日志） 原理：\n把所有写命令：\n追加记录到日志文件。\n例如：\ntxt 1 2 3 SET name ray INCR count LPUSH msg hello 恢复时：\n重新执行命令。\n生成文件：\nbash 1 appendonly.aof AOF 三种刷盘策略：\n策略 含义 always 每次写都刷盘，最安全最慢 everysec 每秒刷盘一次，默认 no 由 OS 决定 默认：\nconf 1 appendfsync everysec 最多丢失约 1 秒数据。\n混合持久化 Redis 4.0 后支持。\n核心思想：\n前半部分用 RDB 快照 后半部分追加 AOF 日志 这样：\n启动恢复更快 数据也更安全 属于目前推荐方案。\n一句话记忆 方式 理解 RDB “存结果” AOF “存过程” 混合持久化 “结果 + 过程” 口诀：\nRDB 快但可能丢数据 AOF 安全但更重 混合持久化最均衡 ","permalink":"/posts/%E9%A1%B6%E7%BA%A7%E4%B8%B4%E9%98%B5%E7%A3%A8%E6%9E%AA%E8%BD%AF%E8%80%83%E9%AB%98%E7%BA%A7%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84-0/","summary":"\u003ch2 id=\"前趋图precedence-graph\"\u003e\u003cstrong\u003e前趋图（Precedence Graph）\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e前趋图（Precedence Graph）是一种用于描述任务、事件或进程之间先后依赖关系的有向无环图（Directed Acyclic Graph，DAG）。\u003c/p\u003e\n\u003cp\u003e通常记作：\u003c/p\u003e\n\u003cp\u003eG = (P, E)\u003c/p\u003e\n\u003cp\u003e其中：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eP：顶点集合（Vertex Set），表示进程、任务或事件\u003c/li\u003e\n\u003cli\u003eE：边集合（Edge Set），表示前驱约束关系\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e边一般表示为：\u003c/p\u003e","title":"顶级临阵磨枪（软考高级系统架构-0）"},{"content":"开发目标检测、姿态识别这类视觉程序的时候，有个很烦的问题：你需要一个可控的、可重复的视频输入源。\n对着真实的摄像头调试？光线一会变一个样，人走来走去的，画面完全不可控。用视频文件做输入吧，有些程序又只认摄像头设备或者RTSP地址，不认本地文件路径。\n所以我需要一个东西，既能把视频文件\u0026quot;伪装\u0026quot;成一个实时的视频流，让下游程序当摄像头一样去读。又能快速接入各种我想要做出的画面或者 usb 摄像头来测试视频效果。\n当然如果有更好的方案欢迎大家留言（鬼知道有没有人看得到我的文章）\n其实 OBS 自带的 Virtual Camera 也能解决一部分问题，但我没怎么用过。\n这次搭的链路长这样：\ntext 1 视频文件 → OBS 推流 → MediaMTX → RTSP → ffplay / OpenCV / 目标检测程序 MediaMTX 是什么 之前没见过 MediaMTX，一开始还以为是某种虚拟摄像头设备。不是。\nMediaMTX 是一个本地流媒体中转服务器，用 Go 写的，一个二进制文件就能跑。它能把推过来的流转成 RTSP、RTMP、HLS、WebRTC、SRT 等各种协议的输出。也就是说你往里面塞一路流，下游不管你用什么协议来读，它都能接。\nGitHub 地址：bluenviron/mediamtx，macOS arm64 直接下载解压就能用。\n启动 MediaMTX bash 1 2 cd /Volumes/extStorage/rayEx/Downloads/mediamtx_v1.18.2_darwin_arm64 ./mediamtx gemini 最早叫我下载完之后直接刷机打开，启动的时候会报一条 warning：是因为双击启动的目录对不上，他找不到 yml，进入路径执行就没问题了\ntext 1 WAR configuration file not found (looked in .../rtsp-simple-server.yml, .../mediamtx.yml), using an empty configuration 默认配置已经把常用端口都打开了：\ntext 1 2 3 4 5 RTSP :8554 RTMP :1935 HLS :8888 WebRTC :8889 SRT :8890 本地调试足够用了，不用改 yml。后面需要鉴权、固定路径、改端口的时候再配。\nOBS 推流 OBS 里面设置推流地址：\ntext 1 2 3 4 Settings → Stream Service: Custom... Server: rtmp://localhost/mystream Stream key: 留空 然后点 Start Streaming。\n切回 MediaMTX 的终端，如果看到类似这样的日志：\ntext 1 is publishing to path \u0026#39;mystream\u0026#39; 就说明 OBS 的流已经成功推到 MediaMTX 了。中间经历了 RTMP 这一跳。\nffplay 读 RTSP 验证链路通不通，最简单的方式就是 ffplay：\nbash 1 ffplay rtsp://127.0.0.1:8554/mystream 跑起来的话，整个链路就通了：OBS → RTMP → MediaMTX → RTSP → ffplay。\n后续目标检测程序也可以读同一个地址 rtsp://127.0.0.1:8554/mystream，跟 ffplay 互不影响。\nWebRTC 那边翻车了 顺手试了一下浏览器预览：\ntext 1 http://localhost:8889/mystream MediaMTX 报了：\ntext 1 2 WebRTC doesn\u0026#39;t support H264 streams with B-frames WAR [WebRTC] skipping track 2 (MPEG-4 Audio) OBS 的流已经推到了 MediaMTX，这个没问题。但浏览器的 WebRTC 不支持带 B-frames 的 H264，音频编码也不对路。\n这个只影响 WebRTC 浏览器预览，RTSP 链路完全正常。所以暂时不纠结这个问题了，反正 ffplay 能看就行。\n后面真要用 WebRTC 页面看的话，可以在 OBS 编码设置里关掉 B-frames、音频改成 Opus，或者干脆用 WHIP 推流。\n延迟问题 第一次跑通的时候延迟不小。这条链路里可能产生延迟的地方有四段：\ntext 1 OBS 编码队列 → RTMP 推流 → MediaMTX 转发 → RTSP 客户端缓冲 不过 MediaMTX 本身更像是一个协议转发中枢，不太会主动开大缓存。延迟主要还是 OBS 编码器和播放器默认缓冲的锅。\nOBS 端优化 OBS 编码器为了画质会引入缓冲，低延迟方向：\nB-frames 设为 0 如果用 x264 编码器，设置 tune=zerolatency 关键帧间隔短一点，1s 或 2s 关掉 Look-ahead 分辨率和帧率别太高，720p / 25fps 做调试够用了 ffplay 端优化 ffplay 默认也会做探测和缓冲。低延迟播放命令：\nbash 1 2 3 4 5 6 ffplay \\ -fflags nobuffer \\ -flags low_delay \\ -framedrop \\ -rtsp_transport udp \\ rtsp://127.0.0.1:8554/mystream UDP 不稳定就换 TCP：\nbash 1 2 3 4 5 6 ffplay \\ -fflags nobuffer \\ -flags low_delay \\ -framedrop \\ -rtsp_transport tcp \\ rtsp://127.0.0.1:8554/mystream 更激进的版本，把探测尺寸和分析时长都压到最小：\nbash 1 2 3 4 5 6 7 8 9 10 ffplay \\ -fflags nobuffer \\ -flags low_delay \\ -probesize 32 \\ -analyzeduration 0 \\ -max_delay 0 \\ -framedrop \\ -sync ext \\ -rtsp_transport udp \\ rtsp://127.0.0.1:8554/mystream 这些参数会让起播变快、延迟变低，但也更容易花屏卡顿。调试用用就行，正式程序得自己权衡。\n还有一种更短的链路 OBS 先 RTMP 再转 RTSP，中间多了一跳。如果要进一步降延迟，可以让 OBS 通过 FFmpeg Output 直接推 RTSP：\ntext 1 2 3 4 5 6 7 Settings → Output → Recording Type: Custom Output (FFmpeg) FFmpeg output type: Output to URL File path or URL: rtsp://127.0.0.1:8554/mystream Container format: rtsp Video encoder: libx264 Video encoder settings: bf=0 注意这种方式点的是 Start Recording，不是 Start Streaming。\n目标检测程序的读取方式 这个其实是个很容易被忽略的坑。目标检测程序如果按队列逐帧处理，推理速度跟不上帧率的话，会越读越落后，延迟越来越大。\n推荐的做法是单独一个线程持续读取，只保留最新一帧，推理线程永远拿最新帧处理，丢掉积压的旧帧：\npython 1 2 3 4 5 6 7 8 9 10 11 12 13 14 latest_frame = None # reader thread while True: ok, frame = cap.read() if ok: latest_frame = frame # detector loop while True: frame = latest_frame if frame is None: continue result = detector(frame) 常用地址备忘 text 1 2 3 4 RTMP 推流入口: rtmp://localhost/mystream RTSP 读取地址: rtsp://127.0.0.1:8554/mystream WebRTC 预览地址: http://localhost:8889/mystream WHIP 推流地址: http://localhost:8889/mystream/whip 调试顺序 启动 MediaMTX OBS 推流到 rtmp://localhost/mystream ffplay 读 RTSP 验证链路通 加低延迟参数观察延迟差异 程序读 RTSP，先只显示画面不跑模型 加入目标检测，改成\u0026quot;只处理最新帧\u0026quot;模式 根据瓶颈再调 OBS 编码参数、RTSP transport、分辨率和检测频率 这套方案跑通之后，开发视觉程序既可以快速用上外接的 usb 摄像头或者来自其他地方的视频流。又或者最重要的视频文件当摄像头用，调试效率高很多。\n","permalink":"/posts/%E7%94%A8-obs-+-mediamtx-%E6%90%AD%E4%B8%80%E4%B8%AA%E6%9C%AC%E5%9C%B0%E8%A7%86%E9%A2%91%E6%B5%81%E7%BB%99%E8%A7%86%E8%A7%89%E7%A8%8B%E5%BA%8F%E7%94%A8/","summary":"\u003cp\u003e开发目标检测、姿态识别这类视觉程序的时候，有个很烦的问题：你需要一个可控的、可重复的视频输入源。\u003c/p\u003e\n\u003cp\u003e对着真实的摄像头调试？光线一会变一个样，人走来走去的，画面完全不可控。用视频文件做输入吧，有些程序又只认摄像头设备或者RTSP地址，不认本地文件路径。\u003c/p\u003e","title":"用 OBS + MediaMTX 搭一个本地视频流给视觉程序用"},{"content":" 昨天看到一篇 drawio-mcp 的文章，说 draw.io 官方开源了 MCP，可以让 AI 直接生成可编辑架构图。\n真是天赐的礼物\n目前我画图基本都是用 processon这个平台，因为他在线编辑，所以等于一次编辑，导出修改查看。但是可惜的是这种肯定是要收费的，所以一直想找个替代版，另外还有一个问题是，让 LLM 输出 ASCII 字符画的话 很难进行拖动框框和一些 线路的修改。\n看了一眼，mcp GitHub - jgraph/drawio-mcp · GitHub，我似乎没有太多的 在线浏览之类的需求，首先我之前没怎么用过 drawio，我只有一个大概的概念。我找了一下他有 desktop 版本的，于是我下载了他的 desktop。然后仓库里面其实有一个 skills：drawio-mcp/skill-cli/drawio/SKILL.md at main · jgraph/drawio-mcp · GitHub 你可以直接叫 codex 或者 claude code去安装这个 skills\n这个 drawio skill，本质上就是一份 Agent 指令。它会让 Claude Code 直接生成原生 .drawio 文件，也就是 draw.io 自己用的 mxGraphModel XML。\n这点很关键。它不是先写 Mermaid，然后丢给某个服务转换；也不是生成一张死图。它写出来的就是 draw.io 能继续编辑的源文件。\n用法也很直接，在 Claude Code 里喊 /drawio 就行：\ntext 1 2 3 /drawio create a flowchart for user login /drawio sequence diagram for API auth /drawio ER diagram for blog posts and tags 如果不指定格式，它就生成 .drawio 文件，然后尝试打开 draw.io。\n比如：\ntext 1 /drawio create an architecture diagram for this Hugo blog deployment 最后应该会得到类似这种文件：\ntext 1 hugo-blog-deployment.drawio 如果想直接要图片，也可以在 prompt 里指定格式：\ntext 1 2 3 /drawio png flowchart for user login /drawio svg ER diagram for e-commerce /drawio pdf architecture overview 这个时候它会尝试找 draw.io Desktop 自带的 CLI，然后执行类似这样的导出命令：\nbash 1 drawio -x -f png -e -b 10 -o diagram.drawio.png diagram.drawio 这里最重要的是 -e，也就是 --embed-diagram。导出的 PNG / SVG / PDF 里面会嵌入原始 XML，所以这些文件不是普通截图，拖回 draw.io 里还能继续编辑。\n这个设计挺香的。\n这就像是把图纸和渲染图塞进了同一个文件里。平时发出去别人看到的是图片，真要改的时候还能把图纸掏出来继续施工。\n不过前提是本机得有 draw.io Desktop。macOS 下默认路径是：\nbash 1 /Applications/draw.io.app/Contents/MacOS/draw.io Linux 一般看 drawio 在不在 PATH 里。WSL2 稍微麻烦点，要去 Windows 那边找：\nbash 1 `/mnt/c/Program Files/draw.io/draw.io.exe` 注意这个反引号不是装饰，是为了处理 Program Files 里的空格。这个设计看着有点怪，但 skill 里就是这么写的。\n如果找不到 draw.io CLI，也不是完全不能用。它会保留 .drawio 文件，然后我再自己用 draw.io 打开。也就是说，导出图片失败不影响生成源文件。\n说到官方 drawio-mcp，它其实不止这一种玩法。\n我参考了一下之前整理的那篇文章，官方仓库里大概可以分成这几类：\n方式 适合什么 大概是什么感觉 Skill + CLI 在代码仓库里产出图文件 本地写 .drawio，可提交、可版本管理 MCP Tool Server 临时把图打开到 draw.io 编辑器 npx @drawio/mcp 起一个本地 MCP server MCP App Server 支持 MCP Apps 的聊天界面 在聊天里直接预览/交互 Project Instructions 临时试一下 不安装，复制 prompt 规则硬上 我现在更喜欢 Skill + CLI 这套。\n原因很简单：它的产物明确。\n生成出来就是文件，放进仓库就完事。博客里要图、项目文档里要图、README 里要图，都适合这种方式。临时看一眼图的话，MCP Tool Server 可能更顺手；但如果我想让这个图以后还能被找到、被 git 追踪，那还是本地 .drawio 文件比较安心。\n还有一个区别是格式支持。\nMCP Tool Server 那边可以打开 XML / CSV / Mermaid。也就是说你可以让它把 Mermaid 送进 draw.io 编辑器里。\n但这个 skill 明确要求直接生成 XML，不走 Mermaid/CSV 转换。看起来少了点自由度，但好处也明显：落地就是原生 draw.io 文件，不依赖服务端转换，不绕一圈。\n简单让他画了一个架构图，回头测一个复杂架构\nok，复杂结构测试效果如下\n丑了点，但是逻辑还是可以的，整体来讲 做一个可迭代的架构图是没问题了，而且 drawio 的特性保存文件在本地，也很有安全感\n","permalink":"/posts/%E8%AE%A9-claude-code-%E7%9B%B4%E6%8E%A5%E7%94%BB-draw.io-%E5%9B%BE/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e昨天看到一篇 drawio-mcp 的文章，说 draw.io 官方开源了 MCP，可以让 AI 直接生成可编辑架构图。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e真是天赐的礼物\u003c/p\u003e\n\u003cp\u003e目前我画图基本都是用 processon这个平台，因为他在线编辑，所以等于一次编辑，导出修改查看。但是可惜的是这种肯定是要收费的，所以一直想找个替代版，另外还有一个问题是，让 LLM 输出 ASCII 字符画的话 很难进行拖动框框和一些 线路的修改。\u003c/p\u003e","title":"让 Claude Code 直接画 draw.io 图"},{"content":"Token 已成为新型货币！！！\n那么省钱是第一要素，这里介绍一个好玩的东西：RTK（Rust Token Killer）\n说能省 60-90% 的 token。是不是吹，试试就知道\nRTK 是什么 简单说，RTK 是一个 CLI 代理，用 Rust 写的单文件二进制，零依赖。它做的事情就是卡在 AI 编程助手和 shell 命令之间，把命令的输出压缩过滤之后再返回给 AI。\n举个例子，你让 Claude Code 跑一个 git status，正常情况下它会拿到完整的输出——每个文件的路径、状态、分支信息，全都一股脑塞进去，可能得 2000 个 token。但经过 RTK 过滤之后，真正有用的信息被提取出来，没用的噪音被砍掉，可能就 200 个 token 搞定了。\n这就好比你朋友问你\u0026quot;今天中午吃了啥\u0026quot;，你不用把整个食堂菜单念一遍，直接说\u0026quot;黄焖鸡\u0026quot;就行了。\n看一下这个原理图就明白了：\n它支持四种压缩策略：智能过滤（去掉注释、空行这些噪音）、分组聚合（把同类文件按目录归拢）、截断保留（只留关键上下文）、去重折叠（重复的日志行显示为计数）。\n怎么使用 安装方式挺多的，Homebrew、cargo、curl 脚本、直接下二进制都行：\nbash 1 2 3 4 5 6 7 8 # Homebrew brew install rtk # 或者一键脚本 curl -fsSL https://rtk-ai.dev/install.sh | bash # 或者 cargo（注意要用 git 源，crates.io 上有个同名的 Rust Type Kit 别装错了） cargo install --git https://github.com/rtk-ai/rtk 装完跑一下 rtk --version 确认没问题。\n然后关键一步——初始化：\nbash 1 rtk init 这个命令会自动帮你配好 Claude Code 的 hook。配完之后，以后 Claude Code 在 bash 里执行的命令，都会自动被 RTK 拦截重写，git status 会变成 rtk git status，完全透明，不用手动改任何东西。\n支持的命令 RTK 支持的命令非常多，100 多个，基本上日常开发用到的都覆盖了：\nGit 系列：git status、git log、git diff、git commit、git push 这些是最常用的，省 token 效果也最明显。比如 git commit 只返回一个 ok abc1234，git push 只返回 ok main——AI 不需要看完整的 push 日志，知道成功了、推到哪个分支就行了 文件操作：ls、read、find、grep、diff，都能压缩输出 测试跑器：Jest、Vitest、pytest、go test、cargo test 都支持，而且特别暴力——失败的测试只显示失败的部分，成功的直接跳过，能省 90% 构建和 lint：eslint、tsc、prettier、cargo clippy、ruff 这些也能过滤 包管理器：pnpm、pip、cargo 的安装输出也会被压缩 AWS CLI：这个我没用过，但看文档支持得挺全的，EC2、Lambda、CloudWatch、S3 都能过滤 Docker / k8s：docker ps、kubectl get pods 这些日常运维命令也有 还有个比较有意思的是 rtk read，带 -l aggressive 参数的时候只返回函数签名，不返回函数体，看代码结构的时候特别有用。\n实际用下来 装好之后重启 Claude Code 就自动生效了。用的过程中其实感受不到 RTK 的存在——它就是在后台默默干活。你跟 Claude Code 对话，它该怎么跑命令还怎么跑，只是返回的输出变精简了。\n想看看省了多少可以跑：\nbash 1 rtk gain 可以看到每个命令省了多少 token、平均压缩率多少、总执行时间多少。 我才刚装十分钟 哈哈\n今日未安装前使用统计 按这个比例，一天能省不少\n另外 rtk discover 也很实用，它会扫描你历史的 Claude Code 会话，找出那些没走 RTK 的命令——意思就是\u0026quot;你还有这些命令可以优化，别浪费了\u0026quot;：\nbash 1 rtk discover 这个暂时就没收集到信息\n一些细节 几个值得注意的点：\n失败时的 tee 机制。RTK 默认会保存命令的完整原始输出。如果过滤后的输出太精简导致 AI 判断失误、命令执行失败了，它可以从保存的完整输出里读，不用重新跑一遍命令。这个设计挺聪明的。\n不是万能的。RTK 只能过滤 bash 工具的输出，Claude Code 内置的 Read、Glob、Grep 这些原生工具它似乎是管不到的。所以要最大化效果，还是得配合 hook 用。\n支持的 AI 工具不止 Claude Code。GitHub Copilot、Cursor、Gemini CLI、Codex、Windsurf、Cline 这些也都支持，基本上主流的 AI 编程助手都能用。不过 hook 的接入方式各不相同，有的是 PreToolUse，有的是 AGENTS.md 注入，具体得看文档。\n一个 Rust 单文件二进制 最后说一下，RTK 本身是一个 Rust 写的单个二进制文件，不到 10MB，没有任何运行时依赖。GitHub 上 47k star，900 多个 commit，迭代挺活跃的（写这篇文章的时候最新版是 v0.39.0）。\n对了，它还有个 rtk telemetry 命令，默认是关闭的。如果你愿意贡献使用数据来帮助改进，可以手动打开：\nbash 1 rtk telemetry enable 收集的数据不含任何源码、文件路径或个人信息，就是一些聚合的命令统计和 token 节省数据。\n总结 总之，目前用下来感觉就是一个\u0026quot;装了就忘\u0026quot;的工具——装好之后完全透明地工作，你不会注意到它，但钱包会替你记住它。对于重度 vibe coder 来说，值得试用。\n我主要担心的是，他压缩后内容给到 AI 太少，导致 AI 判断错误。目前看来这些可压缩命令应该不会造成这个问题，而且 45k + 的 stars 哪怕挤掉水分也是大户了。\n","permalink":"/posts/%E7%94%A8-rtk-%E7%BB%99-claude-code-%E7%9C%81%E7%82%B9-token/","summary":"\u003cp\u003eToken 已成为新型货币！！！\u003c/p\u003e\n\u003cp\u003e那么省钱是第一要素，这里介绍一个好玩的东西：RTK（Rust Token Killer）\u003c/p\u003e\n\u003cp\u003e说能省 60-90% 的 token。是不是吹，试试就知道\u003c/p\u003e\n\u003ch2 id=\"rtk-是什么\"\u003eRTK 是什么\u003c/h2\u003e\n\u003cp\u003e简单说，RTK 是一个 CLI 代理，用 Rust 写的单文件二进制，零依赖。它做的事情就是卡在 AI 编程助手和 shell 命令之间，把命令的输出压缩过滤之后再返回给 AI。\u003c/p\u003e","title":"用 RTK 给 Claude Code 省点 token"},{"content":"为什么不叫指南，因为不配。这篇文章就记几个我自己运维的时候老是去搜的命令，写下来方便以后翻。三个方向：Docker、网络、进程。\n不会讲 Docker 基础用法，也不讲怎么装。假设你跟我一样，知道 docker 是啥，但具体运维的时候老忘命令。\nDocker 这玩意占了多少资源？ 服务器上跑了十几个容器，有时候磁盘或者内存告警了，第一反应就是\u0026quot;到底谁在吃资源\u0026quot;。\n磁盘占用：\nbash 1 2 # 所有容器的磁盘占用，大的排前面 docker system df -v 这个命令会显示镜像、容器、build cache 各占了多少空间。-v 是 verbose，会逐个列出每个镜像/容器的大小。\n如果发现某个镜像版本占了巨大空间，docker image prune 可以清理悬空镜像（就是那种 \u0026lt;none\u0026gt;:\u0026lt;none\u0026gt; 的）。加上 -a 会清掉所有没被容器引用的镜像，慎用。\nbash 1 2 3 4 5 6 7 8 # 只清悬空镜像 docker image prune # 清所有没在用的镜像（慎） docker image prune -a # 全家桶：清容器、网络、镜像、build cache docker system prune -a 内存和 CPU 实时占用：\nbash 1 docker stats 实时刷新的，类似 top。按 CPU 或内存排序，一眼就能看出谁是大户。Ctrl+C 退出。\n如果只想看某个容器：\nbash 1 docker stats \u0026lt;container_name_or_id\u0026gt; 说实话 docker stats 的输出格式不太好用，信息太多了。我一般就盯着 CPU% 和 MEM USAGE 两列看。\n这个容器到底怎么起来的？ 这是运维中最常遇到的灵魂拷问。服务器上跑着一个容器，你知道它存在，但不知道它是 docker run 起来的还是 docker compose 起来的，如果是 compose，compose 文件在哪？\n先看容器的启动命令：\nbash 1 docker inspect \u0026lt;container_name_or_id\u0026gt; 这玩意输出一大坨 JSON，但有几个关键字段：\nbash 1 2 3 4 5 6 7 8 9 10 11 # 看启动命令（Cmd 字段） docker inspect --format=\u0026#39;{{.Config.Cmd}}\u0026#39; \u0026lt;container\u0026gt; # 看环境变量 docker inspect --format=\u0026#39;{{.Config.Env}}\u0026#39; \u0026lt;container\u0026gt; # 看挂载 docker inspect --format=\u0026#39;{{range .Mounts}}{{.Source}} -\u0026gt; {{.Destination}}{{\u0026#34;\\n\u0026#34;}}{{end}}\u0026#39; \u0026lt;container\u0026gt; # 看端口映射 docker inspect --format=\u0026#39;{{json .HostConfig.PortBindings}}\u0026#39; \u0026lt;container\u0026gt; 判断是不是 compose 起来的：\nbash 1 2 # 看容器的 label，compose 起来的会有 com.docker.compose 相关标签 docker inspect --format=\u0026#39;{{json .Config.Labels}}\u0026#39; \u0026lt;container\u0026gt; | python3 -m json.tool 如果看到类似这样的标签：\njson 1 2 3 4 5 { \u0026#34;com.docker.compose.project\u0026#34;: \u0026#34;myapp\u0026#34;, \u0026#34;com.docker.compose.project.working_dir\u0026#34;: \u0026#34;/opt/myapp\u0026#34;, \u0026#34;com.docker.compose.service\u0026#34;: \u0026#34;web\u0026#34; } 那 Working Dir 就是 compose 文件所在目录。com.docker.compose.project.working_dir 直接告诉你 compose 文件在哪。挂载的数据卷目录大概率也在那附近。\n如果啥 label 都没有，那就是 docker run 起来的。这时候只能靠 docker inspect 的 Mounts 字段找挂载点了。\n实际运维中，我更推荐的做法是：所有服务都用 compose，compose 文件统一放在 /opt/\u0026lt;service_name\u0026gt;/ 下面。这样接手别人的服务时不用猜。\n一个快速恢复 run 命令的技巧：\nbash 1 2 3 4 5 # 把容器当前的配置还原成 docker run 命令 docker run --name \u0026lt;container\u0026gt; \u0026lt;image\u0026gt; # 配合 inspect 的输出手动拼，或者用这个工具： # https://github.com/Red5d/docker-autocompose docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/red5d/docker-autocompose \u0026lt;container_name\u0026gt; 这个 docker-autocompose 工具能直接把运行中的容器转成 docker-compose.yml，接手别人的服务的时候特别好用。\n网络 端口到底被谁占了？ \u0026ldquo;端口已被占用\u0026rdquo;——程序员最常遇到的报错之一。排查这个问题，不同发行版用的命令还不太一样。\n通用方案（推荐）：lsof\nbash 1 2 3 4 5 6 7 8 9 10 11 # 查看某个端口被谁占了 sudo lsof -i :8080 # 查看某个端口的 TCP 连接 sudo lsof -i tcp:8080 # 查看某个进程打开了哪些端口 sudo lsof -i -p \u0026lt;pid\u0026gt; # 查看某个用户的所有网络连接 sudo lsof -i -u \u0026lt;username\u0026gt; lsof 的输出信息很全：进程名、PID、用户、文件描述符、协议、本地地址、远程地址、连接状态。\nbash 1 2 3 4 5 # 常用组合：看 80 端口的 ESTABLISHED 连接 sudo lsof -i :80 -s tcp:ESTABLISHED # 只看 LISTEN 状态（谁在监听这个端口） sudo lsof -i :3306 -s tcp:LISTEN Ubuntu / Debian 原生方案：\nbash 1 2 3 4 5 6 7 8 9 10 11 12 # ss 替代了旧的 netstat，更快 ss -tlnp # t=TCP, l=LISTEN, n=数字显示端口, p=显示进程 # 看所有 TCP 连接 ss -tnp # 看某个端口 ss -tlnp \u0026#39;sport = :8080\u0026#39; # 如果你习惯了 netstat（需要安装 net-tools） sudo apt install net-tools netstat -tlnp CentOS / RHEL 原生方案：\nbash 1 2 3 4 5 6 7 8 # ss 同样可用（CentOS 7+ 默认有） ss -tlnp # netstat 也行 netstat -tlnp # CentOS 的 firewall 查看开放端口 sudo firewall-cmd --list-ports 现在大多数发行版都自带 ss 了，它比 netstat 快很多，因为直接读 netlink socket。能用 ss 就用 ss。\nrustnet：Rust 写的终端网络监控神器 之前搜到一个叫 rustnet 的工具，Rust 写的，功能出乎意料地全。跟 ss 和 lsof 不同，它是实时显示的 TUI 界面，能看到每个进程的网络连接、带宽消耗、协议识别，还带深度包检测 (DPI)。\n安装：\n[bash] 显示已折叠代码（17 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # macOS / Linux brew tap domcyrus/rustnet brew install rustnet # Ubuntu 25.10+ sudo add-apt-repository ppa:domcyrus/rustnet sudo apt update \u0026amp;\u0026amp; sudo apt install rustnet # Fedora 42+ sudo dnf copr enable domcyrus/rustnet sudo dnf install rustnet # Arch Linux sudo pacman -S rustnet # 通用（需要 Rust 环境） cargo install rustnet-monitor 基本使用：\nbash 1 2 3 4 5 6 7 8 9 10 11 # 需要 root 权限（抓包需要） sudo rustnet # 指定网卡 sudo rustnet -i eth0 # 看 localhost 连接 sudo rustnet --show-localhost # 关闭 DNS 反解（更快） sudo rustnet --no-resolve-dns 不想每次都 sudo？给它加个 capability：\nbash 1 2 sudo setcap \u0026#39;cap_net_raw,cap_bpf,cap_perfmon+eip\u0026#39; $(which rustnet) rustnet 最实用的几个功能：\n看谁在吃带宽：进入界面后按 s 切换排序列，切到 \u0026ldquo;Down/Up ↓\u0026rdquo; 就是按带宽排序 按进程看连接：按 a 开启进程分组，每个进程下面展开它所有的连接 过滤特定端口/进程：按 / 进入过滤模式 bash 1 2 3 4 5 6 # 过滤示例 /port:443 # 只看 443 端口 /process:nginx # 只看 nginx 进程 /state:established # 只看已建立的连接 /sni:github.com # 只看 SNI 是 github.com 的（HTTPS 连接） /dport:443 sni:api # 组合过滤：目标端口 443 且 SNI 包含 api 查看连接详情：选中一条连接按 Enter，能看到协议、加密套件、GeoIP 等信息 保留已关闭的连接：按 t 可以让已关闭的连接不消失，方便事后排查 这个工具的过滤语法很像 vim 的搜索，支持正则：/(?i)pattern/。用起来很顺。\n键盘速查：\n按键 作用 j/k 或方向键 上下移动 Enter 查看连接详情 / 进入过滤模式 Esc 返回/清除过滤 s / S 切换排序列 / 切换排序方向 a 开启/关闭进程分组 t 显示/隐藏已关闭连接 i 切换网卡统计视图 c 复制远程地址 q 退出 关于抓包 tcpdump 和 Wireshark 的水太深了，这里不展开。就记几个最常用的：\nbash 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 抓 eth0 上 80 端口的包 sudo tcpdump -i eth0 port 80 # 抓某个 host 的包 sudo tcpdump -i eth0 host 192.168.1.100 # 抓 TCP SYN 包（看谁在连你） sudo tcpdump -i eth0 \u0026#39;tcp[tcpflags] \u0026amp; tcp-syn != 0\u0026#39; # 保存到文件，用 Wireshark 打开 sudo tcpdump -i eth0 -w capture.pcap port 443 # 读 pcap 文件 tcpdump -r capture.pcap rustnet 也能导出 pcap：rustnet --pcap-export，而且导出的 pcap 里还带进程信息（PID 和进程名），这个在 Wireshark 里是看不到的。有需要可以配合它自带的 pcap_enrich.py 脚本做分析。\n进程排查 服务器 CPU 飙了或者内存不够了，第一件事就是看哪个进程在作妖。\ntop top 是最基础的，所有 Linux 系统都有。\nbash 1 top 进来之后看到的几个关键列：\n%CPU — CPU 占用百分比 %MEM — 内存占用百分比 RES — 实际物理内存占用（常驻内存） COMMAND — 进程名 常用快捷键：\n按键 作用 P 按 CPU 排序（大写） M 按内存排序（大写） k 杀进程（会让你输 PID） 1 显示每个 CPU 核心的占用 c 显示完整命令行 H 显示线程 f 选择显示哪些列 q 退出 有个容易踩的坑：top 默认是\u0026quot;按启动以来的平均 CPU 占用\u0026quot;排序的。如果你要看当前谁在吃 CPU，按一下 P（大写）确认是按 CPU 降序排列。有些系统默认就是，有些不是。\nbash 1 2 3 4 5 6 7 8 # 只看某个用户进程 top -u mysql # 只看某个 PID top -p 12345 # 批处理模式，输出到文件（配合脚本用） top -b -n 1 \u0026gt; top_output.txt htop htop 是 top 的加强版，界面好看很多，操作也更方便。大多数发行版需要单独安装：\nbash 1 2 3 4 5 6 7 8 # Ubuntu / Debian sudo apt install htop # CentOS / RHEL sudo dnf install htop # Arch sudo pacman -S htop 用法：\nbash 1 htop 比起 top 的优势：\n鼠标可点 — 可以直接点列头排序，点进程选中 树状视图 — 按 t 显示进程树，看父子关系一目了然 搜索 — 按 F3 或 / 搜索进程名 过滤 — 按 F4 只显示匹配的进程 选中多个进程批量操作 — 按空格选中多个，然后 F9 批量杀 常用快捷键：\n按键 作用 F1 帮助 F2 设置（自定义显示列、颜色方案等） F3 / F4 搜索 / 过滤 F5 树状视图 F6 选择排序列 F9 发信号（杀进程用 SIGTERM/SIGKILL） F10 退出 t 树状视图开关 H 显示/隐藏线程 u 按用户过滤 Space 标记进程（可多选） 顶部的彩色条含义：\n绿色 = 用户态进程占用 红色 = 内核态占用 蓝色 = 低优先级（nice 值） 橙色 = IRQ 时间 灰色 = IO 等待 我个人更喜欢 htop，主要是树状视图和搜索功能太实用了。top 的好处是\u0026quot;到哪台机器都有\u0026quot;，应急的时候不用装东西。两个都得会。\n一些组合用法 排查问题的时候，一般不会只用一个命令，而是组合着来：\n[bash] 显示已折叠代码（17 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 先看整体负载 uptime # 输出：14:23:01 up 45 days, 3:12, 2 users, load average: 5.20, 3.15, 2.80 # load average 三个数字分别是 1分钟、5分钟、15分钟的平均负载 # 2. 找到吃 CPU 的进程 top -bn1 | head -20 # 3. 看这个进程的详细信息 ls -l /proc/\u0026lt;pid\u0026gt;/exe # 进程对应的可执行文件 cat /proc/\u0026lt;pid\u0026gt;/cmdline # 完整的启动命令 ls -l /proc/\u0026lt;pid\u0026gt;/fd # 进程打开了哪些文件描述符 # 4. 看这个进程的网络连接 sudo lsof -i -p \u0026lt;pid\u0026gt; # 或者 ss -tnp | grep \u0026lt;pid\u0026gt; 这三个方向基本覆盖了我日常运维 80% 的场景。剩下 20% 比如日志排查、磁盘 IO、内核参数调优，以后踩到了再补。\n这篇跟刚刚写的\u0026quot;斜杠命令大全\u0026quot;一样，会慢慢补充更新。\n","permalink":"/posts/simple-ops-guide/","summary":"\u003cp\u003e为什么不叫指南，因为不配。这篇文章就记几个我自己运维的时候老是去搜的命令，写下来方便以后翻。三个方向：Docker、网络、进程。\u003c/p\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e不会讲 Docker 基础用法，也不讲怎么装。假设你跟我一样，知道 docker 是啥，但具体运维的时候老忘命令。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003chr\u003e\n\u003ch2 id=\"docker\"\u003eDocker\u003c/h2\u003e\n\u003ch3 id=\"这玩意占了多少资源\"\u003e这玩意占了多少资源？\u003c/h3\u003e\n\u003cp\u003e服务器上跑了十几个容器，有时候磁盘或者内存告警了，第一反应就是\u0026quot;到底谁在吃资源\u0026quot;。\u003c/p\u003e","title":"简易运维指北"},{"content":" 这是一篇长期更新的博客。只要我还在用 Claude Code，或者 Claude Code 还在更新，这篇就会跟着更新。哪天我不用了或者 CC 停更了，会在标题加个 [已停更]。如果对你有用，欢迎留言催更。\n用 Claude Code (下文简称 CC) 有段时间了。它最让我觉得舒服的一点是，进了交互会话之后，输入 / 就能看到一堆斜杠命令，该干嘛一目了然。\n但我一直有个困扰——命令越来越多，有些是后来加的，有些改了名字，有些直接被删了。光靠 /help 的列表已经不够用了，我想有个完整的地方能看到\u0026quot;到底有多少个命令，每个干嘛用的\u0026quot;。\n官方文档其实整理得不错，但散落在好几个页面里。我决定自己手动过一遍，把所有斜杠命令都扒下来，记录用法，贴上参考链接。\n调研日期: 2026-05-13，CC 版本 v2.1.140。数据来源主要是 官方 Commands 文档、CLI Reference 和 Changelog。\n一点点点背景 Claude Code 是 Anthropic 在 2025年2月 以 \u0026ldquo;research preview\u0026rdquo; 形式发布的终端 AI 编码工具。简单说就是一个跑在终端里的 AI agent，能读你的代码、改文件、跑命令、操作 git，你用自然语言跟它说就行。\n它一直在快速迭代。让 cc 查了一下官方 changelog，发现最早的记录只到 v2.1.94 (2026-04-07)，再往前的版本历史已经查不到了。所以很多命令具体是哪个版本加进来的，我没法给出确切时间。如果你知道，欢迎留言补充。\n当前总览 截至 v2.1.140 (2026-05-12)，CC 的交互会话里一共有 70个 斜杠命令（算上别名的话是 86 个条目）。这个数字不含你自己写的 custom skills，也不含 MCP server 暴露出来的命令。\n数量确实不少了。我按功能分了几个大类来整理，方便查找。\n所有命令的详细文档入口: code.claude.com/docs/en/commands\n会话管理 这类命令管的是\u0026quot;对话\u0026quot;本身——开始、继续、压缩、分支、回退。\n命令 用途 /clear [name] 清空上下文开始新会话。之前的历史还在 /resume 里。别名: /reset, /new /compact [instructions] 压缩对话上下文，释放 token 空间。对话太长的时候救命用的 /resume [session] 恢复之前的会话，可以按 ID 或名字。别名: /continue /branch [name] 在当前对话点创建一个分支，原对话保留。别名: /fork /rewind 回退对话和/或代码到之前某个点。别名: /checkpoint, /undo /rename [name] 给当前会话改名 /export [filename] 把对话导出成纯文本文件 /recap 生成当前会话的一句话摘要，离开一会儿回来用 /btw \u0026lt;question\u0026gt; 快速旁问，不进对话历史。上下文够用但不占位置，问完即走 /exit 退出。别名: /quit 模型与推理 命令 用途 /model [model] 切换模型，比如 sonnet、opus /effort [level|auto] 调努力等级: low / medium / high / xhigh / max。不加参数会出个滑块让你选 /fast [on|off] 切换 fast mode，用更快的输出速度跑（不降级模型） 项目初始化与配置 第一次在某个 repo 里用 CC，或者想调整配置，用这些。\n命令 用途 /init 初始化项目的 CLAUDE.md，CC 的\u0026quot;项目记忆\u0026quot;入口 /config 打开设置界面，主题、模型、输出风格都在这调。别名: /settings /memory 编辑 CLAUDE.md 记忆文件，管理 auto-memory /hooks 查看 hook 配置（在工具调用前后执行 shell 命令） /permissions 管理工具权限规则: allow / ask / deny。别名: /allowed-tools /keybindings 打开或创建快捷键配置 /terminal-setup 配置终端快捷键，比如 Shift+Enter 换行（VS Code 等终端需要手动配） /theme 切换配色主题，支持 auto、亮色/暗色、色障友好、ANSI、自定义主题 /tui [default|fullscreen] 设置终端渲染器。fullscreen 是无闪烁的 alt-screen 渲染 /statusline 配置状态栏显示内容 /scroll-speed 调鼠标滚轮速度（仅 fullscreen 模式） /color [color|default] 设提示栏颜色。不加参数随机一个颜色 /sandbox 切换沙箱模式 /add-dir \u0026lt;path\u0026gt; 添加额外工作目录，授权文件访问 代码审查与安全 命令 用途 /review [PR] 本地审查 PR /security-review 分析当前分支待提交更改的安全漏洞 /ultrareview [PR] 云端多 agent 深度代码审查，比 /review 更重 /simplify [focus] 审查最近改过的文件，找代码复用、质量和效率问题并修复 /diff 打开交互式 diff 查看器，看未提交更改和逐轮 diff 并行工作与 Agents CC 能同时跑多个 agent，这部分命令管的就是这些。\n命令 用途 /agents 管理 subagent 配置 /background [prompt] 把当前会话丢到后台跑，终端空出来干别的。别名: /bg /batch \u0026lt;instruction\u0026gt; 大规模并行变更。CC 拆成 5~30 个独立单元，每个跑在自己的 worktree 里，各自开 PR /tasks 列出和管理后台任务。别名: /bashes /stop 停止当前后台会话 /loop [interval] [prompt] 循环执行 prompt，可以指定间隔。不加间隔就 CC 自己控节奏。别名: /proactive /goal [condition|clear] 设个目标条件，CC 会持续跨轮工作直到满足 /plan [description] 进入计划模式，先规划再动手 远程与跨设备 CC 的会话不绑定一个终端，可以在设备间切换。\n命令 用途 /remote-control 让当前会话可从 claude.ai 或 Claude app 远程控制。别名: /rc /teleport 把 web session 拉到本地终端继续。别名: /tp /desktop 在 Claude Desktop 应用里继续当前会话。macOS/Windows only。别名: /app /autofix-pr [prompt] 启动 web session 监控当前分支的 PR，CI 失败或 review 评论时自动修复 /remote-env 配置 web session 的默认远程环境 云端能力 CC 有些功能是跑在 Anthropic 的云上的。\n命令 用途 /schedule [description] 创建/管理 routines，云端定时任务。别名: /routines /ultraplan \u0026lt;prompt\u0026gt; 云端沙箱里起草计划，浏览器里 review 后执行或发回终端 /ultrareview [PR] (同上，也归这里) 云端多 agent 审查 集成 (MCP / IDE / 平台) 命令 用途 /mcp 管理 MCP 服务器连接和 OAuth 认证 /ide 管理 IDE 集成并显示状态 /chrome 配置 Chrome 浏览器集成 /install-github-app 安装 Claude GitHub Actions app /install-slack-app 安装 Claude Slack app /web-setup 把 GitHub 账户连接到 Claude Code on the web /setup-bedrock 配置 Amazon Bedrock 认证和模型 /setup-vertex 配置 Google Vertex AI 认证和模型 /plugin 管理 Claude Code 插件 /reload-plugins 重新加载所有活跃插件 诊断与反馈 命令 用途 /doctor 诊断安装和配置问题，按 f 让 CC 自动修 /debug [description] 开启调试日志，排查问题用 /heapdump 写 JS heap 快照，排查内存问题 /feedback [report] 提交反馈。别名: /bug /usage 显示会话成本、计划用量和活动统计。别名: /cost, /stats /extra-usage 配置 extra usage 以突破速率限制 /release-notes 查看更新日志，交互式版本选择器 /insights 生成会话分析报告，包括项目区域、交互模式、摩擦点 账户与订阅 命令 用途 /login 登录 Anthropic 账户 /logout 登出 /upgrade 打开升级页面 /privacy-settings 查看和更新隐私设置 (Pro/Max) /passes 分享免费体验周给朋友 内置 Skills 这些是 CC 自带的 skill，用的是和你自己写的 skill 一样的机制——本质上是一个 prompt 交给 CC 执行。\n命令 用途 /batch 大规模并行变更（上面说过了） /claude-api 加载 Claude API 参考资料，支持代码迁移 /debug 调试日志排查（上面说过了） /fewer-permission-prompts 扫描历史记录，添加常用只读操作的允许列表 /loop 循环执行（上面说过了） /simplify 代码质量审查（上面说过了） 查看所有可用 skills: /skills\n其他 零零碎碎归不到上面类别的：\n命令 用途 /help 显示帮助 /status 版本、模型、账户、连接状态 /context [all] 可视化上下文使用情况，彩色网格 /copy [N] 复制最后一个回复到剪贴板。/copy 2 复制倒数第二个 /focus 切换焦点视图，只显示最后一个 prompt 和回复 /skills 列出可用 skills /powerup 交互式功能课程，带动画演示 /team-onboarding 从你的 CC 使用历史生成团队入门指南 /voice [hold|tap|off] 语音输入 /mobile 显示下载 Claude 移动应用的 QR 码。别名: /ios, /android /stickers 订购 CC 贴纸 [Doge] /radio 浏览器打开 Claude FM lo-fi radio 已被移除的命令 命令 移除版本 去哪了 /vim v2.1.92 改为 /config → Editor mode /pr-comments v2.1.91 改为直接让 Claude 查看 PR 评论 别名速查 有些命令有多个名字，怕你用混了，列一下：\n主命令 别名 /background /bg /branch /fork /clear /reset, /new /config /settings /cost → /usage /stats /desktop /app /exit /quit /feedback /bug /loop /proactive /mobile /ios, /android /permissions /allowed-tools /remote-control /rc /resume /continue /rewind /checkpoint, /undo /schedule /routines /tasks /bashes /teleport /tp 更新时间线 能查到的版本发布时间线。CC 在 2025 年 2 月首次发布，但官方 changelog 从 v2.1.94 (2026-04-07) 才有记录，中间有一大段空白。如果你知道更早版本的信息，留言告诉我。\n日期 版本 关键变更 2025-02 research preview CC 首次发布，包含基础命令 2026-04-07 2.1.94 changelog 可追溯的最早版本 2026-04-10 2.1.101 新增 /team-onboarding 2026-04-13 2.1.105 新增 /proactive (别名 for /loop) 2026-04-14 2.1.108 新增 /recap, /undo 2026-04-15 2.1.110 新增 /tui, /focus 2026-04-16 2.1.111 新增 /fewer-permission-prompts, /ultrareview 2026-04-23 2.1.118 /usage 合并了 /cost 和 /stats 2026-05-04 2.1.128 /color 增强 2026-05-11 2.1.139 新增 /goal, /scroll-speed 参考链接 Commands 完整文档 — 最权威的命令列表 CLI Reference — 启动参数和 CLI 子命令 Interactive Mode — 键盘快捷键、Vim 模式、命令历史 Skills — 自定义技能 Changelog — 官方更新日志 MCP — Model Context Protocol 文档 Settings — 配置选项 这篇会随着 CC 更新持续维护。命令多了少了改名了，我都会跟着更新。最新的 CC 版本号和本文最后更新时间在 frontmatter 的 lastmod 里。大家有发现什么遗漏或者错误，直接留言就好。\n","permalink":"/posts/claude-code-slash-commands/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e这是一篇\u003cstrong\u003e长期更新\u003c/strong\u003e的博客。只要我还在用 Claude Code，或者 Claude Code 还在更新，这篇就会跟着更新。哪天我不用了或者 CC 停更了，会在标题加个 \u003ccode\u003e[已停更]\u003c/code\u003e。如果对你有用，欢迎留言催更。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003chr\u003e\n\u003cp\u003e用 Claude Code (下文简称 CC) 有段时间了。它最让我觉得舒服的一点是，进了交互会话之后，输入 \u003ccode\u003e/\u003c/code\u003e 就能看到一堆斜杠命令，该干嘛一目了然。\u003c/p\u003e","title":"Claude Code 斜杠命令大全"},{"content":" 本文转载自 Datawhale easy-vibe 教程，加了自己的理解和全链路自建的部分\n手机上写代码？ 地铁上突然想到一个 bug 怎么修，买一杯瑞排队的时候收到线上告警，躺床上不想起来但又想让 Claude 帮你跑个任务——这种场景太多了。传统 IDE 那套东西在手机上根本跑不动，虚拟键盘输入代码效率低得离谱，小屏幕连代码和终端都放不下。\n但换个思路想，手机只需要当一个控制台就够了——输入指令、看结果、审批操作。真正干活的还是你家里/公司的开发机。\n这就是\u0026quot;瘦客户端\u0026quot;的核心思想：手机只管发指令，活在别处干。\n现有方案快速过一遍 社区里能用的方案其实不少了，快速过一下：\niOS 官方 App Anthropic 出的 Claude App 里直接有 Code 标签页。手机发指令，代码在 Anthropic 云端沙盒执行，结果通过 GitHub 同步。零配置，Pro 订阅就能用。但——中国大陆用不了，功能也受限，不能访问本地文件系统。pass。\nHappy Coder 开源的，跨平台（iOS/Android/Web），端到端加密。电脑上装个 happy-coder，手机扫码配对就能远程控制 Claude Code 和 Codex。配置简单，扫码即用，这个方案用的人很多。\n但连接不太稳定，断线了上下文就丢了。而且依赖第三方中继服务器，代码安全心里没底。\nSSH + Tailscale + Tmux 最硬核的方案。Tailscale 做 VPN 打洞（打洞失败自动中继），手机用 SSH 客户端连上开发机，Tmux 保持会话不丢。功能最完整，桌面级体验。但配置复杂，电脑得一直开着，还得会 Tmux。适合老手。\nTermux Android 用户可以在手机上直接跑 Claude Code CLI。装个 Termux（注意从 F-Droid 下，Google Play 版本过时了），装 Node.js，装 Claude Code，完事。手机性能有限，编译大型项目就算了，但写个 Python 脚本、跑个 Web 项目还是没问题的。仅限 Android。\nClaude Code UI 电脑上起个 Web 服务器，手机通过浏览器访问。支持局域网直接访问，外网需要内网穿透（ngrok 之类的）。图形界面，有文件浏览器和 Git 操作界面。体验还行，就是需要穿透。\n重头戏：HAPI 全链路自建 前面那些方案要么依赖第三方服务器，要么配置复杂。HAPI 是 Happy Coder 的替代方案，设计上就是本地优先的。\nHAPI 和 Happy Coder 的区别 Happy Coder 是云端优先，你的流量过他们的中继服务器。HAPI 是本地优先，开箱就能自建中继。加密方式也不同——Happy Coder 用 WebSocket + E2E，HAPI 用 WireGuard + TLS。多模型支持方面 HAPI 也更强，除了 Claude Code 和 Codex 还支持 Gemini、OpenCode 这些。\n特性 Happy Coder HAPI 设计理念 云端优先 本地优先 加密方式 WebSocket + E2E WireGuard + TLS 多模型 Claude Code, Codex Claude, Codex, Gemini, OpenCode 访问方式 iOS/Android/Web PWA, Telegram, 更多 语音控制 没有 支持 AFK 审批 没有 支持 自建中继 麻烦 开箱即用 全链路自建架构 这是我要重点讲的。按\u0026quot;全链路自建 + 本地开发机执行任务 + VPS 只做中继\u0026quot;的思路来画：\nflowchart LR phone[\u0026#34;手机 / 浏览器 / PWA\u0026#34;] dns[\u0026#34;你的域名\u0026lt;br/\u0026gt;hapi.your.com / *.relay.your.com\u0026#34;] vps[\u0026#34;你的 VPS\u0026lt;br/\u0026gt;tunwg relay server\u0026#34;] localTunnel[\u0026#34;本地开发机\u0026lt;br/\u0026gt;tunwg client\u0026#34;] hub[\u0026#34;本地开发机\u0026lt;br/\u0026gt;HAPI Hub :3006\u0026lt;br/\u0026gt;SQLite / API / Web\u0026#34;] cli[\u0026#34;本地开发机\u0026lt;br/\u0026gt;HAPI CLI\u0026#34;] agent[\u0026#34;Claude Code / Codex / Gemini\u0026lt;br/\u0026gt;真实执行代码的 Agent\u0026#34;] repo[\u0026#34;本地项目代码\u0026lt;br/\u0026gt;文件系统 / Git / 终端\u0026#34;] phone --\u0026gt;|\u0026#34;HTTPS\u0026lt;br/\u0026gt;访问 HAPI UI\u0026#34;| dns dns --\u0026gt;|\u0026#34;解析到 VPS\u0026#34;| vps vps \u0026lt;--\u0026gt;|\u0026#34;WireGuard + TLS\u0026lt;br/\u0026gt;加密隧道\u0026#34;| localTunnel localTunnel --\u0026gt;|\u0026#34;forward\u0026lt;br/\u0026gt;http://localhost:3006\u0026#34;| hub hub \u0026lt;--\u0026gt;|\u0026#34;Socket.IO\u0026#34;| cli cli \u0026lt;--\u0026gt;|\u0026#34;包装 / 驱动\u0026#34;| agent agent \u0026lt;--\u0026gt;|\u0026#34;读写 / 执行\u0026#34;| repo hub --\u0026gt;|\u0026#34;SQLite 本地存储\u0026#34;| hub 更直观点的简化版：\n[text] 显示已折叠代码（17 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 手机 | | https://hapi.your.com v 你的 VPS | | tunwg relay，只转发加密流量 v 你的本地开发机 | | localhost:3006 v HAPI Hub | | Socket.IO v HAPI CLI -\u0026gt; Codex/Claude -\u0026gt; 本地代码仓库 核心原则 就三句话：\nVPS：只跑 tunwg relay，不存你的会话和代码 本地开发机：跑 HAPI Hub + HAPI CLI + Agent + SQLite 数据库 手机：只访问你自己的域名 VPS 就是个流量中转站，全程加密它啥也看不到。你的代码、会话、数据全在本地开发机上。\n搞起来 第一步：安装 HAPI bash 1 2 3 4 5 # 全局安装 npm install -g @twsxtd/hapi # 或者临时用 npx @twsxtd/hapi 第二步：自建 Relay 默认情况下 hapi hub --relay 会连接官方的 relay.hapi.run。你要自建的话，需要改环境变量：\nbash 1 2 3 # 设定你自己的 relay 域名和认证密钥 export HAPI_RELAY_API=relay.your.com export HAPI_RELAY_AUTH=你的密钥 然后启动：\nbash 1 hapi hub --relay 这时候 HAPI Hub 会在本地起一个服务（默认 :3006），同时自动拉起 tunwg client 连接你的 relay 服务器。\n第三步：VPS 上跑 tunwg relay VPS 上需要跑一个 tunwg relay server，负责转发加密流量。tunwg 基于 WireGuard + TLS，VPS 上只看到加密数据，解不开。\nbash 1 2 3 4 5 # VPS 上安装 tunwg # 具体安装方式参考 tunwg 文档 # 启动 relay server，绑定你的域名 tunwg serve --domain relay.your.com 域名那边配好 DNS 解析，A 记录指向 VPS IP。如果要给 HAPI Web UI 单独一个子域名（比如 hapi.your.com），配一个通配符 *.relay.your.com 指过去就行。\n第四步：VPS 安全加固 既然 VPS 是面向公网的，基本安全得做好：\nbash 1 2 3 4 5 6 7 8 9 10 11 # 只开放必要端口 ufw default deny incoming ufw default allow outgoing ufw allow ssh ufw allow 443/tcp # HTTPS ufw enable # SSH 禁用密码登录 # /etc/ssh/sshd_config # PasswordAuthentication no # PermitRootLogin no 第五步：手机端访问 浏览器打开 https://hapi.your.com，装个 PWA 就跟原生 App 一样。也支持 Telegram Mini App，在 Telegram 里直接用。\n扫个码配对设备，完事。\nHAPI 的几个亮点 无缝切换：电脑前坐下了，按任意键就切回本地控制。手机端自动变成看客模式。再站起来，手机又接管了。\nAFK 审批：离开电脑的时候 Claude 要执行一些需要审批的操作（比如写文件、跑命令），手机端会收到审批请求。不用再担心 Claude 在你不在的时候干了啥。\n多模型：不只是 Claude Code，Codex、Gemini CLI、OpenCode 都能用。HAPI CLI 本质上是个包装器，底下跑哪个 Agent 你自己选。\n语音控制：手机上对着说话就能发指令。地铁上打字不方便的时候这个真有用。\n安全性 用第三方中继服务器（包括 Happy Coder 官方的），你的流量理论上都经过它们的服务器。如果 E2E 加密实现有问题，代码和 API Key 都可能泄露。\n自建中继的好处就是这个中继是你自己的，VPS 上跑的 tunwg 只转发加密流量，它自己解不开。会话数据存在本地开发机的 SQLite 里，不过 VPS。\n代码敏感度分级建议：\n公开项目/学习代码 → 都行，第三方中继也无所谓 私人项目 → 自建中继或 SSH+Tailscale 商业代码 → 只用自建中继或 SSH+Tailscale，别碰第三方 方案对比 方案 难度 需要穿透 费用 适用场景 iOS 官方 App 简单 不需要 $20/月 快速查看、简单任务（非中国大陆） Happy Coder 较简单 不需要 免费 日常使用，扫码即用 HAPI 自建 中等 不需要 免费 多模型、本地优先、数据安全 SSH+Tailscale 较复杂 不需要 免费 专业开发，功能最完整 Termux 中等 不需要 免费 Android 本地开发 Claude Code UI 中等 需要 免费 需要 Web 界面 云端 DevBox 简单 不需要 按量付费 没有常开电脑 怎么选 想要数据安全 + 多模型 → HAPI 自建 最省心 → Happy Coder，扫码就用 最硬核 → SSH + Tailscale 中国大陆 → Happy Coder（配国内 API 中转） 没有常开电脑 → 云端 DevBox 我个人推荐 HAPI 自建。VPS 成本不高（最低配就够），域名自己控制，数据全在本地，加密隧道自己看着。比把流量交给第三方服务器心里踏实多了。\n原文内容很全面，七个方案都讲了，这里重点把 HAPI 自建的部分展开写了。想看完整的方案对比和更多细节的可以去 原文 看看\nHAPI 项目地址：github.com/tiann/hapi\n","permalink":"/posts/%E6%89%8B%E6%9C%BA%E8%BF%9C%E7%A8%8B-vibe-codinghapi-%E5%85%A8%E9%93%BE%E8%B7%AF%E8%87%AA%E5%BB%BA/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e本文转载自 \u003ca href=\"https://datawhalechina.github.io/easy-vibe/zh-cn/stage-3/core-skills/mobile-development/\"\u003eDatawhale easy-vibe 教程\u003c/a\u003e，加了自己的理解和全链路自建的部分\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"手机上写代码\"\u003e手机上写代码？\u003c/h2\u003e\n\u003cp\u003e地铁上突然想到一个 bug 怎么修，买一杯瑞排队的时候收到线上告警，躺床上不想起来但又想让 Claude 帮你跑个任务——这种场景太多了。传统 IDE 那套东西在手机上根本跑不动，虚拟键盘输入代码效率低得离谱，小屏幕连代码和终端都放不下。\u003c/p\u003e","title":"手机远程 vibe coding——HAPI 全链路自建"},{"content":" 原文来自 Datawhale 的 Easy-Vibe 教程：如何让 Claude Code 长长时间工作\n之前一直在用 Claude Code 写代码，但有一个很烦的问题——它干着干着就停了。你让它重构一个模块，它改了两个文件就跟你说\u0026quot;搞定了\u0026quot;，其实还有一堆没改。或者你让它跑测试，跑到一半报错了，它就卡在那里不动了，也不说重试一下。\n总之，最近看到了 Datawhale 的 vibe coding 教程里有一节专门讲这个，讲得挺系统的，我这里也记录一下大概，顺便加点自己的理解。\n核心问题：AI 不知道自己没干完 这个其实是所有 AI 编程工具的通病。人判断\u0026quot;干完了没\u0026quot;靠的是客观标准——测试跑通了没、功能能用不、代码质量行不行。但 AI 靠的是\u0026quot;感觉\u0026quot;——它觉得自己输出的差不多了，就停下来了。\n所以解决方案的核心思路就是：别让 AI 自己判断什么时候停，让外部系统来检查。\n具体来说就是三个问题：\n真的完成了吗？ 满足客观验收标准吗？ 有没有漏掉什么？ 如果答案都是\u0026quot;没有\u0026quot;，就把任务重新丢给它，继续干。\n方法一：While True 循环（最朴素） 五行代码搞定：\nbash 1 2 3 4 #!/bin/bash while true; do cat PROMPT.md | claude done 逻辑很简单——从 PROMPT.md 读任务，丢给 Claude，Claude 干完了退出，循环又把它拉起来继续干。Ctrl+C 手动停。\n可以，简单粗暴。但问题也很明显——它不知道啥时候该停，你忘了关它就一直循环，API 账单蹭蹭涨。\n加个安全版本的话，限制最大迭代次数就行：\nbash 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/bin/bash MAX_ITERATIONS=50 iteration=0 while true; do iteration=$((iteration + 1)) echo \u0026#34;=== 迭代 $iteration/$MAX_ITERATIONS ===\u0026#34; cat PROMPT.md | claude if [ $iteration -ge $MAX_ITERATIONS ]; then echo \u0026#34;达到最大迭代次数，停止\u0026#34; break fi sleep 5 done 方法二：Ralph Wiggum 插件（推荐） 这个是正经方案。Ralph Wiggum 是 Anthropic 官方的插件，核心机制叫 Stop Hook——Claude 想退出的时候，Hook 会拦截，检查输出里有没有你设定的完成标记。没有就重新注入任务让它继续干，有了才放它走。\n安装：\nbash 1 2 3 # 在 Claude Code 里 /plugin marketplace add anthropics/claude-code /plugin install ralph-wiggum@claude-code-plugins 用法：\nbash 1 2 3 4 /ralph-wiggum:ralph-loop \u0026#34;构建一个待办事项 API，包含 CRUD 操作、输入验证、测试。 全部完成后输出 \u0026lt;promise\u0026gt;COMPLETE\u0026lt;/promise\u0026gt;\u0026#34; \\ --max-iterations 50 \\ --completion-promise \u0026#34;COMPLETE\u0026#34; 两个关键参数：\n--max-iterations：安全阀，建议 20-100，到了就强制停 --completion-promise：完成标记，Claude 输出里出现这个字符串就算完成 原文里有个很有意思的例子——Ralph 的命名来源是《辛普森一家》里的 Ralph Wiggum，一个傻乎乎的小孩角色。澳大利亚开发者 Geoffrey Huntley 2025 年夏天写的这个脚本，两周就拿了 7000+ 的 star。有人用它一晚上跑了 6 个完整项目，有人花了 297 美刀的 API 费用就干完了价值 5 万美金的合同活。\nPrompt 怎么写 这个挺关键的。Prompt 写得烂，Ralph 循环再多次也没用。\n坏的 prompt：\u0026quot;写一个 todo API\u0026quot;——啥验收标准都没有，AI 自己都判断不了完没完。\n好的 prompt 要包含：\n分阶段的明确要求：第一阶段做 CRUD，第二阶段加验证，第三阶段写测试 客观验收标准：所有测试通过、linter 没报错、README 有 API 文档 完成标记：\u0026lt;promise\u0026gt;TODO_API_COMPLETE\u0026lt;/promise\u0026gt; 原文给了好几个模板，挑几个我觉得实用的：\n测试迁移（Jest → Vitest）：\nbash 1 2 3 4 5 6 7 8 9 10 11 12 /ralph-wiggum:ralph-loop \u0026#34; 将项目中所有测试从 Jest 迁移到 Vitest： - 保持所有测试逻辑不变 - 更新配置文件 - 替换 Jest 特有 API（jest.mock → vi.mock） - 确保所有测试通过 - 移除 Jest 相关依赖 验收标准：npm test 全部通过，package.json 中无 jest 依赖，项目能正常构建 完成后输出：\u0026lt;promise\u0026gt;VITEST_MIGRATION_COMPLETE\u0026lt;/promise\u0026gt; \u0026#34; --max-iterations 40 --completion-promise \u0026#34;VITEST_MIGRATION_COMPLETE\u0026#34; 批量加 TypeScript 类型注解：\nbash 1 2 3 4 5 6 7 8 9 10 /ralph-wiggum:ralph-loop \u0026#34; 给项目中所有函数添加 TypeScript 类型注解： - 优先处理 src/ 目录 - 为函数参数和返回值添加类型 - 避免使用 any，用具体类型或 unknown 验收标准：npm run typecheck 通过，无 @ts-ignore，代码能正常运行 完成后输出：\u0026lt;promise\u0026gt;TYPES_ADDED\u0026lt;/promise\u0026gt; \u0026#34; --max-iterations 30 --completion-promise \u0026#34;TYPES_ADDED\u0026#34; 实战案例 原文里有几个案例挺震撼的：\nY Combinator 黑客松：一个团队晚上 11 点把 6 个产品的 MVP spec 丢给 Ralph，设了 200 次迭代上限，然后去睡觉了。第二天早上起来——6 个可以 demo 的项目，API 费用 297 美刀。\nBoris Cherny（Claude Code 负责人）：用 Ralph + Opus 4.5 干了 30 天，259 个 PR，497 次 commit，加了 4 万行代码，删了 3.8 万行。100% 由 Claude Code 写的。\nCURSED 编程语言：Geoffrey Huntley 用 Ralph Loop 花了 3 个月，从零构建了一个完整的编程语言，关键字是 Z 世代的网络用语（slay、sus、based），包含完整的 LLVM 编译器和标准库。\n遗留项目重构：有人周末把一个烂摊子项目丢给 Ralph，周一来——47 次 commit，干净的代码结构，75% 测试覆盖率，完整的 API 文档。费用大约 12 美刀。\n方法三：增强版 Ralph 社区有人做了个增强版：frankbria/ralph-claude-code\n多了几个东西：\n双重退出条件：不仅要有完成标记，还要有显式的 EXIT_SIGNAL 限速：默认每小时 100 次，防止 API 账单爆炸 智能断路器：连续 5 次输出完成标记但没有实质变化，强制退出 实时仪表盘：CLI 显示迭代次数、进度、预估费用 适合生产环境用，基础版 Ralph 够用的话没必要上这个。\n方法四：Ctrl+B 后台任务 这个其实不算\u0026quot;让 AI 一直干活\u0026quot;的方案，但也很实用。Claude Code 里按 Ctrl+B 可以把当前任务推到后台，然后你继续干别的。用 /tasks 看后台任务列表。\n适合跑测试、build 这种你不想等的长时间操作。\n什么时候该用，什么时候不该用 原文给了一个很实用的判断标准，问自己三个问题：\n能定义明确的完成标准吗？（能 → 适合） 有客观的验证方法吗？（测试、build、typecheck → 适合） 这个任务需要我持续介入吗？（不需要 → 适合） 三个都是\u0026quot;不需要/有\u0026quot;，就放心交给 Ralph。\n适合的场景：测试迁移、大规模重构、框架迁移、批量加类型、提升测试覆盖率、文档生成、UI 统一。\n不适合的场景：架构决策、安全关键代码、需求模糊、探索性工作、创意设计。\n安全机制 不管用哪种方法，一定要加安全阀：\n迭代上限：--max-iterations 必须设 API 预算预警：设个 10/50/100 美刀的阈值 检查实质性变更：如果最近 5 次 commit 没有实质变化，可能在空转 bash 1 2 3 4 if [ $(git diff HEAD~5 | wc -l) -eq 0 ]; then echo \u0026#34;最近 5 次提交没有实质变化，可能陷入循环\u0026#34; exit 1 fi 这篇教程挺全的，从最朴素的 while true 循环到 Ralph 插件到增强版都讲了，还有个完整的 BBS 论坛系统的实战案例。\n晚上丢给它跑，第二天来收货的感觉肯定很好。\n","permalink":"/posts/%E8%AE%A9ai%E4%B8%80%E7%9B%B4%E5%B9%B2%E6%B4%BB%E5%88%AB%E5%81%9C/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e原文来自 Datawhale 的 Easy-Vibe 教程：\u003ca href=\"https://datawhalechina.github.io/easy-vibe/zh-cn/stage-3/core-skills/long-running-tasks/\"\u003e如何让 Claude Code 长长时间工作\u003c/a\u003e\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e之前一直在用 Claude Code 写代码，但有一个很烦的问题——它干着干着就停了。你让它重构一个模块，它改了两个文件就跟你说\u0026quot;搞定了\u0026quot;，其实还有一堆没改。或者你让它跑测试，跑到一半报错了，它就卡在那里不动了，也不说重试一下。\u003c/p\u003e","title":"让AI一直干活别停"},{"content":" 原文：AI 输出中的 ** 是怎么来的：谈中文 Markdown 强调标记的渲染问题 作者：PlatyHsu 发布时间：2026-04-17 18:01（UTC+8） 说明：这里不做全文转载，只做摘要整理和少量引用；完整内容请回原站阅读。\n最近看少数派这篇文章，刚好把一个平时经常见到、但很少认真追根溯源的问题讲透了：\n为什么 AI 明明吐出了 Markdown 的 **，结果中文里的加粗却没有生效？\n比如下面这种内容，我们大概都见过：\nmd 1 2 **重要：**请注意。 这是**「重要」**的内容。 看起来像是标准 Markdown，但不少渲染器最后会把 ** 原样显示出来，而不是变成粗体。\n这篇文章讲了什么 原文的核心观点很简单：\n早年的原版 Markdown 规则其实比较“松”，很多中文场景下反而能正常加粗。 现在更主流的实现，大多跟随 CommonMark 规范。 CommonMark 为了避免误判数学式、变量名等内容，引入了更严格的“贴合”规则。 这套规则主要按英文的分词和标点习惯设计，对中文这种不依赖空格分词的文本并不友好。 所以问题不在 AI，也不在你写错了 **，而在于很多现代 Markdown 渲染器对“强调”这件事判断得更严格了。\n为什么中文里更容易出问题 原文提到，CommonMark 在处理强调标记时，会判断一串 * 或 _ 是否满足“左侧贴合”和“右侧贴合”。\n说人话就是：\n开始的强调标记，后面最好直接跟文字，或者某些“起始型”标点。 结束的强调标记，前面最好直接跟文字，或者某些“结束型”标点。 这套思路放在英文里比较自然，因为英文大量依赖空格来分隔词语和标点位置；但到了中文里，文字之间本来就没有空格，于是像中文冒号、书名号、引号这样的场景，就很容易让强调判断失败。\n也就是说，下面这两种写法在不少 CommonMark 系实现里都可能失效：\nmd 1 2 **重要：**请注意。 这是**「重要」**的内容。 前者的问题在 ：，后者的问题在 「」 这种中文标点环境。\n原文给出的几种解决办法 1. 直接改用 HTML 标签 这是最直接、也最稳的办法：\nhtml 1 2 \u0026lt;strong\u0026gt;重要：\u0026lt;/strong\u0026gt;请注意。 这是\u0026lt;strong\u0026gt;「重要」\u0026lt;/strong\u0026gt;的内容。 优点是稳定，缺点是写起来没有纯 Markdown 那么顺手。\n2. 加空格，硬凑出 CommonMark 想要的边界 md 1 2 **重要：** 请注意。 这是 **「重要」** 的内容。 这样确实有机会让强调重新生效，但中文里硬塞空格通常不太好看，也不太自然。\n3. 用零宽空格 原文给的办法里，我觉得这个最有意思。思路是插入 U+200B ZERO WIDTH SPACE，也就是零宽空格。\n示意写法：\nmd 1 2 **重要：[ZWSP]**请注意。 这是**[ZWSP]「重要」[ZWSP]**的内容。 这里的 [ZWSP] 只是占位说明，实际字符本身是不可见的。它不占显示宽度，但可以帮助强调标记满足渲染器对边界的判断。\n4. 如果你是开发者，直接补渲染器 如果你能控制 Markdown 渲染链路，那么最彻底的办法不是教育编辑人员怎么绕规则，而是直接修规则。\n原文提到了 https://github.com/azu/markdown-cjk-friendly 这类补丁思路：在解析强调标记时，把 CJK 字符视作一种可以帮助判断边界的上下文，从而修复中文环境下的强调失效问题。\n这条路更适合站点、编辑器、知识库或 AI 产品的开发者。\n我自己的记法 如果只是平时写博客、记笔记、整理 AI 输出，我会这样选：\n要稳，就直接上 \u0026lt;strong\u0026gt;...\u0026lt;/strong\u0026gt;。 要兼顾可读性和原味 Markdown，可以试零宽空格。 如果是产品或工具链层面的问题，就别让用户背锅，直接改渲染器。 原文最后那句我很喜欢：“真正掷地有声的话是不需要靠吼的。”\n放回中文写作里也确实成立。很多时候，与其在一堆句子里到处加粗，不如直接把句式、措辞和标点写清楚。\n延伸链接 原文：AI 输出中的 ** 是怎么来的：谈中文 Markdown 强调标记的渲染问题 规范：CommonMark Spec - Emphasis and strong emphasis 项目：https://github.com/azu/markdown-cjk-friendly ","permalink":"/posts/%E4%B8%AD%E6%96%87-markdown-%E9%87%8C%E7%9A%84%E5%8A%A0%E7%B2%97%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E5%A4%B1%E6%95%88/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e原文：\u003ca href=\"https://sspai.com/post/108768\"\u003eAI 输出中的 ** 是怎么来的：谈中文 Markdown 强调标记的渲染问题\u003c/a\u003e\n作者：PlatyHsu\n发布时间：2026-04-17 18:01（UTC+8）\n说明：这里不做全文转载，只做摘要整理和少量引用；完整内容请回原站阅读。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e最近看少数派这篇文章，刚好把一个平时经常见到、但很少认真追根溯源的问题讲透了：\u003c/p\u003e","title":"中文 Markdown 里的 ** 为什么会失效"},{"content":" TL;DR 本地网络一切正常，能够正常访问外网/使用 Codex 没有问题\n远程主机：只能通过 SSH 连接，无法联网（主要是翻墙不可用，无网的情况比较少） 之前使用 Cursor 时，网络请求走本地，context 在远端，所以迁移到 Codex 和 Claude Code 会有些许不便，今天才有空处理一下 简单来说，直接设置代理即可\n本文以 VS Code 为例，因为 VS Code + Remote SSH + Codex 插件组合使用比较方便\n最近 GPT-5.4 的好用程度加上 Claude 的获取太难。以半价怒冲了 200 美元的 GPT Pro，确实非常爽。\n第一步：在 VS Code 中配置 SSH 反向转发 在本机的 VS Code 上打开命令面板（ Ctrl+Shift+P (Mac 上是 Cmd+Shift+P)） 输入 SSH: Open SSH Configuration File 找到你的配置文件（通常是 ~/.ssh/config 或 C:\\Users\\你的用户名\\.ssh\\config） 在你需要配置的服务器上添加如下操作： Clash 默认是 7890，如果使用的是 Clash Verge 默认的是 7897\n第二步：告诉远程 VS Code 使用这个代理 重新连接现在的远程服务器，打开 VS Code 设置（ Ctrl + , 或 Cmd + ,）\n注意： 在设置页面的顶部标签栏，点击 \u0026ldquo;远程 [你的服务器名]\u0026rdquo; (Remote)。如下图：\n在搜索框中输入 http.proxy，输入http://127.0.0.1:7890， 也就是默认的代理\n取消勾选 http.proxyStrictSSL，具体如下图：\n第三步：使用 重连服务器～ OK，完美 这里安装完 Codex 插件之后，选择登录什么的不用担心回调，因为在本地唤起的浏览器回调的是本地的 VS Code 会处理。所以这个登录没有问题。\n参考文章 刚开始看了一遍知乎的这篇文章，感觉搞复杂了，但是也可以作为参考，以后也有思路，L 站这位作者的就很精准，方便，一分钟搞定\n在受限网络环境下使用 VS Code Remote-SSH + 本地代理 + Codex 插件完整教程\n【教程】解决使用 Codex（Claude Code 应该也是一样）无法在没有互联网的主机上运行的问题 - 搞七捻三 - LINUX DO\n","permalink":"/posts/%E4%B8%80%E5%88%86%E9%92%9F%E8%A7%A3%E5%86%B3%E6%80%8E%E4%B9%88%E5%9C%A8%E6%B2%A1%E6%9C%89%E4%BA%92%E8%81%94%E7%BD%91%E7%9A%84%E4%B8%BB%E6%9C%BA%E4%B8%8A%E8%BF%90%E8%A1%8C-codex/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-abstract\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M4 6h16\"/\u003e\u003cpath d=\"M4 12h16\"/\u003e\u003cpath d=\"M4 18h12\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      TL;DR\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e本地网络一切正常，能够正常访问外网/使用 Codex 没有问题\u003cbr\u003e\n远程主机：只能通过 SSH 连接，无法联网（主要是翻墙不可用，无网的情况比较少）\n之前使用 Cursor 时，网络请求走本地，context 在远端，所以迁移到 Codex 和 Claude Code 会有些许不便，今天才有空处理一下\n简单来说，直接设置代理即可\u003c/p\u003e","title":"一分钟解决怎么在没有互联网的主机上运行 codex"},{"content":" Info 成为魔法师已经不知道多少年、为了魔法，年少无知的深夜不知多少次抓耳挠腮 某一天、某论坛、刷到一个路由器的推荐：H3CNX30pro，至今为止我都能背出型号，实在是第一个折腾的给到的印象太深。当时给到的教程配套了刷机教程，刷了 immortalwrt，一个裁剪后的 openwrt，虽然功能不多，但是核心功能已有，让我体验了丝滑的上网体验，但是如果家中只有这一台路由器，当魔法失效时，全家的网都会崩溃。 随后在前辈的指引下，他给了我一台 R2S，实在是太美好了，小巧的机身，极致极客的金属质感。起初我在这台完整的 openwrt 作为入网主路由，LAN 口后面带着 H3C 做 AP，但是这依旧没有解决魔法失效全家网络崩溃的问题。于是我找到了旁路由这个方案，随后由于魔法频繁失效、至最近半年无法订阅等各种问题出现，一直没有解决，所以我的 R2S 已经荒废已久。在今晚，他迎来了重生！！！\n环境情况 至今，我家里的网络拓扑如下 这里直接用 iStoreOS 的图表示了\n其中 WIFI-路由器是我的主路由，入网用以及担任 zerotier 组网 还有无线 AP 其中一条 LAN 口连接 R2S 的 LAN 口\nH3C IP：192.168.6.1 R2S IP：192.168.6.80\n以往使用时，我会将需要魔法的设备网关指定 R2S 来使用，而如果我修改了这个配置、他是对我的网络适配器做修改，也就是说当我换一个环境和网络，他连上不同的网络还用这个固定配置，就会失效（这里我没仔细研究，只是觉得不方便）这是我的第一个需求，要能自动从路由器发 IP 的时候就自动分开，但是老版本的 immortalwrt 或者 R2S 的完整 openwrt，DHCP 的 GUI 界面里面都暂时没有这种功能。分配静态地址时只能指定 IP 给指定 MAC\n所以也一直搁置着，昨晚我购买了一份更昂贵的套餐后，选择复活 R2S\n升级固件 刚开始来到 passwall2的页面，再次尝试订阅和手动导入，都没有成功。于是我推测是 Xray 那些固件太老了。于是我尝试通过页面手动更新，结果直接失败。\n那我就想找到新的 passwall2 来手动更新整个服务 Releases · Openwrt-Passwall/openwrt-passwall2\n结果是完全不行，okpg 似乎非常落后，到处都不兼容，所以没研究太多。我就决定直接升级整个 wrt\n按照 G 老师的推荐，我找到了iStoreOS download | SourceForge.net ，不得不说（AI 万岁） 配合balenaEtcher - Flash OS images to SD cards \u0026amp; USB drives进行刷机，断电拔掉内存卡\n下载好的系统镜像传入 Etcher 插入读卡器，选择读卡器 开始刷 刷完之后插上内存卡，等待灯闪烁平稳后，访问 192.168.100.1 默认密码 password 进入页面后一看服务啥都木有，先装个 passwall2，我在市场里没找到，所以就用.run 自己安装 Are-u-ok/apps/all/PassWall2_26.2.14_aarch64_a53_all_sdk_22.03.7.run at main · AUK9527/Are-u-ok · GitHub 安装 .run 安装完成后看日志有几个包没成功 几个老的 shadowsocksr-libev-ssr-xxx 包（SSR 协议的旧实现），因为它们依赖 libopenssl1.1，而 iStoreOS 24.10 系列已经切换到 OpenSSL 3.x，不再提供 libopenssl1.1。\n刷新后 发现系统已经自动重启，随后左侧菜单服务里面能看到 passwall2，导入订阅，一波通！！！！ 畅快！\n随后做了 AI 的相关分流规则，其余的暂时都还先不做了，先解决我上面的问题\n配置旁路由 IP R2S 此时连接我的 macbook，给他的 LAN 口接口进行配置 IP 地址还是设置成 192.168.6.80，网关指向主路由：192.168.6.1 然后断开网线，连上主路由，等待片刻后即可通过主路由的网访问到 R2S\nDHCP 指哪打哪 最开始我回主路由又找了一圈，还是一样的结果，没有找到相关指定IP还能指定网关的设置\n这个功能需要 dnsmasq 的 tag 功能，显然我的阉割 wrt 没有，我也懒得刷主路由（稳定点算了）\n把 DHCP 服务交给旁路由 B 1. 在主路由 A（ImmortalWrt）上关闭 DHCP 登录主路由后台（192.168.6.1） 进入 网络 → 接口 → LAN → 编辑 在 DHCP 服务器 标签页，把 忽略此接口 勾上（或直接禁用 DHCP 服务） 保存并应用 这样主路由就不再发放 IP 了，所有设备都会向旁路由请求 IP。 2. 在旁路由 B（iStoreOS）上配置 DHCP（关键部分） 登录 R2S 后台（192.168.6.80） 进入 网络 → 接口 → LAN → 编辑 基本设置（保持不变）：\n协议：静态地址 IPv4 地址：192.168.6.80 子网掩码：255.255.255.0 IPv4 网关：192.168.6.1（指向主路由 A，让所有流量先经过主路由拨号） DHCP 设置唯一授权 DHCP 服务器 → 高级设置（给普通设备用）： 在 DHCP 选项 中添加两行：\n3,192.168.6.1 （网关指向主路由） 6,192.168.6.1 （DNS 也指向主路由） Tip 注意是两行，我刚开始写在同一行也不报错，苦恼了很久\n创建标签（给需要翻墙的设备用）：\n进入 网络 → DHCP/DNS → 标签 添加一个新标签，名称填：proxy（或 t_proxy） 在这个标签的 DHCP 选项 中添加两行： 3,192.168.6.80 6,192.168.6.80 （网关和 DNS 都指向自己） 静态地址分配（绑定 MAC）：\n进入 网络 → DHCP/DNS → 静态地址分配 添加一条： 主机名称：随便填（比如 my-pc） MAC 地址：填你那台电脑的 MAC IPv4 地址：可以留空（自动分配），或填一个固定 IP（如 192.168.6.100） 标签：选择刚才的 proxy 保存并应用 这里我的 macbook 有 保护 MAC 地址的功能。所以我把真假两个都填上去了。\n在需要翻墙的电脑上：断开网络 → 重新连接（或 ipconfig /release + /renew） 用 ipconfig（Windows）检查： 默认网关应该是 192.168.6.80 DNS 也是 192.168.6.80 Hint 然后现在遇到了个倒霉的事情，在外面跑的时候，发现 Todesk 回不了家，他说我家里电脑是海外用户。回头还要配置一下\ntodesk rustdesk 无法连接 打开passwall2 开启fakeDNS 分流规则China组增加todesk域名 转发配置TCP代理方式选择 TPROXY 除此之外，暂时没搞懂为什么，但是rustdesk能连上了。todesk似乎还不稳定，还是接着留问题在这吧。\n","permalink":"/posts/r2s%E5%A4%8D%E6%B4%BB%E8%AE%B0/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-info\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4\"/\u003e\u003cpath d=\"M12 8h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Info\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e成为魔法师已经不知道多少年、为了魔法，年少无知的深夜不知多少次抓耳挠腮\n某一天、某论坛、刷到一个路由器的推荐：H3CNX30pro，至今为止我都能背出型号，实在是第一个折腾的给到的印象太深。当时给到的教程配套了刷机教程，刷了 immortalwrt，一个裁剪后的 openwrt，虽然功能不多，但是核心功能已有，让我体验了丝滑的上网体验，但是如果家中只有这一台路由器，当魔法失效时，全家的网都会崩溃。\n随后在前辈的指引下，他给了我一台 R2S，实在是太美好了，小巧的机身，极致极客的金属质感。起初我在这台完整的 openwrt 作为入网主路由，LAN 口后面带着 H3C 做 AP，但是这依旧没有解决魔法失效全家网络崩溃的问题。于是我找到了旁路由这个方案，随后由于魔法频繁失效、至最近半年无法订阅等各种问题出现，一直没有解决，所以我的 R2S 已经荒废已久。在今晚，他迎来了重生！！！\u003c/p\u003e","title":"R2S复活记"},{"content":"基础概念 CDN（Content Delivery Network / Content Distribution Network） 内容分发网络（或内容交付网络）。它是一个分布在全球各地的服务器网络，把你的网站内容（图片、JS、CSS、视频、HTML 等）缓存到离用户更近的服务器上，从而加快访问速度、减轻源站压力、提高可用性。 源站（Origin Server / 源服务器） 你自己的服务器（你例子里的服务器 A），存放网站原始内容的服务器。CDN 所有内容最初都来自这里。 边缘节点（Edge Node / PoP - Point of Presence） CDN 在全球各地部署的“边缘”服务器。用户访问时，CDN 会把请求调度到离用户最近的边缘节点。如果节点有缓存，就直接返回；没有就去源站拉取（回源）。 初次使用CDN 部署了网站，假设域名为 666.com 客户反馈访问经常断联。那是由于我的服务器在地球的另一端，所以域名直接解析到的是我的服务器IP\n由于使用的DNS服务商并没有像CF的小黄云代理我的网络，所以这个做法一来是连接不稳定、二来是暴露服务器公网地址容易被打死，所以在这个情况下，逼我去看了一下CDN\n首先看了一下阿里云的（贵，放弃了） 在热佬的社区留言发现了这个平台，Doki CDN\n配置过程 环境情况 我已有美国服务器A，域名666.com直接指向服务器A的地址，A部署nginx代理并且部署证书开启TSL\n配置CDN 我购买了一个套餐，选择了我要加速的域名 平台会给到我一个用来CNAME指向的域名，让我去DNS解析平台配置，而我为了快速上线，这里没有切换主域名的使用，而是选择加速了一个www.666.com\nDokiDoki CDN 侧完整配置步骤 登录 DokiDoki CDN 控制台（dooki.cloud 后台）：\n添加加速域名 去「CDN加速」→ 点击「添加网站」。 加速域名 填：www.666.com（强烈推荐子域名） 源站类型：选「源站 IP」→ 填服务器 A 的 IP（或者选「源站域名」填原来的域名也行）。 回源协议：建议选「协议跟随」或「HTTP」（先用 HTTP 回源最稳，等全通后再改 HTTPS）。 提交，等待审核通过（一般几分钟）。 上传证书（必须做！） 去「证书管理」或「SSL 证书」页面 → 「添加证书」/「上传证书」。 证书名称：随便起个（如 666-letsencrypt）。 证书公钥（PEM）：复制 Let\u0026rsquo;s Encrypt fullchain.pem（或 crt 文件）全部内容（从 \u0026mdash;\u0026ndash;BEGIN CERTIFICATE\u0026mdash;\u0026ndash; 到结束，包含中间证书链）。 私钥（PEM）：复制 privkey.pem（或 .key 文件）全部内容。 保存。 回到刚添加的域名配置里，找到 HTTPS 设置 → 选择刚上传的证书 → 开启 HTTPS。 获取 CNAME 在域名列表里找到 www.666.com，复制它对应的 CNAME 值 填写到DNS解析平台处，我这里www原本有一个默认的 www 的A记录，直接改了就行 Hint 用工具验证是否生效：\n命令行输入 nslookup www.666.com 大功告成\n给大家看看效果 加速前 加速后 什么叫端口跟随 这里是我的疑问，就是用户访问 http的时候 理论走的是CDN的80端口，那么他回源到服务器要访问 80还是443就由这个配置指定\n回源协议 + 端口 的设置，决定了 CDN 节点从服务器 A 拉取资源时，用什么协议和哪个端口。\n什么叫回源主机名 （也叫「回源主机头」、「随源站主机名」、「Origin Host」或「回源 HOST」）\n这是 CDN 回源时，在 HTTP 请求头里带的 Host 字段 是什么。\n我的服务器 A 上跑了网站 666.com，Nginx/Apache 等 Web 服务器是根据请求头里的 Host 来决定把请求交给哪个虚拟主机（virtual host）处理的。\n不随源（默认或跟随加速域名）： CDN 回源时，Host 头会带 www.666.com（也就是用户访问的加速域名）。 适合情况：服务器上，www.666.com 这个域名已经正确绑定了对应的网站配置。 随源主机名（跟随源站）： CDN 回源时，Host 头会带我们填的源站域名（通常是源站配置里填的那个域名或 IP 对应的主机名）。 适合情况：当源站服务器上绑定的站点域名 和 加速域名 不一样时，用这个可以让 CDN 告诉源站“我要访问的是源站上的哪个站点”。 分割线\n遇到无法回源502 问题 补充一下遇到的问题，简单来说我有一条 CDN 配置\n访问 www.666.com 会 CNAME 到 CDN 给我的域名 回源主机域名是 src.666.com:443 此时注意，src 使用的 443 会进行 SSL 校验，如果 src 部署的证书是仅 src.666.com的话 用户访问 www.666.com 用的的 www.666.com的证书到达 src 时，会握手失败，所以要在 CDN 里源站配置里面注意这个配置，我的 CDN 平台这里配置是折叠的，加上我不够细心，之前是用的通配符证书，所以没注意，迁移了一次服务器后用了二级域名的证书时一直没解决这个问题，问了 G 老师才知道这个。\n","permalink":"/posts/%E9%A6%96%E6%AC%A1%E5%B0%9D%E8%AF%95%E4%BD%BF%E7%94%A8-cdn-%E5%8A%A0%E9%80%9F%E7%BD%91%E7%AB%99%E8%AE%BF%E9%97%AE/","summary":"\u003ch3 id=\"基础概念\"\u003e基础概念\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCDN\u003c/strong\u003e（Content Delivery Network / Content Distribution Network） 内容分发网络（或内容交付网络）。它是一个分布在全球各地的服务器网络，把你的网站内容（图片、JS、CSS、视频、HTML 等）缓存到离用户更近的服务器上，从而加快访问速度、减轻源站压力、提高可用性。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e源站（Origin Server / 源服务器）\u003c/strong\u003e 你自己的服务器（你例子里的服务器 A），存放网站原始内容的服务器。CDN 所有内容最初都来自这里。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e边缘节点（Edge Node / PoP - Point of Presence）\u003c/strong\u003e CDN 在全球各地部署的“边缘”服务器。用户访问时，CDN 会把请求调度到离用户最近的边缘节点。如果节点有缓存，就直接返回；没有就去源站拉取（回源）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"初次使用cdn\"\u003e初次使用CDN\u003c/h2\u003e\n\u003cp\u003e部署了网站，假设域名为 666.com\n客户反馈访问经常断联。那是由于我的服务器在地球的另一端，所以域名直接解析到的是我的服务器IP\u003c/p\u003e","title":"首次尝试使用 CDN 加速网站访问"},{"content":" Info 昨天在 L 站逛的时候，看到一位佬友推荐的信息源，Hacker News，我英文水平没那么好，自然是少听说这些海外的信息平台，借着翻译进去看了一圈，真的非常有意思，于是又想盯他，然而此时我已经有几个平台会每天盯了。所以想到了很久之前听说过的 RSS。\nRSS是什么 RSS 是一种用于自动获取网站更新内容的订阅技术，全称是 Really Simple Syndication。它本质上是一种基于 XML 的数据格式，让网站把最新内容“推送”给订阅者，而不是你手动去刷新页面查看。\n它是怎么工作的 核心流程很简单：\n网站生成一个 RSS Feed（订阅源） 通常是一个 .xml 地址 里面包含最新文章、标题、摘要、链接等 用户用 RSS 阅读器订阅这个 Feed 常见工具： Feedly Inoreader FreshRSS（自建） 阅读器定时拉取更新 有新内容就显示给你 本次我 在对比了几个 RSS 的订阅器后选择了 FreshRSS （单纯看起来比较简单），先上手用起来，再考虑以后的优化\nFreshRSS 选择这个 FreshRSS 还有几个原因是，\n我有服务器可以满足自托管的需求 有一些信息源需要海外获取 轻量 而且支持一些插件，所以我先打通我的 V1 部署之后，未来配合一个好用的 Claw 打通 V2 的需求\n1 2 3 4 [RSS源] ---\u0026gt; [FreshRSS] ---\u0026gt; [数据库] | +--\u0026gt; Web UI +--\u0026gt; API（客户端/自定义服务） 1 FreshRSS --\u0026gt; LLM服务 --\u0026gt; 智能摘要 / 分类 / 推荐 这样未来我的Claw 可以不止收集一个免费的头条信息。而是可以从我收藏的这些订阅源里面筛选出 总结出 我想看的内容\nDocker部署 实在是简单\n[yaml] 显示已折叠代码（17 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 version: \u0026#34;3\u0026#34; services: freshrss: image: freshrss/freshrss:latest container_name: freshrss restart: always ports: - \u0026#34;8080:80\u0026#34; volumes: - ./data:/var/www/FreshRSS/data - ./extensions:/var/www/FreshRSS/extensions environment: - TZ=Asia/Shanghai docker compose up -d 即可。我简单 nginx 反代了一下，所以用域名进来就行\n配置订阅源 我对具体的协议了解不够深，我只知道订阅源是一个 xml 的内容，在配置页面进行订阅后如下，找一找都能找到这些 RSS 订阅源，这里给大家推荐一个 github 仓库 GitHub - amazingcoderpro/rss-recomanded: 值得推荐 RSS 订阅源整理，不定时持续更新 · GitHub linux.do 的 Rss 论坛无处不在的RSS - 搞七捻三 - LINUX DO 我的博客的 RSS 安落滢 blog rss\n这里比较遗憾的是，美团技术团队的文章很好看，但是似乎没有官方的 feed、我找到的几个在我部署的 FreshRSS 中无法订阅\n只能之后再研究了\n订阅后效果如下 在 PC web 端用这个阅读也没啥毛病了，但是手机还得找个单独的阅读器\n手机阅读器 我直接 google play 找了两个，最后选择FeedMe 当然大家可以了解透彻一点，关于不同阅读器和他们的特性我感觉不是很有时间一一挑选，差不多就先用着，未来不满意了再根据缺点换\n这里我刚开始用了一个叫 Feeder 的，但是我 FreshRSS 聚合后有一些标题/来源 在 Feeder 里面看不出来，所以就不用他了。而 FeedMe可以分辨，就先用他了（我对使用的逻辑不算很满意，可能和我不怎么懂也有关系，我觉得交互不够直白，小白不友好）\n阅读器推荐-知乎\n这里唯一一个配置的坑就是选择 FreshRSS 之后要输入 api 和 账号密码\napi 从我箭头处查看，我直接复制了 第一个 Google Reader compatible API 密码这里设置后用的这个密码\n参考文章 GitHub - FreshRSS/FreshRSS: A free, self-hostable news aggregator… · GitHub # 迄今感觉最好用的RSS阅读器——FreshRSS安装使用教程+RSS 订阅源\n","permalink":"/posts/freshrss-self-hosted/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-info\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4\"/\u003e\u003cpath d=\"M12 8h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Info\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e昨天在 L 站逛的时候，看到一位佬友推荐的信息源，\u003ca href=\"https://news.ycombinator.com/\"\u003eHacker News\u003c/a\u003e，我英文水平没那么好，自然是少听说这些海外的信息平台，借着翻译进去看了一圈，真的非常有意思，于是又想盯他，然而此时我已经有几个平台会每天盯了。所以想到了很久之前听说过的 RSS。\u003c/p\u003e","title":"自建 RSS 聚合——freshRSS"},{"content":"起因 上午醒来看到邮件里面有一封 netdata 发的警告 netdata 我只部署了少量的服务器，本文和这次警告没有太大关系，而是让我想到了一个很苦恼的事情。\n这个应该是 ubuntu 的自动更新机制、他在更新后需要进行重启操作，有一些驱动甚至会重新编译。\n触发原因：Ubuntu 系统最近通过 apt 安装了一些软件包更新（最常见的是 Linux 内核 linux-image 或核心库如 libc6），这些更新需要重启系统才能完全生效。Netdata 检测到系统中存在 /run/reboot-required 这个文件（以及里面的 /run/reboot-required.pkgs 列出了具体包），就触发了这个警告。\n之前有一台生产的服务器上，有用 cuda、某天 ubuntu 自动更新后、内核态和运行态版本对不齐、导致 cuda 无法使用，当时状态又不好重启、基本没什么办法，比较糟心、现在看到这个之后回想这件事情、决定搜一下文档，看看怎么关闭自动更新。\n解决办法 推荐方案1（最优）：保留自动安全更新，但黑名单内核和 NVIDIA 包（不影响其他安全补丁） 这样其他软件的安全更新继续自动装，但内核和 NVIDIA 不会自动动，避免重启和 CUDA 问题。\n编辑配置文件： Bash 1 sudo vim /etc/apt/apt.conf.d/50unattended-upgrades 在文件里找到 Unattended-Upgrade::Package-Blacklist 部分（如果没有就自己加），添加： conf 1 2 3 4 5 6 7 8 9 10 Unattended-Upgrade::Package-Blacklist { \u0026#34;linux-generic\u0026#34;; \u0026#34;linux-image-generic\u0026#34;; \u0026#34;linux-headers-generic\u0026#34;; \u0026#34;linux-image.*\u0026#34;; \u0026#34;linux-headers.*\u0026#34;; \u0026#34;nvidia-*\u0026#34;; \u0026#34;libnvidia-*\u0026#34;; \u0026#34;cuda*\u0026#34;; }; 保存后重启 unattended-upgrades 服务（或重启服务器）： bash 1 sudo systemctl restart unattended-upgrades （可选）永久锁定内核版本： bash 1 sudo apt-mark hold linux-generic linux-image-generic 以后内核更新就只在你手动 apt upgrade 时才发生，你可以在维护窗口处理 NVIDIA driver 重建（sudo ubuntu-drivers autoinstall）并重启。\n方案2（最彻底）：完全关闭自动更新 编辑：\nbash 1 sudo vim /etc/apt/apt.conf.d/20auto-upgrades 改成：\nconf 1 2 APT::Periodic::Update-Package-Lists \u0026#34;0\u0026#34;; APT::Periodic::Unattended-Upgrade \u0026#34;0\u0026#34;; 或者一键：\nbash 1 sudo dpkg-reconfigure unattended-upgrades 选择 no。\n以后你想更新就手动执行：\nbash 1 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade （建议在维护窗口做，更新完检查 /run/reboot-required 再重启）\n相关文档： Automatic updates - Ubuntu Server documentation\n","permalink":"/posts/ubuntu-automatic-updates/","summary":"\u003ch2 id=\"起因\"\u003e起因\u003c/h2\u003e\n\u003cp\u003e上午醒来看到邮件里面有一封 netdata 发的警告\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/03/5166a6f8bddc71000885d021a616a06b.png\"\u003e\n        \u003cimg alt=\"Netdata 重启警告邮件截图\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/03/5166a6f8bddc71000885d021a616a06b.png\" width=\"300\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003enetdata 我只部署了少量的服务器，本文和这次警告没有太大关系，而是让我想到了一个很苦恼的事情。\u003c/p\u003e\n\u003cp\u003e这个应该是 ubuntu 的自动更新机制、他在更新后需要进行重启操作，有一些驱动甚至会重新编译。\u003c/p\u003e\n\u003cp\u003e触发原因：Ubuntu 系统最近通过 apt 安装了一些软件包更新（最常见的是 Linux 内核 linux-image 或核心库如 libc6），这些更新需要重启系统才能完全生效。Netdata 检测到系统中存在 /run/reboot-required 这个文件（以及里面的 /run/reboot-required.pkgs 列出了具体包），就触发了这个警告。\u003c/p\u003e","title":"Ubuntu Automatic updates"},{"content":"先上图，当前效果 Info 刚换这个 mac 的时候，我用的是原生的 terminal，因为很多 remote 开发任务都是使用 vscode 来进行连接，所以当时找了个不知名教程简单配了下 oh my zsh。 之后 ssh 连接的多了后用了 terminus，对它真的是又爱又恨，功能恰好满足我的需求、外观也基本看习惯了。但是某一次 update 后 由于我没有登录账号也没有购买订阅，本地的所有 ssh 连接保存在里面的全部丢失了。这件事情还发生了不止一次。所以一怒之下找了很久其他开源替代。目前暂时使用 tabby，同时在当时切换 terminal -\u0026gt; iTerm，一个月后的今天，心血来潮试了试 Ghostty，那么按目前来说，效果满意\n安装 一键安装\nbash 1 brew install --cask ghostty 小工具\nbash 1 2 brew install btop neofetch brew install starship btop：htop/top 加强版，这个推荐 neofetch：系统信息展示工具，一次性装逼 这俩工具是示例图的右侧和下方的东西\nStarship（终端显示 Git、CPU、时间等）\n对我来讲略微有点花哨\n配置 这里我直接摘抄 树獭非懒 文中的配置，直接一键复制 就是毛玻璃紫色效果\n显示已折叠代码（52 行） 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 # --- Typography --- font-family = \u0026#34;Maple Mono NF CN\u0026#34; font-size = 14 adjust-cell-height = 2 # --- Theme and Colors --- theme = Catppuccin Mocha # --- Window and Appearance --- background-opacity = 0.85 background-blur-radius = 30 macos-titlebar-style = transparent window-padding-x = 10 window-padding-y = 8 window-save-state = always window-theme = auto # --- Cursor --- cursor-style = bar cursor-style-blink = true cursor-opacity = 0.8 # --- Mouse --- mouse-hide-while-typing = true copy-on-select = clipboard # --- Quick Terminal --- quick-terminal-position = top quick-terminal-screen = mouse quick-terminal-autohide = true quick-terminal-animation-duration = 0.15 # --- Security --- clipboard-paste-protection = true clipboard-paste-bracketed-safe = true # --- Shell Integration --- shell-integration = zsh # --- Claude 专属优化 --- # initial-command = /opt/homebrew/bin/claude initial-window = true quit-after-last-window-closed = true notify-on-command-finish = always # --- Performance --- scrollback-limit = 25000000 # --- 基础分屏（左右添加屏幕）--- keybind = cmd+d=new_split:right keybind = cmd+shift+enter=toggle_split_zoom keybind = cmd+shift+f=toggle_split_zoom ok，到这一步我已知足，日后研究字体相关（等我自己的 mac 到了再折腾）\n参考 参考文章 5 分钟打造你的“幽灵搭档”终端-Ghostty Ghostty 是由 HashiCorp 联合创始人 Mitchell - 掘金 Ghostty 折腾小记 - 阿猫的博客\n官方开源仓库 GitHub - ghostty-org/ghostty: 👻 Ghostty is a fast, feature-rich, and cross-platform terminal emulator that uses platform-native UI and GPU acceleration. · GitHub 官网 Ghostty\n","permalink":"/posts/ghostty-guide/","summary":"\u003cp\u003e先上图，当前效果\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/03/e489b2cfe72961fb6f75143717e037e2.png\"\u003e\n        \u003cimg alt=\"Ghostty 终端配置效果图\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/03/e489b2cfe72961fb6f75143717e037e2.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cdiv class=\"markdown-alert markdown-alert-info\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4\"/\u003e\u003cpath d=\"M12 8h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Info\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e刚换这个 mac 的时候，我用的是原生的 terminal，因为很多 remote 开发任务都是使用 vscode 来进行连接，所以当时找了个不知名教程简单配了下 oh my zsh。\n之后 ssh 连接的多了后用了 terminus，对它真的是又爱又恨，功能恰好满足我的需求、外观也基本看习惯了。但是某一次 update 后 由于我没有登录账号也没有购买订阅，本地的所有 ssh 连接保存在里面的全部丢失了。这件事情还发生了不止一次。所以一怒之下找了很久其他开源替代。目前暂时使用 tabby，同时在当时切换 terminal -\u0026gt; iTerm，一个月后的今天，心血来潮试了试 Ghostty，那么按目前来说，效果满意\u003c/p\u003e","title":"Ghostty食用指南"},{"content":"openclaw是什么 OpenClaw是一个AI Agent框架，和我们常使用的 Claude code、codex类似，只不过后者专注于编码领域，但是关注相关应用的应该了解有不少程序员 魔改这类cli-agent 让他们进行通用任务（背后的模型足够强大）\nOpenclaw 爆火的原因主要在于做足了周边适配，一个agent配置了基座模型后能够轻易的接入WhatsApp、telegram。让他像真-助理一样存在于你的日常对话APP中。\n同时早期的许多案例、给足了openclaw足够的权限、模型能基于shell环境基本控制整个Linux系统，基于强大的模型，做到了很多让普通人眼前一亮的事情、比如帮忙炒股、帮忙购买物品、帮忙制作视频并且上传。\n**AI Agent从来不是一个全新的概念。**但OpenClaw本身并不是人工智能——它只是一个“翻译官”和“执行官”。真正聪明的，是它背后接入的语言模型（比如GPT、Claude、Gemini等）。\n由于context window 的存在，所以LLM的输入、输出长度是有限的，每个模型的上限不同、现在比较好的模型基本上是 1M上下文\n也就是说，模型有严重的失忆症\n这就像电影《我的失忆女友》里的女主角，每天早上醒来都会忘记前一天发生的一切。她的男友只能把重要的事情写下来，让她每天早上读一遍。\n每次你和它聊天时，它都会把以下内容打包成一段超长的文字，再传给语言模型：\n你是谁（主人信息）它自己是谁（身份设定）过去所有的对话记录今天要执行的任务\n语言模型看完这一大段“剧本”后，才开始做文字接龙——于是它就接出“我是XX（身份认定），很高兴为您服务”这样的回答。\n下面是一些介绍、关于ai agent如何工作，怎么规避LLM的弊端，大部分由 openclaw 为例。\n龙虾怎么干活 Function call （最早openai的概念、翻译就是工具调用、另外几家有叫 tool use、或者tool什么的。本质上就是工具调用）\n假设你让它“打开question.txt文件，读里面的问题，把答案写到answer.txt里”。流程是这样的：\n1、你的指令传到OpenClaw，加上系统提示后发给语言模型\n2、语言模型看完指令，发现需要读文件，于是返回一条特殊指令：“请使用read工具，读取question.txt”\n3、OpenClaw看到这条指令，直接执行read工具，读取文件内容\n4、读到的内容（比如“李宏毅几班”）又被送回语言模型\n5、语言模型发现需要写答案，再返回：“请使用write工具，把‘大金’写到answer.txt”\n6、OpenClaw执行write工具，完成任务\n7、最后语言模型接出“主人，任务完成”，OpenClaw把这句话发回给你\n整个过程就像语言模型在手把手地指挥OpenClaw，而OpenClaw就像一个听话的机器人，让做什么就做什么。\n工具调用潜在的风险点：exec 接触shell环境将可以执行任何命令，如 rm -rf\n当然、这种危险指令可以黑名单、也可以让他的shell进入sandbox执行\n自建工具 Function call 由于各家的标准不同、后来出现了统一的mcp范式\n而如果一个agent在每次使用时 全量加载它的 二十个mcp，关于各个mcp的描述可能来到了十万上下文\n后面就出现了 skills 规范，可以理解为 渐进式加载提示词 ，或者说工作的标准流程\n比如 只描述，操作Excel 时需要用到 excel tool 这个skill，这个skill 存放在某某目录\n只有当模型判断当前任务需要使用excel时他才会读取该目录下 各个工具的具体用法，避免每次无关的任务也处理这些工具信息从而污染上下文\nskills可以通过指定的 skill create 遵循规范来创造。模型本身也会进行一些简单的脚本创建。\n网上已经有人建立了Skill Hub，上面有成百上千个技能可以下载。但要注意，有些恶意Skill会诱导OpenClaw下载病毒文件，所以下载前最好读一下内容。\n如下面应用中我们公司群内的 每日晨报、就是模型写了一个 get-news.py 调用相关的api，获得各个领域的头条，然后把执行结果送给模型后 模型润色输出一份发送在群里供大家查看\n子代理 subagent ，常使用 Claude code的朋友应该清楚，模型由于单次上下文有限、而一次任务可能可以拆分多个子任务，那么就出现了subagent的概念，可以主动提出让agent去调用、也可能由agent自主判断调用。\n以openclaw为例：\n比如你要比较两篇论文的方法。大OpenClaw接到任务后，可以召唤两个子OpenClaw：\n子OpenClaw A：去读论文A，做摘要子OpenClaw B：去读论文B，做摘要\n这两个子OpenClaw各自去找语言模型对话，执行搜索、下载、阅读、摘要等一系列操作。大OpenClaw则坐在原地等结果，等两个子OpenClaw把摘要送回来，再交给语言模型做比较。\n这种机制的好处是节省上下文窗口。大OpenClaw的上下文里不会出现论文全文、搜索过程这些“脏活累活”，只有最终的精简摘要，可以更专注地完成高层任务。\n最近上线的 grok 4.20beta 就是多agent集合、国内的Kimi也做过类似的（但是grok信息源更丰富强大且算力充足）甚至可以进行 20+个subagents同时开始调研、几分钟内完成 1000+个信息源的整理\n长期记忆 openclaw的处理方式就是写日记\n在OpenClaw的系统提示里，有这么一段话：“每次醒来你的记忆都会清空。为了永久保存记忆，请把它们写下来。”\n所以，当你告诉它“我的生日是3月13日”，它会觉得这件事很重要，于是调用写入工具，把“我的生日是3月13日”写进memory.md文件。\n下次它醒来时，会先读memory.md，把里面的内容放进系统提示，于是它又“记得”了自己的生日。\n需要回忆过去时，它用RAG（检索增强生成）技术：把问题转成关键词，去记忆库里搜索最相关的内容，再读出来放进上下文。\n心跳机制 以上的所有介绍中，都可以看出、每次要AI做事情、都是要我们主动发起消息他才能做出响应。为了解决这种问题，心跳机制就出现了\n每隔一段时间（比如30分钟），它自动向语言模型发一个固定指令：“读一下HEARTBEAT.md，执行里面的任务。”\n这个md文件可能是之前我们和他提到的，让他时不时检查一下邮件、或者是检查一下某某项目工作进度\n那么每30分钟被触发时、他就会在背后进行工作、完成后汇报进度\nCron Job调度 如果有一些定时任务、如每天早上七点安排我当日的穿着和携带物品、心跳机制就不够精准、甚至会导致错过事情，那么在agent内部的cron调度系统就是为了这些定时任务存在的\nContext compact 24小时运行的OpenClaw，对话记录会越来越长，迟早超出语言模型的上下文窗口。怎么办？\nOpenClaw有一个叫**“记忆压缩”的机制。**当上下文快满时，它会启动压缩：把旧的历史对话发给语言模型，让语言模型\n成一个摘要，然后用摘要替换掉原始记录。如果摘要又长了，就再压缩一次——套娃式压缩，不断精炼。\n还有更暴力的方法，比如**“软修剪”：把工具输出的长内容只保留开头和结尾，中间用省略号代替。或者“硬清除”**：直接把工具的输出换成一句话“这里曾有一段工具输出”。\n危险的原因 现在的Agent像个拿枪的小孩\n最近比较有名的**“Meta研究员删邮件事件”。**一位AI安全研究员让OpenClaw帮他整理邮件，还特意叮嘱“删除前要经过同意”。但后来他发现，OpenClaw在没有经过他同意的情况下，开始疯狂删邮件。他不断发消息说“停下”，但OpenClaw完全不理。最后他只能物理拔掉电源。\n应用场景举例 精彩案例地址awesome-openclaw-usecases/usecases at main · hesamsheikh/awesome-openclaw-usec\n由于24H运行的agent消耗token量非常庞大、我司当前部署的 openclaw 使用私有化模型。qwen3.5-flash同等级模型（qwen3.5-35b-a3b）该模型对比主流旗舰模型还有不少差异、但是在适当的配置下、也能实现强大的助力效果、以下实际贴图例子均为该模型实现。\n直接搭建应用程序 通过SSH+cron实现服务器自愈 24小时值班的AI运维、当服务器出现问题自动修复\n自主项目管理 多渠道AI客服 收件箱整理 日常用法 查询当日天气 定时每日总结头条新闻——晨间早报 联网搜索总结简报 获得浏览器权限截图页面 ","permalink":"/posts/openclaw-summary/","summary":"\u003ch2 id=\"openclaw是什么\"\u003eopenclaw是什么\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOpenClaw是一个AI Agent框架，和我们常使用的 Claude code、codex类似，只不过后者专注于编码领域，但是关注相关应用的应该了解有不少程序员 魔改这类cli-agent 让他们进行通用任务（背后的模型足够强大）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eOpenclaw 爆火的原因主要在于做足了周边适配，一个agent配置了基座模型后能够轻易的接入WhatsApp、telegram。让他像真-助理一样存在于你的日常对话APP中。\u003c/p\u003e","title":"openclaw食用小结"},{"content":" 如图所示、小猫在会 睡觉、打哈欠、挠头、追踪你的鼠标。点一下睡觉都它就会起来了，别的太花哨的又不是很喜欢。目前这个小猫的实现逻辑非常灵巧。未来希望能够搞一搞它\nGitHub - adryd325/oneko.js: Cat follow mouse real! A hacky script I wrote to put a cat on my site. · GitHub\n基于这个仓库实现，一个 oneko.js + 一张 oneko.gif 就搞定，没有太多的交互，以后仰仗肖工帮我实现一下 摸头交互功能\n可以抄我的作业如下 layouts/partials/extend_footer.html\n[html] 显示已折叠代码（19 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 {{- $neko := site.Params.neko | default dict -}} {{- $enabled := true -}} {{- if isset $neko \u0026#34;enable\u0026#34; -}} {{- $enabled = $neko.enable -}} {{- end -}} {{- $onlyHome := true -}} {{- if isset $neko \u0026#34;onlyHome\u0026#34; -}} {{- $onlyHome = $neko.onlyHome -}} {{- end -}} {{- if and $enabled (or (not $onlyHome) .IsHome) -}} {{- $script := resources.Get \u0026#34;js/neko.js\u0026#34; | js.Build (dict \u0026#34;minify\u0026#34; hugo.IsProduction) -}} {{- if not site.Params.assets.disableFingerprinting -}} {{- $script = $script | fingerprint -}} \u0026lt;script defer crossorigin=\u0026#34;anonymous\u0026#34; data-oneko-gif=\u0026#34;/hugo-neko/oneko.gif\u0026#34; src=\u0026#34;{{ $script.RelPermalink }}\u0026#34; integrity=\u0026#34;{{ $script.Data.Integrity }}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; {{- else -}} \u0026lt;script defer crossorigin=\u0026#34;anonymous\u0026#34; data-oneko-gif=\u0026#34;/hugo-neko/oneko.gif\u0026#34; src=\u0026#34;{{ $script.RelPermalink }}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; {{- end -}} {{- end -}} hugo.yaml\nyaml 1 2 3 neko: enable: true onlyHome: true assets/js\n放入 neko.js\n[javascript] 显示已折叠代码（322 行） 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 (function onekoHome() { const prefersReducedMotion = window.matchMedia(\u0026#34;(prefers-reduced-motion: reduce)\u0026#34;); const coarsePointer = window.matchMedia(\u0026#34;(pointer: coarse)\u0026#34;); const header = document.querySelector(\u0026#34;.home-info .entry-header h1\u0026#34;); if (prefersReducedMotion.matches || coarsePointer.matches || !header) { return; } let headerAnchor = header.querySelector(\u0026#34;.oneko-title-anchor\u0026#34;); if (!headerAnchor) { headerAnchor = document.createElement(\u0026#34;span\u0026#34;); headerAnchor.className = \u0026#34;oneko-title-anchor\u0026#34;; headerAnchor.style.display = \u0026#34;inline-block\u0026#34;; while (header.firstChild) { headerAnchor.appendChild(header.firstChild); } header.appendChild(headerAnchor); } const currentScript = document.currentScript; const spriteUrl = currentScript \u0026amp;\u0026amp; currentScript.dataset.onekoGif ? currentScript.dataset.onekoGif : \u0026#34;/hugo-neko/oneko.gif\u0026#34;; const nekoEl = document.createElement(\u0026#34;div\u0026#34;); let resizeObserver = null; let frameCount = 0; let idleTime = 0; let idleAnimation = \u0026#34;sleeping\u0026#34;; let idleAnimationFrame = 0; let lastFrameTimestamp = 0; let sleeping = true; let justAwake = false; let mousePosX = 0; let mousePosY = 0; let nekoPosX = 32; let nekoPosY = 32; const nekoSpeed = 10; const spriteSets = { idle: [[-3, -3]], alert: [[-7, -3]], scratchSelf: [ [-5, 0], [-6, 0], [-7, 0], ], scratchWallN: [ [0, 0], [0, -1], ], scratchWallS: [ [-7, -1], [-6, -2], ], scratchWallE: [ [-2, -2], [-2, -3], ], scratchWallW: [ [-4, 0], [-4, -1], ], tired: [[-3, -2]], sleeping: [ [-2, 0], [-2, -1], ], N: [ [-1, -2], [-1, -3], ], NE: [ [0, -2], [0, -3], ], E: [ [-3, 0], [-3, -1], ], SE: [ [-5, -1], [-5, -2], ], S: [ [-6, -3], [-7, -2], ], SW: [ [-5, -3], [-6, -1], ], W: [ [-4, -2], [-4, -3], ], NW: [ [-1, 0], [-1, -1], ], }; const clamp = (value, min, max) =\u0026gt; Math.min(Math.max(value, min), max); const placeBesideHeader = () =\u0026gt; { const headerRect = headerAnchor.getBoundingClientRect(); const preferredX = headerRect.right + 10; const preferredY = headerRect.bottom - 24 + window.scrollY; nekoPosX = clamp(preferredX, 16, document.documentElement.clientWidth - 16); nekoPosY = Math.max(16, preferredY); nekoEl.style.left = `${nekoPosX - 16}px`; nekoEl.style.top = `${nekoPosY - 16}px`; }; const setSprite = (name, frame) =\u0026gt; { const sprite = spriteSets[name][frame % spriteSets[name].length]; nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`; }; const resetIdleAnimation = () =\u0026gt; { idleAnimation = null; idleAnimationFrame = 0; }; const idle = () =\u0026gt; { idleTime += 1; if ( idleTime \u0026gt; 10 \u0026amp;\u0026amp; Math.floor(Math.random() * 200) === 0 \u0026amp;\u0026amp; idleAnimation == null ) { const availableIdleAnimations = [\u0026#34;sleeping\u0026#34;, \u0026#34;scratchSelf\u0026#34;]; if (nekoPosX \u0026lt; 32) { availableIdleAnimations.push(\u0026#34;scratchWallW\u0026#34;); } if (nekoPosY \u0026lt; 32) { availableIdleAnimations.push(\u0026#34;scratchWallN\u0026#34;); } if (nekoPosX \u0026gt; window.innerWidth - 32) { availableIdleAnimations.push(\u0026#34;scratchWallE\u0026#34;); } if (nekoPosY \u0026gt; window.innerHeight - 32) { availableIdleAnimations.push(\u0026#34;scratchWallS\u0026#34;); } idleAnimation = availableIdleAnimations[ Math.floor(Math.random() * availableIdleAnimations.length) ]; } switch (idleAnimation) { case \u0026#34;sleeping\u0026#34;: if (idleAnimationFrame \u0026lt; 8 \u0026amp;\u0026amp; !sleeping) { setSprite(\u0026#34;tired\u0026#34;, 0); break; } setSprite(\u0026#34;sleeping\u0026#34;, Math.floor(idleAnimationFrame / 4)); if (idleAnimationFrame \u0026gt; 192 \u0026amp;\u0026amp; !sleeping) { resetIdleAnimation(); } break; case \u0026#34;scratchWallN\u0026#34;: case \u0026#34;scratchWallS\u0026#34;: case \u0026#34;scratchWallE\u0026#34;: case \u0026#34;scratchWallW\u0026#34;: case \u0026#34;scratchSelf\u0026#34;: setSprite(idleAnimation, idleAnimationFrame); if (idleAnimationFrame \u0026gt; 9) { resetIdleAnimation(); } break; default: setSprite(\u0026#34;idle\u0026#34;, 0); return; } idleAnimationFrame += 1; }; const wakeUp = () =\u0026gt; { sleeping = false; justAwake = true; idleAnimation = null; idleAnimationFrame = 0; idleTime = 999; nekoPosY -= window.scrollY; nekoPosX = clamp(nekoPosX, 16, window.innerWidth - 16); nekoPosY = clamp(nekoPosY, 16, window.innerHeight - 16); nekoEl.style.left = `${nekoPosX - 16}px`; nekoEl.style.top = `${nekoPosY - 16}px`; nekoEl.style.position = \u0026#34;fixed\u0026#34;; nekoEl.style.pointerEvents = \u0026#34;none\u0026#34;; nekoEl.style.cursor = \u0026#34;default\u0026#34;; if (resizeObserver) { resizeObserver.disconnect(); } }; const frame = () =\u0026gt; { frameCount += 1; const diffX = nekoPosX - mousePosX; const diffY = nekoPosY - mousePosY; const distance = Math.sqrt(diffX ** 2 + diffY ** 2); if (!justAwake \u0026amp;\u0026amp; (distance \u0026lt; nekoSpeed || distance \u0026lt; 48 || sleeping)) { idle(); return; } idleAnimation = null; idleAnimationFrame = 0; if (idleTime \u0026gt; 1) { setSprite(\u0026#34;alert\u0026#34;, 0); idleTime = Math.min(idleTime, 7); idleTime -= 1; return; } justAwake = false; let direction = diffY / distance \u0026gt; 0.5 ? \u0026#34;N\u0026#34; : \u0026#34;\u0026#34;; direction += diffY / distance \u0026lt; -0.5 ? \u0026#34;S\u0026#34; : \u0026#34;\u0026#34;; direction += diffX / distance \u0026gt; 0.5 ? \u0026#34;W\u0026#34; : \u0026#34;\u0026#34;; direction += diffX / distance \u0026lt; -0.5 ? \u0026#34;E\u0026#34; : \u0026#34;\u0026#34;; setSprite(direction, frameCount); nekoPosX -= (diffX / distance) * nekoSpeed; nekoPosY -= (diffY / distance) * nekoSpeed; nekoPosX = clamp(nekoPosX, 16, window.innerWidth - 16); nekoPosY = clamp(nekoPosY, 16, window.innerHeight - 16); nekoEl.style.left = `${nekoPosX - 16}px`; nekoEl.style.top = `${nekoPosY - 16}px`; }; const onAnimationFrame = (timestamp) =\u0026gt; { if (!nekoEl.isConnected) { return; } if (!lastFrameTimestamp) { lastFrameTimestamp = timestamp; } if (timestamp - lastFrameTimestamp \u0026gt; 100) { lastFrameTimestamp = timestamp; frame(); } window.requestAnimationFrame(onAnimationFrame); }; nekoEl.id = \u0026#34;oneko\u0026#34;; nekoEl.ariaHidden = true; nekoEl.style.width = \u0026#34;32px\u0026#34;; nekoEl.style.height = \u0026#34;32px\u0026#34;; nekoEl.style.position = \u0026#34;absolute\u0026#34;; nekoEl.style.backgroundImage = `url(${spriteUrl})`; nekoEl.style.backgroundRepeat = \u0026#34;no-repeat\u0026#34;; nekoEl.style.imageRendering = \u0026#34;pixelated\u0026#34;; nekoEl.style.cursor = \u0026#34;pointer\u0026#34;; nekoEl.style.zIndex = \u0026#34;2147483647\u0026#34;; document.body.appendChild(nekoEl); placeBesideHeader(); document.addEventListener( \u0026#34;mousemove\u0026#34;, (event) =\u0026gt; { mousePosX = event.clientX; mousePosY = event.clientY; }, { passive: true }, ); document.addEventListener( \u0026#34;scroll\u0026#34;, () =\u0026gt; { if (sleeping) { placeBesideHeader(); } }, { passive: true }, ); window.addEventListener( \u0026#34;resize\u0026#34;, () =\u0026gt; { if (sleeping) { placeBesideHeader(); } }, { passive: true }, ); if (\u0026#34;ResizeObserver\u0026#34; in window) { resizeObserver = new ResizeObserver(() =\u0026gt; { if (sleeping) { placeBesideHeader(); } }); resizeObserver.observe(headerAnchor); } nekoEl.addEventListener(\u0026#34;click\u0026#34;, wakeUp, { once: true }); window.requestAnimationFrame(onAnimationFrame); })(); 然后把 gif 放到 static/hugo-neko/ 下\n就这么简单。完成了\n","permalink":"/posts/blog-neko-cat/","summary":"\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/03/b9a93fb9ffe3c6db301521fc9a191bb2.png\"\u003e\n        \u003cimg alt=\"博客首页显示的小猫动画\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/03/b9a93fb9ffe3c6db301521fc9a191bb2.png\" width=\"400\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/03/d21a1d091e2f70f06f32765c55d4055d.png\"\u003e\n        \u003cimg alt=\"博客页面的小猫交互效果\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/03/d21a1d091e2f70f06f32765c55d4055d.png\" width=\"400\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e如图所示、小猫在会 睡觉、打哈欠、挠头、追踪你的鼠标。点一下睡觉都它就会起来了，别的太花哨的又不是很喜欢。目前这个小猫的实现逻辑非常灵巧。未来希望能够搞一搞它\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/adryd325/oneko.js\"\u003eGitHub - adryd325/oneko.js: Cat follow mouse real! A hacky script I wrote to put a cat on my site. · GitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e基于这个仓库实现，一个 oneko.js + 一张 oneko.gif 就搞定，没有太多的交互，以后仰仗肖工帮我实现一下 摸头交互功能\u003c/p\u003e","title":"在博客养了只小猫🐱"},{"content":" Info 在Linux中创建文件或运行应用时，出现错误提示No space left on device，表明存储资源已耗尽。\n参考文章 ：诊断并解决Linux实例磁盘空间满的多种场景-云服务器 ECS-阿里云 GPT5.4 指导\n排查过程 由于该服务器是公司的主开发、测试服务器，使用人数较多经常会出现这个问题，这次统一整理一下操作过程，记录学习\n检查分区情况 首先使用 df -h 查看 哪个分区满了 df : disk free -h: human-readable，不加该参数默认会以“字节”（Bytes）为单位显示，加上后系统会自动以 G (GB)、M (MB)、K (KB) 等单位显示\nbash 1 2 3 4 5 6 7 8 9 10 11 (base) yangr@172-16-99-32-Dev:/data/huggingface_model$ df -h Filesystem Size Used Avail Use% Mounted on tmpfs 13G 29M 13G 1% /run /dev/mapper/ubuntu--vg-ubuntu--lv 501G 481G 0 100% / tmpfs 63G 3.0M 63G 1% /dev/shm tmpfs 5.0M 0 5.0M 0% /run/lock /dev/sdb1 3.6T 2.8T 622G 82% /data /dev/sda2 2.0G 252M 1.6G 14% /boot /dev/sda1 1.1G 6.1M 1.1G 1% /boot/efi tmpfs 13G 12K 13G 1% /run/user/1000 tmpfs 13G 12K 13G 1% /run/user/1002 我这里 / 分区已经 100% 此时大部分程序写入 日志或者 /tmp之类都会失效，严重影响服务器使用\n找到哪个目录最大 du 命令，同理 disk used\nbash 1 sudo du -h --max-depth=1 / | sort -hr 我的结果如下\n[bash] 显示已折叠代码（20 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 3.4T / 2.8T /data 279G /var 251G /home 36G /usr 3.6G /opt 1.4G /snap 836M /tmp 258M /boot 30M /run 21M /root 12M /etc 3.0M /dev 16K /lost+found 8.0K /backup 4.0K /srv 4.0K /mnt 4.0K /media 0 /sys 0 /proc 其中 /data 和 / 在不同磁盘，所以爆满的根源主要是\n•\t/var 约 279G •\t/home 约 251G 然后类似的命令检查这两个目录，\nbash 1 2 sudo du -h --max-depth=2 /var | sort -hr sudo du -h --max-depth=1 /home | sort -hr 一般 var 里面是 docker 占用的比较多，运行这几个命令全盘扫速度不会很快，耐心等待一下\n[bash] 显示已折叠代码（47 行） 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 (base) yangr@172-16-99-32-Dev:/data/huggingface_model$ sudo du -h --max-depth=1 /home | sort -hr 251G /home 62G /home/liz 58G /home/zhangwl 42G /home/lihao 36G /home/yangr 20G /home/xiedx 11G /home/lvxy 9.4G /home/ubuntu 8.9G /home/xiaoss 2.4G /home/chenh 1.5G /home/zhaoly 1.1G /home/hupan 748M /home/liusm 36K /home/duanxd2 28K /home/chenhao 24K /home/zhouk 24K /home/zhangn 16K /home/pengzy 16K /home/huangyf (base) yangr@172-16-99-32-Dev:/data/huggingface_model$ sudo du -h --max-depth=2 /var | sort -hr 279G /var 275G /var/lib 274G /var/lib/docker 4.0G /var/log 3.3G /var/log/journal 1.1G /var/lib/snapd 580M /var/log/harbor 294M /var/lib/apt 194M /var/lib/plocate 162M /var/lib/texmf 152M /var/cache 133M /var/cache/apt 102M /var/lib/dpkg 6.3M /var/lib/ubuntu-advantage 5.9M /var/backups 5.0M /var/lib/containerd 4.4M /var/cache/debconf 4.1M /var/lib/aspell 4.1M /var/cache/apparmor 4.0M /var/cache/snapd 3.6M /var/log/supervisor 3.5M /var/lib/command-not-found 2.4M /var/cache/man 2.3M /var/cache/fontconfig 1.7M /var/lib/nginx 1.5M /var/log/nginx 可以看到 home 目录有一些同学都比较多，以及 docker 占用的比较多\n先迁移一下 home 目录，然后用软链接\n迁移home目录 以 liz 为例\n迁移目录：\nbash 1 sudo mv /home/liz /data/home/ 然后建立链接\nbash 1 sudo ln -s /data/home/liz /home/liz 同理挪动另外几位同学的 home 目录\n迁移docker目录 原本准备按照一样的处理逻辑，用软链接来替代，但是这样子如果原本机器空间不足，而不是像我这样子需要切换磁盘的各位就没什么帮助了。这里就贴一些 docker 清理空间常用命令\n确定Docker内部资源占用情况。\n运行sudo docker system df命令，查看Size和RECLAIMABLE字段，确定文件占用情况\n清除所有已停止的容器：执行sudo docker container prune。 清除所有dangling镜像（即无tag的镜像）：执行sudo docker image prune。 清除不再使用的构建缓存：执行sudo docker builder prune。 注意、执行这些命令前，你最好知道自己在做什么\n总结 阿里云的文档写的不错，我就不要脸的把它摘抄到这里吧\n随着Linux实例上应用服务的持续运行，日志、缓存、业务数据等文件会不断累积，逐渐耗尽磁盘可用空间。一旦空间不足，新的数据将无法写入，会直接导致服务中断或功能异常。\n故障现象 在Linux实例中创建文件或运行应用时，出现错误提示No space left on device，表明存储资源已耗尽。\n问题诊断和解决方案 重要\n在操作前请创建快照备份数据，防止误操作导致数据丢失，影响业务运行。\n场景一：磁盘空间耗尽 查看磁盘使用率。\n系统下执行sudo df -h，查看各挂载点的磁盘使用情况，若Use%为100%，则说明对应空间已满。\n清理无用的文件或目录。\n使用sudo du -sh \u0026lt;目录名称\u0026gt;/*，查看指定目录下的文件及子目录的大小。若有需要，可进入目录，逐级查看占用情况。\n例如使用sudo du -sh /mnt/*，查看/mnt目录下文件及子目录占用空间的大小。\n若清理后仍空间不足，可扩容云盘。\n场景二：Inode资源耗尽 每个文件都会占用一个Inode。如果磁盘上存在大量小文件，即使磁盘空间有剩余，Inode 也可能被耗尽，导致无法新建文件。\n查看Inode使用率。\n执行命令sudo df -i，若IUse%达到100%，则表示Inode资源已耗尽。\n清理无用的文件或目录。\n可使用sudo du -sh --inodes \u0026lt;目录名称\u0026gt;/*，查看指定目录下的文件及子目录占用的Inode数量。若有需要，可进入目录，利用此命令逐级查看占用情况。\n例如使用sudo du -sh --inodes /mnt/*查看/mnt目录下文件及子目录占用空间的大小。\n若清理后Inode数仍不足，可扩容云盘。\n场景三：存在已删除未释放空间的文件 即使一个文件被删除，只要仍有进程正在使用（即持有其文件句柄），系统就不会释放其占用的磁盘空间，直至进程终止或主动关闭文件后才会被真正回收。\n安装lsof工具。\n已删除但未释放空间的文件无法通过df或du指令查看，需要利用lsof工具将其列出。\nAlibaba Cloud Linux、CentOS 1 sudo yum install -y lsof Debian、Ubuntu 1 sudo apt install -y lsof 查看已删除文件未被释放的存储空间。\n1 sudo lsof | grep delete | sort -k7 -rn | more 输出第7列为文件大小（单位Byte），累加可计算未释放空间总量。 记录占用进程的名称和PID。\n执行sudo lsof | grep delete指令，通过COMMAND和PID字段获取进程名称和进程PID。\n重启或停止相关服务。\n执行sudo ps -ef | grep \u0026lt;PID\u0026gt;，进一步确认进程用途，评估影响后重启或停止相关服务。\n重要\n重启或停止服务可能会影响业务，请谨慎评估，选择合适时间进行操作。\n场景四：挂载点被覆盖。 非空目录被其他设备挂载后，其下数据虽会被隐藏，但已打开此目录的进程仍可写入覆盖空间。此类“隐藏”空间消耗无法通过df命令观测，容易造成空间意外耗尽。\n查看重复的目录信息。\n运行sudo lsblk，查看MOUNTPOINT，记录重复挂载目录名称。\n1 sudo lsblk 1 2 3 4 5 6 7 8 9 NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT vda 253:0 0 40G 0 disk ├─vda1 253:1 0 2M 0 part ├─vda2 253:2 0 200M 0 part /boot/efi └─vda3 253:3 0 39.8G 0 part / vdb 253:16 0 40G 0 disk └─vdb1 253:17 0 40G 0 part /mnt vdc 253:32 0 40G 0 disk └─vdc1 253:33 0 40G 0 part /mnt 示例中分区vdb1和vdc1的挂载目录均为/mnt，存在挂载点被覆盖风险。\n卸载文件系统。\n重要\n卸载文件系统，可能导致依赖该路径的服务中断，请评估风险，选择合适的时间操作。\n\u0026lt;重复挂载目录\u0026gt;可通过场景四的挂载点检查获取。\n1 sudo umount \u0026lt;重复挂载目录\u0026gt; 示例中/mnt为重复挂载目录，执行sudo umount /mnt，可卸载最后挂载的设备vdc1。\n获取被覆盖挂载点的设备名称。\n运行sudo df -h，定位被覆盖挂载点的设备名称。\n1 sudo df -h 1 2 3 4 5 6 7 8 9 Filesystem Size Used Avail Use% Mounted on devtmpfs 3.7G 0 3.7G 0% /dev tmpfs 3.7G 0 3.7G 0% /dev/shm tmpfs 3.7G 524K 3.7G 1% /run tmpfs 3.7G 0 3.7G 0% /sys/fs/cgroup /dev/vda3 40G 4.5G 33G 12% / /dev/vda2 200M 5.8M 194M 3% /boot/efi /dev/vdb1 40G 40G 0 100% /mnt tmpfs 747M 0 747M 0% /run/user/0 示例中当前挂载至/mnt的分区名称为vdb1，因此被覆盖挂载点的设备名称为vdb1。\n解决磁盘空间满问题。\n清理被覆盖空间中无用的文件或目录。\n示例中，需要清理vdb1挂载的/mnt目录。\n若清理后空间仍不足，可扩容云盘后，挂载至其他空目录下使用。\n示例中，需要扩容的目标设备名称为vdb1。\n重要\n请勿将多个设备挂载至同一目录。\n多个设备挂载至相同目录，先挂载的设备空间会被隐藏，可能导致数据写入错误设备。请在后续使用中确保不同设备挂载至不同的空目录。\n场景五：Docker相关文件占用空间较大。 Docker运行过程中会产生大量中间镜像、已停止容器和构建缓存，这些对象长期积累会占用磁盘空间。\n查看Docker文件磁盘空间占用率。\n执行sudo df -h，Filesystem为overlay的Use%达到100%。\n确定Docker内部资源占用情况。\n运行sudo docker system df命令，查看Size和RECLAIMABLE字段，确定文件占用情况。\n1 sudo docker system df 1 2 3 4 5 TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 21 9 13.94GB 10.66GB（76%） Containers 9 5 30.09MB 0B（0%） Local volumes 6 6 259.9MB 0B（0%） Build Cache 0 0 0B 0B 示例中，Docker镜像占用13.94GB，其中10.66GB可回收，建议优先清理无用镜像。\n清理无用文件。\n若Docker文件无法清理，可尝试依照场景一：磁盘空间耗尽处理问题。\n清除所有已停止的容器：执行sudo docker container prune。\n清除所有dangling镜像（即无tag的镜像）：执行sudo docker image prune。\n清除不再使用的构建缓存：执行sudo docker builder prune。\n场景六：inotify watches达到上限。 执行类似sudo tail -f命令时提示tail: cannot watch '...': No space left on device，并非磁盘空间不足，而是用来跟踪文件和目录变化的inotify watches达到上限，需要提升。\n查看当前inotify watches的上限值。\n执行sudo cat /proc/sys/fs/inotify/max_user_watches命令，查看inotify watches当前的上限值。\n提升inotify watches的上限值。\n提升上限值可能导致inotify占用更多系统内存，在修改前请谨慎评估，\u0026lt;新的上限值\u0026gt;一般不建议超过524288。\n1 sudo sh -c \u0026#34;echo fs.inotify.max_user_watches=\u0026lt;新的上限值\u0026gt; \u0026gt;\u0026gt; /etc/sysctl.conf\u0026#34; 加载新配置。\n执行sudo sysctl --system加载新配置并使其生效。\n验证配置结果。\n再次执行sudo cat /proc/sys/fs/inotify/max_user_watches命令，确认已更新为预期的inotify watches上限。\n相关文档 对于海量静态文件（如图片、视频、归档）的存储需求，推荐使用对象存储OSS。\n若需要高性能、高并发的文件共享，建议使用文件存储NAS来存储文件。\n针对大规模日志采集和分析的场景，可将日志存储到日志服务SLS，便于查询日志的同时，减少存储空间占用。\n","permalink":"/posts/linux-%E5%AD%98%E5%82%A8%E7%A9%BA%E9%97%B4%E7%88%86%E6%BB%A1%E6%8E%92%E6%9F%A5%E5%A4%84%E7%90%86/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-info\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4\"/\u003e\u003cpath d=\"M12 8h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Info\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e在Linux中创建文件或运行应用时，出现错误提示No space left on device，表明存储资源已耗尽。\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\u003cp\u003e参考文章\n：\u003ca href=\"https://help.aliyun.com/zh/ecs/user-guide/resolve-the-issue-of-insufficient-disk-space-on-a-linux-instance\"\u003e诊断并解决Linux实例磁盘空间满的多种场景-云服务器 ECS-阿里云\u003c/a\u003e\nGPT5.4 指导\u003c/p\u003e\n\u003ch2 id=\"排查过程\"\u003e排查过程\u003c/h2\u003e\n\u003cp\u003e由于该服务器是公司的主开发、测试服务器，使用人数较多经常会出现这个问题，这次统一整理一下操作过程，记录学习\u003c/p\u003e","title":"Linux 存储空间爆满排查处理"},{"content":" Info 我实在是受不了这个网络问题，不能畅快的 pull image 就和便秘一样。 然而，大部分的曾经一配即可用的镜像源大多数不好用或者开始收费，这些就不多说了，总之就是不好用了，那么我就打算自建一个。\n之前是给公司打了个 Harbor，不过Harbor 所在的环境也无法连接外网、且多管理内部镜像为主。前段时间刷到一篇文章，但是一直迟迟没有行动，直到今天连文章都找不到了，那就只能自己处理了。grok 给我搜到了官方的方案Docker Registry Pull-Through Cache，但是他一次一个容器配置一个源，我也不知道我会用到多少个，于是搜了一下果然有人做了\n第一次grok 推荐的，GitHub - rpardini/docker-registry-proxy: An HTTPS Proxy for Docker providing centralized configuration and caching of any registry (quay.io, DockerHub, registry.k8s.io, ghcr.io)\n后续找到的： GitHub - dqzboy/Docker-Proxy: 🔥 🔥 🔥 自建Docker镜像加速服务，基于官方Docker Registry 一键部署Docker、K8s、Quay、Ghcr、Mcr、Nvcr等镜像加速\\管理服务。支持免服务器部署到 ClawCloud\\Render\\Koyeb\n准备一台海外服务器、我还准备了个域名，ok 一键部署，按照脚本部署.\n坑来了。没白写 由于我的粗心大意，作者 README 中提到的安装Hub-cm-ui 的安装过程被我误认为是全部的安装过程。\n正当我感慨作者大大做了一个很好的集成时开开心心把这个 ui 的地址配置了域名反代并且直接加入我的 docker 配置中，pull 了一下。\n失败后折腾了很久才意识到。这是个客户端或者说是展示用的 UI 而不是一个集成体\n所以，老实部署每一个 reg 的docker 容器并且配置反代。这下成功了\n官方文档 📝 准备工作 | Docker Proxy\nCaution 看文档 看文档 看文档，细心 细心 细心\n重要的事情说三遍\n舒服了,全部配置完后，这里搜索想要的镜像，会生成对应的加速命令\n当然，也可以直接在常用的服务器上配置一下 registry-mirrors，如图所示 修改/etc/docker/daemon.json\n","permalink":"/posts/docker%E8%87%AA%E5%BB%BA%E9%95%9C%E5%83%8F%E5%8A%A0%E9%80%9F/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-info\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4\"/\u003e\u003cpath d=\"M12 8h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Info\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e我实在是受不了这个网络问题，不能畅快的 pull image 就和便秘一样。\n然而，大部分的曾经一配即可用的镜像源大多数不好用或者开始收费，这些就不多说了，总之就是不好用了，那么我就打算自建一个。\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\u003cp\u003e之前是给公司打了个 Harbor，不过Harbor 所在的环境也无法连接外网、且多管理内部镜像为主。前段时间刷到一篇文章，但是一直迟迟没有行动，直到今天连文章都找不到了，那就只能自己处理了。grok 给我搜到了官方的方案Docker Registry Pull-Through Cache，但是他一次一个容器配置一个源，我也不知道我会用到多少个，于是搜了一下果然有人做了\u003c/p\u003e","title":"docker自建镜像加速"},{"content":"安装 OpenClaw 我也不知道啥情况，总之是三巨头各种折腾然后似乎被 openai 收入麾下了，我在第一次使用龙虾到现在他名字都换了两次了。所以把之前的清空配置，再重新安装。\nInstall | OpenClaw | The AI That Actually Does Things\n这里我使用他的 sh 脚本访问不到，于是使用 pnpm 安装，然后 onboard\n这次多了不少配置，我们选择 vLLM 并且输入我对应的信息\n这次有飞书可以选择了\n配置飞书 参考文档 Feishu - OpenClaw\n在权限页面，批量导入以下权限\n[json] 显示已折叠代码（23 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { \u0026#34;scopes\u0026#34;: { \u0026#34;tenant\u0026#34;: [ \u0026#34;aily:file:read\u0026#34;, \u0026#34;aily:file:write\u0026#34;, \u0026#34;application:application.app_message_stats.overview:readonly\u0026#34;, \u0026#34;application:application:self_manage\u0026#34;, \u0026#34;application:bot.menu:write\u0026#34;, \u0026#34;contact:user.employee_id:readonly\u0026#34;, \u0026#34;corehr:file:download\u0026#34;, \u0026#34;event:ip_list\u0026#34;, \u0026#34;im:chat.access_event.bot_p2p_chat:read\u0026#34;, \u0026#34;im:chat.members:bot_access\u0026#34;, \u0026#34;im:message\u0026#34;, \u0026#34;im:message.group_at_msg:readonly\u0026#34;, \u0026#34;im:message.p2p_msg:readonly\u0026#34;, \u0026#34;im:message:readonly\u0026#34;, \u0026#34;im:message:send_as_bot\u0026#34;, \u0026#34;im:resource\u0026#34; ], \u0026#34;user\u0026#34;: [\u0026#34;aily:file:read\u0026#34;, \u0026#34;aily:file:write\u0026#34;, \u0026#34;im:chat.access_event.bot_p2p_chat:read\u0026#34;] } } 接下来没有踩坑。按照文档，发布版本-\u0026gt;设置长连接-\u0026gt;在 openclaw 配置 APPID 和 Secret -\u0026gt; 然后飞书里审核过了找机器人发一句话\n然后把拿到的 Code 配置一下 openclaw pairing approve feishu {code} 这样子之后就可以正常问答了\n模型整体能力不是很强，后面配一下对应的 skills 看看怎么用它\n","permalink":"/posts/openclaw-feishu-integration/","summary":"\u003ch2 id=\"安装-openclaw\"\u003e安装 OpenClaw\u003c/h2\u003e\n\u003cp\u003e我也不知道啥情况，总之是三巨头各种折腾然后似乎被 openai 收入麾下了，我在第一次使用龙虾到现在他名字都换了两次了。所以把之前的清空配置，再重新安装。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://openclaws.io/install/\"\u003eInstall | OpenClaw | The AI That Actually Does Things\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e这里我使用他的 sh 脚本访问不到，于是使用 pnpm 安装，然后 onboard\u003c/p\u003e\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/02/6de7d07bb00cb146cf390c9fea81b60d.png\"\u003e\n        \u003cimg alt=\"OpenClaw 初始化界面\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/02/6de7d07bb00cb146cf390c9fea81b60d.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2026/02/08264248fcc549fa8e29e429ecf54553.png\"\u003e\n        \u003cimg alt=\"OpenClaw 模型配置界面\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2026/02/08264248fcc549fa8e29e429ecf54553.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e","title":"OpenClaw 二吃"},{"content":"Qwen3.5-35B-A3B Info Qwen3.5 系列终于发布了小杯模型，对应的在阿里云提供 API，Qwen3.5-Plus 即 Qwen3.5-397B-A17B，Qwen3.5-Flash 即 Qwen3.5-35B-A3B。\n前面几天对于 Qwen3.5-Plus 的表现，网友评论都是不错、那么这个小杯表现如何是我们比较关注的，毕竟一点资源就能跑起来了。\nQwen3.5 Usage Guide - vLLM Recipes Qwen/Qwen3.5-35B-A3B · Hugging Face\n服务器信息：\n显示已折叠代码（29 行） 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 【CPU 信息】 CPU 型号: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz CPU 核心数: 32 【系统版本】 操作系统: Ubuntu 版本: 22.04.5 LTS (Jammy Jellyfish) 内核版本: 5.15.0-168-generic 【GPU 信息】 NVIDIA GPU 检测到: GPU 0: NVIDIA GeForce RTX 3090 显存: 24576 MiB 驱动版本: 580.126.09 GPU 1: NVIDIA GeForce RTX 3090 显存: 24576 MiB 驱动版本: 580.126.09 GPU 2: NVIDIA GeForce RTX 3090 显存: 24576 MiB 驱动版本: 580.126.09 GPU 3: NVIDIA GeForce RTX 3090 显存: 24576 MiB 驱动版本: 580.126.09 GPU 4: NVIDIA GeForce RTX 3090 显存: 24576 MiB 驱动版本: 580.126.09 CUDA 版本: 13.0 部署 按照官方文档 安装最新的 vLLM，使用 vLLM 部署qwen3.5-35b-a3b，（我习惯使用 vLLM，好用、性能也很好）\n根据我的显卡资源情况，保守估计，调整 tp size 并行跑在 4 张卡上、max-model-len 设置为 128k，也是官方表示的最低限制。并发最高 32\nbash 1 vllm serve /data/huggingface_model/Qwen/Qwen3.5-35B-A3B --served-model-name qwen3.5-35b-a3b --port 13538 --tensor-parallel-size 4 --max-model-len 128000 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder --gpu-memory-utilization 0.85 --max_num_seqs 32 单请求输出速度 200+Tokens/S ，且原生支持多模态\n之前写的小脚本测一下32个并发下简单问题输出情况\n符合预期，MOE 模型的吞吐还是很满意的，但是这个思考不知道是否可开关，以及还有一个 27B 的 Dense 模型理论上应该更聪明。打算后续接入 openClaw 测试一下效果。\n","permalink":"/posts/qwen3.5-35b-a3b%E6%B5%85%E5%B0%9D/","summary":"\u003ch2 id=\"qwen35-35b-a3b\"\u003eQwen3.5-35B-A3B\u003c/h2\u003e\n\u003cdiv class=\"markdown-alert markdown-alert-info\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4\"/\u003e\u003cpath d=\"M12 8h.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Info\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003eQwen3.5 系列终于发布了小杯模型，对应的在阿里云提供 API，Qwen3.5-Plus 即 Qwen3.5-397B-A17B，Qwen3.5-Flash 即 Qwen3.5-35B-A3B。\u003c/p\u003e\n\u003cp\u003e前面几天对于 Qwen3.5-Plus 的表现，网友评论都是不错、那么这个小杯表现如何是我们比较关注的，毕竟一点资源就能跑起来了。\u003c/p\u003e","title":"Qwen3.5-35B-A3B浅尝"},{"content":" 阿里新开源了 zvec，开个小仓库浅浅试一下。\n这篇就是折腾记录，不作为任何指南、是为了给未来如果做 LLM桌面应用时，多一个 vector DB 的选择。\nGitHub - alibaba/zvec\nGitHub - Coco422/zvec-try\n怎么测的 目标很简单：先把“建库 -\u0026gt; 入库 -\u0026gt; 向量查询 -\u0026gt; 命中评估”这条链路跑通。\n我在 zvec-try 里做了这几件事：\n用 datasets/*.json 配 4 组中英混合语料 每条 query 写 expected_doc_ids，按 top-k 命中做 PASS/FAIL embedding 走第三方 API（SiliconFlow / 阿里云/自部署） 每个语料单独创建一个本地 zvec collection，方便重复跑 配环境 我还是用 uv。\nbash 1 2 uv sync cp .env.example .env .env 里填这几个值就能跑：\nbash 1 2 3 AI_BASE_URL=https://api.siliconflow.cn AI_API_KEY=sk-xxx EMBEDDING_MODEL_NAME=Qwen/Qwen3-Embedding-0.6B 代码里对 endpoint 处理了一下：\n如果你已经写到 /v1，会自动补成 /v1/embeddings 如果你直接写域名，也会自动拼接 embeddings 路径 数据格式 每个语料是一个 JSON，结构大概这样：\n[json] 显示已折叠代码（15 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { \u0026#34;name\u0026#34;: \u0026#34;consumer_tech_cn_en\u0026#34;, \u0026#34;docs\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;ctech_001\u0026#34;, \u0026#34;lang\u0026#34;: \u0026#34;zh\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;...\u0026#34; } ], \u0026#34;queries\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;ctech_q01\u0026#34;, \u0026#34;direction\u0026#34;: \u0026#34;zh-\u0026gt;zh\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;expected_doc_ids\u0026#34;: [\u0026#34;ctech_001\u0026#34;], \u0026#34;topk\u0026#34;: 3 } ] } 我这次放了 4 份语料：\nconsumer_tech.json health_sports.json travel_food.json work_learning.json 跑起来 bash 1 uv run python main.py 也可以指定参数：\nbash 1 uv run python main.py --config-dir datasets --pattern \u0026#34;*.json\u0026#34; --default-topk 3 --batch-size 32 脚本输出会按 query 打印：\nPASS/FAIL 期望文档 id 实际 topk 命中 id top1 文本截断内容 最后会汇总每个语料和整体命中率，后续换 embedding 模型时直接横向对比就行。\n对 zvec 的第一印象 API 比较直给，schema -\u0026gt; insert -\u0026gt; query 路线清楚。 本地落盘调试很方便，做小规模评估很顺手。 对我这种“先跑通再说”的场景，门槛不高。 下一步准备做啥 原本还想测试 image 或者 audio 的，暂时不支持，那就放入遥遥无期的 TODO 中 加 rerank（和纯向量召回分开看） 记录 recall@k、延迟、不同 batch size 的变化 再补一版偏业务文本的数据，不只测短句问答 ","permalink":"/posts/zvec-%E6%B5%85%E5%B0%9D/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e阿里新开源了 zvec，开个小仓库浅浅试一下。\u003c/p\u003e\n\u003cp\u003e这篇就是折腾记录，不作为任何指南、是为了给未来如果做 LLM桌面应用时，多一个 vector DB 的选择。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e\u003ca href=\"https://github.com/alibaba/zvec\"\u003eGitHub - alibaba/zvec\u003c/a\u003e\u003cbr\u003e\n\u003ca href=\"https://github.com/Coco422/zvec-try\"\u003eGitHub - Coco422/zvec-try\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"怎么测的\"\u003e怎么测的\u003c/h2\u003e\n\u003cp\u003e目标很简单：先把“建库 -\u0026gt; 入库 -\u0026gt; 向量查询 -\u0026gt; 命中评估”这条链路跑通。\u003c/p\u003e\n\u003cp\u003e我在 \u003ccode\u003ezvec-try\u003c/code\u003e 里做了这几件事：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e用 \u003ccode\u003edatasets/*.json\u003c/code\u003e 配 4 组中英混合语料\u003c/li\u003e\n\u003cli\u003e每条 query 写 \u003ccode\u003eexpected_doc_ids\u003c/code\u003e，按 top-k 命中做 PASS/FAIL\u003c/li\u003e\n\u003cli\u003eembedding 走第三方 API（SiliconFlow / 阿里云/自部署）\u003c/li\u003e\n\u003cli\u003e每个语料单独创建一个本地 zvec collection，方便重复跑\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"配环境\"\u003e配环境\u003c/h2\u003e\n\u003cp\u003e我还是用 \u003ccode\u003euv\u003c/code\u003e。\u003c/p\u003e","title":"zvec 浅尝"},{"content":"原因 我的 Open Web UI 最早的时候用的是 SQLite 数据库，用久了之后，发现性能总感觉跟不上。所以决定迁移到 Postgres 数据库里。\n脚本 用 claude opus 4.6 分析了一圈\n关键差异总结：\nauth.active: SQLite 是 INTEGER (0/1)，PG 是 BOOLEAN — 需要转换 chat.created_at/updated_at: SQLite 存的是 Unix 时间戳整数，PG 是 BIGINT — 兼容 config.created_at/updated_at: SQLite 是 DATETIME 字符串，PG 是 timestamp without time zone — 需要保持字符串格式 function/model/tool.is_active/is_global: SQLite 是 INTEGER (0/1)，PG 是 BOOLEAN 一些旧表（auth, chatidtag, file, function, model, tool）在两边都没有显式 PK 约束 以下是用到的 python 脚本，注意，本脚本只对 23f47d28bfc3 这个 ID 的镜像有效，请注意有效期。各位可以按照差不多的规则自己 下载 pg 的镜像和 sqlite 的 然后对比分析下\n[python] 显示已折叠代码（332 行） 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 #!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; Open WebUI SQLite -\u0026gt; PostgreSQL Migration Script Usage: python3 migrate_sqlite_to_pg.py \\ --sqlite /path/to/webui.db \\ --pg \u0026#34;postgresql://user:pwd@127.0.0.1:5432/postgres\u0026#34; Prerequisites: pip install psycopg2-binary Notes: - The target PostgreSQL database must already have the schema created (by running Open WebUI once with the PG connection). - This script will TRUNCATE all target tables before inserting. - Run this with the Open WebUI container STOPPED. \u0026#34;\u0026#34;\u0026#34; import argparse import json import sqlite3 import sys import time try: import psycopg2 import psycopg2.extras except ImportError: print(\u0026#34;ERROR: psycopg2 not installed. Run: pip install psycopg2-binary\u0026#34;) sys.exit(1) # Tables in dependency order (parents before children) to handle FK constraints. # Tables without FK dependencies come first. TABLES_ORDERED = [ \u0026#34;user\u0026#34;, \u0026#34;auth\u0026#34;, \u0026#34;config\u0026#34;, \u0026#34;document\u0026#34;, \u0026#34;migratehistory\u0026#34;, \u0026#34;alembic_version\u0026#34;, \u0026#34;memory\u0026#34;, \u0026#34;model\u0026#34;, \u0026#34;function\u0026#34;, \u0026#34;tool\u0026#34;, \u0026#34;skill\u0026#34;, \u0026#34;prompt\u0026#34;, \u0026#34;prompt_history\u0026#34;, \u0026#34;knowledge\u0026#34;, \u0026#34;file\u0026#34;, \u0026#34;knowledge_file\u0026#34;, \u0026#34;group\u0026#34;, \u0026#34;group_member\u0026#34;, \u0026#34;channel\u0026#34;, \u0026#34;channel_webhook\u0026#34;, \u0026#34;channel_member\u0026#34;, \u0026#34;message\u0026#34;, \u0026#34;message_reaction\u0026#34;, \u0026#34;channel_file\u0026#34;, \u0026#34;chat\u0026#34;, \u0026#34;chat_file\u0026#34;, \u0026#34;chat_message\u0026#34;, \u0026#34;chatidtag\u0026#34;, \u0026#34;tag\u0026#34;, \u0026#34;folder\u0026#34;, \u0026#34;feedback\u0026#34;, \u0026#34;note\u0026#34;, \u0026#34;api_key\u0026#34;, \u0026#34;oauth_session\u0026#34;, \u0026#34;access_grant\u0026#34;, ] # Columns that need INTEGER (0/1) -\u0026gt; BOOLEAN conversion BOOL_COLUMNS = { \u0026#34;auth\u0026#34;: [\u0026#34;active\u0026#34;], \u0026#34;function\u0026#34;: [\u0026#34;is_active\u0026#34;, \u0026#34;is_global\u0026#34;], \u0026#34;model\u0026#34;: [\u0026#34;is_active\u0026#34;], \u0026#34;chat\u0026#34;: [\u0026#34;archived\u0026#34;, \u0026#34;pinned\u0026#34;], \u0026#34;channel\u0026#34;: [\u0026#34;is_private\u0026#34;], \u0026#34;channel_member\u0026#34;: [\u0026#34;is_active\u0026#34;, \u0026#34;is_channel_muted\u0026#34;, \u0026#34;is_channel_pinned\u0026#34;], \u0026#34;chat_message\u0026#34;: [\u0026#34;done\u0026#34;], \u0026#34;folder\u0026#34;: [\u0026#34;is_expanded\u0026#34;], \u0026#34;message\u0026#34;: [\u0026#34;is_pinned\u0026#34;], \u0026#34;prompt\u0026#34;: [\u0026#34;is_active\u0026#34;], \u0026#34;skill\u0026#34;: [\u0026#34;is_active\u0026#34;], } # config table: created_at/updated_at are DATETIME strings in SQLite, # timestamp without time zone in PG — pass as-is, PG will parse them. def get_sqlite_columns(sqlite_cur, table_name): \u0026#34;\u0026#34;\u0026#34;Get column names for a SQLite table.\u0026#34;\u0026#34;\u0026#34; sqlite_cur.execute(f\u0026#39;PRAGMA table_info(\u0026#34;{table_name}\u0026#34;)\u0026#39;) return [row[1] for row in sqlite_cur.fetchall()] def get_pg_columns(pg_cur, table_name): \u0026#34;\u0026#34;\u0026#34;Get column names for a PG table.\u0026#34;\u0026#34;\u0026#34; pg_cur.execute( \u0026#34;\u0026#34;\u0026#34; SELECT column_name FROM information_schema.columns WHERE table_schema = \u0026#39;public\u0026#39; AND table_name = %s ORDER BY ordinal_position \u0026#34;\u0026#34;\u0026#34;, (table_name,), ) return [row[0] for row in pg_cur.fetchall()] def convert_row(table_name, columns, row): \u0026#34;\u0026#34;\u0026#34;Convert a SQLite row for PG insertion, handling type differences.\u0026#34;\u0026#34;\u0026#34; converted = list(row) bool_cols = BOOL_COLUMNS.get(table_name, []) for i, col in enumerate(columns): val = converted[i] # INTEGER -\u0026gt; BOOLEAN conversion if col in bool_cols and val is not None: converted[i] = bool(val) # Ensure JSON columns that are strings get passed as-is # (psycopg2 handles this correctly with Json wrapper if needed, # but PG json type accepts text strings directly) return tuple(converted) def migrate_table(sqlite_cur, pg_cur, table_name, batch_size=1000): \u0026#34;\u0026#34;\u0026#34;Migrate a single table from SQLite to PostgreSQL.\u0026#34;\u0026#34;\u0026#34; # Get columns present in both databases sqlite_cols = get_sqlite_columns(sqlite_cur, table_name) pg_cols = get_pg_columns(pg_cur, table_name) # Use intersection, preserving PG column order common_cols = [c for c in pg_cols if c in sqlite_cols] if not common_cols: print(f\u0026#34; SKIP {table_name}: no common columns found\u0026#34;) return 0 # Extra columns in either side (for info) sqlite_only = set(sqlite_cols) - set(pg_cols) pg_only = set(pg_cols) - set(sqlite_cols) if sqlite_only: print(f\u0026#34; INFO: columns only in SQLite (will be skipped): {sqlite_only}\u0026#34;) if pg_only: print(f\u0026#34; INFO: columns only in PG (will use defaults): {pg_only}\u0026#34;) # Read from SQLite col_list_sqlite = \u0026#34;, \u0026#34;.join(f\u0026#39;\u0026#34;{c}\u0026#34;\u0026#39; for c in common_cols) sqlite_cur.execute(f\u0026#39;SELECT {col_list_sqlite} FROM \u0026#34;{table_name}\u0026#34;\u0026#39;) total = 0 while True: rows = sqlite_cur.fetchmany(batch_size) if not rows: break converted_rows = [convert_row(table_name, common_cols, row) for row in rows] # Build INSERT statement col_list_pg = \u0026#34;, \u0026#34;.join(f\u0026#39;\u0026#34;{c}\u0026#34;\u0026#39; for c in common_cols) placeholders = \u0026#34;, \u0026#34;.join([\u0026#34;%s\u0026#34;] * len(common_cols)) insert_sql = f\u0026#39;INSERT INTO \u0026#34;{table_name}\u0026#34; ({col_list_pg}) VALUES ({placeholders})\u0026#39; psycopg2.extras.execute_batch(pg_cur, insert_sql, converted_rows, page_size=batch_size) total += len(rows) return total def reset_sequences(pg_cur): \u0026#34;\u0026#34;\u0026#34;Reset auto-increment sequences for tables with serial/sequence PKs.\u0026#34;\u0026#34;\u0026#34; sequences = [ (\u0026#34;config\u0026#34;, \u0026#34;config_id_seq\u0026#34;, \u0026#34;id\u0026#34;), (\u0026#34;document\u0026#34;, \u0026#34;document_id_seq\u0026#34;, \u0026#34;id\u0026#34;), (\u0026#34;migratehistory\u0026#34;, \u0026#34;migratehistory_id_seq\u0026#34;, \u0026#34;id\u0026#34;), ] for table, seq, col in sequences: pg_cur.execute(f\u0026#39;SELECT COALESCE(MAX(\u0026#34;{col}\u0026#34;), 0) FROM \u0026#34;{table}\u0026#34;\u0026#39;) max_val = pg_cur.fetchone()[0] if max_val: pg_cur.execute(f\u0026#34;SELECT setval(\u0026#39;{seq}\u0026#39;, {max_val})\u0026#34;) print(f\u0026#34; Sequence {seq} set to {max_val}\u0026#34;) def main(): parser = argparse.ArgumentParser(description=\u0026#34;Migrate Open WebUI from SQLite to PostgreSQL\u0026#34;) parser.add_argument(\u0026#34;--sqlite\u0026#34;, required=True, help=\u0026#34;Path to SQLite database file\u0026#34;) parser.add_argument(\u0026#34;--pg\u0026#34;, required=True, help=\u0026#34;PostgreSQL connection string\u0026#34;) parser.add_argument(\u0026#34;--batch-size\u0026#34;, type=int, default=500, help=\u0026#34;Batch size for inserts (default: 500)\u0026#34;) parser.add_argument(\u0026#34;--dry-run\u0026#34;, action=\u0026#34;store_true\u0026#34;, help=\u0026#34;Show what would be done without writing\u0026#34;) args = parser.parse_args() print(f\u0026#34;=== Open WebUI SQLite -\u0026gt; PostgreSQL Migration ===\u0026#34;) print(f\u0026#34;Source: {args.sqlite}\u0026#34;) print(f\u0026#34;Target: {args.pg.split(\u0026#39;@\u0026#39;)[0].split(\u0026#39;://\u0026#39;)[0]}://***@{args.pg.split(\u0026#39;@\u0026#39;)[-1]}\u0026#34;) print() # Connect to SQLite sqlite_conn = sqlite3.connect(args.sqlite) sqlite_conn.row_factory = None # return tuples sqlite_cur = sqlite_conn.cursor() # Connect to PostgreSQL pg_conn = psycopg2.connect(args.pg) pg_cur = pg_conn.cursor() # Verify both databases have the same alembic version sqlite_cur.execute(\u0026#34;SELECT version_num FROM alembic_version\u0026#34;) sqlite_ver = sqlite_cur.fetchone() pg_cur.execute(\u0026#34;SELECT version_num FROM alembic_version\u0026#34;) pg_ver = pg_cur.fetchone() if sqlite_ver and pg_ver and sqlite_ver[0] != pg_ver[0]: print(f\u0026#34;WARNING: Alembic version mismatch!\u0026#34;) print(f\u0026#34; SQLite: {sqlite_ver[0]}\u0026#34;) print(f\u0026#34; PostgreSQL: {pg_ver[0]}\u0026#34;) resp = input(\u0026#34;Continue anyway? (y/N): \u0026#34;) if resp.lower() != \u0026#34;y\u0026#34;: print(\u0026#34;Aborted.\u0026#34;) sys.exit(1) elif sqlite_ver and pg_ver: print(f\u0026#34;Alembic version match: {sqlite_ver[0]}\u0026#34;) print() if args.dry_run: print(\u0026#34;=== DRY RUN MODE ===\u0026#34;) print() # Disable FK checks and truncate all target tables (reverse order) print(\u0026#34;Preparing target database...\u0026#34;) pg_cur.execute(\u0026#34;SET session_replication_role = \u0026#39;replica\u0026#39;;\u0026#34;) # disable FK triggers for table in reversed(TABLES_ORDERED): try: if not args.dry_run: pg_cur.execute(f\u0026#39;TRUNCATE TABLE \u0026#34;{table}\u0026#34; CASCADE\u0026#39;) print(f\u0026#34; TRUNCATE {table}\u0026#34;) except Exception as e: pg_conn.rollback() pg_cur.execute(\u0026#34;SET session_replication_role = \u0026#39;replica\u0026#39;;\u0026#34;) print(f\u0026#34; TRUNCATE {table} - skipped ({e})\u0026#34;) if not args.dry_run: pg_conn.commit() print() # Migrate each table start_time = time.time() total_rows = 0 errors = [] for table in TABLES_ORDERED: try: sqlite_cur.execute(f\u0026#39;SELECT count(*) FROM \u0026#34;{table}\u0026#34;\u0026#39;) count = sqlite_cur.fetchone()[0] if count == 0: print(f\u0026#34;[{table}] 0 rows - skip\u0026#34;) continue print(f\u0026#34;[{table}] {count} rows...\u0026#34;, end=\u0026#34; \u0026#34;, flush=True) if args.dry_run: print(\u0026#34;(dry run)\u0026#34;) continue migrated = migrate_table(sqlite_cur, pg_cur, table, args.batch_size) pg_conn.commit() print(f\u0026#34;OK ({migrated} migrated)\u0026#34;) total_rows += migrated except Exception as e: pg_conn.rollback() # Re-disable FK triggers after rollback pg_cur.execute(\u0026#34;SET session_replication_role = \u0026#39;replica\u0026#39;;\u0026#34;) print(f\u0026#34;ERROR: {e}\u0026#34;) errors.append((table, str(e))) print() # Reset sequences if not args.dry_run: print(\u0026#34;Resetting sequences...\u0026#34;) try: reset_sequences(pg_cur) pg_conn.commit() except Exception as e: pg_conn.rollback() print(f\u0026#34; Sequence reset error: {e}\u0026#34;) # Re-enable FK triggers pg_cur.execute(\u0026#34;SET session_replication_role = \u0026#39;origin\u0026#39;;\u0026#34;) pg_conn.commit() elapsed = time.time() - start_time print() print(f\u0026#34;=== Migration Complete ===\u0026#34;) print(f\u0026#34;Total rows migrated: {total_rows}\u0026#34;) print(f\u0026#34;Time: {elapsed:.1f}s\u0026#34;) if errors: print(f\u0026#34;\\nErrors ({len(errors)}):\u0026#34;) for table, err in errors: print(f\u0026#34; {table}: {err}\u0026#34;) sys.exit(1) # Verification print(\u0026#34;\\n=== Verification ===\u0026#34;) for table in TABLES_ORDERED: sqlite_cur.execute(f\u0026#39;SELECT count(*) FROM \u0026#34;{table}\u0026#34;\u0026#39;) s_count = sqlite_cur.fetchone()[0] if s_count == 0: continue pg_cur.execute(f\u0026#39;SELECT count(*) FROM \u0026#34;{table}\u0026#34;\u0026#39;) p_count = pg_cur.fetchone()[0] status = \u0026#34;OK\u0026#34; if s_count == p_count else f\u0026#34;MISMATCH (sqlite={s_count}, pg={p_count})\u0026#34; print(f\u0026#34; {table}: {p_count} {status}\u0026#34;) sqlite_conn.close() pg_cur.close() pg_conn.close() print(\u0026#34;\\nDone.\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: main() 执行 执行完之后 再运行 即可\nImportant 这里要注意，volume还是要挂载回去，因为像upload的file之类的还在里面，并不在数据库里\n","permalink":"/posts/open-webui-%E8%BF%81%E7%A7%BB%E6%95%B0%E6%8D%AE%E5%BA%93/","summary":"\u003ch2 id=\"原因\"\u003e原因\u003c/h2\u003e\n\u003cp\u003e我的 Open Web UI 最早的时候用的是 SQLite 数据库，用久了之后，发现性能总感觉跟不上。所以决定迁移到 Postgres 数据库里。\u003c/p\u003e\n\u003ch2 id=\"脚本\"\u003e脚本\u003c/h2\u003e\n\u003cp\u003e用 claude opus 4.6 分析了一圈\u003c/p\u003e\n\u003cp\u003e关键差异总结：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eauth.active: SQLite 是 INTEGER (0/1)，PG 是 BOOLEAN — 需要转换\u003c/li\u003e\n\u003cli\u003echat.created_at/updated_at: SQLite 存的是 Unix 时间戳整数，PG 是 BIGINT — 兼容\u003c/li\u003e\n\u003cli\u003econfig.created_at/updated_at: SQLite 是 DATETIME 字符串，PG 是 timestamp without time zone — 需要保持字符串格式\u003c/li\u003e\n\u003cli\u003efunction/model/tool.is_active/is_global: SQLite 是 INTEGER (0/1)，PG 是 BOOLEAN\u003c/li\u003e\n\u003cli\u003e一些旧表（auth, chatidtag, file, function, model, tool）在两边都没有显式 PK 约束\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e以下是用到的 python 脚本，注意，本脚本只对 \u003ccode\u003e23f47d28bfc3\u003c/code\u003e 这个 ID 的镜像有效，请注意有效期。各位可以按照差不多的规则自己 下载 pg 的镜像和 sqlite 的 然后对比分析下\u003c/p\u003e","title":"open-webui 迁移数据库"},{"content":"一，先在本机生成 SSH key 推荐用 ed25519，短、安全、GitHub 首选。\n1 ssh-keygen -t ed25519 -C \u0026#34;your_email@example.com\u0026#34; 一路回车即可，默认会生成在：\n1 2 ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub 如果你已经有这个文件，说明之前生成过，可以直接用，不用再建。\n二，确认 ssh-agent 正在运行并加载 key 1 2 eval \u0026#34;$(ssh-agent -s)\u0026#34; ssh-add ~/.ssh/id_ed25519 三，把公钥复制出来 复制的是 .pub 文件内容。\nLinux / WSL：\n1 cat ~/.ssh/id_ed25519.pub macOS：\n1 pbcopy \u0026lt; ~/.ssh/id_ed25519.pub Windows PowerShell：\n1 Get-Content $env:USERPROFILE\\.ssh\\id_ed25519.pub 复制整行，以 ssh-ed25519 开头的那一串。\n四，在 GitHub 新建 Key 网页操作路径：\nGitHub -\u0026gt; 右上角头像 -\u0026gt; Settings -\u0026gt; SSH and GPG keys -\u0026gt; New SSH key\n填法建议： Title：\n比如\nmckj-dev-ue5 或者\nwsl-ubuntu\nKey type：\nAuthentication Key\nKey：\n把刚才复制的公钥粘进去\n点 Add SSH key。\n五，测试是否生效 1 ssh -T git@github.com 第一次会问：\n1 Are you sure you want to continue connecting (yes/no)? 输入 yes。\n如果看到类似：\n1 Hi \u0026lt;your-username\u0026gt;! You\u0026#39;ve successfully authenticated 说明已经完全 OK。\n六，常见踩坑快速排雷 1）clone 用的是 https\nSSH key 只对 git@github.com:xxx/yyy.git 生效\n可以用：\n1 git remote -v 确认是不是 git@github.com。\n2）WSL 和 Windows 是两套 key\nWSL 里生成的 key 只对 WSL 生效\nWindows 原生 git 要单独一套。\n3）权限不对\n如果 ssh 报权限问题：\n1 2 3 chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/id_ed25519.pub GitHub 的 key 本质就是\n你本机的一把 SSH 私钥\n加到 GitHub 账户里的一把公钥。\n","permalink":"/posts/github%E6%96%B0%E5%BB%BAssh-key/","summary":"\u003ch2 id=\"一先在本机生成-ssh-key\"\u003e一，先在本机生成 SSH key\u003c/h2\u003e\n\u003cp\u003e推荐用 ed25519，短、安全、GitHub 首选。\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003essh-keygen -t ed25519 -C \u0026#34;your_email@example.com\u0026#34;\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e一路回车即可，默认会生成在：\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~/.ssh/id_ed25519\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~/.ssh/id_ed25519.pub\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e如果你已经有这个文件，说明之前生成过，可以直接用，不用再建。\u003c/p\u003e\n\u003ch2 id=\"二确认-ssh-agent-正在运行并加载-key\"\u003e二，确认 ssh-agent 正在运行并加载 key\u003c/h2\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eeval \u0026#34;$(ssh-agent -s)\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003essh-add ~/.ssh/id_ed25519\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003ch2 id=\"三把公钥复制出来\"\u003e三，把公钥复制出来\u003c/h2\u003e\n\u003cp\u003e复制的是 \u003cstrong\u003e.pub 文件内容\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003eLinux / WSL：\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecat ~/.ssh/id_ed25519.pub\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003emacOS：\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003epbcopy \u0026lt; ~/.ssh/id_ed25519.pub\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003eWindows PowerShell：\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eGet-Content $env:USERPROFILE\\.ssh\\id_ed25519.pub\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e复制整行，以 \u003ccode\u003essh-ed25519\u003c/code\u003e 开头的那一串。\u003c/p\u003e","title":"Github新建SSH Key"},{"content":"前言 说是 被 WSL 给吃干抹净是不严谨的，但是这次出现的问题是公司一台开发机器。512 拆分两个盘，而我在运行 WSL Ubuntu 的过程中突然断联，说\npowershell 1 2 3 4 5 6 (base) PS C:\\Users\\mck-dev\u0026gt; wsl \u0026lt;3\u0026gt;WSL (40532 - Relay) ERROR: CreateProcessParseCommon:1003: getpwuid(1000) failed 5 \u0026lt;3\u0026gt;WSL (40532 - Relay) ERROR: ConfigUpdateLanguage:2519: fopen(/etc/default/locale) failed 5 \u0026lt;3\u0026gt;WSL (40532 - Relay) ERROR: operator():577: getpwuid(0) failed 5 \u0026lt;3\u0026gt;WSL (40532) ERROR: I/O error @util.cpp:1356 (UtilInitGroups) \u0026lt;3\u0026gt;WSL (40532 - Relay) ERROR: CreateProcessCommon:805: Create process failed 一看C盘已经不是爆红，而是彻底满了。\n这让我想起来 WSL 默认的文件系统应该也是在 C 盘（上次操作docker 给客户操作了一次），那我应该迁移一次，但是这次不是docker，怎么迁移这个文件系统\n在WSL setting中找到文件系统的菜单中只有大小的设置，那么看来没有什么GUI的工具能帮我啦。\n遇到这种问题其实很生气，因为我当时强行合盘，在我获得一个 2T的 C盘时，给我电脑造成超多碎片垃圾到现在没有清理干净。真是丧尽天良的落后设计。\n导出内容 WSL 的“默认盘符”本质上是 发行版的 ext4.vhdx 所在位置。\n只要 ext4.vhdx 在 D 盘，本质就已经迁移完成。\n他已经来到了8G，对于这台118GB的 C盘，之前又装了 Epic 开发UE5，爆满在意料之中\n执行以下命令把他导出\npowershell 1 2 3 4 5 (base) PS C:\\Users\\mck-dev\u0026gt; wsl --export Ubuntu-22.04 D:\\wsl\\ubuntu.tar 正在导出，这可能需要几分钟时间。 (0 MB) 无法启动分发。错误代码： 6，失败步骤： 2 错误代码: Wsl/Service/E_FAIL 结果失败了，我猜爆满的C盘已经没有能力启动 WSL 。那么通过 WSL 去导出文件已经不可能。那就只能先清理一部分了。\n清理了常见的 temp 和download，没有任何改善，此时我可以抛弃wsl的数据，\npowershell 1 2 wsl --shutdown wsl --unregister Ubuntu-22.04 但是我决定抢救一下。\n找到C盘根目录有一个 pyhon3.13，删除！解救 150 MB。尝试export，还是失败\n看到个 dotnet，删了，回头有要跑的同事就再下载一次吧。结果还是失败\n一怒之下，认识的全删了\n注销原发行版 wsl --unregister Ubuntu-22.04\n重新导入 wsl --import Ubuntu-22.04 D:\\wsl\\Ubuntu D:\\wsl\\ubuntu.tar --version 2\n完成后，Ubuntu 的 ext4.vhdx 就会在： D:\\wsl\\Ubuntu\\ext4.vhdx\n意外出现，WSL默认用户变化了 当我重连后，发现我的默认用户mckjue5丢了\n登录后看到的是 # 但是好在数据都还在\nbash 1 2 root@mckj-dev-ue5:/home/mckjue5/qwen3asr# grep mckjue5 /etc/passwd mckjue5:x:1000:1000:,,,:/home/mckjue5:/bin/bash 太好了，那使用这个用户登录\npowershell 1 (base) PS C:\\Users\\mck-dev\u0026gt; wsl -d Ubuntu -u mckjue5 bash 1 mckjue5@mckj-dev-ue5:/mnt/c/Users/mck-dev$ ubuntu2204.exe config --default-user mckjue5 powershell 1 (base) PS C:\\Users\\mck-dev\u0026gt; wsl bash 1 2 3 4 To run a command as administrator (user \u0026#34;root\u0026#34;), use \u0026#34;sudo \u0026lt;command\u0026gt;\u0026#34;. See \u0026#34;man sudo_root\u0026#34; for details. mckjue5@mckj-dev-ue5:/mnt/c/Users/mck-dev$ 可以，成功了。\n","permalink":"/posts/c%E7%9B%98%E8%A2%ABwsl%E5%90%83%E5%B9%B2%E6%8A%B9%E5%87%80/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e说是 被 WSL 给吃干抹净是不严谨的，但是这次出现的问题是公司一台开发机器。512 拆分两个盘，而我在运行 WSL Ubuntu 的过程中突然断联，说\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \u003cspan class=\"code-lang\"\u003epowershell\u003c/span\u003e\n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebase\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nb\"\u003ePS \u003c/span\u003e\u003cspan class=\"n\"\u003eC:\u003c/span\u003e\u003cspan class=\"p\"\u003e\\\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\u003c/span\u003e\u003cspan class=\"p\"\u003e\\\u003c/span\u003e\u003cspan class=\"nb\"\u003emck-dev\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ewsl\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"mf\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003eWSL\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mf\"\u003e40532\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003eRelay\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eERROR\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateProcessParseCommon\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e\u003cspan class=\"mf\"\u003e1003\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003egetpwuid\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mf\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003efailed\u003c/span\u003e \u003cspan class=\"mf\"\u003e5\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"mf\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003eWSL\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mf\"\u003e40532\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003eRelay\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eERROR\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eConfigUpdateLanguage\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e\u003cspan class=\"mf\"\u003e2519\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003efopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(/\u003c/span\u003e\u003cspan class=\"n\"\u003eetc\u003c/span\u003e\u003cspan class=\"p\"\u003e/\u003c/span\u003e\u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003elocale\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003efailed\u003c/span\u003e \u003cspan class=\"mf\"\u003e5\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"mf\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003eWSL\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mf\"\u003e40532\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003eRelay\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eERROR\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eoperator\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e\u003cspan class=\"mf\"\u003e577\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003egetpwuid\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mf\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003efailed\u003c/span\u003e \u003cspan class=\"mf\"\u003e5\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"mf\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003eWSL\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mf\"\u003e40532\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eERROR\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eI\u003c/span\u003e\u003cspan class=\"p\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003eO\u003c/span\u003e \u003cspan class=\"n\"\u003eerror\u003c/span\u003e \u003cspan class=\"nv\"\u003e@util\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecpp\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e\u003cspan class=\"mf\"\u003e1356\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUtilInitGroups\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"mf\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003eWSL\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mf\"\u003e40532\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003eRelay\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eERROR\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateProcessCommon\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e\u003cspan class=\"mf\"\u003e805\u003c/span\u003e\u003cspan class=\"err\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eCreate\u003c/span\u003e \u003cspan class=\"k\"\u003eprocess\u003c/span\u003e \u003cspan class=\"n\"\u003efailed\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e一看C盘已经不是爆红，而是彻底满了。\u003c/p\u003e\n\u003cp\u003e这让我想起来 WSL 默认的文件系统应该也是在 C 盘（上次操作docker 给客户操作了一次），那我应该迁移一次，但是这次不是docker，怎么迁移这个文件系统\u003c/p\u003e","title":"C盘被wsl吃干抹净"},{"content":" 手动处理图片链接太痛苦了，每次都要下载、上传、替换链接，步骤多还容易出错。写个脚本让它全自动，一劳永逸。\n问题 博客图片管理一直是个麻烦事：\n从微信公众号转载文章，图片都是外链 外链不稳定，说不定哪天就挂了 想迁移图床，手动改链接太繁琐 一不小心就忘了某个步骤，图片上传了链接没更新 原理 其实就三步：\n下载：扫描 Markdown 文件，找到所有外部图片，下载到本地 上传：把本地图片通过 PicGo 上传到图床 替换：更新 Markdown 中的图片链接 核心是用正则提取图片引用 ![](url)，然后做 URL 映射替换。\n加上缓存机制（基于文件 hash），避免重复上传。再加个备份功能，出问题能回滚。\n实现 写了三个脚本：\n1. auto_migrate.py - 一键式 最常用，自动完成所有步骤：\nbash 1 2 python3 scripts/auto_migrate.py --dry-run # 预览 python3 scripts/auto_migrate.py # 正式执行 运行前会提醒检查 PicGo 配置，避免上传到错误图床。\n2. fix_current_links.py - 修复 如果图片已上传但链接没更新，用这个：\nbash 1 python3 scripts/fix_current_links.py --yes 从缓存文件 .image_cache.json 读取映射关系，批量更新链接。\n3. blog_image_manager.py - 手动模式 需要精细控制时用，可以单独执行下载或上传。\n使用 最简单就两步：\nbash 1 2 3 4 5 # 1. 预览（首次推荐） python3 scripts/auto_migrate.py --dry-run # 2. 正式执行 python3 scripts/auto_migrate.py 出问题可以回滚：\nbash 1 python3 scripts/auto_migrate.py --rollback 备份保存在 ~/.blog_image_backups/。\n代码 脚本在我博客的 GitHub 仓库：blog/scripts at main · Coco422/blog · GitHub\n主要模块：\nauto_migrate.py - 一键自动化 fix_current_links.py - 修复链接 markdown_processor.py - Markdown 解析和替换 image_uploader.py - PicGo 上传 image_downloader.py - 图片下载 cache_manager.py - 缓存管理 效果 无需多说\n总结 懒逼推动世界进步\n写脚本的时间成本远低于每次手动处理的累积成本。\n代码不多，核心就是文件操作 + API 调用 + 字符串替换。遇到类似需求可以参考改改。\n","permalink":"/posts/%E5%85%A8%E8%87%AA%E5%8A%A8%E6%9B%BF%E6%8D%A2%E5%9B%BE%E5%BA%8A%E9%93%BE%E6%8E%A5/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e手动处理图片链接太痛苦了，每次都要下载、上传、替换链接，步骤多还容易出错。写个脚本让它全自动，一劳永逸。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"问题\"\u003e问题\u003c/h2\u003e\n\u003cp\u003e博客图片管理一直是个麻烦事：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e从微信公众号转载文章，图片都是外链\u003c/li\u003e\n\u003cli\u003e外链不稳定，说不定哪天就挂了\u003c/li\u003e\n\u003cli\u003e想迁移图床，手动改链接太繁琐\u003c/li\u003e\n\u003cli\u003e一不小心就忘了某个步骤，图片上传了链接没更新\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"原理\"\u003e原理\u003c/h2\u003e\n\u003cp\u003e其实就三步：\u003c/p\u003e","title":"全自动替换图床链接"},{"content":" 截止到写这篇文章，ClawdBot 已经火出圈了，被 Anthropic 告了一手后现在已经改名 Moltbot，🦞龙虾 bot。\n之前在我自己的服务器上安装过，但是看到很多 skills 的配置比较繁琐（我连敲代码都很少配置外部能力。）实在是没有精力搞，所以也没觉得很牛。但是最近翻出公司的老树莓派就不一样了。废物必须利用，折腾永垂不朽。\n安装 🦞bot 的作者是推崇 一键安装的，所以他们为这个努力了不少，那么理论上我应该也可以诶 curl -fsSL https://clawd.bot/install.sh | bash，果然一路畅通无阻\n这其中唯一的坑就是 minimax 的官方配置是 海外的站、而我使用的账号是国内版、所以需要修改一下 baseurl、这里当时忘记截图了，总之根据官方文档改一下 json 就行\n使用 写文时想到使用飞书、此时尚未有人做适配，我也为了浅尝就不折腾太多了，配置 tg bot 试试，\n效果图：\n叫上同事进来试了一下，没想到还真的能用。功能一切正常，简单试了一下，模型能力挺重要的，如果没有定时提醒之类的能力，agent 还是比较鸡肋，但是如果未来接入飞书、配合适当的 skills、做好持久 memory，那么个人秘书就在眼前了。\n唯一的缺点是每次运行都在持续消耗 money。\n","permalink":"/posts/clawdbot-%E5%9C%A8-%E6%A0%91%E8%8E%93%E6%B4%BE%E4%B8%8A/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e截止到写这篇文章，ClawdBot 已经火出圈了，被 Anthropic 告了一手后现在已经改名 Moltbot，🦞龙虾 bot。\u003c/p\u003e\n\u003cp\u003e之前在我自己的服务器上安装过，但是看到很多 skills 的配置比较繁琐（我连敲代码都很少配置外部能力。）实在是没有精力搞，所以也没觉得很牛。但是最近翻出公司的老树莓派就不一样了。废物必须利用，折腾永垂不朽。\u003c/p\u003e","title":"ClawdBot 在 树莓派上"},{"content":"什么是 PostGIS PostGIS 是 PostgreSQL 数据库的一个扩展，它允许在数据库中存储和操作 GIS（地理信息系统）空间数据。它为 PostgreSQL 添加了空间数据类型、索引和大量空间函数，可以进行地理分析、距离计算、空间查询等操作。 PostGIS\n安装并启用 PostGIS 在 PostgreSQL 数据库中启用 PostGIS，需要使用 SQL 命令创建扩展。示例：\nSQL 1 CREATE EXTENSION postgis; 启用后，数据库将支持空间数据类型如 geometry 和 geography。\ngeometry 与 geography 空间数据类型 坐标系统与单位 SRID 4326：WGS84 坐标系，通常用于 GPS 经纬度数据。 GEOGRAPHY 类型：采用球面/椭球面计算真实地球表面距离（单位是米）. GEOMETRY 类型：采用平面坐标计算，通常用于投影坐标系，不适合直接计算地球表面距离。 geometry geometry 是用于表示平面（笛卡尔）空间几何对象的数据类型，支持点、线、多边形等多种几何类型。空间行为依赖于坐标参考系统（SRID）。\n常见的几何类型包括：\nPOINT：单个点 LINESTRING：线段序列 POLYGON：多边形 MULTIPOINT、MULTILINESTRING、MULTIPOLYGON：复合几何类型 geography geography 是用于地理空间数据的类型，面向真实球面或椭球体，适合经纬度数据和真实地球距离的计算。使用此类型的距离和面积计算会基于地球模型返回现实世界的单位（如米）。\nSRID：空间参考系统 空间数据的坐标依赖空间参考系统（SRID）。常见的 SRID 包括：\n4326（WGS84）：基于全球 GPS 坐标，经度/纬度，常用于地理坐标。 使用 SRID 能确保空间对象在正确的参考系统下解释和计算。\n创建、标注与转换空间数据 创建几何点 Sql 1 SELECT ST_MakePoint(-121.97, 37.38); 这个函数会返回一个 geometry 类型的点对象。\n设置 SRID 几何对象默认没有空间参考，必须设置 SRID：\nsql 1 SELECT ST_SetSRID(ST_MakePoint(-121.97, 37.38), 4326); 这样这个点就有了 WGS84 坐标系统标识。\n将 geometry 转为 geography 对真实球体距离计算，常用地理类型：\nsql 1 SELECT ST_SetSRID(ST_MakePoint(-121.97, 37.38), 4326)::geography; WKT 与空间对象的文本表示 空间对象可以转换为可读的文本格式（WKT）：\nsql 1 SELECT ST_AsText(geom); 返回类似 POINT(-121.97 37.38) 的文本。\n提取几何属性 ST_X(geom)：提取点的 X 坐标（经度）。 ST_Y(geom)：提取点的 Y 坐标（纬度）。 例如：\nsql 1 SELECT ST_X(geom), ST_Y(geom) FROM points; 空间距离与范围查询 计算距离 对地理对象之间距离的计算（返回米）：\nsql 1 SELECT ST_Distance(a::geography, b::geography); 这里将 geometry 强制转换为 geography 用于真实地球距离计算。\n按距离范围查询 查找在某点 R 米以内的对象：\nPlain 1 WHERE ST_DWithin(location::geography, center::geography, R); ST_DWithin 在范围比较中常与索引结合使用提高性能。\n创建空间表 示例：创建包含 geography(Point) 类型的表\nsql 1 2 3 4 CREATE TABLE airports ( code VARCHAR(3), geog GEOGRAPHY(Point) ); 然后插入点数据：\nsql 1 INSERT INTO airports VALUES (\u0026#39;LAX\u0026#39;, \u0026#39;POINT(-118.4079 33.9434)\u0026#39;); 新插入数据的 geog 列类型为 地理点类型。\n为空间查询创建索引 创建空间索引可以显著提高查询性能。常用的是 GiST 索引：\nsql 1 CREATE INDEX idx_airports_geog ON airports USING GIST (geog); 索引对大规模空间数据的查询是关键。\n常用 PostGIS 函数 PostGIS 包含大量函数用于计算几何/地理属性、空间关系和分析。以下仅列出常见的一部分来自官方函数清单：\nST_Transform(geometry, srid)：将几何对象转换到指定的 SRID。 ST_Distance(geomA, geomB)：计算两空间对象的最小距离。 ST_DWithin(geomA, geomB, distance)：判断对象是否在指定距离内。 官方文档与参考 强烈建议学习官方手册和教程，它们包括更详细的章节和练习：\n官方手册（English）：https://postgis.net/docs/manual/ 官方入门教程（Workshops / Introduction）：https://postgis.net/workshops/postgis-intro/ 常用函数速查 函数 用途 示例 ST_MakePoint(lng, lat) 创建二维点几何对象 SELECT ST_MakePoint(-121.97, 37.38); ST_SetSRID(geom, 4326) 为几何对象设置空间参考系统 SELECT ST_SetSRID(point, 4326); ::geography 将几何类型转换为地理类型 SELECT point::geography; ST_AsText(geom) 将几何或地理对象转为 WKT 可读文本 SELECT ST_AsText(geog); ST_X(geom) 提取点的经度 SELECT ST_X(geom); ST_Y(geom) 提取点的纬度 SELECT ST_Y(geom); ST_Distance(geog1, geog2) 计算两点间距离（地理类型返回米） SELECT ST_Distance(a::geography, b::geography); ST_DWithin(geog, center, radius) 判断点是否在指定半径范围内 WHERE ST_DWithin(location::geography, center::geography, 10000); geometry 与 geography 的选择 SRID 4326：WGS84 坐标系，通常用于 GPS 经纬度数据。 GEOGRAPHY 类型：采用球面/椭球面计算真实地球表面距离（单位是米）。 GEOMETRY 类型：采用平面坐标计算，通常用于投影坐标系，不适合直接计算地球表面距离。 ","permalink":"/posts/postgis-%E6%B5%85%E5%B0%9D/","summary":"\u003ch2 id=\"什么是-postgis\"\u003e什么是 PostGIS\u003c/h2\u003e\n\u003cp\u003ePostGIS 是 PostgreSQL 数据库的一个扩展，它允许在数据库中存储和操作 GIS（地理信息系统）空间数据。它为 PostgreSQL 添加了空间数据类型、索引和大量空间函数，可以进行地理分析、距离计算、空间查询等操作。 \u003ca href=\"https://www.postgis.net/docs\"\u003ePostGIS\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"安装并启用-postgis\"\u003e安装并启用 PostGIS\u003c/h2\u003e\n\u003cp\u003e在 PostgreSQL 数据库中启用 PostGIS，需要使用 SQL 命令创建扩展。示例：\u003c/p\u003e","title":"PostGIS 入门：geometry、geography、SRID 与空间查询示例"},{"content":" 之前看到多次那种AI虚拟盘炒股大战了，今天又刷到个股市分析的项目，忍不住了，搞一个下来玩玩\n以下记录折腾过程而已\nGitHub - ZhuLinsen/daily_stock_analysis: LLM驱动的 A/H股智能分析器，多数据源行情 + 实时新闻 + Gemini 决策仪表盘 + 多渠道推送，零成本，纯白嫖，定时运行\n我就不搞actions了，反正有服务器\n第一步先拉仓库下来 配环境 随后我最近在学uv，让uv接管依赖管理。直接 uv add -r requirements.txt\n然按照作者的环境配置指南，配上gemini的连接方式，由于没看到在哪里自定义gemini的调用方式，进代码一看，发现作者使用的是google.generativeai包，该包已被弃用，虽然还能用，但是我改成了使用 google.genai\nanalyzer.py 中 _init_model 方法更新成如下\n[python] 显示已折叠代码（58 行） 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 def _init_model(self) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; 初始化 Gemini 模型 配置： - 使用 gemini-3-flash-preview 或 gemini-2.5-flash 模型 - 不启用 Google Search（使用外部 Tavily/SerpAPI 搜索） - 支持自定义 endpoint（通过 GEMINI_API_ENDPOINT 环境变量） Note: 使用新的 google.genai 包（旧的 google.generativeai 已弃用） \u0026#34;\u0026#34;\u0026#34; try: from google import genai from google.genai import types # 从配置获取参数 config = get_config() model_name = config.gemini_model fallback_model = config.gemini_model_fallback # 构建客户端配置 client_kwargs = { \u0026#39;api_key\u0026#39;: self._api_key, } # 配置自定义 endpoint（如果设置） if config.gemini_api_endpoint: logger.info(f\u0026#34;使用自定义 Gemini Endpoint: {config.gemini_api_endpoint}\u0026#34;) client_kwargs[\u0026#39;http_options\u0026#39;] = { \u0026#39;baseUrl\u0026#39;: config.gemini_api_endpoint, # 注意：参数名是 baseUrl，不是 api_endpoint } # 如果配置了 API 版本，也添加进去 if config.gemini_api_version: client_kwargs[\u0026#39;http_options\u0026#39;][\u0026#39;apiVersion\u0026#39;] = config.gemini_api_version # 注意：参数名是 apiVersion # 创建 Gemini 客户端 self._genai_client = genai.Client(**client_kwargs) # 尝试初始化主模型 try: # 新版 API 不再使用 GenerativeModel，而是直接通过 client.models.generate_content # 这里我们只需要记录模型名称 self._current_model_name = model_name self._using_fallback = False self._model = self._genai_client # 保存客户端引用 logger.info(f\u0026#34;Gemini 模型初始化成功 (模型: {model_name})\u0026#34;) except Exception as model_error: # 尝试备选模型 logger.warning(f\u0026#34;主模型 {model_name} 初始化失败: {model_error}，将尝试备选模型 {fallback_model}\u0026#34;) self._current_model_name = fallback_model self._using_fallback = True self._model = self._genai_client logger.info(f\u0026#34;Gemini 备选模型配置完成 (模型: {fallback_model})\u0026#34;) except Exception as e: logger.error(f\u0026#34;Gemini 模型初始化失败: {e}\u0026#34;) self._model = None self._genai_client = None config加上\npython 1 2 gemini_api_endpoint: Optional[str] = None # 自定义 API Endpoint（如使用代理或第三方兼容服务） gemini_api_version: str = \u0026#34;v1beta\u0026#34; # API 版本 环境变量配上，同时依赖 uv add google-genai 即可\n随后去注册了 Tushare Pro、Tavily、BOCHA\n还是这个 Tushare 最神奇了，第一次接触这种数据平台。\n随后按照指示运行代码后，过一会就能看到报告发给了我的飞书\n分析一下我套牢的股票，可以，比我厉害。周一开盘拿他分析一下然后操作操作[Doge]\n","permalink":"/posts/%E6%B5%85%E5%B0%9Dai%E5%B8%AE%E6%88%91%E5%88%86%E6%9E%90%E8%82%A1%E7%A5%A8/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e之前看到多次那种AI虚拟盘炒股大战了，今天又刷到个股市分析的项目，忍不住了，搞一个下来玩玩\u003c/p\u003e\n\u003cp\u003e以下记录折腾过程而已\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e\u003ca href=\"https://github.com/ZhuLinsen/daily_stock_analysis\"\u003eGitHub - ZhuLinsen/daily_stock_analysis: LLM驱动的 A/H股智能分析器，多数据源行情 + 实时新闻 + Gemini 决策仪表盘 + 多渠道推送，零成本，纯白嫖，定时运行\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e我就不搞actions了，反正有服务器\u003c/p\u003e","title":"浅尝AI帮我分析股票"},{"content":" 手头有个项目，涉及到 app 端蓝牙连接。硬件开发伙伴给到了 BLE 协议，完全一头雾水\n最近忙的没空推进 Go 的学习，想着博客至少不能落下更新，不管怎么样也要逼自己写一篇水文。\n什么是 BLE 首先得知道什么是蓝牙（Bluetooth） 小时候肯定有人好奇为什么要叫 Bluetooth，无奈好奇心还不够，只停留在发起 why，今天才是真的解惑。\n不出意外是和人名有关，他是Harald Bluetooth（哈拉尔德·蓝牙王） 他是丹麦国王，功绩差不多是统一了分裂的丹麦与挪威。90 年代蓝牙技术的研发小组以其名号期许新技术能集成各大资通品牌的标准。蓝牙的 logo 也是如此 卢恩字母 （Hagall，ᚼ）和 （Bjarkan，ᛒ）的组合，也就是Harald Blåtand的首字母HB的合写。\n蓝牙技术规范由蓝牙技术联盟 (Bluetooth Special Interest Group, SIG) 制定，版本现如今已经更新到 第六代了\n其中第四代开始，支持了本文的主角，也就是蓝牙低功耗 (Bluetooth Low Energy, BLE)\n目前蓝牙最为普遍使用的有两种规格：\n蓝牙基础率/增强数据率 (Bluetooth Basic Rate/Enhanced Data Rate, BR/EDR): 也称为经典蓝牙。常用在对数据传输带宽有一定要求的场景上，比如需要传输音频数据的蓝牙音箱、蓝牙耳机等；\n蓝牙低功耗 (Bluetooth Low Energy, BLE): 从蓝牙 4.0 起支持的协议，特点就是功耗极低、传输速度更快，常用在对续航要求较高且只需小数据量传输的各种智能电子产品中，比如智能穿戴设备、智能家电、传感器等，应用场景广泛。\nBLE 大概是什么 大概分成三个核心维度来了解 BLE：架构（Stack）、连接机制（GAP-Generic Access Profile） 和 数据交互（GATT-Generic Attribute Profile）。\nBLE 的分层架构 (Protocol Stack) BLE 协议栈主要分为两大部分：控制器 (Controller) 和 主机 (Host)。\nController (底层): 负责无线电信号的发送、接收和时序控制。它直接处理硬件。 Host (上层): 运行在主控 MCU 上，负责逻辑处理、数据封装和应用层接口。 HCI (接口): 两者之间的沟通桥梁。 其中 Host 层中以下两层是开发时常见的\nGAP (Generic Access Profile): 负责“如何让别人发现我”或“如何连接别人”。 GATT (Generic Attribute Profile): 负责“连接后如何传输数据”。 这个架构分层前期接触搞不太明白，先学会应用不用明白原理 只用知道 需要先建立连接 -\u0026gt; 才能传输数据， 这两步分别用到刚刚的两个规范\n发现和连接 GAP (Generic Access Profile) GAP 定义了设备在网络中的角色以及它们如何建立连接。这里有两个关键概念：广播 (Advertising) 和 角色。\n四种主要角色 Broadcaster (广播者): 只发广播，不建立连接（例如：温湿度计、Beacon 信标）。 Observer (观察者): 只扫描广播，不建立连接（例如：网关）。 Peripheral (外围设备): 发送广播，允许被连接（例如：智能手环）。这是最常见的从机角色。 Central (中心设备): 扫描广播，发起连接（例如：手机）。这是最常见的主机角色。 在我遇到的应用需求中。我们的微信小程序所在的手机作为 Central，硬件设备是一个数字钥匙，作为 Peripheral。\n具体的连接流程如下\n外设周期性发送广播包 广播包里会包含： 自己的名字（Device Name） MAC 地址 支持的服务 UUID 可能还有一点点数据（最多 31 字节） 手机扫描（Scanning） 手机（Central）在蓝牙信道上监听广播。 一旦发现目标设备，就可以发起连接请求。 建立连接（Connecting） 手机 → 设备：发送连接请求 设备 → 手机：回应并建立 GATT 连接 这时候双方就有了一个“逻辑通道”，后续可以读/写/通知数据了。 交换数据就用到了另一个规范。\n数据交互：GATT (Generic Attribute Profile) 建立连接之后就可以发送数据了 GATT 也采用的 C/S 架构，但是和我们开发 Web 的 C/S 架构方向上不太一样，此时 外围设备是 Server，我们的 中心设备是 Client\nTip 接下来文章中多次会举例，我会直接默认手机为中心设备，设备/手环 为外围设备\n相关概念 根据GATT (Generic Attribute Profile) 规范可以定义出一个个配置文件 (Profile)，描述该蓝牙设备提供的服务 (Service)。\n配置文件 (Profile): Profile 是被蓝牙标准预先定义的一些 Service 的集合，并不真实存在于蓝牙设备中。如果蓝牙设备之间要相互兼容，它们只要支持相同的 Profile 即可。一个蓝牙设备可以支持多个 Profile。 服务 (Service): Service 是蓝牙设备对外提供的服务，一个设备可以提供多个服务，比如电量信息服务、系统信息服务等。每个服务由一个 UUID 唯一标识。 特征 (Characteristic): 每个 Service 包含 0 至多个 Characteristic。比如， 电量信息服务就会有个 Characteristic 表示电量数据。 Characteristic 包含一个值 (value) 和 0 至多个描述符 (Descriptor) 组成。 在与蓝牙设备通信时，主要就是通过读写 Characteristic 的 value 完成。 每个 Characteristic 由一个 UUID 唯一标识。 描述符 (Descriptor): Descriptor 是描述特征值的已定义属性。例如，Descriptor 可指定人类可读的描述、特征值的取值范围或特定于特征值的度量单位。每个 Descriptor 由一个 UUID 唯一标识。 Info 通知（Notify）的概念\n在 BLE 中，手机（Client）通常不会一直轮询（Polling）手环（Server）来查电量。而是手环在电量变化时，主动“推”数据给手机。这种机制叫 Notify 或 Indicate，是 BLE 省电的关键。\nBLE 中的 UUID UUID (Universally Unique Identifier) 根据蓝牙 4.2 协议规范(Vol 3, Part B, section 2.5.1 UUID)，UUID 是一个 128 位的唯一标识符，用来标识 Service 和 Characteristic 等。\n为了减少存储和传输 128 位 UUID 值的负担，蓝牙技术联盟预分配了一批 UUID，这一批 UUID 拥有一个共同部分，被称为 Bluetooth Base UUID，即 00000000-0000-1000-8000-00805F9B34FB。因此，预分配的 UUID 也可以使用 16 位或 32 位表示，其中 16 位 UUID 最为常用。使用 16/32 位的 UUID 可以降低存储和传输的负载。开发者自定义的 UUID 应注意不能与预分配的 UUID 冲突。\n举个🌰 要开发一个“智能灯泡”：\n定义角色 (GAP): 灯泡是 Peripheral（外围设备），手机是 Central（中心设备）。 设计广播: 灯泡上电后发送广播包，包含名字“Smart Light”。 设计数据 (GATT): 创建一个 Service (自定义 UUID)。 在下面创建一个 Characteristic 用于控制开关（支持 Write 属性）。 再创建一个 Characteristic 用于反馈亮度（支持 Read 和 Notify 属性）。 交互: 手机扫描到“Smart Light” -\u0026gt; 连接 -\u0026gt; 手机往“开关特征值”写入 0x01 -\u0026gt; 灯泡亮起。 微信小程序 原本想记录微信小程序的过程。后发现官方文档的内容足以，就不多赘述了，当了解了上面的概念之后，就能看懂官方文档，写小程序来实现蓝牙已经不复杂了\n参考文章 感谢 gemini 和 gpt 两位 G 老师\nBluetooth Low Energy - Wikipedia Bluetooth - Wikipedia 连接硬件能力 / 蓝牙 / 介绍 经典蓝牙与低功耗蓝牙BLE开发基础知识：服务、特征、属性、UUID-云社区-华为云 另外调试过程中有两个手机 app 挺有帮助\nBLE调试助手，南京沁恒开发的app nRF Connect，Nordic官方开发（这个全英文用起来有点难度，上面那个比较简单，但是功能感觉不够多，作为从机而言） 公司有一块闲置的树莓派 3B，看了下支持 蓝牙 4.1 ，刷回官方系统回头看看能不能拿来当调试工具\n","permalink":"/posts/%E5%88%9D%E8%AF%86-ble-%E5%8D%8F%E8%AE%AE%E4%BB%A5%E5%8F%8A%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e手头有个项目，涉及到 app 端蓝牙连接。硬件开发伙伴给到了 BLE 协议，完全一头雾水\u003c/p\u003e\n\u003cp\u003e最近忙的没空推进 Go 的学习，想着博客至少不能落下更新，不管怎么样也要逼自己写一篇水文。\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"什么是-ble\"\u003e什么是 BLE\u003c/h2\u003e\n\u003ch3 id=\"首先得知道什么是蓝牙bluetooth\"\u003e首先得知道什么是蓝牙（Bluetooth）\u003c/h3\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e小时候肯定有人好奇为什么要叫 Bluetooth，无奈好奇心还不够，只停留在发起 why，今天才是真的解惑。\u003c/p\u003e","title":"初识 BLE 协议以及微信小程序"},{"content":" 我是如何爽用OpenCode的 - 开发调优 - LINUX DO\nOpenCode 确实听说有一段时间，但是由于手头 codex+claudecode 以及cursor 三个工具配合使用对我而言vibe coding的体验已经算是不错的了。gemini3 出世以来 虽说强，但是没用过gemini cli，antigravity使用体感不佳（也许是用的太早了？）至少觉得简中交互不佳。\n那受佬友这篇文章的启发，怎么说我也要尝尝咸淡。\n什么是OpenCode GitHub - anomalyco/opencode: The open source coding agent.\nThe open source coding agent.\n感谢开源贡献者。\n怎么用 我的开发工具是 家中的windows+wsl 和公司的mac\n写本文时在家里，那么就在vscode+wsl 里面实践一下\n官方的安装方式\n1 2 3 4 5 6 7 8 9 10 11 12 # YOLO curl -fsSL https://opencode.ai/install | bash # Package managers npm i -g opencode-ai@latest # or bun/pnpm/yarn scoop bucket add extras; scoop install extras/opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date) brew install opencode # macOS and Linux (official brew formula, updated less frequently) paru -S opencode-bin # Arch Linux mise use -g opencode # Any OS nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch Vscode插件市场可以直接找到Opencode 的插件，安装一下，(ok 我以为这个就是完全体，原来只是个类似之前的 cc for vscode一样的东西) 那还是执行 curl -fsSL https://opencode.ai/install | bash，在点击运行。\n嘶，没看到哪里有配置的入口，我一发消息就能用了，啊这？\n但是几个亮点用的时候就震惊到我了。这个TUI可以和鼠标交互的含金量让我感到爽！点击一条message的时候 居然可以进行 revert、copy、fork，惊呼了要。先研究一下怎么配置模型。\nok，Intro | OpenCode 文档写着运行 /connect 命令来选择 Provider，那么可以看到其实刚刚应该是OpenCode给我提供了一个 free model ”big-pickle“\n配置更多”懒人包“ 和各种小技巧 Oh-my-opencode GitHub - code-yeongyu/oh-my-opencode: The Best Agent Harness. Meet Sisyphus: The Batteries-Included Agent that codes like you.\n准备好环境运行pnpx oh-my-opencode install即可\n我由于有google pro，嫖上这个gemini后就没有探索过别的内容了。 这两天用的不多，留给以后更新。如果好用的话。\n","permalink":"/posts/opencode-%E5%88%9D%E4%BD%93%E9%AA%8C/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e\u003ca href=\"https://linux.do/t/topic/1404993\"\u003e我是如何爽用OpenCode的 - 开发调优 - LINUX DO\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eOpenCode 确实听说有一段时间，但是由于手头 codex+claudecode 以及cursor 三个工具配合使用对我而言vibe coding的体验已经算是不错的了。gemini3 出世以来 虽说强，但是没用过gemini cli，antigravity使用体感不佳（也许是用的太早了？）至少觉得简中交互不佳。\u003c/p\u003e","title":"OpenCode 初体验"},{"content":" 依稀记得大一寒假打开 小甲鱼 python教程的一周后就认识了 Anaconda 这个解放我安装python 依赖的工具，至今已经过去了六年了，conda 似乎也做了一些商业收费的举措之类的，让很多大公司用上了新的环境管理工具，由于习惯我还一直用着conda，以至于我对 python 自己的 pyenv 都不是很了解（Damn！！！太丢人了）\n那么这次就来用用 uv这个所谓的 超高速python工具链\nuv\n安装 uv macOS / Linux / WSL curl -LsSf https://astral.sh/uv/install.sh | sh\nWindows（PowerShell） irm https://astral.sh/uv/install.ps1 | iex\n验证：\nuv --version\n常见用法 1. 创建项目并指定python版本 在空目录或者指定目录创建项目\nbash 1 2 # 创建新目录并初始化项目 uv init my-app 或者你当前在一个空目录下：\nuv init\n结果：\nuv 会生成一套基础文件，例如：\n1 2 3 4 5 my-app/ ├── pyproject.toml # 项目配置（依赖/元数据） ├── .python-version # 记录 Python 版本 ├── README.md ├── main.py (示例入口) 如果指定了应用名 my-app，uv 也会生成对应样板代码和README.md。\n如果在已有的项目里面创建环境 没有 pyproject.toml 直接 uv init .即可\n有的话就 uv sync 除非这个里面放的是旧的 pip体系\n那么pyproject.toml 中没有依赖的管理也木有项目声明 例如\n[toml] 显示已折叠代码（28 行） 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 [tool.black] line-length = 120 target-version = [\u0026#39;py310\u0026#39;, \u0026#39;py311\u0026#39;, \u0026#39;py312\u0026#39;] include = \u0026#39;\\.pyi?$\u0026#39; exclude = \u0026#39;\u0026#39;\u0026#39; /( \\.git | \\.hg | \\.mypy_cache | \\.tox | \\.venv | venv | _build | buck-out | build | dist | __pycache__ )/ \u0026#39;\u0026#39;\u0026#39; [tool.isort] profile = \u0026#34;black\u0026#34; line_length = 120 skip = [\u0026#34;.git\u0026#34;, \u0026#34;__pycache__\u0026#34;, \u0026#34;.env\u0026#34;, \u0026#34;venv\u0026#34;, \u0026#34;.venv\u0026#34;] [tool.bandit] exclude_dirs = [\u0026#34;tests\u0026#34;, \u0026#34;test_*.py\u0026#34;] skips = [\u0026#34;B101\u0026#34;] # assert 语句在测试中是允许的 那么其实可以加上 项目的声明，消除警告\n1 2 3 4 [project] name = \u0026#34;daily_stock_analysis\u0026#34; version = \u0026#34;0.1.0\u0026#34; requires-python = \u0026#34;\u0026gt;=3.10,\u0026lt;3.13\u0026#34; 通过下面的命令迁移，只有requirements 时同理\n1 uv add -r requirements.txt 2. 激活虚拟环境（可选） bash 1 source .venv/bin/activate # macOS / Linux / WSL 也可以不激活\nbash 1 2 uv run python main.py uv run uvicorn app.main:app --reload 3. 安装依赖（替代 pip install） uv add fastapi uvicorn\n效果：\n修改 pyproject.toml 自动生成 / 更新 uv.lock 安装进 .venv 4. 删除依赖 bash 1 uv remove fastapi 5. 同步依赖 bash 1 uv sync uv 如何管理 Python 版本 查看可用 Python uv python list\n安装 Python uv python install 3.12\n项目绑定 Python 版本 uv init --python 3.12\n或已有项目：\nuv python pin 3.11\nPython 会被缓存到：\n~/.cache/uv/python/\n如果你是 pip + venv 老项目 bash 1 2 3 rm -rf venv uv init uv add -r requirements.txt ","permalink":"/posts/%E6%80%8E%E4%B9%88%E4%BD%BF%E7%94%A8uv%E6%9D%A5%E7%AE%A1%E7%90%86-python-%E7%8E%AF%E5%A2%83/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e依稀记得大一寒假打开 小甲鱼 python教程的一周后就认识了 Anaconda 这个解放我安装python 依赖的工具，至今已经过去了六年了，conda 似乎也做了一些商业收费的举措之类的，让很多大公司用上了新的环境管理工具，由于习惯我还一直用着conda，以至于我对 python 自己的 \u003ccode\u003epyenv\u003c/code\u003e 都不是很了解（Damn！！！太丢人了）\u003c/p\u003e","title":"怎么使用uv来管理 python 环境"},{"content":" 本文大大大大量参考 阮一峰老师的博客，虽然我在之前的使用中没有翻阅他的文章，不过哪怕是抄，我也觉得应该抄个优秀的。 Tmux 使用教程 - 阮一峰的网络日志 这我没想到这篇写在19年，比我上次看的那个 async 要小很多了\nTmux 是一个终端复用器（terminal multiplexer），非常有用，属于常用的开发工具。\n我常用是因为两个情况\n懒得开多个窗口 远控服务器经过堡垒机 一、Tmux是什么 1.1 会话与进程 命令行的典型使用方式是，打开一个终端窗口（terminal window，以下简称\u0026quot;窗口\u0026quot;），在里面输入命令。用户与计算机的这种临时的交互，称为一次\u0026quot;会话\u0026quot;（session） 。\n会话的一个重要特点是，窗口与其中启动的进程是连在一起的。打开窗口，会话开始；关闭窗口，会话结束，会话内部的进程也会随之终止，不管有没有运行完。\n也就是初学的时候，连上虚拟机或者ssh连上Linux之后部署运行程序是直接在这个窗口中运行的。当窗口关闭时程序也随之关闭，在当时接触了解了nohup 和 screen 。\n一个典型的例子就是，SSH 登录远程计算机，打开一个远程窗口执行命令。这时，网络突然断线，再次登录的时候，是找不回上一次执行的命令的。因为上一次 SSH 会话已经终止了，里面的进程也随之消失了。\nhistory能存上一次执行的命令，这里回头一定要学习一下，因为我也不知道什么情况才会存进history中\n为了解决这个问题，会话与窗口可以\u0026quot;解绑\u0026quot;：窗口关闭时，会话并不终止，而是继续运行，等到以后需要的时候，再让会话\u0026quot;绑定\u0026quot;其他窗口。\n1.2 Tmux 的作用 Tmux 就是会话与窗口的\u0026quot;解绑\u0026quot;工具，将它们彻底分离。\n（1）它允许在单个窗口中，同时访问多个会话。这对于同时运行多个命令行程序很有用。\n（2） 它可以让新窗口\u0026quot;接入\u0026quot;已经存在的会话。\n（3）它允许每个会话有多个连接窗口，因此可以多人实时共享会话。\n（4）它还支持窗口任意的垂直和水平拆分。\n类似的终端复用器还有 GNU Screen。Tmux 与它功能相似，但是更易用，也更强大。\n二、基本用法 2.1 安装 Tmux 一般需要自己安装。\nbash 1 2 3 4 5 6 7 8 # Ubuntu 或 Debian $ sudo apt-get install tmux # CentOS 或 Fedora $ sudo yum install tmux # Mac $ brew install tmux 2.2 启动与退出 安装完成后，键入tmux命令，就进入了 Tmux 窗口。\nbash 1 $ tmux 上面命令会启动 Tmux 窗口，底部有一个状态栏。状态栏的左侧是窗口信息（编号和名称），右侧是系统信息。\n这里我很不要脸的替换截图，不然照抄多没意思，我用我的wsl来做演示\n按下Ctrl+d或者显式输入exit命令，就可以退出 Tmux 窗口。\n2.3 前缀键 Tmux 窗口有大量的快捷键。所有快捷键都要通过前缀键唤起。默认的前缀键是Ctrl+b，即先按下Ctrl+b，快捷键才会生效。\n举例来说，帮助命令的快捷键是Ctrl+b ?。它的用法是，在 Tmux 窗口中，先按下Ctrl+b，再按下?，就会显示帮助信息。\n然后，按下 ESC 键或q键，就可以退出帮助。\n这些应该都是指 按下 Ctrl+b 之后再按什么的意思\n三、会话管理 3.1 新建会话 第一个启动的 Tmux 窗口，编号是0，第二个窗口的编号是1，以此类推。这些窗口对应的会话，就是 0 号会话、1 号会话。\n使用编号区分会话，不太直观，更好的方法是为会话起名。\nbash 1 $ tmux new -s \u0026lt;session-name\u0026gt; 上面命令新建一个指定名称的会话。\n3.2 分离会话 在 Tmux 窗口中，按下Ctrl+b d或者输入tmux detach命令，就会将当前会话与窗口分离。\nbash 1 $ tmux detach 上面命令执行后，就会退出当前 Tmux 窗口，但是会话和里面的进程仍然在后台运行。\ntmux ls命令可以查看当前所有的 Tmux 会话。\n这是我第一次知道，之前都是把Tmux当做分割复用一个窗口的工具，如果这样那还用个鸟的screen，这个看起来功能更多\nbash 1 2 3 $ tmux ls # or $ tmux list-session 3.3 接入会话 tmux attach命令用于重新接入某个已存在的会话。\n我给会话取了名字 编号不会生效了\nbash 1 2 3 4 5 # 使用会话编号 $ tmux attach -t 0 # 使用会话名称 $ tmux attach -t \u0026lt;session-name\u0026gt; 3.4 杀死会话 tmux kill-session命令用于杀死某个会话。\n3.5 切换会话 tmux switch命令用于切换会话。\n3.6 重命名会话 tmux rename-session命令用于重命名会话。\nbash 1 $ tmux rename-session -t 0 \u0026lt;new-name\u0026gt; 上面命令将0号会话重命名。 也可以是 old-name new-name\n3.7 会话快捷键 下面是一些会话相关的快捷键。\nCtrl+b d：分离当前会话。 Ctrl+b s：列出所有会话。 Ctrl+b $：重命名当前会话。 四、最简操作流程 综上所述，以下是 Tmux 的最简操作流程。\n新建会话tmux new -s my_session。 在 Tmux 窗口运行所需的程序。 按下快捷键Ctrl+b d将会话分离。 下次使用时，重新连接到会话tmux attach-session -t my_session。 五、窗格操作 Tmux 可以将窗口分成多个窗格（pane），每个窗格运行不同的命令。以下命令都是在 Tmux 窗口中执行。\n这是我常用的，因为堡垒机的页面不方便开多个终端，但是有时候运维需要监看多个画面\n5.1 划分窗格 tmux split-window命令用来划分窗格。\nbash 1 2 3 4 5 # 划分上下两个窗格 $ tmux split-window # 划分左右两个窗格 $ tmux split-window -h 5.2 移动光标 tmux select-pane命令用来移动光标位置。\nbash 1 2 3 4 5 6 7 8 9 10 11 # 光标切换到上方窗格 $ tmux select-pane -U # 光标切换到下方窗格 $ tmux select-pane -D # 光标切换到左边窗格 $ tmux select-pane -L # 光标切换到右边窗格 $ tmux select-pane -R 5.3 交换窗格位置 tmux swap-pane命令用来交换窗格位置。\nbash 1 2 3 4 5 # 当前窗格上移 $ tmux swap-pane -U # 当前窗格下移 $ tmux swap-pane -D 5.4 窗格快捷键 下面是一些窗格操作的快捷键。\nCtrl+b %：划分左右两个窗格。 Ctrl+b \u0026quot;：划分上下两个窗格。 Ctrl+b \u0026lt;arrow key\u0026gt;：光标切换到其他窗格。\u0026lt;arrow key\u0026gt;是指向要切换到的窗格的方向键，比如切换到下方窗格，就按方向键↓。 Ctrl+b ;：光标切换到上一个窗格。 Ctrl+b o：光标切换到下一个窗格。 Ctrl+b {：当前窗格与上一个窗格交换位置。 Ctrl+b }：当前窗格与下一个窗格交换位置。 Ctrl+b Ctrl+o：所有窗格向前移动一个位置，第一个窗格变成最后一个窗格。 Ctrl+b Alt+o：所有窗格向后移动一个位置，最后一个窗格变成第一个窗格。 Ctrl+b x：关闭当前窗格。 Ctrl+b !：将当前窗格拆分为一个独立窗口。 Ctrl+b z：当前窗格全屏显示，再使用一次会变回原来大小。 Ctrl+b Ctrl+\u0026lt;arrow key\u0026gt;：按箭头方向调整窗格大小。 Ctrl+b q：显示窗格编号。 六、窗口管理 除了将一个窗口划分成多个窗格，Tmux 也允许新建多个窗口。\n6.1 新建窗口 tmux new-window命令用来创建新窗口。\nbash 1 2 3 4 $ tmux new-window # 新建一个指定名称的窗口 $ tmux new-window -n \u0026lt;window-name\u0026gt; 6.2 切换窗口 tmux select-window命令用来切换窗口。\nbash 1 2 3 4 5 # 切换到指定编号的窗口 $ tmux select-window -t \u0026lt;window-number\u0026gt; # 切换到指定名称的窗口 $ tmux select-window -t \u0026lt;window-name\u0026gt; 可以看到右下角，0,1,2 都是我刚刚建立的几个窗口\n6.3 重命名窗口 tmux rename-window命令用于为当前窗口起名（或重命名）。\nbash 1 $ tmux rename-window \u0026lt;new-name\u0026gt; 6.4 窗口快捷键 下面是一些窗口操作的快捷键。\nCtrl+b c：创建一个新窗口，状态栏会显示多个窗口的信息。 Ctrl+b p：切换到上一个窗口（按照状态栏上的顺序）。 Ctrl+b n：切换到下一个窗口。 Ctrl+b \u0026lt;number\u0026gt;：切换到指定编号的窗口，其中的\u0026lt;number\u0026gt;是状态栏上的窗口编号。 Ctrl+b w：从列表中选择窗口。 Ctrl+b ,：窗口重命名。 七、其他命令 下面是一些其他命令。\nbash 1 2 3 4 5 6 7 8 9 10 11 # 列出所有快捷键，及其对应的 Tmux 命令 $ tmux list-keys # 列出所有 Tmux 命令及其参数 $ tmux list-commands # 列出当前所有 Tmux 会话的信息 $ tmux info # 重新加载当前的 Tmux 配置 $ tmux source-file ~/.tmux.conf 题外话：window 和 pane 的区别 tmux 的三层结构\n1 2 3 4 5 6 7 8 9 10 tmux server └── session（会话） ├── window（窗口，类似“标签页”） │ ├── pane（分屏） │ └── pane ├── window │ └── pane └── window ├── pane └── pane window 做任务隔离\npane 做协同观察 session 就是我们最刚开始建立的那个，可以detach 然后断开ssh，也可以 attach 回来\n这样一来 服务器上可以搞好一套配置，每次断开连接后回来可以用 tmux 快速恢复一套页面。\nscreen有一个问题就是如果宿主机reboot了 screen也会丢失，不知道tmux或者screen有没有什么办法持久化？留给下次翻这篇文章的我了。\n","permalink":"/posts/tmux%E6%98%AF%E4%BB%80%E4%B9%88/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e本文大大大大量参考 阮一峰老师的博客，虽然我在之前的使用中没有翻阅他的文章，不过哪怕是抄，我也觉得应该抄个优秀的。\n\u003ca href=\"https://www.ruanyifeng.com/blog/2019/10/tmux.html\"\u003eTmux 使用教程 - 阮一峰的网络日志\u003c/a\u003e\n这我没想到这篇写在19年，比我上次看的那个 async 要小很多了\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003eTmux 是一个终端复用器（terminal multiplexer），非常有用，属于常用的开发工具。\u003c/p\u003e","title":"tmux是什么"},{"content":"我想写年度总结，但是我打开Obsidian的时候左思右想翻来覆去也挖不出一点内容，脑子里空空，肚子里装得满满（不是墨水，是三个生蚝一个汉堡一个鸡翅一个鸡腿八个寿司两杯橙汁）。那就从头开始回看一下吧。\n要说2025围绕的几件事情，其实第一个想到的是大模型，从部署到微调到应用，从去年底就开始了，这一年常常倒着时差在关注着另一个白天的发布会，常常和朋友感慨，我获取这些前沿消息的及时性已经快和测评博主齐平了，远远快过搞新闻的，区别是我在发布会后倒头就睡，测评博主需要测和评，而我第二天早上睁眼窃取他们熬夜的劳动成果，在公司群里发出来获得一些简单的满足感和虚荣感。但是后来看到一位网友的说法，说他今年也是如此，一直追着最新的技术，不停换着各种工具，体验各个产品，本来还有一些优越感的，但是后来发现身边的一些朋友，虽然获得的信息延后且被筛选，但是也省去了他们自己筛选和试错的时间，他这么一说，又动摇了我的想法，不过，我喜欢在关心的领域处在上风口。\n题外话，从年初到年底，几乎几周就会迎来一次革新，每次一个阶梯也已经来到了新的楼层。LLM的应用应该会在今年开出遍地的花。\n我还换了个手机，翻了翻旧手机的相册，实在没有很多内容，既是因为旧手机拍照不好看，也是因为天天上班我拍甜妹啊。25年总计出游似乎不出双手，还真是，年初去了香港、不久后公司去穿越东西涌、回家过年算出游否？后来去爬了梧桐山、国庆回了趟家，again，同时杭州宁波舟山几日游。不是很有滋味呢，至少觉得不够尽兴，因为不管怎么算。更多的时间都扑在了工作上。\n上班还行，尽管不可能天天愉快轻松，但是上班应该是实现自我价值的，不然还能干嘛呢，又没有金山可以坐吃。今年开始了几个好习惯，年初搭了博客，虽说进度一至断断续续，但是好歹到现在还在更新，虽欠了一些呢，但是日拱一卒！漏一天那就隔一天再拱一卒！虽然有一些隔了挺久，但是无所谓！补上就是胜利！这是这两天学到的新心态，通俗来讲就是坚持！但不是那种要死要活的咬牙坚持，而是把一些想坚持的事情，如染料般一点点浸入自己的生活日常，这也不容易哈。\n说到坚持，年底的两个月还看起书来，说不知道为什么，但是是从凡人修仙传开始看的。买了一些书发现没有想象那么好看，后来看了一本海边的卡夫卡，看村上的书总感觉又坐回高中教室，其实我一直不是特别能懂村上的书，看完后总想搜一搜书评，又发现谁讲的都好像想到过，便又觉得乏味，但是村上的书就像那种酒一样的感觉，我喝不明白啊，也许不那么好喝，但是微醺的后劲终究会来，且是悄悄的来，后来开始看起来明朝那些事儿，那是真好看，看历史使人心胸宽广气质雄厚，\n三千年读史不外乎功名利禄，九万里悟道终归诗酒田园\n为此还买了微信读书的会员，参加21天打卡活动，结果昨天最后一天没有打开过APP泡汤了，沮丧了好一会儿。希望新的一年能满足我25年补充的24年没有完成的23年的年初计划，嘿嘿。\n凡人修仙传是今年看的一部印象很深刻的动画片了，可以说是国漫近几年看的最满意的动画了，前期建模虽粗糙，但是制作组诚意满满，不断重制，优秀的改编，当我阅读完小说后回头看动画，不得不感慨改编的完美。当然今年还有罗小黑，灵笼，带着980看了巨人，看了2077，看了剑来，看了牧神记等等。年底还有怪奇物语。\n今年也是偶尔会有些模仿文人感叹的时候，奈何库存实在少的可怜，搜刮来搜刮去尽都是些网络用语，丢人的很。让GPT5.2总结我的一年，他说我今年更明白了什么叫持续，说我允许自己接受节奏的不稳定，接受自己可以反复校准方向，我不知道他说的对不对，我只是想努力做好一些事情罢了。我也希望成为他描述的人。 知行合一的人。\n今年胖了许多，打工很快满两年了，加在一起快胖出超市一袋米了。体检也是处处过胖的红灯，年底开始少量的运动，慢慢开始减肥了，减肥倒是曾有一些经验，不过，确实不容易坚持，尤其不是一个人的时候，更容易放纵。\n今年常去980家吃饭，几乎隔周一次，每次都会觉得有一道门在向我靠近。\n今年比去年更想家了。\n今年想学，想多赚点钱的欲望更多了，下半年打游戏时间总和几乎只有几个小时，有时候都不是事情逼着我，是我觉得累了，我觉得这太不健康了，是那种毫无精力的空虚麻木。也许工作状态该好好调整了。\n我很想像大佬们一样，总是引经据典，信手拈来，言简意赅的几句道过。我做不到，我本来想喝点酒的，对了，年底还买了俩尊美醇、常和980在家微醺。喝点酒也许迷糊的时候更适合这种碎碎念。我刚搭建博客的时候取名是 Ray的碎碎念，后面那个主题太花哨了无法接受（实则是懒得配置过多东西）挺好的。今年常遵循的原则是 KISS 原则，无论是生活和工作，当然，工作更多，生活还是喜欢锦上添花的。如果有人看到这里不知道我说的 KISS 什么意思，诶嘿，你去搜一搜吧，不想解释。这篇文章不是解惑的。\n我也想像大佬们一样，文绉绉的满口，而不是各种网络用语满肚子，不行，脑子里开始放库里库里。\n今年还买了个十年的域名，哈哈，长期计划嘛。 我还给十年后？还是多少年后的自己写了一封邮件，到今天为止已经忘记了邮件的内容，无非一些不礼貌的问候罢了，毕竟是给自己的，已经2026了，十年前我刚上高中，不行，说到这鼻子会酸了，美好的青春啊！一去不复返咯，不全是惋惜过去，而是对当下的怜悯。\n突然海马效应了一会儿，也许是困了，不到夜半憋不出半句骚话，那我也就不接着搜肠刮肚了，抄几句骚话结束吧。\n由于读《海边的卡夫卡》，再次看到了《挪威的森林》的摘句，这本书是我高中花了两节课偷偷看完的，是我看村上的入门书，对我影响很大应该，虽然剧情都快忘了，回头再看一遍，抛开里面奔放的剧情不说，整书实在适合我当下的心境，我再找了出来\n“我也二十岁了，有点儿像开玩笑似的。”直子说，“我，一点儿也没做二十岁的准备，挺纳闷儿的，就像谁从背后硬推给我的一样”\n时间无情的推着我往前走，他不会管我有没有做好准备，挺纳闷的。我问 gpt 我今天的年度总结应该怎么取标题，他说我月下小札的气质不适合写什么“年度”，“总结”，搞得像汇报一样，\n更应该像一页在夜里合上的书。\n那么，我将合上2025这一本咯，\n翻开2026的第一页，内容还是由我亲笔来写。\n","permalink":"/posts/2025/","summary":"\u003cp\u003e我想写年度总结，但是我打开Obsidian的时候左思右想翻来覆去也挖不出一点内容，脑子里空空，肚子里装得满满（不是墨水，是三个生蚝一个汉堡一个鸡翅一个鸡腿八个寿司两杯橙汁）。那就从头开始回看一下吧。\u003c/p\u003e\n\u003cp\u003e要说2025围绕的几件事情，其实第一个想到的是大模型，从部署到微调到应用，从去年底就开始了，这一年常常倒着时差在关注着另一个白天的发布会，常常和朋友感慨，我获取这些前沿消息的及时性已经快和测评博主齐平了，远远快过搞新闻的，区别是我在发布会后倒头就睡，测评博主需要测和评，而我第二天早上睁眼窃取他们熬夜的劳动成果，在公司群里发出来获得一些简单的满足感和虚荣感。但是后来看到一位网友的说法，说他今年也是如此，一直追着最新的技术，不停换着各种工具，体验各个产品，本来还有一些优越感的，但是后来发现身边的一些朋友，虽然获得的信息延后且被筛选，但是也省去了他们自己筛选和试错的时间，他这么一说，又动摇了我的想法，不过，我喜欢在关心的领域处在上风口。\u003c/p\u003e","title":"《2025》"},{"content":"首先准备环境。（之前为了搞些小工具，多多少少接触一点go，不过几乎可以说为0） 那么最重要的：官网，哪里的文档都不如亲妈的描述\nNote go.dev Documentation - The Go Programming Language go.dev/learn\n打开网站反正左右都是个download\n找个适合的下载吧（感觉这个都能单开一篇来写，这里没写是因为之前已经装过了） 完成之后terminal里可以看到版本号\nNote 如果暂时不装环境。官方有一个 A Tour of Go，可以直接在这里学最基础的一些语法 A Tour of Go\n这里有个很有意思的事情，可以看到他的那句 打印居然是 Hello，世界，而不是咱们熟知的hello world，我还特意检查了一下是不是我的翻译插件又出bug了 那么grok小助手帮我搜一下吧（最近弄了个小号池） 主要是为了展示 Go对Unicode和UTF-8的完美支持\n这里的指南总计需要半个小时左右即可阅读完毕，建议可以先读一遍。同时，我在读了一半的中文版后强烈建议有能力的一定要读英文版，当然，我读起来还是一卡一卡的啦（和今晚的网络质量一样😡）\nPackages Every Go program is made up of packages.\nPrograms start running in package main.\nThis program is using the packages with import paths \u0026ldquo;fmt\u0026rdquo; and \u0026ldquo;math/rand\u0026rdquo;.\nBy convention, the package name is the same as the last element of the import path. For instance, the \u0026ldquo;math/rand\u0026rdquo; package comprises files that begin with the statement package rand.\npackage main: 告诉 Go 编译器，这个文件是一个可执行程序的入口，而不是一个库（Library）。 func main(): 这是程序启动时第一个执行的函数。入口函数 import \u0026quot;fmt\u0026quot;: 导入格式化包，这样你才能使用 Println 打印内容。 go 1 2 3 4 5 6 7 8 9 10 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; ) func main() { fmt.Println(\u0026#34;My favorite number is\u0026#34;, rand.Intn(10)) } This code groups the imports into a parenthesized, \u0026ldquo;factored\u0026rdquo; import statement.\nYou can also write multiple import statements, like:\n1 2 import \u0026#34;fmt\u0026#34; import \u0026#34;math\u0026#34; But it is good style to use the factored import statement.\nExported names（可见性） In Go, a name is exported if it begins with a capital letter. For example, Pizza is an exported name, as is Pi, which is exported from the math package.\npizza and pi do not start with a capital letter, so they are not exported.\nWhen importing a package, you can refer only to its exported names. Any \u0026ldquo;unexported\u0026rdquo; names are not accessible from outside the package.\nRun the code. Notice the error message.\nTo fix the error, rename math.pi to math.Pi and try it again.\ngo 1 2 3 4 5 6 7 8 9 10 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math\u0026#34; ) func main() { fmt.Println(math.pi) } Go 语言中，没有像 Java 或 C++ 那样的 public、private 关键字。Go 直接通过首字母来区分\n首字母的大小写决定了该变量、函数或类型能否被“外人”看到。\n这里给到的示例代码运行会报错\n./prog.go:9:19: undefined: math.pi (but have Pi)\nTip 大写字母开头 (Exported)：它是“导出的”，意思是可以被外部包访问（相当于 public）。 小写字母开头 (Unexported)：它是“未导出的”，意思是只能在自己包内部使用（相当于 private）。 改成Pi之后输出就是\n3.141592653589793\n这个设计是go的极简思想（说实话，感觉很有python的宗旨，比python还python）\n函数 A function can take zero or more arguments.\nIn this example, add takes two parameters of type int.\nNotice that the type comes after the variable name.\n(For more about why types look the way they do, see the article on Go\u0026rsquo;s declaration syntax.)\ngo 1 2 3 4 5 6 7 8 9 10 11 package main import \u0026#34;fmt\u0026#34; func add(x int, y int) int { return x + y } func main() { fmt.Println(add(42, 13)) } 说实话这个看起来很费劲，当然，对于等下的变量来说，更费劲的还没来\nFunctions continued When two or more consecutive named function parameters share a type, you can omit the type from all but the last.\nx, y int\nok熟悉，这不是 C/C++ 的 int x, y; 可是，这里是函数里啊，真的相当极简了 好在我适应性强呢 [/狗头]\nMultiple results A function can return any number of results.\nThe swap function returns two strings.\ngo 1 2 3 4 5 6 7 8 9 10 11 12 package main import \u0026#34;fmt\u0026#34; func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap(\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;) fmt.Println(a, b) } Named return values Go\u0026rsquo;s return values may be named. If so, they are treated as variables defined at the top of the function.\nThese names should be used to document the meaning of the return values.\nA return statement without arguments returns the named return values. This is known as a \u0026ldquo;naked\u0026rdquo; return.\nNaked return statements should be used only in short functions, as with the example shown here. They can harm readability in longer functions.\ngo 1 2 3 4 5 6 7 8 9 10 11 12 13 package main import \u0026#34;fmt\u0026#34; func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return } func main() { fmt.Println(split(17)) } WTF，说实话，震惊来着的。有啥用，只能说，短函数真的能偷懒就偷懒了。这里既在函数开始的时候就定义了x y 变量。还在return的时候啥都没带，这就是Naked return~，他会返回x和y。当然，官方也说了，长函数别这样玩\n变量声明和初始化 The var statement declares a list of variables; as in function argument lists, the type is last.\nA var statement can be at package or function level. We see both in this example.\nvar c, python, java bool\nVariables with initializers A var declaration can include initializers, one per variable.\nIf an initializer is present, the type can be omitted; the variable will take the type of the initializer.\ngo 1 2 3 4 5 6 7 8 9 10 package main import \u0026#34;fmt\u0026#34; var i, j int = 1, 2 func main() { var c, python, java = true, false, \u0026#34;no!\u0026#34; fmt.Println(i, j, c, python, java) } output\n1 2 true false no!\nShort variable declarations Inside a function, the := short assignment statement can be used in place of a var declaration with implicit type.\nOutside a function, every statement begins with a keyword (var, func, and so on) and so the := construct is not available.\n1 2 3 4 5 6 7 8 9 10 11 package main import \u0026#34;fmt\u0026#34; func main() { var i, j int = 1, 2 k := 3 c, python, java := true, false, \u0026#34;no!\u0026#34; fmt.Println(i, j, k, c, python, java) } 可以，这个喜欢\n基本类型 Go\u0026rsquo;s basic types are\nbool\nstring\nint int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr\nbyte // alias for uint8\nrune // alias for int32 // represents a Unicode code point\nfloat32 float64\ncomplex64 complex128\nThe example shows variables of several types, and also that variable declarations may be \u0026ldquo;factored\u0026rdquo; into blocks, as with import statements.\nThe int, uint, and uintptr types are usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems. When you need an integer value you should use int unless you have a specific reason to use a sized or unsigned integer type.\n后面的数字代表占多少位(bit)，默认int长度取决于系统\nuintptr 这是一个特殊类型，大到足以存储指针的位模式，底层开发时用。\nuint 只能表示 0 和正数。常用于处理二进制数据、内存地址等。这个类型写C单片机常构造\n默认用 int：除非有非常明确的理由（比如：这个数一定很大，超过了 20 亿，需要用 int64；或者你在处理二进制字节流，需要用 byte），否则一律声明为 int。\n没有隐式转换：\n在 Python 中，可以用 1 + 1.5。 在 Go 中，int 和 int32 是两种完全不同的类型。不能直接把它们相加，必须手动转换：int64(myInt32) + myInt64。 零值 (Zero Value)：在 Go 中，声明一个变量但不给它赋值，它不会是 undefined 或 None。\n数字类型默认是 0。 布尔类型默认是 false。 字符串类型默认是 \u0026quot;\u0026quot; (空字符串)。 [go] 显示已折叠代码（18 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/cmplx\u0026#34; ) // factored into blocks 就是这种写法，不需要在每一行都写 `var`，代码显得整洁。 var ( ToBe bool = false MaxInt uint64 = 1\u0026lt;\u0026lt;64 - 1 z complex128 = cmplx.Sqrt(-5 + 12i) ) func main() { fmt.Printf(\u0026#34;Type: %T Value: %v\\n\u0026#34;, ToBe, ToBe) fmt.Printf(\u0026#34;Type: %T Value: %v\\n\u0026#34;, MaxInt, MaxInt) fmt.Printf(\u0026#34;Type: %T Value: %v\\n\u0026#34;, z, z) } 1 2 3 Type: bool Value: false Type: uint64 Value: 18446744073709551615 Type: complex128 Value: (2+3i) 这里我有个疑问，没有None之类的吗？ 那么go里面 用的是 nil ，啊哈，我在Lua那里认识你\n由于int、bool、string等都有初始值，也就是 0 和false 以及 \u0026ldquo;\u0026quot;，nil不能赋值给他们\n下一页就讲了这个咯\nzero value Type conversions The expression T(v) converts the value v to the type T.\nSome numeric conversions:\nvar i int = 42 var f float64 = float64(i) var u uint = uint(f)\nOr, put more simply:\ni := 42 f := float64(i) u := uint(f)\nUnlike in C, in Go assignment between items of different type requires an explicit conversion. Try removing the float64 or uint conversions in the example and see what happens.\nT(v) 把变量 v 转换为类型 T\ngo 1 2 3 4 5 6 7 8 9 10 11 12 13 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math\u0026#34; ) func main() { var x, y int = 3, 4 var f float64 = math.Sqrt(float64(x*x + y*y)) var z uint = uint(f) fmt.Println(x, y, z) } output 3 4 5\n浮点数转整数：\n当使用 int(f) 转换浮点数时，Go 会直接截断小数部分（也就是向 0 取整），而不是四舍五入。\nint(3.9) 结果是 3 int(-3.9) 结果是 -3 Type inference When declaring a variable without specifying an explicit type (either by using the := syntax or var = expression syntax), the variable\u0026rsquo;s type is inferred from the value on the right hand side.\nWhen the right hand side of the declaration is typed, the new variable is of that same type:\nvar i int j := i // j is an int\nBut when the right hand side contains an untyped numeric constant, the new variable may be an int, float64, or complex128 depending on the precision of the constant:\ni := 42 // int f := 3.142 // float64 g := 0.867 + 0.5i // complex128\n我试了一下 字符串可以，gemini提醒我，单引号包裹字符是 int32，也就是rune类型\n数字常量在 Go 里面有 “默认类型” 的概念，这很容易让初学者困惑：\n整数：如果你写 i := 42，即使 42 还没超过 int8 的范围，Go 也会统一推断为 int（在 64 位系统上就是 int64）。它不会自作聪明帮你选个小的类型。 浮点数：如果你写 f := 3.14，Go 永远会推断为 float64，而不是 float32。因为在现代计算机中，float64 是精度和性能最平衡的选择。 go 1 2 3 4 5 6 7 8 // 推断为切片 []int nums := []int{1, 2, 3} // 推断为函数类型 add := func(a, b int) int { return a + b } // 从函数返回值推断 result := math.Sqrt(100) // 因为 Sqrt 返回 float64，所以 result 也是 float64 Go 的类型推断遵循 “所见即所得”：\n右边是什么类型，左边就是什么类型。 如果是没有明确类型的数字常量，则按“大类”走（整数给 int，小数给 float64）。 常量 Constants are declared like variables, but with the const keyword.\nConstants can be character, string, boolean, or numeric values.\nConstants cannot be declared using the := syntax.\nNumeric Constants Numeric constants are high-precision values. An untyped constant takes the type needed by its context. Try printing needInt(Big) too. (An int can store at maximum a 64-bit integer, and sometimes less.)\n[go] 显示已折叠代码（24 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main import \u0026#34;fmt\u0026#34; const ( // Create a huge number by shifting a 1 bit left 100 places. // In other words, the binary number that is 1 followed by 100 zeroes. Big = 1 \u0026lt;\u0026lt; 100 // Shift it right again 99 places, so we end up with 1\u0026lt;\u0026lt;1, or 2. Small = Big \u0026gt;\u0026gt; 99 ) func needInt(x int) int { return x*10 + 1 } func needFloat(x float64) float64 { return x * 0.1 } func main() { // 加这个会报错./prog.go:20:22: cannot use Big (untyped int constant 1267650600228229401496703205376) as int value in argument to needInt (overflows) fmt.Println(needInt(Big)) fmt.Println(needInt(Small)) fmt.Println(needFloat(Small)) fmt.Println(needFloat(Big)) } 常量是“理想数字”，编译时判断一次，如果变量参与那就可能编译判断可能运行判断\n结束 至此第一天结束，但是我没有按照G老师的课程，内心愧疚。明天继续\n","permalink":"/posts/day01-go-%E7%8E%AF%E5%A2%83%E4%B8%8E%E5%9F%BA%E7%A1%80/","summary":"\u003cp\u003e首先准备环境。（之前为了搞些小工具，多多少少接触一点go，不过几乎可以说为0）\n那么最重要的：官网，哪里的文档都不如亲妈的描述\u003c/p\u003e\n\u003cdiv class=\"markdown-alert markdown-alert-note\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 16h.01\"/\u003e\u003cpath d=\"M12 8v4\"/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Note\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e\u003ca href=\"https://go.dev/\"\u003ego.dev\u003c/a\u003e\n\u003ca href=\"https://go.dev/doc/\"\u003eDocumentation - The Go Programming Language\u003c/a\u003e\n\u003ca href=\"https://go.dev/learn/\"\u003ego.dev/learn\u003c/a\u003e\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\u003cp\u003e打开网站反正左右都是个download\u003c/p\u003e\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/12/b73fbe81308d98d1ba1058bba2d8d679.png\"\u003e\n        \u003cimg alt=\"Go 官网下载页面\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/12/b73fbe81308d98d1ba1058bba2d8d679.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e找个适合的下载吧（感觉这个都能单开一篇来写，这里没写是因为之前已经装过了）\n完成之后terminal里可以看到版本号\u003c/p\u003e","title":"Day01 Go 环境与基础"},{"content":" Note 微软大战代码的 SSH 工具挺好用，一直用它连接远程服务器进行开发，控制文件或者直接 terminal 运行命令都很方便。 但是当remote server 网络不好以及系统太老就歇菜了，那么如果下载 server 很久都没解决，我觉得应该可以离线下载并上传这个 server\n主要参考文章 知乎\n前言 要用 remote ssh in vscode，首先使用 vscode 的这一端，比如你的 windows or mac 就需要有 ssh client。25 年了，一般都会自带，如果这个都不知道，应该不会搜到我这篇文章。\n其次 安装 remote-ssh 插件，我这里由于现在在用 cursor 就直接用 cursor 做例子截图\n方法 1：直接在 vscode 中的 sidebar 中 Extentions 下载 我这里是已经下载的样子\n方法2：从网页下载离线插件包 vscode market vscode 应该有 UI 可以点到，但是我这里就用命令行表达一下意思。install from VSIX 然后选择刚刚下载的插件包就可以了\n重点，在远程服务器安装vscode server 在装完插件之后 重启 vscode 就可以看到界面有变化 如果顺利的话 新建连接，连接上去后会自动下载 vscode-ssh-server。\n1 2 3 4 安装完成后，个人用户目录HOME下会出现一个.vscode-server文件夹，里面有三个目录bin、extensions、data。 bin目录下面存放的是VS Code Server程序，extensions目录下是VS Code Server端安装的插件，data目录下是用户数据。 **那么在服务器上离线安装VS Code Server，只需要在个人用户目录$HOME下新建.vscode-server文件夹，在其中建立bin目录，放置“对应版本”的VS Code Server程序。** 这里由于知乎的文章是 21 年的，所以有一些变动了，我截图看一下，这是 25 年 8 月 7 日安装的，有些许不一样。但是套路一样 我刚刚遇到问题的服务器由于是 cursor，他换了个名字，汤药不变 可以看到 CommitID 和服务端的 ID 一致\nvscode-server 的下载链接\n1 https://update.code.visualstudio.com/commit:${commit_id}/server-linux-x64/stable（注意把:${commit_id}替换成对应的Commit ID） 这个我试了一下 应该还可以但是不知道什么时候会换。所以友友们可以在需要的时候搜一下或者最好的方法是从网络 OK 的服务器上下载后挪移\n另外由于我用的 cursor 我就再次 google 了一下他的 download 地址 https://cursor.blob.core.windows.net/remote-releases/${CURSOR_VERSION}-${CURSOR_COMMIT}/vscode-reh-${REMOTE_OS}-${REMOTE_ARCH}.tar.gz\nTip 注意系统和架构\n另外贴上一个脚本 to upload the Cursor server to offline Linux servers\n显示已折叠代码（170 行） 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 #!/bin/bash # ========================================================= # Cursor Remote Server Update Script # Used to download Cursor server files locally and then upload them to a remote Linux server # ========================================================= # ==================== Local Configuration ==================== SCRIPT_DIR=\u0026#34;$( cd \u0026#34;$( dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34; )\u0026#34; \u0026amp;\u0026gt; /dev/null \u0026amp;\u0026amp; pwd )\u0026#34; LOCAL_DOWNLOAD_DIR=\u0026#34;$SCRIPT_DIR/cursor_downloads\u0026#34; # Local download directory # Remote server configuration REMOTE_PORT=22 # SSH port REMOTE_USER=\u0026#34;root\u0026#34; # Remote username REMOTE_HOST=\u0026#34;mycloud.com\u0026#34; # Remote host # ==================== Target Architecture Configuration ==================== # Local architecture - determines which version you want to download locally # Possible values: \u0026#34;x64\u0026#34; or \u0026#34;arm64\u0026#34; # - x64: For Intel-based Macs # - arm64: For Apple Silicon (M1/M2/M3) based Macs LOCAL_ARCH=\u0026#34;arm64\u0026#34; # Remote architecture - determines which version you want to use on the remote server # Possible values: \u0026#34;x64\u0026#34; or \u0026#34;arm64\u0026#34; # - x64: For Intel/AMD servers (most common) # - arm64: For ARM-based servers (e.g., AWS Graviton) REMOTE_ARCH=\u0026#34;x64\u0026#34; # Remote operating system - usually Linux # Possible values: \u0026#34;linux\u0026#34; # Note: Cursor server currently mainly supports Linux REMOTE_OS=\u0026#34;linux\u0026#34; # ==================== Script Functions ==================== # Output colored messages print_message() { local color=$1 local message=$2 case $color in \u0026#34;green\u0026#34;) echo -e \u0026#34;\\033[0;32m$message\\033[0m\u0026#34; ;; \u0026#34;red\u0026#34;) echo -e \u0026#34;\\033[0;31m$message\\033[0m\u0026#34; ;; \u0026#34;yellow\u0026#34;) echo -e \u0026#34;\\033[0;33m$message\\033[0m\u0026#34; ;; \u0026#34;blue\u0026#34;) echo -e \u0026#34;\\033[0;34m$message\\033[0m\u0026#34; ;; *) echo \u0026#34;$message\u0026#34; ;; esac } # Check if a command exists check_command() { if ! command -v $1 \u0026amp;\u0026gt; /dev/null; then print_message \u0026#34;red\u0026#34; \u0026#34;Error: Command \u0026#39;$1\u0026#39; not found, please install it first.\u0026#34; exit 1 fi } # Get Cursor version information get_cursor_version() { if ! command -v cursor \u0026amp;\u0026gt; /dev/null; then print_message \u0026#34;red\u0026#34; \u0026#34;Error: \u0026#39;cursor\u0026#39; command not found. Please ensure Cursor is installed.\u0026#34; exit 1 fi print_message \u0026#34;blue\u0026#34; \u0026#34;Fetching Cursor version information...\u0026#34; # Run cursor --version and capture the output local version_info=$(cursor --version) # Use regex to extract version, commit hash, and architecture CURSOR_VERSION=$(echo \u0026#34;$version_info\u0026#34; | sed -n \u0026#39;1p\u0026#39;) CURSOR_COMMIT=$(echo \u0026#34;$version_info\u0026#34; | sed -n \u0026#39;2p\u0026#39;) CURSOR_ARCH=$(echo \u0026#34;$version_info\u0026#34; | sed -n \u0026#39;3p\u0026#39;) print_message \u0026#34;green\u0026#34; \u0026#34;Retrieved Cursor information:\u0026#34; print_message \u0026#34;green\u0026#34; \u0026#34; Version: $CURSOR_VERSION\u0026#34; print_message \u0026#34;green\u0026#34; \u0026#34; Commit: $CURSOR_COMMIT\u0026#34; print_message \u0026#34;green\u0026#34; \u0026#34; Architecture: $CURSOR_ARCH\u0026#34; } # Download Cursor server download_cursor_server() { print_message \u0026#34;blue\u0026#34; \u0026#34;Preparing to download Cursor server...\u0026#34; # Create download directory mkdir -p \u0026#34;$LOCAL_DOWNLOAD_DIR\u0026#34; # Build download URL DOWNLOAD_URL=\u0026#34;https://cursor.blob.core.windows.net/remote-releases/${CURSOR_VERSION}-${CURSOR_COMMIT}/vscode-reh-${REMOTE_OS}-${REMOTE_ARCH}.tar.gz\u0026#34; # Set download filename DOWNLOAD_FILENAME=\u0026#34;cursor-server-${CURSOR_VERSION}-${CURSOR_COMMIT}-${REMOTE_OS}-${REMOTE_ARCH}.tar.gz\u0026#34; DOWNLOAD_PATH=\u0026#34;$LOCAL_DOWNLOAD_DIR/$DOWNLOAD_FILENAME\u0026#34; print_message \u0026#34;yellow\u0026#34; \u0026#34;Download URL: $DOWNLOAD_URL\u0026#34; print_message \u0026#34;yellow\u0026#34; \u0026#34;Downloading to: $DOWNLOAD_PATH\u0026#34; # Download the file if curl -L \u0026#34;$DOWNLOAD_URL\u0026#34; -o \u0026#34;$DOWNLOAD_PATH\u0026#34;; then print_message \u0026#34;green\u0026#34; \u0026#34;Cursor server downloaded successfully!\u0026#34; else print_message \u0026#34;red\u0026#34; \u0026#34;Download failed!\u0026#34; exit 1 fi } # Upload and deploy Cursor server to remote host deploy_to_remote() { print_message \u0026#34;blue\u0026#34; \u0026#34;Preparing to upload to remote server...\u0026#34; # Build SSH command prefix SSH_CMD=\u0026#34;ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST}\u0026#34; SCP_CMD=\u0026#34;scp -P $REMOTE_PORT\u0026#34; # Ensure remote directory exists print_message \u0026#34;yellow\u0026#34; \u0026#34;Creating remote directory structure...\u0026#34; $SSH_CMD \u0026#34;mkdir -p ~/.cursor-server/cli/servers/Stable-${CURSOR_COMMIT}/server/\u0026#34; # Upload the file print_message \u0026#34;yellow\u0026#34; \u0026#34;Uploading server files to remote host...\u0026#34; $SCP_CMD \u0026#34;$DOWNLOAD_PATH\u0026#34; \u0026#34;${REMOTE_USER}@${REMOTE_HOST}:~/.cursor-server/cursor-server.tar.gz\u0026#34; if [ $? -ne 0 ]; then print_message \u0026#34;red\u0026#34; \u0026#34;Upload failed!\u0026#34; exit 1 fi # Extract the file print_message \u0026#34;yellow\u0026#34; \u0026#34;Extracting files on remote host...\u0026#34; $SSH_CMD \u0026#34;tar -xzf ~/.cursor-server/cursor-server.tar.gz -C ~/.cursor-server/cli/servers/Stable-${CURSOR_COMMIT}/server/ --strip-components=1\u0026#34; if [ $? -ne 0 ]; then print_message \u0026#34;red\u0026#34; \u0026#34;Extraction failed!\u0026#34; exit 1 fi # Clean up temporary files print_message \u0026#34;yellow\u0026#34; \u0026#34;Cleaning up remote temporary files...\u0026#34; $SSH_CMD \u0026#34;rm ~/.cursor-server/cursor-server.tar.gz\u0026#34; print_message \u0026#34;green\u0026#34; \u0026#34;Deployment complete! Server files have been successfully installed on the remote host.\u0026#34; print_message \u0026#34;green\u0026#34; \u0026#34;Remote server path: ~/.cursor-server/cli/servers/Stable-${CURSOR_COMMIT}/server/\u0026#34; } # ==================== Main Program ==================== # Check necessary commands check_command \u0026#34;curl\u0026#34; check_command \u0026#34;ssh\u0026#34; check_command \u0026#34;scp\u0026#34; # Get Cursor version information get_cursor_version # Download Cursor server download_cursor_server # Confirm upload print_message \u0026#34;blue\u0026#34; \u0026#34;Preparing to upload to remote server ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}\u0026#34; print_message \u0026#34;yellow\u0026#34; \u0026#34;Continue? [y/N]: \u0026#34; read -r confirmation if [[ $confirmation =~ ^[Yy]$ ]]; then deploy_to_remote else print_message \u0026#34;yellow\u0026#34; \u0026#34;Upload canceled. Files have been downloaded locally: $DOWNLOAD_PATH\u0026#34; fi print_message \u0026#34;green\u0026#34; \u0026#34;Script execution complete!\u0026#34; 参考文章：How to download cursor remote-ssh server manually？ - #6 by shiw-yang - How To - Cursor - Community Forum\n","permalink":"/posts/vscode-ssh-server-%E7%A6%BB%E7%BA%BF%E4%B8%8B%E8%BD%BD/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-note\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 16h.01\"/\u003e\u003cpath d=\"M12 8v4\"/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Note\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e微软大战代码的 SSH 工具挺好用，一直用它连接远程服务器进行开发，控制文件或者直接 terminal 运行命令都很方便。\n但是当remote server 网络不好以及系统太老就歇菜了，那么如果下载 server 很久都没解决，我觉得应该可以离线下载并上传这个 server\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/12/59349a63a7621b6d2852624a70cd2882.png\"\u003e\n        \u003cimg alt=\"VS Code Remote SSH 连接界面\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/12/59349a63a7621b6d2852624a70cd2882.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e","title":"VScode ssh server 离线下载"},{"content":" Note socks5 是什么，是不是还有 1234，socks5 和 常见的 vless 等一样吗 ？和 vpn 一样吗？怎么用呢？怎么建立呢？假如我有一个服务器 A 在美国，还有一个服务器B 在香港。我要访问的服务不允许香港IP。我应该怎么做呢？\n最近做一些小项目遇到的问题，让我云里雾里，本身关于这些的知识就没有系统学过，当实践用到时难受的一比，而 GPT 最近的一次更新后一直根据长期 memory 回答我的问题，污染严重，蠢得如同**\n项目概况 项目可以理解为某平台简易注册机、批量注册账号有一个需要注意的就是隐藏你的真实信息，当我固定一个 IP 和固定的 session 信息或 浏览器指纹进行大量账号注册是肯定会出发风控的。 而 playwright 配合 Chromium + 带认证的 SOCKS5 代理是不能很好的兼容，所以引发了一些知识补充过程\n本文主要是解决网络问题。\n术语解释 SOCKS5 是什么？ SOCKS - Wikipedia\nSOCKS5（Socket Secure 5）是一种网络代理协议，作用就是在你的电脑（客户端）和目标网站/服务之间插入一个“中间人”（代理服务器），负责转发数据。它支持 TCP 和 UDP（游戏、视频、BT 都行），还支持用户名密码认证、IPv6 和域名解析。\n它不自带加密（数据明文），所以速度快，但单独裸用不太安全（常和 TLS 等结合）。\nSOCKS5 和常见的 VLESS 等一样吗？ 不一样，是不同层面的东西：\nSOCKS5：一般是本地接口（你电脑上 127.0.0.1:1080），浏览器或软件直接连它。 VLESS（或 VMess、Trojan、Hysteria2 等）：是远程服务器协议，主要用在 V2Ray/Xray/sing-box/Clash 这类工具里。它轻量、支持加密+伪装，专门用来“翻墙”或抗检测。 典型用法： 你的软件（Clash/sing-box）本地开一个 SOCKS5 端口 → 用 VLESS 协议 加密连海外服务器 → 服务器再直连互联网。\n所以 VLESS 常“包裹”在外面，SOCKS5 是给你本地程序用的出口。\n和 VPN 一样吗？ 不一样：\n项目 SOCKS5 / 代理 VPN（如 WireGuard、OpenVPN） 工作层级 应用层（可只代理部分程序） 网络层（几乎全流量） 加密 默认不加密（更快） 强制全加密 速度 更快 稍慢（但现代协议差距小） 使用方式 需要软件支持或 Proxifier 强制 系统全局，无感 灵活性 极高（可分流规则） 一般 现在很多工具（Clash Meta、sing-box）的 TUN 模式 可以让代理效果几乎和 VPN 一样全局，还能智能分流。\n实现思路 Python + Qt 桌面程序控制的 playwright 无法直接通过配置参数来使用我的”需要认证“的 IP 池，所以想到加一个中间层。那就是在本机先用支持认证的库构建连接，转成本地无认证 SOCKS5，然后 playwright 连接本地的端口\n这里由 GPT 推荐。我使用的是 gost。Releases · ginuerzh/gost 解压得到 gost.exe，放到你的项目目录（用 PyInstaller 打包时加到 data_files）\n我的程序最终使用路径如下，当然 HK 这个server 也可以不要，直接本地也是可以用的。这里知识进行了一些基础术语的使用和思路，代码不方便分享，直接问 AI 就能得到了。\nHK（低延迟入口）+ US 服务器\n电脑 → 香港 B（入口） → 美国 A 或代理池（出口）\n实现方式：\n在香港 B 上跑一个 gost 监听 1080（或多端口） 香港 B 的 gost 再转发到你的动态代理池（或美国 A） 桌面程序只连 香港B_IP:1080/1081\u0026hellip;（延迟最低） ","permalink":"/posts/%E6%B5%85%E5%B0%9D-socks5/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-note\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 16h.01\"/\u003e\u003cpath d=\"M12 8v4\"/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Note\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003esocks5 是什么，是不是还有 1234，socks5 和 常见的 vless 等一样吗 ？和 vpn 一样吗？怎么用呢？怎么建立呢？假如我有一个服务器 A 在美国，还有一个服务器B 在香港。我要访问的服务不允许香港IP。我应该怎么做呢？\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\u003cp\u003e最近做一些小项目遇到的问题，让我云里雾里，本身关于这些的知识就没有系统学过，当实践用到时难受的一比，而 GPT 最近的一次更新后一直根据长期 memory 回答我的问题，污染严重，蠢得如同**\u003c/p\u003e","title":"浅尝 SOCKS5"},{"content":" Note 突发奇想，想做一个镜像站，不想用 Java，觉得 go 可以试试，问问 gpt 速学 go可不可能。让他给我安排 28 天的计划如下。后续开启 go-learn 进行学习\n🗓 Go 学习每日任务清单（28 天 × 每天 2 小时） 阶段 1：Go 基础与并发模型（Day 1–7） 🧠 目标：掌握 Go 核心语法 + 并发思想 天数 主题 任务与练习 Day 1 Go 环境与项目结构 - 安装 Go- 运行 go mod init demo- 了解 go run, go build, go fmt, go test- 写出第一个 “Hello, Go!” Day 2 基础语法与函数 - 学习变量声明、常量、函数、多返回值、error- 练习：写一个简单计算器函数- 理解 defer、panic、recover Day 3 Struct 与方法 - 定义一个 User 结构体并实现 Hello() 方法- 使用指针接收者 vs 值接收者- 熟悉 JSON 序列化 (encoding/json) Day 4 Slice / Map / Range / 指针 - 练习创建与遍历 Slice 与 Map- 理解 make 与 new- 写一个词频统计器（word counter） Day 5 并发基础：goroutine + channel - 启动多个 goroutine 打印任务编号- 使用 channel 汇总结果- 写一个并发 worker pool Day 6 同步机制：sync + context - 使用 sync.WaitGroup 控制 goroutine 结束- 写一个带超时的 context 任务- 理解 select 与 channel 超时 Day 7 小项目复盘：并发爬虫 - 实现：并发请求一组 URL，输出状态码与耗时- 使用 goroutine + WaitGroup + channel 汇总结果 阶段 2：Web 服务与数据库实战（Day 8–14） 🧠 目标：掌握 Gin + GORM + JWT + Viper 的使用 天数 主题 任务与练习 Day 8 Gin 入门 - 安装 Gin- 编写基础路由 /ping, /hello/:name- 使用 POST 接收 JSON Day 9 中间件与统一响应 - 编写 Logging Middleware- 返回标准格式：{\u0026quot;code\u0026quot;:0,\u0026quot;msg\u0026quot;:\u0026quot;ok\u0026quot;,\u0026quot;data\u0026quot;:{}}- 全局异常恢复 Day 10 GORM ORM 基础 - 初始化数据库连接（SQLite 或 PostgreSQL）- 定义 User 模型并自动建表- 实现基础 CRUD Day 11 配置管理：Viper - 使用 viper 读取 .env / config.yaml- 设置多环境配置（dev/prod） Day 12 用户注册 + 登录 + JWT - 安装 github.com/golang-jwt/jwt/v5- 编写 /register /login- JWT 中间件鉴权 Day 13 日志系统 - 使用 zap 替换 fmt.Println- 日志包含 traceID / request path- 按天滚动日志文件 Day 14 阶段项目复盘 项目：UserService- 注册 / 登录 / 获取用户信息- 使用 Gin + GORM + JWT + Zap 组合完成 Demo 阶段 3：多租户架构与上下文传递（Day 15–21） 🧠 目标：理解租户隔离 + 实现租户中间件 天数 主题 任务与练习 Day 15 多租户概念与三种模型 - 理解 DB 级 / Schema 级 / Row 级（tenant_id）隔离差异- 选择你项目的隔离策略（建议 Row 级） Day 16 模型扩展 tenant_id - 修改所有表结构添加 tenant_id 字段- 写 GORM hook：自动注入 tenant_id Day 17 请求上下文传递 - Middleware 读取 Header 中的 X-Tenant-ID- 注入到 context.Context 并传入 GORM Day 18 多数据源支持 - 设计 map[string]*gorm.DB 缓存连接池- 允许按租户动态切换数据源 Day 19 路由分组与租户验证 - 添加租户中间件：验证租户合法性- 限制跨租户访问 Day 20 RBAC 权限控制 - 表设计：tenant, user, role, user_role- 定义角色枚举（ADMIN / USER）- 编写权限检查中间件 Day 21 阶段项目复盘 项目：TenantHub- 多租户用户系统- Header 传递租户- 每租户独立用户空间 阶段 4：工程化、测试与部署（Day 22–28） 🧠 目标：形成可部署的生产级项目骨架 天数 主题 任务与练习 Day 22 项目结构重构 - 使用 Clean Architecture- 拆分目录：cmd/, internal/, pkg/- 抽象 service/dao/controller Day 23 单元测试与 Mock - 使用 testing + testify/assert- 对 service 层编写测试- 模拟数据库操作 Day 24 CLI 工具开发 - 创建 cmd/migrate/main.go- 支持命令：create-tenant, migrate Day 25 Docker 化部署 - 编写多阶段 Dockerfile- 使用 docker-compose 启动 Postgres + API 服务 Day 26 性能与并发优化 - 使用 go test -bench . 做基准测试- 优化 goroutine 复用、连接池大小 Day 27 CI/CD 与版本管理 - 用 Makefile 或 GitHub Actions 构建发布包- 输出二进制 ./bin/mthub Day 28 项目总结与发布 - 写 README- 总结架构与经验- 打包上传到私有镜像仓库（可选） 🧱 最终成果（Day 28 完成后） 你将拥有：\n显示已折叠代码（19 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 multi-tenant-hub/ ├── cmd/ │ └── server/main.go # 主入口 ├── internal/ │ ├── config/ │ ├── controller/ │ ├── service/ │ ├── dao/ │ ├── middleware/ │ └── model/ ├── pkg/ │ ├── jwt/ │ ├── tenant/ │ └── logger/ ├── configs/ │ └── config.yaml ├── Dockerfile ├── docker-compose.yml └── go.mod ⚙️ 推荐工具与命令 热重载：air\n代码格式化：go fmt ./...\n依赖管理：go mod tidy\n编译优化：go build -ldflags \u0026quot;-s -w\u0026quot;\n测试命令：go test ./... -v\n","permalink":"/posts/go-%E6%80%8E%E4%B9%88%E5%AD%A6/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-note\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 16h.01\"/\u003e\u003cpath d=\"M12 8v4\"/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Note\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e突发奇想，想做一个镜像站，不想用 Java，觉得 go 可以试试，问问 gpt 速学 go可不可能。让他给我安排 28 天的计划如下。后续开启 go-learn 进行学习\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\u003ch2 id=\"-go-学习每日任务清单28-天--每天-2-小时\"\u003e🗓 Go 学习每日任务清单（28 天 × 每天 2 小时）\u003c/h2\u003e\n\u003chr\u003e\n\u003ch2 id=\"阶段-1go-基础与并发模型day-17\"\u003e\u003cstrong\u003e阶段 1：Go 基础与并发模型（Day 1–7）\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"-目标掌握-go-核心语法--并发思想\"\u003e🧠 目标：掌握 Go 核心语法 + 并发思想\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e天数\u003c/th\u003e\n          \u003cth\u003e主题\u003c/th\u003e\n          \u003cth\u003e任务与练习\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eGo 环境与项目结构\u003c/td\u003e\n          \u003ctd\u003e- 安装 Go- 运行 \u003ccode\u003ego mod init demo\u003c/code\u003e- 了解 \u003ccode\u003ego run\u003c/code\u003e, \u003ccode\u003ego build\u003c/code\u003e, \u003ccode\u003ego fmt\u003c/code\u003e, \u003ccode\u003ego test\u003c/code\u003e- 写出第一个 “Hello, Go!”\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e基础语法与函数\u003c/td\u003e\n          \u003ctd\u003e- 学习变量声明、常量、函数、多返回值、error- 练习：写一个简单计算器函数- 理解 \u003ccode\u003edefer\u003c/code\u003e、\u003ccode\u003epanic\u003c/code\u003e、\u003ccode\u003erecover\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 3\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eStruct 与方法\u003c/td\u003e\n          \u003ctd\u003e- 定义一个 \u003ccode\u003eUser\u003c/code\u003e 结构体并实现 \u003ccode\u003eHello()\u003c/code\u003e 方法- 使用指针接收者 vs 值接收者- 熟悉 JSON 序列化 (\u003ccode\u003eencoding/json\u003c/code\u003e)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 4\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eSlice / Map / Range / 指针\u003c/td\u003e\n          \u003ctd\u003e- 练习创建与遍历 Slice 与 Map- 理解 \u003ccode\u003emake\u003c/code\u003e 与 \u003ccode\u003enew\u003c/code\u003e- 写一个词频统计器（word counter）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 5\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e并发基础：goroutine + channel\u003c/td\u003e\n          \u003ctd\u003e- 启动多个 goroutine 打印任务编号- 使用 channel 汇总结果- 写一个并发 worker pool\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 6\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e同步机制：sync + context\u003c/td\u003e\n          \u003ctd\u003e- 使用 \u003ccode\u003esync.WaitGroup\u003c/code\u003e 控制 goroutine 结束- 写一个带超时的 context 任务- 理解 \u003ccode\u003eselect\u003c/code\u003e 与 channel 超时\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 7\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e小项目复盘：并发爬虫\u003c/td\u003e\n          \u003ctd\u003e- 实现：并发请求一组 URL，输出状态码与耗时- 使用 goroutine + WaitGroup + channel 汇总结果\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"阶段-2web-服务与数据库实战day-814\"\u003e\u003cstrong\u003e阶段 2：Web 服务与数据库实战（Day 8–14）\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"-目标掌握-gin--gorm--jwt--viper-的使用\"\u003e🧠 目标：掌握 Gin + GORM + JWT + Viper 的使用\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e天数\u003c/th\u003e\n          \u003cth\u003e主题\u003c/th\u003e\n          \u003cth\u003e任务与练习\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 8\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eGin 入门\u003c/td\u003e\n          \u003ctd\u003e- 安装 Gin- 编写基础路由 \u003ccode\u003e/ping\u003c/code\u003e, \u003ccode\u003e/hello/:name\u003c/code\u003e- 使用 \u003ccode\u003ePOST\u003c/code\u003e 接收 JSON\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 9\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e中间件与统一响应\u003c/td\u003e\n          \u003ctd\u003e- 编写 Logging Middleware- 返回标准格式：\u003ccode\u003e{\u0026quot;code\u0026quot;:0,\u0026quot;msg\u0026quot;:\u0026quot;ok\u0026quot;,\u0026quot;data\u0026quot;:{}}\u003c/code\u003e- 全局异常恢复\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 10\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eGORM ORM 基础\u003c/td\u003e\n          \u003ctd\u003e- 初始化数据库连接（SQLite 或 PostgreSQL）- 定义 \u003ccode\u003eUser\u003c/code\u003e 模型并自动建表- 实现基础 CRUD\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 11\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e配置管理：Viper\u003c/td\u003e\n          \u003ctd\u003e- 使用 \u003ccode\u003eviper\u003c/code\u003e 读取 \u003ccode\u003e.env\u003c/code\u003e / \u003ccode\u003econfig.yaml\u003c/code\u003e- 设置多环境配置（dev/prod）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 12\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e用户注册 + 登录 + JWT\u003c/td\u003e\n          \u003ctd\u003e- 安装 \u003ccode\u003egithub.com/golang-jwt/jwt/v5\u003c/code\u003e- 编写 \u003ccode\u003e/register\u003c/code\u003e \u003ccode\u003e/login\u003c/code\u003e- JWT 中间件鉴权\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 13\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e日志系统\u003c/td\u003e\n          \u003ctd\u003e- 使用 \u003ccode\u003ezap\u003c/code\u003e 替换 \u003ccode\u003efmt.Println\u003c/code\u003e- 日志包含 traceID / request path- 按天滚动日志文件\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 14\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e阶段项目复盘\u003c/td\u003e\n          \u003ctd\u003e项目：\u003ccode\u003eUserService\u003c/code\u003e- 注册 / 登录 / 获取用户信息- 使用 Gin + GORM + JWT + Zap 组合完成 Demo\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"阶段-3多租户架构与上下文传递day-1521\"\u003e\u003cstrong\u003e阶段 3：多租户架构与上下文传递（Day 15–21）\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"-目标理解租户隔离--实现租户中间件\"\u003e🧠 目标：理解租户隔离 + 实现租户中间件\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e天数\u003c/th\u003e\n          \u003cth\u003e主题\u003c/th\u003e\n          \u003cth\u003e任务与练习\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 15\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e多租户概念与三种模型\u003c/td\u003e\n          \u003ctd\u003e- 理解 DB 级 / Schema 级 / Row 级（tenant_id）隔离差异- 选择你项目的隔离策略（建议 Row 级）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 16\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e模型扩展 tenant_id\u003c/td\u003e\n          \u003ctd\u003e- 修改所有表结构添加 \u003ccode\u003etenant_id\u003c/code\u003e 字段- 写 GORM hook：自动注入 tenant_id\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 17\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e请求上下文传递\u003c/td\u003e\n          \u003ctd\u003e- Middleware 读取 Header 中的 \u003ccode\u003eX-Tenant-ID\u003c/code\u003e- 注入到 \u003ccode\u003econtext.Context\u003c/code\u003e 并传入 GORM\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 18\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e多数据源支持\u003c/td\u003e\n          \u003ctd\u003e- 设计 map[string]*gorm.DB 缓存连接池- 允许按租户动态切换数据源\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 19\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e路由分组与租户验证\u003c/td\u003e\n          \u003ctd\u003e- 添加租户中间件：验证租户合法性- 限制跨租户访问\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 20\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eRBAC 权限控制\u003c/td\u003e\n          \u003ctd\u003e- 表设计：tenant, user, role, user_role- 定义角色枚举（ADMIN / USER）- 编写权限检查中间件\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 21\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e阶段项目复盘\u003c/td\u003e\n          \u003ctd\u003e项目：\u003ccode\u003eTenantHub\u003c/code\u003e- 多租户用户系统- Header 传递租户- 每租户独立用户空间\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"阶段-4工程化测试与部署day-2228\"\u003e\u003cstrong\u003e阶段 4：工程化、测试与部署（Day 22–28）\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"-目标形成可部署的生产级项目骨架\"\u003e🧠 目标：形成可部署的生产级项目骨架\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e天数\u003c/th\u003e\n          \u003cth\u003e主题\u003c/th\u003e\n          \u003cth\u003e任务与练习\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 22\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e项目结构重构\u003c/td\u003e\n          \u003ctd\u003e- 使用 Clean Architecture- 拆分目录：\u003ccode\u003ecmd/\u003c/code\u003e, \u003ccode\u003einternal/\u003c/code\u003e, \u003ccode\u003epkg/\u003c/code\u003e- 抽象 service/dao/controller\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 23\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e单元测试与 Mock\u003c/td\u003e\n          \u003ctd\u003e- 使用 \u003ccode\u003etesting\u003c/code\u003e + \u003ccode\u003etestify/assert\u003c/code\u003e- 对 service 层编写测试- 模拟数据库操作\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 24\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eCLI 工具开发\u003c/td\u003e\n          \u003ctd\u003e- 创建 \u003ccode\u003ecmd/migrate/main.go\u003c/code\u003e- 支持命令：\u003ccode\u003ecreate-tenant\u003c/code\u003e, \u003ccode\u003emigrate\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 25\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eDocker 化部署\u003c/td\u003e\n          \u003ctd\u003e- 编写多阶段 \u003ccode\u003eDockerfile\u003c/code\u003e- 使用 \u003ccode\u003edocker-compose\u003c/code\u003e 启动 Postgres + API 服务\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 26\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e性能与并发优化\u003c/td\u003e\n          \u003ctd\u003e- 使用 \u003ccode\u003ego test -bench .\u003c/code\u003e 做基准测试- 优化 goroutine 复用、连接池大小\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 27\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eCI/CD 与版本管理\u003c/td\u003e\n          \u003ctd\u003e- 用 \u003ccode\u003eMakefile\u003c/code\u003e 或 GitHub Actions 构建发布包- 输出二进制 \u003ccode\u003e./bin/mthub\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eDay 28\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e项目总结与发布\u003c/td\u003e\n          \u003ctd\u003e- 写 README- 总结架构与经验- 打包上传到私有镜像仓库（可选）\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"-最终成果day-28-完成后\"\u003e🧱 最终成果（Day 28 完成后）\u003c/h2\u003e\n\u003cp\u003e你将拥有：\u003c/p\u003e","title":"go 怎么学"},{"content":" Note 又剁手搞了台小鸡，重温一下服务器初始化 参考文章\nLinux服务器的初步配置流程 - 阮一峰的网络日志\n之前我写的 博客服务器初始化 | 安落滢 Blog但是写的比较简单，这次稍微整理一下格式和一些细节,并且加上一些安全配置\n那么文章是老文章了，所以新的流程会有一丢丢变化，可以偷一些懒\n📌 前提 本文假设你已经：\n安装好最新的 Ubuntu Server LTS（例如 Ubuntu 24.04 LTS 或 22.04 LTS）系统；参考官方安装流程可以先完成基本安装和网络配置。 有 root 或 sudo 权限来执行系统初始化操作。 步骤 内容 登录 \u0026amp; 用户 创建普通用户、禁用 root SSH 系统更新 更新升级及自动安全升级 安全配置 SSH 强化、UFW、Fail2Ban 系统增强 Chrony、Audit、安全扫描 其他 时区、Swap、常用工具 🚀 一、首次登录 \u0026amp; 用户账号设定 1. 登录服务器 1 ssh root@your_server_ip 如果是云主机一般默认创建了一个 sudo 用户，可以直接用该用户登录，而不要直接用 root 登录。\n我一般喜欢改个 hostname 和时区\n1 2 hostnamectl set-hostname \u0026lt;new-hostname\u0026gt; sudo timedatectl set-timezone Asia/Hong_Kong 2. 创建新用户（避免日常使用 root） 1 2 sudo adduser youruser sudo usermod -aG sudo youruser 👉 将 youruser 换成你自己的管理用户名。\n这避免了一直用 root 操作，有助于提高安全性。\n3. 禁止 root SSH 登录（强烈建议） ✔️ 推荐使用 SSH key 登录（不允许密码，防止暴力破解）。谢谢ssh-copy-id 下面有写\n🧰 二、系统更新与基础软件 1. 更新系统 1 2 3 4 sudo apt update sudo apt upgrade -y sudo apt autoremove -y sudo apt clean 确保系统内核和软件包是最新的。\n2. 安装日常工具 1 sudo apt install -y htop curl wget lsof git vim net-tools unzip build-essential 可以根据实际需求添加其他工具，如 nano 等。\n3. 安装自动安全更新（Unattended Upgrades） 1 2 sudo apt install unattended-upgrades sudo dpkg-reconfigure unattended-upgrades gpt 建议我这样做。我试试\n🔐 三、SSH \u0026amp; 防火墙配置强化 1. 配置 UFW（Uncomplicated Firewall） 1 2 sudo ufw allow OpenSSH sudo ufw enable 检查状态：\n1 sudo ufw status 只开放需要的端口。\n2. 安装 Fail2Ban（阻止暴力攻击） 1 sudo apt install fail2ban Fail2Ban 监控失败登录尝试并自动屏蔽恶意 IP。\n🔎 四、安全加固建议（生产级） 这些是当下较常见的进一步加固措施：\n1. 禁止 SSH 密码登录 \u0026amp; 强制使用 Key SSH 配置中如前所示设置：\n1 PasswordAuthentication no 强制仅使用 Key 登录。\n2. 安全增强工具 安装并启用 AppArmor：\n1 2 sudo apt install apparmor apparmor-utils sudo aa-enforce /etc/apparmor.d/* 3. 系统审计 \u0026amp; 漏洞扫描 安装 Lynis 做安全扫描：\n1 2 sudo apt install lynis sudo lynis audit system 帮助发现潜在安全隐患。\n4. 强密码 \u0026amp; 密码策略 可以使用 PAM 密码策略增强密码强度：\n1 sudo apt install libpam-cracklib 修改 /etc/pam.d/common-password 设置强口令规则。\n📊 五、监控与时钟同步 1. 安装 Chrony（时间同步） 1 sudo apt install chrony 这是比传统 ntp 更稳健的时间服务。\n2. 基本监控工具 用的 netdata\n提供 UI 监控机器的 CPU/内存/网络等。\n📦 六、其他基础设置 1. 设置时区 1 sudo timedatectl set-timezone Asia/Shanghai 可根据地域调整。\n2. 配置 Swap（如果是无 Swap 系统） 1 2 3 4 sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile 并写入 /etc/fstab 保持重启后生效。\n","permalink":"/posts/%E6%96%B0%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9D%E5%A7%8B%E5%8C%96/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-note\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 16h.01\"/\u003e\u003cpath d=\"M12 8v4\"/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Note\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e又剁手搞了台小鸡，重温一下服务器初始化\n参考文章\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.ruanyifeng.com/blog/2014/03/server_setup.html\"\u003eLinux服务器的初步配置流程 - 阮一峰的网络日志\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e之前我写的 博客\u003ca href=\"https://blog.anluoying.com/posts/%E9%98%BF%E9%87%8C%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9D%E5%A7%8B%E5%8C%96/\"\u003e服务器初始化 | 安落滢 Blog\u003c/a\u003e但是写的比较简单，这次稍微整理一下格式和一些细节,并且加上一些安全配置\u003c/p\u003e\n\u003cp\u003e那么文章是老文章了，所以新的流程会有一丢丢变化，可以偷一些懒\u003c/p\u003e","title":"新服务器初始化 2"},{"content":"作者：AdsPower — 2024-04-29 整理者：gpt5.1\nNote 虽说摘自他山，但是这是开启什么系列以来第一个合适的命题，干脆直接塞进分类充数。跑起来最重要。\n简介 当你访问网站（例如社交网站 / 电商网站）时，网站可能会悄悄收集你浏览器 / 设备的各种细节 —— 比如 IP、所用操作系统、屏幕分辨率、安装字体等。 这些看似无关紧要的信息组合在一起，就可能构成一个“浏览器指纹”（browser fingerprint），使网站能够识别你为一个独特访问者 —— 即使你清除 Cookies 或启用了“隐私模式”。\n理解常见的浏览器指纹项，有助于：\n理解网站为何／如何“识别”你；\n在使用“指纹浏览器”或“反指纹／虚拟环境”时，更理性地设计你的环境配置；\n提高多个账号并行运营时（例如电商、社媒、营销等）的安全性与隐蔽性。\n为什么网站要收集浏览器指纹 浏览器指纹可以让网站“记住你是谁”。即便你清除 Cookies，网站也可能通过指纹判断“你回来过”。\n对于电商网站 / 社交平台，识别用户是否“真实”、是否“同人 操作多个账号”很重要，指纹就是一个重要依据。\n对于需要管理多个账号（例如营销、社媒运营、电商多店铺等）的用户／团队，了解、控制指纹可以帮助减少账号关联、降低被封禁／风控的风险。\n常见的 16 种浏览器指纹及其含义 以下是文章列出的 16 项指纹（fingerprint）及其作用／特性：\n# 指纹类型 含义 / 来源 / 作用 1 IP 地址 用于识别用户设备的网络地址，是最基础的识别方式。 2 地理位置 通过 IP 推断出用户所在国家 / 地区 /城市甚至经纬度／邮政编码等。 3 时区与当地时间 基于地理位置或系统设置，可以反映用户的“真实”或“伪装”时区信息；如果与 IP 的地理位置不一致，则可能被检测为异常。 4 User Agent（UA） 表示浏览器类型、版本、操作系统等信息，是常见且重要的指纹组成。 5 浏览器语言 浏览器设置的首选语言 (Accept-Language) 。不同语言设置也可能被用作识别。 6 字体 (Fonts) 设备安装的字体集合 — 不同用户可能安装不同字体，通过字体列表也能区分用户。 7 屏幕分辨率 包括显示器分辨率以及浏览器窗口的可视区域大小，不同显示设备和窗口设置会不同。 8 WebGL / WebGPU 通过图形 API 获取设备 GPU / 显示能力相关信息，作为硬件相关指纹的一部分。 9 媒体设备指纹 (Media Devices) 通过浏览器对音频 / 视频设备 (如麦克风、摄像头) 的检测获取硬件信息 (设备类型、标签、分辨率等)。 10 AudioContext 浏览器处理音频的能力 /特性 — 也可能因设备 /软件设置不同而不同，可用于区分用户。 11 ClientRects 浏览器在渲染网页时，对页面元素的位置、布局进行计算 — 这些布局信息（受设备/窗口/浏览器微差异影响）也可能差异显著。 12 SpeechVoices 浏览器／系统提供的语音合成 (Text-to-Speech) 选项 (语音种类、语言、性别等) — 设备不同可能有不同选项，也可作为指纹。 13 硬件并发数 (hardwareConcurrency) 表示设备处理器的逻辑核心数 (CPU 线程数)，通过 navigator.hardwareConcurrency 获取 — 不同设备可能不同。 14 设备内存 (deviceMemory) 设备可用内存 (RAM) 大小 (通过 navigator.deviceMemory 获取) — 虽然该值有限制 (通常返回 2／4／6／8 之一)，但仍可能被用于区分。 15 Do Not Track (DNT) 浏览器隐私设置中的 “不追踪” 标志 — 如果用户开启了 DNT，该设置也可能成为唯一性特征。 16 Canvas 指纹 (Canvas Fingerprint) 通过让浏览器在一个 \u0026lt;canvas\u0026gt; 上绘制图形／文本，然后分析其渲染结果 (轮廓／像素／抗锯齿等细节) — 因为每台设备 (硬件 + 字体 + 显卡) 不同，渲染结果可能略有不同，从而形成独特“指纹”。 使用“指纹浏览器 / 虚拟环境”保护账户安全 如果你在运营多个账号 — 比如社交媒体账号、电商店铺账号、营销账号等 — 想避免因“相同设备／环境指纹”被关联或封禁，可以考虑使用“指纹浏览器 / 虚拟浏览器环境”。例如 AdsPower 就是这样一种工具，它允许你为每个账号创建独立的浏览器环境，并对上述多个指纹项目进行自定义／模拟。\nAdsPower 的优势 支持自定义多个参数 (至少覆盖文中提到的这 16 项)，使每个账号环境指纹互不相同。\n可以为每个环境设置专属代理 (IP)、地理位置、时区、语言等，使环境看起来“像是真实独立设备”。\n在同一台电脑上可以创建多个“虚拟浏览器”，方便批量管理多个账号，同时减少关联风险。\n总结 / 个人理解 浏览器指纹 (browser fingerprint) 本质上是“设备／浏览器 + 系统 + 环境 + 配置 + 硬件 + 软件”等多重信息的综合 — 远比传统的 Cookie 更难清除，也更隐蔽。\n只依靠清除 Cookies、使用隐私模式，往往不能阻止指纹识别。除非你对几乎所有可能被利用的指纹维度都进行“伪装／匿名化”处理。\n如果你需要长期、多账号、多平台操作 (例如社媒账号运营 / 跨境电商 / 营销 / 爬虫 / 自动化脚本等)，了解并管理好指纹，是非常必要的一步。\n使用类似 AdsPower 的指纹浏览器 / 虚拟环境，是一种方便／实用／高效的做法，但也要谨慎 —— 因为任何不一致 (如 IP 和时区不匹配) 都可能被平台怀疑为异常。\n","permalink":"/posts/%E4%BB%80%E4%B9%88%E6%98%AF%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8C%87%E7%BA%B9/","summary":"\u003cp\u003e\u003cstrong\u003e作者\u003c/strong\u003e：AdsPower — 2024-04-29\n整理者：gpt5.1\u003c/p\u003e\n\u003cdiv class=\"markdown-alert markdown-alert-note\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 16h.01\"/\u003e\u003cpath d=\"M12 8v4\"/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Note\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003e虽说摘自他山，但是这是开启什么系列以来第一个合适的命题，干脆直接塞进分类充数。跑起来最重要。\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003e当你访问网站（例如社交网站 / 电商网站）时，网站可能会悄悄收集你浏览器 / 设备的各种细节 —— 比如 IP、所用操作系统、屏幕分辨率、安装字体等。\n这些看似无关紧要的信息组合在一起，就可能构成一个“浏览器指纹”（browser fingerprint），使网站能够识别你为一个\u003cstrong\u003e独特访问者\u003c/strong\u003e —— 即使你清除 Cookies 或启用了“隐私模式”。\u003c/p\u003e","title":"什么是浏览器指纹：10 分钟彻底了解 16 个指纹"},{"content":" 偷听会议室老板拷打同事知道什么是 lambda 吗？同时我正在看一篇关于使用aws mqtt的文章，里面提到了aws lambda。我一想，lambda 不就是匿名函数吗，然后呢，似乎我并不了解他，就好像我看到一只鸟，我知道他叫布谷鸟，也许我知道他还叫大杜鹃（实际不知道，我刚刚搜的），英文名叫Cuculus canorus，但是我依旧不了解他（ 费曼父亲的教导），那么我深深反思，我不知道什么是 lambda，我现在需要知道一下。\n所以在这过程里，我问了一些问题，什么是 lambda、什么是 Lisp、什么是一等公民、什么是闭包、python 的 map 是啥\n什么是Lambda 我有一种中国学生的特性，写东西模糊有一种框架 1. 定义 2. 内容 3. 意义 4. 展望。 感觉这很蠢，但我暂时没有更棒的方式，当然这种文章不会有展望\n这个就读 Lambda λ ，希腊字母表第 11 个字母 Lambda - Wikipedia\n为什么匿名函数叫 Lambda Anonymous function - Wikipedia\nAnonymous functions originate in the work of Alonzo Church in his invention of the lambda calculus, in which all functions are anonymous, in 1936, before electronic computers.[2] In several programming languages, anonymous functions are introduced using the keyword lambda, and anonymous functions are often referred to as lambdas or lambda abstractions. Anonymous functions have been a feature of programming languages since Lisp in 1958, and a growing number of modern programming languages support anonymous functions.\n36 年联合抗日呢，该死的小鬼子，Alonzo发明了 lambda 微积分，其中所有函数都是匿名的，当时还没有电子计算机，但是在之后就引用了这个名字，这里有个 Lisp，第一次听说这个语言是 李继刚老师写提示词给 claude 的时候用的，当时看了一眼，我滴天，这是啥？\n以下用 python 做例子，我也只会 python 和 javascript\npython 中的 lambda 函数就像这样\npython 1 2 3 4 5 6 7 8 9 10 # no lambda def double(x): return x * 2 nums = [1, 2, 3] result = map(double, nums) # use lambda nums = [1, 2, 3] result = map(lambda x: x * 2, nums) 语法就像是 lambda 参数: 表达式 等价于\n1 2 def 函数名(参数): return 表达式 什么是 Lisp 函数式编程 (Functional Programming, FP) (像 C, Java, Python 的基础写法）叫命令式编程 (Imperative Programming)\nLisp - Wikipedia\ngemini3 解释\n函数式编程 (FP) 的思维方式完全不同：\n函数式： 像是在设计流水线（数学公式）。 它不关心“怎么一步步做”，它只关心“数据怎么转换”。 核心思想： 不可变 (Immutability)： 数据一旦创建就不能改。你想把 x 变成 x+1？不行，你必须创建一个新的 y 等于 x+1。 纯函数 (Pure Function)： 函数不能有副作用（比如修改全局变量、打印东西）。给它同样的输入，它永远吐出同样的输出。 函数是“一等公民”： 函数可以像数字、字符串一样，被传参数、被返回、被赋值给变量。 例子：\nLisp 1 2 3 4 (defun factorial (n) (if (\u0026lt;= n 1) 1 (* n (factorial (- n 1))))) (defun factorial (n) \u0026hellip;)：定义函数 factorial，参数是 n。 (if (\u0026lt;= n 1) \u0026hellip;)：如果 n 小于等于 1。 (* n (factorial (- n 1)))：计算 n 乘以 (n-1) 的阶乘。 这不是很好读的样子，至少作为代码来说，我不喜欢，数括号很花时间 这是老祖宗级别的语言，虽然现在使用率应该非常低，但是他有很多灵魂都活到了现在 （Lambda, 闭包, 动态类型）\n有意思的是，python 的 lambda 是社区贡献者引入的，这个特性 python 作者并不喜欢（我也不喜欢≈我和 python 作者差不多） The fate of reduce() in Python 3000\n在这里我再次触发了初学 javascript 的疑问。一等公民和闭包\n什么是一等公民和闭包 一等公民（First-class citizen） 頭等物件 - 维基百科，自由的百科全书\n有一等就有二等，下面是 gemini 给的解释 闭包（Closure） 闭包 (计算机科学) - 维基百科，自由的百科全书\n闭包的概念是在1960年代为采用lambda演算的表达式的机器求值而开发的，它首次在1970年于PAL编程语言中完全实现，用来支持词法作用域的头等函数。\n哈基米装萌给我一个他认为很通俗的解释\n闭包 = 函数 + 它“出生”时的环境（随身携带的数据）。\n通常情况下，一个函数跑完，它里面的变量就会被销毁（像是一个临时的酒店房间，你退房了，清洁工就把东西清空了）。\n但是，如果这个函数在内部定义了一个小函数，并且把这个小函数送（return）了出来：\n这个小函数会背着一个背包，背包里装了它在“老家”用到的变量。即使“老家”已经被销毁了，这个小函数依然能从背包里拿出那些变量来用。\n这个“背着背包的小函数”，就叫闭包。\n来个例子\n[python] 显示已折叠代码（21 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def make_multiplier(n): # --- 这是一个外部函数（老家） --- # n 是这里的局部变量 def multiplier(x): # --- 这是一个内部函数（孩子） --- # 注意：这里用到了外部的 n return x * n return multiplier # 把内部函数送出去 #“一等公民”权利：**函数可以作为返回值** # 1. 创建一个“乘以 3”的函数 times3 = make_multiplier(3) # times3 就是闭包，他不仅带着 x*n 还带着 n=3 # 此时，make_multiplier(3) 已经运行结束了！ # 按理说，变量 n=3 应该消失了。 # 2. 但是，当我们调用 times3 时... print(times3(10)) # 输出 30 闭包在实现上是一个结构体，它存储了一个函数（通常是其入口地址）和一个关联的环境（相当于一个符号查找表）。 具体的 wiki 里面写的很详细，可以细细看一遍\npython的map是啥 这个 map 混淆到我了。Python map() 函数 | 菜鸟教程 确实没怎么用过\nAWS Lambda AWS Lambda 是亚马逊云服务中的无服务器计算平台，它命名灵感来自 lambda 演算中的匿名函数，但本质是事件驱动的小型计算单元，不同于编程语言里的 lambda 函数实现。不过它们都体现了“小而灵活”的思想。\n最后 我就来记下了这些内容，感谢 AI，（希望科技继续发展。早日实现赛博飞升。）开始说梦话\n","permalink":"/posts/lambda-%E5%BC%95%E5%8F%91%E7%9A%84%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E9%99%88%E5%B9%B4%E7%9F%A5%E8%AF%86%E5%9B%9E%E9%A1%BE/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e偷听会议室老板拷打同事知道什么是 lambda 吗？同时我正在看一篇关于使用aws mqtt的文章，里面提到了aws lambda。我一想，lambda 不就是匿名函数吗，然后呢，似乎我并不了解他，就好像我看到一只鸟，我知道他叫布谷鸟，也许我知道他还叫大杜鹃（实际不知道，我刚刚搜的），英文名叫Cuculus canorus，但是我依旧不了解他（ \u003ccode\u003e费曼父亲的教导\u003c/code\u003e），那么我深深反思，我不知道什么是 lambda，我现在需要知道一下。\u003c/p\u003e","title":"lambda 引发的编程语言陈年知识回顾"},{"content":"凌晨总是那么美妙，那种静谧。\n一个人洗完澡，喝杯水，身上保持干爽，头脑略微犯困却又不至于昏昏沉沉，反而是更加灵敏的时候，看点书在这个时候最棒最棒了，有一种独坐在宇宙中心尽情汲取能量的感觉。\n今晚看了一点点故园风雨后，翻开过三四次，每次五六页，还不足以启动这趟新的旅程，就像推动静止物体所需的力，大于物体在运动过程中推动他的力一样，需要一段启动的过程。\n书讲什么的，没仔细看，只是看评价不错，尺寸小小的，深得我意。开篇描述战争期间军士官的自述，我虽然选择过学历史，可是我不爱记也不爱特意了解，读的书也少，肚子里没啥货，所以我也不知道背景是什么战争，有提到希特勒，那就是二战了，我不特意了解太多背景，像是预习或者调研，有时候看书爱保留一些朦胧感。看书时像品红酒一样，其实不必要知道太多背景，那是他的title or tag，可以先来一口，闻一闻，抿一抿，酒总是刺痛滚进喉咙，除了米酒会给点甜头，酒并不好喝的，大人爱喝，大概我还小，今晚在对象家和她家人喝了几杯，我酒量还好，预估是一斤白酒的感觉，不过伤身伤脑，还是少喝为妙，能偷就偷一点。\n书有满满的译制腔味，脑海总是浮现出那类声音\n哦我的上帝，实在是不早了，早点休息吧，明天还要上班呢！\n晚安😴\n","permalink":"/posts/%E5%87%8C%E6%99%A8%E6%80%BB%E6%98%AF%E9%82%A3%E4%B9%88%E7%BE%8E%E5%A6%99%E9%82%A3%E7%A7%8D%E9%9D%99%E8%B0%A7/","summary":"\u003cp\u003e凌晨总是那么美妙，那种静谧。\u003c/p\u003e\n\u003cp\u003e一个人洗完澡，喝杯水，身上保持干爽，头脑略微犯困却又不至于昏昏沉沉，反而是更加灵敏的时候，看点书在这个时候最棒最棒了，有一种独坐在宇宙中心尽情汲取能量的感觉。\u003c/p\u003e\n\u003cp\u003e今晚看了一点点故园风雨后，翻开过三四次，每次五六页，还不足以启动这趟新的旅程，就像推动静止物体所需的力，大于物体在运动过程中推动他的力一样，需要一段启动的过程。\u003c/p\u003e","title":"凌晨总是那么美妙，那种静谧。"},{"content":" gemini3 发布几天了。都说很叼，前端很叼，画页面很牛。那就帮我画个工具箱吧\n设计 于是打开 chatgpt 和他讨论一下，主要思想就是我希望前端能够非常的模块化\n写一个工具的时候，不需要关注太多和我网页的配合。按照一定的规范写完工具后放在指定位置就行了。基于 git 我就可以审核后发布，前端就可以在某个目录某个地方看到这个工具，搜到这个工具。暂时不考虑性能问题\ngemini3 目前我两个用的地方，一个在 ai studio 里面直接用，另一个就是在 cursor 里面用了，姑且信一把 cursor\n开搓 接一杯水的功夫出来了。这个设计很符合我的概念（但是我本能的觉得性能影响很大，以后堆积越来越多的工具，一打开我的网站，啪的一下要下载好几十MB 的内容。）\n编译预览一下，这期间有一点小问题，反正我也不是很懂前端，他给修好了 随后上点强度，一口气提了 9 个比较常见的网页工具，一次性完成。\n可以，很满意哦，回头做个深色浅色模式随系统切换\n部署 那么想把它部署起来，在这整个工具箱的设计里，是存在后端的，因为对于我的需求而言 有一些工具我是希望他有历史记录的，当然量不是很大。所以一开始我是想部署在自己的 VPS 上。但是想了一圈后犯懒了（原本想基于 github 的 webhook 或者 actions build \u0026amp; push docker images，服务器搞个小 deploy agent 等着更新就行）有 github pages，不如直接放在 pages 里就好了\n叫 g 老师给写个 actions\n关于 actions 的文档和介绍 GitHub Actions 入门教程 - 阮一峰的网络日志 GitHub Actions 文档 - GitHub 文档\n因为我的仓库内有前后端的代码。所以 working-directory 指定 frontend\n[yaml] 显示已折叠代码（38 行） 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 name: Build and Deploy to GitHub Pages on: push: branches: - main workflow_dispatch: permissions: contents: write pages: write id-token: write jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 - name: Install dependencies run: npm install working-directory: ./frontend - name: Build run: npm run build working-directory: ./frontend - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./frontend/dist GitHub Pages 文档 - GitHub 文档 关于自定义域名和 GitHub 页面 - GitHub 文档 ok 耐心等个几分钟。之后就可以愉快的访问我自己的工具箱啦\n问题 从 2022 年起，GITHUB_TOKEN 的默认权限 变成只读 (read only)，所以默认情况下它 没权限 向仓库写（push commit）了 Permissions denied to github actions bot while building documentation - General Usage - Julia Programming Language 改成下面这样就行了\n1 2 3 permissions: contents: write pages: write ","permalink":"/posts/%E6%90%93%E4%BA%86%E4%B8%AA%E5%B7%A5%E5%85%B7%E7%AE%B1+github-pages/","summary":"\u003cblockquote\u003e\n    \u003cp\u003egemini3 发布几天了。都说很叼，前端很叼，画页面很牛。那就帮我画个工具箱吧\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"设计\"\u003e设计\u003c/h2\u003e\n\u003cp\u003e于是打开 chatgpt 和他讨论一下，主要思想就是我希望前端能够非常的模块化\u003c/p\u003e\n\u003cp\u003e写一个工具的时候，不需要关注太多和我网页的配合。按照一定的规范写完工具后放在指定位置就行了。基于 git 我就可以审核后发布，前端就可以在某个目录某个地方看到这个工具，搜到这个工具。暂时不考虑性能问题\u003c/p\u003e","title":"搓了个工具箱+github pages"},{"content":" 昨天抄来改改一篇很长的 cc 教程，里面有不少图片，发现我的主题似乎没法点击放大图片，于是求助 gemini3\n实现方式 用Hugo Render Hook（渲染钩子），它接管了 Markdown 中图片语法 ![Alt](src \u0026quot;Title\u0026quot;) 的渲染过程。 用 fancybox 库实现图片放大之类的 代码 layouts/_default/_markup/render-image.html 修改渲染逻辑\n[html] 显示已折叠代码（40 行） 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 {{- /* 1. 解析 Markdown 里的图片路径 */ -}} {{- $u := urls.Parse .Destination -}} {{- $src := $u.String -}} {{- /* 2. 如果不是绝对路径（如 http://...），则尝试在资源中查找 */ -}} {{- if not $u.IsAbs -}} {{- $path := strings.TrimPrefix \u0026#34;./\u0026#34; $u.Path }} {{- /* 3. 尝试从“页面资源”或“全局资源”中查找该图片 */ -}} {{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}} {{- /* 4. 如果找到了，获取它在网站最终生成的正确相对链接 */ -}} {{- $src = .RelPermalink -}} {{- /* 5. 假如原链接带参数(?size=small)或锚点(#top)，把它们拼回去 */ -}} {{- with $u.RawQuery -}} {{- $src = printf \u0026#34;%s?%s\u0026#34; $src . -}} {{- end -}} {{- with $u.Fragment -}} {{- $src = printf \u0026#34;%s#%s\u0026#34; $src . -}} {{- end -}} {{- end -}} {{- end -}} {{- /* 6. 构建 img 标签属性：合并默认属性和自动生成的属性 */ -}} {{- $attributes := merge .Attributes (dict \u0026#34;alt\u0026#34; .Text \u0026#34;src\u0026#34; $src \u0026#34;title\u0026#34; (.Title | transform.HTMLEscape) \u0026#34;loading\u0026#34; \u0026#34;lazy\u0026#34;) -}} \u0026lt;div class=\u0026#34;post-img-view\u0026#34;\u0026gt; \u0026lt;!-- href: 指向图片大图的链接（Fancybox 需要知道点开后显示什么） data-fancybox=\u0026#34;gallery\u0026#34;: 告诉 Fancybox JS 插件，这个链接要用灯箱打开，而不是跳转页面 --\u0026gt; \u0026lt;a data-fancybox=\u0026#34;gallery\u0026#34; href=\u0026#34;{{ $src }}\u0026#34;\u0026gt; \u0026lt;img {{- range $k, $v := $attributes -}} {{- if $v -}} {{- printf \u0026#34; %s=%q\u0026#34; $k $v | safeHTMLAttr -}} {{- end -}} {{- end -}}\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; layouts/partials/extend_head.html 引入 fancybox 资源\nhtml 1 2 3 4 5 6 7 8 9 10 11 12 {{- if or .Site.Params.fancybox (not (isset .Site.Params \u0026#34;fancybox\u0026#34;)) }} \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css\u0026#34;/\u0026gt; \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; // 等待 DOM 加载完成 document.addEventListener(\u0026#34;DOMContentLoaded\u0026#34;, function() { Fancybox.bind(\u0026#39;[data-fancybox=\u0026#34;gallery\u0026#34;]\u0026#39;, { // Your custom options }); }); \u0026lt;/script\u0026gt; {{- end }} ","permalink":"/posts/hugo-%E7%9A%84%E5%8D%9A%E5%AE%A2%E7%82%B9%E5%87%BB%E5%9B%BE%E7%89%87%E6%94%BE%E5%A4%A7/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e昨天抄来改改一篇很长的 cc 教程，里面有不少图片，发现我的主题似乎没法点击放大图片，于是求助 gemini3\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"实现方式\"\u003e实现方式\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e用Hugo Render Hook（渲染钩子），它接管了 Markdown 中图片语法 \u003ccode\u003e![Alt](src \u0026quot;Title\u0026quot;)\u003c/code\u003e 的渲染过程。\u003c/li\u003e\n\u003cli\u003e用 fancybox 库实现图片放大之类的\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003elayouts/_default/_markup/render-image.html\u003c/code\u003e 修改渲染逻辑\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdetails class=\"code-details\"\u003e\n  \u003csummary\u003e\n      \u003cspan class=\"code-summary-left\"\u003e\n          \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n          \u003cspan\u003e[html] 显示已折叠代码（40 行）\u003c/span\u003e\n      \u003c/span\u003e\n  \u003c/summary\u003e\n  \u003cdiv class=\"collapsible-content\"\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{- /* 1. 解析 Markdown 里的图片路径 */ -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{- $u := urls.Parse .Destination -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{- $src := $u.String -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{- /* 2. 如果不是绝对路径（如 http://...），则尝试在资源中查找 */ -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{- if not $u.IsAbs -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  {{- $path := strings.TrimPrefix \u0026#34;./\u0026#34; $u.Path }}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  {{- /* 3. 尝试从“页面资源”或“全局资源”中查找该图片 */ -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  {{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    {{- /* 4. 如果找到了，获取它在网站最终生成的正确相对链接 */ -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    {{- $src = .RelPermalink -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    {{- /* 5. 假如原链接带参数(?size=small)或锚点(#top)，把它们拼回去 */ -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    {{- with $u.RawQuery -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      {{- $src = printf \u0026#34;%s?%s\u0026#34; $src . -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    {{- end -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    {{- with $u.Fragment -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      {{- $src = printf \u0026#34;%s#%s\u0026#34; $src . -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    {{- end -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  {{- end -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{- end -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{- /* 6. 构建 img 标签属性：合并默认属性和自动生成的属性 */ -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{- $attributes := merge .Attributes (dict \u0026#34;alt\u0026#34; .Text \u0026#34;src\u0026#34; $src \u0026#34;title\u0026#34; (.Title | transform.HTMLEscape) \u0026#34;loading\u0026#34; \u0026#34;lazy\u0026#34;) -}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e \u003cspan class=\"na\"\u003eclass\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;post-img-view\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e       href: 指向图片大图的链接（Fancybox 需要知道点开后显示什么）\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e       data-fancybox=\u0026#34;gallery\u0026#34;: 告诉 Fancybox JS 插件，这个链接要用灯箱打开，而不是跳转页面 \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e    --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ea\u003c/span\u003e \u003cspan class=\"na\"\u003edata-fancybox\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;gallery\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ehref\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;{{ $src }}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003eimg\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"err\"\u003e{{\u003c/span\u003e\u003cspan class=\"na\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003erange\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"na\"\u003ek\u003c/span\u003e\u003cspan class=\"err\"\u003e,\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"na\"\u003ev\u003c/span\u003e \u003cspan class=\"na\"\u003e:\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e$attributes\u003c/span\u003e \u003cspan class=\"na\"\u003e-\u003c/span\u003e\u003cspan class=\"err\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"err\"\u003e{{\u003c/span\u003e\u003cspan class=\"na\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003eif\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"na\"\u003ev\u003c/span\u003e \u003cspan class=\"na\"\u003e-\u003c/span\u003e\u003cspan class=\"err\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"err\"\u003e{{\u003c/span\u003e\u003cspan class=\"na\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003eprintf\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"err\"\u003e%\u003c/span\u003e\u003cspan class=\"na\"\u003es\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e%q\u0026#34;\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"na\"\u003ek\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"na\"\u003ev\u003c/span\u003e \u003cspan class=\"err\"\u003e|\u003c/span\u003e \u003cspan class=\"na\"\u003esafeHTMLAttr\u003c/span\u003e \u003cspan class=\"na\"\u003e-\u003c/span\u003e\u003cspan class=\"err\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"err\"\u003e{{\u003c/span\u003e\u003cspan class=\"na\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003eend\u003c/span\u003e \u003cspan class=\"na\"\u003e-\u003c/span\u003e\u003cspan class=\"err\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"err\"\u003e{{\u003c/span\u003e\u003cspan class=\"na\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003eend\u003c/span\u003e \u003cspan class=\"na\"\u003e-\u003c/span\u003e\u003cspan class=\"err\"\u003e}}\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cp\u003e\u003ccode\u003elayouts/partials/extend_head.html\u003c/code\u003e 引入 fancybox 资源\u003c/p\u003e","title":"hugo 的博客点击图片放大"},{"content":"如何查看当前网飞的画质 播放状态下，按Ctrl + Alt + Shift +q 显示当前串流状态，如图所示。我当前是2K的状态，但是之前一直好奇怎么调整，今天一位L站佬友给解惑了 如何看上4K 原文 【教學】如何讓Windwos 電腦看Netflix 4K HDR 全面一次搞懂 - 瘋先生 避免原链接失效。我这里也记录一下大概吧。 总之要看上4K的话要求挺多的\n基于该作者的介绍，设置串流画质前对网络测速一下确认自己网速状态，，按他的描述，1080p需要5Mbps，否则就是HD 720p，4K需要25Mbps，1080p只需要5Mbps，非常庆幸，我有看起来不错的网速網路速度測試 | Fast.com ![Fast.com 网速测试结果](https://imgbed.anluoying.com/2025/11/e642b55197d792b0e420c45eeb7a03b2.png =200x200)\n随后安装相关的扩展。我看了一下资料不少，日后不至于失联，那么这里就到此为止啦，因为目前没有4K以上的播放设备。所以暂时不纠结这个问题了。\n","permalink":"/posts/netflix-%E6%80%8E%E4%B9%88%E8%B0%83%E6%95%B4%E7%94%BB%E8%B4%A8/","summary":"\u003ch2 id=\"如何查看当前网飞的画质\"\u003e如何查看当前网飞的画质\u003c/h2\u003e\n\u003cp\u003e播放状态下，按\u003ccode\u003eCtrl + Alt + Shift +q\u003c/code\u003e 显示当前串流状态，如图所示。我当前是2K的状态，但是之前一直好奇怎么调整，今天一位L站佬友给解惑了\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/11/111f881102b9a59cf22efb2a66392044.png\"\u003e\n        \u003cimg alt=\"Netflix 播放统计信息界面\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/11/111f881102b9a59cf22efb2a66392044.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003ch2 id=\"如何看上4k\"\u003e如何看上4K\u003c/h2\u003e\n\u003cp\u003e原文 \u003ca href=\"https://mrmad.com.tw/netflix-4k-hdr-for-windows\"\u003e【教學】如何讓Windwos 電腦看Netflix 4K HDR 全面一次搞懂 - 瘋先生\u003c/a\u003e\n避免原链接失效。我这里也记录一下大概吧。\n总之要看上4K的话要求挺多的\u003c/p\u003e","title":"Netflix 怎么调整画质"},{"content":" 迄今为止最先进的代码助手： claude，anthropic家最强垂直领域编码模型之王，三个系列，haiku、sonnet、opus 截至写这个教程时模型全面更新到4.5版本了。\nclaude code是他们家推出的 cli agent工具，个人心中排第一没什么问题（贵应该是我的问题）\n以下使用cc 称呼 claude code\n其他 cli类的工具层出不穷，google的 gemini cli。google随着 gemini 3发布出了个antigravityGoogle Antigravity\ngemini3 发布接近一周了，从网上的反响和我的使用来讲。觉得preview还是不稳定，前端确实很惊艳，这一点从 nanobanana pro上看的出来，也许谷歌找到了他们要入场的赛道。\n编码 gpt-5.1 还是略胜一筹，不过大部分情况还是claude更强\n国产的 kimicli之类的，国产模型想打这个赛道也会兼容 cc，但是国产模型基本是上一代国外主流模型的能力\n插件类型的 如augment windsurf都还不错。但是结合我所拥有的资源 cursor是获得最容易且价格还能接受的\ncc支持的IDE Visual Studio Code（包括 Cursor 和 Windsurf 等流行分支） JetBrains IDEs（包括 PyCharm、WebStorm、IntelliJ 和 GoLand） 以下是收集的一些教程，供参考\n一、ClaudeCode 操作系统: macOS 10.15+ / Ubuntu 20.04+/Debian 10+ / Windows\n硬件: 最少 4GB RAM\n软件: Node.js 18+\n1.1、安装 安装官方 Claude Code\nBash 1 2 npm install -g @anthropic-ai/claude-code claude --version 以下是 Windows、macOS 和 Linux 系统下设置 ANTHROPIC_BASE_URL 和 ANTHROPIC_API_KEY 环境变量的详细方法：\nWindows 系统 方法1（永久设置）：配置settings.json 创建(如果不存在)或编辑 C:\\Users{用户名}.claude\\settings.json，输入以下值并保存\nBash 1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;env\u0026#34;: { \u0026#34;ANTHROPIC_AUTH_TOKEN\u0026#34;: \u0026#34;替换为您的API Key\u0026#34;, \u0026#34;ANTHROPIC_BASE_URL\u0026#34;: \u0026#34;https://code.newcli.com/claude\u0026#34;, \u0026#34;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\u0026#34;: 1 }, \u0026#34;permissions\u0026#34;: { \u0026#34;allow\u0026#34;: [], \u0026#34;deny\u0026#34;: [] } } 方法2：临时设置（仅当前终端有效） 在 PowerShell 或 CMD 中执行： PowerShell 1 2 3 4 5 6 7 # PowerShell $env:ANTHROPIC_BASE_URL=\u0026#34;https://code.newcli.com/claude\u0026#34; $env:ANTHROPIC_API_KEY=\u0026#34;替换为您的API Key\u0026#34; # CMD set ANTHROPIC_BASE_URL=https://code.newcli.com/claude set ANTHROPIC_API_KEY=替换为您的API Key 方法3：永久设置（全局生效） 图形界面：\n右键「此电脑」→「属性」→「高级系统设置」→「环境变量」 在「用户变量」或「系统变量」中新建： 变量名：ANTHROPIC_BASE_URL 变量值：https://code.newcli.com/claude 同样方法添加 ANTHROPIC_API_KEY PowerShell 永久设置：\nPowerShell 1 2 [System.Environment]::SetEnvironmentVariable(\u0026#39;ANTHROPIC_BASE_URL\u0026#39;, \u0026#39;https://code.newcli.com/claude\u0026#39;, \u0026#39;User\u0026#39;) [System.Environment]::SetEnvironmentVariable(\u0026#39;ANTHROPIC_API_KEY\u0026#39;, \u0026#39;替换为您的API Key\u0026#39;, \u0026#39;User\u0026#39;) 1. 重启终端后生效。 macOS 系统 方法1（推荐）：配置settings.json 创建或编辑 ~/.claude/settings.json，并填入以下内容 Bash 1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;env\u0026#34;: { \u0026#34;ANTHROPIC_AUTH_TOKEN\u0026#34;: \u0026#34;替换为您的API Key\u0026#34;, \u0026#34;ANTHROPIC_BASE_URL\u0026#34;: \u0026#34;https://code.newcli.com/claude\u0026#34;, \u0026#34;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\u0026#34;: 1 }, \u0026#34;permissions\u0026#34;: { \u0026#34;allow\u0026#34;: [], \u0026#34;deny\u0026#34;: [] } } 方法2：临时设置（仅当前终端有效） 在 终端 中执行： Bash 1 2 export ANTHROPIC_BASE_URL=\u0026#34;https://code.newcli.com/claude\u0026#34; export ANTHROPIC_API_KEY=\u0026#34;替换为您的API Key\u0026#34; 方法3：永久设置 编辑 shell 配置文件（根据使用的 shell 选择）： Bash 1 2 3 4 5 6 7 # 如果是 bash（默认） echo \u0026#39;export ANTHROPIC_BASE_URL=\u0026#34;https://code.newcli.com/claude\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bash_profile echo \u0026#39;export ANTHROPIC_API_KEY=\u0026#34;替换为您的API Key\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bash_profile # 如果是 zsh echo \u0026#39;export ANTHROPIC_BASE_URL=\u0026#34;https://code.newcli.com/claude\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.zshrc echo \u0026#39;export ANTHROPIC_API_KEY=\u0026#34;替换为您的API Key\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.zshrc 立即生效： Bash 1 source ~/.bash_profile # 或 source ~/.zshrc Linux 系统 和 mac 类似\n既然用这个。那可以关注一下之前的博客，ccs 这个sh工具也还能用claude code配置 | 安落滢 Blog\n通用验证方法 在所有系统中，可以通过以下命令验证是否设置成功：\nBash 1 2 3 4 5 6 7 8 9 10 11 # macOS/Linux echo $ANTHROPIC_BASE_URL echo $ANTHROPIC_API_KEY # Windows PowerShell echo $env:ANTHROPIC_BASE_URL echo $env:ANTHROPIC_API_KEY # Windows CMD echo %ANTHROPIC_BASE_URL% echo %ANTHROPIC_API_KEY% 关于Windows 尽管cc已经支持 windows。但是个人认为 模型的Linux知识更加雄厚，所以推荐在WSL下使用能有更好的体验\n第三方客户端 在VSCode中使用 安装官方claudecode插件\nvscode中claudecode新版本插件强制登录解决方案： 新建 ~/.claude/config.json 内容： {\u0026quot;primaryApiKey\u0026quot;: \u0026quot;fox\u0026quot;}\n二、CodeX安装教程 1、安装Codex 使用 npm 进行安装\nPlain 1 npm install -g @openai/codex mac 直接使用brew进行安装。使用时登录chatgpt账号即可。当前team账号性价比很高，量大管饱，大项目debug很合适\n四、ClaudeCode官方中文文档 claude-code/quickstart\n五、Claude Code功能 1. 直接进行交互： Claude Code 提供两种主要的交互方式： 交互模式：运行 claude 启动 REPL 会话 单次模式：使用 claude -p \u0026quot;查询\u0026quot; 进行快速命令 您可以参考： Bash 1 2 3 4 5 6 7 8 9 10 11 # 启动交互模式 claude # 以初始查询启动 claude \u0026#34;解释这个项目\u0026#34; # 运行单个命令并退出 claude -p \u0026#34;这个函数做什么？\u0026#34; # 处理管道内容 cat logs.txt | claude -p \u0026#34;分析这些错误\u0026#34; 对于 Claude Code Client的常用参数和功能，可以访问官方文档：CLI 使用和控制 - Anthropic 2. 支持连接到主流IDE 您可以直接在IDE中看到Claude Code的改动，在IDE中与其交互。 现在支持 VSCode 与 JetBrains 如果您使用Linux / MacOS，您可以直接使用该插件 如果您使用VSCode，在VSCode的内置终端唤起Claude Code，插件将被自动安装 如果您使用JetBrains，您需要通过此链接下载：Claude Code [Beta] - IntelliJ IDEs Plugin | Marketplace 您可能需要手动指定IDE，通过在Claude Code进行以下交互选择 Bash 1 \u0026gt; /ide 对于更多的用法，您可以参考Claude Code的官方文档：IDE integrations - Anthropic 3. 支持连接到Cursor： 方法一：直接安装插件 方法二、基于wsl 使用本质：在cursor/vscode中本地连接Ubuntu终端使用Claude Code，可以可视化代码的操作！步骤如下：\n序号 操作 图例 1 打开cursor 2 点击左下角终端图标 3 在弹出来的选项框里点击第三个；在弹出来的新选项框里点击Ubuntu选项，cursor就会自动连接Ubuntu系统。 连接完成后显示 没有Connect to WSL using Distro选项\n若打开只有2个选项，没有五个选项，原因是没有安装扩展，安装扩展之后重启即可。\n按下图依次点击，进入扩展界面。\n扩展详情页\n在搜索框里搜索WSL，找到图示这个扩展，不要选错，点击安装。安装过程需要翻墙，不然可能会因为网络安装失败。\n此时再点击就有5个选项，选择第三个就行！\n4. 切换模型 Claude 4 Opus 与 Claude 4 Sonnet： /model 命令进行模型切换\n5. 压缩上下文以节省额度： Claude Code 通常会有长上下文，我们建议您使用以下斜杠命令来压缩以节省点数，较长的上下文往往需要更多点数。 Bash 1 /compact [instructions] #您可以添加说明 使用 /context 查看当前上下文情况 6. 恢复以前的对话： 使用以下命令可以恢复您上次的对话 Bash 1 2 3 4 5 claude --continue or claude -c 这会立即恢复您最近的对话，无需任何提示。\n您如果需要显示时间，可以输入此命令\nBash 1 2 3 4 5 claude --resume or claude -r 这会显示一个交互式对话选择器，显示： 对话开始时间 初始提示或对话摘要 消息数量 使用箭头键导航并按Enter选择对话，您可以使用这个方法选择上下文。 7. 处理图像信息： 您可以使用以下任何方法： 将图像拖放到Claude Code窗口中（在MacOS上） 复制图像并使用Ctrl+v粘贴到CLI中（在MacOS上） 提供图像路径 Bash 1 \u0026gt; 分析这个图像：/path/to/your/image.png 您可以完全使用自然语言要求他进行工作，如： Bash 1 2 3 4 5 \u0026gt; 这是错误的截图。是什么导致了它？ \u0026gt; 这个图像显示了什么？ \u0026gt; 描述这个截图中的UI元素 \u0026gt; 生成CSS以匹配这个设计模型 \u0026gt; 什么HTML结构可以重新创建这个组件？ 8. 深入思考： 您需要通过自然语言要求其进行深入思考，或者使用tab键打开think模式 Bash 1 2 3 \u0026gt; 我需要使用OAuth2为我们的API实现一个新的身份验证系统。深入思考在我们的代码库中实现这一点的最佳方法。 \u0026gt; 思考这种方法中潜在的安全漏洞 \u0026gt; 更深入地思考我们应该处理的边缘情况 推荐在使用复杂问题的时候使用这一功能，这也会消耗大量的额度点数。 9. 通过 Claude.md 存储重要记忆：！！ 这很重要 可以使用以下命令设置一个CLAUDE.md文件来存储重要的项目信息、约定和常用命令。 Bash 1 \u0026gt; /init 包括常用命令（构建、测试、lint）以避免重复搜索 记录代码风格偏好和命名约定 添加特定于您项目的重要架构模式 CLAUDE.md记忆可用于与团队共享的指令和您的个人偏好。 更多关于记忆的设置，您可以访问此官方文档了解：Claude Code 概述 - Anthropic 在官方文档中，此部分记录了记忆的常用用法：管理Claude的内存 - Anthropic 10. 自动化 CI 和基础设施工作流程 Claude Code 提供非交互模式，用于无头执行。这在非交互上下文（如脚本、管道和 Github Actions）中运行 Claude Code 时特别有用。 使用 --print (-p) 在非交互模式下运行 Claude，如： Bash 1 claude -p \u0026#34;使用最新更改更新 README\u0026#34; --allowedTools \u0026#34;Bash(git diff:*)\u0026#34; \u0026#34;Bash(git log:*)\u0026#34; Write --disallowedTools .. 11. 上下文通用协议（MCP）： 模型上下文协议(MCP)是一个开放协议，使LLM能够访问外部工具和数据源。 这是高级功能，您可以访问此文档获取更多配置信息：Introduction - Model Context Protocol Claude Code不仅支持接入MCP，同样支持作为MCP服务器等各类高级功能，您可以访问此文档获得更多信息：教程 - Anthropic 通常可以配置 如context7，search ，或者一些数据库连接mcp来给他使用足够。过多的MCP会影响模型性能\n12. 使用Git工作树运行并行Claude Code会话： 开发者熟悉git的时候再用！！！不熟的话也是个学习的好机会，可以问他怎么用git，但是不要完全信任他，注意每一条命令的原理，执行前务必检查，数据无价\nClaude Code 支持使用自然语言操作Git，如： Bash 1 2 3 4 \u0026gt; 提交我的更改 \u0026gt; 创建一个 pr \u0026gt; 哪个提交在去年十二月添加了 markdown 测试？ \u0026gt; 在 main 分支上变基并解决任何合并冲突 您可以使用工作树创建隔离的编码环境。\n如果您您需要同时处理多个任务，并在Claude Code实例之间完全隔离代码，您可以使用此功能：\nGit工作树允许您从同一存储库中检出多个分支到单独的目录。每个工作树都有自己的工作目录，文件是隔离的，同时共享相同的Git历史。在官方Git工作树文档中了解更多。 创建新工作树 Bash 1 2 3 4 5 # 创建带有新分支的工作树 git worktree add ../project-feature-a -b feature-a # 或使用现有分支创建工作树 git worktree add ../project-bugfix bugfix-123 这会创建一个包含存储库单独工作副本的新目录\n在每个工作树中运行Claude Code Bash 1 2 3 4 5 # 导航到您的工作树 cd ../project-feature-a # 在这个隔离环境中运行Claude Code claude 在另一个终端中： Bash 1 2 cd ../project-bugfix claude 管理您的工作树 Bash 1 2 3 4 5 # 列出所有工作树 git worktree list # 完成后移除工作树 git worktree remove ../project-feature-a 每个工作树都有自己独立的文件状态，非常适合并行Claude Code会话 在一个工作树中所做的更改不会影响其他工作树，防止Claude实例相互干扰 所有工作树共享相同的Git历史和远程连接 对于长时间运行的任务，您可以让Claude在一个工作树中工作，同时您在另一个工作树中继续开发 使用描述性目录名称，以便轻松识别每个工作树的任务 记得根据项目的设置在每个新工作树中初始化开发环境。根据您的技术栈，这可能包括： JavaScript项目：运行依赖安装（npm install、yarn） Python项目：设置虚拟环境或使用包管理器安装 其他语言：遵循项目的标准设置流程 13. 其他的自然语言功能： 识别未文档化的代码 Bash 1 \u0026gt; 在auth模块中查找没有适当JSDoc注释的函数 生成文档 Bash 1 \u0026gt; 为auth.js中未文档化的函数添加JSDoc注释 理解陌生代码 Bash 1 2 3 \u0026gt; 支付处理系统做什么？ \u0026gt; 查找用户权限在哪里被检查 \u0026gt; 解释缓存层是如何工作的 智能编辑代码 Bash 1 2 3 \u0026gt; 为注册表单添加输入验证 \u0026gt; 重构日志记录器以使用新的 API \u0026gt; 修复工作队列中的竞态条件 测试或编辑您的代码 Bash 1 2 3 \u0026gt; 运行 auth 模块的测试并修复失败 \u0026gt; 查找并修复安全漏洞 \u0026gt; 解释为什么这个测试失败了 14. 常见的斜杠命令： 命令 用途 /bug 报告错误（将对话发送给 Anthropic） /clear 清除对话历史 /compact [instructions] 压缩对话，可选择焦点说明 /config 查看/修改配置 /cost 显示令牌使用统计 /doctor 检查 Claude Code 安装的健康状况 /help 获取使用帮助 /init 使用 CLAUDE.md 指南初始化项目 /login 切换 Anthropic 账户 /logout 从 Anthropic 账户登出 /memory 编辑 CLAUDE.md 记忆文件 /pr_comments 查看拉取请求评论 /review 请求代码审查 /status 查看账户和系统状态 /terminal-setup 安装 Shift+Enter 换行键绑定（仅限 iTerm2 和 VSCode） /vim 进入 vim 模式以切换插入和命令模式 15. 常用的快捷键： 使用 # 快速记忆\n通过以 # 开始输入来即时添加记忆 始终使用描述性变量名\n系统会提示你选择要将其存储在哪个记忆文件中。 终端中的换行\n使用以下方式输入多行命令：\n快速转义：输入 \\ 后按 Enter\n键盘快捷键：Option+Enter（或配置后的 Shift+Enter）\n在终端中设置 Option+Enter：\n对于 Mac Terminal.app\n打开设置 → 配置文件 → 键盘\n勾选”将 Option 键用作 Meta 键”\n对于 iTerm2 和 VSCode 终端：\n打开设置 → 配置文件 → 按键\n在常规设置下，将左/右 Option 键设置为”Esc+”\niTerm2 和 VSCode 用户提示：在 Claude Code 中运行 /terminal-setup 以自动配置 Shift+Enter 作为更直观的替代方案。\n有关配置详情，请参见官方文档：设置中的终端设置。\nVim 模式\nClaude Code 支持一部分 Vim 键绑定，可以通过 /vim 启用或通过 /config 配置。\n支持的功能包括：\n模式切换：Esc（到 NORMAL），i/I，a/A，o/O（到 INSERT）\n导航：h/j/k/l，w/e/b，0/$/^，gg/G\n编辑：x，dw/de/db/dd/D，cw/ce/cb/cc/C，.（重复）\n16. 常见的报错： 400 - invalid_request_error：您的请求格式或内容存在问题。我们也可能对下面未列出的其他 4XX 状态码使用此错误类型。\n401 - authentication_error：您的 API 密钥存在问题。\n403 - permission_error：您的 API 密钥没有使用指定资源的权限。\n404 - not_found_error：未找到请求的资源。\n413 - request_too_large：请求超过了允许的最大字节数。 建议使用/compact命令\n429 - rate_limit_error：您的账户达到了速率限制。\n500 - api_error：Anthropic 系统内部发生了意外错误。\n529 - overloaded_error：Anthropic 的 API 暂时过载。\n当 Anthropic API 在所有用户中遇到高流量时，可能会出现 529 错误。在极少数情况下，如果您的组织使用量急剧增加，您可能会看到此类错误。 为避免 529 错误，请逐步增加流量并保持一致的使用模式。\n当通过 SSE 接收流式响应时，可能在返回 200 响应后发生错误，在这种情况下错误处理不会遵循这些标准机制。\n17. 其他的高级功能： Claude Code可以被用作Claude用作类Unix工具：教程 - Anthropic Claude Code支持自定义斜杠指令：教程 - Anthropic Claude Code支持使用$ARGUMENTS添加命令参数：教程 - Anthropic Claude Code支持高级设置，您可以参考此文档：Claude Code 设置 - Anthropic 命令行参数 本地项目设置 共享项目设置 用户设置 Claude Code的安全设置，请参考此官方文档：管理权限和安全 - Anthropic ","permalink":"/posts/vibe-coding%E6%95%99%E7%A8%8Bclaude-code-%E5%92%8C-codex/","summary":"\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/11/94f41f82704abb8f3c4bc281cc97c1ee.png\"\u003e\n        \u003cimg alt=\"Claude Code 与 Codex 使用示意图\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/11/94f41f82704abb8f3c4bc281cc97c1ee.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003ch2 id=\"迄今为止最先进的代码助手\"\u003e\u003cstrong\u003e迄今为止最先进的代码助手\u003c/strong\u003e：\u003c/h2\u003e\n\u003cp\u003eclaude，anthropic家最强垂直领域编码模型之王，三个系列，haiku、sonnet、opus\n截至写这个教程时模型全面更新到4.5版本了。\u003c/p\u003e\n\u003cp\u003eclaude code是他们家推出的 cli agent工具，个人心中排第一没什么问题（贵应该是我的问题）\u003c/p\u003e","title":"vibe coding教程，claude code 和 codex"},{"content":"很狼狈的一件事情，到手一个 tar 压缩包之后。直接运行 tar -xvf 罪魁祸首.tar 结果就像一个在我混乱的卧室里 爆开一包薯片一样，撒的到处都是，一点点捡可能还捡不干净，有一些文件的修改日期十分远古，看不出来是否来自压缩包内\n首先，快捷查看包内容可以使用 tar -tf myfile.tgz 或者 -tzf\n-t（或 \u0026ndash;list）表示列出内容。 -z 表示使用 gzip 解压（因为 .tgz 通常是 .tar + gzip 压缩） -f 指定档案文件。 如果想看得更 “详细”（包括权限、时间戳、大小等），可以加 -v： 1 2 3 (base) yangr@172-16-99-32-Dev:/data/yangr$ tar -tf meetingasr.tgz meetingasr/ meetingasr/app/ 这样可以看到包里的顶层是一个文件夹，而不是那一堆薯片碎，现在直接解压会在当前目录解压到这个名字的文件夹内\n但是，如果某一天忘记看一眼里面的结构，我之前会使用 -C 参数 change to directory\ntar -xzf archive.tgz -C /path/to/targetdir\n但是这个参数需要先有指定的目录才能执行，这很麻烦\n所以还有一个方案就是使用 --one-top-level tar -xzf myfile.tgz --one-top-level=newdir\n这样无论包里是什么结构，都会把内容解压到 newdir/ 目录内。\n那如果我已经是上面例子 有一个文件夹，那岂不是会变成 newdir/meetingasr/ 了\n所以还能用 tar -xzf myfile.tgz --strip-components=1 剥掉包里路径的第一级目录\n","permalink":"/posts/tar-%E5%8C%85%E7%88%86%E7%82%B8%E4%BA%86/","summary":"\u003cp\u003e很狼狈的一件事情，到手一个 tar 压缩包之后。直接运行 \u003ccode\u003etar -xvf 罪魁祸首.tar\u003c/code\u003e 结果就像一个在我混乱的卧室里 爆开一包薯片一样，撒的到处都是，一点点捡可能还捡不干净，有一些文件的修改日期十分远古，看不出来是否来自压缩包内\u003c/p\u003e\n\u003cp\u003e首先，快捷查看包内容可以使用\n\u003ccode\u003etar -tf myfile.tgz\u003c/code\u003e 或者 \u003ccode\u003e-tzf\u003c/code\u003e\u003c/p\u003e","title":"tar 包爆炸了"},{"content":"我真的快没招了，这个东西折磨了我很久了 具体是什么情况 如下\n1 2 3 4 docker push hub.szmckj.cn/hotpotcat/perm-check:zz The push refers to repository [hub.szmckj.cn/hotpotcat/perm-check] 17eec7bbc9d7: Unavailable error from registry: unauthorized to access repository: hotpotcat/perm-check, action: push: unauthorized to access repository: hotpotcat/perm-check, action: push 如这里所见。我在公司的服务器上装了 VMware Harbor 来多地同步docker镜像，同时存一些基本镜像避免网络问题。\n但是无论我在哪里都愉快使用的时候，在家里的Windows遇到了这个 无法push的问题\n首先在Harbor的配置处 我检查了项目名称，全小写，一个字母没错，我检查了登录的用户。是我在UI登录的账号密码。我logout再login 也依旧报错。我检查了 项目的策略。这是一个干净的仓库，创建人是我，并且我上午在公司的mac 和 ubuntu服务器成功推送过。\n甚至他是个公开仓库啊\n怀疑系统，用wsl试试 还是这样\n怀疑代理问题 关了clash，切换了旁路由，还是这样子\ngpt叫我搞个机器人账号试试 我都拉满权限了还是这样\n看后台日志 到这一步我其实严重怀疑是网络问题。但是我没证据所以还是先看后台日志，好在看了\n显示已折叠代码（29 行） 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 registry | ::1 - - [29/Oct/2025:15:38:14 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 0 \u0026#34;\u0026#34; \u0026#34;curl/8.12.0\u0026#34; nginx | 127.0.0.1 - \u0026#34;GET / HTTP/1.1\u0026#34; 200 785 \u0026#34;-\u0026#34; \u0026#34;curl/8.12.0\u0026#34; 0.001 0.001 . harbor-portal | 172.31.0.9 - - [29/Oct/2025:15:38:17 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 785 \u0026#34;-\u0026#34; \u0026#34;curl/8.12.0\u0026#34; registry | 172.31.0.4 - - [29/Oct/2025:15:38:19 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 0 \u0026#34;\u0026#34; \u0026#34;Go-http-client/1.1\u0026#34; harbor-portal | 172.31.0.4 - - [29/Oct/2025:15:38:19 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 785 \u0026#34;-\u0026#34; \u0026#34;Go-http-client/1.1\u0026#34; registryctl | 172.31.0.4 - - [29/Oct/2025:15:38:19 +0000] \u0026#34;GET /api/health HTTP/1.1\u0026#34; 200 9 nginx | 172.16.99.6 - \u0026#34;HEAD /v2/hotpotcat/perm-check/blobs/sha256:17eec7bbc9d79fa397ac95c7283ecd04d1fe6978516932a3db110c6206430809 HTTP/1.1\u0026#34; 401 0 \u0026#34;-\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\x5C(windows\\x5C))\u0026#34; 0.003 0.003 . nginx | 172.16.99.6 - \u0026#34;HEAD /v2/hotpotcat/perm-check/blobs/sha256:1b44b5a3e06a9aae883e7bf25e45c100be0bb81a0e01b32de604f3ac44711634 HTTP/1.1\u0026#34; 401 0 \u0026#34;-\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\x5C(windows\\x5C))\u0026#34; 0.005 0.005 . nginx | 172.16.99.6 - \u0026#34;POST /service/token HTTP/1.1\u0026#34; 405 19 \u0026#34;-\u0026#34; \u0026#34;containerd/2.1.4+unknown\u0026#34; 0.002 0.001 . harbor-core | 2025-10-29T15:38:26Z [INFO] [/server/middleware/security/robot.go:71][requestID=\u0026#34;55026fa7-8ad3-4fac-a387-d3db6484ac5c\u0026#34; traceID=\u0026#34;aa950751535cc6b087bf5ec533951b10\u0026#34;]: a robot security context generated for request GET /service/token nginx | 172.16.99.6 - \u0026#34;GET /service/token?scope=repository%3Ahotpotcat%2Fperm-check%3Apull\u0026amp;scope=repository%3Ahotpotcat%2Fperm-check%3Apull%2Cpush\u0026amp;service=harbor-registry HTTP/1.1\u0026#34; 200 1010 \u0026#34;-\u0026#34; \u0026#34;containerd/2.1.4+unknown\u0026#34; 0.025 0.025 . nginx | 172.16.99.6 - \u0026#34;HEAD /v2/hotpotcat/perm-check/blobs/sha256:1b44b5a3e06a9aae883e7bf25e45c100be0bb81a0e01b32de604f3ac44711634 HTTP/1.1\u0026#34; 404 0 \u0026#34;-\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\x5C(windows\\x5C))\u0026#34; 0.005 0.005 . nginx | 172.16.99.6 - \u0026#34;HEAD /v2/hotpotcat/perm-check/blobs/sha256:17eec7bbc9d79fa397ac95c7283ecd04d1fe6978516932a3db110c6206430809 HTTP/1.1\u0026#34; 404 0 \u0026#34;-\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\x5C(windows\\x5C))\u0026#34; 0.022 0.023 . registry | time=\u0026#34;2025-10-29T15:38:26.164978329Z\u0026#34; level=info msg=\u0026#34;authorized request\u0026#34; go.version=go1.23.8 http.request.host=\u0026#34;hub.szmckj.cn:443\u0026#34; http.request.id=e560674b-c756-457d-8294-cbc6e4d85504 http.request.method=POST http.request.remoteaddr=116.30.100.87 http.request.uri=\u0026#34;/v2/hotpotcat/perm-check/blobs/uploads/\u0026#34; http.request.useragent=\u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\(windows\\))\u0026#34; vars.name=\u0026#34;hotpotcat/perm-check\u0026#34; registry | time=\u0026#34;2025-10-29T15:38:26.168010906Z\u0026#34; level=info msg=\u0026#34;response completed\u0026#34; go.version=go1.23.8 http.request.host=\u0026#34;hub.szmckj.cn:443\u0026#34; http.request.id=e560674b-c756-457d-8294-cbc6e4d85504 http.request.method=POST http.request.remoteaddr=116.30.100.87 http.request.uri=\u0026#34;/v2/hotpotcat/perm-check/blobs/uploads/\u0026#34; http.request.useragent=\u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\(windows\\))\u0026#34; http.response.duration=11.300209ms http.response.status=202 http.response.written=0 registry | 172.31.0.4 - - [29/Oct/2025:15:38:26 +0000] \u0026#34;POST /v2/hotpotcat/perm-check/blobs/uploads/ HTTP/1.1\u0026#34; 202 0 \u0026#34;\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\\\(windows\\\\))\u0026#34; nginx | 172.16.99.6 - \u0026#34;POST /v2/hotpotcat/perm-check/blobs/uploads/ HTTP/1.1\u0026#34; 202 0 \u0026#34;-\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\x5C(windows\\x5C))\u0026#34; 0.022 0.021 . nginx | 172.16.99.6 - \u0026#34;PUT /v2/hotpotcat/perm-check/blobs/uploads/9d9e11fd-abe0-456e-9b9c-d5da9a020239?_state=MvL4ZmK09_2yH7zJzwMYeiWVk_vgc4FBa4AHfNLaNs97Ik5hbWUiOiJob3Rwb3RjYXQvcGVybS1jaGVjayIsIlVVSUQiOiI5ZDllMTFmZC1hYmUwLTQ1NmUtOWI5Yy1kNWRhOWEwMjAyMzkiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMjUtMTAtMjlUMTU6Mzg6MjYuMTY1MTIxNTg2WiJ9\u0026amp;digest=sha256%3A1b44b5a3e06a9aae883e7bf25e45c100be0bb81a0e01b32de604f3ac44711634 HTTP/1.1\u0026#34; 401 190 \u0026#34;-\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\x5C(windows\\x5C))\u0026#34; 0.003 0.003 . registry | time=\u0026#34;2025-10-29T15:38:26.184206457Z\u0026#34; level=info msg=\u0026#34;authorized request\u0026#34; go.version=go1.23.8 http.request.host=\u0026#34;hub.szmckj.cn:443\u0026#34; http.request.id=487e9723-c8f5-4646-af27-b301329de187 http.request.method=POST http.request.remoteaddr=116.30.100.87 http.request.uri=\u0026#34;/v2/hotpotcat/perm-check/blobs/uploads/\u0026#34; http.request.useragent=\u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\(windows\\))\u0026#34; vars.name=\u0026#34;hotpotcat/perm-check\u0026#34; registry | 172.31.0.4 - - [29/Oct/2025:15:38:26 +0000] \u0026#34;POST /v2/hotpotcat/perm-check/blobs/uploads/ HTTP/1.1\u0026#34; 202 0 \u0026#34;\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\\\(windows\\\\))\u0026#34; registry | time=\u0026#34;2025-10-29T15:38:26.186531266Z\u0026#34; level=info msg=\u0026#34;response completed\u0026#34; go.version=go1.23.8 http.request.host=\u0026#34;hub.szmckj.cn:443\u0026#34; http.request.id=487e9723-c8f5-4646-af27-b301329de187 http.request.method=POST http.request.remoteaddr=116.30.100.87 http.request.uri=\u0026#34;/v2/hotpotcat/perm-check/blobs/uploads/\u0026#34; http.request.useragent=\u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\(windows\\))\u0026#34; http.response.duration=10.154146ms http.response.status=202 http.response.written=0 nginx | 172.16.99.6 - \u0026#34;POST /v2/hotpotcat/perm-check/blobs/uploads/ HTTP/1.1\u0026#34; 202 0 \u0026#34;-\u0026#34; \u0026#34;docker/28.4.0 go/go1.24.7 git-commit/249d679 kernel/6.6.87.2-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/2.1.4+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/28.4.0 \\x5C(windows\\x5C))\u0026#34; 0.022 0.022 . registry | 172.31.0.4 - - [29/Oct/2025:15:38:29 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 0 \u0026#34;\u0026#34; \u0026#34;Go-http-client/1.1\u0026#34; registryctl | 172.31.0.4 - - [29/Oct/2025:15:38:29 +0000] \u0026#34;GET /api/health HTTP/1.1\u0026#34; 200 9 harbor-portal | 172.31.0.4 - - [29/Oct/2025:15:38:29 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 785 \u0026#34;-\u0026#34; \u0026#34;Go-http-client/1.1\u0026#34; harbor-portal | 127.0.0.1 - - [29/Oct/2025:15:38:33 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 785 \u0026#34;-\u0026#34; \u0026#34;curl/8.12.0\u0026#34; registryctl | ::1 - - [29/Oct/2025:15:38:33 +0000] \u0026#34;GET /api/health HTTP/1.1\u0026#34; 200 9 registry | 172.31.0.4 - - [29/Oct/2025:15:38:39 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 0 \u0026#34;\u0026#34; \u0026#34;Go-http-client/1.1\u0026#34; harbor-portal | 172.31.0.4 - - [29/Oct/2025:15:38:39 +0000] \u0026#34;GET / HTTP/1.1\u0026#34; 200 785 \u0026#34;-\u0026#34; \u0026#34;Go-http-clien 这是进行push时看到的日志。太长啦。感谢G老师再次拯救我 G老师分析如下\n显示已折叠代码（23 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 日志解读（按时间顺序） 客户端探测 blob 是否存在 → 被 401 挑战 HEAD /v2/hotpotcat/perm-check/blobs/... 401 客户端去拿 Bearer Token（scope 里明确包含 pull,push）→ 200 GET /service/token?... scope=repository:hotpotcat/perm-check:pull \u0026amp; scope=...:pull,push 200 开始上传（创建上传会话）→ 202 ✅ POST /v2/hotpotcat/perm-check/blobs/uploads/ 202 在“完成上传（PUT 带 digest）”这一步被 401 拦截 ❌ PUT /v2/hotpotcat/perm-check/blobs/uploads/\u0026lt;UUID\u0026gt;?_state=...\u0026amp;digest=sha256:... 401 这说明：鉴权没问题（能拿到 push token，并且能启动上传会话），但在“最终提交”这一步 Authorization 失效/丢失，或者 _state（Harbor/registry 的上传状态令牌）验证失败，于是被 401。 nginx 问题 后台日志拉出来一读，果然，前面正常，中间一步鉴权失败。那肯定是中间的网关把我的认证给搞丢了。最后更新 nginx 配置，让他把 header 都传过来就正常了\n结论 nginx 配置我就不贴了。具体原因在内网没遇到过是哪怕内网用域名 解析的逻辑和在外网不一样。少经过一层 nginx。AI 造福人类，多读日志头脑清晰，这是个小问题。\n","permalink":"/posts/harbor-push-unauthorized/","summary":"\u003cp\u003e我真的快没招了，这个东西折磨了我很久了 具体是什么情况\n如下\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker push hub.szmckj.cn/hotpotcat/perm-check:zz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eThe push refers to repository [hub.szmckj.cn/hotpotcat/perm-check]                                         \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e17eec7bbc9d7: Unavailable\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eerror from registry: unauthorized to access repository: hotpotcat/perm-check, action: push: unauthorized to access repository: hotpotcat/perm-check, action: push\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e如这里所见。我在公司的服务器上装了 VMware \u003ca href=\"https://goharbor.io/\"\u003eHarbor\u003c/a\u003e 来多地同步docker镜像，同时存一些基本镜像避免网络问题。\u003c/p\u003e\n\u003cp\u003e但是无论我在哪里都愉快使用的时候，在家里的Windows遇到了这个 无法push的问题\u003c/p\u003e","title":"Harbor push unauthorized"},{"content":" Note lua 有很多有趣的语法和平时的不一样，比如这个打印字符串\nlua 1 2 3 4 5 local banner = string.rep(\u0026#34;★\u0026#34;, 12) .. \u0026#34; ORDER_NO: \u0026#34; .. orderNo .. \u0026#34; \u0026#34; .. string.rep(\u0026#34;★\u0026#34;, 12) session:consoleLog(\u0026#34;ALERT\u0026#34;, banner .. \u0026#34;\\n\u0026#34;) session:consoleLog(\u0026#34;ALERT\u0026#34;, string.rep(\u0026#34;=\u0026#34;, #banner) .. \u0026#34;\\n\u0026#34;) 这里的写法有几个点\nsession:consoleLog中 : 有点像 . 就像是 session.consoleLog一样，他第一个默认参数就是 self，ALERT就是红色的 ERROR 那种样子的样式\n随后 .. 是 lua 的字符串连接符号\nstring.rep(A,B)函数的用处就是返回 A 字符串重复 B 次 这里就是用 #banner 获取banner的长度。然后重复\n","permalink":"/posts/lua%E6%89%93%E6%97%A5%E5%BF%97/","summary":"\u003cdiv class=\"markdown-alert markdown-alert-note\"\u003e\n    \u003cp class=\"markdown-alert-title\"\u003e\n      \u003cspan class=\"markdown-alert-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M12 16h.01\"/\u003e\u003cpath d=\"M12 8v4\"/\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003c/svg\u003e\u003c/span\u003e\n      Note\n    \u003c/p\u003e\n    \u003cdiv class=\"markdown-alert-content\"\u003e\n      \u003cp\u003elua 有很多有趣的语法和平时的不一样，比如这个打印字符串\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \u003cspan class=\"code-lang\"\u003elua\u003c/span\u003e\n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-lua\" data-lang=\"lua\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003elocal\u003c/span\u003e \u003cspan class=\"n\"\u003ebanner\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estring.rep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;★\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e..\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34; ORDER_NO: \u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e..\u003c/span\u003e \u003cspan class=\"n\"\u003eorderNo\u003c/span\u003e \u003cspan class=\"o\"\u003e..\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34; \u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e..\u003c/span\u003e \u003cspan class=\"n\"\u003estring.rep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;★\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esession\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003econsoleLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;ALERT\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebanner\u003c/span\u003e \u003cspan class=\"o\"\u003e..\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esession\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003econsoleLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;ALERT\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estring.rep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;=\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"n\"\u003ebanner\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e..\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e这里的写法有几个点\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003esession:consoleLog\u003c/code\u003e中 \u003ccode\u003e:\u003c/code\u003e 有点像 \u003ccode\u003e.\u003c/code\u003e 就像是 \u003ccode\u003esession.consoleLog\u003c/code\u003e一样，他第一个默认参数就是 self，ALERT就是红色的 ERROR 那种样子的样式\u003c/p\u003e","title":"lua打日志"},{"content":"阿里云 ubuntu 服务器 * 1 windows 11 家里电脑 * 1\n有开发环境在家里电脑。需要在公司调用，简单搭一个\n在ubuntu上部署frps（服务端） 查看 github 最新的 release 版本 下载\n1 2 3 wget https://github.com/fatedier/frp/releases/download/vX.Y.Z/frp_X.Y.Z_linux_amd64.tar.gz tar zxvf frp_X.Y.Z_linux_amd64.tar.gz cd frp_X.Y.Z_linux_amd64 我的阿里云下不了 github.不知道啥情况。换个命令\nwget https://sourceforge.net/projects/frp.mirror/files/v0.64.0/frp_0.64.0_linux_amd64.tar.gz/download -O frp_0.64.0_linux_amd64.tar.gz\n下载后解压 配置 frps.toml 调整端口和 token 防火墙放开对应端口 服务器或者云服务安全组\n配成系统服务(可选) 不做演示。\nwindows 安装 frpc 我去 拦在第一步。说release 的 exe 有问题。自己编译的比较靠谱是吧\n安装 go 环境。自己编译出 frpc 客户端\n启动后可以看到\n配置改成下面的足够了\n显示已折叠代码（19 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [common] server_addr = XXXX server_port = 7000 token = \u0026#34;XXX\u0026#34; # 比如映射一个本地 web 服务（假设端口 8080） [[proxies-lout]] name = \u0026#34;proxies-lout\u0026#34; type = tcp local_ip = \u0026#34;127.0.0.1\u0026#34; local_port = 8080 remote_port = 8080 [[proxies-ai]] name = \u0026#34;proxies-ai\u0026#34; type = tcp local_ip = \u0026#34;127.0.0.1\u0026#34; local_port = 8081 remote_port = 8081 写了个 start.bat\nbat 1 2 3 4 5 6 7 8 9 10 11 @echo off title FRP Client echo Starting FRP Client... echo. echo 配置文件: frpc.toml echo 日志文件: frpc.log echo. echo 按 Ctrl+C 停止服务 echo. bin\\frpc.exe -c frpc.toml pause 启动后 可以看到日志输出 这个输出表示可以。然后我可以去 云服务器上看到端口监听，把安全组安全放开\n测了一下就 ok 了\n","permalink":"/posts/%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F%E6%B5%85%E5%B0%9D/","summary":"\u003cp\u003e阿里云 ubuntu 服务器 * 1\nwindows 11 家里电脑 * 1\u003c/p\u003e\n\u003cp\u003e有开发环境在家里电脑。需要在公司调用，简单搭一个\u003c/p\u003e\n\u003ch2 id=\"在ubuntu上部署frps服务端\"\u003e在ubuntu上部署frps（服务端）\u003c/h2\u003e\n\u003cp\u003e查看 github 最新的 release 版本\n下载\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-gdscript3\" data-lang=\"gdscript3\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ewget\u003c/span\u003e \u003cspan class=\"n\"\u003ehttps\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"o\"\u003e//\u003c/span\u003e\u003cspan class=\"n\"\u003egithub\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecom\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003efatedier\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003efrp\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003ereleases\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003edownload\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003evX\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eY\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eZ\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003efrp_X\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eY\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eZ_linux_amd64\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etar\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003egz\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003etar\u003c/span\u003e \u003cspan class=\"n\"\u003ezxvf\u003c/span\u003e \u003cspan class=\"n\"\u003efrp_X\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eY\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eZ_linux_amd64\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etar\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003egz\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ecd\u003c/span\u003e \u003cspan class=\"n\"\u003efrp_X\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eY\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eZ_linux_amd64\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cblockquote\u003e\n    \u003cp\u003e我的阿里云下不了 github.不知道啥情况。换个命令\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e\u003ccode\u003e wget https://sourceforge.net/projects/frp.mirror/files/v0.64.0/frp_0.64.0_linux_amd64.tar.gz/download -O frp_0.64.0_linux_amd64.tar.gz\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e下载后解压\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/09/829175c4bb45913db16992d27e38ab9e.png\"\u003e\n        \u003cimg alt=\"frp 压缩包解压后的目录\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/09/829175c4bb45913db16992d27e38ab9e.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003ch2 id=\"配置-frpstoml\"\u003e配置 frps.toml\u003c/h2\u003e\n\u003cp\u003e调整端口和 token\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/09/8d42119854901762f0690386598a9d3c.png\"\u003e\n        \u003cimg alt=\"frps.toml 端口与 token 配置\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/09/8d42119854901762f0690386598a9d3c.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003ch2 id=\"防火墙放开对应端口\"\u003e防火墙放开对应端口\u003c/h2\u003e\n\u003cp\u003e服务器或者云服务安全组\u003c/p\u003e","title":"内网穿透浅尝"},{"content":"问题 vllm 推理 qwen3-30b-a3b-2507 遇到没有第一个\u0026lt;think\u0026gt;标签 这个之前 DeepSeek-R1 出来后就遇到过几次，一般改一下chat_template 可以解决。这次记一下\n在进行聊天补全请求时，模型的推理内容出现在 content 字段中，具有以下特征：\n缺少开始的 \u0026lt;think\u0026gt; 标签 包含结束的 \u0026lt;/think\u0026gt; 标签 所有推理内容与最终回复混合在 content 中 reasoning_content 字段为 null 由于最近升级了 vllm 以及推理的是 第三方 4bit 量化版，所以这些细节也记一下\n版本 vLLM version: 0.10.1 Model: btbtyler09/Qwen3-30B-A3B-Thinking-2507-gptq-4bit Startup command: 1 2 3 4 5 6 7 8 9 10 11 12 13 python3 -m vllm.entrypoints.openai.api_server --model /model --host 0.0.0.0 --port 8000 --api-key token-ray123 --served-model-name mckj/Qwen3-30B-A3B-Thinking-2507 --tensor-parallel-size 1 --gpu-memory-utilization 0.96 --max-model-len 6144 --max-num-batched-tokens 8192 --max-num-seqs 64 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser hermes 参考此 discussionQwen/Qwen3-30B-A3B-Thinking-2507 · Using vllm 0.10.0, `reasoning content` appears in content field instead of reasoning_content with Qwen3-30B-A3B-Thinking model\n把他提到的 chat_template.jinja 按照他的替换。解决字段错位问题\n可以看到重启之后 思考内容会解析到reasoning_content中了\n[json] 显示已折叠代码（34 行） 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 { \u0026#34;id\u0026#34;: \u0026#34;chatcmpl-b02318c5e3c24e6eaf12918e5dc3181d\u0026#34;, \u0026#34;object\u0026#34;: \u0026#34;chat.completion\u0026#34;, \u0026#34;created\u0026#34;: 1757059274, \u0026#34;model\u0026#34;: \u0026#34;mckj/Qwen3-30B-A3B-Thinking-2507\u0026#34;, \u0026#34;choices\u0026#34;: [ { \u0026#34;index\u0026#34;: 0, \u0026#34;message\u0026#34;: { \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;\\n\\nTo solve the expression **2 + 2 * 5**, we must follow the **order of operations** (PEMDAS/BODMAS), which dictates that **multiplication is performed before addition**.\\n\\n---\\n\\n### Step-by-Step Breakdown:\\n\\n1. **Identify the operations**: \\n The expression contains **addition (+)** and **multiplication (*)**.\\n\\n2. **Apply the order of operations**:\\n - First, perform the **multiplication**: \\n $ 2 \\\\times 5 = 10 $\\n - Then, perform the **addition**: \\n $ 2 + 10 = 12 $\\n\\n---\\n\\n### Why Not $ (2 + 2) \\\\times 5 $? \\nIf the expression were written as $ (2 + 2) \\\\times 5 $, then the parentheses would change the order and the result would be $ 4 \\\\times 5 = 20 $. However, **there are no parentheses** in the original expression, so we do **not** add first.\\n\\n---\\n\\n### Final Answer:\\n$$\\n\\\\boxed{12}\\n$$\u0026#34;, \u0026#34;refusal\u0026#34;: null, \u0026#34;annotations\u0026#34;: null, \u0026#34;audio\u0026#34;: null, \u0026#34;function_call\u0026#34;: null, \u0026#34;tool_calls\u0026#34;: [], \u0026#34;reasoning_content\u0026#34;: \u0026#34;\\nOkay, the user is asking 2+2*5. Let me think about how to approach this. First, I remember that in math, there\u0026#39;s an order of operations, like PEMDAS. That stands for Parentheses, Exponents, Multiplication and Division, Addition and Subtraction. So multiplication comes before addition.\\n\\nSo the problem is 2 + 2 * 5. According to PEMDAS, I should do the multiplication first. 2 times 5 is 10. Then add the 2. So 2 + 10 equals 12. Wait, but sometimes people might forget the order and add first, which would be 4*5=20, but that\u0026#39;s wrong because multiplication has higher precedence. Let me double-check. Yeah, multiplication before addition. So the correct answer should be 12.\\n\\nWait, let me make sure. Let\u0026#39;s break it down step by step. The expression is 2 + 2 * 5. The multiplication part is 2*5, which is 10. Then add the 2 to that result: 2 + 10 = 12. Yep, that\u0026#39;s right. I think that\u0026#39;s the standard way to solve it. If there were parentheses, like (2+2)*5, then it would be 4*5=20, but there are no parentheses here. So the answer is 12.\\n\\nAnother way to think about it: in most programming languages, the order of operations is the same, so if you type 2+2*5 into a calculator or a code, it should give 12. Let me confirm with a quick calculation. 2*5 is 10, plus 2 is 12. Yep. So the user might be testing if I know the order of operations. Maybe they\u0026#39;re a student learning basic math. I should explain the steps clearly so they understand why it\u0026#39;s 12 and not 20. Let me make sure to mention PEMDAS or the standard order of operations to clarify the reasoning.\\n\u0026#34; }, \u0026#34;logprobs\u0026#34;: null, \u0026#34;finish_reason\u0026#34;: \u0026#34;stop\u0026#34;, \u0026#34;stop_reason\u0026#34;: null } ], \u0026#34;service_tier\u0026#34;: null, \u0026#34;system_fingerprint\u0026#34;: null, \u0026#34;usage\u0026#34;: { \u0026#34;prompt_tokens\u0026#34;: 14, \u0026#34;total_tokens\u0026#34;: 680, \u0026#34;completion_tokens\u0026#34;: 666, \u0026#34;prompt_tokens_details\u0026#34;: null }, \u0026#34;prompt_logprobs\u0026#34;: null, \u0026#34;kv_transfer_params\u0026#34;: null } 小补充 vllm 开启 think 标签解析到reasoning_content中是从 DeepSeek-R1 开始的范式，一般配置 --reasoning-parser deepseek-r1就行 这次写配置是 gpt 帮忙写的。他说可以 --reasoning-parser qwen3 我没确认文档，不过生效了也就无所谓了\n另外要开启 function call 就要配置如下\n1 2 --enable-auto-tool-choice --tool-call-parser hermes hermes也是官方推荐的，建议这些参数都看一下最新文档为主\ntool use 的效果如下\n[bash] 显示已折叠代码（27 行） 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 curl -s http://localhost:8011/v1/chat/completions \\ -H \u0026#34;Authorization: Bearer token-ray123\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;mckj/Qwen3-30B-A3B-Thinking-2507\u0026#34;, \u0026#34;messages\u0026#34;: [ {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;现在洛杉矶几点？帮我换算成北京时间\u0026#34;} ], \u0026#34;tools\u0026#34;: [{ \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;convert_time\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Convert time between timezones\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;:\u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;:{ \u0026#34;from_tz\u0026#34;:{\u0026#34;type\u0026#34;:\u0026#34;string\u0026#34;}, \u0026#34;to_tz\u0026#34;:{\u0026#34;type\u0026#34;:\u0026#34;string\u0026#34;}, \u0026#34;time_str\u0026#34;:{\u0026#34;type\u0026#34;:\u0026#34;string\u0026#34;} }, \u0026#34;required\u0026#34;:[\u0026#34;from_tz\u0026#34;,\u0026#34;to_tz\u0026#34;,\u0026#34;time_str\u0026#34;] } } }], \u0026#34;tool_choice\u0026#34;: \u0026#34;auto\u0026#34;, \u0026#34;stream\u0026#34;: false }\u0026#39; 响应内容\n[json] 显示已折叠代码（43 行） 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 { \u0026#34;id\u0026#34;: \u0026#34;chatcmpl-050207e5faee4e64b0b03966baecc2c6\u0026#34;, \u0026#34;object\u0026#34;: \u0026#34;chat.completion\u0026#34;, \u0026#34;created\u0026#34;: 1757059508, \u0026#34;model\u0026#34;: \u0026#34;mckj/Qwen3-30B-A3B-Thinking-2507\u0026#34;, \u0026#34;choices\u0026#34;: [ { \u0026#34;index\u0026#34;: 0, \u0026#34;message\u0026#34;: { \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;\\n\\n\u0026#34;, \u0026#34;refusal\u0026#34;: null, \u0026#34;annotations\u0026#34;: null, \u0026#34;audio\u0026#34;: null, \u0026#34;function_call\u0026#34;: null, \u0026#34;tool_calls\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;chatcmpl-tool-19b6780b78244bdfae923e8c90156593\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;convert_time\u0026#34;, \u0026#34;arguments\u0026#34;: \u0026#34;{\\\u0026#34;from_tz\\\u0026#34;: \\\u0026#34;America/Los_Angeles\\\u0026#34;, \\\u0026#34;to_tz\\\u0026#34;: \\\u0026#34;Asia/Shanghai\\\u0026#34;, \\\u0026#34;time_str\\\u0026#34;: \\\u0026#34;now\\\u0026#34;}\u0026#34; } } ], \u0026#34;reasoning_content\u0026#34;: \u0026#34;\\n一堆思考.\\n\u0026#34; }, \u0026#34;logprobs\u0026#34;: null, \u0026#34;finish_reason\u0026#34;: \u0026#34;tool_calls\u0026#34;, \u0026#34;stop_reason\u0026#34;: null } ], \u0026#34;service_tier\u0026#34;: null, \u0026#34;system_fingerprint\u0026#34;: null, \u0026#34;usage\u0026#34;: { \u0026#34;prompt_tokens\u0026#34;: 189, \u0026#34;total_tokens\u0026#34;: 1724, \u0026#34;completion_tokens\u0026#34;: 1535, \u0026#34;prompt_tokens_details\u0026#34;: null }, \u0026#34;prompt_logprobs\u0026#34;: null, \u0026#34;kv_transfer_params\u0026#34;: null } ","permalink":"/posts/vllm-%E6%8E%A8%E7%90%86-qwen3-%E6%B2%A1%E6%9C%89-think-%E6%A0%87%E7%AD%BE/","summary":"\u003ch2 id=\"问题\"\u003e问题\u003c/h2\u003e\n\u003cblockquote\u003e\n    \u003cp\u003evllm 推理 qwen3-30b-a3b-2507 遇到没有第一个\u003ccode\u003e\u0026lt;think\u0026gt;\u003c/code\u003e标签 这个之前 DeepSeek-R1 出来后就遇到过几次，一般改一下chat_template 可以解决。这次记一下\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e在进行聊天补全请求时，模型的推理内容出现在 \u003ccode\u003econtent\u003c/code\u003e 字段中，具有以下特征：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e缺少开始的 \u003ccode\u003e\u0026lt;think\u0026gt;\u003c/code\u003e 标签\u003c/li\u003e\n\u003cli\u003e包含结束的 \u003ccode\u003e\u0026lt;/think\u0026gt;\u003c/code\u003e 标签\u003c/li\u003e\n\u003cli\u003e所有推理内容与最终回复混合在 \u003ccode\u003econtent\u003c/code\u003e 中\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ereasoning_content\u003c/code\u003e 字段为 \u003ccode\u003enull\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e由于最近升级了 vllm 以及推理的是 第三方 4bit 量化版，所以这些细节也记一下\u003c/p\u003e","title":"vllm 推理 qwen3 没有 think 标签"},{"content":"在代理下，git使用ssh 22端口时连接不上。关掉代理就可以\n参考本文 本地打开代理，Git无法使用SSH连接GitHubGitHub SSH连接的血泪史:短短一行报错，让我在接下来的48小时 - 掘金\n但是实际上我并没有使用ncat\n只是在ssh 的config中\nyaml 1 2 3 4 5 6 Host github.com Hostname ssh.github.com IdentityFile ~/.ssh/id_rsa Port 443 ServerAliveInterval 20 User git Port 443 绕过。似乎暂时没遇到什么问题\n","permalink":"/posts/%E4%BB%A3%E7%90%86%E4%B8%8Bgit%E8%BF%9E%E4%B8%8D%E4%B8%8A/","summary":"\u003cp\u003e在代理下，git使用ssh 22端口时连接不上。关掉代理就可以\u003c/p\u003e\n\u003cp\u003e参考本文\n\u003ca href=\"https://juejin.cn/post/7477133262423900160\"\u003e本地打开代理，Git无法使用SSH连接GitHubGitHub SSH连接的血泪史:短短一行报错，让我在接下来的48小时 - 掘金\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e但是实际上我并没有使用ncat\u003c/p\u003e\n\u003cp\u003e只是在ssh 的config中\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \u003cspan class=\"code-lang\"\u003eyaml\u003c/span\u003e\n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003eHost github.com \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"l\"\u003eHostname ssh.github.com \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"l\"\u003eIdentityFile ~/.ssh/id_rsa \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"l\"\u003ePort 443 \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"l\"\u003eServerAliveInterval 20 \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"l\"\u003eUser git\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003ePort 443 绕过。似乎暂时没遇到什么问题\u003c/p\u003e","title":"代理下git连不上"},{"content":"windows使用cluade code\n安装node，百度安装nvm。选择一个LTS的 node版本使用\n之后\nnpm install -g @anthropic-ai/claude-code npm install -g @musistudio/claude-code-router\nccr的 git地址GitHub - musistudio/claude-code-router: Use Claude Code as the foundation for coding infrastructure, allowing you to decide how to interact with the model while enjoying updates from Anthropic.\n安装后启动 ccr ui 按照教程配置自己的模型供应商\nccr code替代claude code 启动 就能成功使用自己要用的模型了\n","permalink":"/posts/claude-code%E9%85%8D%E7%BD%AE/","summary":"\u003cp\u003ewindows使用cluade code\u003c/p\u003e\n\u003cp\u003e安装node，百度安装nvm。选择一个LTS的 node版本使用\u003c/p\u003e\n\u003cp\u003e之后\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003enpm install -g @anthropic-ai/claude-code\u003c/code\u003e\n\u003ccode\u003enpm install -g @musistudio/claude-code-router\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eccr的 git地址\u003ca href=\"https://github.com/musistudio/claude-code-router\"\u003eGitHub - musistudio/claude-code-router: Use Claude Code as the foundation for coding infrastructure, allowing you to decide how to interact with the model while enjoying updates from Anthropic.\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e安装后启动 ccr ui\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/08/39cc32a0655c35b9cedbbfc2e3f64fcd.png\"\u003e\n        \u003cimg alt=\"69af067f-1610-4311-b4f6-1b07b0dc5d5b.png\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/08/39cc32a0655c35b9cedbbfc2e3f64fcd.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e按照教程配置自己的模型供应商\u003c/p\u003e\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/08/2b70b63ea9f1fa84f3b7b6a6a8af8c45.png\"\u003e\n        \u003cimg alt=\"455ce2cc-b732-45bc-8151-765d27099bea.png\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/08/2b70b63ea9f1fa84f3b7b6a6a8af8c45.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eccr code\u003c/code\u003e替代claude code 启动 就能成功使用自己要用的模型了\u003c/p\u003e","title":"claude code配置"},{"content":"接上篇，定义 SLM 的模型架构 代码如下\n[python] 显示已折叠代码（172 行） 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 import torch import torch.nn as nn import torch.nn.functional as F import math from dataclasses import dataclass import numpy as np from tqdm.auto import tqdm from contextlib import nullcontext import os class LayerNorm(nn.Module): \u0026#34;\u0026#34;\u0026#34; - 与 `nn.LayerNorm(ndim, elementwise_affine=bias)` 等价，手搓是为了**可控是否带 bias**。 - 作用：把最后一维做标准化，稳定训练。 - 形状不变：`(B, T, C) → (B, T, C)`。 \u0026#34;\u0026#34;\u0026#34; def __init__(self, ndim, bias): super().__init__() self.weight = nn.Parameter(torch.ones(ndim)) self.bias = nn.Parameter(torch.zeros(ndim)) if bias else None def forward(self, x): return F.layer_norm(x, self.weight.shape, self.weight, self.bias, 1e-5) class CausalSelfAttention(nn.Module): \u0026#34;\u0026#34;\u0026#34; 自回归注意力 \u0026#34;\u0026#34;\u0026#34; def __init__(self, config): super().__init__() assert config.n_embd % config.n_head == 0 self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias) # 生成 Q,K,V self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias) # 多头拼回后做投影 # 注意力/残差丢弃 self.attn_dropout = nn.Dropout(config.dropout) self.resid_dropout = nn.Dropout(config.dropout) self.n_head = config.n_head self.n_embd = config.n_embd self.flash = hasattr(F, \u0026#39;scaled_dot_product_attention\u0026#39;) if not self.flash: self.register_buffer(\u0026#34;bias\u0026#34;, torch.tril(torch.ones(config.block_size, config.block_size)) .view(1, 1, config.block_size, config.block_size)) def forward(self, x): B, T, C = x.size() q, k, v = self.c_attn(x).split(self.n_embd, dim=2) k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) if self.flash: y = F.scaled_dot_product_attention(q, k, v, attn_mask=None, dropout_p=self.attn_dropout.p if self.training else 0.0, is_causal=True) else: att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1))) att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float(\u0026#39;-inf\u0026#39;)) att = F.softmax(att, dim=-1) att = self.attn_dropout(att) y = att @ v y = y.transpose(1, 2).contiguous().view(B, T, C) y = self.resid_dropout(self.c_proj(y)) return y class MLP(nn.Module): def __init__(self, config): super().__init__() self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias) self.gelu = nn.GELU() self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias) self.dropout = nn.Dropout(config.dropout) def forward(self, x): return self.dropout(self.c_proj(self.gelu(self.c_fc(x)))) class Block(nn.Module): def __init__(self, config): super().__init__() self.ln1 = LayerNorm(config.n_embd, config.bias) self.attn = CausalSelfAttention(config) self.ln2 = LayerNorm(config.n_embd, config.bias) self.mlp = MLP(config) def forward(self, x): x = x + self.attn(self.ln1(x)) x = x + self.mlp(self.ln2(x)) return x @dataclass class GPTConfig: block_size: int vocab_size: int n_layer: int n_head: int n_embd: int dropout: float = 0.0 bias: bool = True class GPT(nn.Module): def __init__(self, config): super().__init__() self.config = config self.transformer = nn.ModuleDict(dict( wte=nn.Embedding(config.vocab_size, config.n_embd), wpe=nn.Embedding(config.block_size, config.n_embd), drop=nn.Dropout(config.dropout), h=nn.ModuleList([Block(config) for _ in range(config.n_layer)]), ln_f=LayerNorm(config.n_embd, config.bias), )) self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False) self.transformer.wte.weight = self.lm_head.weight # weight tying self.apply(self._init_weights) for pn, p in self.named_parameters(): if pn.endswith(\u0026#39;c_proj.weight\u0026#39;): nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * config.n_layer)) def _init_weights(self, module): if isinstance(module, nn.Linear): nn.init.normal_(module.weight, mean=0.0, std=0.02) if module.bias is not None: nn.init.zeros_(module.bias) elif isinstance(module, nn.Embedding): nn.init.normal_(module.weight, mean=0.0, std=0.02) def forward(self, idx, targets=None): device = idx.device b, t = idx.size() assert t \u0026lt;= self.config.block_size pos = torch.arange(0, t, dtype=torch.long, device=device) tok_emb = self.transformer.wte(idx) pos_emb = self.transformer.wpe(pos) x = self.transformer.drop(tok_emb + pos_emb) for block in self.transformer.h: x = block(x) x = self.transformer.ln_f(x) if targets is not None: logits = self.lm_head(x) loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1) return logits, loss else: logits = self.lm_head(x[:, [-1], :]) return logits, None @torch.no_grad() def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None): \u0026#34;\u0026#34;\u0026#34; Generate tokens given a conditioning sequence. idx: Tensor of shape (B, T) \u0026#34;\u0026#34;\u0026#34; for _ in range(max_new_tokens): idx_cond = idx if idx.size(1) \u0026lt;= self.config.block_size else idx[:, -self.config.block_size:] logits, _ = self(idx_cond) logits = logits[:, -1, :] / temperature if top_k is not None: v, _ = torch.topk(logits, min(top_k, logits.size(-1))) logits[logits \u0026lt; v[:, [-1]]] = -float(\u0026#39;Inf\u0026#39;) probs = F.softmax(logits, dim=-1) idx_next = torch.multinomial(probs, num_samples=1) idx = torch.cat((idx, idx_next), dim=1) return idx config = GPTConfig( vocab_size=50257, # 使用分词器的词汇表大小 block_size=128, # 或你训练时使用的任何上下文大小 n_layer=6, n_head=6, n_embd=384, dropout=0.1, bias=True ) model = GPT(config) CausalSelfAttention（自回归注意力） python 1 2 3 4 assert n_embd % n_head == 0 self.c_attn = nn.Linear(C, 3C) # 生成 Q,K,V self.c_proj = nn.Linear(C, C) # 多头拼回后做投影 self.attn_dropout / resid_dropout # 注意力/残差丢弃 self.flash = hasattr(F, \u0026#39;scaled_dot_product_attention\u0026#39;) if not flash: self.register_buffer(\u0026#34;bias\u0026#34;, tril(ones(T,T)).view(1,1,T,T)) 将输入 x 线性变换得到 q/k/v，每个 shape (B, T, C)，再 reshape 成 (B, n_head, T, head_dim)。 Flash/SDPA 路径：PyTorch 内置 scaled_dot_product_attention，设 is_causal=True 自动做下三角 mask，快而省显存。 回退路径：手动算 att = q @ k^T / sqrt(d)，再用注册的下三角 bias 做 mask： bias 维度 (1,1,T,T)，只保留 i≥j 的位置，保证只能看见过去。 最后把多头输出拼回 (B, T, C)，投影并做残差丢弃。 形状小抄 入：x: (B, T, C) 出：y: (B, T, C) 注意 bias 的 T 用的是 config.block_size。若推理时序列长度 \u0026gt; block_size，回退路径会越界；但 Flash 路径不受此限。 解释 基础设定 x: (B, T, C) 你有一段输入，比如一句话里的字/词。 B = batch，大概就是“同时处理多少句话”。 T = 时间步长，就是句子有多少个字/词。 C = 每个字/词的向量维度，可以理解成“每个字有多少个特征”。 Q/K/V（查询、键、值） •\t把输入向量 x 分三份： •\tQ (Query 查询)：我要看别的词。 •\tK (Key 键)：我能提供什么信息。 •\tV (Value 值)：具体信息内容。 •\t每个 shape 最开始都是 (B, T, C)。再 reshape 成 (B, n_head, T, head_dim)：就像让很多小组（head）分头看，不同小组看问题的角度不同。 Flash 路径 vs 回退路径 •\tFlash / SDPA 路径：PyTorch 内置的 scaled_dot_product_attention，就像显卡加速版，自动帮你算“谁可以看谁”。只要设 is_causal=True，它会自动加下三角遮罩（mask），保证当前词不能偷看未来。又快又省内存。 •\t回退路径（手工实现）：如果没有显卡加速，就自己算： 做点积：att = q @ k^T / sqrt(d) → 代表“查询词对键的相关性”。 用 bias（下三角矩阵）遮住未来：bias 形状 (1, 1, T, T)，保证第 i 个词只能看到自己和之前的词。 拼回与投影 •\t各个头（小组）得到的信息会合并成 (B, T, C)。 •\t再过一层线性变换（c_proj），把信息“压缩整理”回原来的维度。 •\t最后做 dropout（随机丢弃部分连接），防止过拟合。 形象比喻 想象你在写作文，每个字要决定自己怎么写： •\tQ = 我在想：我要参考哪些前面的字？ •\tK = 每个字举手说：我能告诉你些什么。 •\tV = 每个字手里的小抄内容。 •\tAttention = 根据 Q 和 K 的匹配程度，决定 V 的权重。 •\tCausal Mask（下三角遮罩） = 老师规定：写第 i 个字时，只能看前面写过的，不许看后面的。 •\t多头 (Multi-head) = 你同时派出好几个“审稿小人”，从不同角度帮你挑参考内容，最后合并。 注意点 •\tbias 的大小是按照 最大序列长度 block_size 来建的。如果实际推理时句子更长，mask 不够用，就会报错。 •\t但用 Flash 路径就不会有这个问题，因为它是动态生成的。 总结 CausalSelfAttention 就像写作文时的小抄机制。每个字只能看前面的字，不许看未来。多个小组（头）一起决定要参考谁，最后合并结果。\nMLP（前馈网络） python 1 2 3 4 self.c_fc : Linear(C, 4C) self.gelu : GELU self.c_proj : Linear(4C, C) self.dropout •\t它的作用是：先把每个位置的表示（维度为 C）“放大”成 4C，做非线性变换（GELU），然后再“缩回来”到 C。这就像把每个词的信息拉出来、进行深入加工，然后再压缩整合，丰富表达力。 •\t这种“中间层扩大 4 倍”的设置在 GPT、BERT 里很常见，因为它能让网络学到更复杂、更细致的表达。 •\tGELU 是一种激活函数，形状比起常见的 ReLU 更平滑、更“智能”。它会根据输入的正负和大小决定“保留多少信息”（通过乘以正态累计函数）。 •\tFFN 是 position-wise，意味着它对每个 token（词）的位置完全独立操作，互不干扰。就好像你有一堆独立的“机械臂”，每个机械臂只负责处理一个词的信息。 •\t这样做让所有 token 都能并行处理，极大提升效率，同时还能保留每个 token 的独特表示。\nBlock（残差结构单元） python 1 2 x = x + self.attn(self.ln1(x)) x = x + self.mlp(self.ln2(x)) Pre‑LN 结构（先 LN 再子层），更稳定、易收敛。 两次残差。 配置类 python 1 2 3 4 5 6 7 8 9 @dataclass class GPTConfig: block_size: int vocab_size: int n_layer: int n_head: int n_embd: int dropout: float = 0.0 bias: bool = True 所有超参数都装这里。block_size 是上下文长度（位置嵌入表大小）。 训练循环 这里我改造了一下代码。加强ckpt 的保存。但是跑完一次之后，我发现似乎是我的数据处理有问题，时间有限。大概理解了一点点训练流程，估计还是要从基础开始学起。这个后面再进行了，这次训练模型只花了两个小时左右\n[python] 显示已折叠代码（197 行） 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 import os, math, csv, time import torch from torch.optim.lr_scheduler import LinearLR, SequentialLR, CosineAnnealingLR # ====== 设备/AMP 设置 ====== device = \u0026#34;cuda\u0026#34; if torch.cuda.is_available() else \u0026#34;cpu\u0026#34; device_type = \u0026#34;cuda\u0026#34; if device == \u0026#34;cuda\u0026#34; else \u0026#34;cpu\u0026#34; dtype = \u0026#34;bfloat16\u0026#34; if (device == \u0026#34;cuda\u0026#34; and torch.cuda.is_bf16_supported()) else (\u0026#34;float16\u0026#34; if device == \u0026#34;cuda\u0026#34; else \u0026#34;float32\u0026#34;) ptdtype = {\u0026#34;float32\u0026#34;: torch.float32, \u0026#34;bfloat16\u0026#34;: torch.bfloat16, \u0026#34;float16\u0026#34;: torch.float16}[dtype] ctx = (torch.amp.autocast(device_type=device_type, dtype=ptdtype) if device != \u0026#34;cpu\u0026#34; else torch.autocast(\u0026#34;cpu\u0026#34;, dtype=ptdtype)) if dtype != \u0026#34;float32\u0026#34; else torch.no_grad if False else torch.enable_grad() torch.set_float32_matmul_precision(\u0026#34;high\u0026#34;) if device == \u0026#34;cuda\u0026#34; else None torch.manual_seed(42) if device == \u0026#34;cuda\u0026#34;: torch.cuda.manual_seed_all(42) # ====== 工具：初始化/追加 CSV ====== def _open_metrics_csv(path): new_file = not os.path.exists(path) f = open(path, \u0026#34;a\u0026#34;, newline=\u0026#34;\u0026#34;) w = csv.writer(f) if new_file: w.writerow([\u0026#34;step\u0026#34;, \u0026#34;opt_step\u0026#34;, \u0026#34;time_s\u0026#34;, \u0026#34;train_loss\u0026#34;, \u0026#34;val_loss\u0026#34;, \u0026#34;lr\u0026#34;]) f.flush(); os.fsync(f.fileno()) return f, w # ====== 工具：保存/加载断点 ====== def _to_bytetensor(x): \u0026#34;\u0026#34;\u0026#34;尽量把 x 转成 CPU 上的 uint8 ByteTensor；不行就抛异常。\u0026#34;\u0026#34;\u0026#34; if isinstance(x, torch.Tensor): return x.detach().to(\u0026#39;cpu\u0026#39;).to(torch.uint8) try: import numpy as np if isinstance(x, np.ndarray): return torch.from_numpy(x.astype(\u0026#39;uint8\u0026#39;, copy=False)) except Exception: pass if isinstance(x, (bytes, bytearray)): return torch.tensor(list(x), dtype=torch.uint8) if isinstance(x, (list, tuple)): # 可能是纯 int 列表 try: return torch.tensor(x, dtype=torch.uint8) except Exception: pass raise TypeError(f\u0026#34;cannot convert type {type(x)} to ByteTensor\u0026#34;) def save_ckpt(path, model, optimizer, scheduler, scaler, step, opt_step, best_val_loss): payload = { \u0026#34;model\u0026#34;: model.state_dict(), \u0026#34;optimizer\u0026#34;: optimizer.state_dict(), \u0026#34;scheduler\u0026#34;: scheduler.state_dict(), \u0026#34;scaler\u0026#34;: (scaler.state_dict() if scaler is not None else None), \u0026#34;step\u0026#34;: int(step), \u0026#34;opt_step\u0026#34;: int(opt_step), \u0026#34;best_val_loss\u0026#34;: float(best_val_loss), \u0026#34;rng\u0026#34;: { \u0026#34;torch\u0026#34;: _to_bytetensor(torch.get_rng_state()), \u0026#34;cuda\u0026#34;: ( [ _to_bytetensor(s) for s in torch.cuda.get_rng_state_all() ] if torch.cuda.is_available() else None ), } } torch.save(payload, path) def load_ckpt(path, model, optimizer, scheduler, scaler): ckpt = torch.load(path, map_location=\u0026#39;cpu\u0026#39;) # 先加载到 CPU 更安全 model.load_state_dict(ckpt[\u0026#34;model\u0026#34;]) optimizer.load_state_dict(ckpt[\u0026#34;optimizer\u0026#34;]) scheduler.load_state_dict(ckpt[\u0026#34;scheduler\u0026#34;]) if scaler is not None and ckpt.get(\u0026#34;scaler\u0026#34;) is not None: scaler.load_state_dict(ckpt[\u0026#34;scaler\u0026#34;]) step = int(ckpt.get(\u0026#34;step\u0026#34;, 0)) opt_step = int(ckpt.get(\u0026#34;opt_step\u0026#34;, step)) best_val_loss = float(ckpt.get(\u0026#34;best_val_loss\u0026#34;, float(\u0026#34;inf\u0026#34;))) # —— 尝试恢复 RNG（失败就给提示并跳过）—— try: tstate = ckpt.get(\u0026#34;rng\u0026#34;, {}).get(\u0026#34;torch\u0026#34;, None) if tstate is not None: torch.set_rng_state(_to_bytetensor(tstate)) except Exception as e: print(f\u0026#34;[WARN] skip restoring torch RNG: {e}\u0026#34;) try: cstates = ckpt.get(\u0026#34;rng\u0026#34;, {}).get(\u0026#34;cuda\u0026#34;, None) if (cstates is not None) and torch.cuda.is_available(): # 允许各种形态：list[Tensor/ndarray/list...] → list[ByteTensor] states_bt = [] for s in cstates: try: states_bt.append(_to_bytetensor(s)) except Exception: pass # 如果数量不匹配，就尽力把第一个应用到所有可见 GPU if len(states_bt) == torch.cuda.device_count(): torch.cuda.set_rng_state_all(states_bt) elif len(states_bt) \u0026gt;= 1: for dev in range(torch.cuda.device_count()): torch.cuda.set_rng_state(states_bt[0], device=dev) else: print(\u0026#34;[WARN] cuda RNG in ckpt is empty; skip\u0026#34;) except Exception as e: print(f\u0026#34;[WARN] skip restoring cuda RNG: {e}\u0026#34;) return step, opt_step, best_val_loss # ====== 构建优化器/调度器/AMP ====== def build_optim_sched(model): optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, betas=betas, weight_decay=weight_decay, eps=eps) # 以“优化步”为时间单位设置 scheduler，比用微步更合理 total_updates = math.ceil(max_iters / gradient_accumulation_steps) warmup_updates = max(1, math.ceil(warmup_iters / gradient_accumulation_steps)) decay_updates = max(1, total_updates - warmup_updates) scheduler_warmup = LinearLR(optimizer, start_factor=1e-8, total_iters=warmup_updates) scheduler_decay = CosineAnnealingLR(optimizer, T_max=decay_updates, eta_min=min_lr) scheduler = SequentialLR(optimizer, [scheduler_warmup, scheduler_decay], milestones=[warmup_updates]) scaler = torch.cuda.amp.GradScaler(enabled=(dtype == \u0026#34;float16\u0026#34;)) return optimizer, scheduler, scaler, total_updates, warmup_updates # ====== 主训练 ====== def train(model): model = model.to(device) optimizer, scheduler, scaler, total_updates, warmup_updates = build_optim_sched(model) # 断点恢复（可多次中断/继续） start_step = -1 opt_step = 0 best_val_loss = float(\u0026#34;inf\u0026#34;) if resume_if_possible and os.path.exists(last_ckpt_path): start_step, opt_step, best_val_loss = load_ckpt(last_ckpt_path, model, optimizer, scheduler, scaler) print(f\u0026#34;[RESUME] from {last_ckpt_path}: step={start_step}, opt_step={opt_step}, best_val={best_val_loss:.6f}, lr={optimizer.param_groups[0][\u0026#39;lr\u0026#39;]:.6f}\u0026#34;) metrics_f, metrics_w = _open_metrics_csv(metrics_csv_path) t0 = time.time() try: for step in range(start_step + 1, max_iters): # === 取数据（建议你的 get_batch 已经把张量送到 device）=== X, y = get_batch(\u0026#34;train\u0026#34;) # === 前向/反向（累积梯度）=== with ctx: logits, loss = model(X, y) loss = loss / gradient_accumulation_steps scaler.scale(loss).backward() # === 到了累积边界：做一次优化步 \u0026amp; scheduler === boundary = ((step + 1) % gradient_accumulation_steps == 0) or (step + 1 == max_iters) if boundary: if scaler.is_enabled(): scaler.unscale_(optimizer) # 先反缩放，再裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.5) scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True) scheduler.step() # 只在真正 optimizer.step() 后走一步 opt_step += 1 # === 周期性评估/记录/保存最优 === if (step % eval_every == 0) and (step != start_step + 1): # 避免刚恢复的一次立刻评估（可按需调整） losses = estimate_loss(model) lr = optimizer.param_groups[0][\u0026#34;lr\u0026#34;] print(f\u0026#34;[step {step} | opt {opt_step}] train {losses[\u0026#39;train\u0026#39;]:.4f} val {losses[\u0026#39;val\u0026#39;]:.4f} lr {lr:.6f}\u0026#34;) # 写 CSV（训练/验证损失 + lr） metrics_w.writerow([step, opt_step, round(time.time() - t0, 2), float(losses[\u0026#34;train\u0026#34;]), float(losses[\u0026#34;val\u0026#34;]), float(lr)]) metrics_f.flush(); os.fsync(metrics_f.fileno()) # 保存 best if losses[\u0026#34;val\u0026#34;] \u0026lt; best_val_loss: best_val_loss = losses[\u0026#34;val\u0026#34;] torch.save(model.state_dict(), best_model_params_path) # === 可选：定期保存完整断点（例如每 2000 微步）=== if step % 2000 == 0 and step != start_step + 1: save_ckpt(last_ckpt_path, model, optimizer, scheduler, scaler, step, opt_step, best_val_loss) # 训练正常结束再保存一次断点 save_ckpt(last_ckpt_path, model, optimizer, scheduler, scaler, step, opt_step, best_val_loss) print(\u0026#34;Training finished.\u0026#34;) except KeyboardInterrupt: print(\u0026#34;\\n[Interrupt] Saving last checkpoint...\u0026#34;) save_ckpt(last_ckpt_path, model, optimizer, scheduler, scaler, step, opt_step, best_val_loss) print(f\u0026#34;Saved to {last_ckpt_path}. You can resume later.\u0026#34;) finally: metrics_f.close() 每个 Transformer block 是这样走的： 1.\t输入先过 LayerNorm 2.\t接着进到 自注意力层 → 输出 + 残差连接 3.\t再过一次 LayerNorm 4.\t进入 前馈 MLP（C → 4C → GELU → C） → 输出 + 残差连接 5.\t最终输出传给下一层\n推理 绘图\npython 1 2 3 4 5 6 7 8 9 10 11 import matplotlib.pyplot as plt train_loss_list_converted = [i.cpu().detach() for i in train_loss_list] validation_loss_list_converted = [i.cpu().detach() for i in validation_loss_list] plt.plot(train_loss_list_converted, \u0026#39;g\u0026#39;, label=\u0026#39;train_loss\u0026#39;) plt.plot(validation_loss_list_converted, \u0026#39;r\u0026#39;, label=\u0026#39;validation_loss\u0026#39;) plt.xlabel(\u0026#34;Steps - Every 100 epochs\u0026#34;) plt.ylabel(\u0026#34;Loss\u0026#34;) plt.legend() plt.show() [python] 显示已折叠代码（15 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #加载模型 model = GPT(config) # 使用相同的配置重新创建模型 device = \u0026#34;cuda\u0026#34; if torch.cuda.is_available() else \u0026#34;cpu\u0026#34; best_model_params_path = \u0026#34;best_model_params.pt\u0026#34; model.load_state_dict(torch.load(best_model_params_path, map_location=torch.device(device))) # 加载最优模型状态 sentence = \u0026#34;Once upon a time there was a pumpkin.\u0026#34; context = (torch.tensor(enc.encode_ordinary(sentence)).unsqueeze(dim = 0)) y = model.generate(context, 200) print(enc.decode(y.squeeze().tolist())) sentence = \u0026#34;A little girl went to the woods\u0026#34; context = (torch.tensor(enc.encode_ordinary(sentence)).unsqueeze(dim = 0)) y = model.generate(context, 200) print(enc.decode(y.squeeze().tolist())) 看推理结果可以判断，应该是我切数据集切出问题了。失误了，但是本次学习已经学到一些皮毛，足矣，暂时不重新搞了。以后有机会自己训练一个小 BERT 模型 看看有什么用\n","permalink":"/posts/slm-2-%E6%A8%A1%E5%9E%8B%E8%AE%AD%E7%BB%83/","summary":"\u003ch2 id=\"接上篇定义-slm-的模型架构\"\u003e接上篇，定义 SLM 的模型架构\u003c/h2\u003e\n\u003cp\u003e代码如下\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdetails class=\"code-details\"\u003e\n  \u003csummary\u003e\n      \u003cspan class=\"code-summary-left\"\u003e\n          \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n          \u003cspan\u003e[python] 显示已折叠代码（172 行）\u003c/span\u003e\n      \u003c/span\u003e\n  \u003c/summary\u003e\n  \u003cdiv class=\"collapsible-content\"\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e  1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 49\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 50\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 51\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 52\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 53\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 54\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 55\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 56\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 57\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 58\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 59\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 60\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 61\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 62\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 63\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 64\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 65\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 66\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 67\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 68\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 69\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 70\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 71\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 72\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 73\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 74\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 75\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 76\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 77\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 78\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 79\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 80\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 81\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 82\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 83\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 84\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 85\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 86\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 87\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 88\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 89\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 90\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 91\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 92\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 93\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 94\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 95\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 96\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 97\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 98\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 99\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e100\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e101\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e102\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e103\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e104\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e105\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e106\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e107\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e108\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e109\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e110\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e111\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e112\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e113\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e114\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e115\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e116\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e117\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e118\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e119\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e120\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e121\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e122\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e123\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e124\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e125\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e126\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e127\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e128\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e129\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e130\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e131\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e132\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e133\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e134\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e135\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e136\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e137\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e138\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e139\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e140\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e141\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e142\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e143\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e144\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e145\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e146\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e147\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e148\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e149\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e150\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e151\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e152\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e153\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e154\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e155\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e156\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e157\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e158\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e159\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e160\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e161\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e162\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e163\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e164\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e165\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e166\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e167\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e168\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e169\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e170\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e171\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e172\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003etorch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003etorch.nn\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"nn\"\u003enn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003etorch.nn.functional\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"nn\"\u003eF\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003emath\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"nn\"\u003edataclasses\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003edataclass\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003enumpy\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"nn\"\u003enp\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"nn\"\u003etqdm.auto\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003etqdm\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003efrom\u003c/span\u003e \u003cspan class=\"nn\"\u003econtextlib\u003c/span\u003e \u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003enullcontext\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"nn\"\u003eos\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLayerNorm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModule\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\t- 与 `nn.LayerNorm(ndim, elementwise_affine=bias)` 等价，手搓是为了**可控是否带 bias**。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\t- 作用：把最后一维做标准化，稳定训练。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\t- 形状不变：`(B, T, C) → (B, T, C)`。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\t\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003endim\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nb\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eweight\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParameter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eones\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003endim\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParameter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ezeros\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003endim\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003ebias\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eforward\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eF\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003elayer_norm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eweight\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eshape\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eweight\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mf\"\u003e1e-5\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCausalSelfAttention\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModule\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e自回归注意力\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nb\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eassert\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_attn\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLinear\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e3\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e# 生成 Q,K,V\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_proj\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLinear\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e# 多头拼回后做投影\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e         \u003cspan class=\"c1\"\u003e# 注意力/残差丢弃\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eattn_dropout\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDropout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edropout\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eresid_dropout\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDropout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edropout\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eflash\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003ehasattr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eF\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;scaled_dot_product_attention\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eflash\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eregister_buffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;bias\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etril\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eones\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                                       \u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eview\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eforward\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eC\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ek\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ev\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_attn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esplit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edim\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ek\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ek\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eview\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eC\u003c/span\u003e \u003cspan class=\"o\"\u003e//\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etranspose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eq\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eview\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eC\u003c/span\u003e \u003cspan class=\"o\"\u003e//\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etranspose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ev\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ev\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eview\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eC\u003c/span\u003e \u003cspan class=\"o\"\u003e//\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_head\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etranspose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eflash\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ey\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eF\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003escaled_dot_product_attention\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ek\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ev\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eattn_mask\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edropout_p\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eattn_dropout\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etraining\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"mf\"\u003e0.0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eis_causal\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eatt\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e \u003cspan class=\"o\"\u003e@\u003c/span\u003e \u003cspan class=\"n\"\u003ek\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etranspose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mf\"\u003e1.0\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003emath\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esqrt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ek\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eatt\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eatt\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003emasked_fill\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e[:,\u003c/span\u003e \u003cspan class=\"p\"\u003e:,\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003efloat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;-inf\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eatt\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eF\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esoftmax\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eatt\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edim\u003c/span\u003e\u003cspan class=\"o\"\u003e=-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eatt\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eattn_dropout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eatt\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ey\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eatt\u003c/span\u003e \u003cspan class=\"o\"\u003e@\u003c/span\u003e \u003cspan class=\"n\"\u003ev\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ey\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etranspose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econtiguous\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eview\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eC\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ey\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eresid_dropout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_proj\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ey\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMLP\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModule\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nb\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_fc\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLinear\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003egelu\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGELU\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_proj\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLinear\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e4\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edropout\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDropout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edropout\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eforward\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edropout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_proj\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003egelu\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_fc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e))))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eBlock\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModule\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nb\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eln1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eLayerNorm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eattn\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCausalSelfAttention\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eln2\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eLayerNorm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003emlp\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMLP\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eforward\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eattn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eln1\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003emlp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eln2\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@dataclass\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eGPTConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003evocab_size\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003en_layer\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003en_head\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003edropout\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003efloat\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mf\"\u003e0.0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003ebool\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003eTrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eGPT\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModule\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nb\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etransformer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModuleDict\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003edict\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ewte\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmbedding\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evocab_size\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ewpe\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmbedding\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003edrop\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDropout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edropout\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eh\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModuleList\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"n\"\u003eBlock\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"nb\"\u003erange\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_layer\u003c/span\u003e\u003cspan class=\"p\"\u003e)]),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eln_f\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eLayerNorm\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003elm_head\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLinear\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003evocab_size\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etransformer\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ewte\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eweight\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003elm_head\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eweight\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# weight tying\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eapply\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003e_init_weights\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003epn\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enamed_parameters\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003epn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eendswith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;c_proj.weight\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einit\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emean\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mf\"\u003e0.0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mf\"\u003e0.02\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003emath\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esqrt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003en_layer\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003e_init_weights\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emodule\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nb\"\u003eisinstance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emodule\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLinear\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einit\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emodule\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eweight\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emean\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mf\"\u003e0.0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mf\"\u003e0.02\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003emodule\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e \u003cspan class=\"ow\"\u003eis\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einit\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ezeros_\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emodule\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelif\u003c/span\u003e \u003cspan class=\"nb\"\u003eisinstance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emodule\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmbedding\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einit\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emodule\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eweight\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emean\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mf\"\u003e0.0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mf\"\u003e0.02\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eforward\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eidx\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etargets\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edevice\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eidx\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edevice\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eidx\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eassert\u003c/span\u003e \u003cspan class=\"n\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epos\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003earange\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edtype\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003elong\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edevice\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003edevice\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003etok_emb\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etransformer\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ewte\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eidx\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epos_emb\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etransformer\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ewpe\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epos\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etransformer\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edrop\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etok_emb\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003epos_emb\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003eblock\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etransformer\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eh\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eblock\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etransformer\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eln_f\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003etargets\u003c/span\u003e \u003cspan class=\"ow\"\u003eis\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003elogits\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003elm_head\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eloss\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eF\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecross_entropy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eview\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)),\u003c/span\u003e \u003cspan class=\"n\"\u003etargets\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eview\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003eignore_index\u003c/span\u003e\u003cspan class=\"o\"\u003e=-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eloss\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003elogits\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003elm_head\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e[:,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"p\"\u003e:])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nd\"\u003e@torch.no_grad\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003egenerate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eidx\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emax_new_tokens\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etemperature\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mf\"\u003e1.0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etop_k\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e        Generate tokens given a conditioning sequence.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e        idx: Tensor of shape (B, T)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e        \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"nb\"\u003erange\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emax_new_tokens\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eidx_cond\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eidx\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eidx\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"n\"\u003eidx\u003c/span\u003e\u003cspan class=\"p\"\u003e[:,\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\u003cspan class=\"p\"\u003e:]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eidx_cond\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003elogits\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"p\"\u003e[:,\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e:]\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003etemperature\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003etop_k\u003c/span\u003e \u003cspan class=\"ow\"\u003eis\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ev\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etopk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003emin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etop_k\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003elogits\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ev\u003c/span\u003e\u003cspan class=\"p\"\u003e[:,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e]]]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"nb\"\u003efloat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Inf\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eprobs\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eF\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esoftmax\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogits\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edim\u003c/span\u003e\u003cspan class=\"o\"\u003e=-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eidx_next\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003emultinomial\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprobs\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enum_samples\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eidx\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etorch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecat\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003eidx\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eidx_next\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003edim\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eidx\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGPTConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003evocab_size\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e50257\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e     \u003cspan class=\"c1\"\u003e# 使用分词器的词汇表大小\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eblock_size\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e128\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e       \u003cspan class=\"c1\"\u003e# 或你训练时使用的任何上下文大小\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003en_layer\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e6\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003en_head\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e6\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003en_embd\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e384\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003edropout\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mf\"\u003e0.1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebias\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eTrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGPT\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003ch2 id=\"causalselfattention自回归注意力\"\u003eCausalSelfAttention（自回归注意力）\u003c/h2\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \u003cspan class=\"code-lang\"\u003epython\u003c/span\u003e\n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eassert\u003c/span\u003e \u003cspan class=\"n\"\u003en_embd\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"n\"\u003en_head\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_attn\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLinear\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eC\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"n\"\u003eC\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e      \u003cspan class=\"c1\"\u003e# 生成 Q,K,V \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_proj\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enn\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLinear\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eC\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eC\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e       \u003cspan class=\"c1\"\u003e# 多头拼回后做投影 \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eattn_dropout\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003eresid_dropout\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# 注意力/残差丢弃 \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eflash\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003ehasattr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eF\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;scaled_dot_product_attention\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003eflash\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e     \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eregister_buffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;bias\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etril\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eones\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eview\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cul\u003e\n\u003cli\u003e将输入 \u003ccode\u003ex\u003c/code\u003e 线性变换得到 \u003cstrong\u003eq/k/v\u003c/strong\u003e，每个 shape \u003ccode\u003e(B, T, C)\u003c/code\u003e，再 reshape 成 \u003ccode\u003e(B, n_head, T, head_dim)\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFlash/SDPA 路径\u003c/strong\u003e：PyTorch 内置 \u003ccode\u003escaled_dot_product_attention\u003c/code\u003e，设 \u003ccode\u003eis_causal=True\u003c/code\u003e 自动做下三角 mask，快而省显存。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e回退路径\u003c/strong\u003e：手动算 \u003ccode\u003eatt = q @ k^T / sqrt(d)\u003c/code\u003e，再用注册的下三角 \u003ccode\u003ebias\u003c/code\u003e 做 mask：\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003ebias\u003c/code\u003e 维度 \u003ccode\u003e(1,1,T,T)\u003c/code\u003e，只保留 \u003ccode\u003ei≥j\u003c/code\u003e 的位置，保证\u003cstrong\u003e只能看见过去\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e最后把多头输出拼回 \u003ccode\u003e(B, T, C)\u003c/code\u003e，投影并做残差丢弃。\n\u003cstrong\u003e形状小抄\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e入：\u003ccode\u003ex: (B, T, C)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e出：\u003ccode\u003ey: (B, T, C)\u003c/code\u003e\n\u003cstrong\u003e注意\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ebias\u003c/code\u003e 的 \u003ccode\u003eT\u003c/code\u003e 用的是 \u003ccode\u003econfig.block_size\u003c/code\u003e。若推理时序列长度 \u0026gt; \u003ccode\u003eblock_size\u003c/code\u003e，回退路径会越界；但 Flash 路径不受此限。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"解释\"\u003e解释\u003c/h3\u003e\n\u003ch4 id=\"基础设定\"\u003e基础设定\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003ex: (B, T, C)\u003c/strong\u003e\n你有一段输入，比如一句话里的字/词。\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eB\u003c/strong\u003e = batch，大概就是“同时处理多少句话”。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eT\u003c/strong\u003e = 时间步长，就是句子有多少个字/词。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eC\u003c/strong\u003e = 每个字/词的向量维度，可以理解成“每个字有多少个特征”。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eQ/K/V（查询、键、值）\n•\t把输入向量 x 分三份：\n•\tQ (Query 查询)：我要看别的词。\n•\tK (Key 键)：我能提供什么信息。\n•\tV (Value 值)：具体信息内容。\n•\t每个 shape 最开始都是 (B, T, C)。再 reshape 成 (B, n_head, T, head_dim)：就像让很多小组（head）分头看，不同小组看问题的角度不同。\u003c/li\u003e\n\u003cli\u003eFlash 路径 vs 回退路径\n•\tFlash / SDPA 路径：PyTorch 内置的 scaled_dot_product_attention，就像显卡加速版，自动帮你算“谁可以看谁”。只要设 is_causal=True，它会自动加下三角遮罩（mask），保证当前词不能偷看未来。又快又省内存。\n•\t回退路径（手工实现）：如果没有显卡加速，就自己算：\n\u003col\u003e\n\u003cli\u003e做点积：att = q @ k^T / sqrt(d) → 代表“查询词对键的相关性”。\u003c/li\u003e\n\u003cli\u003e用 bias（下三角矩阵）遮住未来：bias 形状 (1, 1, T, T)，保证第 i 个词只能看到自己和之前的词。\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e拼回与投影\n•\t各个头（小组）得到的信息会合并成 (B, T, C)。\n•\t再过一层线性变换（c_proj），把信息“压缩整理”回原来的维度。\n•\t最后做 dropout（随机丢弃部分连接），防止过拟合。\u003c/li\u003e\n\u003cli\u003e形象比喻\n想象你在写作文，每个字要决定自己怎么写：\n•\tQ = 我在想：我要参考哪些前面的字？\n•\tK = 每个字举手说：我能告诉你些什么。\n•\tV = 每个字手里的小抄内容。\n•\tAttention = 根据 Q 和 K 的匹配程度，决定 V 的权重。\n•\tCausal Mask（下三角遮罩） = 老师规定：写第 i 个字时，只能看前面写过的，不许看后面的。\n•\t多头 (Multi-head) = 你同时派出好几个“审稿小人”，从不同角度帮你挑参考内容，最后合并。\u003c/li\u003e\n\u003cli\u003e注意点\n•\tbias 的大小是按照 最大序列长度 block_size 来建的。如果实际推理时句子更长，mask 不够用，就会报错。\n•\t但用 Flash 路径就不会有这个问题，因为它是动态生成的。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"总结\"\u003e总结\u003c/h4\u003e\n\u003cp\u003e\u003cstrong\u003eCausalSelfAttention 就像写作文时的小抄机制。每个字只能看前面的字，不许看未来。多个小组（头）一起决定要参考谁，最后合并结果。\u003c/strong\u003e\u003c/p\u003e","title":"SLM-2-模型训练"},{"content":"学习别人给到的 Jupyter 代码时，其中有安装依赖的代码 使用\nJupyter 1 !pip install numpy 这里我看到 vscode 插件提示我 但是据我测试，%pip 和 !pip 都是能正常执行的，我不是很明白 py note所以搜索了一下，盲猜和 python 环境是有关的\n相关链接\nMedium,Installing Python Packages in Jupyter Notebooks Trouble with pip installation - JupyterLab / extensions - Jupyter Community Forum Installing Python Packages from a Jupyter Notebook | Pythonic Perambulations\n结论 !pip install ：调用外部 Shell 在 Jupyter 中以 ! 开头的命令会被当作 Shell 命令 执行，比如直接调用系统或虚拟环境中的 pip：\n1 !pip install numpy 它在新的子进程中执行，依赖于系统的 PATH 环境变量，可能会使用错误的 Python 解释器或 pip。\n如果你的 Jupyter kernel 使用的是一个虚拟环境或不同的 Python 环境，!pip 可能安装在与当前 kernel 不一致的地方，导致导入失败或找不到已安装的包。\n%pip install ：Jupyter/IPython 的 Magic 命令 以 % 开头的是 IPython 的 Magic 命令，比如 %pip 是专门为 Jupyter 环境设计的魔法命令：\n1 %pip install numpy 它明确针对当前 Notebook 所使用的 Python kernel 环境运行，确保包安装在正确的环境中。\nJupyter 社区更推荐使用 %pip，它在 2019 年引入，目的是避免 !pip 带来的环境不一致和安装失败问题。\n执行方式 执行环境 风险与优点 推荐用途 !pip install Shell 环境（系统环境） 易导致包安装在与 kernel 不一致的环境 一般不推荐，除非特殊用途 %pip install 当前 Notebook kernel 安装精准、环境一致，可靠性高 推荐在 Jupyter 中使用 ","permalink":"/posts/jupyter-%E5%92%8C/","summary":"\u003cp\u003e学习别人给到的 Jupyter 代码时，其中有安装依赖的代码 使用\u003c/p\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \u003cspan class=\"code-lang\"\u003eJupyter\u003c/span\u003e\n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e!pip install numpy\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e这里我看到 vscode 插件提示我\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/08/f7c91c81c5093e0171ad3a8cf3e8c15f.png\"\u003e\n        \u003cimg alt=\"Jupyter 中使用 !pip 安装依赖\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/08/f7c91c81c5093e0171ad3a8cf3e8c15f.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\n但是据我测试，%pip 和 !pip 都是能正常执行的，我不是很明白 py note所以搜索了一下，盲猜和 python 环境是有关的\u003c/p\u003e\n\u003cp\u003e相关链接\u003c/p\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e\u003ca href=\"https://medium.com/%40aroffe9/installing-python-packages-in-jupyter-notebooks-ca7dc7dd7535\"\u003eMedium,Installing Python Packages in Jupyter Notebooks\u003c/a\u003e\n\u003ca href=\"https://discourse.jupyter.org/t/trouble-with-pip-installation/28560\"\u003eTrouble with pip installation - JupyterLab / extensions - Jupyter Community Forum\u003c/a\u003e\n\u003ca href=\"https://jakevdp.github.io/blog/2017/12/05/installing-python-packages-from-jupyter\"\u003eInstalling Python Packages from a Jupyter Notebook | Pythonic Perambulations\u003c/a\u003e\u003c/p\u003e","title":"jupyter !和%"},{"content":"流程 数据导入 - 使用 TinyStories 数据集 数据分词 - 使用 tiktoken 进行 GPT-2 风格的分词 批次创建 - 为训练创建输入-输出批次 模型架构 - 实现了完整的 GPT 架构，包括： LayerNorm CausalSelfAttention MLP Block GPT 主模型类 损失函数 - estimate_loss() 函数 训练配置 - 学习率、批次大小等超参数设置 优化器和调度器 - AdamW 优化器配合学习率调度 训练循环 - 完整的训练过程 可视化 - 损失函数曲线绘制 推理测试 - 模型生成文本的示例 数据导入 用的数据集是 tinystory roneneldan/TinyStories · Datasets at Hugging Face 这是由 gpt 生成的\n首先安装依赖后加载数据\n依赖\nrequirements.txt 1 2 datasets tiktoken 这里我直接拿到的是 txt 文件。如果网络方便的话可以直接\npython 1 2 3 from datasets import load_dataset # 导入数据，这里记得科学上网，否则无法在hf上下载数据集 ds = load_dataset(\u0026#34;roneneldan/TinyStories\u0026#34;) 所以我这边根据 C 老师的指导加载本地数据 以及 验证数据加载效果。代码如下\n[python] 显示已折叠代码（57 行） 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 from datasets import Dataset, DatasetDict # 导入本地数据集 def load_local_dataset(): train_data = [] val_data = [] # 读取训练数据 with open(\u0026#39;/data/yangr/yyai-slm/datasets/TinyStories-train.txt\u0026#39;, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: train_content = f.read() # 按空行分割故事 stories = train_content.strip().split(\u0026#39;\\n\\n\u0026#39;) train_data = [{\u0026#39;text\u0026#39;: story.strip()} for story in stories if story.strip()] # 读取验证数据 with open(\u0026#39;/data/yangr/yyai-slm/datasets/TinyStories-valid.txt\u0026#39;, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: val_content = f.read() # 按空行分割故事 stories = val_content.strip().split(\u0026#39;\\n\\n\u0026#39;) val_data = [{\u0026#39;text\u0026#39;: story.strip()} for story in stories if story.strip()] # 创建数据集 train_dataset = Dataset.from_list(train_data) val_dataset = Dataset.from_list(val_data) return DatasetDict({ \u0026#39;train\u0026#39;: train_dataset, \u0026#39;validation\u0026#39;: val_dataset }) ds = load_local_dataset() # 查看数据集的键（分割） print(\u0026#34;数据集分割:\u0026#34;, list(ds.keys())) print() # 查看训练集和验证集的大小 print(f\u0026#34;训练集大小: {len(ds[\u0026#39;train\u0026#39;])}\u0026#34;) print(f\u0026#34;验证集大小: {len(ds[\u0026#39;validation\u0026#39;])}\u0026#34;) print() # 查看数据集的特征（列） print(\u0026#34;数据集特征:\u0026#34;, ds[\u0026#39;train\u0026#39;].features) print() # 查看前几个样本 print(\u0026#34;训练集前3个样本:\u0026#34;) for i in range(min(3, len(ds[\u0026#39;train\u0026#39;]))): print(f\u0026#34;样本 {i+1}:\u0026#34;) print(ds[\u0026#39;train\u0026#39;][i][\u0026#39;text\u0026#39;][:200] + \u0026#34;...\u0026#34; if len(ds[\u0026#39;train\u0026#39;][i][\u0026#39;text\u0026#39;]) \u0026gt; 200 else ds[\u0026#39;train\u0026#39;][i][\u0026#39;text\u0026#39;]) print(\u0026#34;-\u0026#34; * 50) print(\u0026#34;\\n验证集前2个样本:\u0026#34;) for i in range(min(2, len(ds[\u0026#39;validation\u0026#39;]))): print(f\u0026#34;样本 {i+1}:\u0026#34;) print(ds[\u0026#39;validation\u0026#39;][i][\u0026#39;text\u0026#39;][:200] + \u0026#34;...\u0026#34; if len(ds[\u0026#39;validation\u0026#39;][i][\u0026#39;text\u0026#39;]) \u0026gt; 200 else ds[\u0026#39;validation\u0026#39;][i][\u0026#39;text\u0026#39;]) print(\u0026#34;-\u0026#34; * 50) 对数据集进行分词 (1) 将数据集分词为 tokenIDs。 (2) 创建名为 \u0026ldquo;train.bin\u0026rdquo; 和 \u0026ldquo;validation.bin\u0026rdquo; 的文件，用于存储整个数据集的 tokenIDs。 (3) 我们确保 tokenIDs 存储在磁盘上，而不是内存中，以实现高效的计算。\n[python] 显示已折叠代码（37 行） 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 !pip install tiktoken import tiktoken import os import numpy as np from tqdm.auto import tqdm enc = tiktoken.get_encoding(\u0026#34;gpt2\u0026#34;) def process(example): ids = enc.encode_ordinary(example[\u0026#39;text\u0026#39;]) # encode_ordinary 会忽略所有特殊令牌 out = {\u0026#39;ids\u0026#39;: ids, \u0026#39;len\u0026#39;: len(ids)} return out if not os.path.exists(\u0026#34;train.bin\u0026#34;): tokenized = ds.map( process, remove_columns=[\u0026#39;text\u0026#39;], desc=\u0026#34;tokenizing the splits\u0026#34;, num_proc=8, ) # 将每个数据集中的所有 id 连接成一个大型文件，供训练使用 for split, dset in tokenized.items(): arr_len = np.sum(dset[\u0026#39;len\u0026#39;], dtype=np.uint64) filename = f\u0026#39;{split}.bin\u0026#39; dtype = np.uint16 # (可以这样做，因为 enc.max_token_value == 50256 小于 2**16) arr = np.memmap(filename, dtype=dtype, mode=\u0026#39;w+\u0026#39;, shape=(arr_len,)) total_batches = 1024 idx = 0 for batch_idx in tqdm(range(total_batches), desc=f\u0026#39;writing {filename}\u0026#39;): # 将样本批量组合以加快写入速度 batch = dset.shard(num_shards=total_batches, index=batch_idx, contiguous=True).with_format(\u0026#39;numpy\u0026#39;) arr_batch = np.concatenate(batch[\u0026#39;ids\u0026#39;]) # 写入内存映射文件（mmap） arr[idx : idx + len(arr_batch)] = arr_batch idx += len(arr_batch) arr.flush() 批次创建-为数据集创建输入-输出批次 从整段 token 流（*.bin）里随机抽取 batch_size 个长度为 block_size 的连续片段作为输入 x，并把它们右移一位得到标签 y，用于下一个 token 的预测（next-token prediction）。\n[python] 显示已折叠代码（15 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def get_batch(split): # 我们每个批次都重新创建 np.memmap，以避免内存泄漏 if split == \u0026#39;train\u0026#39;: data = np.memmap(\u0026#39;train.bin\u0026#39;, dtype=np.uint16, mode=\u0026#39;r\u0026#39;) else: data = np.memmap(\u0026#39;validation.bin\u0026#39;, dtype=np.uint16, mode=\u0026#39;r\u0026#39;) ix = torch.randint(len(data) - block_size, (batch_size,)) x = torch.stack([torch.from_numpy((data[i:i+block_size]).astype(np.int64)) for i in ix]) y = torch.stack([torch.from_numpy((data[i+1:i+1+block_size]).astype(np.int64)) for i in ix]) if device_type == \u0026#39;cuda\u0026#39;: # 固定数组 x 和 y，使我们能够将它们异步地移动到 GPU（non_blocking=True） x, y = x.pin_memory().to(device, non_blocking=True), y.pin_memory().to(device, non_blocking=True) else: x, y = x.to(device), y.to(device) return x, y ","permalink":"/posts/slm-1-%E6%95%B0%E6%8D%AE%E5%87%86%E5%A4%87/","summary":"\u003ch2 id=\"流程\"\u003e流程\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e数据导入\u003c/strong\u003e - 使用 TinyStories 数据集\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e数据分词\u003c/strong\u003e - 使用 tiktoken 进行 GPT-2 风格的分词\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e批次创建\u003c/strong\u003e - 为训练创建输入-输出批次\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e模型架构\u003c/strong\u003e - 实现了完整的 GPT 架构，包括：\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eLayerNorm\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eCausalSelfAttention\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eMLP\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eBlock\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eGPT\u003c/code\u003e 主模型类\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e损失函数\u003c/strong\u003e - \u003ccode\u003eestimate_loss()\u003c/code\u003e 函数\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e训练配置\u003c/strong\u003e - 学习率、批次大小等超参数设置\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e优化器和调度器\u003c/strong\u003e - AdamW 优化器配合学习率调度\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e训练循环\u003c/strong\u003e - 完整的训练过程\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e可视化\u003c/strong\u003e - 损失函数曲线绘制\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e推理测试\u003c/strong\u003e - 模型生成文本的示例\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"数据导入\"\u003e数据导入\u003c/h2\u003e\n\u003cp\u003e用的数据集是 tinystory \u003ca href=\"https://huggingface.co/datasets/roneneldan/TinyStories\"\u003eroneneldan/TinyStories · Datasets at Hugging Face\u003c/a\u003e\n这是由 gpt 生成的\u003c/p\u003e","title":"SLM-1-数据准备"},{"content":"新logo 叫 gpt 给我画了个 logo，元素就是姓名首字母啦，还是蛮喜欢的\n本次博客换了个主题和评论系统，参考文章放在末尾了。算是找到比较靠谱的几篇文章。由于我使用较新的版本，所以并不是能很好的复现文章中的效果，不过也足够满意了\n装修点 原先使用hugo-theme-stack 主题是由于接触 hugo 的教程中使用的就是这个，为了避免不必要的麻烦，初次基本都是按部就班复刻教程为主，这次心血来潮还是想换一个，挑了挑就觉得 papermod 不错，简单不复杂，不是很爱花哨的，但是也不想博客太丑了点\n评论区 原先为了自部署，用了 waline，结果发现 papermod 没有直接支持。由于我前端功力不够，又不会 go，在 gpt-5 的帮助下也没成功用上，只能放弃用个简单的，挑了挑还是选择Giscus Hugo博客系统添加Giscus评论功能 - Whohhの博客 参考这篇文章倒是一点困难没有就完成了，只是还是想要有自己部署的心里才舒服，不明白这是什么心态，精力有限，现在博客是无人问津的状态，也不需要操心这件事情\n便于阅读 Mermaid图 这个还是特别有用的，尤其是在 LLM 的帮助下，不少文章中画一个 markdown 中能渲染的流程图框架图之类的变得异常轻松\n以下摘用自台运鹏大佬的文章\nMermaid js 可以可以让我们用代码的方式画流程图（如上图），在文章的概念比较多或者关系复杂时，流程图就可以让读者更容易看懂，故而也引入了 mermaid 的实现\n首先创建 layouts/_default/_markup/render-codeblock-mermaid.html，写入以下内容：\nhtml 1 2 3 4 5 \u0026lt;!-- 因为正常写会有 ```meraid ... ``` --\u0026gt; \u0026lt;pre class=\u0026#34;mermaid\u0026#34;\u0026gt; {{- .Inner | htmlEscape | safeHTML }} \u0026lt;/pre\u0026gt; {{ .Page.Store.Set \u0026#34;hasMermaid\u0026#34; true }} 这样就可以将 mermaid 这种特殊的 codeblock 加入渲染机制里，同时设置 hasMermaid 为 true，方便后面判断是否加载 mermaid js。接着我们创建 layouts/partials/mermaid.html，来让 mermaid js 对我们写的代码进行渲染\n同时支持亮暗自动切换，大部分代码片段取自于 mermaid-js 社区的讨论，然而默认的代码是初次渲染是查看 localStorage 是否包含 pref-theme，很多时候用户并未手动点击切换是不会有这个值，即为 null。我这里是判断 document.body.className 是否包含 dark 来判断，更为准确\nmermaid 的字体设置依然是对齐正文，使用 mermaid.init() 设置即可：\n显示已折叠代码（66 行） 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 {{ if .Page.Store.Get \u0026#34;hasMermaid\u0026#34; }} \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; const elementCode = \u0026#34;.mermaid\u0026#34;; const loadMermaid = function (theme) { mermaid.initialize({ theme }); mermaid.init({ theme, themeVariables: { // 这里设置字体跟正文一致 fontFamily: [\u0026#34;SFProText-Regular\u0026#34;, \u0026#34;LXGWWenKaiScreenR\u0026#34;] }}, document.querySelectorAll(elementCode)); }; const saveOriginalData = function () { return new Promise((resolve, reject) =\u0026gt; { try { var els = document.querySelectorAll(elementCode), count = els.length; els.forEach((element) =\u0026gt; { element.setAttribute(\u0026#34;data-original-code\u0026#34;, element.innerHTML); count--; if (count == 0) { resolve(); } }); } catch (error) { reject(error); } }); }; const resetProcessed = function () { return new Promise((resolve, reject) =\u0026gt; { try { var els = document.querySelectorAll(elementCode), count = els.length; els.forEach((element) =\u0026gt; { if (element.getAttribute(\u0026#34;data-original-code\u0026#34;) != null) { element.removeAttribute(\u0026#34;data-processed\u0026#34;); element.innerHTML = element.getAttribute(\u0026#34;data-original-code\u0026#34;); } count--; if (count == 0) { resolve(); } }); } catch (error) { reject(error); } }); }; saveOriginalData().catch(console.error); // 不要用 localStorage.getItem(\u0026#34;pref-theme\u0026#34;)，因为有些时候会为 null let isdark = document.body.className.includes(\u0026#34;dark\u0026#34;); if (isdark) { resetProcessed().then(loadMermaid(\u0026#34;dark\u0026#34;)).catch(console.error); } else { resetProcessed().then(loadMermaid(\u0026#34;neutral\u0026#34;)).catch(console.error); } document.getElementById(\u0026#34;theme-toggle\u0026#34;).addEventListener(\u0026#34;click\u0026#34;, () =\u0026gt; { resetProcessed(); document.body.className.includes(\u0026#34;dark\u0026#34;) ? loadMermaid(\u0026#34;neutral\u0026#34;) : loadMermaid(\u0026#34;dark\u0026#34;).catch(console.error); }); \u0026lt;/script\u0026gt; {{ end }} 最后再在 layouts/_default/single.html 中加入引用 mermaid.html 的部分，注意，single.html 你如果一开始没有，需要先去主题 themes/PaperMod/layouts/_default/single.html 那里拷贝原来的 single.html 到上面这个地址\nhtml 1 2 3 4 \u0026lt;article\u0026gt; \u0026lt;!-- 省略上面的 --\u0026gt; {{- partial \u0026#34;mermaid.html\u0026#34; . }} \u0026lt;/article\u0026gt; 代码折叠 博主原实现\n在 layouts/shortcodes/collapse.html 中加入以下内容：\ngo 1 2 3 4 5 6 7 8 {{ if .Get \u0026#34;summary\u0026#34; }} {{ else }} {{ warnf \u0026#34;missing value for param \u0026#39;summary\u0026#39;: %s\u0026#34; .Position }} {{ end }} \u0026lt;p\u0026gt;\u0026lt;details {{ if (eq (.Get \u0026#34;openByDefault\u0026#34;) true) }} open=true {{ end }}\u0026gt; \u0026lt;summary markdown=\u0026#34;span\u0026#34;\u0026gt;{{ .Get \u0026#34;summary\u0026#34; | markdownify }}\u0026lt;/summary\u0026gt; {{ .Inner | markdownify }} \u0026lt;/details\u0026gt;\u0026lt;/p\u0026gt; 这个我看了一下源码，需要手动在要折叠的块上写\\{\\{\u0026lt; collapse \u0026gt;\\}\\}\\{\\{\u0026lt; /collapse \u0026gt;\\}\\} 我加了反斜杠 因为我的 papermod 里有这个 short code，不记得是我加错地方了还是什么情况\n不行，感觉太不优雅，所以我的方法是下面的\n打开/新建layouts/_markup/render-codeblock.html： 不是 layouts/_default/_markup hugo 文档，Code block render hooks\n写入以下内容\ngo 1 2 3 4 5 6 7 8 9 10 11 12 13 {{/* 计算行数 */}} {{ $lines := (len (split .Inner \u0026#34;\\n\u0026#34;)) }} {{ $autoThreshold := 15 }} {{/* 超过多少行自动折叠，自行调整 */}} {{ $needCollapse := ge $lines $autoThreshold }} {{ if $needCollapse }} \u0026lt;details\u0026gt; \u0026lt;summary\u0026gt;显示已折叠代码（{{ $lines }} 行）\u0026lt;/summary\u0026gt; {{ end }} {{ $r := transform.HighlightCodeBlock . }} {{ $r.Wrapped }} {{ if $needCollapse }} \u0026lt;/details\u0026gt; {{ end }} 这样子超过多少行就会折叠了，也懒得我去设置\n盘古之白 由于很早换成了微信输入法，中英文或中数字之间会自动空格，美观不少，这次在这里了解到这个空格也被称为「盘古之白」，像是劈开了这几者之间的混沌，很有意思，但是没有装修进来\n侧面目录 这个博主的在我这一直不生效 最终是把 assets/css/extended/toc.css写入以下内容生效的，虽然似乎丑了点。但是现在能用就行\n[css] 显示已折叠代码（202 行） 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 :root { --article-width: 720px; --toc-width: 230px; /* adjust as needed */ --toc-gap: 4.5rem; /* safe gap between TOC and article */ } /* Base TOC box appearance */ .toc { margin: 0 2px 40px 2px; border: 1px solid var(--border); background: var(--entry); border-radius: var(--radius); padding: 0.4em; } /* Fully detached left sidebar TOC on desktop */ @media (min-width: 1100px) { .toc { position: fixed; /* fully detach from article flow */ top: 6.5rem; /* below header; tweak to your navbar height */ left: max(16px, calc(50% - (var(--article-width) / 2) - var(--toc-width) - var(--toc-gap))); transform: translateX(-4px); width: var(--toc-width); max-height: calc(100vh - 8rem); /* keep a bit of breathing room */ overflow: auto; z-index: 3; margin: 0; /* no flow margins when fixed */ box-shadow: 0 4px 14px rgba(0,0,0,.18); backdrop-filter: saturate(140%) blur(6px); } .toc .inner a { text-decoration: none; opacity: .9; transition: opacity .15s ease, color .15s ease; } .toc .inner a:hover { opacity: 1; color: var(--secondary); } .toc ul { list-style: none; padding-left: .2rem; } .toc li { position: relative; padding-left: .75rem; } .toc li::before { content: \u0026#34;\u0026#34;; position: absolute; left: 0; top: .65em; width: .35rem; height: .35rem; border-radius: 50%; background: var(--tertiary); opacity: .6; } } .toc details summary { cursor: zoom-in; margin-inline-start: 20px; padding: 12px 0; } .toc details[open] summary { font-weight: 500; } .toc .inner { margin: 0 0 0 20px; padding: 0 15px 15px 20px; font-size: 16px; max-height: 83vh; overflow-y: auto; } .toc .inner::-webkit-scrollbar-thumb { background: var(--border); border: 7px solid var(--theme); border-radius: var(--radius); } .toc ul { list-style-type: circle; } .toc li ul { margin-inline-start: calc(var(--gap) * 0.5); list-style-type: none; } .toc li { list-style: none; font-size: 0.95rem; padding-bottom: 5px; } .toc .active { font-size: 110%; font-weight: 600; color: #614a85; text-decoration: underline; } .toc li a:hover { color: var(--secondary); } /* Optional: ensure a tiny buffer between page edge and left TOC */ @media (min-width: 1400px) { .page, .main { padding-left: max(var(--gap), 0px); } } @media (max-width: 1099px) { .toc { position: relative; left: auto; top: auto; max-height: none; box-shadow: none; backdrop-filter: none; } } @media (min-width: 1100px) and (max-width: 1360px) { .toc { left: max(12px, calc(50% - (var(--article-width) / 2) - var(--toc-width) - (var(--toc-gap) + 5rem))); } } 参考文章 感谢前辈大佬的文章。\n台运鹏. (Dec. 17, 2024). 《Hugo PaperMod 主题精装修》[Blog post]. Retrieved from http://yunpengtai.top/posts/hugo-journey/\n","permalink":"/posts/%E5%8D%9A%E5%AE%A2%E7%B2%BE%E8%A3%85%E4%BF%AE/","summary":"\u003ch2 id=\"新logo\"\u003e新logo\u003c/h2\u003e\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/08/093b8176c43b030ee99a1c5bbe3ac35a.jpg\"\u003e\n        \u003cimg alt=\"raylogo32*32.jpg\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/08/093b8176c43b030ee99a1c5bbe3ac35a.jpg\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e叫 gpt 给我画了个 logo，元素就是姓名首字母啦，还是蛮喜欢的\u003c/p\u003e\n\u003cp\u003e本次博客换了个主题和评论系统，参考文章放在末尾了。算是找到比较靠谱的几篇文章。由于我使用较新的版本，所以并不是能很好的复现文章中的效果，不过也足够满意了\u003c/p\u003e\n\u003ch2 id=\"装修点\"\u003e装修点\u003c/h2\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e原先使用hugo-theme-stack 主题是由于接触 hugo 的教程中使用的就是这个，为了避免不必要的麻烦，初次基本都是按部就班复刻教程为主，这次心血来潮还是想换一个，挑了挑就觉得 papermod 不错，简单不复杂，不是很爱花哨的，但是也不想博客太丑了点\u003c/p\u003e","title":"博客精装修"},{"content":"PostgreSQL: Documentation: 17: 32.1. Database Connection Control Functions\n使用 pg mcp 连接数据库时遇到的问题\n贴出的命令中密码改了\n[bash] 显示已折叠代码（16 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 (MCBot_py311) yangr@172-16-99-32-Dev:/data/yangr/gitRepos/LG_rag_hw$ npx -y @modelcontextprotocol/server-postgres \u0026#34;postgresql://lmi:abc#123@172.16.99.32:5432/lmi\u0026#34; node:internal/url:818 href = bindingUrl.parse(input, base, true); ^ TypeError: Invalid URL at new URL (node:internal/url:818:25) at file:///home/yangr/.npm/_npx/cd1ce99963b5e8b1/node_modules/@modelcontextprotocol/server-postgres/dist/index.js:21:25 at ModuleJob.run (node:internal/modules/esm/module_job:271:25) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:547:26) at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:116:5) { code: \u0026#39;ERR_INVALID_URL\u0026#39;, input: \u0026#39;postgresql://lmi:abc#123@172.16.99.32:5432/lmi\u0026#39; } Node.js v22.12.0 问了 G 老师，遇到 TypeError: Invalid URL 错误，通常是因为你的连接 URI 中包含了 未经转义 的 “#” 字符——这在 URL 语法中会被误解析为fragment 分隔符，导致 Node.js 的 URL 构造器无效 。\n✅ 解决方案：将“#”字符替换为 %23\n（百分号转义）\n参照 Postgres 官方文档，URI 中的特殊字符必须使用 percent‑encoding 转义 ：\n原符号 转义后 说明 # %23 片段起始符 @ %40 用户/密码分隔符 : %3A host 与 port 分隔符 / %2F 分隔数据库名 ? %3F 查询参数分隔符 例如，如果密码是 pass@word:123，则应写成 pass%40word%3A123。\n可以在 Node.js 或 bash 中使用如下代码快速生成 percent‑encoding 版本：\nbash 1 2 \u0026gt; encodeURIComponent(\u0026#39;abc#12345\u0026#39;) \u0026#34; abc%12345 \u0026#34; 使用环境变量或者还有一些配置文件的方法可以避免这个问题 ","permalink":"/posts/pg-connect-%E5%AD%97%E7%AC%A6%E4%B8%B2%E9%94%99%E8%AF%AF/","summary":"\u003cp\u003e\u003ca href=\"https://www.postgresql.org/docs/current/libpq-connect.html\"\u003ePostgreSQL: Documentation: 17: 32.1. Database Connection Control Functions\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e使用 pg mcp 连接数据库时遇到的问题\u003c/p\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e贴出的命令中密码改了\u003c/p\u003e\n\n  \u003c/blockquote\u003e\n\n \n\n\n\n\n\u003cdetails class=\"code-details\"\u003e\n  \u003csummary\u003e\n      \u003cspan class=\"code-summary-left\"\u003e\n          \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n          \u003cspan\u003e[bash] 显示已折叠代码（16 行）\u003c/span\u003e\n      \u003c/span\u003e\n  \u003c/summary\u003e\n  \u003cdiv class=\"collapsible-content\"\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003eMCBot_py311\u003cspan class=\"o\"\u003e)\u003c/span\u003e yangr@172-16-99-32-Dev:/data/yangr/gitRepos/LG_rag_hw$ npx -y @modelcontextprotocol/server-postgres \u003cspan class=\"s2\"\u003e\u0026#34;postgresql://lmi:abc#123@172.16.99.32:5432/lmi\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enode:internal/url:818\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nv\"\u003ehref\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e bindingUrl.parse\u003cspan class=\"o\"\u003e(\u003c/span\u003einput, base, \u003cspan class=\"nb\"\u003etrue\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                        ^\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eTypeError: Invalid URL\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    at new URL \u003cspan class=\"o\"\u003e(\u003c/span\u003enode:internal/url:818:25\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    at file:///home/yangr/.npm/_npx/cd1ce99963b5e8b1/node_modules/@modelcontextprotocol/server-postgres/dist/index.js:21:25\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    at ModuleJob.run \u003cspan class=\"o\"\u003e(\u003c/span\u003enode:internal/modules/esm/module_job:271:25\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    at async onImport.tracePromise.__proto__ \u003cspan class=\"o\"\u003e(\u003c/span\u003enode:internal/modules/esm/loader:547:26\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    at async asyncRunEntryPointWithESMLoader \u003cspan class=\"o\"\u003e(\u003c/span\u003enode:internal/modules/run_main:116:5\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  code: \u003cspan class=\"s1\"\u003e\u0026#39;ERR_INVALID_URL\u0026#39;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  input: \u003cspan class=\"s1\"\u003e\u0026#39;postgresql://lmi:abc#123@172.16.99.32:5432/lmi\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eNode.js v22.12.0\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cp\u003e问了 G 老师，遇到 TypeError: Invalid URL 错误，通常是因为你的连接 URI 中包含了 \u003cstrong\u003e未经转义\u003c/strong\u003e 的 “#” 字符——这在 URL 语法中会被误解析为\u003cstrong\u003efragment 分隔符\u003c/strong\u003e，导致 Node.js 的 URL 构造器无效  。\u003c/p\u003e","title":"PG connect 字符串错误"},{"content":" 写在前面 在公司偶尔会同时维护三四个甚至五六个项目。稀烂的项目管理导致每次快速更新迭代时部署成了重复性超高且烦人的事情\n部署流程 由于原先开发规范不够，git 使用不足，所以暂时一直拖着。现在对项目管理严抓想起来这件事情。 Javaer 一定见过下面这个 Jenkins 。这是个Java 编写的开源的持续集成工具 持续集成是啥意思，CI/CD 啥意思\n解释：CI/CD或CICD通常指的是持续集成（英语：continuous integration）和持续交付（英语：continuous delivery）或持续部署（英语：continuous deployment）的组合实践 from wiki\n这就能懂啦，我对前端不是很明白但是现在我们的开发流程一般是\n1 前端同学开发完毕-\u0026gt;build-\u0026gt;发 dist 给管服务器的同学-\u0026gt;（往往是我），我把 dist 放在对应的 nginx 目录下 如果他不发给我的情况。我就需要\n1 git 拉取源码-\u0026gt; npm 更新依赖 -\u0026gt; build -\u0026gt; mv to nginx/html 那么这里很明显是可以通过 github/gitlab 的通知机制实现自动化部署的\nWebhooks 凑巧找到一篇文章完美作为教程 【CI/CD】写给前端的自动化部署攻略序 ​ 对于前端开发而言，静态页面的部署是容易的。只需要将打包好的页面，放入已经配 - 掘金\n简而言之，git 仓库会提供 webhooks 功能。就是如果你对仓库做了某些操作，git 将通过 http 向你提供的地址发送操作信息（json）\n这里这位佬用的 gitlab 做的例子。我们自部署的也是 gitlab，太好了\n这是新建 webhooks 的页面 我们暂时不用 release ，所以就以 tag push event 作为发布的信号\n如果使用内网地址 还需要配置 gitlab 的 outbound setting 允许 local network\n这里我是配置了这样的地址。 这位佬用的 Next.js 我就用 fastapi 实现这个服务了\n这里就很简单了。描述了一下需求 vibe-coding 建立了这个服务\n最后实验一下，可以看到记录\n非常完美\n","permalink":"/posts/%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2%E5%89%8D%E7%AB%AF%E9%A1%B9%E7%9B%AE/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e写在前面\n在公司偶尔会同时维护三四个甚至五六个项目。稀烂的项目管理导致每次快速更新迭代时部署成了重复性超高且烦人的事情\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"部署流程\"\u003e部署流程\u003c/h2\u003e\n\u003cp\u003e由于原先开发规范不够，git 使用不足，所以暂时一直拖着。现在对项目管理严抓想起来这件事情。\nJavaer 一定见过下面这个\n\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/05/863526d86d4c01e045a7ac9765386a8c.png\"\u003e\n        \u003cimg alt=\"682ee5d6a1773\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/05/863526d86d4c01e045a7ac9765386a8c.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\nJenkins 。这是个Java 编写的开源的持续集成工具\n持续集成是啥意思，CI/CD 啥意思\u003c/p\u003e","title":"自动部署前端"},{"content":" 克隆自己\nGitHub - xming521/WeClone: 🚀从聊天记录创造数字分身的一站式解决方案💡 使用聊天记录微调大语言模型，让大模型有“那味儿”，并绑定到聊天机器人，实现自己的数字分身。 数字克隆/数字分身/数字永生/LLM/聊天机器人/LoRA\n准备环境咯 然后先下载模型，但是我有了所以先准备数据\n1. 数据准备 手机备份数据上电脑 我勒个豆，记得需要从手机上操作聊天记录迁移到电脑。电脑微信的备份聊天记录是加密仅供未来还原到手机的，吭哧吭哧传了很久。发现不是我要的效果\n然后使用GitHub - xaoyaoo/PyWxDump导出数据csv\n2. 洗数据 这里用的 7B 模型 vllm 推理进行打分，我都没看导出来一些啥数据。。洗完再看看 开始训练 这里想用多卡来着。但是我的 23 卡和 01 卡链接似乎有问题，干脆单卡跑算了\n训练的时候看了眼数据。我的聊天又小又短，感觉数据集不是很有用\n结果 果然。这样默认跑出来一个智障，基本上只会回几个字，倒是很符合我的回复习惯\n后面整理个干净点的数据再训练一个\n","permalink":"/posts/weclone-%E8%B7%9F%E9%A3%8E/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e克隆自己\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e\u003ca href=\"https://github.com/xming521/WeClone\"\u003eGitHub - xming521/WeClone: 🚀从聊天记录创造数字分身的一站式解决方案💡 使用聊天记录微调大语言模型，让大模型有“那味儿”，并绑定到聊天机器人，实现自己的数字分身。 数字克隆/数字分身/数字永生/LLM/聊天机器人/LoRA\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e准备环境咯\n然后先下载模型，但是我有了所以先准备数据\u003c/p\u003e","title":"weclone 跟风"},{"content":" 直接一幅图说明白 Harbor 干嘛用的\n下载安装 确保服务器有 docker 和 docker compose 两个链接分别是 官方 docs 和 github Release 页面。我下载 offline 版本的。会带上镜像\nHarbor docs | Harbor Installation and Configuration Release v2.12.3 · goharbor/harbor · GitHub\n英语水平有限，装的我头昏脑胀，整体还是简单的\n配置 tar -xzvf 安装包.tgz\n解压后目录结构如上。 其中 docker-compose.yml，common目录。是脚本后续生成的，nginx 目录是我创建的。大概就是这样\n复制 harbor.yml.tmpl -\u0026gt; harbor.yml\n主要配置点就在于\nhostname http-\u0026gt;port https external_url 如下图所示 因为我是需要另外一台服务器的 nginx 做反代，但是刚开始部署我想简单测试一下（就是这个不灵清的想法折腾我一个多小时）\n我以为 hostname 在配置了external_url 后会失效。于是还是内网地址 172.16.100.1 然后用 openssl 自签名了证书。然后配了 external_url 接着在我的客户端电脑上 配置了/etc/hosts 172.16.100.1 hub.test.cn\n结果就是能访问，但是到下一步。我清掉本地配置。去配置公网服务器的时候\n死活打不通，要么就是页面能到，要么就是 无法登录。自己琢磨半天越想越绕，（这里真的基础功力不够深厚，来个老法师估计十分钟搞完了）\n最后参考了别人的文章 在 HTTPS 的反向代理 Nginx 后运行 HTTP Harbor | 星辰狂澜\u0026rsquo;s Blog 私有镜像仓库 Harbor 安装和使用 - 冯威的博客\n结论配置就是这样 另外反代 nginx 的配置如下\n[nginx] 显示已折叠代码（45 行） 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 server { server_name hub.test.cn; client_max_body_size 2000M; gzip on; location / { proxy_pass http://172.16.100.1:7880; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026#34;upgrate\u0026#34;; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header referer $http_referer; proxy_set_header X-Forwarded-Proto $scheme; } location /v2/ { proxy_pass http://172.16.100.1:7880/v2/; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026#34;upgrate\u0026#34;; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header referer $http_referer; proxy_set_header X-Forwarded-Proto $scheme; } location /service/ { proxy_pass http://172.16.100.1:7880/service/; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026#34;upgrate\u0026#34;; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header referer $http_referer; proxy_set_header X-Forwarded-Proto $scheme; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } 这样终于能走通了\ndocker login 另外两个点记录一下，有关docker login\n这里是问的 chatgpt。我是 mac 系统，存储少得可怜。所以我用的是 Colima+docker 配置 docker 的方式不太一样\n这里没有当时的截图了，总之就是 docker login 172.16.99.32:7880 失败 然后配置如下即可\n第一步：关闭当前的 Colima 实例 1 colima stop 第二步：编辑 Colima 的配置文件（添加insecure-registries） 运行以下命令编辑配置：\n1 colima start --edit 此命令会打开一个编辑器（通常是 vim），在其中添加或修改以下配置：\n1 2 3 4 # 示例配置 docker: insecureRegistries: - 172.16.100.1:7880 ⚠️ 注意缩进必须是空格，不要使用 tab。\n第四步：验证配置是否生效 你可以运行以下命令确认 registry 已被识别为不安全仓库：\n1 docker info | grep -A 5 \u0026#34;Insecure Registries\u0026#34; 应该能看到你配置的 IP 和端口，比如：\n1 2 3 Insecure Registries: 172.16.100.1:7880 127.0.0.0/8 走域名，外网的时候，要注意external_url 的配置。如果你的访问带端口，那这里的配置也要带端口。我是没有配置的，所以这里正常写就通了\n最后测试一下 新建一个项目 test1\ndocker login hub.test.cn\n输入用户名密码，成功可以看到如下\ntag 我的本地nginx 镜像试试\ndocker tag nginx:latest hub.test.cn/test1/nginx:latest\n然后\ndocker push hub.test.cn/test1/nginx:latest\nok 完成。后续就可以使用它来管理用户 分发镜像，配置 webhook 实现 CI/CD等\n","permalink":"/posts/harbor-%E9%83%A8%E7%BD%B2%E8%AE%B0%E5%BD%95/","summary":"\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/05/0baf05570b0301265a092637e6003fba.png\"\u003e\n        \u003cimg alt=\"Harbor 镜像仓库架构图\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/05/0baf05570b0301265a092637e6003fba.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e\n\u003cp\u003e直接一幅图说明白 Harbor 干嘛用的\u003c/p\u003e\n\u003ch3 id=\"下载安装\"\u003e下载安装\u003c/h3\u003e\n\u003cp\u003e确保服务器有 docker 和 docker compose\n两个链接分别是 官方 docs 和 github Release 页面。我下载 offline 版本的。会带上镜像\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://goharbor.io/docs/2.13.0/install-config/\"\u003eHarbor docs | Harbor Installation and Configuration\u003c/a\u003e\n\u003ca href=\"https://github.com/goharbor/harbor/releases/tag/v2.12.3\"\u003eRelease v2.12.3 · goharbor/harbor · GitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e英语水平有限，装的我头昏脑胀，整体还是简单的\u003c/p\u003e\n\u003ch3 id=\"配置\"\u003e配置\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003etar -xzvf 安装包.tgz\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan class=\"post-img-view\"\u003e\n    \u003ca data-fancybox=\"gallery\" href=\"https://imgbed.anluoying.com/2025/05/f6846010e8d77b567239ba5cfce6b0d4.png\"\u003e\n        \u003cimg alt=\"Harbor 解压后的目录结构\" decoding=\"async\" loading=\"lazy\" src=\"https://imgbed.anluoying.com/2025/05/f6846010e8d77b567239ba5cfce6b0d4.png\"\u003e\n    \u003c/a\u003e\n\u003c/span\u003e\n\u003c/p\u003e","title":"Harbor 部署记录"},{"content":"使用 Git 的 amend 功能，将未保存的文件修改后“补充”到你刚刚的那次提交中，而不会增加新的提交记录。\n✅ 1. 保存你漏掉的那个文件 先确保你已经保存了那个文件的最新修改。\n✅ 2. 添加这个文件到暂存区 1 git add path/to/your/file ✅ 3. 使用 \u0026ndash;amend 修改上一次提交 1 git commit --amend --no-edit \u0026ndash;no-edit 表示不更改上一次的提交说明。\n如果你想修改提交说明，可以去掉 \u0026ndash;no-edit，然后在打开的编辑器里改。\n✅ 4. 如果已经推送到远程仓库（GitHub 等） 如果你 已经推送过 这次提交，需要强推（⚠️ 要小心）：\n1 git push --force ","permalink":"/posts/git%E6%BC%8F%E6%8F%90%E4%BA%A4%E4%BA%86%E6%80%8E%E4%B9%88%E5%8A%9E/","summary":"\u003cp\u003e使用 Git 的 \u003cstrong\u003eamend\u003c/strong\u003e 功能，将未保存的文件修改后“补充”到你刚刚的那次提交中，而不会增加新的提交记录。\u003c/p\u003e\n\u003ch3 id=\"-1-保存你漏掉的那个文件\"\u003e\u003cstrong\u003e✅ 1. 保存你漏掉的那个文件\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e先确保你已经保存了那个文件的最新修改。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"-2-添加这个文件到暂存区\"\u003e\u003cstrong\u003e✅ 2. 添加这个文件到暂存区\u003c/strong\u003e\u003c/h3\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit add path/to/your/file\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003chr\u003e\n\u003ch3 id=\"-3-使用\"\u003e\u003cstrong\u003e✅ 3. 使用\u003c/strong\u003e \u003c/h3\u003e\n\u003ch3 id=\"heading\"\u003e\u003cstrong\u003e\u0026ndash;amend\u003c/strong\u003e\u003c/h3\u003e\n\u003ch3 id=\"修改上一次提交\"\u003e \u003cstrong\u003e修改上一次提交\u003c/strong\u003e\u003c/h3\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit commit --amend --no-edit\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u0026ndash;no-edit 表示不更改上一次的提交说明。\u003c/p\u003e","title":"git漏提交了怎么办"},{"content":" 其实有想过保留两个系统，但是备份完之后觉得没这个必要了，主要是不需要 windows 做什么，公司也还有 win 电脑\n准备工作 64GB U 盘，（狗东 26 块的闪迪，有活动 18，够够的了，想当年买个 8G 还要我 72 块钱） \u0004Ventoy 一个（制作可启动U盘的开源工具。1 是微 pe 不能直接装 linux，2 是其他pe 系统我总感觉乱乱脏脏的，这个直接把系统镜像拖进去就可以了。很完美） Ubuntu 镜像，官网下就可 Ventoy安装 我在 mac 上不知道为什么启动不了 arm64 的 ventoy 工具，上 win 直接解决了。傻瓜式操作，不用一分钟，建议看官方文档 News . Ventoy\n镜像 Download Ubuntu Desktop | Ubuntu\n备份老电脑 安装 Ubuntu 插入 U 盘 进 bios，我不知道这个主板用的啥 直接常用按键一把搂 使用 u 盘启动，这个bios 略有区别，可以自行搜索相关资料 很开心的看到了（这里用的官方截图，选择 Ubuntu 系统） 这里我的界面和这个略有差别，但是基本一致。选择了try Ubuntu or install Ubuntu 结果就卡在了一个 LOGO 界面了 郁闷，但是机智的我猜到了是因为显卡驱动的问题，这机子上还有一张 3060，显示屏连着显卡，百度一下\n解决安装Ubuntu N卡冲突 # 安装Ubuntu16.04卡在Ubuntu的logo界面解决方法\n在 GRUB 界面按 e 我这里把 \u0026mdash; quite splash 改成了 quite splash nomodeset F10 保存进入系统。 OK 了。然后安装系统 但是我没有出现后续的问题。安装完毕后直接重启系统可以直接进入\n那就贴一下上面命令的解释。这个 nomodeset 是内核启动时传递的设置\nquiet：禁止大多数启动信息的输出（减少屏幕刷文字）。 splash：显示启动动画（比如 Ubuntu 的 logo）。 $vt_handoff：这个是 Ubuntu 系统中用来控制图形界面接管虚拟终端的参数变量，确保从内核启动到图形界面平滑过渡。 nomodeset 是一种 禁用内核模式设定（KMS, Kernel Mode Setting） 的方式。 nomodeset 设置防止启动时加载显卡驱动并切换分辨率 到此为止系统就 ok 啦。总时长不到 15 分钟。等待安装 10 分钟。\n","permalink":"/posts/%E5%AE%89%E8%A3%85-ubuntu/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e其实有想过保留两个系统，但是备份完之后觉得没这个必要了，主要是不需要 windows 做什么，公司也还有 win 电脑\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"准备工作\"\u003e准备工作\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e64GB U 盘，（狗东 26 块的闪迪，有活动 18，够够的了，想当年买个 8G 还要我 72 块钱）\u003c/li\u003e\n\u003cli\u003e\u0004Ventoy 一个（制作可启动U盘的开源工具。1 是微 pe 不能直接装 linux，2 是其他pe 系统我总感觉乱乱脏脏的，这个直接把系统镜像拖进去就可以了。很完美）\u003c/li\u003e\n\u003cli\u003eUbuntu 镜像，官网下就可\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"ventoy安装\"\u003eVentoy安装\u003c/h2\u003e\n\u003cp\u003e我在 mac 上不知道为什么启动不了 arm64 的 ventoy 工具，上 win 直接解决了。傻瓜式操作，不用一分钟，建议看官方文档\n\u003ca href=\"https://www.ventoy.net/cn/doc_news.html\"\u003eNews . Ventoy\u003c/a\u003e\u003c/p\u003e","title":"安装 Ubuntu"},{"content":" 前几天给同事分享文档的时候一直在用 语雀啥的。突然想到，如果要维护一个像 vue 或者vite 那一类的文档给用户看该怎么快速搭建的。理应有前辈完成这种工作。让 grok 搜了一下。顺便解惑原来对于 wiki doc 等的认识，有兴趣的小伙伴们自己搜搜，我这里就不贴 grok 的长篇大论了\n总之找到了 vite press。确实是曾经认识过它（好像是在鱼皮那里）\n那么这篇博客就记录一下搭建过程（跟官方文档一致）Getting Started | VitePress\n首先准备 node 环境。因为最近在开发 mcp 应用。所以心血来潮用了 bun\n1 2 3 bun add -D vitepress bun vitepress init 随后会有一次互动式的初始化\n显示已折叠代码（30 行） 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 ┌ Welcome to VitePress! │ ◇ Where should VitePress initialize the config? │ ./docs │ ◇ Where should VitePress look for your markdown files? │ ./docs │ ◇ Site title: │ My Awesome Project │ ◇ Site description: │ A VitePress Site │ ◇ Theme: │ Default Theme │ ◇ Use TypeScript for config and theme files? │ Yes │ ◇ Add VitePress npm scripts to package.json? │ Yes │ ◇ Add a prefix for VitePress npm scripts? │ Yes │ ◇ Prefix for VitePress npm scripts: │ docs │ └ Done! Now run pnpm run docs:dev and start writing. 这里对于初始化的目录结构会对后面有一定的影响。由于我是使用的./作为默认目录，也可以在已有的项目中构建 docs 目录跟项目结合在一起\n接下来就可以\n1 bun run docs:dev 随后在页面中打开就能看到美丽的文档界面了，怪像一回事儿的，以下截图我已经略微修改过了，跟着官方文档改 index.md 文件就能看到对首页的改动了。\n部署 github pages 要给大家访问，且是静态页面，那就当然嫖一嫖这些服务啦，这次就用 github 的 pages。公司的 gitlab 也部署了，vitepress 文档里都有教程 Deploy Your VitePress Site | VitePress\n只需要准备deploy.yml 放入 .github/workflows\n由于我本身这个仓库存放的就是这个项目。且初始化的时候使用的不是./docs 而是 ./\n所以配置文件有一点不同的位置在于这里 upload artifact 的 path 是我 vite config 里的位置\n1 2 3 4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: /dist vite config.mts\n1 2 3 outDir: \u0026#39;./dist\u0026#39;, base: \u0026#39;/ray-doc/\u0026#39; base 最早是使用/ 但是当我部署后出现了各种资源 404 的情况，发现要改成和仓库名相同才能访问。\ngithub 的仓库中要设置 pages，这里我跟踪 build from main 分支的 root 路径 根据需求调整\n随后推送之后就能等待流程完成。最后访问给到的地址就能看到页面咯 Ray-Doc\n","permalink":"/posts/vite-press%E9%A3%9F%E7%94%A8%E6%8C%87%E5%8D%97/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e前几天给同事分享文档的时候一直在用 语雀啥的。突然想到，如果要维护一个像 vue 或者vite 那一类的文档给用户看该怎么快速搭建的。理应有前辈完成这种工作。让 grok 搜了一下。顺便解惑原来对于 wiki doc 等的认识，有兴趣的小伙伴们自己搜搜，我这里就不贴 grok 的长篇大论了\u003c/p\u003e","title":"vite-press 食用指南"},{"content":"我现在在后端项目上 使用自签证书 对域名 172.16.99.32:1043 生成了证书和密钥。并在 nginx 中启动服务使用这个\n我使用 curl 加 -K 这个参数 可以成功调用 https 的接口\n但是我的前端页面服务直接使用 python http.server 启动访问的时候 无法调用这个接口。报错net::ERR_CERT_AUTHORITY_INVALID\n我该怎么解决呢。因为我只是为了 加密通信，且没有域名 。是不是可以在两端互相认证证书之类的呢？\n解决方案概述 by-grok3 理解问题 您的问题是前端页面（通过 Python 的 http.server 提供服务）无法调用使用自签名证书的 HTTPS 接口（172.16.99.32:1043），而使用 curl -k 可以成功调用。错误为 net::ERR_CERT_AUTHORITY_INVALID，因为浏览器默认不信任自签名证书。由于您是为了开发环境中的加密通信，且没有域名，我们需要让浏览器信任该证书。\n推荐解决方案：使用 mkcert 使用 mkcert 是最方便的方法，它会生成本地信任的证书，浏览器会自动识别。步骤如下：\n安装 mkcert（在 macOS/Linux 上用 brew install mkcert，Windows 上从 GitHub 页面 下载）。 运行 mkcert -install 安装本地 CA。 为您的 IP 生成证书：运行 mkcert 172.16.99.32。 在 nginx 中使用新生成的证书和密钥，更新配置并重启 nginx。 完成后，浏览器应该会信任该证书，前端可以正常访问 HTTPS 接口。\n替代方案：手动信任证书 如果不使用 mkcert，可以手动将现有自签名证书添加到浏览器的信任存储：\n导出证书文件（例如 server.crt）。 根据操作系统添加至信任存储： Windows：使用 certmgr.msc 导入到“受信任的根证书颁发机构”。 macOS：通过 Keychain Access 导入并设置为“始终信任”。 Linux：添加到系统 CA 存储（如 Debian 的 /etc/ssl/certs/）。 重启浏览器后测试。 这个方法适合单机使用，但如果有多台机器或浏览器，操作会比较繁琐。\n关于相互认证 您提到是否可以在两端相互认证证书。相互 TLS（mTLS）需要客户端和服务器都提供证书，但在开发环境中，浏览器通常不支持为任意网站提供客户端证书。由于您只需加密通信，信任服务器的证书已足够，mTLS 在此场景下可能不必要。\n详细报告 背景与问题分析 您当前在后端项目中使用自签名证书为域名 172.16.99.32:1043 生成证书和密钥，并通过 nginx 提供 HTTPS 服务。使用 curl -k 可以成功调用接口，但前端页面（通过 Python 的 http.server 提供服务）访问时，浏览器报错 net::ERR_CERT_AUTHORITY_INVALID。这是因为自签名证书未由受信任的证书颁发机构（CA）签发，浏览器默认不信任。\ncurl -k 选项会跳过证书验证，因此能正常工作，但这在生产环境中不安全。对于浏览器，特别是在开发环境中，您希望加密通信且没有域名，解决方案需要让浏览器信任该证书。\n解决方案详解 推荐方法：使用 mkcert 生成本地信任证书 mkcert 是一个专门为开发环境设计的工具，可以创建浏览器自动信任的证书。它通过安装一个本地 CA 并为指定主机（包括 IP 地址）生成证书来实现。\n实施步骤 安装 mkcert：\nmacOS/Linux：运行 brew install mkcert（需安装 Homebrew），或从 mkcert GitHub 页面 获取安装说明。 Windows：从 GitHub 发布页面 下载二进制文件，并添加到系统 PATH。 安装本地 CA：\n运行 mkcert -install，这会将 mkcert 的根证书添加到系统信任的 CA 列表中，浏览器会自动识别。 为 IP 生成证书：\n运行 mkcert 172.16.99.32，这会生成两个文件，例如 172.16.99.32.pem（证书）和 172.16.99.32-key.pem（密钥）。 更新 nginx 配置：\n在 nginx 配置文件中，替换现有的证书和密钥路径为 mkcert 生成的文件。例如： 1 2 ssl_certificate /path/to/172.16.99.32.pem; ssl_certificate_key /path/to/172.16.99.32-key.pem; 保存配置并重启 nginx（sudo systemctl restart nginx 或相应命令）。 测试：\n访问前端页面，通过 JavaScript 或浏览器直接请求 https://172.16.99.32:1043，浏览器应不再报错。 优势 mkcert 跨平台支持（Windows、macOS、Linux），适合团队开发。 支持为 IP 地址生成证书，符合您的需求（mkcert GitHub 页面 明确支持 IP）。 自动处理信任问题，无需手动为每个浏览器配置。 注意事项 确保 nginx 配置正确，且端口（1043）在防火墙中开放。证书适用于主机名，不涉及端口，端口由服务器配置处理。\n替代方法：手动添加证书到信任存储 如果不使用 mkcert，可以手动将自签名证书添加到系统的信任证书存储中，但需要根据操作系统和浏览器进行配置。\n实施步骤 导出证书：\n找到现有自签名证书文件（例如 server.crt 或 server.pem）。如果没有，可以从 nginx 配置中提取。 添加至信任存储：\nWindows： 打开 certmgr.msc（通过运行输入）。 导航至“受信任的根证书颁发机构” \u0026gt; “证书”。 右键点击“所有任务” \u0026gt; “导入”，选择证书文件，按照向导完成。 macOS： 打开 Keychain Access（通过启动台搜索）。 转到“文件” \u0026gt; “导入项目”，选择证书文件。 双击导入的证书，进入“信任”选项卡，将“When using this certificate”设置为“Always Trust”。 Linux： 过程因发行版不同而异。例如，在 Debian/Ubuntu 上，将证书复制到 /etc/ssl/certs/，然后运行 update-ca-certificates。 重启浏览器：\n关闭并重新打开浏览器，确保新信任的证书生效。 测试：\n访问 https://172.16.99.32:1043，确认错误消失。 局限性 需要为每台机器和每个浏览器重复操作，适合单机开发，不适合团队。 配置过程可能因浏览器版本和操作系统不同而复杂。 关于相互认证（mTLS） 您提到是否可以在两端相互认证证书。这涉及到相互 TLS（mTLS），即客户端和服务器都提供证书进行身份验证。在生产环境中，这常用于内部网络以增强安全性。\n然而，在您的开发场景中：\n浏览器通常不支持为任意网站提供客户端证书，配置复杂。 您只需加密通信，信任服务器的证书已足够。 mTLS 在开发环境中可能不必要，且实施成本较高。 因此，建议专注于让浏览器信任服务器证书，而非实现 mTLS。\n额外考虑 CORS 问题 由于前端通过 Python 的 http.server 提供服务（通常为 HTTP，例如 http://localhost:8000），而后端为 HTTPS（https://172.16.99.32:1043），它们被视为不同来源（协议不同）。如果前端通过 JavaScript（如 fetch 或 XMLHttpRequest）请求后端，可能遇到 CORS 错误。\n解决方法：\n将前端也通过 HTTPS 提供服务（例如使用 mkcert 为前端生成证书）。 在后端（nginx）配置 CORS，允许来自前端的请求。例如，在 nginx 中添加： 1 2 3 add_header Access-Control-Allow-Origin http://localhost:8000; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; add_header Access-Control-Allow-Headers Authorization, Content-Type; 测试后确保请求正常。 IP 地址证书的特殊性 证书通常为域名签发，但也支持 IP 地址。浏览器会接受 IP 地址的证书，只要信任该证书。mkcert 明确支持 IP 地址生成证书（mkcert GitHub 页面），因此不会出现问题。\n总结 对于开发环境，推荐使用 mkcert 生成本地信任的证书，步骤简单且适合团队使用。如果不方便安装额外工具，可以手动将证书添加到信任存储，但操作较繁琐。关于相互认证，mTLS 在此场景下不必要，信任服务器证书已满足加密通信需求。\n关键引用 mkcert GitHub 页面详细安装和使用指南 创建浏览器信任的自签名 SSL 证书 将自签名证书添加到浏览器信任存储 ","permalink":"/posts/%E8%87%AA%E7%AD%BE%E8%AF%81%E4%B9%A6/","summary":"\u003cp\u003e我现在在后端项目上 使用自签证书  对域名 \u003ccode\u003e172.16.99.32:1043\u003c/code\u003e 生成了证书和密钥。并在 nginx 中启动服务使用这个\u003cbr\u003e\n我使用 curl 加 -K 这个参数 可以成功调用 https 的接口\u003cbr\u003e\n但是我的前端页面服务直接使用 python http.server 启动访问的时候 无法调用这个接口。报错net::ERR_CERT_AUTHORITY_INVALID\u003c/p\u003e","title":"自签证书"},{"content":"参考 把 Obsidian 变为 Hugo 博客的集成管理平台 | 胡说 参考这位佬的文章，一共三篇。主要以下几个点\n实现 关于我的博客是使用 github 管理，但是部署是在 cloudflare 上。这里原先的流程是这样的\n编辑器打开仓库 （可选）git pull hugo new post/xxx.md 编辑 md，往往是我在别的编辑器（Obsidian） copy 进来的 编辑完后 git commit 然后 git push 这样的流程最大的问题出现在 写博客的位置和发布时的割裂\n这也是我抓紧时间切换 md 编辑器到 Obsidian 的原因。插件万岁\n使用插件有以下\nAuto Link Title 粘贴链接自动获取标题 Charts 生图 Dataview 查看数据 Emoji Toolbar 挑选 emoji Git 进行 git 管理 Homepage 制作看板页 Image auto upload 结合 picgo 实现粘贴图片自动上传 QuickAdd 制作按钮 具体参考文章开头引用的 大佬的博客 有几个坑 倒是有热心网友在评论区提到了。看一下即可。\n","permalink":"/posts/obsidian+hugo/","summary":"\u003ch2 id=\"参考\"\u003e参考\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://blog.zhangyingwei.com/posts/2024m1d30h10m7s52/\"\u003e把 Obsidian 变为 Hugo 博客的集成管理平台 | 胡说\u003c/a\u003e\n参考这位佬的文章，一共三篇。主要以下几个点\u003c/p\u003e\n\u003ch2 id=\"实现\"\u003e实现\u003c/h2\u003e\n\u003cp\u003e关于我的博客是使用 github 管理，但是部署是在 cloudflare 上。这里原先的流程是这样的\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e编辑器打开仓库\u003c/li\u003e\n\u003cli\u003e（可选）git pull\u003c/li\u003e\n\u003cli\u003ehugo new post/xxx.md\u003c/li\u003e\n\u003cli\u003e编辑 md，往往是我在别的编辑器（Obsidian） copy 进来的\u003c/li\u003e\n\u003cli\u003e编辑完后 git commit 然后 git push\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这样的流程最大的问题出现在 写博客的位置和发布时的割裂\u003c/p\u003e","title":"obsidian+hugo"},{"content":"惯例先放网址，我这里参考了 好多，但是终究没有能抄的作业 hugo-theme-stack/config.yaml at master · CaiJimmy/hugo-theme-stack · GitHub 独立部署 | Waline waline/assets/waline.sqlite at main · walinejs/waline · GitHub 知乎 # 博客建站10 - 选择博客评论系统\n系统架构 网站示例： Ray Blog 服务器: Cloudflare pages + 一台 aws 白嫖的服务器独立部署 waline 服务器系统： Ubuntu 24.04 LTS 博客框架： The world\u0026rsquo;s fastest framework for building websites 网站主题： Stack | Card-style Hugo theme designed for bloggers 安装 waline 这里根据官方文档独立部署目录下的指引 以及评论区的提示，我就不走多余的路。直接拉取官方镜像 直接写 docker-compose.yml\n显示已折叠代码（17 行） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 services: waline: container_name: waline image: lizheming/waline:latest restart: always ports: - 8360:8360 volumes: - ${PWD}/data:/app/data environment: TZ: \u0026#39;Asia/Shanghai\u0026#39; SQLITE_PATH: \u0026#39;/app/data\u0026#39; JWT_TOKEN: \u0026#39;Your token\u0026#39; SITE_NAME: \u0026#39;Your site name\u0026#39; SITE_URL: \u0026#39;https://example.com\u0026#39; SECURE_DOMAINS: \u0026#39;example.com\u0026#39; AUTHOR_EMAIL: \u0026#39;mail@example.com\u0026#39; 这里配置的环境变量似乎都没怎么用上。说实话我没看明白这个配置。直接运行后。在上一篇文章提到的 nginx 中进行反代。\nTip 这里我刚开始犯个小错误，nginx 刚开始反代的 127.0.0.1 但是访问不到，因为这里容器都没有用 host网络。所以在bridge下 宿主机的 ip 需要查看一下 在容器内执行，ip route | grep default 因为我nginx 那里用的不是默认的网段。默认一般宿主机是 执行docker network inspect bridge | grep Gateway可以看到 \u0026quot;Gateway\u0026quot;: \u0026quot;172.17.0.1\u0026quot;\n所以 nginx 需要反代 172.18.0.1:8360\n还有一点很重要。刚开始没注意。这里参考多数据库服务支持 | Waline 使用 SQLite 时需要下载 waline.sqlite 文件至合适的位置。之后在项目中配置如下环境变量。\n随后测试就能用了。其他的功能后续再研究\n","permalink":"/posts/hugo-%E9%85%8D%E7%BD%AEwaline/","summary":"\u003cp\u003e惯例先放网址，我这里参考了 好多，但是终究没有能抄的作业\n\u003ca href=\"https://github.com/CaiJimmy/hugo-theme-stack/blob/master/config.yaml#L38\"\u003ehugo-theme-stack/config.yaml at master · CaiJimmy/hugo-theme-stack · GitHub\u003c/a\u003e\n\u003ca href=\"https://waline.js.org/guide/deploy/vps.html\"\u003e独立部署 | Waline\u003c/a\u003e\n\u003ca href=\"https://github.com/walinejs/waline/blob/main/assets/waline.sqlite\"\u003ewaline/assets/waline.sqlite at main · walinejs/waline · GitHub\u003c/a\u003e\n\u003ca href=\"https://zhuanlan.zhihu.com/p/17088061312\"\u003e知乎 # 博客建站10 - 选择博客评论系统\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"系统架构\"\u003e系统架构\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e网站示例： \u003ca href=\"https://blog.anluoying.com/\"\u003eRay Blog\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e服务器: Cloudflare pages + 一台 aws 白嫖的服务器独立部署 waline\u003c/li\u003e\n\u003cli\u003e服务器系统： \u003ca href=\"https://zhida.zhihu.com/search?content_id=252439408\u0026amp;content_type=Article\u0026amp;match_order=1\u0026amp;q=Ubuntu+24.04+LTS\u0026amp;zhida_source=entity\"\u003eUbuntu 24.04 LTS\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e博客框架： \u003ca href=\"https://gohugo.io/\"\u003eThe world\u0026rsquo;s fastest framework for building websites\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e网站主题： \u003ca href=\"https://stack.jimmycai.com/\"\u003eStack | Card-style Hugo theme designed for bloggers\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"安装-waline\"\u003e安装 waline\u003c/h2\u003e\n\u003cp\u003e这里根据官方文档独立部署目录下的指引 以及评论区的提示，我就不走多余的路。直接拉取官方镜像\n直接写 docker-compose.yml\u003c/p\u003e","title":"Hugo 配置waline"},{"content":"Nginx and Let’s Encrypt with Docker 这篇主要都是来自 medium 的这篇文章 Nginx and Let’s Encrypt with Docker in Less Than 5 Minutes | by Philipp | Medium\n非常简单，我也就不多赘述，如果有不明白的问问 g 老师或者 c 老师即可 配完后确保域名解析到服务器即可（一直忘记改 cf，懵了好一会儿）\n主要是为了下一篇文章 配置评论系统\n","permalink":"/posts/nginx-certbot/","summary":"\u003ch2 id=\"nginx-and-lets-encrypt-with-docker\"\u003eNginx and Let’s Encrypt with Docker\u003c/h2\u003e\n\u003cp\u003e这篇主要都是来自 medium 的这篇文章\n\u003ca href=\"https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71\"\u003eNginx and Let’s Encrypt with Docker in Less Than 5 Minutes | by Philipp | Medium\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e非常简单，我也就不多赘述，如果有不明白的问问 g 老师或者 c 老师即可\n配完后确保域名解析到服务器即可（一直忘记改 cf，懵了好一会儿）\u003c/p\u003e\n\u003cp\u003e主要是为了下一篇文章 配置评论系统\u003c/p\u003e","title":"Nginx Certbot"},{"content":" 显示已折叠代码（28 行） 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 server { listen 1043 ssl default_server; server_name 172.16.99.32; ssl_certificate /etc/nginx/ssl/1043/server.crt; ssl_certificate_key /etc/nginx/ssl/1043/server.key; access_log /var/log/nginx/1043_access.log; error_log /var/log/nginx/1043_error.log warn; location / { add_header Access-Control-Allow-Origin \u0026#34;*\u0026#34;; add_header Access-Control-Allow-Methods \u0026#34;GET, POST, OPTIONS\u0026#34;; add_header Access-Control-Allow-Headers \u0026#34;Authorization, Content-Type\u0026#34;; add_header Access-Control-Allow-Credentials true; proxy_pass http://X.X.X.X:1030; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 处理 OPTIONS 预检请求 if ($request_method = OPTIONS) { return 204; } } } 这个 nginx 配置做了什么事情\n1 server { • 定义一个 server 块，表示一个独立的虚拟主机配置。\n1 listen 1043 ssl default_server; • 监听 1043 端口，并启用 SSL（即 HTTPS）。\n• ssl 关键字表示该服务器使用 TLS/SSL 加密。\n• 添加 default_server，确保该 server 块是 1043 端口的默认处理服务器。\n• 指定该服务器的 server_name（主机名）。\n• 注意：server_name 不能包含端口号，应该改成（这里原来带了端口号）：\n1 server_name 172.16.99.32; 1 2 ssl_certificate /etc/nginx/ssl/1043/server.crt; ssl_certificate_key /etc/nginx/ssl/1043/server.key; • 指定 SSL 证书和私钥的路径，使 Nginx 能够加密 HTTPS 连接。\n1 access_log /var/log/nginx/1043_access.log; • 记录访问日志，存放在 /var/log/nginx/1043_access.log 文件中。\n1 error_log /var/log/nginx/1043_error.log warn; • 记录错误日志，存放在 /var/log/nginx/1043_error.log，日志级别为 warn（警告）。\n1 location / { • 定义 URL 访问 /（根路径）时的处理方式。\n1 add_header Access-Control-Allow-Origin \u0026#34;*\u0026#34;; • 允许跨域请求（CORS），* 代表任何来源都可以访问。\n1 add_header Access-Control-Allow-Methods GET, POST, OPTIONS; • 允许的 HTTP 请求方法：GET、POST 和 OPTIONS。\n1 add_header Access-Control-Allow-Headers Authorization, Content-Type; • 允许客户端请求时使用 Authorization 和 Content-Type 头部。\n1 proxy_pass http://X.X.X.X:1030; • 将请求反向代理到 http://X.X.X.X:1030，即所有请求都会被转发到该服务器。\n1 proxy_set_header Host $host; • 将原始请求的 Host 头部信息传递给后端服务器。\n1 proxy_set_header X-Real-IP $remote_addr; • 将客户端的真实 IP 地址传递给后端服务器（默认情况下，后端服务器只会看到代理服务器的 IP）。\n1 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; • 传递 X-Forwarded-For 头部，包含客户端的真实 IP 地址和所有经过的代理服务器地址。\n1 proxy_set_header X-Forwarded-Proto $scheme; • 传递 X-Forwarded-Proto 头部，告诉后端服务器当前请求使用的是 http 还是 https。\n最后 结束花括号\n这里最后主要是证书问题 以后再研究了，还是太菜\n","permalink":"/posts/nginx-%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%9F%BA%E7%A1%80%E8%BD%AC%E5%8F%91%E9%85%8D%E7%BD%AE/","summary":"\u003cdetails class=\"code-details\"\u003e\n  \u003csummary\u003e\n      \u003cspan class=\"code-summary-left\"\u003e\n          \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n          \u003cspan\u003e显示已折叠代码（28 行）\u003c/span\u003e\n      \u003c/span\u003e\n  \u003c/summary\u003e\n  \u003cdiv class=\"collapsible-content\"\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-gdscript3\" data-lang=\"gdscript3\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eserver\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003elisten\u003c/span\u003e \u003cspan class=\"mi\"\u003e1043\u003c/span\u003e \u003cspan class=\"n\"\u003essl\u003c/span\u003e \u003cspan class=\"n\"\u003edefault_server\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eserver_name\u003c/span\u003e \u003cspan class=\"mf\"\u003e172.16\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"mf\"\u003e99.32\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003essl_certificate\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003eetc\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003enginx\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003essl\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"mi\"\u003e1043\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003eserver\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecrt\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003essl_certificate_key\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003eetc\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003enginx\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003essl\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"mi\"\u003e1043\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003eserver\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eaccess_log\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"k\"\u003evar\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nb\"\u003elog\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003enginx\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"mi\"\u003e1043\u003c/span\u003e\u003cspan class=\"n\"\u003e_access\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eerror_log\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"k\"\u003evar\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nb\"\u003elog\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003enginx\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"mi\"\u003e1043\u003c/span\u003e\u003cspan class=\"n\"\u003e_error\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003elog\u003c/span\u003e \u003cspan class=\"n\"\u003ewarn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003elocation\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eadd_header\u003c/span\u003e \u003cspan class=\"n\"\u003eAccess\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"ne\"\u003eControl\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eAllow\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eOrigin\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;*\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eadd_header\u003c/span\u003e \u003cspan class=\"n\"\u003eAccess\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"ne\"\u003eControl\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eAllow\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eMethods\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;GET, POST, OPTIONS\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eadd_header\u003c/span\u003e \u003cspan class=\"n\"\u003eAccess\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"ne\"\u003eControl\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eAllow\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eHeaders\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Authorization, Content-Type\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eadd_header\u003c/span\u003e \u003cspan class=\"n\"\u003eAccess\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"ne\"\u003eControl\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eAllow\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eCredentials\u003c/span\u003e \u003cspan class=\"bp\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eproxy_pass\u003c/span\u003e \u003cspan class=\"n\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"o\"\u003e//\u003c/span\u003e\u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"mi\"\u003e1030\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eproxy_set_header\u003c/span\u003e \u003cspan class=\"n\"\u003eHost\u003c/span\u003e \u003cspan class=\"o\"\u003e$\u003c/span\u003e\u003cspan class=\"n\"\u003ehost\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eproxy_set_header\u003c/span\u003e \u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eReal\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"ne\"\u003eIP\u003c/span\u003e \u003cspan class=\"o\"\u003e$\u003c/span\u003e\u003cspan class=\"n\"\u003eremote_addr\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eproxy_set_header\u003c/span\u003e \u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eForwarded\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eFor\u003c/span\u003e \u003cspan class=\"o\"\u003e$\u003c/span\u003e\u003cspan class=\"n\"\u003eproxy_add_x_forwarded_for\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eproxy_set_header\u003c/span\u003e \u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eForwarded\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eProto\u003c/span\u003e \u003cspan class=\"o\"\u003e$\u003c/span\u003e\u003cspan class=\"n\"\u003escheme\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e# 处理 OPTIONS 预检请求\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e$\u003c/span\u003e\u003cspan class=\"n\"\u003erequest_method\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eOPTIONS\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e204\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cp\u003e这个 nginx 配置做了什么事情\u003c/p\u003e\n\u003chr\u003e\n\n\n \n\n\n\n\n\u003cdiv class=\"code-block-wrap\"\u003e\n  \u003cdiv class=\"code-titlebar\"\u003e\n      \u003cspan class=\"code-dots\"\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/span\u003e\n      \n  \u003c/div\u003e\n\n\n\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eserver {\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003e• 定义一个 server 块，表示一个独立的虚拟主机配置。\u003c/p\u003e","title":"Nginx 记一次基础转发配置"},{"content":" 去年装的新电脑，刚开始只买了一块2T的SSD。自作聪明给C盘分了100G觉得足矣。结果随后不到两个月就是无尽的红色警告\n直到今天终于忍不了了。 之前因为存储焦虑（因为大学配的ITX只买了一块西数500G的，那真的是捉襟见肘的存储。游戏随玩随删） 这次配电脑没多久还买了一块二手16T希捷企业机械盘。买之前担惊受怕 买之后只能说（真香） 这里必须提醒。二手盘有风险，数据无价。我有多次备份的习惯，所以大胆入了，除了炒豆子有点吵，用着感觉还行。\n解决方案 bb太多有点偏题，解决方案很简单。\n右键我的电脑。有一个计算机管理（Manage） 打开存储-\u0026gt;磁盘管理 可以看到下面是我的SSD 此时C盘右侧邻近的是我的D盘.F盘是从D盘压缩出来的\n右键D盘 点击压缩卷(Shrink Volume). 不知道为什么我这里最多只能压缩出来640G 但是D盘我本身还有很多,这里一路Next.随后压缩出来的卷会显示未分配.我这里因为已经新建了F盘了 就不截图了,右键未分配进行新建卷即可\n然后备份D盘的内容到F盘.\nTip 如果D盘大于F盘怎么办呢(哥们我全给删了.反正都是游戏) 哈哈备份走就好了\n文件清空后对D盘 Delete Volume/ 删除卷\n长达两个小时的安静\n沃日 太麻烦了 一怒之下直接全部清空。重装。现在一块2T C盘 一块 16T E盘。舒服！！！！\n","permalink":"/posts/win-%E6%9D%82%E8%AE%B0c%E7%9B%98%E6%BB%A1%E4%BA%86/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e去年装的新电脑，刚开始只买了一块2T的SSD。自作聪明给C盘分了100G觉得足矣。结果随后不到两个月就是无尽的红色警告\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e直到今天终于忍不了了。\n之前因为存储焦虑（因为大学配的ITX只买了一块西数500G的，那真的是捉襟见肘的存储。游戏随玩随删）\n这次配电脑没多久还买了一块二手16T希捷企业机械盘。买之前担惊受怕 买之后只能说（真香）\n这里必须提醒。二手盘有风险，数据无价。我有多次备份的习惯，所以大胆入了，除了炒豆子有点吵，用着感觉还行。\u003c/p\u003e","title":"Win 杂记C盘满了"},{"content":" 记上一次急躁的我为了扩C盘直接抛弃了D盘后。所有开发环境也被我删干净了，都在D盘。正好那就重新安装，顺便记录一下。 我主要用到的环境一般是python java和node 这里java因为许久未碰。我都把我JB系列的软件都删了，现在是cursor用户也足以。主要是常常会在公司打开接近十几个工作空间。五个桌面，轻量的vscode配合插件也足以\n所以我首先安装miniconda，用来管理python环境\nConda Conda is an open source package management system and environment management system for installing multiple versions of software packages and their dependencies and switching easily between them. It works on Linux, OS X and Windows, and was created for Python programs but can package and distribute any software.\n最初接触Python的时候还是 Python3.7，就是五年前的事情了。那时候对于Anaconda的安装都是模模糊糊的。后来也是摸爬滚打，像Python Java系列的依赖，觉得比C++真的容易太多了。\n这里准备安装的是miniConda。顾名思义，mini版\n这一类安装 建议参考官方文档，所以先放上链接miniconda/intstall\n然后是我的安装步骤\n诶，太久没装了吗。原来win直接装一个exe就一步到位了 尬住了 那就装一下node吧\nnode 因为调试需求。node也会需要不同的版本。之前用的是nvm管理不同的版本，所以还是继续安装nvm\n题外话好像也有人做了jvm 管理不同的jdk版本。不过之前都是手动切换 顶多也是java1.8和java十几\nGitHub - nvm-sh/nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions\n突然想了想 在家里开发用到node应该很少了，先不要增加太多实体。参考git仓库应该足矣\n","permalink":"/posts/win-%E5%B8%B8%E7%94%A8%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E5%AE%89%E8%A3%85/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e记上一次急躁的我为了扩C盘直接抛弃了D盘后。所有开发环境也被我删干净了，都在D盘。正好那就重新安装，顺便记录一下。\n我主要用到的环境一般是python java和node\n这里java因为许久未碰。我都把我JB系列的软件都删了，现在是cursor用户也足以。主要是常常会在公司打开接近十几个工作空间。五个桌面，轻量的vscode配合插件也足以\u003c/p\u003e","title":"Win 常用开发环境安装"},{"content":" 钉钉创建一个群。然后创建一个普通机器人。 这个思路是自己想的，随手搜了一下 真有佬做了.下面是链接 如何使用钉钉机器人通知接收服务器SSH登录提醒 - 阿豪运维笔记\n根据佬的写法 后面再加点功能。最近忙的紧，先把这个脚本部署尝试\n主要原理就是sshrc是SSH服务的一个特殊文件，它会在每次SSH会话建立时自动执行。 执行时获取一些信息，执行 POST 请求 到钉钉的 webhook 地址，群里的机器人就可以发消息了\n注意关键词的设置\n[sh] 显示已折叠代码（70 行） 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 # 编辑/etc/ssh/sshrc文件 最后更新时间 2025-03-17 11:09:34 # 设置日志文件 LOG_FILE=\u0026#34;/tmp/ssh_notification_debug.log\u0026#34; # 记录详细日志的函数 log_debug() { echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] [PID:$$] [SSH_TTY:$SSH_TTY] [PPID:$PPID] $1\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; } # 记录脚本启动 log_debug \u0026#34;脚本开始执行 ====================\u0026#34; log_debug \u0026#34;SSH_CLIENT: $SSH_CLIENT\u0026#34; # 获取登录者的用户名 user=$USER # 获取登录IP地址 ip=${SSH_CLIENT%% *} # 创建基于用户、IP和当前小时的锁文件名 # 这样同一IP同一用户每小时只会通知一次 current_hour=$(date +%Y%m%d%H) LOCK_FILE=\u0026#34;/tmp/ssh_notify_${user}_${ip}_${current_hour}\u0026#34; GLOBAL_LOCK=\u0026#34;/tmp/ssh_notification.lock\u0026#34; log_debug \u0026#34;检查锁文件: $LOCK_FILE\u0026#34; # 简化的锁定机制，兼容dash shell if [ -f \u0026#34;$LOCK_FILE\u0026#34; ]; then log_debug \u0026#34;本小时已经为用户${user}从IP${ip}发送过通知\u0026#34; echo \u0026#34;Welcome back. The administrator has already been notified of your login this hour.\u0026#34; exit 0 fi # 创建锁定文件 touch \u0026#34;$LOCK_FILE\u0026#34; log_debug \u0026#34;已创建锁文件: $LOCK_FILE\u0026#34; # 获取登录的时间 time=$(date +%F%t%k:%M) # 服务器的IP地址和自定义名称 server=\u0026#39;204-ray-server-in-mckj\u0026#39; # 修改函数声明语法，使其兼容Dash DingDingalarm() { log_debug \u0026#34;开始发送钉钉通知\u0026#34; local url=\u0026#34;https://oapi.dingtalk.com/robot/send?access_token=钉钉token\u0026#34; local UA=\u0026#34;Mozilla/5.0(WindowsNT6.2;WOW64)AppleWebKit/535.24(KHTML,likeGecko)Chrome/19.0.1055.1Safari/535.24\u0026#34; local res res=$(curl -XPOST -s -L -H\u0026#34;Content-Type:application/json\u0026#34; -H\u0026#34;charset:utf-8\u0026#34; \u0026#34;$url\u0026#34; -d \u0026#34;{\\\u0026#34;msgtype\\\u0026#34;:\\\u0026#34;markdown\\\u0026#34;,\\\u0026#34;markdown\\\u0026#34;:{\\\u0026#34;title\\\u0026#34;:\\\u0026#34;$1\\\u0026#34;,\\\u0026#34;text\\\u0026#34;:\\\u0026#34;$2\\\u0026#34;}}\u0026#34;) if [ $? -eq 0 ]; then log_debug \u0026#34;钉钉通知发送成功: $res\u0026#34; echo \u0026#34;钉钉通知已发送，结果：$res\u0026#34; echo \u0026#34;Notification sent to admin.\u0026#34; else log_debug \u0026#34;钉钉通知发送失败: $res\u0026#34; echo \u0026#34;钉钉通知发送失败，错误信息：$res\u0026#34; fi } # 使用Markdown格式美化通知内容 message=\u0026#34;### 🔔 服务器登录通知 🔔\\n\\n**时间**：\u0026lt;font color=\u0026#39;#FF5722\u0026#39;\u0026gt;$time\u0026lt;/font\u0026gt;\\n\\n**服务器**：\u0026lt;font color=\u0026#39;#2196F3\u0026#39;\u0026gt;$server\u0026lt;/font\u0026gt;\\n\\n**用户**：\u0026lt;font color=\u0026#39;#4CAF50\u0026#39;\u0026gt;$user\u0026lt;/font\u0026gt;\\n\\n**来源IP**：\u0026lt;font color=\u0026#39;#9C27B0\u0026#39;\u0026gt;$ip\u0026lt;/font\u0026gt;\\n\\n**会话信息**：TTY=$SSH_TTY, PID=$$, PPID=$PPID\\n\\n\u0026gt; Please make sure to check if this login is expected.\u0026#34; DingDingalarm \u0026#34;服务器登录通知\u0026#34; \u0026#34;$message\u0026#34; # 打印日志，通知管理员并告知操作将被记录 echo \u0026#34;The administrator has been notified. All actions will be logged.\u0026#34; log_debug \u0026#34;通知完成，脚本结束 ====================\u0026#34; # 要定期清理锁文件，请使用以下命令设置cron作业（在root权限下执行一次）： # echo \u0026#34;5 * * * * root find /tmp -name \u0026#39;ssh_notify_*\u0026#39; -type f -mmin +60 -delete\u0026#34; \u0026gt; /etc/cron.d/clean_ssh_locks 小插曲 直接 cp 到 sshrc 中时报错\n/etc/ssh/sshrc: 11: Syntax error: \u0026quot;(\u0026quot; unexpected\n这个错误是因为在某些系统的默认shell（如Dash）中，函数声明的语法与Bash不同。在Dash中，函数声明不支持function functionname()的语法，而应该使用functionname()的形式。 去掉 function 即可\n更新 实际使用时，会发送三次消息给钉钉，看了一下代码应该不会执行三次，而是 sshrc 被执行了三次。\n加了日志和具体调用消息，在钉钉里收到三条信息如下\n1 2 3 会话信息：TTY=/dev/pts/0, PID=3230809, PPID=3230808 会话信息：TTY=, PID=3230824, PPID=3230823 会话信息：TTY=, PID=3230869, PPID=3230868 claude提示\n在SSH连接过程中，有三个不同的会话或进程被创建，每个都执行了sshrc脚本。这是SSH连接的典型行为。当建立SSH连接时，通常会有多个相关进程: 主SSH会话进程（带有TTY）\n可能的X11转发会话 可能的端口转发会话 可能的SFTP子会话\nclaude 建议使用锁文件来实现,然后写 crontab 定时清除锁文件。就可以控制时间段内登录了。\n","permalink":"/posts/%E7%BB%99ssh%E7%99%BB%E5%BD%95%E5%81%9A%E4%B8%80%E4%B8%AA%E9%80%9A%E7%9F%A5/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e钉钉创建一个群。然后创建一个普通机器人。\n这个思路是自己想的，随手搜了一下 真有佬做了.下面是链接\n\u003ca href=\"https://www.ahaoyw.com/article/843.html\"\u003e如何使用钉钉机器人通知接收服务器SSH登录提醒 - 阿豪运维笔记\u003c/a\u003e\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003cp\u003e根据佬的写法 后面再加点功能。最近忙的紧，先把这个脚本部署尝试\u003c/p\u003e\n\u003cp\u003e主要原理就是\u003ccode\u003esshrc\u003c/code\u003e是SSH服务的一个特殊文件，它会在每次SSH会话建立时自动执行。\n执行时获取一些信息，执行 POST 请求 到钉钉的 webhook 地址，群里的机器人就可以发消息了\u003c/p\u003e","title":"给ssh登录做一个通知"},{"content":" 参考链接\nhttps://www.ruanyifeng.com/blog/2014/03/server_setup.html\nhttps://hufangyun.com/2017/linux-init/\n第一篇阮一峰老师的，第二篇胡方运老师的， 主要内容可能与第二篇相似，希望以后我能进行完善，不要太过于臃肿也好。 针对于自己的个人习惯。也记录一个流程，以后配置服务器标准一点\n刚购买完服务器要做的操作 这里就忽视安全组的问题了。因为之前买过几年。安全组的配置基本一致，我也不记得初始化啥样子了，如果进不去自己检查网络连接，或者网页进去也行\n这里买的是阿里云的 99 一年,2c2g ubuntu24,3M 固定带宽。控制台启动后先重置root密码\n然后远程登录\nssh root@ip.ip.ip.ip\n进来看着原先的一串主机名眼睛花，改一手主机名\nhostnamectl set-hostname \u0026lt;new-hostname\u0026gt;\n然后重新登录就行了。\n如果只是要改命令行提示符一串可以研究~/.bashrc和各种插件，关于美化命令行那也是水很深，我就喜欢傻黑粗的玩意儿\n新账号 操作 Linux 系统任何时候尽量不使用 root 账户，毕竟权限太大对小白不是好事，有自知之明且要学一下好习惯，创建一个管理员账户\n[[Linux-用户管理#新建用户]]\n这里基本和文章开头贴的博客一致。我也创建admin用户组\naddgroup admin\n添加新用户(ray)\nuseradd -d /home/ray -s /bin/bash -m ray\n上面命令中，参数 d 指定用户的主目录，参数 s 指定用户的 shell，参数 m 表示如果该目录不存在，则创建该目录。\n接着，设置新用户的密码。\npasswd ray\n将新用户（ray）添加到用户组（admin）。\nusermod -a -G admin ray\n接着，为新用户设定sudo权限。\nsudo vim /etc/sudoers\n找到下面这一行。\nroot ALL=(ALL:ALL) ALL\n在这一行的下面，再添加一行。\n1 2 root ALL=(ALL:ALL) ALL ray ALL=(ALL:ALL) ALL 验证\n1 2 su - ray sudo whoami 如果一切设置正确，命令将输出 root，这表示 ray 用户已经拥有 sudo 权限。\n配置 SSH 连接 把自己电脑的 ssh 公钥，保存到服务器的 ~/.ssh/authorized_keys 文件中 这里使用 ssh-copy-id （之前傻傻的都是手动 copy 的\n-i 是指定公钥\n1 2 3 4 // 默认端口 22 ssh-copy-id -i ~/.ssh/id_rsa.pub root@123.456.78 // 带端口 25000 ssh-copy-id -i ~/.ssh/id_rsa.pub -p 25000 root@123.456.78 快捷ssh连接 这里还有个很有用的配置\n本地电脑配置 ssh config\n修改 ~/.ssh/config 文件\n1 2 3 4 5 6 7 8 9 10 // Host 字段后面的名称自定义 Host day-root hostname 123.456.78 port 25000 user root Host day hostname 123.456.78 port 25000 user www 之后 ssh day 无须输入密码，就可以登入服务器了。\nssh端口更改 编辑SSH配置文件/etc/ssh/sshd_config。\n1 2 sudo cp /etc/ssh/sshd_config ~ (备份，复原时使用) sudo vim /etc/ssh/sshd_config 在配置文件中，将 SSH 的默认端口 22 改掉。假设使用 22980\nPort 22980\n如果修改了端口，记的也修改:\n本地的 ssh 配置 ~/.ssh/config 内的端口 云服务器的防火墙 ssh 的端口设置 然后，检查几个设置是否设成下面这样，确保去除前面的#号。\n选项 含义 Protocol 2 ssh 协议使用新版的 PermitRootLogin no 不允许 root 登录 PermitEmptyPasswords no 不允许空密码登录 PasswordAuthentication no 使用密码授权登录 GSSAPIAuthentication no 加快连接 PubkeyAuthentication yes 允许公钥认证 UseDNS no 禁用DNS反向解析 会加快速度 SyslogFacility AUTHPRIV 记录用户登录信息 上面主要是禁止 root 用户登录，以及禁止用密码方式登录。\n保存后，退出文件编辑。\n接着，改变authorized_keys文件的权限。\nsudo chmod 600 ~/.ssh/authorized_keys \u0026amp;\u0026amp; chmod 700 ~/.ssh/\n接着重启 ssh 服务\nsystemctl daemon-reload\nsudo service ssh restart 或 sudo /etc/init.d/ssh restart\n如果配置文件报错可以用/usr/sbin/sshd -T检查错误并更改\n这样子配置一下 安全组就可以测试连接了\n运行环境配置 这里不记录了 主要是换时区什么的\n配置防火墙 配置哪吒 说实话是有点不放心监控软件，但是现在作为小白。这种酷炫装逼的东西还是想要的 这里看文档装好了 没什么难度，之后配置复杂的时候再记录吧\n配置通知服务 这个比如 钉钉 微信啥的推送。ssh 登录时或者之类的。哪吒貌似也可以。 自己参考大佬做了脚本\n","permalink":"/posts/%E9%98%BF%E9%87%8C%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9D%E5%A7%8B%E5%8C%96/","summary":"\u003cblockquote\u003e\n    \u003cp\u003e参考链接\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.ruanyifeng.com/blog/2014/03/server_setup.html\"\u003ehttps://www.ruanyifeng.com/blog/2014/03/server_setup.html\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://hufangyun.com/2017/linux-init/\"\u003ehttps://hufangyun.com/2017/linux-init/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e第一篇阮一峰老师的，第二篇胡方运老师的，\n主要内容可能与第二篇相似，希望以后我能进行完善，不要太过于臃肿也好。\n针对于自己的个人习惯。也记录一个流程，以后配置服务器标准一点\u003c/p\u003e\n\n  \u003c/blockquote\u003e\u003ch2 id=\"刚购买完服务器要做的操作\"\u003e刚购买完服务器要做的操作\u003c/h2\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e这里就忽视安全组的问题了。因为之前买过几年。安全组的配置基本一致，我也不记得初始化啥样子了，如果进不去自己检查网络连接，或者网页进去也行\u003c/p\u003e","title":"服务器初始化"},{"content":"Hi there.This is my first rambling note. 笔记工具决赛 其实我从小时候写日记本开始就非常挑工具，我字写的不咋地，但是家里有一堆很好看的小本子。每一本上面都稀稀疏疏都记录了几篇日记，不超过一个月。看得出来真的很懒了。 有时候选本子就有点像是想激励一下自己。啊，这么好看的本子，得写日记了。就好像办健身卡前，啊，都付了钱了。不得不练了。但是如果这样非常有效的话也不会在我同时有着 Keep 包月会员和乐刻健身房包季会员的情况下一个季度胖 40 斤了\n先记录下截止到目前为止。对我而言 出了决赛圈的工具。\n先列一下我的 电子设备\nmatebook 14 macmini 3 windows company 给同事用了 windows home redmi k70 主要是写 markdown 文档，记录一些杂七杂八和开发日志或文档\n1-3 都是经常在公司用着的。由于 windows 不适合长期开着。之前windows 作为主力办公 开着一周句柄爆多。卡的不能用。所以舔着脸叫领导换了 mac。现在就是在 mac 上写这篇笔记\n在 windows 上我习惯使用 typora 配合图床写笔记。看情况导入语雀分享或者什么样。但是我发现 typora 实在是只能作为编辑器，有点太局部了 语雀又不适合做一个 经常能打开的编辑器（尤其是用的这两年崩了两次 如果是 vscode ，我敲代码也用 vscode ，写笔记也用。会疯（主要是觉得 不如 typora 写 md 方便，而且最近一个月用 cursor 量更大（AI 造福增删改查小子） Obsidian(真神) [[Obsidian 入门指东]]\n其实曾经用过一段时间的 Obsidian。但是糊里糊涂了五年前的事情。笔记不多但是也全部丢了。当时主要卡在 git 同步笔记遇到不少问题。现在认知略微增加了一些。以前碰到的一些问题 都不算是问题\n因为在重新用回 Obsidian 前。公司 nas 搭建了 seafile 网盘。用来多端同步typora 写的笔记和文档。突然意识到最近又有记录的心了。而且在 L 站混迹一段时间 和 在网上搜索一些技术解答时见到了不少大佬的博客。实在心痒痒，那么我的目标到目前为止很明确了。\nHello Blog 博客我想要简约 但是又要好看（因为要炫） 简约是为了不想让博客的经营 大于 我写博客的精力。 磨刀误了砍柴功 那就非初衷了。\n其次对于博客 我期待能长久运行。哪怕现在使用的是 他人的框架。我还是期待以后能有时间自己实现一下的，这里贴一下一位佬的博客， 所以 其实我是想有两套系统 一套 可以说是笔记知识库，基于 Obsidian 一套 作为博客系统。发布一些编辑好的笔记\n对于博客系统中 我受语雀的印象。对于博客中我可能会有两种形式的笔记存在。当然，笔记同理 一种是卡片。原子性较强，一般会是名词解释 或者灵光一现 一种是笔记。往往长篇大论，会有技术型，日志型，自言自语生活型。\ngraph TD A[笔记系统] --\u0026gt; B[卡片] A --\u0026gt; C[笔记] B --\u0026gt; D[名词解释] B --\u0026gt; E[灵光一现] C --\u0026gt; F[技术型] C --\u0026gt; G[日志型] C --\u0026gt; H[自言自语生活型] 技术型笔记 期待 是少些废话，仅探讨某个情况 或某个概念，会是多个卡片的集合。更像小型论文\n日志型 一般就是 操作过程中 的碎碎念。我会尽量减少碎碎念，但是真的忍不住，半夜 debug 的秃头程序员真的无处发泄\n生活型就是私人的啦\n最后再给自己许个愿。 不积跬步，无以至千里 共勉\n","permalink":"/posts/hello-blog/","summary":"\u003ch2 id=\"hi-therethis-is-my-first-rambling-note\"\u003eHi there.This is my first rambling note.\u003c/h2\u003e\n\u003ch2 id=\"笔记工具决赛\"\u003e笔记工具决赛\u003c/h2\u003e\n\u003cblockquote\u003e\n    \u003cp\u003e其实我从小时候写日记本开始就非常挑工具，我字写的不咋地，但是家里有一堆很好看的小本子。每一本上面都稀稀疏疏都记录了几篇日记，不超过一个月。看得出来真的很懒了。\n有时候选本子就有点像是想激励一下自己。啊，这么好看的本子，得写日记了。就好像办健身卡前，啊，都付了钱了。不得不练了。但是如果这样非常有效的话也不会在我同时有着 Keep 包月会员和乐刻健身房包季会员的情况下一个季度胖 40 斤了\u003c/p\u003e","title":"Hello Blog"},{"content":" 欢迎交换友链 🔗\n请按照以下格式在评论区留言：\nyaml 1 2 3 4 - name: 博客名称 - url: 博客地址 - icon: 头像链接 (可选，默认读取网站 favicon) - description: 一句话介绍 ","permalink":"/links/","summary":"links","title":"友链"}]