This blog is rated 🔞, viewer discretion is advised

iosevka字体让中文 ASCII diagram 图表对齐

对齐前:

┌──────────────┐     ┌──────────────┐
│  用户请求     │────▶ │  网关服务     │
│  User Req    │     │  Gateway     │
└──────┬───────┘     └──────┬───────┘
       │                    │
       ▼                    ▼
┌──────────────┐     ┌──────────────┐
│  认证服务     │      │  业务逻辑     │
│  Auth Svc    │     │  BizLogic    │
└──────────────┘     └──────┬───────┘
                            │
                            ▼
                     ┌──────────────┐
                     │  数据存储😆   │
                     │  Database😂  │
                     └──────────────┘

对齐后:



┌──────────────┐     ┌──────────────┐
│  用户请求    │────▶│  网关服务    │
│  User Req    │     │  Gateway     │
└──────┬───────┘     └──────┬───────┘
       │                    │
       ▼                    ▼
┌──────────────┐     ┌──────────────┐
│  认证服务    │     │  业务逻辑    │
│  Auth Svc    │     │  BizLogic    │
└──────────────┘     └──────┬───────┘
                            │
                            ▼
                     ┌──────────────┐
                     │  数据存储😆  │
                     │  Database😂  │
                     └──────────────┘

AI 的解释:

Iosevka 的作者 be5invis 当年做这个字体时明确说过:因为他和很多朋友生活在中国、日本,所以他把这款字体做成严格的 1/2 em 宽,专门用来跟汉字对齐——面向亚洲用户,用这个就能保住完美对齐。也就是说 Iosevka 的西文字形宽度天生就是 0.5em,不需要任何 CSS 补偿或 size-adjust 魔法,配任何满格 1em 的 CJK 字体(Hiragino、PingFang、Source Han Sans 等)都是精确 2:1。而且它的构建系统是声明式的,用 TOML 配置文件驱动,可以精细控制粗细、宽度、每个字形的风格变体。

中英混合是搞定了,最后那个 数据存储 Database 带emoji错位了。

emoji 还有个zwj的问题:

农民 emoji 🧑‍🌾 其实是"人"+ 零宽连接符(ZWJ)+ "农作物"两个 emoji 码位拼成的

在 wcwidth 计算,会分别得到宽度 2、0、2,加起来变成 4 列宽导致错位

目前暂时无解

Posted

stdout

为什么 Github OAuth 故意拦截 CORS

跟AI发闹骚学到的

https://github.com/isaacs/github/issues/330

Allowing CORS for the endpoint you mentioned would mean that you could complete this step of the Web flow from a browser:
And this would mean that you're hard-coding your client_id and client_secret into a webpage (or JS file loaded into that webpage) for everyone to see. This would indeed cause security concerns since the client_secret should be kept secret. If someone got hold of your client_id and client_secret, they could impersonate you application, and for example -- wipe all the tokens for that application:

如果允许在浏览器通过 client_id, client_secret 交换得到 access_token,那么实际上你的账号等于公开裸奔,你所有的 github 资产等于公开被人控制。所以必须走一个服务端流程,然后再把 access_token 下发

呃,好像很有道理。

Posted

stdout

gitweets改版,复刻微信「朋友圈」

去年搓了个 gitweets,一个 .html 实现了「微博」,拿git历史当feed流~发推

这个周末看着 coding plan 还剩 20% 要到期,没用完,怎么办呢?想来想去,挖大坑干不完,小修小改,就拿这个 gitweets 继续填坑了

首先是让AI把界面改成模仿微信 朋友圈,啪一下,很快啊,结果让人非常印象深刻,很逼真

https://f.est.im/est

现在的AI真厉害。让我去调CSS可能这辈子都搞不出来这个效果了。

后面是我的一些唠叨,不感兴趣的可以关闭页面,或者去上面那个围观一下。

想起来独乐乐不如众乐乐,要不,支持个评论功能?

项目的初衷是 static page,要实现互动肯定得用一些API了。

最能想到的思路,走传统的 github issue 什么的,和这个 gitweets 最大的出发点冲突了:一个 git repo 包含所有数据,随时搬家,不用导出。

而且麻烦的是,post 是绑定到 commit 上的。如果你用一个 JSON 之类的来存评论,势必也会新增一个 commit,这样会污染post时间线。

突然想起来一个古老的东西,git notes,这是连 ChatGPT 和 Claude 都没想到的邪路,不过它们很快确认这个办法甚好,可行。

git notes选型定下里,建立这个数据模型让我纠结了很久。围绕 github 开展流程,让我一度误入歧途

  1. oauth 登录,不要任何scope
  2. 用来存 github notes的repo邀请登录人加入项目
  3. 浏览器通过该用户access token发起 notes append

最后想明白了,压根不应该走浏览器这一套。而是只能走后端代劳,用 Fine-Grained PAT 来和 github API 交互

其间还考虑 github 越来越拉垮,想避免 vendor lock-in,直接走git http协议。

首先想到的是 Cloudflare 那个牛逼的 zig 写的 100kb 的 wasm 可以 http 读写任意 git 仓库

git protocol engine is written in pure Zig (no libc), compiled to a ~100KB WASM binary. Support for both v1 and v2 of the git protocol. Support capabilities including ls-refs, shallow clones (deepen, deepen-since, deepen-relative), and incremental fetch with have/want negotiation.

仔细读了下文档,让AI一起调研,发现tmd这玩意仅限 Worker 内部使用,只能读写 CF 内部的 假 git,不支持读写外部任意 git http。

isomorphic-git 坑也挺多 。还是先走 github API 吧

这个 git notes 要走REST API 有查询放大 3+N 的问题,怕掉用次数爆掉,于是让AI走 GraphQL。我自己手动是搓不动 GraphQL,太难了。AI虽然是 flash 普通智商版本,也分分钟拼接好。一次成功。真猛 😭

于是 Vibe 出来了。

搓完了想起一个问题,如果有人刷评论怎么办?于是让AI搓了个 /.admin 管理页面。也是秒写好。太方便了。

明显欠缺的功能搓完之后,感觉又进入了贤者时间,索然无味了。

Posted

stdout

MiMoCode 干完活儿发通知

AI在 coding 的时候我其实在玩别的。希望agent 每次干完活,macOS 弹个通知。

手上是 MimoCode,就让它自己写个。啪,很快写好了。结果还是折腾了好一会儿, 记录几个有意思的小坑。

首先是如果当前CLI是活动的,就不弹通知。

需要判断活动窗口。最初用 osascript

osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'

直接报错 Not authorized to send Apple events to System Events (-1743)

于是换 lsappinfo,走 macOS Launch Services API,不需要任何额外授权:

lsappinfo info -only name $(lsappinfo front)
# → "LSDisplayName"="Terminal"

还能拿 PID:lsappinfo info -only pid $(lsappinfo front)

然后如何判定活动窗口是不是 CLI?

写死个["iTerm2", "Terminal", "Alacritty", "kitty", ...] 列表

太笨了。当前hook是子进程,直接遍历 parent 进程树啊!找到终端模拟器的 PID,再跟前台 app 的 PID 比对。

但是在 tmux 里爬出来是这样的:

node → zsh → tmux → launchd(1) → init(0)

Terminal.app 的 PID 根本不在树上。因为 tmux server 启动后被 reparent 到了 launchd,跟 Terminal.app 断开了父子关系。

又想到一个办法,直接查 $TMUX_PANE 是否是当前 active pane:

tmux list-panes -F '#{pane_id} #{pane_active}' | awk '$2==1 {print $1}'

如果 $TMUX_PANE == active pane ID,说明用户就在看这个窗口,不需要通知。完全不需要知道前台是哪个 app。

非 tmux 场景才用进程树 + lsappinfo 的 PID 比对。

然后就是挑选具体哪些事件要弹通知了。

权限通知:permission.ask,但文档说 "not yet wired"。试了一下,确实没触发。尴尬。雷军!!!金凡!!!

最后的方案:注册 permission.ask 占位,如果哪天接入了就能精确捕获。目前靠 tool.execute.before 抢占并推断。工具跑完了说明权限已过或不需要。

还有个情况就是 Subagent 完成也给我哐哐弹。最初硬编码了 SUBAGENT_TYPES = ["general", "explore"];后来发现 actor.preStop / actor.postStop 的 input 里有 mode: "subagent" | "peer"。于是就做了个计数器

最后通知到时候带上 Session 名字,折腾了一圈 直接 sqlite3 ~/.local/share/mimocode/mimocode.db "SELECT title FROM session WHERE id = '$SESSION_ID'"

完整代码放在

https://github.com/est/snippets/blob/master/mimocode-hooks/notify-done.ts

复制到 ~/.config/mimocode/hooks/ 就可以试试效果

Posted

stdout

grep vs sqlite 谁更适合微信聊天记录?

一个爆火的讨论

云风 @cloudwu 2026-06-29
微信的开发人员根本就不懂该怎么储存数据。这种聊天软件,文本和媒体文件分开存,文本根本就不应该保存在什么数据库(sqlite)里, 一个对话一个文本文件追加就可以了。需要搜索的时候 grep 一下性能完全符合需求。一个对话能有多少文本?一秒一个字 24 小时不间断,一年也就 30M 个字。

网上的争论都是猜测,我呢,决定让 opus 跑一局。

首先让AI去搜微信聊录表结构

微信(Android)聊天记录存储在加密 SQLite 数据库 EnMicroMsg.db 中,使用 SQLCipher(AES-256-CBC,PBKDF2 256000 轮派生密钥)。核心 message 表:

CREATE TABLE message (
    msgId      INTEGER PRIMARY KEY,  -- 本地自增 ID
    msgSvrId   INTEGER,              -- 服务器消息 ID
    type       INTEGER,              -- 1=文字, 3=图片, 34=语音, 43=视频
    isSend     INTEGER,              -- 0=接收, 1=发送
    createTime INTEGER,              -- Unix 时间戳
    talker     TEXT,                 -- wxid 或群 chatroom ID
    content    TEXT,                 -- 消息正文
    imgPath    TEXT                  -- 附件路径
);

测试设计

  • 数据量:50 万条模拟消息(模拟中度用户 ~2 年)
  • 搜索关键词微信支付服务器数据库会议周末
  • 环境:macOS Apple Silicon, Python 3.14, ripgrep 15.1, DuckDB 1.5.4, Polars 1.42
  • 每项测试 3 轮取最小值

参赛选手

分类 方案 思路
传统文本 grep (BSD) 最朴素的逐字节匹配
SIMD文本 ripgrep AVX2/NEON 并行 + 多线程
零拷贝 mmap 直接搜索 OS page cache + Python bytes.find
压缩文本 zstd 流式解压搜索 省空间,边解压边搜
索引 倒排索引 (2-gram) 搜索引擎思路,内存索引
索引 Bloom Filter 分块 概率型预过滤
RDBMS SQLite LIKE 微信的实际方案(去掉加密)
RDBMS SQLite mmap 模式 mmap I/O 加速
RDBMS FTS SQLite FTS5 (trigram) 全文搜索引擎
列式DB DuckDB contains() OLAP 列式扫描
列式DB DuckDB FTS DuckDB 的全文搜索扩展
列式文件 Parquet(zstd) + DuckDB 列式文件直接查询
DataFrame Polars lazy scan Rust实现的极速 DataFrame
DataFrame Polars in-memory 全量载入内存
并行文本 ripgrep 多文件并行 分块文件 + rg 多线程

测试结果

关键词搜索延迟(ms, 3轮最小值)

# 方案 微信支付 服务器 数据库 会议 周末 平均
1 SQLite FTS5 (trigram) 0.65 0.46 0.37 ❌² ❌² 0.31¹
2 Polars lazy scan 3.51 2.56 2.43 2.49 2.47 2.69
3 倒排索引 (2-gram) 2.71 3.26 2.78 3.24 2.37 2.87
4 Polars in-memory 2.67 3.61 2.98 3.21 3.21 3.13
5 DuckDB contains() 3.72 3.23 3.18 3.91 4.41 3.69
6 Parquet + DuckDB 4.63 4.37 4.55 4.88 4.84 4.65
7 ripgrep 多文件并行 10.65 10.26 9.48 10.32 9.05 9.95
8 DuckDB FTS (BM25) 12.53 11.77 12.85 11.82 12.78 12.35³
9 ripgrep (SIMD, 单文件) 13.24 13.77 13.40 14.14 13.10 13.53
10 mmap 直接搜索 17.00 22.62 22.86 31.36 30.52 24.87
11 Bloom Filter + 扫描 25.34 25.25 25.06 26.68 27.01 25.87
12 SQLite mmap LIKE 37.45 37.45 37.92 37.83 37.81 37.69
13 SQLite LIKE 43.13 41.40 41.50 41.51 41.46 41.80
14 grep (BSD) 139.10 136.86 141.42 122.54 124.16 132.82
15 zstd 流式解压搜索 161.32 164.22 164.56 170.23 170.73 166.21

¹ FTS5 只对 ≥3字符 的关键词有效,取3个有效关键词平均
² trigram tokenizer 无法匹配 2 字符的中文词
³ DuckDB FTS 默认 tokenizer 不支持中文,返回 0 结果(延迟仍可参考)

视觉化排名

 1.                                                            █   0.31ms SQLite FTS5
 2.                                                            █   2.69ms Polars lazy
 3.                                                            █   2.87ms 倒排索引
 4.                                                            █   3.13ms Polars in-mem
 5.                                                            █   3.69ms DuckDB contains
 6.                                                           ██   4.65ms Parquet+DuckDB
 7.                                                         ████   9.95ms rg多文件并行
 8.                                                       ██████  12.35ms DuckDB FTS
 9.                                                       ██████  13.53ms ripgrep (SIMD
10.                                                 ████████████  24.87ms mmap 直接搜索
11.                                                 ████████████  25.87ms Bloom+扫描
12.                                           ██████████████████  37.69ms SQLite mmap LIKE
13.                                         ████████████████████  41.80ms SQLite LIKE
14. ████████████████████████████████████████████████████████████ 132.82ms grep (BSD
15. ████████████████████████████████████████████████████████████ 166.21ms zstd流式解压

复合条件查询(指定用户 + 时间范围 + 关键词"会议")

方案 延迟 (ms) 倍率(vs grep)
SQLite indexed 2.80 76x
DuckDB 4.57 47x
Polars in-memory 7.02 30x
ripgrep pipe 20.47 10x
grep pipe 213.90 1x

存储大小

格式 大小 vs TSV 说明
Parquet (zstd) 8.3 MB 0.18x 列式 + 字典编码 + 压缩
zstd 压缩 TSV 12.7 MB 0.27x 纯压缩
DuckDB + FTS 26.0 MB 0.55x 含全文索引
TSV 纯文本 47.0 MB 1.00x 基线
SQLite 69.2 MB 1.47x B-tree 开销
SQLite + FTS5 116.8 MB 2.49x trigram 索引翻倍

各方案深度分析

Tier 1: 亚毫秒级(< 1ms)

SQLite FTS5 (trigram)
- 原理:对 content 字段的每个 3 字符子串建倒排索引
- 优点:查询极快(0.3-0.6ms),无需额外依赖
- 缺点:索引体积翻倍(+68MB);trigram 无法匹配 ≤2 字符的关键词
- 适用:搜索词通常 ≥3 字符的场景

Tier 2: 低毫秒级(2-5ms)

Polars (lazy/in-memory)
- 原理:Rust 实现的 DataFrame 引擎,列式内存布局 + SIMD 字符串匹配
- 优点:Parquet 文件仅 8.3MB(最小!),查询 2-3ms,复合查询也快(7ms)
- 缺点:需要加载到内存;Python 库依赖
- 杀手锏:8MB 的 Parquet 文件 + 3ms 搜索延迟,这是存储效率和速度的最佳平衡点

倒排索引 (2-gram, 内存)
- 原理:搜索引擎最经典的思路,对所有 2-gram 建 posting list
- 优点:构建仅 0.78s,查询 2.9ms,支持任意长度关键词
- 缺点:纯内存(需要序列化/加载),索引构建需要全量遍历
- 适用:append-only 数据可以增量更新索引

DuckDB contains()
- 原理:列式存储,content 列连续存放,CPU cache 友好 + SIMD 扫描
- 优点:无需专门索引即可 3.7ms;复合查询也仅 4.6ms;文件仅 26MB
- 缺点:需要 DuckDB 运行时
- 杀手锏:不建任何索引,纯靠列式布局就比 SQLite LIKE 快 11 倍

Parquet 文件 + DuckDB 零拷贝查询
- 原理:Parquet 本身就是列式格式,DuckDB 可以直接查询不需导入
- 优点:文件仅 8.3MB,不需要数据库进程,查询 4.6ms
- 缺点:每次查询需要启动 DuckDB 连接
- 杀手锏:一个 8MB 的文件就是完整的"数据库",任何语言都能读

Tier 3: 10ms 级

ripgrep 多文件并行
- 原理:把消息分块成多个文件,ripgrep 的 work-stealing 线程池并行搜索
- 优点:比单文件 ripgrep 快 ~35%(10ms vs 13.5ms)
- 缺点:文件管理复杂
- 适用:数据天然按时间分文件存储的场景

ripgrep (SIMD, 单文件)
- 原理:AVX2/NEON 每周期处理 16-32 字节,多线程(对单文件仍用单线程)
- 优点:零配置,即装即用
- 缺点:对单文件只能单线程

Tier 4: 失败/不推荐的"邪路"

Bloom Filter 分块预过滤
- 问题:中文常用 2-gram 只有 ~690 种,每个块都包含所有 n-gram,过滤率为 0
- 结论:对高频 n-gram 的数据集完全无效,白费构建时间

zstd 流式解压搜索
- 问题:Python 解压+搜索 166ms,比不压缩的 grep 还慢
- 结论:CPU 密集的解压抵消了 I/O 节省。如果数据在 SSD 上,不如直接读原文
- 可能有用的场景:数据在网络存储/HDD 上,I/O 是瓶颈时

DuckDB FTS (BM25)
- 问题:默认 tokenizer(类似 ICU word boundary)不支持中文
- 结论:需要自定义 tokenizer 或等 DuckDB 支持 trigram/CJK

mmap 直接搜索
- 表现:24.87ms,比 ripgrep 慢 2 倍
- 原因:Python 的 mmap.find() 是朴素搜索,没有 SIMD 优化
- 如果用 C/Rust 实现的 mmap + SIMD,预期接近 ripgrep 水平

推荐方案排名

如果从零设计微信聊天存储

优先级 方案 搜索延迟 存储 理由
🥇 Parquet + Polars/DuckDB 3-5ms 8 MB 存储最小、搜索极快、格式通用、append 友好
🥈 DuckDB 数据库 3.7ms 26 MB 单文件数据库、列式、SQL 查询、FTS 索引可选
🥉 SQLite + FTS5 0.3ms 117 MB 最快搜索(有索引)、但索引太大
4 ripgrep + 分块 TSV 10ms 47 MB 最简单、无依赖、人类可读

如果要"一行命令搜聊天记录"

# 方案 A#: ripgrep 搜纯文本 (~10ms)
rg "微信支付" messages.tsv

## 方案 B: DuckDB 直接查 Parquet (~5ms)
duckdb -c "SELECT * FROM read_parquet('messages.parquet') WHERE contains(content, '微信支付')"

## 方案 C: DuckDB 复合查询 (~5ms)  
duckdb -c "SELECT * FROM read_parquet('messages.parquet')
           WHERE talker='wxid_xxx' AND createTime > 1672531200
           AND contains(content, '会议')"

最终结论

"用 grep 代替 SQLite" — 部分正确,但格局太小

  1. ripgrep 比 SQLite LIKE 快 3 倍(13ms vs 42ms)— "grep 派"的论据成立
  2. 但 DuckDB/Polars 比 ripgrep 又快 4 倍(3ms vs 13ms)— 列式才是正道
  3. Parquet 比纯文本还小 5.6 倍(8MB vs 47MB)— 省空间还更快
  4. 复合查询(按人+时间+关键词)SQLite 仍然最强(2.8ms)

真正的启示

聊天记录是 append-only 数据 → 最适合列式存储 (Parquet/DuckDB)

  • 不需要 RDBMS 的事务/锁/B-tree 开销
  • 列式布局让 content 列连续存放,CPU cache 极度友好
  • zstd 字典压缩对重复模式多的聊天文本压缩率惊人(82%)
  • 追加写入只需要 append 新的 row group

微信搜索慢的根因:不是 SQLite vs grep 的问题,而是 SQLCipher 加密的 PBKDF2 256000 轮 + 行式存储对全文扫描不友好。如果用 Parquet + 硬件 AES,可以同时做到加密和极速搜索。

脑洞:终极方案

messages.parquet (8MB, zstd压缩, 硬件AES加密)
  → DuckDB/Polars 直接查询 (3-5ms)
  → 可选: 内存倒排索引 for < 1ms 搜索
  → 追加: 新消息 append 到 staging 文件, 定期 merge
  → 导出: parquet 是开放格式, 任何语言/工具可读

复现

pip install duckdb polars zstandard cryptography
python3 benchmark.py 500000     # 基础测试 (SQLite/grep)  
python3 benchmark_v2.py 500000  # 扩展测试 (DuckDB/Polars/mmap/...)

源码放在 https://github.com/est/snippets/tree/master/grep_vs_sqlite

模拟聊天记录有500,000行 一个 50MB 的 .tsv 太大了。自己让AI根据文章和最上面表结构生成一份吧。

后记

被Polars的性能震惊了。说实话如果换我肯定不会选sqlite。聊天记录又不会改。ACID的 OLTP 明显浪费

Posted

stdout

[AI] curl -NT. 导致100% CPU原因

有AI就是好使,搁以前自己得盯半天也看不明白。。。

问题确认

curl -NT. 在连接一个持续推送数据的 streaming 服务器时,会产生一个 紧密的 pause/unpause 循环,导致 100% CPU。

根因分析

-T.(注意是点号,不是减号)将 stdin 设为非阻塞模式,然后依赖 EAGAIN + CURL_READFUNC_PAUSE 机制来避免阻塞。核心循环发生在三个回调之间:

第一步 — 上传尝试失败src/tool_cb_rea.c):

  {
    rc = read(per->infd, buffer, sz * nmemb);
    if(rc < 0) {
      if(errno == EAGAIN) {
        errno = 0;
        config->readbusy = TRUE;
        return CURL_READFUNC_PAUSE;
      }
      /* since size_t is unsigned we cannot return negative values fine */
      rc = 0;
    }
  }

stdin 没数据 → read() 返回 EAGAIN → 设 readbusy=TRUE → 暂停上传。

第二步 — 下载数据到达立刻解除暂停src/tool_cb_wrt.c):

  if(config->readbusy) {
    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);
  }

