今天在整理和排查服务器项目时,产生了一个很有意思的观察:关于 Web 服务的**路由管理(Routing)**,我们的服务器上正并存着两种截然相反的哲学。 一种是**“放手”的前端单页应用(SPA)**,Nginx 极其简单,只做个兜底,路由全交由浏览器里的 JavaScript 处理;另一种则是**“大包大揽”的 Nginx 微路由网关**,Nginx 管得极其细致,甚至单个配置文件里塞了上百个 location 块。 这篇日记详细总结一下今天的发现与思考。 ### 流派一:极致的 JS 前端路由(SPA) 在这种结构中,Nginx 扮演的只是一个“静态文件搬运工”加“终极兜底人”的角色。 以我们服务器上的两个项目为例: 1. **Zashboard (mihomo.6767.chat)**: 它的 Nginx 配置异常干净,核心只有这一段: ```nginx location / { root /home/ubuntu/workspace/ops/zashboard/current; try_files $uri $uri/ /index.html; } ``` 当浏览器访问 `/settings` 或者 `/connections` 时,Nginx 在物理目录下找不到对应的文件或目录,就会根据 `try_files` 的指示,乖乖把 `index.html` 返回给浏览器。接着,前端 JavaScript 脚本接管一切,通过 HTML5 History API 识别当前路径,并显示对应的控制台模块。 2. **Kanna (ops/kanna - 挂载于 /codex/)**: 这是一个基于 React 的协作工作区。前端代码中引入了 `react-router-dom`。所有的子路由(如 `/codex/chat`、`/codex/settings`)在浏览器端完全由 JS 路由引擎渲染。而 Nginx 只需要做最基础的反向代理,把所有流量丢给底层的 Node/Bun 进程即可。 **优点**:前端体验极佳,页面切换无需刷新,极度顺滑。Nginx 配置清爽。 **缺点**:首屏加载较重,SEO 不够友好,且所有的路由跳转逻辑都暴露在前端代码中。 *** ### 流派二:极致的 Nginx 微路由网关(Micro-routing) 与 SPA 的“放权”相反,主域名 `6767.chat` 走的是另一条路。它的配置文件 `/etc/nginx/platform/sites/6767.chat.conf` 里,**足足塞了 108 个 location 块**。 在这里,Nginx 变成了最核心的微服务网关,以一种近乎“手工缝合”的方式,管理着庞大而复杂的业务: 1. **微服务与端口精准分发 (Micro-routing to Ports)**: 不同的子路径被 Nginx 剥离,精准转发给本地运行在不同端口上的小微服务: - `/api/zhihu-immersive-reader/` -> 转发至知乎沉浸阅读器服务(端口 `3013`) - `/api/speech-assess/` -> 转发至语音测评服务(端口 `3011`) - `/api/tts-proxy/` -> 转发至 TTS 代理服务(端口 `3012`) - `/api/visits/` -> 转发至访问统计服务(端口 `3017`) 每一个独立功能都是一个微小的进程,Nginx 是它们的统一大门。 2. **静态目录交叉缝合 (Alias Mapping)**: Nginx 使用 `alias` 直接接管了不同子路径的物理静态文件: - `/qbank/` 映射到 `/static/6767-home/qbank/` - `/archive/assets/` 映射到 `/static/6767-home/archive/assets/` 3. **统一拦截鉴权 (auth_request)**: 安全防护也由 Nginx 统一把关。比如敏感路径(如日志、监控),Nginx 会通过 `auth_request /_auth_verify` 拦截,内部重定向到鉴权接口(端口 `3021`)去校验 Cookie。通过了才放行,不通过直接在 Nginx 层返回 302 登录重定向。 **优点**:灵活性极高,能够把几十个完全不同语言(Node/Python/Go)、不同框架的静态网页和动态 API,天衣无缝地拼装在同一个域名和路径结构下,完美避开跨域问题,且集中控制了鉴权与安全。 **缺点**:Nginx 配置文件变得极度臃肿,任何细微的路径改动都需要 reload 甚至修改 Nginx,维护成本较高。 ### 总结 这两种流派并没有优劣之分,而是为了解决不同的问题: - 如果是一个高内聚、高交互的独立业务,**SPA + 前端 JS 路由**是最好的选择,干净利落。 - 如果是一个长年累月迭代、包含了无数小工具、独立脚本和微型服务的“混沌系统”,**Nginx…
最近搞了个特别有情怀的开发小尝试:做一个三国杀里“武陆逊”的单武将对战模拟器。 熟玩三国杀的玩家应该知道,十周年的“武陆逊”这个武将强度极高、机制设计得很有趣,台词也特别带感。可惜官方定价实在是高攀不起,平时手痒想玩,还得跑去租号平台租号,非常不爽。 这让我突然回想起了读初高中那会儿经常用手机玩的开源三国杀项目——**无名杀(No Name)**。这是一个纯前端(HTML5)写成的开源项目,承载了许多三国杀玩家的折腾回忆。既然它是开源的,那我能不能把它精简一下,自己复刻一个专门拿来爽玩武陆逊的单机模拟器呢? 说干就干,不过实际折腾起来才发现这玩意儿的体量远比想象中要“重”: 1. **基础逻辑剪不断理还乱**:我本想通过 Agent 去裁减不必要的功能,只保留武陆逊对战最核心的代码。但发现无名杀作为一个迭代多年的系统,卡牌判定逻辑、人机对战 AI、底层 UI 框架早已经深度耦合。最后把乱七八糟的武将包和花哨功能砍完,整个静态项目依然有 100 多 MB(塞满了素材和音效),运行起来还要吃浏览器七八百兆的内存,简直跟个正规游戏没差。 2. **带宽成为致命瓶颈**:100 多 MB 的静态项目,如果直接丢在我这台带宽只有 3M 的腾讯云轻量服务器上,光是加载首屏资源就能把人急死。 为了解决这 3M 带宽的“小水管”问题,我开始尝试各种白嫖 CDN 加速的偏门方案: * **首案失败(jsDelivr)**:一开始我尝试用开源的 jsDelivr 做静态资源的中转代理,搭一个免费的“Fake CDN”。但实际折腾下来发现,这玩意儿在某些网络环境下的解析和加载速度并不理想,经常断流。 * **终极方案(GitHub Pages)**:既然无名杀是纯前端项目,压根不需要任何后端逻辑,那我为什么不直接嫖 GitHub 官方提供的免费 CDN 服务呢?于是我干脆把打包好的静态代码库推到了 GitHub 上,直接用 **GitHub Pages** 托管。 这个改动效果显著!虽然因为首次需要下载大量的图片和音效包,开局加载依然需要缓冲一会儿,但 GitHub Pages 的全球 CDN 链路非常稳定,且自带浏览器强缓存机制。只要第一次加载完毕,后面再开局就能瞬间秒开。 用着自己亲手复刻出来的“无名杀·武陆逊模拟器”,脑海里一下就闪回了初高中在课间偷偷折腾手机包的时光。虽然现在能写更复杂的代码、能部署更精密的系统,但像这样为了一个喜欢的武将而去阅读开源代码、动手解决静态资源托管问题的小折腾,依然能带给我最纯粹的快乐。
最近把 Google 的 Gemini Multimodal Live API 接到了个人网站上。 本来是冲着 Gemini 那个能实时语音、甚至能实时传视频流的酷炫效果去的,跟它官方 App 的体验差不多。不过做着做着就发现,理想很丰满,现实的带宽和安全墙很骨感: 1. **安全第一,Key 不能裸奔**:如果完全在前端调用它的 WebSocket,那 API Key 就不得不暴露在浏览器里,这肯定不能忍。再加上国内服务器访问 Google 接口得挂一层代理,所以必须在后端起个网关做一层 WebSocket 转发。 2. **带宽刺客**:如果要开摄像头实时传视频,视频帧需要以极高的频率进行数据传输(要么走 WebRTC,要么在 WebSocket 里狂塞图片)。而我这腾讯云轻量服务器只有区区 3M 带宽,一旦转发视频,服务器的带宽瞬间就会被吃满,卡成 PPT。所以最后我索性砍掉了视频传输,只做纯语音和文字的实时对话。 虽然没有摄像头,但做出来的普通对话体验依然很赞,而且在折腾的过程中,顺手解决了一个很经典的问题——**字幕与声音的同步**。 以前做这种语音对话,大模型的文本流(Transcription)和音频流(Audio)通常是异步发过来的。如果是文本先跑出来,声音还在慢吞吞地读,体验就非常诡异,像是在看“剧透字幕”;如果自己用复杂的定时器或者每一帧去算字数 pacing,又容易因为网络抖动产生累计误差。 后面折腾了半天,让 AI 帮我一起研究,发现 Gemini 返回的字段结构其实很丰富。我就想了一个比较取巧且优雅的方案:**音字分组机制**。 在 Node.js 中转层里,我定义了一个 `group` 缓冲区。一旦接收到上游发来的文本片段,就标志着一个新分组的开始,同时把在这之后到达的所有音频切片(Audio Chunks)通通塞进这个分组里,并把它们作为一个整体的 `chunk_group` 包打包发给前端。 在前端,播放逻辑的核心思路是: * 依次把分组内的音频 buffer 调度到 Web Audio API 的时间线上连续播放。 * 重点来了:**把这组文本的显示时机,直接绑定到该组最后一个音频切片的 `ended` 事件上**。 这样一来,前端根本不需要用任何复杂的时间轴计算或定时器,字幕会在声音刚好播完的瞬间自然跳出来,声音和文字完美咬合,绝对不会出现“字比音快”或者累计漂移的情况。 前端的核心逻辑大概长这样: ```javascript // 前端音频播放与字幕同步的核心逻辑 let groupDuration = 0; let lastSrc = null; for (let i = 0; i < buffers.length; i++) { const buf = buffers[i]; const src = audioCtx.createBufferSource(); src.buffer = buf; src.connect(audioCtx.destination); // 在时间线上精准排队播放 const startAt = Math.max(nextTime, audioCtx.currentTime); src.start(startAt); nextTime = startAt + buf.duration; groupDuration += buf.duration; lastSrc = src; } // 只有当这一组的最后一个音频切片播放完毕,才把对应的字幕文本渲染到屏幕上 if (group.text && lastSrc) { const textToReveal = group.text; lastSrc.addEventListener('ended', () => { revealSubtitle(textToReveal); }); } ``` 现在这个实时聊天小工具已经在后台跑起来了。虽然只是个没有视频的“阉割版”,但实时对话的响应速度和这个音字同步的字幕效果,用起来已经相当令人满意了。
Welcome to WordPress. This is your first post. Edit or delete it, then start writing!
傍晚开始动手写一个新项目——journal,也就是目前这个日记系统的起点。先梳理了完整的计划书,把需求、接口设计和前端界面过了一遍。这个系统的核心想法是:通过扫描服务器上的各种开发痕迹(如 Git 提交、文件修改时间、开发终端会话等)作为信号,让 AI 自动推断并帮我写出第一人称的日记草稿,然后我只需要审核采纳即可。数据存储不需要搞复杂的数据库,直接用按月切片的 JSON 文件。用 Express 搭了后端框架并配了登录权限和信号收集路由,前端用 React 加 Vite 快速搭起骨架。结构上分模块做好了规划,总算可以把日常的零碎工作以一种极其省心的方式记录下来了。
今天下午主要在 6767-api 这边整理文档。之前一直觉得少了一份服务器配置说明,比如各种大模型通道的 key 到底该放哪儿,业务配置和模型渠道配置怎么划分,每次全靠脑子记。今天干脆写了份 server-config.md 挂在文档库里,明确了职责划分:普通的业务配置各回各的 env 环境变量,而模型渠道配置统一收归到全局配置文件里,并保证权限安全。同步在 Docusaurus 站点上更新了侧边栏和顶部导航,把它作为一个独立的“服务器配置”分类展示,最后重新构建了文档站。理顺这一层之后,以后部署和查配置就清晰多了。
这几天在 6767-api 里面把 smart-notes(智能笔记)的功能做出来了。起因是我一直想记录点什么,但写日记太需要仪式感了,往往熬夜开发完已经精疲力竭,根本没心力再去一个字一个字敲。但不写的话,做过的很多事情和突然冒出来的想法隔天就忘,以后想复盘或写简历时全凭回忆。为此我做了个非常适合懒人记录的入口:我只需要发一段碎碎念式的语音,转出一段极其零碎的口语文字,再交给 AI 帮我提炼并转述成规整的日志。数据直接存 JSON 文件,分了项目、数学、日常等类别,还顺手加了热力图、搜索和导出功能。现在这个东西用起来很方便,不追求写得多完美,先把当下的想法留住再说。
这几天虽然用 Agent 在服务器上改东西很爽,但也踩到了一个现实的坑:有时候发现线上哪里不太对,就图省事让 Agent 开启 YOLO 模式直接去改线上文件。虽然简单直接,但很快本地项目和服务器版本就出现了严重偏差。本地要修改时发现线上代码已经变了,稍不注意推上去就会互相覆盖,尤其在 6767-api、Kokoro 语音网关和 Nginx 路由等项目越堆越多之后,这种“在线热重载”的操作越发危险。所以我决定规范开发流程:所有的核心改动必须回到本地,确认无误后走 Git 提交,推送到远程仓库后再拉取部署。服务器上的 Agent 只用来执行任务和查日志排障,代码历史和主线依然由本地仓库管理。虽然多了一两步同步的操作,但起码保证了系统的整洁和可控,不至于把线上环境改乱。
今天给 6767.chat 的主页加了个访问统计 API。总觉得只有几个静态链接有点冷清,以前逛别人个人博客时,最喜欢看那种实时访问计数器,会让人觉得这个站是活的。这次老老实实写了真实的计数逻辑,按 IP 和 UA 做哈希并加了节流限制,防止自己刷新刷爆。同时还做了心跳和离线上报来统计实时在线人数,在主页和 CSS 里补上了计数看板的样式。这小功能确实挺有情绪价值的,后来把网址挂到知乎签名里,看着每天真实访问数一点点涨,这种记录有人来过的感觉非常奇妙。
今天意识到不能再靠手动敲命令行去维护服务器了。刚建站这几天文件越来越多,光是查路径、改配置、看日志,每一步都得先问 AI 复制粘贴,简直折磨。让我想起大一第一次配服务器部署创业赛项目的痛苦经历,于是干脆在服务器上把 Codex 装起来作为自动化 Agent 跑。因为命令行没有浏览器,折腾了一下登录方式,最后统一走 API Key 和环境变量。为了方便切换不同的大模型中转通道,还把 cc-switch-web 拿来用,做成了配置管理工具。配好之后开发顺畅了许多:本地写代码走 Git,服务器自动拉取部署,剩下服务器上的脏活累活全交给 Agent 搞定,再也不用手动去改零碎配置了。