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 && ( +
+ + +
+

+ Slides + + {decks.length} + +

+ + View all + + +
+ +
+
+

+ + {latest.title} + +

+
+ + {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 时代为什么可以用手机编程? + +告别语法打字,手机是意图控制台,算力与环境后置到主机 + +
+ +
+

+ ❌ 传统移动端编程痛点 +

+ +
+ +
+

+ ✨ AI Native 时代的 Vibe Coding +

+ +
+ +
+ +
+ 💡 核心哲学:计算留在家里,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} + 左右滑动翻页 +
+
+ ))} +
+ + + + +
+ +
+
+ +
+ 1 + / {deck.slideCount} +
+ +
+ + + + + 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.title} + +

+

+ {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 */