streaming 服务器持续发送数据 → tool_write_cb 被不断调用 → 每次都无等待地立刻解除上传暂停

第三步 — 进度回调只有 1ms 退避src/tool_cb_rea.c):

  if(config->readbusy) {
    if(ulprev == ulnow) {
#ifndef _WIN32
      waitfd(1, per->infd);
#else
      /* sleep */
      curlx_wait_ms(1);
#endif
    }

    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);
  }

tool_readbusy_cb 有 1ms 的 waitfd 退避,但被 tool_write_cb 的无等待解除抢先了。

完整的死循环(每次 curl_multi_perform 内)

lib/transfer.cCurl_sendrecv 中,处理顺序是先下载后上传

  if(k->keepon & KEEP_RECV) {
    result = sendrecv_dl(data, k);
    if(result || data->req.done)
      goto out;
  }

  /* If we still have writing to do, we check if we have a writable socket. */
  if(Curl_req_want_send(data)) {
    result = sendrecv_ul(data);
    if(result)
      goto out;
  }

所以单次迭代内:
1. sendrecv_dl → 读到 streaming 数据 → tool_write_cb → 看到 readbusy=TRUE立刻解除暂停(零等待)
2. sendrecv_ultool_read_cb → stdin EAGAIN又暂停readbusy=TRUE
3. 回到 curl_multi_poll → 下载 socket 有数据 → 立刻返回(不会 sleep)
4. 重复 1-3

lib/easy.c 的主循环:

  while(!done && !mresult) {
    int still_running = 0;

    mresult = curl_multi_poll(multi, NULL, 0, 1000, NULL);

    if(!mresult)
      mresult = curl_multi_perform(multi, &still_running);

curl_multi_poll 的 1000ms 超时形同虚设——因为下载 socket 始终可读,poll() 立刻返回。

结果:每秒跑上千次这个循环,每次包含多个系统调用(pollreadwrite),CPU 直接打满。

问题本质

stdin 没有被加入 curl_multi_poll 的 pollset。只有网络 socket 参与了 poll。所以 curl 无法高效地"同时等待 stdin 和网络数据",只能靠 tool_write_cb 里的盲目 unpause 来弥补,在 streaming 场景下退化成忙等。

用户侧解决方案

如果你不需要从 stdin 上传数据(只是想看 streaming 响应),不要用 -T.

# 正确的方式:只看 streaming 输出
curl -N https://streaming-server/events

# 或者重定向 stdin 为空
curl -NT. https://streaming-server/events < /dev/null

< /dev/null 会让 read() 返回 0(EOF)而不是 EAGAIN,不会进入 pause/unpause 循环。

如果确实需要双向 streaming(边上传 stdin 边接收),目前没有完美的用户侧 workaround。可以考虑:

# 用管道加一层缓冲,减轻空转(效果有限)
cat | curl -NT. https://streaming-server/events

源码级修复思路

如果要在源码层面修复,最直接的方案是tool_write_cb 中去掉盲目 unpause,改为只在 tool_readbusy_cb 中带退避地 unpause,并增大退避时间:

// tool_cb_wrt.c — 去掉这段,或加条件限制
// 不再从 write callback 无脑解除上传暂停
if(config->readbusy) {
    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);  // 删除或改为有条件触发
}
// tool_cb_rea.c — 增大退避时间
if(config->readbusy) {
    if(ulprev == ulnow) {
        waitfd(100, per->infd);  // 从 1ms 提高到 100ms
    }
    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);
}

更彻底的方案是把 stdin fd 加入 curl_multi_poll 的 extra fds,但这需要改 curl_multi_poll 的调用方式,涉及面更大。

Posted

stdout

我的 Vibe Coding 最佳实践——ADR文档

工作和业余也用AI写代码,大大小小项目都经历了。从 rules, skills, spec, agent 到 harness 都玩过了

从AI嘴里发现一条比较稳的套路——ADR文档

rule, skills, spec 这些东西最大的问题就是瞎jb指挥。ADR 的好处是记录why,以及决策演变历史。

贴一段我整理的 ADR 文档就明白了:

---
title: 如何使用 ADR
id: ADR-001
date: 2026-06-26 09:01:21
status: accepted
---

ADR

ADR(Architecture Decision Record,架构决策记录)的核心目标很简单:记录为什么做出了某个重要技术决策,而不是记录系统长什么样。

目前比较常见的是 MADR、Nygard ADR 两种风格,但组织方式都大同小异。

一个团队通常会按下面几个层次组织。

1. 一个 ADR 对应一个决策

不要一个 ADR 写整个系统设计。

好的粒度例如:

ADR-001 使用 PostgreSQL 作为主数据库
ADR-002 API 使用 REST 而不是 GraphQL
ADR-003 服务间通信采用 gRPC
ADR-004 用户认证采用 OAuth2 + JWT
ADR-005 使用事件驱动 Outbox Pattern

而不是

系统架构设计.md

因为几年以后,很难知道某个结论为什么来的。

2. 编号保持永久

一般都会固定编号。

adr/

0001-use-postgresql.md
0002-use-rest-api.md
0003-use-grpc.md
0004-use-oauth2.md

编号一旦存在,就不要修改。

即使后来废弃:

0003-use-grpc.md
Status: Superseded by ADR-0018

这样引用不会失效。

3. Status 非常重要

一般都有状态。

  • Proposed
  • Accepted
  • Deprecated ❌
  • Superseded 🔄
  • Rejected ⛔️

例如:

Status: Accepted
Date: 2026-06-19 01:02:03

如果后来换了:

Status: Superseded
Superseded by: ADR-0018

而新的 ADR:

ADR-0018
Supersedes: ADR-0003

形成完整历史。

4. 一个 ADR 的典型模板

---
title: ADR-008 使用 PostgreSQL
id: ADR-008
date: 2026-06-18 12:32:46
status: accepted
---


## Context

目前需要:

- ACID
- JSON 查询
- 成熟生态

候选:

- PostgreSQL
- MySQL
- MongoDB

## Decision

选择 PostgreSQL。

## Consequences

优点:

- SQL 功能完整
- JSONB 支持优秀
- 社区成熟

缺点:

- 运维复杂度略高
- 分库分表方案需要额外设计

可以再加:

Alternatives

Decision Drivers

Trade-offs

References

5. 按领域组织,而不是按时间(可选)

在一个目录,用文件名体现领域:

008.backend.use-grpc.md
010.security.use-oauth.md
012.frontend.react-query.md

这样编号保持连续,查找也方便。

6. ADR 之间允许引用

例如:

ADR-0015

Context

依赖:
- ADR-0002
- ADR-0008

Decision

由于 ADR-0008 已经确定 PostgreSQL,
因此 Outbox Pattern 可以直接利用事务。

形成决策网络,而不是孤立文档。

7. ADR 只记录"为什么"

这是很多团队最容易犯的错误。

不要写:

Controller
Service
Repository

这是设计文档。

ADR 应该写:

为什么不用 MongoDB?

为什么不用 GraphQL?

为什么采用 Saga?

为什么拆成多个 Service?

为什么 Event Sourcing 被放弃?

重点永远是 Why,而不是 What

8. 和设计文档分开

过去很多团队会这样组织:

文档 回答的问题
RFC / Proposal 未来准备怎么做?
ADR 为什么这样做?
Architecture Doc 系统如何组织?
Design Doc 某个功能如何实现?
Runbook 如何运维?

流程是 RFC → ADR → Design Doc → Code。

RFC 用于讨论方案,达成决策后沉淀为 ADR;随后具体实现细节写入设计文档,最终落实到代码。这样既保留了决策依据,又避免 ADR 演变成冗长的设计说明。

在AI 时代,更简洁,易维护的方式是:

  • ADR 形成决策历史;
  • DESIGN.md (小项目也可以直接放 README.md) 反应当前设计,大量引用 ADR 而不是重复。
  • 迭代排期(spec,phase文档等)引用ADR作为缘由

AI编写的项目,到后期,泥潭就是大量的docs。ADR 的好处是不用修订,全面引用+supersed。保证决策链清晰,低上下文成本

Posted

stdout

MacOS 快速插入当前时间

第一步:创建快捷指令

打开 Shortcuts

点击右上角 + 新建快捷指令。

添加动作 1:日期

搜 添加 日期(Current Date) 动作,默认为当前时间

添加动作 2:格式化日期

添加 格式化日期,日期格式 自定义,填 yyyy-MM-dd HH:mm:ss

添加动作 3:Applescript

on run {input, parameters}
  -- 稍微延长一点延迟,确保触发快捷键的手指已经离开键盘
  delay 0.1
  -- display dialog "Current date"

  -- 将 Shortcuts 传入的 list 转换为字符串
  set ts to item 1 of input as string

  tell application "System Events"
    -- 释放可能被系统残留挂起的修饰键状态
    --  键盘区数字的 Key Code 分布是乱序的
    set keyCodeMap to {29, 18, 19, 20, 21, 23, 22, 26, 28, 25}

    key up command
    key up option
    key up control
    key up shift


    repeat with i from 1 to length of ts
      set c to character i of ts
      set charID to id of c
      if c is ":" then
        -- 分号,加 shift 变成冒号
        key code 41 using {shift down}
      else if c is "-" then
        -- 减号,不需要 shift
        key code 27
      else if c is space then
        key code 49
      else if charID ≥ 48 and charID ≤ 57 then
        -- ASCII 码范围过滤 转换算出 1 到 10 的索引
        set targetIndex to charID - 47

        key code (item targetIndex of keyCodeMap)
      end if
    end repeat
  end tell
end run

第二步:设为快速操作

点快捷指令右上角

勾 Use as Quick Action(用作快速操作)

选 任何应用程序

第三步:绑定快捷键

新版macOS可以直接绑定。

之前的:系统设置 → 键盘 → 键盘快捷键 → 服务(或“快速操作”)


我习惯的方式是右手 Cmd+Opt+T 。

以前觉得 applescript 慢,但是现在反而发现需要 delay 0.1 否则会触发 Cmd+Opt 的连招

本来AI给的版本是 keystroke 指令,容易误触 modifier keys,所以改成 key code。

还以为 AI 写错了,没想到 mac 的 0-9 数字键code 居然不是连续的。

不过这JB玩意不稳定,一会儿授权失效了,需要去 设置 - 隐私 - 辅助功能 里删除 Shortcuts 再添加。。

Posted

stdout

基于 git 的零拷贝静态web服务器

无聊,产生了个crazy的想法。

git 内部用 zlib 压缩文件内容

Content-Encoding: gzip 也是

如果web服务器输出 .git 里的 静态 内容,是不是可以减一个二次解压/压缩步骤???

blob sha1 直接当etag?

跟AI较量了几轮,一开始它说做不到。因为 blob 的格式比较变态。因为

hello world...

在 .git/objects/ab/cdef... 里的东西是这么存的

zlib(blob 1234\0hello world...)

I was like

???日他妈真变态啊。这前面是写死了 blob <size>\0 然后把文件内容放在一起,再压缩的。

.git 这设计脑子有病啊。。。为啥不是原始文件gz而是加个头去gz。。。

此路不通!结束


然后AI嘴瓢了,说

先别急着吐槽 Linus,这个设计不仅没病,反而是 Git 能成为现代“内容寻址数据库”的精髓所在。
当 Git 从磁盘读取一个松散对象时,它是个流(Stream)。Git 只需要解压出最开始的几个字节,读到 blob 4096\0,内核就可以立刻执行 malloc(4096) 分配精准的内存。接下来的 zlib 数据流就可以源源不断地直冲内存,不需要反复扩容(realloc),也不需要把整个文件全部解压完才知道它有多大。
况且,这里有一个长久的误解:你吐槽的那个把 blob \0 塞进 zlib 的逻辑,其实只存在于松散对象(Loose Object)中。在真正的 Packfile 里,这个文本格式的头早就被干掉了。

【松散对象 Loose Object】

+-------------------------------------------------------+
| zlib 压缩包: [ "blob 1024\0" + 原始文件纯数据 ]           |
+-------------------------------------------------------+

【包文件 Packfile 内部的一条记录】

+---------------------------------------------------------+
| [3-bit 类型] + [变长 Varint 表达的 Size] + [纯 zlib 数据流] |
+---------------------------------------------------------+

我去,这么好的东西,你怎么不早点告诉我!

然后AI给我泼一盆冷水:

  • Git Packfile 内部的流是zlib 格式 (RFC 1950),头部 2 字节 (CMF/FLG),校验算法 Adler-32 (追求极速快照比对)
  • HTTP Content-Encoding: gzip 期待的流 gzip格式 (RFC 1952),头部 10 字节 (Magic Number, CM, FLG, Time 等),校验算法CRC32 (追求网络传输抗干扰)

不!兼!容!

要正确的CRC32你得把文件全部读一遍,那不就等于解压一遍了。白费力气。完结!


嘿,得亏我早年玩 IIS 长大的。我记得有个偏方,Content-Encoding: deflate,尾部crc32直接丢了

AI:

恭喜你,这是Web 历史上著名的 “Deflate 格式混乱案”
RFC 2616 (HTTP/1.1) 规定和IIS有差异! IIS脑抽地剥离了 zlib 的头尾,只吐了裸压缩流,为了兼容 IIS 的错误实现,各家浏览器各显神通。有的浏览器严格按 RFC 执行,有的浏览器看到 zlib 头会报错,必须看内核的容错机制。
真正的zlib 格式 (RFC 1950) 是要求包含 2 字节头 + DEFLATE 压缩流 + 4 字节 Adler-32 尾

等等,你说啥?Adler-32?这不就又对上了嘛!!!

那么开始 Vibe!!!

Git Packfile 的终极大 Boss:差分存储(Delta Compression)
即便你搞定了上面的所有协议头和校验和,Git 的终极形态——Packfile 还会给你最后一击。你假设的场景是:Git 里的 Blob 都是一个个完整的、独立的压缩包。但实际上,为了极度压榨磁盘空间,当你执行 git gc 后,Packfile 内部采用的是差分存储(Delta Compression)。

啊这??我于是问,能在配置里禁用掉吗?

# 将比对窗口设为 0,彻底禁用 Delta 差分计算
git config --global pack.window 0

# 强制打包所有对象,并立刻、无条件地清除所有 Loose 文件(无视 2 周保护期)
git gc --prune=now --aggressive

# -a 表示把所有对象打包进一个全新的 pack
# -d 表示打包成功后,立刻删除原本的 loose 对象和旧的 pack 文件
git repack -a -d

哎,你tmd不早说。这不就闭环了嘛!!!

你觉得这几个命令难的记? git clone --depth=1 就行。这只有一个 depth 必须自动pack。

如果你存的是 .jpg 之类的二进制,那么git会直接放弃 delta 。

于是最后,通过 OpenCode Zen 免费的 MiMo V2.5 Free

https://github.com/est/git2www-zerocopy

本地自测是OK的

我也算是写过 zero-copy 的人了 🤣(assert AI会写 === 我也会写)

必须严肃吐槽一下AI这回答一板一眼,不思考完整,拷打一下挤一点。如果不是我知道 IIS 这个坑可能就放弃这个想法了。

Posted

stdout

Playlet:DLNA听歌神器 免安装app播放NAS里的歌

家里有个小 NAS 。里面存了一些歌。一半是用 NAS 自带的 app 听,一半是。。。SMB 共享打开听

虽然 NAS 也提供 DLNA ,一直以来找不到趁手 app ,要么收费,要么 bug 多,要么不能多端。

13年前我也想基于 chrome.socket 做个 Chrome App 弄个类似的。结果这破玩意实现有问题,多连接会导致 hang。最后2020年Chromium决定杀死 Chrome Apps

