diff --git a/astro.config.mjs b/astro.config.mjs
index 7a3238e..fb12f2c 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -106,6 +106,34 @@ function thoughtsCanvasLayoutDevPlugin() {
}
}
+function slidesDevPlugin() {
+ const slidesContentDir = path.resolve(__dirname, 'src/content/slides')
+ return {
+ name: 'slides-dev-plugin',
+ configureServer(server) {
+ server.httpServer?.once('listening', () => {
+ setTimeout(() => {
+ const address = server.httpServer?.address()
+ const port = typeof address === 'object' && address?.port ? address.port : 4321
+
+ console.log('\x1B[35m%s\x1B[0m', ' 🎴 [slides] Presentations ready:')
+
+ console.log('\x1B[36m%s\x1B[0m', ` ➜ Slides Hub: http://localhost:${port}/slides`)
+
+ console.log('\x1B[32m%s\x1B[0m', ` ➜ Mobile Vibe: http://localhost:${port}/slides/mobile-vibe-coding\n`)
+ }, 150)
+ })
+
+ server.watcher.add(slidesContentDir)
+ server.watcher.on('change', (file) => {
+ if (file.startsWith(slidesContentDir)) {
+ server.ws.send({ type: 'full-reload' })
+ }
+ })
+ },
+ }
+}
+
// https://astro.build/config
export default defineConfig({
site: 'https://yuler.dev',
@@ -137,7 +165,7 @@ export default defineConfig({
],
vite: {
- plugins: [tailwindcss(), thoughtsCanvasLayoutDevPlugin()],
+ plugins: [tailwindcss(), thoughtsCanvasLayoutDevPlugin(), slidesDevPlugin()],
server: {
watch: {
ignored: [thoughtsCanvasLayoutPath],
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 1226b52..5f8fa45 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -2,6 +2,7 @@ import antfu from '@antfu/eslint-config'
export default antfu({
astro: true,
+ ignores: ['src/content/slides/**'],
formatters: {
/** Markdown: dprint via eslint-plugin-format (`format/dprint`, language `markdown`) */
markdown: 'dprint',
diff --git a/package.json b/package.json
index e22476f..e1f300b 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"date-fns": "^4.4.0",
"html-to-image": "^1.11.13",
"leaflet": "^1.9.4",
+ "marked": "^18.1.1",
"mermaid": "^11.16.1",
"qrcode": "^1.5.4",
"sharp": "^0.34.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 462de1b..babef80 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
leaflet:
specifier: ^1.9.4
version: 1.9.4
+ marked:
+ specifier: ^18.1.1
+ version: 18.1.1
mermaid:
specifier: ^11.16.1
version: 11.16.1
@@ -2983,6 +2986,11 @@ packages:
engines: {node: '>= 20'}
hasBin: true
+ marked@18.1.1:
+ resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
mdast-util-definitions@6.0.0:
resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
@@ -7151,6 +7159,8 @@ snapshots:
marked@16.4.2: {}
+ marked@18.1.1: {}
+
mdast-util-definitions@6.0.0:
dependencies:
'@types/mdast': 4.0.4
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
index ce54e1d..b0813c1 100644
--- a/src/components/Footer.astro
+++ b/src/components/Footer.astro
@@ -7,7 +7,7 @@ const showSiteLinks = Astro.url.pathname !== '/'
{
showSiteLinks && (
- This website follows the design guidelines and provides agent instructions.
+ This website follows the design guidelines, provides agent instructions, and features slides.
)
}
diff --git a/src/components/cards/AgentStuff.astro b/src/components/cards/AgentStuff.astro
index 7ac22da..31c87ee 100644
--- a/src/components/cards/AgentStuff.astro
+++ b/src/components/cards/AgentStuff.astro
@@ -27,5 +27,12 @@ import Sparkles from '../icons/Sparkles.astro'
>
agent instructions
+ , and presents
+
+ slides
+
diff --git a/src/components/cards/SlidesCard.astro b/src/components/cards/SlidesCard.astro
new file mode 100644
index 0000000..78ef4e2
--- /dev/null
+++ b/src/components/cards/SlidesCard.astro
@@ -0,0 +1,58 @@
+---
+import { getAllSlideDecks } from '../../lib/slides'
+import CornerMarkers from '../CornerMarkers.astro'
+import ArrowRight from '../icons/ArrowRight.astro'
+import Play from '../icons/Play.astro'
+
+const decks = await getAllSlideDecks()
+const latest = decks[0]
+---
+
+{latest && (
+
+
+
+
+
+
+
+
+
+
+ {latest.slideCount} Slides
+
+
+ Vibe Coding
+
+
+
+
+
+
+ 播放
+
+
+
+)}
diff --git a/src/content/slides/mobile-vibe-coding/images/01-uu-remote.jpg b/src/content/slides/mobile-vibe-coding/images/01-uu-remote.jpg
new file mode 100644
index 0000000..e0fac2f
Binary files /dev/null and b/src/content/slides/mobile-vibe-coding/images/01-uu-remote.jpg differ
diff --git a/src/content/slides/mobile-vibe-coding/images/02-termius-cursor-cli.jpg b/src/content/slides/mobile-vibe-coding/images/02-termius-cursor-cli.jpg
new file mode 100644
index 0000000..ceb73f4
Binary files /dev/null and b/src/content/slides/mobile-vibe-coding/images/02-termius-cursor-cli.jpg differ
diff --git a/src/content/slides/mobile-vibe-coding/images/03-cursor-ios.jpg b/src/content/slides/mobile-vibe-coding/images/03-cursor-ios.jpg
new file mode 100644
index 0000000..18b4d26
Binary files /dev/null and b/src/content/slides/mobile-vibe-coding/images/03-cursor-ios.jpg differ
diff --git a/src/content/slides/mobile-vibe-coding/images/04-grok-bot-im.jpg b/src/content/slides/mobile-vibe-coding/images/04-grok-bot-im.jpg
new file mode 100644
index 0000000..703013e
Binary files /dev/null and b/src/content/slides/mobile-vibe-coding/images/04-grok-bot-im.jpg differ
diff --git a/src/content/slides/mobile-vibe-coding/images/05-github-pr.jpg b/src/content/slides/mobile-vibe-coding/images/05-github-pr.jpg
new file mode 100644
index 0000000..8fe3980
Binary files /dev/null and b/src/content/slides/mobile-vibe-coding/images/05-github-pr.jpg differ
diff --git a/src/content/slides/mobile-vibe-coding/images/06-browser-production.jpg b/src/content/slides/mobile-vibe-coding/images/06-browser-production.jpg
new file mode 100644
index 0000000..05d0c17
Binary files /dev/null and b/src/content/slides/mobile-vibe-coding/images/06-browser-production.jpg differ
diff --git a/src/content/slides/mobile-vibe-coding/slides.md b/src/content/slides/mobile-vibe-coding/slides.md
new file mode 100644
index 0000000..248e98e
--- /dev/null
+++ b/src/content/slides/mobile-vibe-coding/slides.md
@@ -0,0 +1,891 @@
+---
+theme: default
+title: 纯手机 Vibe Coding 实战
+info: |
+ ## 纯手机 Vibe Coding 实战
+ 基于 Mac mini 私有算力与多 Agent 协作的移动端极客开发范式
+class: text-center
+highlighter: shiki
+lineNumbers: true
+drawings:
+ persist: false
+transition: slide-left
+css: unocss
+---
+
+# 纯手机 Vibe Coding 实战
+
+### 用一部 iPhone 掌控 Mac mini 与多 Agent 生产闭环
+
+
+ 📱 iOS / Termius 控制面
+ 🖥️ Mac mini 私有算力端
+ 🤖 多 Agent 协同生态
+ 🚀 双轨验证与秒级上线
+
+
+
+ 分享人:Yuler · 开发者极客社区技术分享
+
+
+
+
+---
+layout: default
+---
+
+# 核心思想:AI 时代为什么可以用手机编程?
+
+告别语法打字,手机是意图控制台,算力与环境后置到主机
+
+
+
+
+
+ ❌ 传统移动端编程痛点
+
+
+ - 输入低效: 手机软键盘缺少代码符号(括号、分号、缩进),手工输入效率暴跌 80%
+ - 环境缺失: 移动端 OS 无法本地运行 Node、Docker、编译器等重型开发套件
+ - 调试艰难: 屏幕局限,无法同时容纳代码编辑器、终端和浏览器多窗口
+ - 工具繁冗: 依赖各种沉重的 IDE 插件与复杂的语法提示/补全配置
+
+
+
+
+
+ ✨ AI Native 时代的 Vibe Coding
+
+
+ - 编码全包: 代码编写由 AI 全权接管,不再需要任何软键盘符号与语法提示
+ - SSH 极简通道: 核心本质就是通过极简的 SSH 终端,直连自己常开的电脑或服务器
+ - 算力私有: 家里一台静音低功耗 Mac mini 常年开机,免额外云服务器费用且复用本地凭据
+ - 职责升维: 手机只负责发号施令、把关架构与验收成果,成为掌上总指挥台
+
+
+
+
+
+
+ 💡 核心哲学:计算留在家里,AI 顶在前面,控制握在掌心。
+
+
+
+
+---
+layout: default
+---
+
+# 整体系统拓扑:端到端解耦架构
+
+内网直连 + 外网主力穿透 + 双轨验证闭环
+
+```mermaid
+flowchart TB
+ subgraph Mobile ["📱 移动控制面 (iPhone)"]
+ Termius["Termius (汤米老师)"]
+ UU_App["UU 远程 (主通道)"]
+ Cursor_iOS["Cursor iOS 原生 App"]
+ Grok_IM["Grok Bot 群聊"]
+ GH_App["GitHub App (PR 审查)"]
+ Safari["Safari 浏览器 (验证)"]
+ end
+
+ subgraph Network ["🌐 连接与穿透矩阵"]
+ Home_LAN["在家: Wi-Fi 局域网直连 SSH\n(无需开显示器)"]
+ UU_Tunnel["外出主力: UU 远程内置 SSH\n(随时一键滑入图形桌面)"]
+ TS_Mesh["安全兜底: Tailscale Mesh\n(加密私网穿透)"]
+ end
+
+ subgraph Host ["🖥️ 宿主机算力端 (Mac mini)"]
+ CLI_Agent["Agent CLI (YOLO 自治执行)"]
+ Bot_Agent["Grok Coder & Reviewer"]
+ Dev_Stack["Node.js / Docker / Git / Vite"]
+ end
+
+ subgraph Verification ["🔍 双轨验证与交付体系"]
+ Local_Preview["【开发联调态】本地暴露内网端口\n--host 0.0.0.0 手机秒级热更新"]
+ Prod_Deploy["【最终交付态】GitHub Actions 自动构建\n云服务器 + 生产域名 (k41.yuler.cc)"]
+ end
+
+ Mobile --> Network
+ Network --> Host
+ Host --> Local_Preview
+ Host -->|Commit & PR| GH_App
+ GH_App -->|Merge 自动打通| Prod_Deploy
+ Safari -.->|即时验收| Local_Preview
+ Safari -.->|生产验收| Prod_Deploy
+```
+
+
+
+---
+layout: default
+---
+
+# 隧道连接工具矩阵:内外网打通方案
+
+宿主机预先授权 Agent,手机随时随地秒进开发会话
+
+
+
+
+
+
+
🖥️ 电脑/节点端:穿透与远程矩阵
+ 宿主机已授权 Agent
+
+
+ -
+ 🏠 在家直连场景:
+ 无需开电脑屏幕,直接基于家庭局域网 IP 直连 SSH,延迟趋近于 0,极速进入工作状态。
+
+ -
+ 🌟 外出主力 · UU 远程 (UU Remote):
+ • 常规主力:使用 UU 远程内置的 SSH 终端,穿透稳定迅速。
+ • 备用逃生舱:自带低延迟图形桌面,需要启动特定 GUI 应用或看 DevTools 时一键切入。
+
+ -
+ 🛡️ 安全兜底 · Tailscale:
+ 基于 WireGuard 的加密 Mesh 虚拟私网,在复杂网络环境下提供零端口暴露的安全直连兜底。
+
+
+
+
+ 💡 提示:Mac mini 无头运行,通过快捷脚本自动解锁 Keychain。
+
+
+
+
+
+
+
📱 手机端:终端连接利器
+ iOS 首选推荐
+
+
+ -
+ ⚡ Termius(汤米老师):
+ iOS 生态综合体验最好的 SSH 客户端。支持多主机分组、会话保持与弱网自动重连。
+
+ -
+ 🚀 Snippets 快捷宏指令:
+ 把常用长指令(如调起 Agent CLI、进入工程目录、启动 dev 服务)预置为快捷块,单指一触即发。
+
+ -
+ ⌨️ 触控优化手势键盘:
+ 配备专为 Terminal 设计的辅助功能键(Esc、Tab、Ctrl、方向键),操作 Shell 行云流水。
+
+
+
+
+ 💡 场景:在家用局域网直连 Termius;在外直接走 UU 远程内置终端或 Tailscale。
+
+
+
+
+
+
+ 🔑 核心认知:连接工具的目标是“消除物理距离”,让常开的主机如同放在口袋里一样触手可得。
+
+
+
+
+---
+layout: default
+---
+
+# 工具一 · UU 远程:外出主力穿透与图形化“逃生舱”
+
+常规使用内置 SSH 终端,随时一键滑入 macOS 桌面
+
+
+
+
+
+
🎯 主力穿透与最终兜底双重定位
+
出门在外的连接主力。常规使用其内置 SSH 极速打通;当终端遇到瓶颈需要查看复杂 UI、本地 DevTools 或处理特定弹窗时,随时滑入桌面接管。
+
+
+
+
🔍 截图中的实战场景
+
+ - 展示手机屏幕直接操控 Mac mini 上的 Cursor IDE 窗口
+ - 查看
k41 工作区中 SignInDialog 组件代码与未提交变更
+ - 右侧内嵌浏览器:直观观测本地服务端口与页面截图渲染
+
+
+
+
+
💡 极客配置建议
+
将 Mac mini 配合虚拟显示插件(如 BetterDisplay 虚拟 1080p),开启触控惯性指针,单指滑动、双指缩放极度丝滑。
+
+
+
+
+

+
+
+
+
+
+
+---
+layout: default
+---
+
+# 工具二 · Termius + Agent CLI:掌上纯文本极速交互
+
+在家庭局域网与移动网络中,秒级唤醒 YOLO 自治 Agent
+
+
+
+
+
+
🔑 关键痛点解决:SSH 解锁 Keychain
+
+ yule@macmini k41 % mac-unlock-keychain
+ login keychain unlocked
+
+
根除 macOS 无头 SSH 会话中无法读取 Git/SSH 密钥与私有环境变量的痛点。
+
+
+
+
⚡ Agent CLI YOLO 自治模式
+
+ - 终端输入命令(如
ca)调起 CLI Agent
+ - 开启 YOLO 模式:赋予防灾沙盒内的终端自主执行权,自动改文件与跑单测,杜绝手机端频繁弹窗按确认
+
+
+
+
+
📱 Termius(汤米老师)实操利好
+
在家里直接敲局域网 IP 秒连;外出配合穿透无缝重连,后台挂起不掉线。
+
+
+
+
+

+
+
+
+
+
+
+---
+layout: default
+---
+
+# 第三部分 · 多元 Agent 工具生态
+
+终端交互流 vs 免终端轻量流,按需选择交互深度
+
+
+
+
+
+
+
轨 A · 终端交互流 (Direct CLI)
+ 深度聚焦 / 掌控思维链
+
+
直面 Agent 思维脉络与终端日志,适合架构重构与疑难 Debug。
+
+
💻 Termius: SSH 直连宿主机
+
⬇️ ca [yolo]: 自主规划执行、改文件、跑测试
+
⬇️ 实时打断: 随时按 Ctrl+C 调整提示词
+
+
+
核心优势:无中间层包装,真实输出尽在眼底
+
+
+
+
+
+
轨 B · 免终端轻量流 (Lightweight)
+ 碎片时间 / 单手操作
+
+
不用进黑漆漆的命令行,靠原生 App 与群聊即可派活。
+
+
📱 Cursor iOS App: 语音输入意图、思维链可视化
+
💬 Grok / Grok Bot: 群聊直接派单、多 Agent 自动提 PR
+
🔄 云端漫游: 手机讨论进度随时漫游至电脑
+
+
+
核心优势:自然语言沟通,像和真人同事协作一样轻松
+
+
+
+
+
+ 🔄 最终汇流:
+ 无论哪种交互流,最终产物统一汇聚至 Git 分支与 PR,接受统一架构 Review
+
+
+
+
+---
+layout: default
+---
+
+# 工具三 · Cursor iOS 原生客户端:移动端沉浸对齐
+
+触手可及的上下文探索与思维链结构化呈现
+
+
+
+
+
+
📱 原生交互与语音输入优势
+
比起纯 Web 终端,原生 App 对 iOS 键盘与听写输入支持极佳,大拇指按住语音说话,一句话表达复杂设计意图。
+
+
+
+
🧠 思维链与文件探索可视化
+
+ - 清晰展示 Agent 思考耗时(如:
Thought 1s)
+ - 展开折叠 Agent 探索的文件列表(如:
Explored 1 file)
+ - 清晰查看分步规划与执行结论
+
+
+
+
+
🔄 多端会话状态无缝漫游
+
在手机端发起的讨论和代码生成,回到家打开电脑桌面版 Cursor 时,全部历史原封不动同步,无痛接力。
+
+
+
+
+

+
+
+
+
+
+
+---
+layout: default
+---
+
+# 工具四 · Grok Bot 协同群:多 Agent 的“微信办公室”
+
+人类只做需求提出者,AI 员工自行组队、开 PR、互相审查
+
+
+
+
+
+
🤖 Coder Agent 交付 PR
+
自主完成功能开发后,直接在群内同步:
+
+ 底部 build 时间 + commit 的 PR 开好了:
+ https://github.com/yuler/k41/pull/3
+ 页面底部灰色小字显示 构建时间 + 7 位 commit
+ @PR Reviewer 麻烦看一下。
+
+
+
+
+
🛡️ PR Reviewer Agent 自动化评审
+
+ 收到,开始看 #3。
+ 我会本地分别跑一次带构建参数的打包和一次不带的,截图页面底部,用 Cozilla41 在 PR 上留 comment,再回群同步。
+
+
+
+
+
✨ 极客价值:群聊驱动的敏捷协作
+
无需打开专业 IDE,把工程任务拆解给专职 Agent 消化,人类退居幕后。
+
+
+
+
+

+
+
+
+
+
+
+---
+layout: default
+---
+
+# 第四部分 · 架构 Review 与 GitHub 提交
+
+人机双重把关:AI 自述改动,人类掌上审核并按下合并闸门
+
+
+
+
+
+
📋 真实 PR 审查 (#3)
+
+ yuler/k41 #3: [web] Show build time and commit in footer
+ main ← feat/web-build-footer
+
+
由协同机器人账号 Cozilla41 自动发起,附带清晰的改动 Summary 与影响范围。
+
+
+
+
⚙️ 人机双重架构审查标准
+
+ - 步骤一 (AI 自述): Agent 提交时简述架构改动(如本次通过 Vite define 注入编译期常量,通过 Docker ARG 传参)
+ - 步骤二 (人类把关): 在手机 GitHub App 上花 30 秒扫读 Files Changed,确认无越权改动或多余依赖
+
+
+
+
+
👆 单手决策门禁
+
确认 CI 绿灯后,大拇指轻按 Squash and merge。AI 拥有全权编码自由,但终极合并权永远在人类手中。
+
+
+
+
+

+
+
+
+
+
+
+---
+layout: default
+---
+
+# 第五部分 · 最重要的一点:查看结果的两种方式
+
+开发联调态 vs 最终交付态,让移动端验证清晰解耦
+
+
+
+
+
+
+
方式 1 · 开发联调态 (本地内网即时看)
+ 高频调试 / 秒级热更
+
+
改样式、试效果、调交互,不需要频繁推代码到 GitHub。
+
+
1. 宿主机服务启动:
pnpm dev --host 0.0.0.0
+
2. 手机 Safari 访问:
http://<局域网 IP 或 Tailscale IP>:3000
+
3. 体验:
HMR 毫秒级热更新,代码一改手机即变
+
+
+
核心价值:零发布成本,极速试错,不污染 Git 历史
+
+
+
+
+
+
方式 2 · 最终交付态 (线上域名自动部署)
+ 正式验收 / 生产闭环
+
+
功能通过验收准备发布,直通线上真实生产域名。
+
+
1. 前置资产:
自备云服务器 (VPS) + 独立域名
+
2. 流水线打通:
GitHub PR 合并 ➔ GitHub Actions 自动触发
+
3. 生产部署:
构建 Docker 镜像并推送,自动无缝上线
+
+
+
核心价值:100% 工业级自动化交付,亲眼见证生产产物上线
+
+
+
+
+
+ 💡 极客准则:调试改动看内网,正式上线看域名。 两套验证链路互不干扰。
+
+
+
+
+---
+layout: default
+---
+
+# 生产验收实战:秒级反馈的完美正向闭环
+
+合并即上线,手机 Safari 刷新即见最新生产产物
+
+
+
+
+
+
🌐 生产环境即时可观测
+
PR 合并后,GitHub Actions 自动构建 Docker 镜像并推送至 GHCR,Webhook 触发轻量级生产服务器无缝更新。
+
+
+
+
🎯 线上验收实证 (k41.yuler.cc)
+
+ - 切到手机 Safari,直接访问生产域名
+ - 页面 Footer 底部精准显示:
构建 2026-10-xx 23:39:xx + 7 位 commit hash
+ - 验证实际线上环境完全符合预期,零延迟闭环!
+
+
+
+
+
⚡ 极致的正反馈周期
+
从提出需求、Agent 编码、自动评审、手机审查合并到生产环境见效,整个过程不到 5 分钟,全程无需触碰任何实体电脑!
+
+
+
+
+

+
+
+
+
+
+
+---
+layout: default
+---
+
+# 深度硬核实战:三大工程支柱剖析
+
+让“手机打代码”从玩具走向高可用工程实践的底层秘密
+
+
+
+
+
01 · Headless 凭据与网络
+
UU 远程 + Tailscale + Keychain
+
+ • 内外网双通道: 在家局域网极速直连,在外 UU 远程主力穿透 + Tailscale 加密 Mesh 私网兜底。
+ • mac-unlock-keychain: 自动化解开 macOS 登录钥匙串,彻底根除 SSH 会话中无法拉取私钥与 GitHub Token 的顽疾。
+
+
+
+
+
02 · YOLO 机制与多角自治
+
Agent CLI & 角色分权
+
+ • YOLO 自治执行: 给 Agent 赋予终端执行自治权,减少 95% 手机端冗余按键交互。
+ • Coder 与 Reviewer 分权: Coder 负责产出与本地预检;Reviewer 负责双构建对比、截图打标与 PR 留言。
+ • 超大上下文: 256K 超大上下文保持全工程理解,避免手机端反复喂文件。
+
+
+
+
+
03 · 编译注入与双轨验证
+
--host 暴露 + GitHub Actions
+
+ • 联调态内网暴露: 开发服务器绑定 --host 0.0.0.0,手机 Safari 免构建即时验证。
+ • 交付态参数注入: CI 阶段通过 Docker build-arg 捕获精确 commit 和 timestamp。
+ • Vite define: 编译期常数替换,零运行时开销渲染构建信息。
+
+
+
+
+
+
+
+---
+layout: default
+---
+
+# 安全防护网:手机端敲代码如何防止“翻车”?
+
+三道不可跨越的防线,确保生产环境坚如磐石
+
+
+
+
+
1
+
+
网络与身份隔离防线 (Zero-Trust Access)
+
家里的 Mac mini 从不向路由器开放公网端口映射(Port Forwarding)。全部通信收敛在 UU 远程安全加密通道或 Tailscale 虚拟私网内,外部黑客根本探测不到端口。
+
+
+
+
+
2
+
+
Git 分支保护与 CI 门禁防线 (Strict Branch Protection)
+
仓库主分支 (main) 开启保护规则:禁止任何直接 push,只接受 PR 合并。所有 PR 必须强制跑通 TypeScript 静态检查、Linter 格式化规范以及单元测试,绿灯前无法合并。
+
+
+
+
+
3
+
+
人类双眼裁决机制 (Human-in-the-Loop Gatekeeper)
+
AI Agent 拥有全自动编码、自我测试、开 PR 和留评论的全部自由,但唯一没有 Merge 权限。人类只需在手机 GitHub App 上扫一眼 Diff 和 Reviewer 机器人的报告,保留终极控制权。
+
+
+
+
+
+
+
+---
+layout: default
+---
+
+# 踩坑与避坑指南:移动端实操经验谈
+
+血泪经验总结出来的 4 条关键军规
+
+
+
+
+
+ ⚠️ 坑 1:手机输入法自动替换代码符号
+
+
+ 现象: 手机输入法经常会自动把英文半角双引号替换成中文弯引号 “ ”,或者把两个减号替换成长破折号 —,导致脚本执行报错。
+ 解法: 尽可能用自然语言描述意图,不要在手机键盘上手敲复杂的 Shell 指令;或者在 iOS 设置中关闭“智能标点”。
+
+
+
+
+
+ ⚠️ 坑 2:开发服务器未绑定 host 导致手机连不上
+
+
+ 现象: 在电脑上启动了 pnpm dev,但在手机 Safari 里输入内网 IP 却打不开页面。
+ 解法: 很多前端框架默认只监听 localhost (127.0.0.1),必须显式加上 --host 0.0.0.0 才会允许局域网外部设备访问。
+
+
+
+
+
+ ⚠️ 坑 3:SSH 后台挂起被 iOS 系统冻结
+
+
+ 现象: 切到微信看消息,再切回 Termius,SSH 管道断开重连,正在跑的任务丢失交互。
+ 解法: 算力端强依赖 tmux 持久化会话,配合 Termius 开启后台保持与自动重连功能。
+
+
+
+
+
+ ⚠️ 坑 4:任务颗粒度过大导致 Agent 迷航
+
+
+ 现象: Agent 连续修改 10 几个文件,改东坏西,偏离最初目标。
+ 解法: 移动端坚持“小步快跑”微敏捷模式,每个需求拆解为 1~2 个明确改动,Agent 完成率几乎 100%。
+
+
+
+
+
+
+
+---
+layout: default
+---
+
+# 移动端 Vibe Coding 装备清单
+
+极客极简出行流:一部手机搞定全栈开发
+
+
+
+
+
📱 手机端必装软件 (iOS Stack)
+
+
+ Termius(汤米老师) - 核心 SSH 终端管理,支持 Snippets 与手势优化
+ App Store
+
+
+ UU 远程 (UU Remote) - 外出主力穿透,内置 SSH + 备用图形桌面
+ App Store
+
+
+ Cursor iOS / Grok - 原生沉浸对话与群聊异步派单
+ App Store
+
+
+ GitHub App / Safari - 掌上 PR 审查合并与内网/线上双轨验证
+ 原生 / App Store
+
+
+
+
+
+
🖥️ 宿主机基建 (Mac mini + 云端)
+
+
+ 硬件选型 - Mac mini M2/M4(低功耗、免显示器、无额外云成本)
+ 静音待机
+
+
+ Agent CLI - 终端调用 Grok 4.7 Medium YOLO 自治模式
+ CLI 工具
+
+
+ Keychain & Tailscale - 自动化解密脚本与虚拟 Mesh 安全私网
+ 安全底座
+
+
+ 云端资产 - 轻量 VPS 云服务器 + 独立域名 + GitHub Actions CI/CD
+ 自动上线
+
+
+
+
+
+
+
+
+---
+layout: default
+class: text-center
+---
+
+# 总结与思考:未来的开发者长什么样?
+
+
+
+ “计算留在家里,AI 顶在前面,控制握在掌心。”
+
+
+ 纯手机 Vibe Coding 并不是一种博人眼球的妥协,
+ 而是软件工程从“体力劳动(打字编码)”向“脑力决策(架构与意图引导)”演进的必然形态。
+ 无论你身在何处,只要有想法,世界的代码库与生产环境就在你的指尖运转。
+
+
+
+
+
+
项目案例开源地址
+
github.com/yuler/k41
+
+
+
+
线上部署效果预览
+
k41.yuler.cc
+
+
+
+
+ Q & A / 欢迎交流与拍砖
+
+
+
diff --git a/src/lib/slides.ts b/src/lib/slides.ts
new file mode 100644
index 0000000..13adeec
--- /dev/null
+++ b/src/lib/slides.ts
@@ -0,0 +1,165 @@
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import process from 'node:process'
+import { marked } from 'marked'
+
+export interface SlideItem {
+ index: number
+ title: string
+ frontmatter: Record
+ content: string
+ html: string
+ notes: string
+}
+
+export interface SlideDeck {
+ slug: string
+ title: string
+ description?: string
+ frontmatter: Record
+ slides: SlideItem[]
+ slideCount: number
+}
+
+// Custom marked renderer adjustments if needed
+marked.setOptions({
+ gfm: true,
+ breaks: false,
+})
+
+export function parseSlidevContent(content: string, slug: string): { frontmatter: Record, slides: SlideItem[] } {
+ const lines = content.split('\n')
+ const globalFrontmatter: Record = {}
+ let startIdx = 0
+
+ if (lines[0]?.trim() === '---') {
+ for (let i = 1; i < lines.length; i++) {
+ if (lines[i].trim() === '---') {
+ const fmBlock = lines.slice(1, i).join('\n')
+ fmBlock.split('\n').forEach((l) => {
+ const colonIdx = l.indexOf(':')
+ if (colonIdx > 0) {
+ const key = l.slice(0, colonIdx).trim()
+ const val = l.slice(colonIdx + 1).trim()
+ globalFrontmatter[key] = val
+ }
+ })
+ startIdx = i + 1
+ break
+ }
+ }
+ }
+
+ const rawRest = lines.slice(startIdx).join('\n')
+ const rawBlocks = rawRest.split(/\n---\s*\n/)
+ const slides: SlideItem[] = []
+
+ for (let i = 0; i < rawBlocks.length; i++) {
+ const block = rawBlocks[i].trim()
+ if (!block)
+ continue
+
+ const slideFrontmatter: Record = {}
+ let slideContent = block
+
+ // Check if this block is slide-level frontmatter (e.g. layout: default)
+ if (/^(?:layout|class|transition|background):/m.test(block) && !block.includes('#') && i + 1 < rawBlocks.length) {
+ block.split('\n').forEach((l) => {
+ const colonIdx = l.indexOf(':')
+ if (colonIdx > 0) {
+ const key = l.slice(0, colonIdx).trim()
+ const val = l.slice(colonIdx + 1).trim()
+ slideFrontmatter[key] = val
+ }
+ })
+ i++
+ slideContent = rawBlocks[i].trim()
+ }
+
+ // Extract speaker notes
+ const notesMatch = slideContent.match(//)
+ const notes = notesMatch ? notesMatch[1].trim() : ''
+ let cleanContent = slideContent.replace(//g, '').trim()
+
+ // Normalize relative image paths ./images/... -> /slides/{slug}/images/...
+ cleanContent = cleanContent.replace(/src=["']\.\/images\/([^"']+)["']/g, `src="/slides/${slug}/images/$1"`)
+ cleanContent = cleanContent.replace(/src=["']images\/([^"']+)["']/g, `src="/slides/${slug}/images/$1"`)
+
+ // Extract title (first # heading)
+ const headingLine = cleanContent.split('\n').find(line => line.startsWith('# '))
+ const title = headingLine ? headingLine.replace(/^#\s+/, '').trim() : `Slide ${slides.length + 1}`
+
+ // Parse markdown to HTML
+ const html = marked.parse(cleanContent) as string
+
+ slides.push({
+ index: slides.length + 1,
+ title,
+ frontmatter: slideFrontmatter,
+ content: cleanContent,
+ html,
+ notes,
+ })
+ }
+
+ return { frontmatter: globalFrontmatter, slides }
+}
+
+export async function getAllSlideDecks(): Promise {
+ const slidesDir = path.resolve(process.cwd(), 'src/content/slides')
+ try {
+ const entries = await fs.readdir(slidesDir, { withFileTypes: true })
+ const decks: SlideDeck[] = []
+
+ for (const entry of entries) {
+ if (entry.isDirectory()) {
+ const slideFilePath = path.join(slidesDir, entry.name, 'slides.md')
+ try {
+ const content = await fs.readFile(slideFilePath, 'utf-8')
+ const { frontmatter, slides } = parseSlidevContent(content, entry.name)
+ const title = (frontmatter.title as string) || entry.name
+ const description = (frontmatter.description as string) || (frontmatter.info as string) || ''
+
+ decks.push({
+ slug: entry.name,
+ title,
+ description,
+ frontmatter,
+ slides,
+ slideCount: slides.length,
+ })
+ }
+ catch {
+ // No slides.md in this directory
+ }
+ }
+ }
+
+ return decks
+ }
+ catch {
+ return []
+ }
+}
+
+export async function getSlideDeckBySlug(slug: string): Promise {
+ const slideFilePath = path.resolve(process.cwd(), 'src/content/slides', slug, 'slides.md')
+ try {
+ const content = await fs.readFile(slideFilePath, 'utf-8')
+ const { frontmatter, slides } = parseSlidevContent(content, slug)
+ const title = (frontmatter.title as string) || slug
+ const description = (frontmatter.description as string) || (frontmatter.info as string) || ''
+
+ return {
+ slug,
+ title,
+ description,
+ frontmatter,
+ slides,
+ slideCount: slides.length,
+ }
+ }
+ catch {
+ return null
+ }
+}
diff --git a/src/pages/index.astro b/src/pages/index.astro
index efe1c92..dfec05b 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -5,6 +5,7 @@ import LatestThought from '../components/cards/LatestThought.astro'
import Location from '../components/cards/Location.astro'
import Posts from '../components/cards/Posts.astro'
import Profile from '../components/cards/Profile.astro'
+import SlidesCard from '../components/cards/SlidesCard.astro'
import Workouts from '../components/cards/Workouts.astro'
import X from '../components/cards/X.astro'
import Layout from '../layouts/Layout.astro'
@@ -31,18 +32,21 @@ import Layout from '../layouts/Layout.astro'
+
+
+
-
+
diff --git a/src/pages/slides/[...slug].astro b/src/pages/slides/[...slug].astro
new file mode 100644
index 0000000..92a13cf
--- /dev/null
+++ b/src/pages/slides/[...slug].astro
@@ -0,0 +1,668 @@
+---
+import type { SlideDeck } from '../../lib/slides'
+import { getAllSlideDecks } from '../../lib/slides'
+import '../../styles/global.css'
+
+export async function getStaticPaths() {
+ const decks = await getAllSlideDecks()
+ return decks.map(deck => ({
+ params: { slug: deck.slug },
+ props: { deck },
+ }))
+}
+
+interface Props {
+ deck: SlideDeck
+}
+
+const { deck } = Astro.props
+---
+
+
+
+
+
+
+
{deck.title}
+
+
+
+
+ {deck.title}
+
+
+
+
+
+
+
+
+ {deck.slides.map((slide, idx) => (
+
+
+
+
+
+ {idx + 1} / {deck.slideCount}
+ 左右滑动翻页
+
+
+ ))}
+
+
+
+
+
+
+
目录
+
+
+
+ {deck.slides.map((s, idx) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/slides/[slug]/images/[image].ts b/src/pages/slides/[slug]/images/[image].ts
new file mode 100644
index 0000000..bac0a70
--- /dev/null
+++ b/src/pages/slides/[slug]/images/[image].ts
@@ -0,0 +1,52 @@
+import type { APIRoute } from 'astro'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import process from 'node:process'
+
+export async function getStaticPaths() {
+ const slidesDir = path.resolve(process.cwd(), 'src/content/slides')
+ try {
+ const entries = await fs.readdir(slidesDir, { withFileTypes: true })
+ const paths = []
+ for (const entry of entries) {
+ if (entry.isDirectory()) {
+ const imagesDir = path.join(slidesDir, entry.name, 'images')
+ try {
+ const files = await fs.readdir(imagesDir)
+ for (const file of files) {
+ paths.push({
+ params: { slug: entry.name, image: file },
+ props: { filePath: path.join(imagesDir, file) },
+ })
+ }
+ }
+ catch {
+ // No images directory in this slide deck
+ }
+ }
+ }
+ return paths
+ }
+ catch {
+ return []
+ }
+}
+
+export const GET: APIRoute = async ({ props }) => {
+ const file = await fs.readFile(props.filePath)
+ const ext = path.extname(props.filePath).toLowerCase()
+ const mimeTypes: Record
= {
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.png': 'image/png',
+ '.webp': 'image/webp',
+ '.svg': 'image/svg+xml',
+ '.gif': 'image/gif',
+ }
+ return new Response(file, {
+ headers: {
+ 'Content-Type': mimeTypes[ext] || 'application/octet-stream',
+ 'Cache-Control': 'public, max-age=31536000, immutable',
+ },
+ })
+}
diff --git a/src/pages/slides/index.astro b/src/pages/slides/index.astro
new file mode 100644
index 0000000..75ebee9
--- /dev/null
+++ b/src/pages/slides/index.astro
@@ -0,0 +1,122 @@
+---
+import CornerMarkers from '../../components/CornerMarkers.astro'
+import ArrowRight from '../../components/icons/ArrowRight.astro'
+import Layout from '../../layouts/Layout.astro'
+import { getAllSlideDecks } from '../../lib/slides'
+
+const decks = await getAllSlideDecks()
+---
+
+
+
+
+
+
+
+
+
+
+
+
Slides
+
+ {decks.length} {decks.length === 1 ? 'Deck' : 'Decks'}
+
+
+
+ Developer slide decks authored in Slidev-compatible Markdown under src/content/slides/.
+
+
+
+
+
+ {decks.map(deck => (
+
+
+
+
+
+
+ {deck.slideCount} Slides
+
+
+ Slidev Format
+
+
+
+
+ {deck.description || '基于 Mac mini 私有算力与 Grok Bot 多 Agent 协作的移动端极客开发范式。'}
+
+
+ Path: src/content/slides/{deck.slug}/slides.md
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ 💡 本地 Slidev 原生体验与导出
+
+
+ 所有幻灯片均遵循标准 Slidev 规范编写,既可在此页面直接交互播放,也可使用官方 Slidev CLI 获取演讲者模式、画板涂鸦及 PDF/PPTX 导出能力:
+
+
+
# 启动原生 Slidev 演示(支持双屏演讲者备注按 P)
+
pnpm dlx @slidev/cli src/content/slides/mobile-vibe-coding/slides.md --open
+
# 导出为 PowerPoint (.pptx) 或 PDF
+
pnpm dlx @slidev/cli export src/content/slides/mobile-vibe-coding/slides.md --format pptx
+
+
+
+
+
+
+
diff --git a/src/styles/global.css b/src/styles/global.css
index 62be28f..c6ed38b 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -6,6 +6,7 @@
@import '@fontsource/inter/700.css';
@import "tailwindcss";
+@source "../content/slides";
@plugin "@tailwindcss/typography";
/* Global Styles */