feat: 博客栏目——navbar 第三行按月分类(2026-08/2025-07),日期页预览文章卡片,卡片跳文章页;markdown 原文随仓 + 迷你渲染器全量风格化(t2▲节标/积木点列表/墨底码块/虚线引言/幽灵年号横幅),日期取首提 git 日期
This commit is contained in:
1 parent
99948ad129
commit
2f279c2082
14 files changed
+490
-2
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>博客 · 2025-07</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-paper text-ink">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/pages/blog-2025-07/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>博客 · 2026-08</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-paper text-ink">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/pages/blog-2026-08/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>gorzil:面向多人协作的 AI 原生工具开发自述</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-paper text-ink">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/pages/blog-gorzil/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>sapling_transformer 近半月开发自述</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-paper text-ink">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/pages/blog-sapling-transformer/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/** 博客页——日期页(月内文章卡片预览)+ 文章页(markdown 风格化渲染)。
|
||||||
|
* 迷你 md 解析:#/##/###、> 引言、``` 代码块、-/* 列表、**粗体**、`行内码`。 */
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { Page, Section } from '../lib/ui'
|
||||||
|
import { MONTHS, POSTS, monthOf } from '../content/blogs'
|
||||||
|
import { BrickDot, Mark, Tri } from '@stack/design-system'
|
||||||
|
|
||||||
|
/* ── 行内:**粗体** 与 `行内码` ── */
|
||||||
|
function inline(text: string): ReactNode[] {
|
||||||
|
const parts: ReactNode[] = []
|
||||||
|
const re = /(\*\*[^*]+\*\*|`[^`]+`)/g
|
||||||
|
let last = 0
|
||||||
|
let m: RegExpExecArray | null
|
||||||
|
let i = 0
|
||||||
|
while ((m = re.exec(text))) {
|
||||||
|
if (m.index > last) parts.push(text.slice(last, m.index))
|
||||||
|
const t = m[0]
|
||||||
|
if (t.startsWith('**')) parts.push(<b key={i++}>{t.slice(2, -2)}</b>)
|
||||||
|
else parts.push(
|
||||||
|
<code key={i++} className="rounded bg-paper-deep px-1 py-0.5 font-mono text-[0.9em] text-ink">
|
||||||
|
{t.slice(1, -1)}
|
||||||
|
</code>,
|
||||||
|
)
|
||||||
|
last = m.index + t.length
|
||||||
|
}
|
||||||
|
if (last < text.length) parts.push(text.slice(last))
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 块级渲染(对齐字体系统:h2=t2、h3=t3、正文=content、列表=积木点) ── */
|
||||||
|
function Markdown({ raw }: { raw: string }) {
|
||||||
|
const lines = raw.replace(/^---\n[\s\S]*?\n---\n/, '').split('\n')
|
||||||
|
const out: ReactNode[] = []
|
||||||
|
let i = 0
|
||||||
|
let key = 0
|
||||||
|
while (i < lines.length) {
|
||||||
|
const ln = lines[i]
|
||||||
|
if (!ln.trim() || ln.trim() === '{/* truncate */}') {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ln.startsWith('```')) {
|
||||||
|
const buf: string[] = []
|
||||||
|
i++
|
||||||
|
while (i < lines.length && !lines[i].startsWith('```')) buf.push(lines[i++])
|
||||||
|
i++
|
||||||
|
out.push(
|
||||||
|
<pre
|
||||||
|
key={key++}
|
||||||
|
className="overflow-x-auto rounded-xl border-2 border-ink bg-ink p-4 font-mono text-small leading-relaxed text-paper shadow-hard"
|
||||||
|
>
|
||||||
|
{buf.join('\n')}
|
||||||
|
</pre>,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ln.startsWith('> ')) {
|
||||||
|
out.push(
|
||||||
|
<blockquote key={key++} className="m-0 rounded-xl border-2 border-dashed border-ink/40 bg-paper-deep/60 p-4">
|
||||||
|
<p className="m-0 text-content text-body">{inline(ln.slice(2))}</p>
|
||||||
|
</blockquote>,
|
||||||
|
)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ln.startsWith('# ')) {
|
||||||
|
out.push(
|
||||||
|
<h1 key={key++} className="m-0 text-t1 font-extrabold text-ink">
|
||||||
|
{inline(ln.slice(2))}
|
||||||
|
</h1>,
|
||||||
|
)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ln.startsWith('## ')) {
|
||||||
|
out.push(
|
||||||
|
<h2 key={key++} className="m-0 flex items-center gap-2 text-t2 font-extrabold text-ink">
|
||||||
|
<Tri size={13} />
|
||||||
|
{inline(ln.slice(3))}
|
||||||
|
</h2>,
|
||||||
|
)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ln.startsWith('### ')) {
|
||||||
|
out.push(
|
||||||
|
<h3 key={key++} className="m-0 text-t3 font-bold text-ink">
|
||||||
|
{inline(ln.slice(4))}
|
||||||
|
</h3>,
|
||||||
|
)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (/^\d+\. |- /.test(ln)) {
|
||||||
|
const items: string[] = []
|
||||||
|
const ordered = /^\d+\. /.test(ln)
|
||||||
|
while (i < lines.length && (ordered ? /^\d+\. /.test(lines[i]) : /^- /.test(lines[i]))) {
|
||||||
|
items.push(lines[i].replace(/^(\d+\.|-) /, ''))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
out.push(
|
||||||
|
<ul key={key++} className="m-0 list-none space-y-2 p-0">
|
||||||
|
{items.map((it, n) => (
|
||||||
|
<li key={n} className="flex items-start gap-2.5">
|
||||||
|
<span className="mt-[9px] flex shrink-0">
|
||||||
|
<BrickDot size={11} on={ordered} />
|
||||||
|
</span>
|
||||||
|
<span className="text-content text-body">{inline(it)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 段落:连续非格式行合并
|
||||||
|
const buf: string[] = [ln]
|
||||||
|
i++
|
||||||
|
while (
|
||||||
|
i < lines.length &&
|
||||||
|
lines[i].trim() &&
|
||||||
|
!/^(#{1,3} |> |```|\d+\. |- |\/\* |\{\/\*)/.test(lines[i])
|
||||||
|
) {
|
||||||
|
buf.push(lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
out.push(
|
||||||
|
<p key={key++} className="m-0 text-content text-body">
|
||||||
|
{inline(buf.join(' '))}
|
||||||
|
</p>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <div className="space-y-5">{out}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 文章卡片(日期页预览) ── */
|
||||||
|
function BlogCard({ post }: { post: (typeof POSTS)[number] }) {
|
||||||
|
const quote =
|
||||||
|
post.raw
|
||||||
|
.split('\n')
|
||||||
|
.find((l) => l.startsWith('> ') && l.trim() !== '{/* truncate */}') ??
|
||||||
|
post.raw.split('\n').find((l) => l.trim() && !l.startsWith('#') && !l.startsWith('---') && !l.startsWith('---')) ??
|
||||||
|
''
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/blog-${post.slug}.html`}
|
||||||
|
className="block w-[420px] rounded-2xl border-2 border-ink bg-paper p-5 no-underline shadow-hard transition-transform duration-150 hover:-translate-y-[3px] hover:shadow-hard-lift"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span className="rounded-full bg-clash3b/15 px-2.5 py-0.5 font-mono t-small-strong">{post.date}</span>
|
||||||
|
<span className="t-small-mute">{post.author}</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="m-0 mt-3 text-t3 font-extrabold text-ink">{post.title}</h2>
|
||||||
|
<p className="m-0 mt-2 line-clamp-2 text-small leading-relaxed text-body">{quote.replace(/^> /, '').slice(0, 90)}…</p>
|
||||||
|
<p className="m-0 mt-3 t-small-strong">
|
||||||
|
阅读全文 <span aria-hidden>→</span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 日期页:某月的文章卡片 ── */
|
||||||
|
export function BlogDatePage({ dateKey }: { dateKey: string }) {
|
||||||
|
const month = MONTHS.find((m) => m.key === dateKey)!
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
active={`blog-${dateKey}`}
|
||||||
|
title={`博客 · ${dateKey}`}
|
||||||
|
desc={`本月 ${month.posts.length} 篇——开发自述与工程笔记。`}
|
||||||
|
>
|
||||||
|
<Section label="本月文章" note="点卡片进入文章">
|
||||||
|
<div className="flex flex-wrap gap-6">
|
||||||
|
{month.posts.map((p) => (
|
||||||
|
<BlogCard key={p.slug} post={p} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 文章页 ── */
|
||||||
|
export function BlogPostPage({ slug }: { slug: string }) {
|
||||||
|
const post = POSTS.find((p) => p.slug === slug)!
|
||||||
|
return (
|
||||||
|
<Page active={`blog-${monthOf(slug)}`}>
|
||||||
|
<div className="relative overflow-hidden rounded-2xl border-2 border-ink bg-paper p-5 shadow-hard">
|
||||||
|
<span className="t-display pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 select-none" aria-hidden>
|
||||||
|
{post.date.slice(2, 4)}
|
||||||
|
</span>
|
||||||
|
<h1 className="m-0 max-w-[46ch] text-t1 font-extrabold text-ink">{post.title}</h1>
|
||||||
|
<p className="m-0 mt-3 flex flex-wrap items-center gap-3">
|
||||||
|
<span className="rounded-full bg-clash3b/15 px-2.5 py-0.5 font-mono t-small-strong">{post.date}</span>
|
||||||
|
<span className="t-small-mute">{post.author}</span>
|
||||||
|
<Mark className="t-small-strong">开发自述</Mark>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<article className="mt-8 max-w-[76ch]">
|
||||||
|
<Markdown raw={post.raw} />
|
||||||
|
</article>
|
||||||
|
<div className="mt-10 flex items-center justify-between">
|
||||||
|
<a
|
||||||
|
href={`/blog-${monthOf(slug)}.html`}
|
||||||
|
className="rounded-xl border-2 border-ink bg-paper px-4 py-2 text-small font-bold text-ink no-underline shadow-hard"
|
||||||
|
>
|
||||||
|
← 返回 {monthOf(slug)}
|
||||||
|
</a>
|
||||||
|
<a href="/index.html" className="rounded-xl border-2 border-ink bg-ink px-4 py-2 text-small font-bold text-paper no-underline">
|
||||||
|
返回书架
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
authors: cqh963852
|
||||||
|
---
|
||||||
|
|
||||||
|
# gorzil:面向多人协作的 AI 原生工具开发自述
|
||||||
|
|
||||||
|
> 文章结构整理自 glm-5.3
|
||||||
|
|
||||||
|
## 为什么做
|
||||||
|
|
||||||
|
最近观察到一个现象:AI 编码工具越来越强。
|
||||||
|
|
||||||
|
但它们始终是"单人玩具"。Cursor、Claude Code 这类产品默认一个开发者对着一个终端。人与人围绕 AI 工作过程的协作,基本是空白。
|
||||||
|
|
||||||
|
{/* truncate */}
|
||||||
|
|
||||||
|
我想做的事情可以概括为:多人 + 多 agent 共享同一个工作现场的工具。人发起任务,agent 执行,所有人(包括其他 agent)看到同一份不断演进的状态。
|
||||||
|
|
||||||
|
## 发生了什么
|
||||||
|
|
||||||
|
在这个开发过程中,我只负责提 ISSUE、把控架构和技术走向。总结性、文档性的工作交给了 ai,编码工作也让 ai 处理,结果好坏参半,后续也会提到。
|
||||||
|
|
||||||
|
## 核心设计
|
||||||
|
|
||||||
|
gorzil 是一个 Rust 多 Agent 协作平台,三个关键词:CRDT 协同、看板驱动、点对点数据同步。
|
||||||
|
|
||||||
|
### 数据本质是共享状态,不是消息流
|
||||||
|
|
||||||
|
这是整个项目最重要的一个技术决策。传统 IM(Slack 式)把协作建模为消息流,但 AI 协作的场景里,任务状态是不断被多方修改的:人创建任务、agent 领取、执行中、产出物挂回任务。如果用消息流建模,状态同步就是一场灾难。
|
||||||
|
|
||||||
|
所以选择了 CRDT,具体是 Loro。看板/房间状态是共享文档,agent 是文档间的翻译者,图片存引用而非二进制。同步难题由 CRDT 自然消解,不需要中心服务器做仲裁。
|
||||||
|
|
||||||
|
### 三端架构
|
||||||
|
|
||||||
|
```
|
||||||
|
coord(公网):信令 + Logto 认证 + 组织管理 + TURN
|
||||||
|
daemon(PC):本地 agent 宿主,接受 WebRTC,Loro 同步
|
||||||
|
app(浏览器):Dioxus Web 客户端,发起 WebRTC,Loro 同步
|
||||||
|
```
|
||||||
|
|
||||||
|
coord 只做信令和认证,不碰业务数据。数据在 daemon 和 app 之间通过 WebRTC DataChannel 点对点同步,P2P 失败时降级 coturn 中继。这个"云端尽量薄"的形态是有意为之——协作数据不应该经过我的服务器。
|
||||||
|
|
||||||
|
### 双 AI 审查的安全模型
|
||||||
|
|
||||||
|
本地 agent 要执行命令、改文件,安全是绕不开的问题。软件把风险暴露给用户做决策,是很常见的偷懒行为——即对风险不做设计,弹个窗让用户选,责任就转移出去了。
|
||||||
|
|
||||||
|
这是一种经典的工程师做法。静态规则也是同类问题:区分不了 `rm -rf` 清理 target 和清理 home 的区别,于是干脆都问用户。
|
||||||
|
|
||||||
|
但协作软件要面对的是不同身份的人群。既要减少风险对用户的干扰,也要保留工程师的安全审查能力。这两件事不能靠同一个机制完成。
|
||||||
|
|
||||||
|
在这里,gorzil 做了如下设计:
|
||||||
|
|
||||||
|
1. 减少干扰上
|
||||||
|
引入 ai 辅助审查:在 tool_calls 提取和执行之间插入一个独立的审查 AI,做意图对齐判断——主 AI 要执行的动作,是否服务于用户的原始意图。审查 AI 的输入是净化过的结构化摘要,不接收主 AI 的原始上下文,从源头切断 prompt injection 的路径。已知危险命令走硬规则,语义模糊的才交给模型。
|
||||||
|
|
||||||
|
对于审查 AI 的额外要求是,接入用户足够信任的模型,不做中转站之类的危险做法——安全链路上如果再引入一个不受信任的环节,那就等于白做。
|
||||||
|
|
||||||
|
2. 审查能力上
|
||||||
|
要处理的是传统 ai 群聊看不到 agent 工作过程的难题。群聊里 agent 只输出结论,过程是黑盒。虽然有些聊天工具可以不断更新当前的消息,但是我想,应该没有人愿意一直盯着 ai 在干什么。那样没有节省多少时间。
|
||||||
|
|
||||||
|
gorzil 的做法是把工作过程结构化:看板的任务操作记录,将看板任务抽象为 thread 下的引用,操作历史跟着任务走。
|
||||||
|
|
||||||
|
如果要看某个 agent 是怎么做的,则通过 agent doc 检查它的历史记录。工程师不再需要盯着消息流,而是直接审计过程本身。
|
||||||
|
|
||||||
|
### 被否决的路线
|
||||||
|
|
||||||
|
点对点通信、传输层曾经动摇过,最后还是使用 WebRTC 方案。最初的方案也是 WebRTC —— 自己做信令、自己管 ICE、自己部署 coturn。每一个步骤都是琐碎的活,甚至 ai 编码留下了 bug。
|
||||||
|
|
||||||
|
看到 iroh 后,优雅的抽象,地址即身份,打洞内置,看起来可以砍掉一大片代码。几乎要迁过去了,甚至开始规划迁移的步骤。
|
||||||
|
|
||||||
|
但是在让 ai 真正动手时,ai 说“出问题啦:浏览器端的 P2P 做不了”(一些技术上的限制)。
|
||||||
|
|
||||||
|
而 WebRTC 那套虽然琐碎,但每一件琐碎的事我都已经理解了。迁移到 iroh 并不会省掉我熟悉的麻烦,而且增加了我不熟悉的黑盒。
|
||||||
|
|
||||||
|
我最终的选择是那个已经踩过坑的方案。
|
||||||
|
|
||||||
|
### CRDT 增量同步的坑
|
||||||
|
|
||||||
|
wasm 端的 CRDT 增量同步是最痛苦的一段。由于 dioxus 是跨平台,rtc 有 native、wasm 两个端。最痛苦的是,不知道 bug 长什么样。
|
||||||
|
|
||||||
|
bug 修复大部分是 ai 在做。因为无法定位 bug,对 ai 的修复过程既不做严格审查,也不纠结代码对不对。协作方式只有两条:
|
||||||
|
|
||||||
|
1. 反复告诉 ai"出错了",让 ai 打日志,再把日志喂回去
|
||||||
|
2. 后续让他自己控制浏览器去读日志
|
||||||
|
|
||||||
|
折腾到快要放弃 ai 修复,准备人手工处理时,glm-5.3 发布了。用对待 ai 的老方式尝试了一下,问题解决了(绷不住了)。
|
||||||
|
|
||||||
|
为了这个问题,大概燃烧了 2B 的 token。
|
||||||
|
|
||||||
|
解决后的第一件事,没有让 ai 接着做事,而是抓紧把集成测试用例写起来。
|
||||||
|
|
||||||
|
让 ai 趁着神志清晰的时候,把集成测试写上。不至于出现相同的错误时,再费劲告诉 ai 应该怎么做。
|
||||||
|
|
||||||
|
### UI 体系
|
||||||
|
|
||||||
|
UI 刚开始做,所以我几乎是 1:1 的模仿 Slack。三栏布局、频道、右侧详情面板,样式体系切到 daisyUI,抽了 gorzil-ui crate,配 storybook 和 Playwright 视觉基线。这部分编码工作 ai 参与度较高,因为组件迁移是模式明确的机械劳动。
|
||||||
|
|
||||||
|
但模仿的过程中一个问题一直悬着:如果我的软件在形式上和 Slack 几乎接近,那 Slack 也可以解决 gorzil 能解决的问题——除了技术核心不同。这个质疑我给不出反驳。CRDT、P2P、双 AI 审查都是看不见的东西,用户看见的就是又一个聊天软件。
|
||||||
|
|
||||||
|
所以我还在思考:软件的形式本身是否需要变化,才能更好地揭露问题、处理问题。看板驱动是我目前的一个答案——任务状态是一等公民,消息只是状态的注释。但这个答案对不对,要等真实使用之后才知道。形式追随的还是问题本身,而不是 Slack 的截图。
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
authors: cqh963852
|
||||||
|
---
|
||||||
|
|
||||||
|
# sapling_transformer 近半月开发自述
|
||||||
|
|
||||||
|
## 为什么做这件事情
|
||||||
|
|
||||||
|
作为一个在前端深耕的工程师,工程效率,代码运行效率一直是我所关注的内容。受 solidjs 与 rust 的影响,我正在考虑是否可以有更`"好"`的方式来 transfrom jsx/tsx 代码。
|
||||||
|
|
||||||
|
{/* truncate */}
|
||||||
|
|
||||||
|
在这个过程中,对于一些文档性的工作,大部分我交给了ai,也包括这篇文章的 30%。一些编码工作也尝试过让 ai 编写,但是结果通常比较差,所以不做过多描述。
|
||||||
|
|
||||||
|
## 发生了什么
|
||||||
|
|
||||||
|
过去半个月,sapling_transformer 项目经历了多次重构和技术路线调整。项目之所以发展到当前状态,主要是因为在实际开发过程中,原有方案暴露出一系列问题,促使不得不做出改变。
|
||||||
|
|
||||||
|
## 技术路线调整的原因
|
||||||
|
|
||||||
|
最初采用了 oxc 及相关依赖,目的是快速实现 AST 转换。
|
||||||
|
|
||||||
|
但 oxc 方案在实际开发中暴露出多方面问题,尤其是在内存管理上。所以不再描述 oxc 方案下的变化。
|
||||||
|
|
||||||
|
1. 由于 oxc 过度追求性能,放弃了平稳的内存管理库,编写相关代码时异常痛苦,文档匮乏,仅靠 ai 阅读源码,维护难度极高。
|
||||||
|
2. 文档缺失,用例缺失。在 oxc 源码历史中,出现过多次 breaking change,甚至是采用不完整的方案,以至于文档老旧。
|
||||||
|
3. 库的部分缺失,在使用 oxc 方案的后半段,尝试自行实现 Visitor Traverse,但在当时的架构设计下,部分关键实现缺失,以至于无法实现架构目标。
|
||||||
|
|
||||||
|
## biome 方案下的变化
|
||||||
|
|
||||||
|
迁移到 biome 相关依赖后,项目的架构演进经历了四个阶段。
|
||||||
|
|
||||||
|
最初尝试直接使用 biome 的 transform 能力,但发现 biome 作为 lint/format 工具的定位,导致其转换接口无法满足语义转换的需求,实际功能受限。
|
||||||
|
|
||||||
|
为此,第二阶段自建了 Traverse 过程,试图将 solidjs transformer 按语义迁移,但由于架构设计不成熟,代码组织混乱,维护和扩展都遇到障碍。
|
||||||
|
|
||||||
|
第三阶段对 Traverse 过程进行了重构,转向 transform 架构,但实际实现仍存在缺陷,模块划分和接口设计无法支撑复杂转换流程,架构问题依然突出。如父子关系处理,transfrom 复用。
|
||||||
|
|
||||||
|
在第四阶段(当前版本),针对前三版暴露的各种问题,对第三版进行了部分重构。具体做法包括:
|
||||||
|
|
||||||
|
1. 使用自定义的树遍历。自定义的树遍历,相比于高度抽象的遍历,可以更加自由的修改遍历函数,以此来支持父子关系处理
|
||||||
|
2. 增加同类型节点的不同处理方式。针对 ast transfromer,存在单个语句,不同上下文情况下,输出不同结果的场景。
|
||||||
|
3. transfrom 函数复用,结构的重新调整
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/** 博客内容注册表——markdown 原文随仓,日期取首提日期(git) */
|
||||||
|
import gorzil from './blog/gorzil.md?raw'
|
||||||
|
import sapling from './blog/sapling_transformer.md?raw'
|
||||||
|
|
||||||
|
export interface BlogPost {
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
/** YYYY-MM-DD(首提日期) */
|
||||||
|
date: string
|
||||||
|
author: string
|
||||||
|
raw: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POSTS: BlogPost[] = [
|
||||||
|
{
|
||||||
|
slug: 'gorzil',
|
||||||
|
title: 'gorzil:面向多人协作的 AI 原生工具开发自述',
|
||||||
|
date: '2026-08-19',
|
||||||
|
author: 'cqh963852',
|
||||||
|
raw: gorzil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'sapling-transformer',
|
||||||
|
title: 'sapling_transformer 近半月开发自述',
|
||||||
|
date: '2025-07-17',
|
||||||
|
author: 'cqh963852',
|
||||||
|
raw: sapling,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 按月分组(navbar 第三行的日期分类) */
|
||||||
|
export interface BlogMonth {
|
||||||
|
key: string // YYYY-MM
|
||||||
|
posts: BlogPost[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MONTHS: BlogMonth[] = [...POSTS]
|
||||||
|
.sort((a, b) => (a.date < b.date ? 1 : -1))
|
||||||
|
.reduce<BlogMonth[]>((acc, p) => {
|
||||||
|
const key = p.date.slice(0, 7)
|
||||||
|
const last = acc[acc.length - 1]
|
||||||
|
if (last && last.key === key) last.posts.push(p)
|
||||||
|
else acc.push({ key, posts: [p] })
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/** 文章所属月(用于文章页 navbar 高亮) */
|
||||||
|
export const monthOf = (slug: string) => POSTS.find((p) => p.slug === slug)!.date.slice(0, 7)
|
||||||
@@ -5,6 +5,7 @@ import '@stack/design-system'
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import type { ChapterId } from '@stack/design-system'
|
import type { ChapterId } from '@stack/design-system'
|
||||||
import { CHAPTERS } from './chapters'
|
import { CHAPTERS } from './chapters'
|
||||||
|
import { MONTHS } from '../content/blogs'
|
||||||
|
|
||||||
/** 设计系统四册(自 design-lab 迁入) */
|
/** 设计系统四册(自 design-lab 迁入) */
|
||||||
export type DesignKey = 'design-colors' | 'design-typography' | 'design-symbols' | 'design-components'
|
export type DesignKey = 'design-colors' | 'design-typography' | 'design-symbols' | 'design-components'
|
||||||
@@ -34,8 +35,8 @@ export function Page({
|
|||||||
/** 章节页标题在章头横幅里,可省 */
|
/** 章节页标题在章头横幅里,可省 */
|
||||||
title?: string
|
title?: string
|
||||||
desc?: string
|
desc?: string
|
||||||
/** 站头高亮:章节 id / 书架 / 设计系统册 */
|
/** 站头高亮:章节 id / 书架 / 设计系统册 / 博客月(blog-YYYY-MM) */
|
||||||
active?: ChapterId | 'index' | DesignKey
|
active?: ChapterId | 'index' | DesignKey | `blog-${string}`
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -83,6 +84,24 @@ export function Page({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{/* 第三行:博客(按月分类) */}
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-1.5 border-t border-dashed border-ink/20 pt-2">
|
||||||
|
<span className="t-small-mute mr-1">博客</span>
|
||||||
|
{MONTHS.map((m) => {
|
||||||
|
const on = active === `blog-${m.key}`
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={m.key}
|
||||||
|
href={`/blog-${m.key}.html`}
|
||||||
|
className={`rounded-lg px-2.5 py-1 font-mono text-[12.5px] font-bold no-underline transition-colors ${
|
||||||
|
on ? 'bg-paper-deep text-ink shadow-[inset_0_0_0_2px_var(--color-ink)]' : 'text-body hover:bg-paper-deep'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{m.key}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{(title || desc) && (
|
{(title || desc) && (
|
||||||
<div className="mb-7">
|
<div className="mb-7">
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { BlogDatePage } from '../../components/BlogPages'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(<BlogDatePage dateKey="2025-07" />)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { BlogDatePage } from '../../components/BlogPages'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(<BlogDatePage dateKey="2026-08" />)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { BlogPostPage } from '../../components/BlogPages'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(<BlogPostPage slug="gorzil" />)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { BlogPostPage } from '../../components/BlogPages'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(<BlogPostPage slug="sapling-transformer" />)
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
Reference in new issue
Block a user