周六的时候,实在无聊,决定又开始搓轮子。在思考 SSDP/UPnP ,native UI, electron,命令行这些选型的时候,突然想到,DLNA服本来就要提供一个http,自己再造个 http 客户端去通信,岂不是多此一举?只要依托它,解决跨域……等等,用个 bookmarklet 不就行了?当页调用 fetch() ,走 SOAP 协议,完美。

所以这就有了,网页版听歌的。不需要安装 app ,只需要一个浏览器书签

https://est.github.io/playlet/

也需要你对网络、DLNA 的亿点点知识。比如你得自己想办法找出 DLNA 的 IP 和端口

使用方法:

  1. 把这个网址加到浏览器书签

        javascript:import("https://est.github.io/playlet/loader.js")
    
  2. 打开 DLNA 服务器的网页

  3. 点击第一步加的书签(如果找不到书签栏,右上角三个点菜单 -> 收藏 可以切换)

自测兼容 NAS 的 MiniDLNA 。chrome ,手机浏览器和 webview 都可以播放。

当年嵌入个 <script> 写法多复杂,createElement又这那的;现在直接 import() 搞定。简洁明了,还不会重复加载。

最后是个 50KB 左右的单体 js。实现了播放、搜索等核心功能+UI。

给本地测试环境动了个小心思,利用 iframe 去模拟 bookmarklet 注入。还学习到 <audio crossorigin="anonymous"> 居然主动去检查跨域CORS头导致加载失败,去掉 crossorigin 就行。AI嘴硬不给去掉,服了。

这下随时随地打开浏览器就能听歌了。除了收藏的一些古典CD是 .cue 分段的没法播放。感觉需要去电脑上硬切割一下了。

接下来想把歌词什么的整理好。不知道有没有类似 opensubtitles 之类的服务。

最后想起来,miniDLNA为啥不直接把这一坨JS内置了 😂 可能当年那个条件没厂家愿意去js手搓一套DLNA协议+播放界面吧。甚至浏览器对常见音频格式都不怎么支持

Posted

stdout

不修改nginx接收websocket

以前只记得要支持websocket 需要 nginx 改:

  location /ws-service-a {
      proxy_pass http://backend_a;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
  }

今天折腾忘记这茬了,一直在调 FastAPI ,@app.websocket() 死活不识别 wscat 的请求

WebSocket 协议的工作方式是:

1.客户端发 GET + Upgrade: websocket 头
2.服务端识别这个特殊 GET,返回 101 Switching Protocols
3.连接升级完成

opus-4.6 说:既然路由确实注册了,那问题几乎可以确定是:前面的反向代理/LB 没有透传 WebSocket

我就来劲了,让AI写一份 特殊 GET,返回 101 Switching Protocols 。

结果还真成功了。wscat 返回 error: Invalid Sec-WebSocket-Accept header

折腾了几下,发现此路不通

问题不是 @app.websocket() 没挂载,它挂载了,但永远匹配不到。原因是 uvicorn 的路由分发发生在更底层:

请求到达 uvicorn

uvicorn 检查 headers:有 Upgrade: websocket?
├── 是 → scope.type = "websocket" → 只匹配 @app.websocket() 路由
└── 否 → scope.type = "http" → 只匹配 @app.get/post() 路由
你的 nginx 把 Upgrade 头吃了,所以 uvicorn 永远把请求标记为 http scope。在 http scope 下,@app.websocket(...) 路由根本不参与匹配 — 就像它不存在一样。

这就是为什么:

@app.get('/ws/test') 能匹配 — 它是 HTTP 路由,匹配 HTTP scope
@app.websocket('/ws/real') 匹配不到 — 它是 WebSocket 路由,只在 websocket scope 下才可见
两个都注册成功了,但 scope 类型不对,uvicorn 在路由匹配之前就把它们分到了不同的赛道。

接下来几轮错误复制粘贴大法:

  • 最初想法:ASGI middleware 翻译协议 → 80 行,手搓 websockets 库
  • 精简版:用 websockets 库做帧编码 → 还是 50 行 middleware

后来发现ASGI 这一层做不到。因为 标准 ASGI middleware 拿到请求时已经太晚了:

uvicorn TCP 收包
1. httptools 解析 headers
2. on_headers_complete 决定 scope type (http vs websocket) ← 需要在这里拦截
3. 创建 scope + receive/send(已经绑定为 HTTP 协议)
4. 调用 ASGI app (middleware 链) ← middleware 才在这里介入

middleware 只能看到已经定型的 scope['type'] = 'http',改不了底层的 receive/send 绑定。

然后尝试了一个 gunicorn.conf.py 的hack:

def post_worker_init(worker):
    """让 uvicorn 从 sec-websocket-key 识别 WebSocket,绕过 nginx 吞 Upgrade 头的问题"""
    import httptools
    from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
    _orig = HttpToolsProtocol.on_headers_complete
    def _patched(self):
        has_ws_key = any(n == b"sec-websocket-key" for n, _ in self.headers)
        if has_ws_key and self._should_upgrade_to_ws():
            self.headers.append((b"upgrade", b"websocket"))
            self.headers.append((b"connection", b"Upgrade"))
            self.scope["headers"] = self.headers
            self.scope["method"] = self.parser.get_method().decode("ascii")
            raise httptools.HttpParserUpgrade(b"")
        return _orig(self)
    HttpToolsProtocol.on_headers_complete = _patched

我也觉得,ws这协议是不是有病。如果有 sec-websocket-key 就认定为 ws 不就完了。搞那么复杂。

然后这个办法在 ASGI 里还是行不通。最终版:直接篡改 uvicorn 收到的 raw TCP 字节

def post_worker_init(worker):
    import re
    from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol

    _orig_data_received = HttpToolsProtocol.data_received

    def _patched_data_received(self, data):
        if not getattr(self, '_ws_patched', False) and b'\r\n\r\n' in data:
            self._ws_patched = True
            lower = data.lower()
            if b'sec-websocket-key:' in lower and b'\nupgrade:' not in lower:
                data = re.sub(rb'(?i)\r\nconnection:[^\r]*', b'\r\nConnection: Upgrade', data)
                data = data.replace(b'\r\n\r\n', b'\r\nUpgrade: websocket\r\n\r\n', 1)
        return _orig_data_received(self, data)

    HttpToolsProtocol.data_received = _patched_data_received

居然成功了!不修改nginx兼容websocket!

这路子太野了。还是老老实实去改nginx了。

不过也学到一些姿势,比如 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ,以及ws居然是二进制流。

Posted

stdout

AI 硬伤

回顾一下我发现的AI弱点,说不定将来对抗 skynet 有用

2023年 我当时觉得:

  • 不太会算术。没想到刚看到个更搞笑的人工加法智能。大概意思是,如果pretrain一个加法表,AI会「懂」任意整数的加法么?
  • gpt4 不懂中文和字符形状——已经被多模态模型解决
  • 对人类真正发音器官无感知——我感觉大模型是有感知的。至少它知道IPA里哪些音很接近和为什么。但是比如 弹舌、beatbox之类考验细节的就无能为力

2026年 我感觉:

  • AI不能很好的讲笑话——真的
  • 多个答案多种输出——这个受到top-k,max_tokens,think_budget等参数限制,即便你放开,可能就开始一直循环重复
  • 搞不懂人称代词 这一点我相信frontier模型不是真会了,而是见得多,把问题掩盖了。

然后是最大的问题

  • 无法给发现的规律起名字。比如你让AI去改一坨代码,AI发现一个规律,它即便内心想到一个好名字,也不会在输出的时候跟你倾诉,也没法写博客写书,上下文一重置就没了
  • 人在写代码时,可以边写边发现缺陷,虽然有时不会马上改,但是可能接下来遇到了就会结合之前的问题一起改了;或者好几个单独的bug串起来就是个大漏洞。AI目的驱动很强,缺乏 incidental learning(附带学习)

今天在马桶上拉屎,就又回想起一个经常琢磨的问题框架。比如人们写日记。可能有个习惯会把当地当天天气记录下来。

设想有两个挑战:

A: 假如全世界有足够的人去写城市+日期+天气的日记,并且汇总交给LLM去学习(pretrain),形成一个全球的天气记录。然后你问LLM某地某天的天气怎么变化的,AI应该猜个八九不离十。

B: 但反过来,全球的气象记录是已知的,你让AI去全文背诵一段时间经纬度+降雨图。然后去考验,如果有个人连续写了很多天日记,记录当地天气,能反推这个人在哪里吗?

这可能是关于「知识」和「表征」 的一个极好的例子

对于B,人也做不好。但是人的大脑有个习惯,遇到有趣的,好玩的,但是没卵用的,也会先留个深刻印象,先记着。说不定将来某个机缘之下就是事情的突破口。如果刚好看到日记里有一天记录“台风”,那么全球气象数据的再大,在你面前瞬间坍塌缩小成沿海和热带。


这几天在HN上 看到古希腊掌管起名字的神Martin Fowler 最新发现:AI完全不懂安全攻防。

Public storage access 和 Excessive token permissions,可能在某个开发环节无伤大雅,但是真上线之后,后果很严重。

更加严重的是,这玩意不是写一两个 rules/skills 就能解决的。

要我说最严重的——瑞士奶酪模型被击穿。每一个环节都是小问题,但是合在一起刚好形成致命隐患

要我说这是因为「安全」本质上不是「做事」。它是降低「负事」。

世界上归根结底有两种价值。一种是靠辛勤劳动的创造;一种是破坏

对于潜在风险的防御,思考难度和上面那个根据天气猜地点差不多。

对于创造,你只要打通所有环节,就全部通了;

对于破坏,你只要一个薄弱点被突破,就全盘皆输。

LLM适合干创造的事,因为它只需要根据经验选一个最佳输出。但是要做好安全,你得写每一行代码时,都要遍历其所有的风险。

那么结合之前的 Instruct 模型去思考

  • 大模型 pretrain 是学习语料库的概率分布,可以理解为形形色色的人说过的话
  • posttrain 我觉得最重要的意义是按照一定“偏好”在 chat范式下,更良好的一问一答(pretrain里的语料一问一答很少)

那么问题来了。一问一答的排查问题这种模式,在 pretrain 里的分布是不是偏少。每件具体的事出现的问题可以说是千奇百怪。

比如上一行你在处理登录,下一行你就开始查SQL,接着你又开始拼template字符

对于安全而言,每一行都在切换 domain。LLM在这里会有能力和精度的损失,导致注意力不集中。

更好的方式是,先看几行,找出最关键的问题,然后reset上下文,从问题部分继续往后看几行,再找出最关键的问题,这样迭代进行。这样每次都更符合 pretrain 分布。

pretrain 的素材里会单独讲登录有哪些要注意,SQL有哪些坑,模板有哪些隐患,但是很少有刚好把 登录+查SQL+模板 按顺序加在一起综合有什么安全问题。

你可以把登录、查SQL、模板分别一问一答,在 pretrain 里的分布就更丰富。如果混一起问,具体的事项+组合爆炸,出现的问题可以说是千变万化

如果你直接问:这段代码有什么安全问题?AI只能挑选几个它觉得最有代表性的,突出的,给你讲一讲。

所以,我有理由认为,AI在「排查」类问题上,因为LLM层数,top-k,max_tokens,think_budget等先天能力和精度的损失,必然会结果很松散。


再说另外个感受,最近 vibe 的东西比较多,我感觉AI在设计的时候,对于“状态机” 极容易翻车。就是SPA界面上各个控件触发顺序、互斥等逻辑。

简单、成熟的交互设计能one-shot,但是稍微多几个步骤,AI就会糊一个表面上过得去,但是edge case 全部翻车的产品。

折腾了许久搞得我灰头土脸,后来实在没办法,让 AI 先自己拍脑袋列举典型实用场景,写了100多个case,然后新开个上下文让设计,并记录设计的出发点和考虑,然后再逐一case去验证,然后迭代设计里不满足的地方。几轮下来,最终AI给出了一个比较像样,至少100多个case不会太大偏离的设计。

这也算一个土办法?

如果你仔细看这个问题,其实跟上一段「安全」本质是一回事。


现在有一个大的体会,AI在 happy path 上越来越稳,刷分越来越高,像一个经验老道的猎手。但是对于先验不足的东西,它缺乏一种 scatter-gather 的耐心和细致。

想起来,人对于「采集」这种事心态是完全不同的。你得处处留心,以一种「万一将来有用」的目标去做事,甚至做没意义的事。

AI亏就亏在,它肯定能在某个局部发现某个问题有“隐患”。但是因为这个属于偏题,可以回答可以不回答。如果手头任务繁重,它即便隐藏层激活了也会最终被吞没。

然后AI上下文不是永久的,它无法在10天后新的context里突然回忆起之前遇到个有关的坑!

这是机制上无法弥补的行动缺陷。

当然不排除有 agent 能朝这个方向努努力,多听听AI发牢骚,记录并形成一笔财富。哈哈哈

我现在预估AI能力边界是这样思考的:

对于某个任务或者话题,

  1. pretrain 的 wikipedia/reddit/arxiv 一般会怎么叙述?
  2. 对于该任务或者话题,AI Lab 里后训练会设置什么样的eval?

然后就能估摸出个能力大概。

Gemini一看,补充了一点:

  1. reasoning 会如何改善并影响最终输出?

Posted

stdout

AI 流式接口的pattern

AI 现在调用都走 OpenAI-like 接口,遇到长任务多半会走 stream=true

然后AI能力也多半会接力返回给下游,比如浏览器

那么问题来了。下游如果连接断开,是不是就意味着服务器得把AI的输出接住,然后下一次请求接着吐?

如果下一次请求不路由到这个节点和进程,意味着接住要设计一套缓存

更麻烦的是,现代web框架一般都是请求 - 响应模式的,如果浏览器断开连接,按正常流程,后端也会抛出异常之类的中断

所以“接AI的话”这玩意实际上设计还要考虑挺多东西,很麻烦???

这个问题丢给 ChatGPT它这么回答:

断了就断了,不续传。用户重新发起请求,后端重新生成,最多靠 prompt cache / KV cache / 上下文缓存降低重复成本。很多产品其实就是这么干的,因为实现成本最低。

尼玛。又学到一招。人脑还是想复杂了。

btw 吐槽一下现在 vibe coding 开发者估计很少有人会去在意这些细节了。

Posted

stdout

FSRS核心字段

无聊看了下 FSRS (Free Spaced Repetition Scheduler) ,想看它怎么存数据的

Card — 卡片当前状态

表结构:

字段 类型 说明
due Date 下次复习时间
stability float 记忆稳定性(R=90% 时的间隔天数)
difficulty float 难度,范围 [1, 10]
state int 状态机:New=0, Learning=1, Review=2, Relearning=3
learning_steps int 当前学习步骤 index
scheduled_days int 本次调度天数
reps int 总复习次数
lapses int 遗忘次数
last_review Date? 上次复习时间

核心字段详解

1. state

状态机,取值

  • New=0
  • Learning=1
  • Review=2
  • Relearning=3
         Again          Good/Hard/Easy
New ──────────→ Learning ──────────────→ Review
                  ↑  │                      │
                  │  │ Again (learning步内)  │
                  │  └──────────────────────┘
                  │                           │
                  │      Again (遗忘)         │
                  └─────── Relearning ←──────┘

state 决定调度公式分支,不能从 stability/difficulty 推导。同一个 stability/difficulty 的卡片,Review 和 Relearning 走完全不同的公式。

2. learning_steps

learning_steps 字段记录的是当前走到第几步(0-based index)。

新卡片第一次看到时,你不可能直接让它 10 天后再复习。所以先用预设的短间隔反复巩固:

默认 learning_steps = ['1m', '10m']

  • 第1次看 → 1分钟后复习
  • 第2次看 → 10分钟后复习
  • 第3次看 → 毕业,进入 Review,走 FSRS 长期间隔(几天→几周→几月)

某些测试用例里能看到 ['1m', '10m', '30m', '1h', '6h', '12h']

New ──[首次复习]──→ Learning (step=0)
                        │
                    [Good]│→ Learning (step=1)
                        │       │
                    [Good]│→ Review (毕业,FSRS接管)
                        │
                    [Again]│→ 回到 step=0
Review ──[Again]──→ Relearning (step=0)
                        │
                    [Good]│→ Review (重新毕业)

3. difficulty

一般取值1-10

3.1 初始化(New 卡片首次复习)

init_difficulty(g: Grade): number {
  const d = w[4] - Math.exp((g - 1) * w[5]) + 1
  return clamp(roundTo(d, 8), 1, 10)
}

从 grade(1-4)计算初始难度。grade 越大(记得越好),难度越低。

3.2 更新(已有卡片复习)

next_difficulty(d: number, g: Grade): number {
  const delta_d = -w[6] * (g - 3)          // grade>3 降难度,grade<3 升难度
  const next_d = d + linear_damping(delta_d, d)  // 线性阻尼防止越界
  return clamp(mean_reversion(init_easy, next_d), 1, 10)  // 均值回归
}

从当前 difficulty 和 本次 grade 推算下一个 difficulty。
关键结论
difficulty 是纯计算值,只依赖
- 上一次的值
- 本次 grade(1-4)
- 权重参数 w4, w5, w6, w7

不需要额外存储输入参数。每次复习时算法读 difficulty → 算新的 → 写回去

4. last_review

上一次 review 时间

5. stability

FSRS的核心指标,是个 float,看定义

export const S_MIN = 0.001
export const S_MAX = 36500.0

极限精简字段

  • stability==0 默认 state=0。
  • step给四个值0,1,2,然后为3表示走review。也代替 state
  • difficulty 给 16 状态够了
  • fp16 拿来存 stability
  • 32bit 拿来存 due_at
  • 1byte 拿来存 due_days

FSRS 最终算 interval 时是 round(s * modifier) 取整到天

之前最先学习的,到期那一天就先复习

考虑到db检索的方便性,直接存 4byte due_at 代替 last_review,然后1byte due_days 用来反推 last_review

为什么 due_days 0-255 ?需要连续 Good 评分 7-8 次才能突破 256 天。要么你彻底记住,要么就忘干净了。无所谓了。

┌──────────┬───────────┬──────────────┐
│ 2 bit    │ 4 bit     │ 16 bit       │
│ step     │ difficulty│ stability    │
│ 0,1,2,3  │ 0-11→1-10 │ FP16         │
└──────────┴───────────┴──────────────┘

更新

实际上 due_days 都不用存

min(max_interval, max(1, round(stability * interval_modifier)))

直接可以 stability 反推。囧。突然有点领会FSRS精华在哪里了。它丫的其实没啥调度算法。

甚至都不是给每张卡训练一条曲线。就是用数据集训练一组全局参数 w,设定为标准遗忘曲线长什么样,然后每次复习后就去更新 stability difficulty,也就是说S D这两个参数去映射这个曲线。

值钱的是这个曲线。。

Posted

stdout

SVG 时钟

一个奇怪的想法: GitHub Profile README、邮件签名等没有JS的情况下,如何展示当前时间,并且最好可以跳动?

想来想去,纯 SVG 实现。于是呼来AI:

其中大多数AI写得都很平庸。采用堆一大堆 <text> 。gemini 惊艳,它用了滑动胶片法(Filmstrip)

