<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://zhiyi.cjhong.top</id>
    <title>Gridea</title>
    <updated>2026-10-27T12:49:36.619Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://zhiyi.cjhong.top"/>
    <link rel="self" href="https://zhiyi.cjhong.top/atom.xml"/>
    <subtitle>温故而知新</subtitle>
    <logo>https://zhiyi.cjhong.top/images/avatar.png</logo>
    <icon>https://zhiyi.cjhong.top/favicon.ico</icon>
    <rights>All rights reserved 2026, Gridea</rights>
    <entry>
        <title type="html"><![CDATA[把 Gridea 搬上飞牛 NAS：一次 Web 化改造的完整踩坑记录]]></title>
        <id>https://zhiyi.cjhong.top/post/gridea-on-fnos-web/</id>
        <link href="https://zhiyi.cjhong.top/post/gridea-on-fnos-web/">
        </link>
        <updated>2026-10-21T02:07:34.000Z</updated>
        <summary type="html"><![CDATA[<p>家里那台飞牛 NAS 一直吃灰。我想在上面放一个能随时写博客的客户端——不是把文件传上去、再回电脑编辑那种别扭的用法，而是像在本地一样随手写、随手看。</p>
<p>折腾了大半个月，最大的收获不是「跑起来了」，而是想明白了一件事：</p>
<blockquote>
<p><strong>给没有显示器的 NAS 做桌面应用，正确姿势往往是——别做桌面应用。</strong></p>
</blockquote>
]]></summary>
        <content type="html"><![CDATA[<p>家里那台飞牛 NAS 一直吃灰。我想在上面放一个能随时写博客的客户端——不是把文件传上去、再回电脑编辑那种别扭的用法，而是像在本地一样随手写、随手看。</p>
<p>折腾了大半个月，最大的收获不是「跑起来了」，而是想明白了一件事：</p>
<blockquote>
<p><strong>给没有显示器的 NAS 做桌面应用，正确姿势往往是——别做桌面应用。</strong></p>
</blockquote>
<!-- more -->
<h2 id="一-最初的方案把-electron-塞进-nas">一、最初的方案：把 Electron 塞进 NAS</h2>
<p>Gridea 是 Electron 写的静态博客客户端，界面完整、功能齐全。第一反应当然是原样搬过去：</p>
<pre><code>Xvfb（虚拟屏 :99）→ Gridea（Electron）→ x11vnc（127.0.0.1:5900）→ websockify + noVNC（127.0.0.1:6080）
</code></pre>
<p>飞牛桌面点图标 → 桌面窗口（iframe）里加载 <code>/vnc.html</code> → 看到完整的客户端。</p>
<p>原理上完全可行，我确实把它跑起来了，代价是：</p>
<table>
<thead>
<tr>
<th>问题</th>
<th>表现</th>
</tr>
</thead>
<tbody>
<tr>
<td>依赖一堆系统包</td>
<td>缺 <code>xvfb</code> / <code>x11vnc</code> / <code>novnc</code> / <code>websockify</code> 任一个就起不来</td>
</tr>
<tr>
<td>包体大</td>
<td>包里带一整个 Chromium，72 MB</td>
</tr>
<tr>
<td>内存高</td>
<td>Chromium + VNC 服务端常驻约 520 MB</td>
</tr>
<tr>
<td>画面是「远程桌面」</td>
<td>缩放模糊、输入法/滚动不跟手，本质上是在看视频</td>
</tr>
<tr>
<td>链路长</td>
<td>4 个进程串联，任一环挂掉整页白屏</td>
</tr>
</tbody>
</table>
<p>最后一条尤其要命：白屏的时候，你根本不知道是哪一环断了，只能一个一个翻日志。</p>
<h2 id="二-换个思路它本来就是个网页">二、换个思路：它本来就是个网页</h2>
<p>Gridea 的业务层（文章、标签、菜单、主题、渲染、部署）跟 Electron 一点关系都没有——Electron 只是那层「外壳」。既然 NAS 上根本没有显示器，为什么还要背着一整个 Chromium？</p>
<p>于是我加了一层<strong>传输层抽象</strong>：</p>
<ul>
<li>Electron 端：IPC（<code>ipcMain.handle</code> / <code>ipcRenderer.invoke</code>）</li>
<li>Web 端：HTTP + SSE（<code>/api/ipc/:channel</code> 发请求，<code>/api/events</code> 推消息）</li>
</ul>
<p>业务层一行没改。同一份代码，构建出两种形态：</p>
<ul>
<li>桌面端 → Electron 窗口</li>
<li>服务端 → Express + 一个 HTTP 服务</li>
</ul>
<pre><code>浏览器 / 飞牛桌面 ──┐
                    │  HTTP + SSE
              ┌─────▼───────────────────────────┐
              │ headless.js（Express）           │
              │  /            前端静态产物        │
              │  /api/ipc/:ch  IPC 请求入口       │
              │  /api/events   SSE 推送           │
              │  /preview      渲染好的站点预览     │
              │  :52000        内置 MCP 服务       │
              └─────────────────────────────────┘
                       │
              server/  文章 / 标签 / 主题 / 渲染 / 部署 / MCP
</code></pre>
<p>包体从 72 MB 降到 44.7 MB，常驻内存从 520 MB 降到 120 MB，界面从「远程桌面」变成原生网页。飞牛桌面图标直接指向页面，链路从 4 层缩到 1 层。</p>
<h2 id="三-那些把人卡住半小时的坑">三、那些把人卡住半小时的坑</h2>
<p>这一节是本文的重点，因为它们<strong>全都是看起来毫不相关的错</strong>，而且症状极具迷惑性。</p>
<h3 id="1-fpk-里-node_modules-是空的">1. fpk 里 <code>node_modules</code> 是空的</h3>
<p><code>resources/app/node_modules</code> 目录在，打包进去却是空的，主进程 <code>require</code> 直接崩。依赖收集脚本只扫了 <code>src/</code>，漏了 <code>helpers/</code> 目录。</p>
<p><strong>更坑的是本机跑得好好的</strong>——仓库根目录的 <code>node_modules</code> 把问题盖住了，一到 NAS 上才炸。</p>
<p>修完依赖还不够，我又在打包后加了一道自检：拿<strong>包内实际内容</strong>再扫一遍第三方 <code>require</code>，缺一个就直接中止构建。</p>
<h3 id="2-局域网访问被自己的安全策略挡住">2. 局域网访问被自己的安全策略挡住</h3>
<p>Web 版起来了，浏览器访问 NAS IP 却「拒绝连接」。查了半天，最后发现是 websockify 默认只绑 <code>127.0.0.1</code>——安全的默认值在局域网场景下直接变成了 bug。</p>
<p>改成默认 <code>0.0.0.0</code>，同时保留一个 <code>loopback-only</code> 开关给需要收紧的场景。</p>
<h3 id="3-预览页全白样式一个都不生效">3. 预览页全白，样式一个都不生效</h3>
<p>端口是 0。<code>initServer({listen:false})</code> 之后拿到的 preview port 就是 0，于是渲染时 <code>themeConfig.domain</code> 被写成 <code>http://localhost:0</code>，所有 CSS、图片、链接都指向空端口。</p>
<p>改用占位域名 <code>http://gridea.preview</code>，出 HTML 时按本次请求的真实前缀替换。</p>
<h3 id="4-按钮点了没反应也不报错">4. 按钮点了没反应，也不报错</h3>
<p>MCP 的开关在网页上第一次点不动。所有带参数的 invoke 通道「点了没反应且不报错」。</p>
<p>原因：Web 端的 <code>ipcMain.handle</code> 漏传了 Electron 的第一个 <code>event</code> 参数，导致处理器的业务参数整体<strong>左移了一位</strong>。</p>
<pre><code class="language-ts">// 错：业务参数被当成 event
invokers.set(channel, (...args) =&gt; fn(args[0], ...args.slice(1)))
// 对：补上 event
invokers.set(channel, (...args) =&gt; fn(createWebEvent(), ...args))
</code></pre>
<p>这类「签名约定靠约定」的封装，一旦写错就是全盘静默。</p>
<h3 id="5-渲染静默失败产物是空的">5. 渲染静默失败，产物是空的</h3>
<p><code>__non_webpack_require__ is not defined</code>。<code>renderer.ts</code> 用它加载主题的 <code>style-override.js</code>，但这个标识符只存在于 webpack 产物里，tsc 编译的 Web 版没有。</p>
<p>表现是：构建成功、页面有壳、产物是空的——因为抛错的 Promise 没人接。</p>
<pre><code class="language-ts">const req = typeof __non_webpack_require__ !== 'undefined'
  ? __non_webpack_require__
  : eval('require')
</code></pre>
<h3 id="6-head-一下进程就死了">6. <code>| head</code> 一下，进程就死了</h3>
<p>stdout 的 <code>EPIPE</code> 被升级成了 <code>uncaughtException</code>，直接把进程带走。这个坑很隐蔽：我只是想 <code>tail</code> 一下日志。</p>
<h3 id="7-装完不知道装到哪去了">7. 装完不知道装到哪去了</h3>
<p>fnOS 的应用卷是 <code>/vol2</code>，不是 <code>/vol1</code>。我查 <code>/vol1/@appdata</code> 得出「没装过」的错误结论——其实五个目录都在。</p>
<p>而且<strong>应用中心卸载后不会清干净</strong>：<code>@appmeta</code> / <code>@appconf</code> / <code>@appdata</code> / <code>@apphome</code> / <code>@appshare</code> 五个目录和系统用户都会留在盘上。</p>
<h2 id="四-mcp让-ai-直接写博客">四、MCP：让 AI 直接写博客</h2>
<p>这才是我觉得最值的部分。</p>
<p>Gridea 内置了一个 MCP 服务（Streamable HTTP / JSON-RPC 2.0），暴露 8 个工具：</p>
<table>
<thead>
<tr>
<th>工具</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>gridea_status</code></td>
<td>站点状态、目录、文章数量</td>
</tr>
<tr>
<td><code>gridea_list_posts</code> / <code>gridea_get_post</code></td>
<td>读文章</td>
</tr>
<tr>
<td><code>gridea_create_post</code> / <code>gridea_update_post</code> / <code>gridea_delete_post</code></td>
<td>写文章（复用应用自己的保存逻辑）</td>
</tr>
<tr>
<td><code>gridea_build</code></td>
<td>重新渲染静态站点</td>
</tr>
<tr>
<td><code>gridea_publish</code></td>
<td>推送到远端（GitHub / Gitee / Vercel / SFTP）</td>
</tr>
</tbody>
</table>
<p><strong>本文就是用 MCP 写出来的。</strong> 完整链路：</p>
<pre><code>AI 客户端 ──HTTP POST /mcp (Bearer token)──→ Gridea MCP
                                              │
                                       复用 Posts.savePostToFile()
                                              │
                                    posts/&lt;slug&gt;.md
                                              │
                                       文件监听自动刷新界面
</code></pre>
<p>两个关键设计：</p>
<ol>
<li><strong>写文章复用应用自己的 <code>savePostToFile</code></strong>，不是另写一套序列化。否则 AI 写出来的 front matter 和编辑器保存的格式对不上，界面会解析异常。</li>
<li><strong>Bearer token 强制校验</strong>，并提供「允许局域网访问」开关。默认只绑 <code>127.0.0.1</code>，勾选后才改绑 <code>0.0.0.0</code>；无论开关如何都必须带 token。</li>
</ol>
<h2 id="五-现在的样子">五、现在的样子</h2>
<ul>
<li>飞牛应用中心装一个 44.7 MB 的 fpk，桌面点图标打开就是完整客户端</li>
<li>MCP 开着，AI 可以直接读写文章</li>
<li>文章写在 NAS 上，随时换台设备接着写</li>
<li>桌面端（Windows 便携版）照旧用 Electron，两条线互不干扰</li>
</ul>
<h2 id="六-还没解决的">六、还没解决的</h2>
<ul>
<li>站点部署到 GitHub 那条路还没在 NAS 上跑通（<code>repository</code> 还是空的）</li>
<li>多端同时编辑的冲突处理没做</li>
<li>主题切换后 MCP 端偶发缓存没刷新</li>
</ul>
<hr>
<p>折腾下来，绕的最大一个弯是：<strong>一开始就该问「NAS 上需不需要桌面」，而不是「怎么把桌面搬上去」。</strong> 少背一个 Chromium，少拉一条 VNC 链路，稳定性直接翻倍。</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Hello Gridea]]></title>
        <id>https://zhiyi.cjhong.top/post/hello-gridea/</id>
        <link href="https://zhiyi.cjhong.top/post/hello-gridea/">
        </link>
        <updated>2018-12-11T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[<p>👏  欢迎使用 <strong>Gridea</strong> ！<br>
✍️  <strong>Gridea</strong> 一个静态博客写作客户端。你可以用它来记录你的生活、心情、知识、笔记、创意... ...</p>
]]></summary>
        <content type="html"><![CDATA[<p>👏  欢迎使用 <strong>Gridea</strong> ！<br>
✍️  <strong>Gridea</strong> 一个静态博客写作客户端。你可以用它来记录你的生活、心情、知识、笔记、创意... ...</p>
<!-- more -->
<p><a href="https://github.com/getgridea/gridea">Github</a><br>
<a href="https://gridea.dev/">Gridea 主页</a><br>
<a href="https://fehey.com/">示例网站</a></p>
<h2 id="特性">特性👇</h2>
<p>📝  你可以使用最酷的 <strong>Markdown</strong> 语法，进行快速创作</p>
<p>🌉  你可以给文章配上精美的封面图和在文章任意位置插入图片</p>
<p>🏷️  你可以对文章进行标签分组</p>
<p>📋  你可以自定义菜单，甚至可以创建外部链接菜单</p>
<p>💻  你可以在 <strong>Windows</strong>，<strong>MacOS</strong> 或 <strong>Linux</strong> 设备上使用此客户端</p>
<p>🌎  你可以使用 <strong>𝖦𝗂𝗍𝗁𝗎𝖻 𝖯𝖺𝗀𝖾𝗌</strong> 或 <strong>Coding Pages</strong> 向世界展示，未来将支持更多平台</p>
<p>💬  你可以进行简单的配置，接入 <a href="https://github.com/gitalk/gitalk">Gitalk</a> 或 <a href="https://github.com/SukkaW/DisqusJS">DisqusJS</a> 评论系统</p>
<p>🇬🇧  你可以使用<strong>中文简体</strong>或<strong>英语</strong></p>
<p>🌁  你可以任意使用应用内默认主题或任意第三方主题，强大的主题自定义能力</p>
<p>🖥  你可以自定义源文件夹，利用 OneDrive、百度网盘、iCloud、Dropbox 等进行多设备同步</p>
<p>🌱 当然 <strong>Gridea</strong> 还很年轻，有很多不足，但请相信，它会不停向前 🏃</p>
<p>未来，它一定会成为你离不开的伙伴</p>
<p>尽情发挥你的才华吧！</p>
<p>😘 Enjoy~</p>
]]></content>
    </entry>
</feed>