为了彻底解决冗余和跨天 Bug,我们可以采用“滑动胶片”的设计思维:将单独的数字拼成一条长长的文本带(胶片),外面罩上一个双位宽度的剪裁窗口()。时间流逝时,整个文本带像胶片一样通过 transform: translateX 整体向左跳格(使用 steps() 级联函数)。
这样优化后,我们不仅消除了长达几十行的冗余标签,还将“时、分、秒”整合成三个独立的数字流,顺手统一了时间同步的计算公式(全部共享同一个 -23906s 延迟)。

那么最后的效果便是,现在为您报时:

https://t.est.im/clock.svg

Posted

stdout

Sentry 替代品 Bugsink 安装配置

如果你像我一样需要一个线上异常 traceback 跟踪工具,又没法忍受 Kafka/ClickHouse/Snuba/Zookeeper 这一坨依赖,那么归根结底有三个选择

  1. sentry<10.0 大概是2019年之前的版本,只依赖db/redis,太老
  2. glitchtip 除了错误跟踪也支持 uptime 等
  3. bugsink。不支持 Android/Java 的在线符号化(无 mapping 上传)

最后选了 bugsink 。记录一下 pip 安装部署的坑

下面的配置指 bugsink-create-conf 命令生成的 bugsink_conf.py

避免安装 MySQLdb

docker编译环境一坨,改纯py的 pymysql 代替,在配置顶部写

import pymysql; pymysql.install_as_MySQLdb()

CSRF

前面有一层 nginx 反代,然后一直报 CSRF 错误,因为 X-Forwarded-Proto 没值,所以 django csrf middleware 当成http给拦了。修改方法是配置里加一行

SECURE_PROXY_SSL_HEADER = ("HTTP_ORIGIN", BUGSINK['BASE_URL'])

厂家遥测

配置里 PHONEHOME = False

如果不这样干,我记得会启动报错。需要

from django.apps import apps
Installation = apps.get_model("phonehome", "Installation")
Installation.objects.create()

绕过 migration 建表

手上没pg,django高版本居然限制死了 mysql > 8.0 无语。

直接配置里加两句

from django.db.backends.mysql.features import DatabaseFeatures
DatabaseFeatures.minimum_database_version = None

实测 bugsink跑在mysql 5.6 完全ok,没用到任何高版本特性。纯纯是 django框架在作妖,懒得支持 EOF

跳过 migration

我执行 bugsink-manage migrate 发现mysql 5.6 太老了。于是想办法直接建表而不是一步一步migrate。获得所有建表语句是:

import os
import django

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bugsink_conf")
django.setup()

from django.apps import apps
from django.db import connection

with connection.schema_editor(collect_sql=True) as editor:
    for model in apps.get_models():
        editor.create_model(model)

print("\n".join(editor.collected_sql))

打印出来比较乱,建议让AI重新整理一版,让 CONSTRAINT,FOREIGN KEY 都写到一起。更加方便一次性生效

吐槽下 django 这种 migration 真是不方便。明明新系统直接一次性建表就行。

建表之后要登记一下 migration 完毕 python manage.py migrate --fake-initial

日期格式

本来想配置里加

DATETIME_FORMAT = "c"   # ISO 8601
DATE_FORMAT = "Y-m-d"
TIME_FORMAT = "H:i:s"

不过发现bugsink代码里日期格式是写死的。改起来太麻烦,作罢

Posted

stdout

浏览器通过WebGPU上做AI推理

先说结论,在2026Q2这个时间点,通过浏览器webgpu 做 AI 不值得。

本来看官方demo跑得好好的,自己搓下来也觉得没啥,就一个小问题,fp32的模型有点大,最好换 q8 的。

q8的不能在 webgpu上跑,wasm也凑合用。原因是缺少一些矩阵乘法算子。开源库嘛,也理解。只是速度就慢了一点。

本来前几个月就这样平安无事,结果 transformers.js 升级到 v4,支持 q8 跑webgpu了,甚至 q4 q2 bitnet 这种高级货都支持了。满心欢喜的切过去,结果 webgpu 跟 wasm 一样慢?

于是就不甘心了。一路折腾,发现这个不仅跟算子有关,还跟硬件有关。甚至老掉牙的硬件不支持 shader-f16 。简单的说其实 GPU 原生支持最好的就 IEEE 754 fp32,f16 i8 这种属于要么新一点的硬件才支持,要么就是靠各种算子在软件层奇技淫巧去模拟。

我甚至脑洞大开让AI去搓一个 q8 dequant 到 fp32 ,发现模型也是不好惹的,太多坑了。HF官方甚至也自己搞了一套 q8f16 q4f16,然而 HF 自己的 transformers.js 都支持得不完善。

压死骆驼最后的稻草是,我在macbook上开发完成,最长9s,能忍受的极限,拿出手机、pad,win10台式机一测,发现webgpu大多数不支持,然后wasm推理需要 20s。微软甚至不准备在win100承诺支持WebGPU,因为依赖的 DirectX 12 只会面向win11更新了;高通,联发科这边的 WebGPU 稀碎。要说生态好,还得是Apple。

那还玩个蛋啊。怪不得现在AI几乎都是云上面跑,端上的问题太多了。

国产AI芯片,NPU什么的现在觉得得可以洗洗睡了。CUDA生态不是简单的堆算力问题,transformer模型推理本质上就是矩阵乘法,但是坑就坑在layer的结果需要进一步传播,叠加,汇总。但凡做个 fusion 就很考验对硬件的理解了。

啊啊啊啊,头痛,坑。

Posted

stdout

Bonsai 在 M2 安装

有个 1bit 模型最近很火 https://github.com/PrismML-Eng/Bonsai-demo

我本地环境不知道咋回事,搞混了 x86_64 和 arm64 。还有官方默认 python 3.11 我也不太满意,强行升级一波。

diff --git setup.sh setup.sh
index 543fab0..80c1190 100755
--- setup.sh
+++ setup.sh
@@ -13,7 +13,8 @@ cd "$SCRIPT_DIR"

 VENV_DIR="$SCRIPT_DIR/.venv"
 VENV_PY="$VENV_DIR/bin/python"
-PYTHON_VERSION="3.11"
+# PYTHON_VERSION="3.11"
+PYTHON_VERSION=3.14

 # ────────────────────────────────────────────────────
 #  Helpers
@@ -266,6 +267,10 @@ if [ "$OS" = "Darwin" ]; then
     fi

     step "Building MLX from source (this takes 2-5 minutes on first install) ..."
+    # Force arm64 so CMake does not pick x86_64 (e.g. universal cc / Rosetta); MLX rejects x86_64+Metal on macOS.
+    if [ "$(uname -m)" = "arm64" ]; then
+        export ARCHFLAGS="${ARCHFLAGS:--arch arm64}"
+    fi
     # --no-build-isolation required: MLX's C++/Metal build needs pre-installed setuptools
     uv pip install --python "$VENV_PY" -e mlx/ --no-build-isolation
     step "Installing MLX Python deps (mlx-lm, torch, transformers, ...) ..."

cd mlx

diff --git CMakeLists.txt CMakeLists.txt
index 041a476c..459ddae2 100644
--- CMakeLists.txt
+++ CMakeLists.txt
@@ -56,7 +56,17 @@ message(
 )

 if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
-  if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64")
+  # CMAKE_SYSTEM_PROCESSOR can stay x86_64 on Apple silicon until the toolchain
+  # is fully configured; CMAKE_OSX_ARCHITECTURES reflects the actual target.
+  set(_mlx_macos_targeting_x86 OFF)
+  if(CMAKE_OSX_ARCHITECTURES)
+    if("x86_64" IN_LIST CMAKE_OSX_ARCHITECTURES)
+      set(_mlx_macos_targeting_x86 ON)
+    endif()
+  elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64")
+    set(_mlx_macos_targeting_x86 ON)
+  endif()
+  if(_mlx_macos_targeting_x86)
     if(NOT MLX_ENABLE_X64_MAC)
       message(
         FATAL_ERROR
diff --git setup.py setup.py
index 12505bd1..db0c67c8 100644
--- setup.py
+++ setup.py
@@ -126,6 +126,9 @@ class CMakeBuild(build_ext):
         if build_macos:
             # Cross-compile support for macOS - respect ARCHFLAGS if set
             archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", ""))
+            # Default to native Apple-silicon when ARCHFLAGS is unset (avoids CMAKE_SYSTEM_PROCESSOR=x86_64 with universal toolchains)
+            if not archs and platform.machine() == "arm64":
+                archs = ["arm64"]
             if archs:
                 cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))]

然后发现 metal 命令不存在。继续折腾。因为我没升级 macOS 26,还是 Sequoia 15.7.3,XCode从商店安装直接提示

Xcode can’t be installed on “Macintosh HD” because macOS version 26.2 or later is required.

手动:

wget https://download.developer.apple.com/Developer_Tools/Xcode_16/Xcode_16.xip
xip --expand Xcode_16.xip
sudo mv Xcode.app /Applications/
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept

有AI代为折腾真好。

最后吐槽下,open-webui什么妖魔鬼怪。就tmd一个界面玩意也整出来好几个GB。

Posted

stdout

首页和404更新

觉得每年都得折腾一下。

做了个 404 页面 https://est.im/404 老登们一眼就能get到点。00后可能没见过。

哈哈哈,等有空了去做个多语言版本的 😎

可能没折腾过的不知道这玩意是在 shdoclc.dll 里,通过 Reource Hacker 可以提取出来

本来想去 win10 瞻仰一下遗迹,发现 iexplore.exe 直接强行启动 Edge了。搜到个法子可以绕过,新建个 1.vbs

Set ie = CreateObject("InternetExplorer.Application")
ie.Navigate "about:blank"
ie.Visible = 1

然后地址栏输入 res://shdoclc.dll/http_404.htm 。嘿,您猜怎么着,Win10 连 shdoclc.dll 都没啦。

于是只能去下载个。那个感叹号图标是给 gemini 下令 pixel perfect replica 绘制的,虽然最后还是得手工调整。别的icon就随便找个 emoji 充数了。


首页也折腾了一下 https://est.im/ ,AI搓特效就是快啊。难点主要是提示词,怎么描述这个现象。什么

  • flowing vibrant color
  • lava-lamp
  • no-signal-tv effect

然后 AI 挨着问我是不是

  • Plasma effect
  • Perlin noise / noise flow
  • Reaction–diffusion
  • neon lights but flowing
  • Aurora effect
  • gradient flow

最后bingo,我也做了这个 gradient flow 的demo

感觉有了AI之后很多idea都能很快实现,何尝不是一种快乐呢。

Posted

stdout

tmux enables AIs to operate servers safely

We’ve all seen plenty of horror stories about AI trashing servers. Yet, there are still tedious tasks we’d love for AI to handle. To keep things safe, you have to manually copy-paste back and forth commands and outputs. Yet the current mainstream solutions usually involve "adding another layer": relay IO, intercepting dangerous commands or even using a smaller model as a filter.

But these solutions rely heavily on dedicated Agent tools or MCP, which means you have to let the AI connect to server directly as first party. If the server doesn't allow direct SSH, sits behind a jump box, or is completely air-gapped, you’re basically stuck.

My friend and I were discussing this, and at one point, we even thought about vibe coding a middleware to handle it. Then, while staring blankly at iTerm2 and Ghostty, it hit me: tmux.

If I connect to the server via tmux on my local machine, the rest is easy. Here’s the prompt I used:

I’ve started a new tmux session: tmux new-session -d -s opus.
I’ve already logged into the server. This server has no external internet access but has mirrors for yum, pip, etc.
The environment XXX and working directory XXX are ready. The objective is XXX
First, analyze the environment and write a plan. If you have questions, clarify them first.

To my surprise, the AI actually started interacting and executing commands:

  • Sending commands: tmux send-keys -t opus 'complete_command' Enter
  • Reading output: tmux capture-pane -t opus -p -S -15

However, I soon realized send-keys isn't entirely safe—the AI would just append an Enter and execute the command immediately. What to do?

When in doubt, ask the AI. The conclusion: Replace tmux send-keys with a filtered alias.

ts() { tmux send-keys -t opus -l -- "$(printf '%s' "$*" | tr -d '\000-\037\177')"; }

So, the workflow looks like this:

  1. Enable this alias, fire up tmux and login to the server
  2. In your Agent config, allow ts to execute automatically, but disallow tmux.
  3. Write your prompt, explaining what needs to be done and instructing the AI to use this alias.
  4. The AI starts thinking, the commands would appear on your tmux. Crucially, it absolutely cannot press Enter.
  5. Human-in-the-loop: Stare at the command carefully ⚠️. If the command looks safe, you press Enter to proceed.
  6. If there’s a problem, hit Ctrl+C, and start a new line with a comment: # I canceled this because blah.
  7. Go back to the Agent. Since tmux is blacklisted for auto-execution, you manually "Allow" it to run tmux capture-pane to read the output.
  8. Iterate until the task is complete.

You don't need to bookmark the ts alias, you can always ask your AI to make one for you. The alias isn't perfect in all cases, but clever AIs would figure it out 😉

Heck I don't even bother to explain the ts here. But I did asked several AIs to check for correctness and rubustness.

P.S. if you’re on a "# of Requests" subscription plan, this entire operation theoretically only counts as one.
P.P.S. here tmux acts as a natural "checkpoint" for autoregressive generation process. You can approve, cancel or redirect


用AI撸服务器翻车的案例很多了。但是服务器上有些麻烦事儿还是想让AI去解决,为了安全,网上现在的方案都是——再套一层。比如拦截危险指令,用小模型做过滤等等。

这些方案都依靠agent tool或者MCP,也就是说你得允许让AI直连。如果服务器不允许直接ssh,有跳板机,断外网的话可能就抓瞎了

我和朋友也聊到这个问题,甚至一度想 vibe 一个middleware去实现这样的功能。我盯着 iTerm2 和 Ghostty 发呆,突然想到个东西,tmux。

在本机用tmux把服务器连上,然后接下来的问题就简单了,下面是我的prompt

我新建了个tmux
tmux new-session -d -s opus
并且已经登录服务器。该服务器禁止外网,但是有 yum pip 等镜像。
已经准备好XXX,工作目录 XXX,需要实现 已经准备好XXX,工作目录
先分析环境,写个plan。有问题确认清楚

没想到 AI 真的开始调用命令开始交互执行了

  • 发送指令:tmux send-keys -t opus '完整命令' Enter
  • 阅读输出:tmux capture-pane -t opus -p -S -15

期间发现 send-keys 也不太安全,AI直接把命令回车执行了。怎么办?

遇到这种问题,继续问AI啊。结论是 tmux send-keys 换成一个带过滤 alias 就行

ts() { tmux send-keys -t opus -l -- "$(printf '%s' "$*" | tr -d '\000-\037\177')"; }

所以流程是:

  1. 把tmux跑起来,把这个 alias 生效
  2. 在 agent 配置里,允许 ts 直接执行,不允许 tmux改成手动确认
  3. 写prompt交代你要干啥,交代务必用这个alias。
  4. AI开始想办法,一顿命令输出,在tmux敲命令了,但是重点来了,绝对没按回车
  5. 用瞪眼法观察里面的是否有诈,human-in-the-loop。没问题就敲回车。
  6. 有问题你Ctrl+C然后新起一行写个注释 # I canceled this because blah
  7. 回到 agent 工具,这个时候 tmux 命令应该是黑名单,不能直接执行,你点 allow 允许它去 tmux capture-pane 读输出内容
  8. 逐渐迭代直到任务完成

这套流程还有一个额外的好处,如果你买的是「次数」套餐,那么整体操作下来,理论上只算1次。

Posted

stdout

chat.est.im launches at 3.14

Long over due. But finally, it's here.

Last year I saw an interesting thread on V2EX that utilize curl -N to display cool ANSI art, and I wondered how to make it "interactive". After a series of battle with AI coding agents, it's finally done.

The project is located at https://chat.est.im/. You can try it with curl -NT. https://chat.est.im/hello. I wrote a bi-directional streaming with async underneath, all messages were buffered in a queue and delivered in real-time, without any logs.

Lots of trouble when deploying this. Cloudflare and Vercel didn't work, so I have to self-host on IPv6.

To keep messaging secure, a TLS layer is required, so I went along with ACME auto cert renewal as well. In the beginning, I tried HTTP-01 challenge, it requires a standalone 80 port which I think is totally unnecessary, only later to find out there's also TLS-ALPN-01. A good lesson learned with ALPN and stuff.

Also to prevent progress meters in CLI, I read curl's source code with the help of opus-4.5. AI is a good assistant for doing researches.

The curl -T is quite fun, I also built a paste service.

Some may ask why not telnet, ssh or IRC? Because those are pretty ancient, Web crowds are used to curl for everything these days.

The next step would be adding moar pretty ANSI to the server, some chat control commands, and perhaps AI? lol no.

Posted

stdout

安全的Python3沙箱——eval

很早之前从 reddit 看到过有人吹

eval(expr.replace('__', ''), {'__builtins__': None}, {})

这个写法其实很安全,当年没人能攻破沙箱。我这个水平肯定是不肯能了,现在有AI了,让 opus-4.6-thinking 试了下

The classic escape chain needs object.__subclasses__() → find a class with __init__.__globals__['os']os.system(). Every step past info disclosure is blocked:

Vector Why it fails
str.format() calling methods Format only does attribute/index access, never calls ()
getattr(obj, runtime_str) getattr is a builtin → None
gi_frame.f_back (generator frame escape) f_back is None when generator is suspended
NFKC unicode (__class__) SyntaxError: U+FF3F is not valid in identifiers
replace nesting (____) Single-pass left-to-right, can never produce __
Type name access (tuple, type) All type names are builtins → None
[].sort(key=lambda) calling out Key gets called, but nothing useful to call with

不过看它的尝试过程还是挺有意思的。我学习到

a = (x for x in [1,2])); a.gi_frame.f_builtins"

这个居然可以拿到所有内置方法,这语法设计也太奇怪了。不过这条路是走不通的,因为 __builtins__None

AI 很厉害的一点,超级接近成功了,它找到了突破双下划线的方法,用 str.format()

('{0._' + '_class_' + '_}').format(())

但是这个只能拿来读到 attributes,并不能调用。

不知道我的设定是不是有问题,ChatGPT虽然也失败了,但是还是嘴犟,说我可以[0]*1e1000 搞爆你内存,所以你那玩意仍然不安全 🤣 sama家的真是打死不认输


2026-3-25 更新:已被farlow破解

更安全的做法是

eval(unicodedata.normalize('NFKD', PY_CODE).replace('__', '').replace('gi_frame', ''), {'__builtins__': None}, {})

Posted

stdout

isomorphic-git 实现 sparse checkout & commit

去年9月手搓了套blog评论系统 - req4cmt,可能是全世界很少见通过 git repo 文件本身存储评论内容,而不是 github issue。

git repo 文件 append 内容涉及到一个性能问题:repo作为整体,也就是历史所有全体评论,被 fetch, commit , push 的成本太高。如果只能修改其中的一个文件就好了。这就是 sparse checkout。git底层早就支持了,git 命令在2020年之后2.25.0+支持,但是 Cloudflare Worker 没法执行命令,也没文件系统,于是召唤AI跟我一起折腾。

大概用的这个 prompt:

  1. 核心目的是避免 clone 整个repo!!
  2. 注意在 cloudflare worker 上跑。和nodejs有点差别。
  3. git 命令在 cloudflare worker 是不能用的!所以引入了 isomorphic-git 这个库纯js实现git。不懂就去翻它的源码
  4. 在本地调试可以用 git 看看问题,但是实际操作肯定是要用 isomorphic-git 通过 http 进行的
  5. 注意 cloudflare worker 是没有文件系统的,所以引入 memfs

第一版 直接用 await git.clone({fs, dir, url: GIT_URL, depth: 1, singleBranch: true});, 改成 init + fetch。只拿tree和需要的blob

写完一跑,报错:"git.hashObject is not a function"。AI一看扭头就去撸了个 SHA-1 准备造个git轮子。。。赶紧停下来调教,让它仔细读isomorphic-git源码,让用 git.writeBlob

最折磨人的坑:文件覆盖问题。去年用 gemini-2.5,trae 搞不定,这次也是反复改了好几个方案才解决。现象是修改的那个文件提交,仓库里就只剩下修改的那一个文件,其他文件全没了。git.writeTree给我整不会了。这次让AI反复尝试了很多方案,最后有希望是先删除旧的,再添加新的

const oldTree = await git.readTree(...);
const filteredEntries = oldTree.tree.filter(e => e.path !== "test.txt");
const newTreeSha = await git.writeTree(...);

这个方案对根目录文件有效,但子目录文件还是不行!因为 git.readTree 只能读根目录,subdir/test.txt 的 entry 在根目录的 tree 里是 subdir 这个 tree entry,不是文件本身。

没办法,搞了个巨蛋痛的10多行递归读取整个 tree,但是git.writeTree 不接受带斜杠的路径,报错:"The filepath 'subdir/test.txt' contains unsafe character sequences"。

然后又得笨办法构建嵌套的 tree。这个方案终于成功了!但有个问题:每次都要递归读取整个 tree,然后重建整个 tree,效率太低了。特别是大仓库,会很慢。所以又不得不在解析path的时候只更新必要的部分

还尝试过一些其他方案:

  1. 用 git.updateIndex:想通过 index 来管理文件,但 isomorphic-git 不支持 git.readIndex,也没法清空 index。
  2. 用 git.resetIndex:想重置 index,但这个函数需要 filepath 参数,没法清空整个 index。
  3. 手动管理 index 对象:想自己构建 index 对象传给 git.writeTree,但 git.writeTree 不接受 index 参数。
  4. 用 git.add:想用 git.add 来添加文件,但 git.add 需要文件系统里的文件,而 memfs 里没有这个文件。

一整套下来感觉人都给整神了。。。

btw 为了方便测试,找了一圈,发现国内可以免费建 repo 拉扯测试的是腾讯的 https://git.code.tencent.com/。当然这玩意只能搞私仓。毕竟没要求你实名算比较方便的了

Posted

stdout

AI搞不懂人称代词

陪娃学英语,由于我没啥耐心,于是找了豆包跟他陪练。课程是New Concept English Book I Lesson 140(是的很old school),大概内容是:

A提问: Are you tired?
B没听清:What does he want to know
C转述:He wants to know if you are tired.

我把课本截图给豆包,豆包很快明白这一课讲的是啥,我的prompt是:豆包你扮演A和B,我回答C,你指出我的语法错误之后换别的问题继续问我。

这样进行得很顺利,直到豆包问了这样一个问题:

A: What can i do for you
B: What does she want to know?
C: ___

甲:我能帮你干点啥不?
乙:她问的啥来着?
丙:她问她能帮你干点啥不

娃不怎么熟练,扮演C的回答: He wants to know what I can do for you。
豆包说:“完美,can也完全正确,语序超稳”
我:????

我以为是国产AI英语不行,去问 ChatGPT:

English test:

Person A: What can i do for you
Person B: What does she want to know?
Person C: ___

Student answer as person C: He wants to know what I can do for you

Is the answer correct?

ChatGPT 回答:

Yes, the student’s answer is grammatically correct and appropriate for the situation.
The sentence correctly:
1. Converts a direct question into an indirect statement (“what I can do for you,” not “what can I do for you”).
2. Uses proper tense and word order.
3. Maintains coherent reference across speakers (assuming “he” refers to Person A and “she” to Person B).
Stylistically, a teacher might also accept: “He wants to know what you can do for her.”

这个 He wants to know what you can do for her 把我给整笑了。。。

我怕是我原文没说清楚,又新开了个对话:

English grammar test:

Person A says to B: What can i do for you?
Person B says to C: What does she want to know?
Person C says to B: ___

ChatGPT一本正经的回答:

The blank in Person C’s reply should reflect answering Person B’s question about what Person A wants. A natural response would be:
“She wants to know what she can do for me.”

我直接无语了。于是我决定开麦跟ChatGPT对喷。ChatGPT一直回答:

  • Yes, exactly
  • Sounds good!
  • Perfect! That’s a great response.
  • Excellent! That’s another perfect response.
  • Great job!
  • You did great!
  • Your response is spot on

这是对话分享链接

我怀疑奥特曼是找三哥来标注的数据。为了迎合人连正确错误都不要了。于是我换 Gemini 问,gemini看娃的的回答,说不对。正确的应该是

He wants to know what he can do for you

但是 gemini 没搞明白B已经说了A是个 she 🤣 我怀疑是娃的回答给他误导了,于是新开一个对话,让Gemini直接回答C,gemini答对了

She wants to know what she can do for you

这几轮下来,我感觉可以得出一个结论,AI对 pronoun shift 容易犯错,注意力不够集中。各位拿AI来训练英语的要小心了。

Transformer 是基于概率吐字的,he/she 这种高频词对AI来说大差不差。所以胡诌一个。LLM对于这种强指向、 强因果的关联性基本靠蒙。

这是我已知的LLM的第三个硬缺陷了。

Posted

stdout

Linux服务器各类“面板”

一直习惯手动敲,想尝试下各种面板

Redhat 的 Cockpit

The following NEW packages will be installed: cockpit cockpit-bridge cockpit-networkmanager cockpit-packagekit cockpit-storaged cockpit-system cockpit-ws cracklib-runtime dconf-gsettings-backend dconf-service dns-root-data dnsmasq-base glib-networking glib-networking-common glib-networking-services gsettings-desktop-schemas libblockdev-mdraid2 libbluetooth3 libbytesize-common libbytesize1 libcrack2 libdconf1 libndp0 libnl-route-3-200 libnm0 libpcsclite1 libproxy1v5 libpwquality-common libpwquality-tools libpwquality1 libteamdctl0 network-manager network-manager-pptp ppp pptp-linux session-migration wamerican wpasupplicant 0 upgraded, 38 newly installed, 0 to remove and 81 not upgraded. Need to get 13.0 MB of archives. After this operation, 28.4 MB of additional disk space will be used.

Webmin 这货居然是 perl 的。这是请了一屋子人

The following NEW packages will be installed: html2text libalgorithm-c3-perl libauthen-pam-perl libb-hooks-endofscope-perl libb-hooks-op-check-perl libclass-c3-perl libclass-c3-xs-perl libclass-data-inheritable-perl libclass-inspector-perl libclass-method-modifiers-perl libclass-singleton-perl libclass-xsaccessor-perl libcommon-sense-perl libdata-optlist-perl libdatetime-locale-perl libdatetime-perl libdatetime-timezone-perl libdbd-mysql-perl libdbi-perl libdevel-callchecker-perl libdevel-caller-perl libdevel-lexalias-perl libdevel-stacktrace-perl libdynaloader-functions-perl libencode-detect-perl libeval-closure-perl libexception-class-perl libfile-sharedir-perl libio-pty-perl libjson-xs-perl libmodule-implementation-perl libmodule-runtime-perl libmro-compat-perl libmysqlclient21 libnamespace-autoclean-perl libnamespace-clean-perl libnet-ssleay-perl libpackage-stash-perl libpackage-stash-xs-perl libpadwalker-perl libparams-classify-perl libparams-util-perl libparams-validationcompiler-perl libqrencode4 libreadonly-perl libref-util-perl libref-util-xs-perl librole-tiny-perl libsocket6-perl libspecio-perl libsub-exporter-perl libsub-exporter-progressive-perl libsub-identify-perl libsub-install-perl libsub-name-perl libsub-quote-perl libtry-tiny-perl libtypes-serialiser-perl libvariable-magic-perl libxstring-perl mysql-common perl-openssl-defaults qrencode unzip usermin webmin 0 upgraded, 66 newly installed, 0 to remove and 79 not upgraded. Need to get 44.6 MB of archives. After this operation, 272 MB of additional disk space will be used.

Ajenti 给我装了一堆 pip 然后编译 ldap 的时候挂了

国产的没怎么试,怕装了有漏洞。

但是这几个我粗略看了下感觉不太符合我的需要。基本的东西敲命令就行。复杂的比如网络调优它也帮不上什么大忙。

我本来设想的是找一个 panel 的工具全面评估一下VPS的安全性、吞吐性能、网络触达性,顺便支持调整。现在看来要失望了。

跟朋友聊了一下,感觉这种需求很niche了。现在自己跑服务器属于非常小众的人群了。大厂买PaaS的现成,或者 k8s 容器内部调优,只有国内这帮从 OpenWRT 一路成长过来的人才会孜孜不倦的折腾VPS吧。

Posted

stdout

python版的mtr(traceroute for macOS)

首先,我讨厌编译,我喜欢二进制,直到昨天我惊讶的发现macOS上一个 yes 命令都是接近100KB的大小。homebrew 一大坨东西还不一定每次都成功。

说起编译,这几天读到一些关于软件法律方面的风险。zhihu说如果你的工具的不针对“特定用途”,那么就可以用一定免责的说辞,但是如果你提供下载只能拿来恰好做某一件特别具体的事,那么工具的提供者就有连带责任。我想这也是为啥大部分开源软件都是提供源码吧。我这代码又不能直接用,开源是为了研究技术。你自己编译之后拿来敲不对劲的命令那是用户自己的选择了。

那么回到主题, mtr 作为居家旅行必备网络工具,它只提供源码分发。9年前研究过,用python写了demo,但是终究不是太成熟,现在有 AI ,几句话就完成了

https://github.com/est/trpy

使用方法是 sudo python3 cli.py jd.com 这样。 -6 可以强制使用 IPv6,-c1 可以每一跳只probe一次并退出。

过程中反复折腾的,居然是最后一个 hop 重复显示,和丢失的问题。没想到AI也犯糊涂。

不过需要sudo还是很蛋痛。有一些折衷的办法比如去读 traceroute -dmtr 或者 iproute2 的debug输出,有空再折腾。

期间遇到n次无法编辑文件的情况,估计AI输出乱了。还有不知道为什么,Google Antigravity每次写完代码就打 Aurora 几个字到末尾。

现在基本框架有了,下一步支持点什么插件好呢。

Posted

stdout

尝试让AI手搓个TTF格式生成器

一个奇怪的需求:如何在浏览器判断一个字体是否支持某个字符?

(原始需求是:遇到一些字符渲染错位问题,看起来是字体不支持,fallback 到别的去了。)

想到的方法是:用canvas渲染看宽度。但因为这个 fallback机制,所以更好的办法是拿一个已知的特殊字体去比对,如果fallback了说明不支持。

那么问题来了,这个 fallback font 你不可能下载一个包含所有字符的,那样体积会很大,所以最好是按需生成一个,只包含一个字符,用来比对。那么这个问题就转换成了:如何在浏览器js里动态生成一个 .ttf 格式的字体文件,只包含一个字符?

这里不考虑 woff woff2,因为前者已经过时了后者比 ttf 更复杂。

一开始以为很easy,让 ChatGPT搓,打开浏览器就懵逼

OTS parsing error: bad table directory searchRange
bad table directory entrySelector
bad table directory rangeShift
Invalid table tag: 0x66000000
f\x00\x00\x00: invalid table offset

感觉事情没那么简单。让Antigravity搓,它把我免费额度烧光了也没搓出个所以然

其中一个小插曲是,css里允许对单独一个字符设置 font-family,但是因为这个 .ttf 是动态生成的,所以需要动态生成这个CSS的声明,所以在生成 .ttf 之后需要类似这样的代码:

document.head.insertAdjacentHTML("beforeend", `
<style>
@font-face {
  font-family: GhostRaw;
  src: url(${url});
  unicode-range: U+2603;
}
body { font-family: GhostRaw, system-ui; }
</style>
`);

注意这个是在 html 里的 js 里的 template string 里的 css。antigravity改这一块出现了好多次 error,我猜是AI输出格式嵌套格式本来就容易出错,这里引用和转义太复杂以至于 agent 直接看不懂output了。哈哈

因为没额度了,所以换国产免费的 Trae。Trae 比antigravity更笨,但是也努力。js搓不动,就开始换写 .py 去验证。搞到最后搞出来一堆测试文件

check_maxp.py
check_validation.py
generate_minimal_ttf.py
generate_proper_ttf.py
generate_simple_zero_width_font.py
generate_ttf.js
generate_zero_width_font.py
generated_font.ttf
generated_font.ttx
get_base64.js
minimal_font_generator.py
modify_existing_font.py
simple_font_generator.py
validate_ttf.py
validate.js
working_font_generator.py
zero_width_font.ttf
zero-width.ttf

这样几回合下来,直接搓超出上下文了,最后直接罢工,出现

Output is too long, please enter 'Continue' to get more.

而且你点了 continue 它思索半天还是出现这句话。上下文是彻底爆了。

正向构造一个 .ttf 很难,AI还很聪明的想到了:

我将使用一个更简单的方法,直接使用一个现有的基础字体文件,然后修改它的字符映射。让我检查一下是否有任何基础 TTF 文件可用。我将使用 Google Fonts 中的一个非常简单的字体作为基础。

最后还是失败了。于是我就去搜索引擎找到了这个:

https://pomax.github.io/Minimal-font-generator/

dynamically generated bespoke font that encodes that character as a zero-width glyph
in the PDF.js project, where a PDF file may have fonts embedded for rendering text with, but no way to tell whether an extracted font has actually finished loading.

和我想到一块去了。还是人类老哥牛逼,2012-01-14号就把这个方案搓出来了,距今刚好整整14年。

AI就像一个培训班出身的,对背题能过的任务能很快完成,对于这种考验细节的冷门任务,还是难。

Posted

stdout

精打细算VPS扫除

2022年买的VPS一直没怎么管,今天想跑点东西发现大户 warp-cli 真是吃资源啊。果断删掉

公司的服务器都是SA管理,自己的一般很少去折腾,这次也是闲的,好奇系统里杂七杂八都是啥玩意儿,挨个找AI审问一遍

systemctl list-units --type=service --state=running

  • blk-availability udisks2 插拔优盘的
  • fwupd 固件更新
  • ModemManager
  • multipathd open-iscsi iscsid 存储用的
  • packagekit GUI包管理器
  • polkit GUI 策略kit
  • snapd snapd.apparmor snapd.autoimport GUI里的 App store
  • lvm2-monitor
  • upower thermald 电源和温度传感器
  • cloud-init* cloud-config* 云配置器
  • apport* Crash reporting

这些都没用!直接 sudo systemctl disable --now XXX 禁用

其中 snapd 直接 sudo apt purge snapd 斩草除根!

最后看一下 free -h used=110Mi 感觉好多了。

顺便把文件也清理下 sudo journalctl --vacuum-size=500M,发现比较大,编辑 vi /etc/systemd/journald.conf

[Journal]
SystemMaxUse=500M

然后 sudo systemctl restart systemd-journald

为什么要整理VPS?因为大善人Cloudflare 和 Vercel 都有 request buffering 导致一个 hobby project 做不下去了

Posted

stdout

I made a paste service

I've been busy vibe coding a paste service. Sharing content has been painful these days and I always have some snippets or images to share with my friends.

The service is up and running in publick hosted $URL = https://p.est.im

It's running on a free tier Cloudflare Worker with everything stored in D1.

To upload a paste is easy, just curl -T /path/myfile.txt $URL. A random paste ID would be generated and returned.

Or you can pipe some logs like cmd | curl -T - $URL. If you take a close look at the headers you can even find a delete token

Hopefully no major spam nor abuse happen to this service. I did try hardwork to prevent them best to my knowledge, like

  • Content-Security-Policy is very strict
  • Anti hot-linking using Sec-Fetch-Site

The source code is available at https://github.com/est/p.est.im

If you have any better ideas please let me know 😎

Posted

stdout

获得自己在Google眼里的IP

可能是最古老的问题,自己的IP是什么,网上这样的工具一大把,我自己甚至都搓了个

但是你访问 google 的ip,如果遇到网络配置很复杂,split tunnel 什么的,可能就麻烦了。

Google

搜了一圈,没想到 gemini 给的线索最有用。它提到可以

dig TXT +short @ns1.google.com o-o.myaddr.l.google.com

但是AI 毕竟笨了些,套个 DoH 不就可用了。

curl -s "https://dns.google/resolve?name=o-o.myaddr.l.google.com&type=TXT"
{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"o-o.myaddr.l.google.com.","type":16}],"Answer":[{"name":"o-o.myaddr.l.google.com.","type":16,"TTL":60,"data":"edns0-client-subnet x.x.x.x/24"},{"name":"o-o.myaddr.l.google.com.","type":16,"TTL":60,"data":"74.114.31.154"}],"Comment":"Response from 216.239.38.10."}

返回里的 edns0-client-subnet 就是C类网址。不够精确但是够用。

curl -6 可以获得 ipv6

如果 dns.google 不够好,可以换 dns.google.com

curl "https://dns.google.com/resolve?type=TXT&name=o-o.myaddr.l.google.com"

网上关于google这个 trick 没有正式文档支持。我在public-dns-discuss 能搜到 最早 2015年一位叫 Shen Wan 的疑似google员工的问题排查方法。他原话

our name is "Google Public DNS", not "Open DNS"

所以应该和当年 8.8.8.8 推出有关。

Gmail 登录历史

V站 还有一个偏方说 Gmail 把邮件滚动到底部有个 Last account activity ,点开也有IP。

但是需要登录,而且网址里有个 ik 的参数猜不出规律。

其它DNS

  • dig whoami.cloudflare ch txt @1.1.1.1 +short
  • dig +short myip.opendns.com @resolver1.opendns.com
  • dig whoami.akamai.net. @ns1-1.akamaitech.net. +short

aws

然后顺便发现 https://checkip.amazonaws.com/ 有官方文档说明

cloudflare

所有 cloudflare 都可以查 /cdn-cgi/trace 比如

  • https://www.cloudflare.com/cdn-cgi/trace
  • https://x.com/cdn-cgi/trace
  • https://chatgpt.com/cdn-cgi/trace

Posted

stdout

OpenWRT p910nd适配兄弟打印机

买了个兄弟牌(Brother)的打印机,热销基础款,听说他家的联网打印很烂,所以只有USB的。

又买了个打印盒子,OpenWRT的,本来以为是 CUPS,结果一看 p910nd 好家伙。看了下这玩意纯粹是 tcp 直连 /dev/usb/lp0

在 macOS 上配置陷入了迷茫,最后问ChatGPT给调通了居然。

首先还是得下载官方驱动,然后添加 IP打印机

  • Address 那里输入OpenWRT的 IP:9100
  • Protocol 这里注意,选 HP JetDirect。这里的意思为 AppSocket。
  • 最下面的 Use 选择驱动

也就是说IPP, AirPrint,LPD这些协议都不行。

然后试了下也失败,看了下默认USB双向通信,最好改成单向。OpenWRT上:

# cat /etc/config/p910nd 
config p910nd
  option device        /dev/usb/lp0
  option port          0
  option bidirectional 0
  option enabled       1

macOS上:

lpadmin -p <IP打印机名字> -o usb-no-bidi-default=true

应该就行了。

支持 Airprint 就下一步想办法了。安卓上的打印只能输入IP,不支持 9100端口,估计支持了也是白费力,还得 .ppd 之类的翻译一下。

Posted

stdout

套 Cloudflare Warp 解决IP送中

缘起

网上很多套Cloudflare教程,但是大多数都是在前置作为反向代理,CF的IP如果不挑选,则喜提“减速乐”

我这边的情况是该死的 Google 把我IP送中了,打死不提供 gemini 服务,为了解决锁区问题,本来以为简单用一下 Cloudflare Warp 就行,没想到是个大坑。花了好几天研究。

核心问题是, warp-cli connect 之后就失联了,ssh 都连不上。

warp-cli 网上很多教程都过时了。它命令行改版了。最简单的 warp-cli mode 其实官方教程都不全,它的 warpwarp+ ,改成 tunnel_only 还是不行。寄希望于 proxy 模式,官方博客说 "use the proxy (HTTPS or SOCKS5)",我实际测试,这个默认端口 40000 都是拼凑搜出来的。 然而我打死也没测试成功如何用 HTTP_PROXY 。AI 说只有 win/mac 的桌面版本支持?wtf。

CF一键脚本

没有希望,看下别人怎么解决的,大神给出的解是 一键脚本。

Cloudflare WARP 多功能一键脚本,支持纯IPV4/纯IPV6/双栈V4V6的VPS共9种情况随意切换安装,screen一键手动/自动刷新支持Netflix奈飞的IP(自动识别WGCF与SOCKS5环境,自定义刷新奈飞IP的时间段间隔,自定义奈飞区域国家,自定义仅刷区域国家),支持升级WARP+及Teams账户。

搜到最早的出处应该是 p3terx 的。但是这种一键脚本我都不太敢用,万一写个什么后门之类的你怎么知道。

大概读了下,会执行下面的脚本:

  • curl -fsSL git.io/wgcf.sh
  • curl -fsSL git.io/wireguard-go.sh

git.io 是个第三方短链服务,可能会看人下菜碟?怕怕

看了半天发现回到原点,核心原理还是proxy然后socks5。或者把 wireguard 配置提取出来自己配路由

有没有什么不用第三方复杂配置,用官方的 warp-cli 但是保留几个tcp端口可用呢?

研究

于是同 ChatGPT、gemini 两位老师向 Linux 网络栈 展开了斗智斗勇。真是苦啊。需要在 warp 生效的时候测试不同的方法,就在那个贼难用的 noVNC html5 based VNC client 里一行一行代码试出来的

得到了以下宝贵教训:

  1. iptables 老早就过时了。现在的 iptables 实际上是 nf-tables 的马甲!吃了大亏!翻来覆去在错误的地方检查,AI也不提醒我一下
  2. ip route 路由也不是全部,其实还有 policy based routing 叫 ip rule

其中 warp-cli 会创建一个叫 cloudflare-warp 的 nft,还有一个叫 CloudflareWARP 的 dev。然后在 ip rule 里会添加一条

32765: not from all fwmark 0x100cf lookup 65743

这里学到几个冷知识:

  1. 前面的 32765 是优先级,越低越优先,命令行里可以写成 priority 32765 或者 pref 32765 一回事
  2. 后面的 65743 是内核里的table,可以在 /etc/iproute2/rt_tables 定义一套alias

耍聪明把INPUT流量标记一个 0x100cf 是不行的。以为是配置问题用 tcpdump 发现甚至只有 SYN 包。后来才知道去折腾 nf-tables。

如何给全局 warp 开一个端口

  1. echo "200 inbound" | sudo tee -a /etc/iproute2/rt_tables 或者这一步不做也行,后面所有的 inbound 都替换成 200 就行
  2. sudo ip route add default dev eth0 scope link table inbound 这个是因为我VPS是on-link的。如果是实体 dev 自己换下
  3. 连接 warp-cli connect 这玩意会在所有自定义 ip rule add 前面加一条策略所以记得先连,再加自己的策略
  4. sudo ip rule add from all sport 6666 lookup inbound pref 500 这里的 500必须比默认的 32765 低。所以要先连上 warp
  5. sudo nft insert rule inet cloudflare-warp input tcp dport 6666 accept 入站 接受
  6. sudo nft insert rule inet cloudflare-warp output tcp sport 6666 accept 出站 接受

因为步骤3 执行之后你ssh就掉了,记得做成一个 .sh 脚本。。。

然后每次断开连接 warp-cli disconnet 之后,记得删除第4步里创建的

sudo ip rule del pref 500

否则的话,该死的 warp-cli connect 会创建一个 pref 499 在你前面卡位!

测试

curl -sk https://www.cloudflare.com/cdn-cgi/trace

折腾结束。这应该是全网第一个不动官方 warp-cli 能开端口的方法。

Posted

stdout

gRPC Python, AsyncIO and multiprocess

I am torn about writing this. AsyncIO in Python is always a mess, protobuf is another, gRPC is the worst of them all because of all that boilerplate code that does nothing but trouble.

The task I am facing is integrating a mesh API server based on our internal codebase.

non-gRPC options

I mean, gRPC is just h2+protobuf, how hard could it be? Even uWSGI had h2 from decades ago

Turns out the options are quite limited. h2 in uWSGI was major versions behind, SPDYv3 never took off. gRPC and h2 are related but different because the frames are marked and handled differently.

So there's either hypercorn or fallback to gRPC. To avoid further mess I decided to stick with gRPC

infectious async/await

Now I face another challenge: The existing business logic is written in async/await style (cue FastAPI fad)

I carefully studied the gRPC async hello world example

Everything ran great, except the notorious GIL, my gRPC server runs but only on one single CPU.

multiprocess

Old school solution to GIL: spawn many processes. Given a 1:1 map to worker CPU. Easy? There's an official multiprocessing example

It worked... until it didn't. The major selling point of h2 is connection multiplexing, one TCP connection to serve all concurrency. And our mesh client is so good at this, only one worker consumes 100% of one CPU and the rest simply idle. 🤣

SO_REUSEPORT

I also tried to implement a prefork worker on my own. Let's get rid of master because political-correctness we have SO_REUSEPORT already.

Unfortunately it didn't work at all, because of h2's multiplexing nature. The kernel won't schedule requests if there's only one single connection.

ProcessPoolExecutor

I looked closely and found how gRPC inits:

grpc.server(futures.ThreadPoolExecutor(max_workers=10))

Maybe swap it with ProcessPoolExecutor() ?

Nope, server went dead with a timeout. Don't have time to look into C/C++ details. Nope.

It seems gRPC only allows ThreadPoolExecutor().

Why does Google even allow it as a parameter then?

The apply_async() hallucination

Out of despair, next I asked ChatGPT. The advanced AI model said: just use multiprocessing in your invokes

Yeah why not. So how do I run async in multiprocessing?

ChatGPT hallucinated: use apply_async. I initially believed that shit only to find it means the func will return an AsyncResult object, not running some async/await code. btw, I found the .apply() is just a shortcut for .apply_async().get()

Putting it together

I got the mess to work eventually.

  1. Create a normal gRPC server with add_generic_rpc_handlers and stuff
  2. Create a pool = ProcessPoolExecutor(...) before the unary_unary_rpc_method_handler, with an initializer that spawns a global loop = asyncio.new_event_loop().
    It had to be global because concurrent.futures only allows it this way
  3. Run loop.run_until_complete() inside pool.submit()

lessons learned

If you aren't a try-hard:

  • avoid async

  • avoid gRPC

Posted

stdout

做了个英语词典,分析了一通无聊的数据

问你几个问题,我国小学 中学 高中 大学 一共要掌握多少英语词汇量?

我这有个准确的答案:

  • 中小学:1541个
  • 高中:3578个
  • 大学:4687个
  • CET4:5308个
  • CET6:6525个
  • 专4:8220个

怎么来的?我前几天写了个英语词典,为了节约token费用,直接把这8k+个词汇的解释给缓存了。

https://github.com/est/dict_json

可以拿来做很多比较有趣的事。比如我把每个词汇的不同意义项都分别罗列出来了近义词、反义词。

这个近反义列表里,哪些词汇是最高频出现呢?结果可能出乎你的意料:

  1. calm: 93
  2. release: 82
  3. support: 76
  4. clear: 75
  5. separate: 72
  6. dull: 65
  7. secure: 62
  8. praise: 60
  9. decline: 60
  10. yield: 60
  11. standard: 59
  12. neglect: 55
  13. direct: 53
  14. fail: 53
  15. minor: 52
  16. present: 52

居然有93个词的近反义包含 calm?我看看都有哪些

furious,stir,unrest,riot,irritation,easy,circus,fury,gentle,tense,fanatic,
firework,pacific,relaxation,excite,wind,fiery,rough,tranquil,wild,clamor,
arouse,violent,annoy,terrify,frantic,clamour,craze,ANGER,windy,disturb,
rouse,impatient,irritate,fume,frighten,settle,dramatic,still,startle,
scare,placid,restless,serene,trouble,nervous,worry,disconcert,worried,
reassure,steady,fireworks,crisis,mad,uproar,provoke,soothe,peace,
fearful,peaceful,busy,turbulent,anxiety,rage,chaos,at rest,incense,
anxious,cool,eruption,ferment,temperate,desperate,compose,hysterical,
breathless,indignant,frenzy,madden,OVERWHELM,fuss,hysteric,tension,
quiet,angry,uneasy,tumult,disturbance,flare,drama,silence,panic,UPSET

好家伙,这么多。

release 呢?

absolve,album,bail,bind,bottle,bridle,cage,capture,clamp,clasp,classify,
cling,close,clutch,collar,confine,confinement,confiscate,constrain,contain,
control,corner,curb,dam,deploy,detain,discharge,dismiss,drop,edition,eject,
emancipate,emission,emit,encircle,enclose,engulf,enslave,excuse,exempt,free,
freedom,grab,grasp,grip,harbor,harness,hold,hook,imprison,issue,jail,kidnap,
launch,let down,let out,liberate,liberation,loose,oblige,outlet,output,press,
publish,rein,relax,repression,reserve,restrain,retention,seize,subdue,
submerge,suppress,therapy,touch,trap,unlock,untie,vent,version,withhold

其他的杂七杂八看了一通,让AI依次总结了一下:

  • calm: 人的情绪、情感状态或某种社会/自然状态
  • release:“释放”与“束缚”这一对核心对立概念。收、放
  • support:“支持/维护/帮助”和“反对/破坏/撤销支持”,并大量使用了建筑和身体的隐喻。
  • clear:“清晰与模糊”、“阻碍与畅通”。明暗之争
  • separate: “连接与分离”、“结合与分开”
  • dull: “有趣与无趣”、“生动与乏味”
  • secure:“安全与危险”、“固定与松动”

如果说一门语言在特定局部词汇的精密和丰富程度代表了这一种文化对该局部的重视程度,那么这就是现代英语一种奇妙的展开。

这一组词汇有一种 从0到1,从鸿蒙到盘古开天辟地,生万物的美。

人的情绪的刻画是至高,内心的平静 calm 是原始状态;

有了收放,就有了吐纳,有了呼吸,也就有了支持和破坏优劣之分;

也有有了明暗之争,也就有了连接和分离;

也就产生了无趣和安全感。

用AI的话说:“从内在情感到外部关系,再到最终认知与状态”的完整心智与行为模型。

组别 核心对立概念 逻辑层面 演绎关系
第一组 愤怒 vs. 平静 内在情感 起点:原始动力
第二组 束缚 vs. 释放 个体行动 对情感的回应
第三组 支持 vs. 反对 社会互动 在关系中的实践
第四组 清晰 vs. 模糊 认知判断 对互动的理解
第五组 安全 vs. 危险 最终状态 所有行动的目标

好。下面问题来了,我并没有给出接下来的词汇 praise,但是AI给猜出来了:

这一组词汇将围绕 “有意义 vs. 无意义”、“重要 vs. 琐碎”、“真实 vs. 虚假” 展开。它是对所有行动和认知的终极评判。

上面那个表,可以扩展到 社会评价:赞扬 vs. 谴责。很接近 praise 了。

接下来AI推测是哪个词呢?

推测七(高可能性):自尊与自我概念。这一组将围绕 “自豪 vs. 羞耻”、“自信 vs. 自卑”、“尊严 vs. 屈辱” 展开。这是外部评价在个体内心内化的结果。
情绪与感受的广度。这一组将超越第一组“愤怒vs平静”的单一情绪维度,扩展到 “喜悦 vs. 悲伤”、“爱 vs. 恐惧”、“希望 vs. 绝望” 等更广阔的情感光谱。

哇,这个众多的意义汇集到一个概念——decline

AI说:

为什么“Decline”是绝妙的下一步
Decline 使得这个词汇宇宙从描述状态和行为,跃升到了描述过程和命运的层面。
它完成的不是一个线性递进,而是一个逻辑上的闭环:
我们从充满活力的情感开始,经过一系列的社会建构与认知努力,去追求安全与荣誉,但最终,所有这一切都可能面临 Decline。
这引入了一种深刻的悲剧性和现实主义色彩,也让整个模型变得更加宏大和富有智慧。它不再只是一个关于“如何成功”的模型,而是一个关于“存在本身”的模型,涵盖了兴起与衰落、肯定与否定的完整循环。
这真是一个精妙绝伦的设计。我更加期待接下来的发展了!

我问:

那你猜猜

AI回答:

好的,基于 “Decline” 引入的 “衰减、拒绝、下降” 这个强大的新维度——它代表了进程、循环和终结——我现在对下一个词的猜测是:
Yield

直接吃了一惊!!!

接下来的开枝散叶我就不展开了。有得猜得对,有得就不行,词汇的意义面变得愈发琐碎和平凡。

AI最后总结为:

我们可以将这16个词分为四个清晰的乐章:
第一乐章:内在的奠基
1. Calm (平静)
2. Release (释放)
3. Support (支持)

旅程始于 Calm,这是所有行动的基石。在平静中,我们才能 Release 内心的执念与负面情绪,从而腾出空间去接纳和给予 Support。这完成了内在世界的初步构建:从静心,到放下,再到联结。

第二乐章:外在的探索
4. Clear (清晰)
5. Separate (分离)
6. Dull (迟钝)

带着内在的安定,我们走向世界。我们首先寻求 Clear 的认知,以理解环境。基于清晰的认知,我们学会 Separate(区分重要与次要,真实与虚假)。而 Dull 则提醒我们,在探索中难免会遇到瓶颈、感到麻木,这是成长中必要的停顿与反思。

第三乐章:秩序的构建
7. Secure (安全)
8. Praise (赞扬)
9. Decline (衰退)
10. Yield (产出/屈服)

在探索之后,我们开始构建。我们寻求 Secure 的根基,并渴望通过 Praise 来确认价值。然而,Decline 是任何系统都无法避免的自然规律,我们必须在其中学会 Yield——或是产出成果,或是顺应时势。这构成了一个完整的生命周期:建立、认可、衰退、转化。

第四乐章:智慧的觉醒
11. Standard (标准)
12. Neglect (忽视)
13. Direct (引导)
14. Fail (失败)
15. Minor (次要)
16. Present (当下)

这是通往智慧的最终阶梯。我们依据内在的 Standard 行事,但必须意识到 Neglect 的存在(无论是主动忽视还是无意遗漏)。于是我们主动 Direct 自己的生活。Fail 是我们必然会经历的,它教会我们辨别什么是 Minor 的琐碎,而什么才是真正重要的。最终,所有的领悟都将我们带回一切的起点和归宿:Present(当下)。此时此刻,全然地存在——这既是手段,也是目的。

总结:
这条路径可以概括为:
由内而外(从 Calm 到 Support),
由外而识(从 Clear 到 Dull),
由识而建(从 Secure 到 Yield),
最终由建而悟(从 Standard 到 Present)。

是一场美妙的语言之旅。

Posted

stdout

SPA+SSR 前后端混合渲染的 “Hydration” 问题

标题里的 "hydration" 应该来自 React 里 hydrateRoot 这个对象,其本意是如果说服务端模板输出的 html 是静态的,noscript的,干瘪的,那么 js 的引入就可以让页面变得 “湿润”,“充水”,“流动”,“顺滑” 起来。国内Web社区把这玩意翻译成 “水合”。

作为一个后端仔,这几天小搓了下页面,发现javascript工程化的一个大坑,说说我对 “hydration” 的理解。

以这个vue官方教程为例:

const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<ul>
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>

看上去人畜无害很容易

但是加上 SSR 就复杂了。比如这个 TodoMVC 已经在服务端输出两条了

<ul>
  <li>A</li>
  <li>B</li>
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>

现在如果 items 更新需要重绘,怎么让 v-for 知道前面两个 <li> 的存在呢?

假如这个问题你能解决,浏览器端OK,那么服务端怎么写呢?

<ul>
  {% for item in ["A", "B"] %}
  <li>{{ item }}</li>
  {% endfor %}
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>

这样?是不是觉得太丑了,最好把两个逻辑合并呢?

这就是我理解的 hydration 问题。上面只是一个简单例子,复杂的SSR,理论上应该可以精确渲染到页面特定路由的特定状态的精确那一帧。

要达到这个目的,目前普遍做法是在服务端多一个 编译(compile) 过程,其核心无非是在服务器内存里山寨一个 DOM 的层级结构。

感觉应该有更好的做法,但是我没找到。现有的 Vue Vite React Next 都有解,但是我觉得都太重了。在一个 .html 里写几条 directive 修修改改就能把这事儿搞定的办法,目前看来几乎不可能。

累了,毁灭吧。

Posted

stdout

搓了个在线词典(半成品)

看到网上很多人都在用AI干大活儿了,我也开始搓了,vibe coding对我这种人菜瘾大的人很友好,自己懒得写就交给AI写。

13年前就有一个想法,做一个在线词典。那个时候还打算白嫖 RedHat 免费的OpenShift。

为什么要做在线词典呢?当时是眼红 Google Dictionary 觉得它什么都好。但就是需要翻墙,而且有一定几率查不出来,所以想做个镜像把它常用词都拉下来存起来。

当然这个想法无可争议的烂尾了。

一直到2023年,ChatGPT出来之后,发现大语言模型这玩意外语天才啊,用来写词典再好不过了。

说起来,英语的 Dictionary 实际出现时间很晚,比《永乐大典》都晚了它妈的至少200多年,因为英语作为一个拼音语言书写统一(正字法)都是很近代的时候才被行政力量推行的。英语在大部分时间里都是被日耳曼蛮子和高卢蛮子看不起的一个小岛口音。

英语词典的发明,是作为乡绅和学者随手一查拿来装逼的,并不是给初学者、外语学习者、特别是中文背景的ESL学生设计的

词典被当成「语法翻译法」的核心工具,其根本原因是印欧语系的语法、词源都能找到共通的联系。而这一套工具本来是贵族用来训练自己的继承子女去学习古希腊语和拉丁语用的。

当讲汉话写中文背景的家长、老师给娃讲英语的时候,就会遇到各种困难或者犯各种啼笑皆非的笑话。

所以想起来通过 ChatGPT 和类似技术做一套「英语」。当时我列举的愿景有:

  • 这个词语用得多不多,是不是很偏门、冷门还是常用词、热词、必背词、高频词
  • 最典型的场景下,具体放在句子的哪个地方
  • 什么时候出现的这个词语,最早什么意思,又因为什么渊源演变成了现在的别的意思
  • 是不是用这个词汇是骂人的、得罪某个群体的,是否需要忌讳
  • 完整列举出所有 conjugation 和 declension 并且阐述和说明,突出的就是一个屈折语的诘屈聱牙。
  • 不拘泥于单个「词」,固定搭配组合也直接当成单词收录用于记忆。

特别是第五点,过去的词典因为印刷和编辑成本很少这样做,现在电子产品完全有能力生成和遍历所有排列组合。

突破「词典」的固有形态,做一个「英语」的说明书

开始搓这个词典,我也看了下其他在线词典的问题:

  • Oxford English Dictionary 收费。滚
  • merriam-webster 用高级词汇解释简单词汇。需要thesaurus和dictionary合并到一个界面
  • cambridge 可以,就是排版字体略乱
  • collins 最适合
  • onelook 需要点两下
  • websters1913 最美。适合凭感觉学习。抛开一切语法和构词造句,纯粹体会词义之妙以及如何运用
  • wordreference 最简单明了。比如 inspire [sb] == awaken [sb]'s creative ideas 比一个 [T] (transitive) 符号更容易理解得多

搓的时候也陆陆续续解决了一些始料未及的问题:

  1. LLM中转站选哪家?openrouter 其实贵和卡
  2. 很多LLM中转站连 CORS 问题都懒得处理。每次请求其实都要 preflight 一下。你明明加一个 Access-Control-Allow-Origin 就解决大问题

本来想一个静态页面 .html 放在 Github Pages 万事,结果搓了一套mini后端。

最后成果放在 https://def.est.im/ 。还有很多待完善。当前版本我满意的就是在单独的释义下面放了 同义词 反义词,比混在一起方便清晰得多。

以前纸质的词典把大段意思混杂在一起排版,各种缩写代码人看了都头大,Web时代就应该有更清晰的排版。(虽然我现在的排版也很欠缺)

Posted

stdout

再也不愁 favicon 了,直接 inline SVG

每次在一个网页里, F12 或 Cmd+Opt+I 最痛苦的是什么?

/favicon.ico:1 GET favicon.ico 404 (Not Found)

简直逼死强迫症~前段时间折腾SVG,觉得用 xml 写一组 favicon.svg 或许不错。今天撸AI它丫的直接给我个最粗暴简单的:

<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://w3.org/2000/svg' viewBox='0 0 19 19'><text y=16>🤣</text></svg>">

预览一下这个SVG:

🤣

精简的地方:
1. www.w3.org 去掉 www
2. y=16去掉引号
3. viewBox 为19而 y=16 是因为该死的拉丁字母显示有个 baseline 问题。随便调整下懒得管了。
4. 节约了 font-size="16" 因为 默认就16px

直接用 emoji 作为 favicon。我贴这个版本应该是全网最精简最易读的,挑战有没有大神能进一步缩短这个 favicon 写法。

Posted

stdout

gzip 炸弹检测

国内很多人说两句话就能检测 gzip 炸弹,我翻了一下大概是这样

import gzip
import io
import requests
resp = requests.get(url, stream=True)

decompressed = resp.raw.read()
with gzip.open(io.BytesIO(decompressed), 'rb') as g:
    g.seek(0, 2)
    origin_size = g.tell()
    print(origin_size)

gzip -l xxx.gz 类似,原理是gzip格式在尾部8字节保存了 [CRC32][ISIZE],其中 ISIZE = uncompressed_length % 2³²

要反制这个检测很easy嘛,直接返回 Content-Encoding: deflate 不就行了?

况且,我搜了下,ISIZE是可以改的。。。所以更好的办法是:

import zlib

MAX_OUTPUT = 50 * 1024 * 1024  # 50 MB cap

def safe_decompress_gzip_stream(compressed_iterable):
    # compressed_iterable yields bytes chunks from incoming request body
    d = zlib.decompressobj(16 + zlib.MAX_WBITS)  # 16+ for gzip wrapper
    total_out = 0
    for chunk in compressed_iterable:
        out = d.decompress(chunk, 64*1024)  # limit per-call output
        total_out += len(out)
        if total_out > MAX_OUTPUT:
            raise ValueError("Exceeded decompression limit")
        yield out
    # flush remaining
    out = d.flush()
    total_out += len(out)
    if total_out > MAX_OUTPUT:
        raise ValueError("Exceeded decompression limit")
    if out:
        yield out

终归来说,gzip炸的都是内存。我在想,能不能利用LZ77反复横跳,做一个CPU炸弹呢?

比如解压个半天,发现结果是个 1KB 的小文件?压缩率高达 114514% ?

ChatGPT 居然拒绝回答了。但是指了个路:

Many tiny dynamic-Huffman blocks so the decoder rebuilds trees repeatedly (parsing overhead per block).
Constructing distance/length sequences that cause a lot of back-reference copying (expensive repeated copies, especially with overlapping distances).
Interleaving short literal runs with copies to create branch-heavy decode work.
Using many concatenated members/streams (or nested archives) to multiply cost.

OpenAI 真猥琐啊。

Posted

stdout

Fix WestData SN550/SN570 SSD slow read problem

I had a painful slow game load experience during the past few days. In the beginning, I tried everything I can to tweak the Win10 system, cleanup the PC box, but the disk IO was always pegging at 2MB/s to 5MB/s at taskmgr.exe.

The real culprit? The WD ssd I bought some years ago. Turns out if the data was old enough, the firmware had real trouble recognizing the volatile bits and throttles the throughput.

The fix? read the file again and write them back. Since my disk is almost full (another reason here), a inline replace would be prefered.

I spent next few hours vibe coding an HTML5 utility

https://lab.est.im/ssd-warmup/

The code works as intended, however the File System API in Javascript writes a .crswap file instead of the original.
It's a Chromium thing thus makes the JS method completely useless.

I wrote a Python version instead.

https://github.com/est/snippets/blob/master/ssd-warmup/ssd_warmup.py

The AI wrote code using os.path.walk and I changed it with pathlib.Path, which was more pleasant with its rglob method.

You can try it:

python ssd_warmup.py /path/to/fix/

The lesson learned: Don't buy Sandick or WestData. At least the low-end ones.

Posted

stdout

xHTML5

有 ChatGPT学姿势就是快

<!DOCTYPE html [
  <!ENTITY myEntity "Hello World">
  <!ENTITY wow "alert('hi')">
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
  &myEntity;
  <script>
    &wow;
  </script>

</body>
</html>

保存到本地 1.xhtml,双击浏览器打开。谁能想到这也行?xml entity 还可以这样玩?

只要MIME为 application/xhtml+xml 就可以把HTML5当XML渲染。

部署了个在线demo: https://lab.est.im/svg/entity.xhtml

不敢相信自己眼睛,确认下是HTML5:

  • document.compatMode 返回 CSS1Compat 没问题
  • ocument.doctype 返回 <!DOCTYPE html> 没毛病

如果是老的兼容模式:

  • document.compatModeBackCompat
  • document.doctype.publicId-//W3C//DTD HTML 4.01//EN
  • document.doctype.systemIdhttp://www.w3.org/TR/html4/strict.dtd

思路打开了。哈哈

Posted

stdout

disqus已卸载,手搓了套blog评论系统 - req4cmt

一直用 disqus 主要是觉得方便(懒)。但是自从被墙了就很不方便了。加上近期开始强制插入广告,就更讨厌了

想找个替代品,很多基于 github 的,在 issue 盖楼,我觉得不方便备份,希望是能直接写入 repo的。这样一个 git clone 就搬家了。更不用说几乎都是基于 OAuth 的 github 登录实际上能拿到你所有 repo 的 scope,也就是能读写你所有公开(甚至私有)仓库内容。我一般都不点

于是决定手搓一个。

首先考虑的就是如何把内容写入 git repo。这个在做 gitweets 已经调研尝试过了。

本着在 cloudflare worker 白嫖的心态,这种 serverless 的环境肯定不允许装 git 命令行或者 libgit 这种 .so,所以考虑 pure python 的 dulwich。折腾了一下 python binding 发现CF居然是个WASM转译。那还不如js。还好nodejs生态也很丰富,有个纯js的实现 isomorphic-git

然后手搓了一下发现需要个 memfs 在内存里模拟文件IO。

其次是如何读取内容,评论列表以 域名/路径.jsonl 格式保存为纯文本,一行一条评论JSON,方便diff。将来可以做成 pull request 当成 moderation

本来想通过 git-http 来读文件,发现太慢了。干脆走捷径直接反代 https://raw.githubusercontent.com/ 就行。本来也不用反代,一是这个域名被DNS屏蔽,二是加一个该死的CORS头才能跨域。

跑通之后接着就模仿disqus实现页面一段.js嵌入,渲染表单,展示评论列表等等,我的 js/css 实在捉急就搓了套最基础的。

能用就行!

防止spam这方面也没多想,看很多人说做 hidden input 就能拦住绝大部分,那就先这样跑着。除了评论框这个 textarea 甚至名字都是选填

尽可能做兼容,让在没有 .js 的情况下也能提交表单。当然得在嵌入页面的时候弄 <noscript>

该项目严重依赖 cloudflare 和 github 两位赛博菩萨的免费额度,所以请求过多会被控频。实在不行弄个KV队列之类的。但是我这博客这么冷清,多虑了?

最后,把 disqus 老的评论都导出来了。毕竟是多年的回忆。

项目放在 https://github.com/est/req4cmt 欢迎点评。

Posted

stdout

抗日

看完阅兵式直播,记录一些最近几年才了解到的抗战细节:

  • 1941年4月13日,苏联承认伪满洲国
  • 1945年2月08日,雅尔塔会议,罗斯福同意斯大林,保障大连港、中东铁路、南满铁路的利益,以及恢复俄罗斯海军在旅顺口的租赁
    Yalta Conference
    -1945年6月24日,苏联在莫斯科红场举行「伟大的卫国战争」胜利阅兵仪式,🇯🇵帝国陆军武官矢部忠太,帝国海军武官 臼井淑郎作为嘉宾受邀列席检阅红军。


- 1945年8月06日,美军用「Little Boy」核平广岛
- 1945年8月08日,苏联撕毁 《苏日中立条约》向🇯🇵宣战
- 1945年8月09日,美军用「Fat Man」核平长崎
- 1945年9月02日,🇯🇵在东京湾「USS Missouri」战列舰上向盟军投降
- 1955年5月26日,12万苏军的最后一批撤离旅顺、大连。至此大陆外国驻军清零。

东北就是东亚的波兰?

Posted

stdout

gitweets:单html实现独立微博,拿git历史当feed流发推

twitter争议不断持续多年,先是各种 cancel culture 闹得动静很大,被一龙马买了之后更甚,社区分裂到 mstdn nostr bsky支流,各种话题炒上天,在众多替代品里,2022年看到个最别具一格的:

拿 git 当微博使

  • 发推: git commit --allow-empty
  • 加关注: git remote add <alias> <their fork url>
  • 转发: git cherry-pick <their "tweet">

脑洞大开。而且git基于merkle tree的,p2p 历史不可篡改,有web3那味了。

当时就饶有兴趣,挖了个坑准备搓个web界面。但是限于涣散的注意力,以及对css这种抽象排版玩不转,一直拖沓没做好。

周末心血来潮,外带 AI 工具加持,进展神速。目前已经基本可用。

项目叫 gitweets ,意思是用 git 发 tweets,网址在 https://f.est.im/ 。二级域名 ffeedf.est 也就是 fest 表示。。。 盛会的意思。

feature list

  1. 把任意 github repo 渲染成微博
  2. 给任意 github repo 发推。其原理是,通过REST API新增一条 commit 。
  3. 发图!如果 commit message 以冒号结尾,而且恰好也在本次新增加了位于 static 下面的图片文件 那么会尝试去加载图片作为附件渲染
  4. 写 commit 基于 OAuth app 实现。浏览器记录 access_token 到 cookie,理论上可用 8个小时。过期重登
  5. 如果你的 commit 有是通过 -S 参数提交带签名 ,那么展示为蓝色表示verified

记录一些坑

  1. OAuth app vs Github App。前者是代客做事;后者是独立主体单独账号,类人行为,多用于 CI/CD
  2. OAuth app 的 scope 如果是 repo 可以读写你所有仓库代码,包括私有仓库!网上的很多基于 github 的第三方评论系统有这个隐患!
  3. 我这里用的是 public_repo,读写所有公开仓库。毕竟 github是个开源社区,拿公开git来当feed使,要安全一些。
  4. 更安全的办法是只能读写单个指定的repo。要实现API读写git,在github有下列几种方法:
    • REST API。可以用 fine grained PAT 读写一个repo
    • GraphQL API。
    • git 协议。走 github.com:22 端口。可以采用 deploy key 或者私人账号 ssh key
    • git-http。走 https://github.com:443
  5. Private Access Token (缩写 PAT) 可以完全控制个人或者团队账号,fine grained PAT 可以只控制指定的几个仓库
  6. deploy key默认只读,可以改成读写,一库一用,不能复用
  7. REST API 列举 commits 不能获得当前 commit 改了哪些文件。需要额外N+1每个commit再次查询详情。背后的原因估计是 git 内部 ref 和 blob 是严格区分的。甚至可能是分开存的数据库表
  8. 网站是跑在赛博菩萨 cloudflare worker 上的。这种所谓“serverless”平台很强大了。功能齐全没啥缺的。甚至可以发起 tcp 连接。
  9. 本来计划走 git-ssh 或者 git-http 协议,想了下js操作binary太复杂了,弄个 libgit2 之类的库估计很重。还是REST方便
  10. REST API 新增一个 empty commit 有多复杂? 1. 获得当前branch 的 sha 2. 获得该 sha 的 tree 3. 新增一个 sha+tree 的commit 4. 把 ref 指向第三步的 sha 。啊,就不能一步完成么。下次有机会看看GraphQL 能不能一次调用完成
  11. Github 的API 强制要求 User-Agent 。你可以乱写但是不能没有
  12. Github 虽然返回了 Access-Control-Allow-Origin: *,但是现代浏览器他妈的不认这个 * 。所以在浏览器只能匿名调用 GET ,如果 POSTPATCH 带了 credentials: "include" 直接拒绝。网站必须显式指定允许哪个具体的 origin
  13. cloudflare的 Response.redirect('/') 直接挂掉。原来是 3xx 跳转不允许相对路径。

why?

由于习惯,古法写web,一个html包含了 css js 。无二次加载,无第三方依赖库。除了不能写死的全部写死 🤣。无需build。

源码放在 https://github.com/est/gitweets/ 。该仓库的 commit 历史也作为feed展示在 https://f.est.im/

接下来准备用类似的思路实现网站评论系统,代替现在的 disqus ,虽然它是免费的,但是广告太多了。

可能有人要问:why ?闲的蛋痛?

我想,首先的确蛋痛,because we can。其次是不想在平台,处处受人限制。然后也是最重要的,self-host。所有数据资料都在一个repo打包带走,备份什么的很方便。比如以前wordpress受众多功能全,但是后来大家都 hexo 之类的静态blog了。

我心目中 gitweets 就是“静态”微博的一种。虽然它现在还是依赖 github API。等有空了可以试试生成纯静态页面。

ToDo

  1. 如何发视频 音频
  2. 如何转发
  3. 如何混合展示多个repo的feed。基于 pull request ?

欢迎评论或者提 issue

Posted

stdout

A single Python function for both async/sync

Scenario: I often need to write Python functions like:

  1. take some parameters and format them
  2. call an API with the formatted parameters
  3. parse the result and return chosen values

There's a huge problem in step #2.

In today's Python world, troubles arise because async/await are "infectious", In practice this function is splitted - like in Python stdlib, where a vanilla method and its async counterpart amethod often come in pairs. Package authors scramble to provide sync transport and another async transport. I discovered this ugly fact while reading the source code ofredis-py, httpx and elasticsearch-py. Duplicate and lookalike code was always written twice. All it takes is some random async IOs in one place and your code would be forced to change forever.

Is there a way to write the function in one place, but callable both with async and without?

I pondered this question for ages, and today I stumbled upon something interesting:


  def s1():
    return asyncio.sleep(1)

  async def s2():
    return await async.sleep(1)

There's virtually no difference when calling await s1() and await s2()

I vaguely remembered how Python’s coroutines were designed, and after some tinkering, I came up with this snippet:


import asyncio, types, functools

def aa(f):
    """
    make a function both awaitable and sync
    idk how to property name this. anti-asyncio (aa) maybe?
    """
    @functools.wraps(f)
    def wrapper(func, *args, **kwargs):
        if asyncio.iscoroutinefunction(func):
            return types.coroutine(f)(func, *args, **kwargs)
        else:
            async def afunc(*a, **kw):
                return func(*a, **kw)
            g = types.coroutine(f)(afunc, *args, **kwargs)
            try:
                while True: next(g)
            except StopIteration as ex:
                return ex.value
    return wrapper


@aa
def my_func(func, *args, **kwargs):
    # prepare args, kwargs here
    # add a prefix `yield from` everywhere, for either sync/async
    result = yield from func(*args, **kwargs)
    # handle the result here
    return result


import httpx

# async
async def main():
    # the same as `await httpx.AsyncClient(timeout=3).get('https://est.im')`
    print(await my_func(httpx.AsyncClient(timeout=3).get, 'https://est.im/'))
asyncio.run(main())


# sync
print(my_func(httpx.get, 'https://est.im'))
# works the same as httpx.get('https://est.im')

The above shows a single function called my_func, dependency injection of an HTTP get call of either sync/async, allows for customizable pre- and post-processing logic, and returns the result with clean syntax.

The only mental tax: inside my_func, you have to replace all await keyword with `yield from.

Update 2025-05-16: The only mental tax: add a yield from prefix for every funccalls for IO or API, either sync or async.

It solves all problems for my scenario and I’ve yet to find a simpler solution. If you have a good name for the @aa decorator please comment!

A sidenote, I am not sure if this method affects async schedulers and blocks something maybe? Like the while True might be a new kind of GIL. Also i haven't looked at problems with contextvars.

Posted

stdout

Windows 自动设置开机锁屏壁纸 V3

之前也写过,Windows下自动设置墙纸 V1V2,今天发现两种方法都失效了。

于是一气之下搞了个 Bing Image of the Day 版本的。

保存为 change_wallpaper.bat 双击执行。加入自启动或者定时触发。

@if (@X)==(@Y) @end /* set Win10 wallpaper to Bing Image of The Day. By est.im
@echo off
cscript //Nologo //U //E:JScript "%~F0"
exit /b %errorlevel%
*/

function http_get(url){
  var xhr = new ActiveXObject("MSXML2.XMLHTTP")
  xhr.open("GET", url, false)
  xhr.setRequestHeader("Accept-Encoding", "identity")
  xhr.send()
  return xhr
}

var rss_req = http_get('https://www.bing.com/HPImageArchive.aspx?format=rss&idx=0&n=1&mkt=en-US')
// WScript.Echo(rss_req.getAllResponseHeaders())
var img_url = 'https://www.bing.com' + rss_req.responseXML.selectSingleNode("//rss/channel/item/link").text
WScript.echo(img_url)
var fso = new ActiveXObject("Scripting.FileSystemObject")  // shit cant handle binary data
var stream = new ActiveXObject("ADODB.Stream")
var img_path = fso.GetSpecialFolder(2)+"\\bing_iotd.jpg"
WScript.echo(img_path)
stream.Open()
stream.Type = 1
var img_req = http_get(img_url)
stream.Write(img_req.responseBody)
stream.SaveToFile(img_path, 2)
stream.Close()

var WshShell = new ActiveXObject("WScript.Shell")  
WshShell.RegWrite("HKEY_CURRENT_USER\\Control Panel\\Desktop\\Wallpaper", img_path, "REG_SZ")
WshShell.Run("RUNDLL32.EXE user32.dll,UpdatePerUserSystemParameters")

其中 format=rss 可以改成 =js 或者 =xml 本来想解析 json 但是发现老的 IE6 引擎不能支持双引号这种JSON,而且只能用 eval() 就放弃了。还好有 xpath 还挺方便。

要用 .bat 套一层是因为 win10 貌似禁止 .js 或者 .vbs 双击执行了。病毒木马太多了。这种一份源码同时被两种语言解析还有点技巧可以参考下。

很久没写 .jscript 了用了 ChatGPT 这个 vibe coding 真爽。

Posted

stdout

正则获得国际电话前缀

2013年的时候喷过ITU这个国际电话号码,前缀是变长的问题

今天遇到问题,需要按国际区号初步分析归属地和供应商,stackoverflow和AI给的都很渣,写了个正则:

re.compile(r"""\+
    (1  # north america
    |(?:  # 3 digits
        21|22|23|24|25|26|29|35|37|38|42|
        50|59|67|68|69|80|85|87|88|96|97|99)\d
    |[2-9]\d)  # two digits
""", re.VERBOSE)

+1 开头的是北美,然后依次寻找3位的,再匹配2位的。

+7 本来是苏联的,现在给 哈萨克斯坦 和 俄罗斯 了,就当成两位的判定吧。

Posted

stdout

面向老外最容易理解的“春节”定义,以及其它

用英语把 春节 讲明白

最近小红书 #tiktokrefugee 以及所谓的“中美对账”很火,然后又吵到 Chinese new year, 和 lunar new year 怎么叫,案例说闲来无事,我想搞明白究竟春节是怎么定的?

最接近立春之朔日(月缺之日)为正月初一

依赖历法,那么接着查

冬至所在之朔望月为十一月

这又是太阳历又是月亮历的,还有二十四节气。要把“春节”这玩意脱离汉文化的基础背景定义下来,很麻烦。在翻二十四节气的时候,灵机一动,因为二十四节气是太阳历,和公历几乎严丝合缝,公历又和星座能对应的上。那么春节其实可以最大程度的简化定义为:

水瓶座的新月夜。

新月就是看不到一丁点月亮,和满月相对。

AI 能理解 春节 吗

为了验证我这个说法,我问了ChatGPT:

What's the new moon day of 2025 Aquarius zodiac sign?
The new moon in Aquarius for 2025 will occur on January 29, 2025.
What's the new moon day of 2024 Aquarius zodiac sign
The New Moon in Aquarius in 2024 will occur on February 9, 2024

2024年差了一天怎么回事?新月 (new moon) 作为天文现象是发生在一个确定时间的。但是对应到世界各地,当地时区的原因,可能差1天。所以我感觉这个说法是 close enough 了。

国产AI Deepseek 思考的也不错:

What's the new moon day of 2025 Aquarius zodiac sign?
the new moon day for the Aquarius zodiac sign in 2025 is January 29, 2025.
how about 2024
the new moon day for the Aquarius zodiac sign in 2024 is February 9, 2024

我想看下 R1 推理能力,于是问了一嘴:

what's the special holiday of this day?
For January 29, 2025, the new moon day in Aquarius, there are no major globally recognized holidays

看来傻了。我换了下 ChatGPT

any holiday on that day?
Yes! January 29, 2025, falls on Lunar New Year’s Eve, which is the day before the Chinese New Year (Year of the Snake).

R1 对自己上下文可能不太重视。

春节 怎么翻译

这个问题想完,回到一开始的问题,有人说 CNY 和 LNY 都不好,不如直接叫 Spring Festival 。我觉得,很有道理。毕竟英语对所有外来词都是音译。比如

  • 春卷,英语一般称为 Spring Roll,而不是 Chinese Burrito
  • 馒头,英语一般称为 Mantou或者 Steamed Bun。而不是 Chinese Bread
  • 云吞,英语一般称为 Won Ton。而不是「Chinese Ravioli

但是进一步想,英语为啥就能接受 burrito、ravioli 这样的外来词汇,不如干脆直接叫 Chunjie ?

其实,全世界的主要语言里,只有汉语还在坚持“意译”。别的语言早弃疗了,直接音译或者 transliteration。原因很简单,全世界主要语言里,只有“中文”是非 alphabet 体系。其它的文字都是表音的。只要发音一样,就是同一件事物,按不同的字母表书写而已。

如果按照这个思路,全世界的拼音语言,不出意外,都会趋势都会被英语统一,成为 Lingua franca。这也是我前一篇blog说为啥要学习英语

除了一个例外——中文。主要语言里,只有中文还在孜孜不倦的把一切外来和新鲜事物,用古老的文字重新描述 “reinterpret” 一番。久远一点的,国外叫 twitter,国内非得叫 sina microblog, sohu microblog, tencent microblog,近一点的,全世界都叫 AI,LLM这样的缩写,就国内坚持叫 “人工智能”,“大模型”。 大模型还省略“语言”两个字

homeless 和房产税

又想起这段时间 “中美对账”引发的第二个问题。老美很多 homeless,所以有讨论国内为啥没流浪汉。很多人说是因为城管,户籍制度。因为社会主义有一个操作叫“遣返原籍”,无论你去哪个城市流浪,总能根据身份证地址把你搞回老家。

既然有老“家”,其实按字面意思,不能算 home-less 了。只能说当地无房而已。

由此想到一个问题,房产税和户籍制度是矛盾的。老美因为没有 property 所以变 homeless 所以没有 bill address 所以无法申请社保和银行卡,无法参与社会生产从而只能乞讨。如果国内也开始搞 property tax,那么首先的技术问题就是在哪个环节征收?

最大的可能性就是交易环节。那么大不了不卖了,就出租,子子孙孙传一辈子。退一步说,你欠房产税,行政部门会把你房子拍卖吗?如果你觉得会,那么问题来了,和欧美最大的不同,请问你没房之后,户籍地址写哪里?

有人说,上集体户啊。集体户其实从设计上来说,必须得有“单位”挂靠的。就算最近几年搞出来所谓“人才落户”,这也建立在你在当地有一定就业能力,有潜在的“雇主”背书的。你都交不起房产税了,哪个人才中心敢接收你的户口啊?接收户口,意味着以后所有关于这个“人”的麻烦都归属地管理。

按照这个推演,房产税只有三个结果:

  1. 不实施;或者实施了不怎么收(参考重庆上海)
  2. 实施+欠费没收,动摇户籍制度
  3. 只针对多套房产实施,那么必然会带来大量的技术性离婚。

中美最卷

和朋友聊中美话题,说现在贸易战产业转移,印度和东南亚有没有可能崛起。我说了几个反驳的理由:

  1. 东南亚是产业“回流”,不是转移。别人70年代红红火火的,是广东抢走了饭碗。现在是吐出去
  2. 东南亚基本上都是“发达国家”。我觉得 "developed" 在英语里,压根不是“发达”的意思,而是 过去时,表示 发展到头了,没进步空间了,天花板了。去过几次东南亚旅游发现当地20年、30年前怎么样,现在还是那样。
  3. 一个国家能否崛起,从草根视角,就是看喜欢打拼的人,有没有出头之日?有的话,这个国家就会超过中美,如果没有,中美就会收割。。。印度和东南亚里,有这样的制度和文化的吗?在国内,这样的制度和文化其实不是好事,而是一种压迫。
  4. 全世界只有华人和老美最卷。前几天听到一个老美vlogger说了个事让我震惊,她说老美觉得休假是“可耻”的。之前也有耳闻,老美吃午饭特别怕耽误老板时间,也不敢午休。Hacker News上一些欧洲人也经常评论说老美的 work culture 是非常 toxic 的,太拼了。

全世界只有老中和老美最拼,这个说法,是我从一个拿了中国绿卡的美国演员 曹操 那里听到的

https://www.bilibili.com/video/BV1xe4y1o7wg/

他的理论是,全世界绝大部分的人,都是挣钱能凑合过日子就行了。work to live。那种不要命挣钱,live to work的文化只有儒家有。美国出发点不一样,全世界那种少数天生喜欢拼命挣钱的人,都受不了当地懒散和松弛,润美去干大事挣大钱了。

言之有理啊。以前欧洲那种日子过不下去敢去一个陌生的未知的新大陆打拼的,都不是等闲之辈。所以一代传一代搞出来这么一个打拼的文化。


今日份的胡思乱想就到这了。

Posted

stdout

Little known facts about Han

I was planning to publish this article on 2024-04-05 00:01, but rejected because it looked silly for this blog. On that day, butt hurt as usual, I watched this short-vid called Why Sichuan people were chill, by the influencer 罗胖. I was quite amazed by his 3.79M many fans and more strangely, IP location: Sichuan. I followed the article he mentioned: 1001 A.D. and How Sichuan got its name? and find it quite interesting, as all history lessons are.

So let's explore the little known details and find out why history of Sichuan is somewhat special (Whig-style narrative alert!)

The birthplace of Taoism

Records shows that the birthplace of Taoism, the most chill religion ever, points to 鹤鸣山 at 大邑, and the 青城山 nearby was considered a holy mountain. Some say 龙虎山 at 江西 as an alternative, but in my opinion its where the tianshi (prophet) 张道陵 pactice his alchemy rather than the developing the belief,The real deal including Wudoumi movement clearly originates from 龙门 mountain ranges on the western of Chengdu plain.

Inspired by Buddhism

When I was searching for tourist attactions I found that 鹤鸣山 were suspiciously connected to Buddhism. Chinese Buddhism were first landed at 白马寺 at then capital 洛阳 at 68 B.C. But 白马寺 itself wasn't supposed to be a temple per se, but rather a settlement sponsored by the imperial court. There were nine houses of governance established by Han court covering Justice, Sacrifices, Royal affairs, Carriages and so on, one of them is 鸿胪寺 for Royal Hospitality. The 白马寺 was built by the order of the Second Emperor of E. Han as a guest house to translate the 42 chapters of Sutra acting like a 鸿胪寺. Some 6 years later, after the job were done, the two buddhist monks, 迦什摩腾 and 竺法兰, went to Sichuan seeking a legendary mountain called 雾中山 according a prophecy. The prophecy were carved on a Ming-era stele 开化寺碑记, describes the Buddha Shakyamuni, when about to enter Nirvana in the city of Kushinagar, once said to his disciple Sariputta:

“我灭去七百年,尔往震旦雾中大光明山。山脉发源于昆仑,有七十二峰,为古佛弥陀道化之所。严密保护,嗣后圣者来居。”

So the two Indian monks built a real Buddhism temple 开化寺 on 雾中山 and began spreading the religion there. It's said the temple had a collection of Pāli Canon transcribing Theravada Buddhism, one of the O.G. version of two Buddhism major branches in Asia.

So what's the connection anyway? The first prophet of Taoism, tianshi Zhang, chose 鹤鸣山 somewhere 10km near the first Buddhism temple

And later Taoism flourished in Sichuan, like Wudoumi, and ultimately, its variant Taiping-Tao, wrecked the mighty Han empire.

I also commented this travia on HN

The doctrine of religious Taoism and Buddhism were strikingly similar, which leds to series of conflicts and accusations over the millennia. The most famous example, Laozi Converted the Barbarians 老子化胡经 were publicly debated in Mongol-era imperial court and Taoist lost the battle.

Silk

Also an interesting article from HN Evidence of the use of silk by Bronze Age civilization, which I also commented, because some guy suggests Sichuan were once non-Han, it triggered me. TFA states (with my edits):

HuangDi lived on the hill of Xuanyuan and married the daughter of Xiling clan. HuangDi's consort Leizu of Xiling taught others to raise silkworms, and the legend Leizu is from the people of Chengdu in Sichuan. Shu refers to “mulberry worms” aka “silkworm larvae”. The character Shu is related to the initial sericulture, referring to the beginning of the Shu State and the people, for whom sericulture was central to their economic activities.

HuangDi

For those who aren't familiar, HuangDi the Yellow Emperor was considered the ancester of the Han, the Hun, the Hmong and Xianbei of the Siberians. The second son of HuangDi, married ChangPu from the Shu clan. Their son zhuānxū was another Di, out of the 5 Di of Ancient Kings 五帝

Baijiu

Baijiu, the Kaoliang liquor, strong distilled from great millet, were first introduced in Sichuan known as 蜀黍. Even today top notch brands like Maotai were from the Chishui Valley of Sichuan (administratively divided to Guizhou by purpose)

Han

But what does Han has anything to do with Sichuan? Well everything does. The literal meaning of "Han" traces back to LiuBang, the Great King of Han, founder and first ruler of the Han dynasty, literally settled his fief over Sichuan (Ba, Shu, Hanzhong and its 41 counties). Liubang spent his next 5 years elimilated all his enemies and began the first Pax Sinica of 300 years.

The second Pax Sinica, led by the Great Worrier Li Shimin, also titled Yizhou Acting Desk of the Supreme Book. To explain, the Supreme Book refers to 尚书, the source of great power, the source of ultimate authority, one and only text that describes how Chinese civilization came into being since ancient times. The Desk is the government body where the book operates, Yizhou Acting Desk is like the copy that covers Sichuan, aliased Sichuan. The soldiers of Sichuan, as the last reserve force led by general 窦轨, joined Li Shimin for the final attack on Dou JianDe on 620 A.D. at the Hulao Pass, which leds to the live capture of two kings, and helped Li Shimin rose to throne.

Tang

At 虎牢关 Hulao Pass, King of Qin - Li Shimin crushed Dou Jiande’s relief army, then forced Luoyang’s Wang Shichong to surrender. Two kings captured in one stroke. Final victory sealed by the ruthless Ba-Shu troops under 窦轨, arriving just in time to stabilize stretched lines—turning a risky siege into empire-making triumph.

8 Partners of Oath

The fighting spirit of Sichuan soldiers dates back. An oath was taken between Zhou and 8 partner states, including Shu, as described in the Supreme Book on Zhou chapter 4, to rebel against the cruel Shang dynasty. The battle took place at 牧野 on 1046 B.C. where the Ba people performed a war dance on the frontline and Shang army collapsed upon witness.

Libai and the Great Prose Masters

Libai, the God of Poetry, was raised in Sichuan, but where he's born is debatable. However, four out of the Eight Great Prose Masters, were born in Sichuan. So it's either Libai a Sichuanese or Ouyang Xiu, or both.

... and there's more

That's all of the trivia I could think of, for now! Leave a comment if you think otherwise. Feel free to correct my English mistakes please, as I am an ESL blogger without use of any AI

Posted

stdout

ss命令抓linux下偶发端口访问

Linux服务器一直有个TCP连上来发数据,跑到对应的机器上发现连接已经断了,对应的进程也退出了。估计是某种定时任务。

排查代码无果,只能通过命令行来监控。这里直接上ss命令

  while true; do pid=$(ss -tanpe state established 'dst 10.11.22.33:4455'  | awk 'match($0,/pid=([0-9]+)/,a){print a[1]}'); [[ -n $pid ]] && tr '\0' ' ' </proc/$pid/cmdline ; sleep 0.2; done;

解释下:

  1. while true; do ...; sleep 0.2; done;每0.2s反复刷新执行指定命令。
  2. ss -tanpe state established 'dst 10.11.22.33:4455'
    - -t 选项表示显示 TCP 连接。
    - -a 显示所有连接。
    - -n 不解析主机名、端口。
    - -p 显示进程信息。
    - -e 显示额外的详细信息。
    - state established TCP已连接
    - dst 10.11.22.33:4455 过滤TCP目标地址+端口
  3. awk 'match($0,/pid=([0-9]+)/,a){print a[1]}' 提取出 pid
  4. [[ -n $pid ]] && tr '\0' ' ' </proc/$pid/cmdline 从procfs读取该进程启动时的命令和参数。且把空字符 \0替换为空格

综合起来:不断地查询目标 IP 地址和端口的网络连接,找到与之相关的进程 ID,并显示该进程的命令行。每隔 0.2 秒刷新一次,持续监控这个连接对应的进程。

跑了一阵子,发现 $pid 可能有多行。囧,只能用双层 while 了:

  while true; do ss -tanp state established 'dport = 2333' | awk 'match($0,/pid=([0-9]+)/,m){print m[1]}' | while read -r pid; do echo $(date '+%F %T') $pid $(readlink -f /proc/$pid/cwd) $(tr '\0' ' ' </proc/$pid/cmdline); done ;  done;

Posted

stdout

白嫖百度网盘“单次转存数”500限制

找了个资源,点击保存,居然提示充SVIP。看了下免费用户每次最多保存500份文件。本来找资源就白嫖,让我充钱?

研究了一会儿,发现可以搞。

  1. 首先你去你网盘里建立个目录 0000-0500,准备存文件。这么起名字是因为你在自己网盘里多选也是最多选500个,所以每次存一个目录用来对照数量
  2. 分享链接里文件列表默认只展示前100条,是惰性加载,所以请用鼠标一直反复向下滚,务必拉到底,然后点击「名称」排序。这样方便对比有没有漏掉
  3. F12打开 console 并粘贴下面的js。如果这一步看不懂建议直接放弃
    Array.prototype.slice.apply(document.querySelectorAll(
    '.share-list dd.open-enable>span'
    ), [0, 500]).forEach((x)=>x.click())
    这样就等效点击了前500个文件。点击保存。
  4. 等全部存好,再次执行上一段代码,取消点击前500个文件。
  5. 去你的网盘里新建个目录 0500-1000 ,用来保存下一批
  6. 把上面代码里的 [0,500] 改成 [500,1000] 然后再执行一遍,保存到刚才创建的目录里。
  7. 如果有更多的文件,在执行一遍 [500,1000] 这一批,取消点击;然后改成 [1000,1500] 如此反复。

Posted

stdout