{
    "version": "https://jsonfeed.org/version/1",
    "title": "Biulight 文档 Blog",
    "home_page_url": "https://blog.biulight.top/timeline/blog",
    "description": "Biulight 文档 Blog",
    "items": [
        {
            "id": "https://blog.biulight.top/timeline/blog/shine-ubuntu-server",
            "content_html": "<p>拿到一台新的 Ubuntu 服务器后，可以用 Shine 统一完成常用命令行工具的安装和 shell profile 配置。相比逐条复制安装脚本，Shine 可以先预览将要执行的步骤，并记录已经初始化的项目，方便之后检查状态。</p>\n<p>本文使用适合生产服务器的 <code>minimal</code> profile，安装 Neovim、fzf、bat、eza 和 zoxide，不包含 shell 历史同步、提示符、Node.js 工具链或 Homebrew。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"开始前\">开始前<a href=\"https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E5%BC%80%E5%A7%8B%E5%89%8D\" class=\"hash-link\" aria-label=\"开始前的直接链接\" title=\"开始前的直接链接\" translate=\"no\">​</a></h2>\n<p>请先确认：</p>\n<ul>\n<li class=\"\">当前系统为 Ubuntu；</li>\n<li class=\"\">服务器可以访问 GitHub 等软件下载源；</li>\n<li class=\"\">当前用户可以在需要时使用 <code>sudo</code>。</li>\n</ul>\n<p>系统初始化可能安装软件包、下载程序并更新 shell profile。正式执行前先使用 <code>--dry-run</code> 检查实际步骤。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"安装-shine\">安装 Shine<a href=\"https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E5%AE%89%E8%A3%85-shine\" class=\"hash-link\" aria-label=\"安装 Shine的直接链接\" title=\"安装 Shine的直接链接\" translate=\"no\">​</a></h2>\n<p>运行官方安装脚本：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">curl -fsSL https://github.com/biulight/shine/releases/latest/download/install.sh | sh</span><br></span></code></pre></div></div>\n<p>默认安装位置是 <code>~/.local/bin/shine</code>，安装脚本不会修改 shell 配置。确认 <code>~/.local/bin</code> 已加入 <code>PATH</code>，再检查版本：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">export PATH=\"$HOME/.local/bin:$PATH\"</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine --version</span><br></span></code></pre></div></div>\n<p>需要设置永久 <code>PATH</code>、指定安装目录或固定版本时，请查看 Shine 的<a href=\"https://biulight.github.io/shine/zh-Hans/installation\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">安装与升级</a>说明。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"预览服务器配置\">预览服务器配置<a href=\"https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E9%A2%84%E8%A7%88%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%85%8D%E7%BD%AE\" class=\"hash-link\" aria-label=\"预览服务器配置的直接链接\" title=\"预览服务器配置的直接链接\" translate=\"no\">​</a></h2>\n<p>先查看当前版本为 Ubuntu 提供的初始化项目：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine sys list</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine sys bootstrap --preset minimal --dry-run</span><br></span></code></pre></div></div>\n<p>第二条命令只打印选择结果、脚本调用和 profile 更新，不修改系统。确认项目和目标路径符合预期后，再继续执行。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"应用-minimal-profile\">应用 minimal profile<a href=\"https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E5%BA%94%E7%94%A8-minimal-profile\" class=\"hash-link\" aria-label=\"应用 minimal profile的直接链接\" title=\"应用 minimal profile的直接链接\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine sys bootstrap --preset minimal</span><br></span></code></pre></div></div>\n<p>过程中可能因为安装系统软件而请求 <code>sudo</code> 权限。完成后查看 Shine 记录的初始化状态：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine sys status</span><br></span></code></pre></div></div>\n<p>如果以后想检查这些软件是否有可用更新，可以运行：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine sys update</span><br></span></code></pre></div></div>\n<p>该命令只检查由 <code>shine sys bootstrap</code> 记录的软件，不会自动安装升级。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"需要更完整的终端环境\">需要更完整的终端环境<a href=\"https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E9%9C%80%E8%A6%81%E6%9B%B4%E5%AE%8C%E6%95%B4%E7%9A%84%E7%BB%88%E7%AB%AF%E7%8E%AF%E5%A2%83\" class=\"hash-link\" aria-label=\"需要更完整的终端环境的直接链接\" title=\"需要更完整的终端环境的直接链接\" translate=\"no\">​</a></h2>\n<p><code>minimal</code> 适合保持精简的生产服务器。如果这是开发机，或你还需要 AstroNvim、Atuin、Yazi、Starship 和 zsh-vi-mode，可以先预览 <code>recommended</code> profile：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine sys bootstrap --preset recommended --dry-run</span><br></span></code></pre></div></div>\n<p>确认后去掉 <code>--dry-run</code> 即可应用。Ubuntu 还提供包含 ZeroTier、pnpm、mise 和 Homebrew 的 <code>all</code> profile；建议先对照<a href=\"https://biulight.github.io/shine/zh-Hans/reference/built-in-presets#%E7%B3%BB%E7%BB%9F%E9%A2%84%E8%AE%BE\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">内置预设清单</a>，不要在服务器上安装不需要的组件。</p>\n<p>更多交互选择、下载代理和受管系统配置的用法，见 Shine 中文手册的<a href=\"https://biulight.github.io/shine/zh-Hans/guides/system-init\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">初始化与管理系统</a>。</p>",
            "url": "https://blog.biulight.top/timeline/blog/shine-ubuntu-server",
            "title": "使用 Shine 快速配置 Ubuntu 服务器",
            "summary": "拿到一台新的 Ubuntu 服务器后，可以用 Shine 统一完成常用命令行工具的安装和 shell profile 配置。相比逐条复制安装脚本，Shine 可以先预览将要执行的步骤，并记录已经初始化的项目，方便之后检查状态。",
            "date_modified": "2026-08-19T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "Shine",
                "Linux",
                "Ubuntu"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/ai-in-production-migration",
            "content_html": "<p>AI 可以很快读完一个仓库、列出迁移清单、批量修改代码；但在一个缺少业务和技术文档、仍在生产运行的\n系统里，“改得快”并不等于“可以上线”。这次 Vue 2 → Vue 3 迁移让我更确信：AI 最合适的位置不是\n替代工程判断，而是进入一套有阶段、有证据、有回归边界的交付流程。</p>\n<p>这篇文章不讨论 Vue API 本身，而是记录 AI 如何参与一个陌生生产系统的迁移：先收集事实、生成可审查\n计划，再分批实施、手工验收，并把每次发现沉淀成下一轮可复用的规则。有关 Vue 迁移的技术细节见\n<a class=\"\" href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration\">keystone-web 从 Vue 2 到 Vue 3：一场两段式迁移的复盘</a>。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"起点代码很多文档很少不能假装自己已经理解业务\">起点：代码很多，文档很少，不能假装自己已经理解业务<a href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E8%B5%B7%E7%82%B9%E4%BB%A3%E7%A0%81%E5%BE%88%E5%A4%9A%E6%96%87%E6%A1%A3%E5%BE%88%E5%B0%91%E4%B8%8D%E8%83%BD%E5%81%87%E8%A3%85%E8%87%AA%E5%B7%B1%E5%B7%B2%E7%BB%8F%E7%90%86%E8%A7%A3%E4%B8%9A%E5%8A%A1\" class=\"hash-link\" aria-label=\"起点：代码很多，文档很少，不能假装自己已经理解业务的直接链接\" title=\"起点：代码很多，文档很少，不能假装自己已经理解业务的直接链接\" translate=\"no\">​</a></h2>\n<p>项目是一个中大型管理后台，有数百个 Vue 组件、权限路由、复杂表格、多个媒体类型的业务流程，以及\n持续使用中的生产环境。接手时我并不熟悉业务，相关技术与业务文档也不完整。</p>\n<p>在这种条件下，最危险的 AI 使用方式是把问题简化成一句“升级到 Vue 3”，然后接受一份看似完整的\n改动。模型可以根据常见迁移模式给出合理建议，却不知道某个 <code>params</code> 是否承载业务身份、某个表格的\n事件是否被下游依赖，或者一个管理员账号是否绕过了普通用户必经的权限路径。</p>\n<p>因此先确定了一个原则：<strong>AI 不能以推断取代项目事实；每个结论都要有代码、测试、构建结果或手工观察\n作为落点。</strong></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"先降低未知数不要同时替换所有基础设施\">先降低未知数：不要同时替换所有基础设施<a href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E5%85%88%E9%99%8D%E4%BD%8E%E6%9C%AA%E7%9F%A5%E6%95%B0%E4%B8%8D%E8%A6%81%E5%90%8C%E6%97%B6%E6%9B%BF%E6%8D%A2%E6%89%80%E6%9C%89%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD\" class=\"hash-link\" aria-label=\"先降低未知数：不要同时替换所有基础设施的直接链接\" title=\"先降低未知数：不要同时替换所有基础设施的直接链接\" translate=\"no\">​</a></h2>\n<p>迁移被拆成两段。第一段先将 Vue CLI 迁到 Rsbuild，但仍保持 Vue 2.7 上线。随后用约半年的稳定运行期\n理解系统、清理无用代码与技术债，并在新改动中优先采用 Vue 2.7 和 Vue 3 共用的能力，例如\nComposition API 和新插槽语法。</p>\n<p>这一步不是“拖慢升级”，而是在为 AI 和人都创造更好的问题边界。等正式开始 Vue 3 迁移时，构建器、\n环境变量、开发与生产构建已经经过验证；排查一个问题时，不必再同时怀疑 Rsbuild、Vue 运行时和业务\n逻辑。先减少变量，之后的自动化才有可信的上下文。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"用-ai-写计划前先给它经过核实的输入\">用 AI 写计划前，先给它经过核实的输入<a href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E7%94%A8-ai-%E5%86%99%E8%AE%A1%E5%88%92%E5%89%8D%E5%85%88%E7%BB%99%E5%AE%83%E7%BB%8F%E8%BF%87%E6%A0%B8%E5%AE%9E%E7%9A%84%E8%BE%93%E5%85%A5\" class=\"hash-link\" aria-label=\"用 AI 写计划前，先给它经过核实的输入的直接链接\" title=\"用 AI 写计划前，先给它经过核实的输入的直接链接\" translate=\"no\">​</a></h2>\n<p>计划阶段使用 Fable 5，但它不是从一句模糊需求开始猜。输入先由全仓检索和依赖排查组成，包括：</p>\n<ul>\n<li class=\"\">331 个 <code>.vue</code> 组件的规模及业务模块分布；</li>\n<li class=\"\">151 处 <code>.sync</code>、122 个 <code>slot=\"...\"</code> 命中、全局和局部指令的实际数量；</li>\n<li class=\"\">Vue Router、Vuex、Element UI、VXE Table、测试工具与构建配置的现状；</li>\n<li class=\"\">哪些高风险封装需要人工审查，哪些模式可以批量处理；</li>\n<li class=\"\">不做 UI 重设计、不换状态管理、不换构建器等范围边界。</li>\n</ul>\n<p>Fable 5 在这些事实之上生成迁移策略和 PRD：比较全量重写、长期 compat、微前端隔离三种路线；定义\n基线、基础设施、第三方库、机械替换、公共组件、业务模块、测试升级和收尾验收等阶段；再为每一批\n列出依赖关系和验证方式。</p>\n<p>这里真正有价值的不是“AI 写出了一份很长的计划”，而是计划可以被审查、被纠正、被执行。比如\n<code>slot=\"...\"</code> 的 122 个命中并不等于 122 个模板迁移点：其中混有 JSX 中合法的属性和注释。把这个\n前提写进 PRD，就能阻止后续执行模型做危险的全局替换。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"实施时把模型当作分批交付者而不是一次性重写器\">实施时，把模型当作分批交付者，而不是一次性重写器<a href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E5%AE%9E%E6%96%BD%E6%97%B6%E6%8A%8A%E6%A8%A1%E5%9E%8B%E5%BD%93%E4%BD%9C%E5%88%86%E6%89%B9%E4%BA%A4%E4%BB%98%E8%80%85%E8%80%8C%E4%B8%8D%E6%98%AF%E4%B8%80%E6%AC%A1%E6%80%A7%E9%87%8D%E5%86%99%E5%99%A8\" class=\"hash-link\" aria-label=\"实施时，把模型当作分批交付者，而不是一次性重写器的直接链接\" title=\"实施时，把模型当作分批交付者，而不是一次性重写器的直接链接\" translate=\"no\">​</a></h2>\n<p>Opus 4.7 与 Sonnet 5 按 PRD 分批实施。每个批次都有清楚的范围和退出条件：例如先让基础设施能进入\n登录页，再替换 Element UI 和 VXE Table，并用真实列表页验证；之后才处理 <code>.sync</code>、插槽、指令等\n全仓变化；最后按公共组件、业务模块的依赖顺序推进。</p>\n<p>这套节奏给 AI 加了三层约束：</p>\n<ol>\n<li class=\"\"><strong>改动范围小。</strong> 每一批聚焦一类依赖或一个模块，失败时能回到明确的 diff。</li>\n<li class=\"\"><strong>验证紧随改动。</strong> 构建、lint、单测、E2E 和手工页面验证按阶段执行，不等到最后才发现问题。</li>\n<li class=\"\"><strong>不夹带“顺手优化”。</strong> 迁移期间发现的无关重构只记录，不在同一批改动中扩张范围。</li>\n</ol>\n<p>模型很擅长重复性工作：全仓盘点、模式转换、补齐大量相似调用点、汇总失败清单。它不擅长在缺少上下文\n时猜测某个行为是否是业务约定。因此，批次拆分的意义不是限制 AI 的速度，而是让每次推断都有机会被\n最小范围的验证推翻。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"真实反例一个看似合理的路由规则如何造成回归\">真实反例：一个看似合理的路由规则如何造成回归<a href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E7%9C%9F%E5%AE%9E%E5%8F%8D%E4%BE%8B%E4%B8%80%E4%B8%AA%E7%9C%8B%E4%BC%BC%E5%90%88%E7%90%86%E7%9A%84%E8%B7%AF%E7%94%B1%E8%A7%84%E5%88%99%E5%A6%82%E4%BD%95%E9%80%A0%E6%88%90%E5%9B%9E%E5%BD%92\" class=\"hash-link\" aria-label=\"真实反例：一个看似合理的路由规则如何造成回归的直接链接\" title=\"真实反例：一个看似合理的路由规则如何造成回归的直接链接\" translate=\"no\">​</a></h2>\n<p>最有教育意义的不是成功案例，而是一次错误的启发式。</p>\n<p>迁移 Vue Router 3 到 4 时，曾把“内部 <code>router.push()</code> 应使用 <code>query</code>，构造可分享链接的\n<code>router.resolve()</code> 应使用 <code>params</code>”当作批量修改规则。这个规则很顺口，但它是错的。</p>\n<p>真正决定参数位置的是目标路由的 <code>path</code>：未声明动态段的值使用 <code>query</code>；必填动态段必须使用\n<code>params</code>；可选动态段再按约定选择。错误规则把某个内部跳转的必填 <code>id</code> 放进 <code>query</code>，结果在点击列表\n行时同步抛出 <code>Missing required param</code>，跳转被直接阻断。</p>\n<p>这次错误没有被隐藏。它被修复后，完整的根因、适用规则和反例被写进迁移记录，并进一步沉淀为项目\n记忆。下一轮 Claude Code 会话不需要“记得上一次聊天”，只需要读取这条规则。</p>\n<p>我认为这正是 AI 编程在生产环境里最重要的能力之一：<strong>把失败从一次对话中的偶然经验，变成仓库中可\n检查、可复用、可更新的知识。</strong></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"测试覆盖不是安全感手工验收负责发现模型和用例的共同盲区\">测试覆盖不是安全感：手工验收负责发现模型和用例的共同盲区<a href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E6%B5%8B%E8%AF%95%E8%A6%86%E7%9B%96%E4%B8%8D%E6%98%AF%E5%AE%89%E5%85%A8%E6%84%9F%E6%89%8B%E5%B7%A5%E9%AA%8C%E6%94%B6%E8%B4%9F%E8%B4%A3%E5%8F%91%E7%8E%B0%E6%A8%A1%E5%9E%8B%E5%92%8C%E7%94%A8%E4%BE%8B%E7%9A%84%E5%85%B1%E5%90%8C%E7%9B%B2%E5%8C%BA\" class=\"hash-link\" aria-label=\"测试覆盖不是安全感：手工验收负责发现模型和用例的共同盲区的直接链接\" title=\"测试覆盖不是安全感：手工验收负责发现模型和用例的共同盲区的直接链接\" translate=\"no\">​</a></h2>\n<p>自动化用例能保证核心路径持续可跑，却不天然保证覆盖正确。一次手工回归发现，已有 Playwright 冒烟\n只用管理员账号登录；管理员恰好绕开了普通角色才会走到的 Campaign 详情重定向，因此上述路由回归\n没有被 E2E 捕获。</p>\n<p>手工验收还发现过很多“构建、lint、单测都正常”的问题：动态组件在页面显示 <code>[object Promise]</code>、\nElement Plus 日期控件因格式 token 或裸 <code>default-time</code> 解析失败而显示错误日期、全局 passive 事件\n补丁让下拉组件的鼠标选择失效。这些问题往往需要真实数据、空状态、特定角色或连续交互才能出现。</p>\n<p>所以手工验收不是自动化测试的低配替代，而是专门覆盖高不确定性区域的另一层测试。对管理后台而言，\n至少应显式覆盖：</p>\n<ul>\n<li class=\"\">管理员与非管理员的菜单、深链接和权限拦截；</li>\n<li class=\"\">有数据、空数据、可清空字段和默认值；</li>\n<li class=\"\">表格分页、筛选、滚动、弹窗、Popover、上传和动态组件；</li>\n<li class=\"\">控制台中的框架、路由和组件库警告。</li>\n</ul>\n<p>每个手工发现都应有去向：补成自动化用例、加入手工清单，或提炼为迁移规则。没有去向的“已修复”只是\n下一次回归的候选。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"ai-需要的不只是上下文窗口而是项目记忆系统\">AI 需要的不只是上下文窗口，而是项目记忆系统<a href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration#ai-%E9%9C%80%E8%A6%81%E7%9A%84%E4%B8%8D%E5%8F%AA%E6%98%AF%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AA%97%E5%8F%A3%E8%80%8C%E6%98%AF%E9%A1%B9%E7%9B%AE%E8%AE%B0%E5%BF%86%E7%B3%BB%E7%BB%9F\" class=\"hash-link\" aria-label=\"AI 需要的不只是上下文窗口，而是项目记忆系统的直接链接\" title=\"AI 需要的不只是上下文窗口，而是项目记忆系统的直接链接\" translate=\"no\">​</a></h2>\n<p>这次实践中，迁移信息被拆成不同层次保存：</p>\n<table><thead><tr><th>载体</th><th>负责回答的问题</th></tr></thead><tbody><tr><td>迁移 PRD</td><td>为什么采用这条路线、阶段如何依赖、每批如何验收</td></tr><tr><td>迁移问题记录</td><td>具体故障的现象、根因、修复方式与回归规则</td></tr><tr><td>项目 AGENTS.md</td><td>当前技术栈、开发约束、验证命令和禁止重新引入的兼容层</td></tr><tr><td>项目记忆</td><td>一条会在后续任务反复使用的精确规则，例如路由参数边界</td></tr><tr><td>Git 提交与测试</td><td>改动的可审查证据、可回退边界与行为验证</td></tr></tbody></table>\n<p>这种分层比把所有信息塞进一个超长提示词更可靠。提示词会过期、上下文会被截断、不同模型的理解也会\n不同；而仓库里的文档、规则和测试可以随实现变化一起维护。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"我会继续坚持的边界\">我会继续坚持的边界<a href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E6%88%91%E4%BC%9A%E7%BB%A7%E7%BB%AD%E5%9D%9A%E6%8C%81%E7%9A%84%E8%BE%B9%E7%95%8C\" class=\"hash-link\" aria-label=\"我会继续坚持的边界的直接链接\" title=\"我会继续坚持的边界的直接链接\" translate=\"no\">​</a></h2>\n<p>AI 参与生产改造时，我会把下面几条当作默认约束：</p>\n<ol>\n<li class=\"\">让 AI 先完成全仓盘点和证据收集，再由人工审查盘点结论、范围与风险后制定方案。</li>\n<li class=\"\">将大迁移拆成可独立验证、可回退的小批次。</li>\n<li class=\"\">对 API 行为、权限、路由和组件库差异，优先相信源码、测试和真实页面，而不是“通常如此”。</li>\n<li class=\"\">把模型造成或发现的错误写成规则和测试，而不是只留在聊天记录里。</li>\n<li class=\"\">保留人工验收，特别是角色、空状态、视觉和连续交互路径。</li>\n<li class=\"\">不把迁移、性能优化和业务重构混在同一轮交付中。</li>\n</ol>\n<p>AI 没有让生产迁移变成零风险工作；它减少的是盘点、重复修改和信息整理的成本。真正让风险可控的，\n仍然是清晰的范围、持续的验证、可回退的提交，以及愿意把错误沉淀下来的人。</p>",
            "url": "https://blog.biulight.top/timeline/blog/ai-in-production-migration",
            "title": "AI 参与生产系统迁移：把模型能力放进可验证的交付流程",
            "summary": "AI 可以很快读完一个仓库、列出迁移清单、批量修改代码；但在一个缺少业务和技术文档、仍在生产运行的",
            "date_modified": "2026-08-07T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "AI",
                "Claude Code",
                "Fable",
                "Opus",
                "Sonnet",
                "Software Engineering",
                "Migration"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration",
            "content_html": "<p>把一个有 331 个 <code>.vue</code> 组件、仍在持续迭代的管理后台从 Vue 2 迁到 Vue 3，最危险的想法是：\n“把依赖升级完，再把报错逐个修掉。”真正的迁移对象并不只有 Vue，而是路由、状态管理、UI 库、\n表格封装、构建链、测试和数百个已经被用户使用的交互。</p>\n<p>这次升级实际上分成了两段：先将 Vue 2.x + Vue CLI 迁到 Vue 2.7 + Rsbuild 并上线；随后让它在线上\n稳定运行约半年，在这段时间持续清理技术债、删除不再需要的代码、尽量精简实现；最后才将已经稳定的\nVue 2.7 应用升级到 Vue 3。第二段在专用分支中阶段化完成，旧版本继续承接发布，迁移版本验收后再\n整体切换。它没有演变成业务重写，也没有趁机换 Pinia 或 Vite；这份克制比任何 API 转换技巧都更重要。</p>\n<table><thead><tr><th>阶段</th><th>主要工作</th><th>结束条件</th></tr></thead><tbody><tr><td>第一段：构建迁移</td><td>Vue CLI → Vue 2.7 + Rsbuild，保持业务行为</td><td>新构建链上线并稳定运行</td></tr><tr><td>稳定与清债期</td><td>熟悉业务边界，删除无用代码，使用跨 Vue 2.7 / Vue 3 的 API</td><td>业务边界逐步明确，待迁移代码范围收敛</td></tr><tr><td>第二段：运行时迁移</td><td>Vue 3、Router 4、Element Plus、VXE Table 4 与测试体系升级</td><td>移除兼容层，完成自动化与手工验收</td></tr></tbody></table>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"第一段先把构建系统换掉再给业务留出稳定期\">第一段：先把构建系统换掉，再给业务留出稳定期<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E7%AC%AC%E4%B8%80%E6%AE%B5%E5%85%88%E6%8A%8A%E6%9E%84%E5%BB%BA%E7%B3%BB%E7%BB%9F%E6%8D%A2%E6%8E%89%E5%86%8D%E7%BB%99%E4%B8%9A%E5%8A%A1%E7%95%99%E5%87%BA%E7%A8%B3%E5%AE%9A%E6%9C%9F\" class=\"hash-link\" aria-label=\"第一段：先把构建系统换掉，再给业务留出稳定期的直接链接\" title=\"第一段：先把构建系统换掉，再给业务留出稳定期的直接链接\" translate=\"no\">​</a></h2>\n<p>第一段从 Vue CLI 5 迁到 Rsbuild 时，应用仍运行在 Vue 2.7。目标不是借机升级框架，而是将构建链\n现代化并消除已经没有价值的历史配置：移除 Vue CLI 及其插件、删除 <code>vue.config.js</code> 等旧配置，改用\n<code>rsbuild.config.mjs</code>；SVG 从不兼容 Rspack 的 <code>svg-sprite-loader</code> 改为原生 <code>asset/source</code> 加载，\n组件的使用方式保持不变。</p>\n<p>采用这条路线也有现实原因：接手时我对这个项目并不熟悉，手上缺少完整的业务与技术文档。此时若同时\n替换构建器、运行时、组件库并修改业务逻辑，任何异常都很难回答“它来自旧行为、构建链、Vue 3，还是\n我尚未理解的业务规则”。先把范围收缩为 Vue 2.7 + Rsbuild 上线，才能在不改变主要业务语义的前提下\n建立对系统的认识。</p>\n<p>这次迁移也统一了浏览器端环境变量边界。业务代码从 <code>process.env.VUE_APP_*</code> 改为\n<code>import.meta.env.VUE_APP_*</code>；少数第三方包所需的 <code>process.env</code> 则只在构建配置中提供兼容 shim。\n这样既避免浏览器里的 <code>process is not defined</code>，也不需要为了兼容旧写法继续在业务代码中传播\nNode.js 运行时假设。</p>\n<p>Rsbuild 上线后，项目在 Vue 2.7 状态继续正常迭代约半年。这段稳定期的重点不是继续验证已经上线的\n构建工具，而是让团队通过日常需求和真实业务流程，逐步确认业务边界与历史行为。在这个过程中，团队\n持续清理技术债、删除非必要代码，让后续需要维护和迁移的范围更小、更清晰。等到 Vue 3 迁移启动时，\n面对的是一套业务基线清楚、代码范围已经收敛的系统，不再需要一边迁移一边猜测旧逻辑。</p>\n<p>这也是两段式路线最大的收益——先完成构建工具切换，再留出理解业务和收敛代码的稳定阶段，避免把\n构建系统变化、Vue 运行时升级和组件库替换压在同一批改动中。第二阶段因此可以把注意力集中在 Vue 3\n迁移本身。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"vue-27-不是终点而是-vue-3-的预备跑道\">Vue 2.7 不是终点，而是 Vue 3 的预备跑道<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#vue-27-%E4%B8%8D%E6%98%AF%E7%BB%88%E7%82%B9%E8%80%8C%E6%98%AF-vue-3-%E7%9A%84%E9%A2%84%E5%A4%87%E8%B7%91%E9%81%93\" class=\"hash-link\" aria-label=\"Vue 2.7 不是终点，而是 Vue 3 的预备跑道的直接链接\" title=\"Vue 2.7 不是终点，而是 Vue 3 的预备跑道的直接链接\" translate=\"no\">​</a></h2>\n<p>这半年的重构并非只做删除。Vue 2.7 原生提供了 Composition API，因此在新增或调整逻辑时，优先选用\n<code>ref</code>、<code>reactive</code>、<code>computed</code>、<code>watch</code>、<code>onMounted</code> 等 Vue 2.7 与 Vue 3 都支持的能力；保留既有\nOptions API，但不再继续扩散只服务于 Vue 2 的写法。这样做不会改变已经稳定的页面风格，却能让每一次\n局部维护都向 Vue 3 靠近。</p>\n<p><code>this.$set</code> 是典型例子。Vue 2 需要它来让新增对象属性或数组索引更新具备响应式；Vue 3 基于 Proxy，\n普通赋值已经会响应。预备期内，改到相关代码时就倾向写成 <code>object[key] = value</code> 或\n<code>array[index] = value</code>，而不是继续引入 <code>$set</code>。这既减少了后续迁移的工作量，也让代码表达的意图更\n直接。</p>\n<p>ESLint 在这个过程中承担的是“逐步收紧的迁移护栏”，而不是一次性把历史代码全部打红。Vue 2.7 阶段，\n为避免存量项目立刻被遗留写法阻断，曾对 <code>v-bind.sync</code>、<code>$set</code> / <code>$delete</code> 相关的废弃规则保留豁免；\n同时约束新增或重构代码不要继续引入这些 Vue 2 专属模式。正式切到 Vue 3 后，配置目标才改为 Vue 3.5\n并移除豁免，让 lint 明确报出 <code>.sync</code>、旧销毁生命周期、<code>.native</code>、<code>$listeners</code> 等不再可用的写法，\n而不是让它们在运行时才暴露。</p>\n<p>这条护栏最终找出了 18 个文件中的 48 处 <code>$set</code> 调用，并将它们全部替换为普通对象或数组赋值；项目里\n没有实际的 <code>$delete</code> 调用。14 个文件中的 20 处 <code>.native</code> 以及 <code>destroyed</code> / <code>beforeDestroy</code> 生命周期，\n则属于<strong>切换到 Vue 3 运行时之后</strong>才能处理的破坏性变更：它们分别改为 Vue 3 的事件透传方式和\n<code>unmounted</code> / <code>beforeUnmount</code>。后者不能提前放进 Vue 2.7 代码库。重要的不是这些数字本身，而是节奏：\n先让新的维护代码不再制造 Vue 2 专属债务，再在运行时切换后用规则集中清理只能由 Vue 3 支持的写法。</p>\n<p>插槽也遵循同一原则。Vue 2.7 已支持 Vue 3 使用的 <code>v-slot</code> / <code>#</code> 语法，因此在持续重构中，模板插槽\n应从旧的 <code>slot=\"name\"</code>、<code>slot-scope=\"scope\"</code> 迁向 <code>#name=\"scope\"</code>。例如：</p>\n<div class=\"language-vue codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-vue codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;!-- Vue 2 旧写法：Vue 3 不支持 --&gt;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;template slot=\"default\" slot-scope=\"scope\"&gt;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  {{ scope.row.name }}</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;/template&gt;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;!-- Vue 2.7 与 Vue 3 都支持 --&gt;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;template #default=\"scope\"&gt;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  {{ scope.row.name }}</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;/template&gt;</span><br></span></code></pre></div></div>\n<p>不过这不是一次安全的全文替换。全仓检索到 122 个 <code>slot=\"...\"</code> 命中，其中混有真正需要迁移的模板、\nJSX render 中合法的 <code>slot</code> 属性，以及注释里的死代码。正确做法是先让新增和改动的模板采用\n<code>v-slot</code>，再在 Vue 3 阶段逐个分类处理存量命中；把 JSX 一并替换反而会制造新的问题。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"ai-的角色先生成可审查的计划再分批执行\">AI 的角色：先生成可审查的计划，再分批执行<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#ai-%E7%9A%84%E8%A7%92%E8%89%B2%E5%85%88%E7%94%9F%E6%88%90%E5%8F%AF%E5%AE%A1%E6%9F%A5%E7%9A%84%E8%AE%A1%E5%88%92%E5%86%8D%E5%88%86%E6%89%B9%E6%89%A7%E8%A1%8C\" class=\"hash-link\" aria-label=\"AI 的角色：先生成可审查的计划，再分批执行的直接链接\" title=\"AI 的角色：先生成可审查的计划，再分批执行的直接链接\" translate=\"no\">​</a></h2>\n<p>全仓盘点完成后，Fable 5 基于已核实的依赖、特殊用法和范围边界生成迁移 PRD；Opus 4.7 与 Sonnet 5\n再按阶段、任务批次和验证门槛实施。Claude Code 则把手工验收中发现的现象、根因和规则持续沉淀到\n迁移记录与项目记忆中。AI 加速了盘点、模式化修改与信息整理，但每一个结论仍须由代码、测试、构建\n结果或手工观察验证。</p>\n<p>这套协作方式、一次路由参数误判造成的回归，以及自动化和人工验收如何互补，详见\n<a class=\"\" href=\"https://blog.biulight.top/timeline/blog/ai-in-production-migration\">AI 参与生产系统迁移：把模型能力放进可验证的交付流程</a>。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"先做选择为什么不是长期渐进迁移\">先做选择：为什么不是长期渐进迁移<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%85%88%E5%81%9A%E9%80%89%E6%8B%A9%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%98%AF%E9%95%BF%E6%9C%9F%E6%B8%90%E8%BF%9B%E8%BF%81%E7%A7%BB\" class=\"hash-link\" aria-label=\"先做选择：为什么不是长期渐进迁移的直接链接\" title=\"先做选择：为什么不是长期渐进迁移的直接链接\" translate=\"no\">​</a></h2>\n<p>进入第二段时，项目已经是 Vue 2.7、Vue Router 3、Vuex 3、Element UI 2、VXE Table 3、\nvue-i18n 8 和 Rsbuild 的组合。目标是 Vue 3.5、Vue Router 4、Vuex 4、Element Plus、VXE Table 4\n和 vue-i18n 9，同时保留已经稳定的 Rsbuild 与既有 Vuex 模块结构。</p>\n<p>一开始看起来有三条路：全量重写、长期使用 <code>@vue/compat</code> 渐进迁移，或用微前端让两套运行时并行。\n最后都不适合这个项目：</p>\n<ul>\n<li class=\"\">331 个组件的全量重写会把业务回归风险推到最高；</li>\n<li class=\"\">在本项目中，Element UI 2 与 VXE Table 3 不具备可作为发布形态的 Vue 3/compat 支持，两项核心 UI/表格依赖仍须整体迁至对应的 Vue 3 版本；</li>\n<li class=\"\">管理后台共享登录态、布局、权限与路由，专门建设微前端的成本远高于收益。</li>\n</ul>\n<p>于是选择“阶段化的一次性迁移”。<code>@vue/compat</code> 只在中间阶段以 MODE 2 运行，用运行时警告当作\n迁移待办扫描器；发布前必须移除。这个边界很关键：兼容层可以帮助发现问题，但不能成为问题被搁置的\n理由。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"先固定什么都不改时的基线\">先固定什么都不改时的基线<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%85%88%E5%9B%BA%E5%AE%9A%E4%BB%80%E4%B9%88%E9%83%BD%E4%B8%8D%E6%94%B9%E6%97%B6%E7%9A%84%E5%9F%BA%E7%BA%BF\" class=\"hash-link\" aria-label=\"先固定什么都不改时的基线的直接链接\" title=\"先固定什么都不改时的基线的直接链接\" translate=\"no\">​</a></h2>\n<p>迁移第一步不是改 <code>main.js</code>，而是建立可回归的事实基线。</p>\n<p>项目先补齐并跑通 Playwright 冒烟链路：登录、Dashboard、Campaign 列表和分页、报表、组织管理\n弹窗。它们覆盖了权限路由、VXE Table、Element UI 表单和常见异步请求。与此同时，用\n<code>pnpm analyze</code> 记录生产包体：迁移前未压缩总大小为 15345.0 kB，gzip 后为 7397.6 kB。</p>\n<p>这份基线让后续每一个“看起来像 Vue 3 问题”的现象都有判断依据：它是原有问题、迁移回归，还是\n尚未覆盖的新路径。没有它，构建通过只会给人错误的安全感。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"分阶段切换先打通运行时再迁业务\">分阶段切换：先打通运行时，再迁业务<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%88%86%E9%98%B6%E6%AE%B5%E5%88%87%E6%8D%A2%E5%85%88%E6%89%93%E9%80%9A%E8%BF%90%E8%A1%8C%E6%97%B6%E5%86%8D%E8%BF%81%E4%B8%9A%E5%8A%A1\" class=\"hash-link\" aria-label=\"分阶段切换：先打通运行时，再迁业务的直接链接\" title=\"分阶段切换：先打通运行时，再迁业务的直接链接\" translate=\"no\">​</a></h2>\n<p>迁移没有按目录从上到下改，而是按依赖关系推进。</p>\n<p>第一阶段替换框架基础设施：Rsbuild 的 Vue 2 插件换成 Vue 3 插件，<code>new Vue()</code> 改为\n<code>createApp()</code>，<code>Vue.prototype</code> 改到 <code>app.config.globalProperties</code>，路由切到\n<code>createRouter()</code> / <code>createWebHistory()</code>，并将动态路由注册从 <code>addRoutes</code> 改为逐条 <code>addRoute</code>。\nVuex 3 升 Vuex 4 后，已有五个 namespaced 模块的业务 API 基本不需要改；vue-i18n 9 则采用\nlegacy 模式，降低了模板国际化调用的改造量。</p>\n<p>第二阶段才整体替换第三方库：Element UI → Element Plus，VXE Table 3 → 4，并接入与 Element\nPlus 对应的 VXE 渲染插件。紧接着处理 Vue 3 的机械性变化，例如 <code>.sync</code> 改 <code>v-model:prop</code>、\n指令钩子改名、过时插槽语法、<code>$listeners</code>、<code>$scopedSlots</code> 和 test-utils v2 的配置变化。</p>\n<p>机械替换也有边界。全仓有 151 处 <code>.sync</code>，可以分批转换并结合 diff 与构建检查；但 <code>slot=\"...\"</code>\n命中了 122 个文件，其中既有待迁的模板语法，也有合法的 JSX 属性和注释。对它全局替换会直接破坏\n代码。类似地，路由守卫、全局指令、BiuTable / selectgrid 这类 VXE Table 深度封装，以及\nBiuDialog / BiuForm 这样的基础组件，都必须按实际行为人工审查。</p>\n<p>业务页面放到最后，并按依赖与规模推进：先公共页、登录、Dashboard，再到系统配置、报表、订单，\n最后才是近百个组件的 Campaign 模块。每批变更独立提交，并在进入下一批前运行构建、单测和对应\n页面的手工验证。这样出现回归时，排查范围仍然可控。</p>\n<p>每个阶段采用的验证目标也不同：基础设施阶段先验证应用能到登录页；替换组件库后验证 Dashboard\n和一个 VXE Table 列表；公共组件阶段为每一批组件找一个宿主页面；业务模块阶段再执行相应 E2E 与\n完整手工清单。最后才同时要求 lint、单测、生产构建、E2E、兼容警告清理和包体对比。把验证放在\n阶段门槛上，比在最后堆一次全量测试更容易定位问题。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"验收阶段发现最棘手的错误往往不在构建期\">验收阶段发现：最棘手的错误往往不在构建期<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E9%AA%8C%E6%94%B6%E9%98%B6%E6%AE%B5%E5%8F%91%E7%8E%B0%E6%9C%80%E6%A3%98%E6%89%8B%E7%9A%84%E9%94%99%E8%AF%AF%E5%BE%80%E5%BE%80%E4%B8%8D%E5%9C%A8%E6%9E%84%E5%BB%BA%E6%9C%9F\" class=\"hash-link\" aria-label=\"验收阶段发现：最棘手的错误往往不在构建期的直接链接\" title=\"验收阶段发现：最棘手的错误往往不在构建期的直接链接\" translate=\"no\">​</a></h2>\n<p>下面几个问题都没有被“构建成功”捕获，却最能说明为什么迁移需要真实页面和真实交互的回归。</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"动态组件渲染出-object-promise\">动态组件渲染出 <code>[object Promise]</code><a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6%E6%B8%B2%E6%9F%93%E5%87%BA-object-promise\" class=\"hash-link\" aria-label=\"动态组件渲染出-object-promise的直接链接\" title=\"动态组件渲染出-object-promise的直接链接\" translate=\"no\">​</a></h3>\n<p>Vue 2 会把 <code>:is</code> 接收的 <code>() =&gt; import('./Component.vue')</code> 当作异步组件工厂；Vue 3 不再隐式做\n这个判断。Campaign 详情页按媒体类型切换子组件时，页面没有报错，只是渲染出了\n<code>[object Promise]</code>。</p>\n<p>修复是显式使用 <code>defineAsyncComponent(() =&gt; import('./Component.vue'))</code>。这是一个很好的提醒：\n静态检查能发现类型和语法问题，但无法替代“实际点开动态分支”的验证。</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"vue-router-4-中-params-与-query-不能凭习惯替换\">Vue Router 4 中 <code>params</code> 与 <code>query</code> 不能凭习惯替换<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#vue-router-4-%E4%B8%AD-params-%E4%B8%8E-query-%E4%B8%8D%E8%83%BD%E5%87%AD%E4%B9%A0%E6%83%AF%E6%9B%BF%E6%8D%A2\" class=\"hash-link\" aria-label=\"vue-router-4-中-params-与-query-不能凭习惯替换的直接链接\" title=\"vue-router-4-中-params-与-query-不能凭习惯替换的直接链接\" translate=\"no\">​</a></h3>\n<p>迁移时若向没有动态路径段的路由传 <code>params</code>，Vue Router 4 会丢弃它并给出警告；而把本应填入必填\n动态段的值改放到 <code>query</code>，又会直接抛出 <code>Missing required param</code>，使跳转失败。</p>\n<p>最终采用的规则不是“<code>push</code> 用 query、<code>resolve</code> 用 params”，而是只看目标路由的 <code>path</code>：</p>\n<ul>\n<li class=\"\">没有声明动态段的值，一律用 <code>query</code>；</li>\n<li class=\"\">必填动态段必须用 <code>params</code>；</li>\n<li class=\"\">可选动态段两者都可用，但要保持同一路由的读取约定一致。</li>\n</ul>\n<p>这条规则后来也指导了可分享 Campaign 链接和内部跳转的收敛，避免了“页面能打开但详情参数丢失”的\n静默回归。</p>\n<p>这里还有一个值得记录的过程教训：最初的错误启发式是“内部 <code>push()</code> 用 query，构造可分享链接的\n<code>resolve()</code> 用 params”。它听起来合理，却忽略了路由定义本身；一次批量替换后，普通列表点击也因\n缺少必填 <code>id</code> 而被阻断。修复后，这条规则被单独写入项目记忆。对于 AI 参与的大范围迁移，记录\n“曾经为什么改错”与记录最终写法同样重要。</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"element-plus-的日期问题会静默污染业务值\">Element Plus 的日期问题会静默污染业务值<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#element-plus-%E7%9A%84%E6%97%A5%E6%9C%9F%E9%97%AE%E9%A2%98%E4%BC%9A%E9%9D%99%E9%BB%98%E6%B1%A1%E6%9F%93%E4%B8%9A%E5%8A%A1%E5%80%BC\" class=\"hash-link\" aria-label=\"Element Plus 的日期问题会静默污染业务值的直接链接\" title=\"Element Plus 的日期问题会静默污染业务值的直接链接\" translate=\"no\">​</a></h3>\n<p>Element UI 的日期格式使用 <code>yyyy</code>、<code>dd</code> 等小写 token；Element Plus 基于 dayjs，正确写法是\n<code>YYYY</code>、<code>DD</code>。错误 token 不只让输入框显示 <code>Su/02/yyyy</code> 这类乱码，打开面板后会发现实际选中日期\n已经错位。</p>\n<p>另一个更隐蔽的问题是 <code>default-time=\"['00:00:00', '23:59:59']\"</code>。在 Element Plus 中，裸时间\n字符串会被解析为 Invalid Date；当日期范围控件没有绑定值时，内部日期计算继而变成 <code>NaN</code>，日历\n面板整体失效。解决方式是使用完整且合法的日期时间字符串，例如\n<code>'2000-01-01 00:00:00'</code>。这类差异无法靠 API 名称替换发现，必须在“编辑已有数据”和“新建空数据”\n两种场景中分别打开控件核验。</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"全局性能补丁会破坏组件交互\">全局性能补丁会破坏组件交互<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%85%A8%E5%B1%80%E6%80%A7%E8%83%BD%E8%A1%A5%E4%B8%81%E4%BC%9A%E7%A0%B4%E5%9D%8F%E7%BB%84%E4%BB%B6%E4%BA%A4%E4%BA%92\" class=\"hash-link\" aria-label=\"全局性能补丁会破坏组件交互的直接链接\" title=\"全局性能补丁会破坏组件交互的直接链接\" translate=\"no\">​</a></h3>\n<p>Campaign 表单中的 <code>vue-multiselect</code> 出现了鼠标点选即关闭、值无法回填的问题，程序化\n<code>element.click()</code> 却正常。根因不是组件库或弹窗焦点，而是旧项目引入的\n<code>default-passive-events</code>：它把 <code>mousedown</code> 也强制设为 passive，导致组件内部用于保持焦点的\n<code>preventDefault()</code> 失效。</p>\n<p>修复不是给每个下拉框补事件，而是替换全局补丁，只将 <code>scroll</code>、<code>wheel</code>、<code>touch*</code> 等真正可能阻塞\n滚动的事件设为 passive，保留鼠标按键事件的默认行为。迁移过程中遇到“许多地方同时出问题”时，\n先检查全局拦截器、样式覆盖和事件补丁，往往比逐页修补更有效。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"自动化回归不等于验收角色和空状态必须进入清单\">自动化回归不等于验收：角色和空状态必须进入清单<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E8%87%AA%E5%8A%A8%E5%8C%96%E5%9B%9E%E5%BD%92%E4%B8%8D%E7%AD%89%E4%BA%8E%E9%AA%8C%E6%94%B6%E8%A7%92%E8%89%B2%E5%92%8C%E7%A9%BA%E7%8A%B6%E6%80%81%E5%BF%85%E9%A1%BB%E8%BF%9B%E5%85%A5%E6%B8%85%E5%8D%95\" class=\"hash-link\" aria-label=\"自动化回归不等于验收：角色和空状态必须进入清单的直接链接\" title=\"自动化回归不等于验收：角色和空状态必须进入清单的直接链接\" translate=\"no\">​</a></h2>\n<p>迁移中的 Playwright 冒烟用例覆盖了登录、Dashboard 和核心列表，但一次后续排查发现，它只使用\n管理员账号。管理员的权限路由恰好绕过了非管理员才会经过的 Campaign 详情重定向，因此路由参数的\n问题没有在 E2E 中出现，而是在手工以普通角色进入详情时才暴露。</p>\n<p>这不是“自动化测试没价值”，而是覆盖模型不完整。对于权限驱动的管理后台，回归清单至少还应包含：</p>\n<ul>\n<li class=\"\">一种非管理员角色的登录、菜单生成和深链接访问；</li>\n<li class=\"\">有数据与无数据两种表单状态，尤其是日期、默认值和清空操作；</li>\n<li class=\"\">列表替换、分页或媒体类型切换后的滚动位置与筛选条件；</li>\n<li class=\"\">弹窗、Popover、上传和动态组件等只在后续交互出现的分支；</li>\n<li class=\"\">浏览器控制台中的 Vue、路由和组件库警告。</li>\n</ul>\n<p>手工 QA 的价值不是重复点一遍自动化脚本，而是专门覆盖权限、真实数据形态和视觉交互这些很难在\n一次通用冒烟中穷尽的变量。每个发现都应回流为新的自动化用例、手工检查项，或至少是一条有边界的\n迁移规则。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"构建产物不能只看是否超过预算\">构建产物：不能只看是否超过预算<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E6%9E%84%E5%BB%BA%E4%BA%A7%E7%89%A9%E4%B8%8D%E8%83%BD%E5%8F%AA%E7%9C%8B%E6%98%AF%E5%90%A6%E8%B6%85%E8%BF%87%E9%A2%84%E7%AE%97\" class=\"hash-link\" aria-label=\"构建产物：不能只看是否超过预算的直接链接\" title=\"构建产物：不能只看是否超过预算的直接链接\" translate=\"no\">​</a></h2>\n<p>最终移除 <code>@vue/compat</code> 后，生产包体为 17566.2 kB（未压缩）和 8569.6 kB（gzip），相对基线增长\n14.5% 和 15.8%，超过了最初设定的 ±10% 目标。</p>\n<p>分析结果表明，这不是一次偶然的打包回归：本次刻意保持 Element Plus 全量引入以控制迁移范围，而\nVXE Table 4 的运行时也更重。若要把体积拉回预算，需要单独规划 Element Plus 按需引入和相关验证，\n而不是在迁移收尾时混入一次性能优化。把“功能迁移正确”与“下一阶段的优化机会”分开记录，能让交付\n结论保持诚实，也不会掩盖后续工作。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"这次迁移留下的做法\">这次迁移留下的做法<a href=\"https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E8%BF%99%E6%AC%A1%E8%BF%81%E7%A7%BB%E7%95%99%E4%B8%8B%E7%9A%84%E5%81%9A%E6%B3%95\" class=\"hash-link\" aria-label=\"这次迁移留下的做法的直接链接\" title=\"这次迁移留下的做法的直接链接\" translate=\"no\">​</a></h2>\n<ol>\n<li class=\"\">先定义不变项：本次不改业务逻辑、不换状态管理、不换构建器，避免范围膨胀。</li>\n<li class=\"\">让兼容层只做临时诊断工具，并把移除它作为验收条件。</li>\n<li class=\"\">将可机械化的修改和需要人工判断的修改分开管理，拒绝“一把梭”的全局替换。</li>\n<li class=\"\">用登录、列表、表单、详情和弹窗建立真实冒烟基线；构建通过不是功能通过。</li>\n<li class=\"\">每一类迁移差异都记录成“现象、根因、修复规则、回归场景”，让后续维护不必重新踩坑。</li>\n</ol>\n<p>Vue 2 到 Vue 3 的迁移不是为了把所有组件写成 <code>&lt;script setup&gt;</code>。对一个正在运行的业务系统来说，\n更有价值的结果是：应用在原生 Vue 3 运行时上稳定交付，关键交互得到验证，且每一条兼容性债务都有\n明确的边界和去向。</p>",
            "url": "https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration",
            "title": "keystone-web 从 Vue 2 到 Vue 3：一场两段式迁移的复盘",
            "summary": "把一个有 331 个 .vue 组件、仍在持续迭代的管理后台从 Vue 2 迁到 Vue 3，最危险的想法是：",
            "date_modified": "2026-08-07T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "Vue",
                "Vue 2",
                "Vue 3",
                "Element Plus",
                "Rsbuild",
                "Migration"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate",
            "content_html": "<p>这次原本只想给一个已经由 Nginx UI 管理的站点加上 HTTPS，最后却依次排查了\nNginx UI 启动、ACME CA、域名验证、DNS API 权限、证书文件和 Nginx 重载。</p>\n<p>最有价值的结论不是某一条“万能命令”，而是要把问题拆成四段：管理界面能否工作、CA\n能否创建订单、域名控制权能否验证、Nginx 最终是否加载了正确的证书。把这四件事混在\nNginx UI 的一个申请按钮里看，很容易在错误方向上反复尝试。</p>\n<p>本文保留这次排查的时间线，但域名、凭据和服务器信息均已替换为占位符。可直接执行的\n完整流程放在<a class=\"\" href=\"https://blog.biulight.top/timeline/knowledge/nginx-ui-https-certificate\">使用 acme.sh 为 Nginx UI 管理的站点配置 HTTPS</a>。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"起点先遇到的不是证书而是-nginx-ui-启动\">起点：先遇到的不是证书，而是 Nginx UI 启动<a href=\"https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E8%B5%B7%E7%82%B9%E5%85%88%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%8D%E6%98%AF%E8%AF%81%E4%B9%A6%E8%80%8C%E6%98%AF-nginx-ui-%E5%90%AF%E5%8A%A8\" class=\"hash-link\" aria-label=\"起点：先遇到的不是证书，而是 Nginx UI 启动的直接链接\" title=\"起点：先遇到的不是证书，而是 Nginx UI 启动的直接链接\" translate=\"no\">​</a></h2>\n<p>最初看到 Nginx UI 无法正常启动时，我先把它和证书问题联系到了一起。后来才意识到，\nNginx UI 进程、Nginx 进程和 ACME 签发是三条相互关联但可以独立验证的链路。</p>\n<p>排查顺序应该先回到最基础的状态：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl status nginx-ui --no-pager</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">journalctl -u nginx-ui -n 100 --no-pager</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">nginx -t</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl status nginx --no-pager</span><br></span></code></pre></div></div>\n<p>只有 Nginx UI 和 Nginx 本身都稳定后，证书申请日志才有解释价值。否则一次界面上的失败，\n可能只是服务未启动、配置未加载或 Nginx 测试未通过，并不代表 CA 拒绝签发。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"第一次弯路在-nginx-ui-里直接配置证书\">第一次弯路：在 Nginx UI 里直接配置证书<a href=\"https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E7%AC%AC%E4%B8%80%E6%AC%A1%E5%BC%AF%E8%B7%AF%E5%9C%A8-nginx-ui-%E9%87%8C%E7%9B%B4%E6%8E%A5%E9%85%8D%E7%BD%AE%E8%AF%81%E4%B9%A6\" class=\"hash-link\" aria-label=\"第一次弯路：在 Nginx UI 里直接配置证书的直接链接\" title=\"第一次弯路：在 Nginx UI 里直接配置证书的直接链接\" translate=\"no\">​</a></h2>\n<p>服务恢复后，我尝试直接通过 Nginx UI 申请和配置证书。这条路径本身是受支持的，但在这次\n环境里，它同时引入了太多变量：</p>\n<ul>\n<li class=\"\">使用 Let’s Encrypt 还是 ZeroSSL；</li>\n<li class=\"\">选择 HTTP-01 还是 DNS-01；</li>\n<li class=\"\">域名的权威 DNS 到底由 DNSPod 还是阿里云管理；</li>\n<li class=\"\">DNS API 凭据是否有权创建和删除 <code>_acme-challenge</code> TXT 记录；</li>\n<li class=\"\">系统递归 DNS 是否已经看到新记录；</li>\n<li class=\"\">证书与私钥是否以 Nginx UI 能识别的方式落盘。</li>\n</ul>\n<p>这次对话中的日志先后指向不同环节。切换 CA 后错误表现会变化，但这并不能修复域名验证；\nDNS 页面能登录也不代表 API 凭据有修改记录的权限；浏览器能打开域名，也不代表 ACME\n验证节点能获得期望的 TXT 或 HTTP 响应。</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"容易误读的端口\">容易误读的端口<a href=\"https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E5%AE%B9%E6%98%93%E8%AF%AF%E8%AF%BB%E7%9A%84%E7%AB%AF%E5%8F%A3\" class=\"hash-link\" aria-label=\"容易误读的端口的直接链接\" title=\"容易误读的端口的直接链接\" translate=\"no\">​</a></h3>\n<p>Nginx UI 的 <code>HTTPChallengePort</code> 是其后端在 HTTP-01 流程中使用的监听端口，当前文档中的\n默认值是 <code>9180</code>。但 Let’s Encrypt 的 HTTP-01 验证仍然只会从公网访问 80 端口。也就是说，\n仅确认 <code>9180</code> 在本机监听并不能证明验证链路成立，公网 80 端口还必须把\n<code>/.well-known/acme-challenge/</code> 请求正确交给挑战处理程序。</p>\n<p>这也是直接配置时最容易出现的错觉：界面和后端都在运行，CA 却仍然无法验证域名。</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"容易误读的证书已存在\">容易误读的“证书已存在”<a href=\"https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E5%AE%B9%E6%98%93%E8%AF%AF%E8%AF%BB%E7%9A%84%E8%AF%81%E4%B9%A6%E5%B7%B2%E5%AD%98%E5%9C%A8\" class=\"hash-link\" aria-label=\"容易误读的“证书已存在”的直接链接\" title=\"容易误读的“证书已存在”的直接链接\" translate=\"no\">​</a></h3>\n<p>另一个坑是文件形态。Nginx UI 的自动发现会扫描配置的 Nginx <code>ssl</code> 目录，并识别分别保存的\n证书链和私钥，例如 <code>fullchain.pem</code> 与 <code>privkey.pem</code>。把证书链和私钥拼成单个 PEM 文件，\n并不属于它的自动发现范围。</p>\n<p>因此，“服务器上已经有一个 PEM 文件”并不等于 Nginx UI 已经发现它，更不等于站点配置\n已经引用它。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"改变策略把签发和部署拆开\">改变策略：把签发和部署拆开<a href=\"https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E6%94%B9%E5%8F%98%E7%AD%96%E7%95%A5%E6%8A%8A%E7%AD%BE%E5%8F%91%E5%92%8C%E9%83%A8%E7%BD%B2%E6%8B%86%E5%BC%80\" class=\"hash-link\" aria-label=\"改变策略：把签发和部署拆开的直接链接\" title=\"改变策略：把签发和部署拆开的直接链接\" translate=\"no\">​</a></h2>\n<p>继续在界面里同时调整 CA、挑战方式和 DNS 凭据只会让日志越来越难比较。我最后改用\n<code>acme.sh</code> 单独完成 DNS-01 签发，再把稳定的证书文件交给 Nginx UI 和 Nginx 使用。</p>\n<p>选择 DNS-01 的原因不是它天然优于 HTTP-01，而是这次环境中的公网 80 端口和转发链路并不\n适合作为唯一验证入口。DNS-01 直接通过 <code>_acme-challenge.&lt;DOMAIN&gt;</code> TXT 记录证明域名控制权，\n也支持通配符证书。</p>\n<p>在执行前，先确认权威 DNS，而不是根据域名注册商或曾经使用过的控制台猜测：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dig NS example.com +short</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dig TXT _acme-challenge.app.example.com +short</span><br></span></code></pre></div></div>\n<p>然后只为实际的权威 DNS 提供商创建最小权限凭据。本次最终可工作的链路是：</p>\n<ol>\n<li class=\"\"><code>acme.sh</code> 向选定的 ACME CA 创建订单；</li>\n<li class=\"\">DNS API 临时创建 <code>_acme-challenge</code> TXT 记录；</li>\n<li class=\"\">CA 查询权威 DNS 并完成验证；</li>\n<li class=\"\"><code>acme.sh --install-cert</code> 把证书链和私钥复制到 Nginx 的稳定目录；</li>\n<li class=\"\"><code>nginx -t</code> 通过后重载 Nginx。</li>\n</ol>\n<p>CA 从 ZeroSSL 切换到 Let’s Encrypt 时，我明确写出了 <code>--server letsencrypt</code>。这一步只是\n选择 CA，不应被当成修复 DNS 权限或网络问题的方法。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"为什么没有直接引用-acmesh-的内部文件\">为什么没有直接引用 acme.sh 的内部文件<a href=\"https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89%E7%9B%B4%E6%8E%A5%E5%BC%95%E7%94%A8-acmesh-%E7%9A%84%E5%86%85%E9%83%A8%E6%96%87%E4%BB%B6\" class=\"hash-link\" aria-label=\"为什么没有直接引用 acme.sh 的内部文件的直接链接\" title=\"为什么没有直接引用 acme.sh 的内部文件的直接链接\" translate=\"no\">​</a></h2>\n<p><code>acme.sh</code> 会在 <code>~/.acme.sh/</code> 下保存自己的状态和证书材料，但其官方说明明确不建议服务器\n直接引用这些内部文件。正确做法是使用 <code>--install-cert</code> 复制到生产路径，并配置重载命令：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.acme.sh/acme.sh --install-cert -d app.example.com --ecc \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --key-file /etc/nginx/ssl/app.example.com/privkey.pem \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --fullchain-file /etc/nginx/ssl/app.example.com/fullchain.pem \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --reloadcmd \"nginx -t &amp;&amp; systemctl reload nginx\"</span><br></span></code></pre></div></div>\n<p>这样续期后会再次更新稳定路径，并在配置检查通过后重载 Nginx。若只复制一次文件，证书虽然\n会续期，线上服务却可能一直展示旧证书。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"最后如何确认真的成功\">最后如何确认真的成功<a href=\"https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E6%9C%80%E5%90%8E%E5%A6%82%E4%BD%95%E7%A1%AE%E8%AE%A4%E7%9C%9F%E7%9A%84%E6%88%90%E5%8A%9F\" class=\"hash-link\" aria-label=\"最后如何确认真的成功的直接链接\" title=\"最后如何确认真的成功的直接链接\" translate=\"no\">​</a></h2>\n<p>浏览器出现锁形图标只是最后一个信号。我把验证拆成了四层：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dig A app.example.com +short</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ss -lntp | rg ':(80|443)\\b'</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">nginx -t</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">openssl s_client -connect app.example.com:443 \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  -servername app.example.com -showcerts &lt;/dev/null</span><br></span></code></pre></div></div>\n<p>还要核对证书和私钥是否匹配。下面的公钥摘要应相同：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">openssl x509 -in /etc/nginx/ssl/app.example.com/fullchain.pem \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  -pubkey -noout | openssl sha256</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">openssl pkey -in /etc/nginx/ssl/app.example.com/privkey.pem \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  -pubout | openssl sha256</span><br></span></code></pre></div></div>\n<p>最后执行一次不强制续期的定时任务检查，并查看下一次续期时间：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.acme.sh/acme.sh --cron --home ~/.acme.sh</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.acme.sh/acme.sh --info -d app.example.com --ecc</span><br></span></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"这次踩坑留下的判断顺序\">这次踩坑留下的判断顺序<a href=\"https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E8%BF%99%E6%AC%A1%E8%B8%A9%E5%9D%91%E7%95%99%E4%B8%8B%E7%9A%84%E5%88%A4%E6%96%AD%E9%A1%BA%E5%BA%8F\" class=\"hash-link\" aria-label=\"这次踩坑留下的判断顺序的直接链接\" title=\"这次踩坑留下的判断顺序的直接链接\" translate=\"no\">​</a></h2>\n<p>以后再遇到同类问题，我会按这个顺序处理：</p>\n<ol>\n<li class=\"\">先确认 Nginx UI 与 Nginx 服务健康，避免把启动问题当成证书问题。</li>\n<li class=\"\">查权威 DNS，再选择 DNS API，不能根据注册商或控制台印象猜。</li>\n<li class=\"\">一次只改变一个变量；切换 CA 不会修复挑战验证。</li>\n<li class=\"\">区分“签发成功”“文件已部署”和“Nginx 已加载”三个状态。</li>\n<li class=\"\">使用稳定部署路径和可靠的 <code>reloadcmd</code>，把自动续期真正闭环。</li>\n</ol>\n<p>完整的无敏感信息操作清单和回滚方法见：</p>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://blog.biulight.top/timeline/knowledge/nginx-ui-https-certificate\">使用 acme.sh 为 Nginx UI 管理的站点配置 HTTPS</a></li>\n</ul>",
            "url": "https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate",
            "title": "给 Nginx UI 配置 HTTPS：从界面申请失败到拆开证书签发链路",
            "summary": "这次原本只想给一个已经由 Nginx UI 管理的站点加上 HTTPS，最后却依次排查了",
            "date_modified": "2026-07-07T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "Nginx UI",
                "HTTPS",
                "ACME",
                "Troubleshooting"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache",
            "content_html": "<p>给自建的私有域名新增了一条 CoreDNS 记录，<code>ping</code> 却始终报\"无法解析主机\"，而同一个域名后缀下\n其他早就存在的记录一切正常。这次排查依次怀疑了 Surge、Proxifier，最后才发现是 macOS\n<code>mDNSResponder</code> 的陈旧缓存在作怪。</p>\n<p>本文保留这次排查的时间线，域名、内网地址和服务器信息均已替换为占位符。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"背景\">背景<a href=\"https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E8%83%8C%E6%99%AF\" class=\"hash-link\" aria-label=\"背景的直接链接\" title=\"背景的直接链接\" translate=\"no\">​</a></h2>\n<p>设备已经通过 <code>shine sys apply split-dns</code> 把私有域名后缀交给自建 CoreDNS 解析,具体原理见\n<a class=\"\" href=\"https://blog.biulight.top/timeline/knowledge/zerotier-coredns-split-dns\">使用 ZeroTier、CoreDNS 和 Shine 搭建异地私有域名网络</a>。\n这次是往 CoreDNS 的 <code>Corefile</code> 里新增了一条 <code>hosts</code> 记录：</p>\n<div class=\"language-text codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">hosts {</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    10.20.30.10 nginx-ui.example.internal</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    10.20.30.10 blog.example.internal   # 新加的</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ...</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    fallthrough</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></span></code></pre></div></div>\n<p><code>reload</code> 插件会自动感知 <code>Corefile</code> 变化，理论上不需要重启容器。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"现象\">现象<a href=\"https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E7%8E%B0%E8%B1%A1\" class=\"hash-link\" aria-label=\"现象的直接链接\" title=\"现象的直接链接\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">❯ ping blog.example.internal</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ping: cannot resolve blog.example.internal: Unknown host</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">❯ ping nginx-ui.example.internal</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">PING nginx-ui.example.internal (10.20.30.10): 56 data bytes</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">64 bytes from 10.20.30.10: icmp_seq=0 ttl=64 time=11.383 ms</span><br></span></code></pre></div></div>\n<p>同一个域名后缀、同一台 CoreDNS、同一个目标 IP，新记录解析不出来，旧记录完全正常。这种\"只有\n新东西不行\"的现象,比单纯的\"DNS 全部失效\"更容易让人误判方向。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"先排除服务端\">先排除服务端<a href=\"https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E5%85%88%E6%8E%92%E9%99%A4%E6%9C%8D%E5%8A%A1%E7%AB%AF\" class=\"hash-link\" aria-label=\"先排除服务端的直接链接\" title=\"先排除服务端的直接链接\" translate=\"no\">​</a></h2>\n<p>在下结论之前，先直接绕过本机的解析路径，对 CoreDNS 发一次权威查询：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dig @10.20.30.10 blog.example.internal</span><br></span></code></pre></div></div>\n<p>拿到的是正确、带 <code>aa</code>（authoritative answer）标志的应答。这一步很关键：它证明 CoreDNS 本身\n配置正确、<code>hosts</code> 记录也确实生效了，问题出在客户端到 CoreDNS 之间的某个环节，而不是服务端。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"第一个怀疑对象surge\">第一个怀疑对象：Surge<a href=\"https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%80%80%E7%96%91%E5%AF%B9%E8%B1%A1surge\" class=\"hash-link\" aria-label=\"第一个怀疑对象：Surge的直接链接\" title=\"第一个怀疑对象：Surge的直接链接\" translate=\"no\">​</a></h2>\n<p><code>scutil --dns</code> 显示 split-dns 的域名专属 resolver 配置完全正确，指向 CoreDNS 地址；但\n<code>dscacheutil -q host -a name blog.example.internal</code>（真正走系统解析路径的检查方式）却什么\n都拿不到。同时 <code>route -n get 10.20.30.10</code> 显示去 CoreDNS 的路由走的是一个 <code>feth</code> 虚拟网卡，\n而这台机器上 Surge 的 Network Extension 处于 <code>activated enabled</code> 状态，很自然地怀疑是 Surge\n的 Enhanced Mode（TUN 模式）把 DNS 流量整体接管了。</p>\n<p>但这个方向被用户当场否掉：Surge 既没开系统代理，也没开 TUN 模式，只是用来暴露一个代理接口。\n后面证实这个怀疑确实是误判——<code>feth</code> 网卡另有来源，和 Surge 是否处于 Enhanced Mode 无关。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"第二个怀疑对象proxifier\">第二个怀疑对象：Proxifier<a href=\"https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E7%AC%AC%E4%BA%8C%E4%B8%AA%E6%80%80%E7%96%91%E5%AF%B9%E8%B1%A1proxifier\" class=\"hash-link\" aria-label=\"第二个怀疑对象：Proxifier的直接链接\" title=\"第二个怀疑对象：Proxifier的直接链接\" translate=\"no\">​</a></h2>\n<p>同一台机器上还装了 Proxifier，它的系统扩展同样是 <code>activated enabled</code>。检查它当前生效的\nprofile，发现关键设置：</p>\n<div class=\"language-xml codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-xml codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token tag punctuation\" style=\"color:#393A34\">&lt;</span><span class=\"token tag\" style=\"color:#00009f\">Resolve</span><span class=\"token tag punctuation\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:#393A34\">&lt;</span><span class=\"token tag\" style=\"color:#00009f\">ViaProxy</span><span class=\"token tag\" style=\"color:#00009f\"> </span><span class=\"token tag attr-name\" style=\"color:#00a4db\">enabled</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:#393A34\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:#393A34\">\"</span><span class=\"token tag attr-value\" style=\"color:#e3116c\">true</span><span class=\"token tag attr-value punctuation\" style=\"color:#393A34\">\"</span><span class=\"token tag punctuation\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:#393A34\">&lt;</span><span class=\"token tag\" style=\"color:#00009f\">TryLocalDnsFirst</span><span class=\"token tag\" style=\"color:#00009f\"> </span><span class=\"token tag attr-name\" style=\"color:#00a4db\">enabled</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:#393A34\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:#393A34\">\"</span><span class=\"token tag attr-value\" style=\"color:#e3116c\">false</span><span class=\"token tag attr-value punctuation\" style=\"color:#393A34\">\"</span><span class=\"token tag punctuation\" style=\"color:#393A34\">/&gt;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:#393A34\">&lt;/</span><span class=\"token tag\" style=\"color:#00009f\">ViaProxy</span><span class=\"token tag punctuation\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:#393A34\">&lt;</span><span class=\"token tag\" style=\"color:#00009f\">ExclusionList</span><span class=\"token tag\" style=\"color:#00009f\"> </span><span class=\"token tag attr-name\" style=\"color:#00a4db\">...</span><span class=\"token tag punctuation\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">localhost;%SimpleHostnames%;%ComputerName%;*.local;</span><span class=\"token tag punctuation\" style=\"color:#393A34\">&lt;/</span><span class=\"token tag\" style=\"color:#00009f\">ExclusionList</span><span class=\"token tag punctuation\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:#393A34\">&lt;/</span><span class=\"token tag\" style=\"color:#00009f\">Resolve</span><span class=\"token tag punctuation\" style=\"color:#393A34\">&gt;</span><br></span></code></pre></div></div>\n<p><code>ViaProxy enabled=\"true\"</code> 加 <code>TryLocalDnsFirst enabled=\"false\"</code> 意味着：只要域名没有命中\n排除列表，Proxifier 就不会走本地 DNS，而是尝试通过代理解析——代理服务器当然不认识私有域名。\n排除列表里只有 <code>*.local</code>，没有内网域名后缀，这个方向看起来很有说服力。</p>\n<p>第一次修改却改错了地方：把内网域名后缀加进了 <code>RuleList</code> 里某条规则的 <code>Targets</code>（那是连接\n路由规则，Direct/Proxy 二选一），而不是 <code>Options → Resolve</code> 里控制\"域名解析是否走代理\"的\n<code>ExclusionList</code>——域名解析在连接路由匹配之前就已经失败，规则改了也没用。</p>\n<p>修正到正确的位置后仍然不行；干脆完全退出 Proxifier 应用再测试，问题依旧存在。到这一步，\nProxifier 也被排除了。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"真正的原因mdnsresponder-的陈旧缓存\">真正的原因：mDNSResponder 的陈旧缓存<a href=\"https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E7%9C%9F%E6%AD%A3%E7%9A%84%E5%8E%9F%E5%9B%A0mdnsresponder-%E7%9A%84%E9%99%88%E6%97%A7%E7%BC%93%E5%AD%98\" class=\"hash-link\" aria-label=\"真正的原因：mDNSResponder 的陈旧缓存的直接链接\" title=\"真正的原因：mDNSResponder 的陈旧缓存的直接链接\" translate=\"no\">​</a></h2>\n<p>排除了两个代理工具之后，回到最基础的怀疑对象——本机 DNS 解析进程自身的缓存。</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dscacheutil -flushcache</span><br></span></code></pre></div></div>\n<p>在近几个版本的 macOS 上，这条命令并不总能可靠地清掉 <code>mDNSResponder</code> 内部持有的解析结果，\n必须直接让守护进程重新加载：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo killall -HUP mDNSResponder</span><br></span></code></pre></div></div>\n<p>执行后立刻复测：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">❯ ping blog.example.internal</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">PING blog.example.internal (10.20.30.10): 56 data bytes</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">64 bytes from 10.20.30.10: icmp_seq=0 ttl=64 time=19.909 ms</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">64 bytes from 10.20.30.10: icmp_seq=1 ttl=64 time=11.723 ms</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">❯ dscacheutil -q host -a name blog.example.internal</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">name: blog.example.internal</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ip_address: 10.20.30.10</span><br></span></code></pre></div></div>\n<p>问题解决，和 Surge、Proxifier、CoreDNS 配置都没有关系。</p>\n<p>真正的原因是：<code>blog.example.internal</code> 是后加进 CoreDNS 的新记录，而 <code>nginx-ui.example.internal</code>\n这类记录加入的时间更早。大概率是在这条新记录刚加入、split-dns 还没有完全生效的窗口期内，\n<code>mDNSResponder</code> 已经对新名字做过一次失败查询并缓存了负面结果；而老记录当时早已缓存了正确\n结果，所以一直\"看起来没问题\"，制造了\"只有新东西解析不出来\"的假象，把排查方向一路带偏到了\n两个代理工具身上。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"这次排查留下的判断顺序\">这次排查留下的判断顺序<a href=\"https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E8%BF%99%E6%AC%A1%E6%8E%92%E6%9F%A5%E7%95%99%E4%B8%8B%E7%9A%84%E5%88%A4%E6%96%AD%E9%A1%BA%E5%BA%8F\" class=\"hash-link\" aria-label=\"这次排查留下的判断顺序的直接链接\" title=\"这次排查留下的判断顺序的直接链接\" translate=\"no\">​</a></h2>\n<p>以后再遇到\"某个内网域名解析不出来，其他域名都正常\"，会按这个顺序处理：</p>\n<ol>\n<li class=\"\">先用 <code>dig @&lt;DNS服务器IP&gt; &lt;域名&gt;</code> 直接查权威 DNS，确认是服务端问题还是客户端问题，\n不要一上来就怀疑代理软件。</li>\n<li class=\"\">拿同一个域名后缀下\"早就存在、一直正常\"的记录和\"新加的、解析不出来\"的记录做对比。如果\n真的是代理或路由层面的问题，两者应该同时失效；只有新记录失效，更可能是客户端缓存问题。</li>\n<li class=\"\"><code>dscacheutil -flushcache</code> 不一定够用，<code>sudo killall -HUP mDNSResponder</code> 才是更可靠的\n强制刷新方式。</li>\n<li class=\"\">用 Proxifier 一类工具时，分清\"域名解析是否走代理\"（Resolve/ExclusionList）和\"连接走\nDirect 还是走代理\"（RuleList/Targets）是两个独立的配置面，改错地方不会生效，也不会报错，\n容易误以为\"这个方向也不对\"。</li>\n<li class=\"\">新增内网 DNS 记录后，如果客户端之前已经查询过这个域名并失败过，最好主动做一次\n<code>sudo killall -HUP mDNSResponder</code>，而不是假设 <code>reload</code> 插件生效后客户端会自动更新。</li>\n</ol>",
            "url": "https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache",
            "title": "新增了一条 CoreDNS 记录却解析不出来：从怀疑 Surge、Proxifier 到重启 mDNSResponder",
            "summary": "给自建的私有域名新增了一条 CoreDNS 记录，ping 却始终报\"无法解析主机\"，而同一个域名后缀下",
            "date_modified": "2026-07-07T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "macOS",
                "CoreDNS",
                "DNS",
                "Troubleshooting"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier",
            "content_html": "<p>最近在一台已经接入 ZeroTier 的设备上排查服务连通性，本来只是想用 <code>curl</code> 直接访问私有域名，结果请求却被本地 HTTP 代理接管了。</p>\n<p>如果你也在终端里设置了 <code>HTTP_PROXY</code>、<code>HTTPS_PROXY</code>，同时又依赖 ZeroTier 私有域名或 <code>192.168.x.x</code> 这一类局域网地址，最好顺手检查一下 <code>NO_PROXY</code> / <code>no_proxy</code>。否则 DNS 虽然能解析到内网目标，请求仍可能先进入本地代理链路。</p>\n<p>这类场景在结合 Shine 的 <code>proxy</code> shell 预设时尤其容易忽略，因为我们平时更关注“代理有没有打开”，不一定会第一时间想到“哪些地址本来就不该走代理”。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"背景\">背景<a href=\"https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E8%83%8C%E6%99%AF\" class=\"hash-link\" aria-label=\"背景的直接链接\" title=\"背景的直接链接\" translate=\"no\">​</a></h2>\n<p>目标很简单：通过 ZeroTier 提供的私有网络和私有 DNS，直接访问一台内网服务。</p>\n<p>按预期，这条链路应该是：</p>\n<ol>\n<li class=\"\">终端解析 <code>&lt;PRIVATE_HOST&gt;</code> 之类的私有域名。</li>\n<li class=\"\">系统把该域名交给私有 DNS。</li>\n<li class=\"\">DNS 返回 ZeroTier 地址或局域网地址。</li>\n<li class=\"\"><code>curl</code> 直接连向目标服务。</li>\n</ol>\n<p>但实际现象不是这样。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"现象\">现象<a href=\"https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E7%8E%B0%E8%B1%A1\" class=\"hash-link\" aria-label=\"现象的直接链接\" title=\"现象的直接链接\" translate=\"no\">​</a></h2>\n<p>在已经启用终端代理的 shell 中执行：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">curl http://&lt;PRIVATE_HOST&gt;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">curl http://192.168.0.10:8080</span><br></span></code></pre></div></div>\n<p>请求没有直接访问目标，而是进入了本地 HTTP 代理。对某些代理软件来说，这会表现为请求被拦截、拒绝，或者在日志里出现一条本不该经过代理的内网访问记录。</p>\n<p>这时最容易误判成下面几类问题：</p>\n<ul>\n<li class=\"\">ZeroTier 没连通；</li>\n<li class=\"\">CoreDNS 没解析对；</li>\n<li class=\"\">目标服务没启动；</li>\n<li class=\"\">私有域名配置有误。</li>\n</ul>\n<p>但如果浏览器、其他客户端，或者关闭代理后的同一条命令能够访问成功，就该反过来检查当前 shell 的代理环境。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"原因\">原因<a href=\"https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E5%8E%9F%E5%9B%A0\" class=\"hash-link\" aria-label=\"原因的直接链接\" title=\"原因的直接链接\" translate=\"no\">​</a></h2>\n<p><code>curl</code> 这类命令行工具会读取当前终端中的代理变量，例如：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">env | rg '^(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)='</span><br></span></code></pre></div></div>\n<p>只要 <code>HTTP_PROXY</code> / <code>HTTPS_PROXY</code> 已设置，而 <code>NO_PROXY</code> / <code>no_proxy</code> 没有覆盖私有域名后缀和局域网地址段，这些请求仍会走代理。</p>\n<p>这也是 ZeroTier 场景里很容易忽略的一点：</p>\n<ul>\n<li class=\"\">ZeroTier 解决的是“设备之间是否可达”；</li>\n<li class=\"\">私有 DNS 解决的是“私有域名解析到哪里”；</li>\n<li class=\"\"><code>NO_PROXY</code> 解决的是“这个请求该不该绕过终端代理”。</li>\n</ul>\n<p>这三个环节缺一不可。前两项都正确时，最后仍可能卡在代理变量这一层。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"修复\">修复<a href=\"https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E4%BF%AE%E5%A4%8D\" class=\"hash-link\" aria-label=\"修复的直接链接\" title=\"修复的直接链接\" translate=\"no\">​</a></h2>\n<p>如果你用 Shine 管理终端代理，推荐直接维护 <code>PROXY_NO_PROXY</code>，让 Shine 统一生成 <code>NO_PROXY</code> 和 <code>no_proxy</code>。</p>\n<p>先查看当前配置：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine env list</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine info shell/proxy</span><br></span></code></pre></div></div>\n<p>然后把默认值和私有网络需要的排除项一起写进去。下面示例全部使用占位值：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine env set PROXY_NO_PROXY \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  'localhost,127.0.0.1,::1,.home.example.internal,192.168.0.0/16'</span><br></span></code></pre></div></div>\n<p>这里至少建议覆盖：</p>\n<ul>\n<li class=\"\"><code>localhost,127.0.0.1,::1</code></li>\n<li class=\"\">私有域名后缀，例如 <code>.home.example.internal</code></li>\n<li class=\"\">局域网地址段，例如 <code>192.168.0.0/16</code></li>\n</ul>\n<p>修改后不要只停留在 <code>env set</code>。Shine 会先把已安装的 <code>proxy</code> 预设标记为可更新，还需要执行：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine update shell/proxy</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine upgrade shell/proxy</span><br></span></code></pre></div></div>\n<p>如果你不是用 Shine 管理终端代理，本质上也一样：最终要确认当前 shell 里的 <code>NO_PROXY</code> / <code>no_proxy</code> 已经包含这些私有目标。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"验证\">验证<a href=\"https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E9%AA%8C%E8%AF%81\" class=\"hash-link\" aria-label=\"验证的直接链接\" title=\"验证的直接链接\" translate=\"no\">​</a></h2>\n<p>先确认当前 shell 中的代理变量已经变成预期值：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">shine info shell/proxy</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">env | rg '^(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)='</span><br></span></code></pre></div></div>\n<p>再分别验证私有域名和局域网地址：</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">curl http://&lt;PRIVATE_HOST&gt;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">curl http://192.168.0.10:8080</span><br></span></code></pre></div></div>\n<p>如果修复生效，这两类请求应该直接访问目标，而不是继续进入本地代理。</p>\n<p>若你使用的是基于私有域名的 ZeroTier 解析链路，也可以顺手对照这篇原理说明：</p>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://blog.biulight.top/timeline/knowledge/zerotier-coredns-split-dns\">使用 ZeroTier、CoreDNS 和 Shine 搭建异地私有域名网络</a></li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"延伸建议\">延伸建议<a href=\"https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E5%BB%B6%E4%BC%B8%E5%BB%BA%E8%AE%AE\" class=\"hash-link\" aria-label=\"延伸建议的直接链接\" title=\"延伸建议的直接链接\" translate=\"no\">​</a></h2>\n<p>这个坑本质上不是 ZeroTier 独有问题，而是“终端代理”和“私有网络”叠在一起时的常见遗漏。</p>\n<p>如果你希望以后少踩一次，最简单的做法有两个：</p>\n<ol>\n<li class=\"\">把私有域名后缀和常用内网地址段纳入 <code>PROXY_NO_PROXY</code> 的默认维护范围。</li>\n<li class=\"\">每次怀疑 ZeroTier 或私有 DNS 之前，先检查一眼当前 shell 的代理变量。</li>\n</ol>\n<p>如果你只想快速排查同类问题，可以直接看这篇排障文：</p>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://blog.biulight.top/timeline/knowledge/terminal-proxy-no-proxy-zerotier\">终端代理误拦截 ZeroTier 私有域名的排查与修复</a></li>\n</ul>",
            "url": "https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier",
            "title": "终端代理没有排除 ZeroTier 私有域名，结果请求被本地代理拦截",
            "summary": "最近在一台已经接入 ZeroTier 的设备上排查服务连通性，本来只是想用 curl 直接访问私有域名，结果请求却被本地 HTTP 代理接管了。",
            "date_modified": "2026-07-05T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "Shine",
                "ZeroTier",
                "Proxy",
                "Troubleshooting"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker",
            "content_html": "<p>威联通 Docker 国内无法拉取镜像？</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"背景\">背景<a href=\"https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E8%83%8C%E6%99%AF\" class=\"hash-link\" aria-label=\"背景的直接链接\" title=\"背景的直接链接\" translate=\"no\">​</a></h2>\n<p>由于众所周知的原因，国内拉取 docker 镜像愈发可困难，网上现有的教程涉及 NAS 的极少，为了能愉快的使用 NAS，我进行了大量尝试，终于找到了可用的方法，顺手记录下，分享给各位苦苦挣扎的道友。</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"常见的方案\">常见的方案<a href=\"https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E5%B8%B8%E8%A7%81%E7%9A%84%E6%96%B9%E6%A1%88\" class=\"hash-link\" aria-label=\"常见的方案的直接链接\" title=\"常见的方案的直接链接\" translate=\"no\">​</a></h2>\n<p>现有环境下，想成功拉取镜像，无非使用魔法或者换国内镜像。</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"使用代理\">使用代理<a href=\"https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86\" class=\"hash-link\" aria-label=\"使用代理的直接链接\" title=\"使用代理的直接链接\" translate=\"no\">​</a></h3>\n<ol>\n<li class=\"\">开启威联通 ssh 服务,并通过 ssh 连接到 QNAP</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 远程登录</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ssh -p 22 username@192.168.0.155</span><br></span></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p>SSH 服务的默认端口是 22，上述命令是 用户通过 SSH 工具用账号（username）连接到服务器（192.168.0.155）的 22 端口</p></div></div>\n<ol start=\"2\">\n<li class=\"\">增加 docker 的代理配置</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 编辑配置文件</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo vim /share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json</span><br></span></code></pre></div></div>\n<p>增加代理配置</p>\n<div class=\"language-json codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_X2iu\">/share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json</div><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">// 新增配置</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"proxies\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"http-proxy\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://代理地址:端口\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"https-proxy\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://代理地址:端口\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"no-proxy\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"*.test.example.com,.example.org,127.0.0.0/8\"</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></span></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p>ssh 登录后，可以通过 <code>which docker</code>、<code>whereis docker</code> 查看 docker 所在位置</p></div></div>\n<ol start=\"3\">\n<li class=\"\">重启 docker 服务</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo /etc/init.d/container-station.sh restart</span><br></span></code></pre></div></div>\n<ol start=\"4\">\n<li class=\"\">查看配置是否成功</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">docker info</span><br></span></code></pre></div></div>\n<p>如下图：\n<img decoding=\"async\" loading=\"lazy\" alt=\"代理设置成功\" src=\"https://blog.biulight.top/timeline/assets/images/docker-info-dd1c7cccfc8c2e18e2e04eb3ccf43644.jpg\" width=\"1334\" height=\"620\" class=\"img_oexP\"></p>\n<p>完成上述设置后，你就可以愉快的拉取镜像了</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"更换国内镜像源\">更换国内镜像源<a href=\"https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E6%9B%B4%E6%8D%A2%E5%9B%BD%E5%86%85%E9%95%9C%E5%83%8F%E6%BA%90\" class=\"hash-link\" aria-label=\"更换国内镜像源的直接链接\" title=\"更换国内镜像源的直接链接\" translate=\"no\">​</a></h3>\n<blockquote>\n<p>参考<strong>使用代理</strong>，增加镜像源配置</p>\n</blockquote>\n<p>步骤 1、2 参考<a href=\"https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86\" class=\"\">使用代理</a></p>\n<div class=\"language-text codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 编辑配置文件</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo vim /share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json</span><br></span></code></pre></div></div>\n<p>增加镜像源配置</p>\n<div class=\"language-json codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_X2iu\">/share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json</div><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">// 新增配置</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"registry-mirrors\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"https://mirror.ccs.tencentyun.com\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"https://docker.mirrors.ustc.edu.cn\"</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></span></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-info admonition_FTDX alert alert--info\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>信息</div><div class=\"admonitionContent_ESmc\"><p>腾讯云镜像加速器地址：<a href=\"https://mirror.ccs.tencentyun.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://mirror.ccs.tencentyun.com</a></p><p>中国科学技术大学：<a href=\"https://docker.mirrors.ustc.edu.cn/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://docker.mirrors.ustc.edu.cn</a></p><p>Docker 官方镜像（中国区）镜像加速：<a href=\"https://registry.docker-cn.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://registry.docker-cn.com</a></p><p>网易云镜像加速器地址：<a href=\"http://hub-mirror.c.163.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://hub-mirror.c.163.com</a></p><p>南京大学镜像加速器地址：<a href=\"https://docker.nju.edu.cn/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://docker.nju.edu.cn</a></p></div></div>\n<p>剩余步骤参考<a href=\"https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86\" class=\"\">使用代理</a></p>",
            "url": "https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker",
            "title": "拯救你的Docker",
            "summary": "威联通 Docker 国内无法拉取镜像？",
            "date_modified": "2025-02-07T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "Linux",
                "Docker",
                "QNAP"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github",
            "content_html": "<p>怎样通过 Github 自动交付前端编译产物？</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"准备\">准备<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E5%87%86%E5%A4%87\" class=\"hash-link\" aria-label=\"准备的直接链接\" title=\"准备的直接链接\" translate=\"no\">​</a></h2>\n<p>一台阿里云 <code>ubuntu</code> 服务器</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"实施\">实施<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E5%AE%9E%E6%96%BD\" class=\"hash-link\" aria-label=\"实施的直接链接\" title=\"实施的直接链接\" translate=\"no\">​</a></h2>\n<p><strong>以 <code>deploy</code> 用户组下的 <code>deploy001-timeline</code> 用户,部署目录 <code>/path/to/folder</code>，部署工具 <code>easingthemes/ssh-deploy@v2.2.11</code> 为例</strong></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"新建部署用户修改部署目录权限\">新建部署用户，修改部署目录权限<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E6%96%B0%E5%BB%BA%E9%83%A8%E7%BD%B2%E7%94%A8%E6%88%B7%E4%BF%AE%E6%94%B9%E9%83%A8%E7%BD%B2%E7%9B%AE%E5%BD%95%E6%9D%83%E9%99%90\" class=\"hash-link\" aria-label=\"新建部署用户，修改部署目录权限的直接链接\" title=\"新建部署用户，修改部署目录权限的直接链接\" translate=\"no\">​</a></h3>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p>遵循权限最小化原则，创建 <code>deploy001-timeline</code> 用户，用于部署\n若已存在 <code>deploy</code> 用户组，跳过<strong>步骤 1</strong>，直接新建指定项目部署用户</p></div></div>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 检查指定xxx用户组是否存在</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cat /etc/group | grep xxx</span><br></span></code></pre></div></div>\n<ol>\n<li class=\"\">创建用户组</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo addgroup deploy</span><br></span></code></pre></div></div>\n<ol start=\"2\">\n<li class=\"\">创建用户</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo adduser --ingroup deploy deploy001-timeline</span><br></span></code></pre></div></div>\n<ol start=\"3\">\n<li class=\"\">指定部署目录的归属</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo chown deploy001-timeline:deploy /path/to/folder</span><br></span></code></pre></div></div>\n<ol start=\"4\">\n<li class=\"\">更新目录权限</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo chmod 755 /path/to/folder</span><br></span></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"配置部署工具\">配置部署工具<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E9%85%8D%E7%BD%AE%E9%83%A8%E7%BD%B2%E5%B7%A5%E5%85%B7\" class=\"hash-link\" aria-label=\"配置部署工具的直接链接\" title=\"配置部署工具的直接链接\" translate=\"no\">​</a></h3>\n<p><strong>以 <code>easingthemes/ssh-deploy@v2.2.11</code> 为例</strong></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p><a href=\"https://github.com/easingthemes/ssh-deploy\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">easingthemes/ssh-deploy</a>使用手册</p></div></div>\n<h4 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"服务器侧\">服务器侧<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BE%A7\" class=\"hash-link\" aria-label=\"服务器侧的直接链接\" title=\"服务器侧的直接链接\" translate=\"no\">​</a></h4>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p>使用 <code>ssh</code> 登录服务器，需要使用一组秘钥对，其中公钥设置到服务器 <code>~/.ssh/authorized_keys</code> 文件中，私钥设置在客户端</p></div></div>\n<ol>\n<li class=\"\">登录新创建的用户</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">su deploy001-timeline</span><br></span></code></pre></div></div>\n<ol start=\"2\">\n<li class=\"\">生成非对称密钥</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ssh-keygen -m PEM -t rsa -b 4096 -f deploy001-timeline -C \"deploy001-timeline\"</span><br></span></code></pre></div></div>\n<ol start=\"3\">\n<li class=\"\">把公钥粘贴到 <code>authorized_keys</code> 文件中</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 查看公钥，复制输出到终端的结果</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cat deploy001-timeline.pub</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 编辑 authorized_keys 文件，并把复制的结果粘贴到此文件</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">vim ~/.ssh/authorized_keys</span><br></span></code></pre></div></div>\n<div class=\"language-txt codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_X2iu\">~/.ssh/authorized_keys</div><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-txt codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDcGxPtreCc5bBbaN7jIRuVuTQkC0Mp6VxnyC5sQKG1IZwSL5agL3Zdbu3YVzx0rSU33XZZj5lWp0AAz4ABQGhAU6iIgDNjRmhyuLG4VOS3Gas21Si1lK4Dm/D0lBA//zFmDBTwa9sAEl39rJQCQYL3n2prMrlC2k8m93RGLFmgxWDcb2D+LGgi43bgR4TXvJD6dZa7xfIldsQL5SQpCzEMX8uDDlnnsus99AqmmMzUenW6I25G2ApwgUY/Toj/scCGeHRb7PmGK0AwjqBr9siiIRNi0DpX4JbYPqLPn0IciC70RNLs2tOEFV1BwP50gmF5eDD4tI7NOLaI4HvkesYy9xF6qBtNJOfALgu7Dt+s5g5izXg2pfb/RKJw2WEj56hLI2NIOr1CgZRJH6Yq2xlP1SKlo3GwkzT/NhnhiO1NFy/FKFj/sYmn4/SM92QM2OmruPDAU6JUHUT9P7m/bhlAxo5xrzMqH8v9c4acxUGz5s87j8eNcNfaIuzMQOG+jzEOqqdWI4uLeTPHrm4pSuZUuse7IenTN3Wx1MD4CfjyZZZIWR6Xv+f7MJ0CpbvpZ7E4MjenLfb8g/upl2L1kv3Lz/UUbL4n38DCY113wkJY5x6T54ckqHUmhirsL2SCHb71VAtxUhEByCTBdPEN18Hh+QOKKTp4rmJYjNrvm4E5yw== deploy001-timeline</span><br></span></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p>vim 是 linux 常用的终端编辑器，常见命令如下</p><ol>\n<li class=\"\"><code>:wq</code> 保存并退出</li>\n<li class=\"\"><code>i</code> 进入输入视图</li>\n</ol></div></div>\n<h4 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"github-配置\">Github 配置<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#github-%E9%85%8D%E7%BD%AE\" class=\"hash-link\" aria-label=\"Github 配置的直接链接\" title=\"Github 配置的直接链接\" translate=\"no\">​</a></h4>\n<p>配置 <code>secrets</code> ，在 workflow 中使用</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p>配置位置如下图所示\n<img decoding=\"async\" loading=\"lazy\" alt=\"配置位置参考图\" src=\"https://blog.biulight.top/timeline/assets/images/deploy-051e4ae18f9c956904b921a85089bfc1.jpg\" width=\"2774\" height=\"1630\" class=\"img_oexP\"></p></div></div>\n<ol>\n<li class=\"\">新增 <code>ALY_USER</code>，如下</li>\n</ol>\n<div class=\"language-txt codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-txt codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">deploy001-timeline</span><br></span></code></pre></div></div>\n<ol start=\"2\">\n<li class=\"\">新增 <code>ALY_HOST</code>，如下</li>\n</ol>\n<div class=\"language-txt codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-txt codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">192.168.0.155</span><br></span></code></pre></div></div>\n<ol start=\"3\">\n<li class=\"\">新增 <code>ALY_SSH_TOKEN</code>，如下</li>\n</ol>\n<div class=\"language-txt codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-txt codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-----BEGIN RSA PRIVATE KEY-----</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MIIJKQIBAAKCAgEA3BsT7a3gnOWwW2je4yEblbk0JAtDKelcZ8gubEChtSGcEi+W</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">oC92XW7t2Fc8dK0lN912WY+ZVqdAAM+AAUBoQFOoiIAzY0ZocrixuFTktxmrNtUo</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">tZSuA5vw9JQQP/8xZgwU8GvbABJd/ayUAkGC959qazK5QtpPJvd0RixZoMVg3G9g</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/ixoIuN24EeE17yQ+nWWu8XyJXbEC+UkKQsxDF/Lgw5Z57LrPfQKppjM1Hp1uiNu</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RtgKcIFGP06I/7HAhnh0W+z5hitAMI6ga/bIoiETYtA6V+CW2D6iz59CHIgu9ETS</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">7NrThBVdQcD+dIJheXgw+LSOzTi2iOB75HrGMvcReqgbTSTnwC4Luw7frOYOYs14</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NqX2/0SicNlhI+eoSyNjSDq9QoGUSR+mKtsZT9UipaNxsJM0/zYZ4YjtTRcvxShY</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/7GJp+P0jPdkDNjpq7jwwFOiVB1E/T+5v24ZQMaOca8zKh/L/XOGnMVBs+bPO4/H</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jXDX2iLszEDhvo8xDqqnViOLi3kzx65uKUrmVLrHuyHp0zd1sdTA+An48mWWSFke</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">l7/n+zCdAqW76WexODI3py32/IP7qZdi9ZL9y8/1FGy+J9/AwmNdd8JCWOcek+eH</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">JKh1JoYq7C9kgh2+9VQLcVIRAcgkwXTxDdfB4fkDiik6eK5iWIza75uBOcsCAwEA</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AQKCAgA3hxdEd4C6ZFHVBcRxLXmNkK8gRr0XdjlrLzvS8I07KnL6vYm0+LRWRvV3</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Bi8IymOgz9ybJJiNHV2Otsp2xl5mmBRk916/dVRFWOXw+G3I6Y9hom4GPS+XVoB3</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">22CsHH/EXFjt/G+/+WI/7IlcdlWJjeylhaLqRDQCwxvQ1kB4SEMy6WBhTyeoYZUs</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">aK6RVI+IuCCiV4JKxEcaLyErv5iMpCAt/HFe/CrPsuD6/YJzOj+4CVc5RSiBEIP9</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1ZzHUPsekYXdaQ/cMs4bUegTxKsCB/EhlSGa6Nx1Snv9gBKIKy0ZT8GcomoJIPCp</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dXxcPUZFBDbqj3aVPHEEYAmxbu3QFuJDN216BMD5rHgMXfWrGZ/UvTx0+5HmVTiN</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">qhPWUNdkMWUVNqVPNCbHpSpK0ip0vknxqs/xf17GHLKhHh4RJ/gbEnhQOU0E6qIG</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Wke0aM6mHnhuYFbHc6Ux1Ow2S0RZ1pC6kTJC4vpnGGZekdv033MTLNly/8uK6j7x</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">eTQRxEi3cl8aMLRKFYkn2F8sE4ZIq6x7Q2knCaHgELObJmfRDoPD2kDZ+kd2AT9I</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wUXokHtfEOmVQ3N8MA8rneC3DFWfs4eG6w2SzsAYYjEc06g/lzF6jjrLc2RVQNZi</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">EI3e591Kjsu56yvGoGj6PQNpv612oeQdbZQem99BAz128xzijQKCAQEA9SuAAyTA</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2JVCieUYybtjpJ9BNxHchXfqkB9CHOwYCbocjgkfSFzUX5NNkpIqUpaahQYE6+B4</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">zFopyPkzUObdUWGTKmMbnlvkE3d/mgeliDCHMGN/BgmtvnIPfHM72zQHeXwJIEp/</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">p+u7EEr0JdiKHORyAQ3CP2TWA7aKstPSj0lNF9ov68TlWeYmzHZ35W1eAHmuhkA4</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FyhjggUaqf9RD4dO5mJJgbkEn4qIdZjG1KHunOMuFUuotG0BxN1W9ozOFrYqkIYo</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">kh8E5zLUFP2sLA9K2imVDyOBlbMRjCaTx/QWOJpmPdRuhUQPeuQuaD5yZ/DoC4Nj</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">i/00WdwT09OJFwKCAQEA5dQj6cz5oAUA761STeesmjIQx2UeIobPCsIsDjIaqqUn</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">CdT3pKHxstS9tcE77e6vctTGu70IMMJcqVcP7M1p4WyhuqWW4QvmDeXWpfSZCoTm</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">3CPMQe1JgBCxk2Ysv8vCXTxh+pPYRevLJo2LdpxdQhBXVdgq2QdkroAcKNI5kUwy</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">B8HAl/AaIwAInYt0cRJtdxxm1VU6bwfAAtvDdHnf6ozSAsj/qzSKZufR0zGXdHj+</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">d6kuhL3h6XDC8vaDMkhIq4/2vc8Q+Q+uIt89ulXZnm/ecZAaz2I3pRdiYGpK3rJI</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2hSWMa8CwbAO/s0rMi3cOdVtzyB4NQA6YzBfXAHdbQKCAQEAiqd3Vk/K4LJJV0NB</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">qZVY3RlUAJkRZkpTVf7XnRPI5Hdk/s8Z6grV8NeV33baiKqz6NdmoR/qMWgsiYSm</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">CPMH56aPXEGAtSay7m9+stXwP/1x92PNytRedn57U564E1hxgbssTh7T41vmB2jc</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">eyrjsUNhVkc5vDCC+Mab2lcBlheWig7TWzuXmigXeTrmw9H+f7ZRhYt8LVyiKj5I</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">viuu5GFZteGQQNUqUDBAzIECse6CzCePQGvxlViwz21+aSFozF9p4yJ8f2cblsWg</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">O97bKG/dnVQ3AMDL4KXNHr4Shdr9VjW77OvfNW8wcgfqVvo6KZ6NHTXJbrpciRV2</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AS55MQKCAQAgn8+FRcJ9k5hBbZIMWwGmVcsJ8vGnJwnG1i9YyIX+iTkUbTXGGfLs</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">VsSjQ03VhZdELq4TQk8iVMTAiQpKrBIGnNwMWaskQQMkH+crYaBD3P/EahR5m0xe</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sH/0k4FwrGTnxUWHQ2tk6YbJ7zfH24Rld1peZjQW4LTLDIZyyPzBAlbrwPYs2zlq</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">lHukmYIq9tmxGcgDKnkD/86i2T1SkGZXriu1l4CrdebbmravSw/byb0dkVd4Ilgq</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">QgZ6ZMHU5mviZqo44WbgY/htHNFvXPZqRikGZ9VWcPPDLKGbXBtKp3Yt9tQnM1ep</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1OscAlXKZrYnQ4O17j8CDwk6wUgYEG+1AoIBAQCqCU6FMEahhDBJgTGds4zS5DLp</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MFrfHtmo4YwPwk/Q/rt0/Ipdp0WRwcHNtYBHluYB22PaCwQd+k+3KqBDwqSPtLhz</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2qsOgdDiRVsBL/UoCOERMc+YittrgOISYMqKvgi4FL5taK0+Lsy3i9JsMU6wPxy0</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">mMorxklqVarCYYr/ydCpp8Ty48NzYQHyUPIS/b2jdIqBIUMf2OCmw5vvDn4QYmiA</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Kt5ed/7+WQ+2GT9/0FuHrFMkHFRSKTG9ac1B3NyfLBKJ7ZbedMx0MTlmwmwgW79f</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Kj6Wklu6G8M0umKPTs1wPOy/xCtafMeMRf8tVoyfUyhxofkEpkbp1eUFAEhI</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-----END RSA PRIVATE KEY-----</span><br></span></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><ul>\n<li class=\"\"><strong>ALY_USER:</strong> 服务器配置的部署用户名</li>\n<li class=\"\"><strong>ALY_HOST:</strong> 服务器域名</li>\n<li class=\"\"><strong>ALY_SSH_TOKEN:</strong> 通过<code>ssh-keygen</code> 生成的私钥</li>\n</ul></div></div>\n<h4 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"项目-workflow-配置\">项目 workflow 配置<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E9%A1%B9%E7%9B%AE-workflow-%E9%85%8D%E7%BD%AE\" class=\"hash-link\" aria-label=\"项目 workflow 配置的直接链接\" title=\"项目 workflow 配置的直接链接\" translate=\"no\">​</a></h4>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p>配置文件配置在项目中，通常写在 <code>.github/workflows</code> 目录下</p></div></div>\n<p><strong>example:</strong></p>\n<div class=\"language-yml codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_X2iu\">.github/workflows/deploy.yml</div><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-yml codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Deployment</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 触发脚本的条件，develop分支push代码的时候</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">push</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">branches</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> release</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 要执行的任务</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">jobs</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 任务名称</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">build</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># runs-on 指定job任务运行所需要的虚拟机环境（必填）</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># runs-on: self-hosted</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">runs-on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ubuntu</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">latest</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 任务步骤</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">steps</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 获取源码</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> pull code</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 使用action库  actions/checkout获取源码</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">uses</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> actions/checkout@v3</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 安装node</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> create build environments</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 使用action库 actions/setup-node 安装node</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">uses</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> actions/setup</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">node@v3</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">with</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">node-version</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> 18.20.4</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 安装依赖</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> install</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> npm install</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 打包</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> build</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> npm run build</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Upload static files as artifact</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> deployment</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">uses</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> actions/upload</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">pages</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">artifact@v3 </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># or specific \"vX.X.X\" version tag for this action</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">with</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> build/</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 上传打包文件到远程服务器</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ssh deploy</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">uses</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> easingthemes/ssh</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">deploy@v2.2.11</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">env</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">REMOTE_USER</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">secrets.ALY_USER</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">REMOTE_HOST</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">secrets.ALY_HOST</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">REMOTE_PORT</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"22\"</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">ARGS</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"-rltgoDzvO --delete\"</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">SSH_PRIVATE_KEY</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">secrets.ALY_SSH_TOKEN</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 要上传文件所在目录</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">SOURCE</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"build/*\"</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 远程服务器目标路径</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">TARGET</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"/var/www/timeline\"</span><br></span></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"补充\">补充<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E8%A1%A5%E5%85%85\" class=\"hash-link\" aria-label=\"补充的直接链接\" title=\"补充的直接链接\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"nginx-配置\">NGINX 配置<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#nginx-%E9%85%8D%E7%BD%AE\" class=\"hash-link\" aria-label=\"NGINX 配置的直接链接\" title=\"NGINX 配置的直接链接\" translate=\"no\">​</a></h3>\n<p>以访问 <code>http://blog.biulight.cn/timeline</code> 能显示 <code>/var/www/timeline</code> 目录内容为例</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><ol>\n<li class=\"\">通常，Nginx 的虚拟主机配置位于<code>/etc/nginx/sites-available/</code>目录下。如果已经有针对<code>timeline</code>的配置文件，可以直接编辑它；否则，创建一个新的配置文件。</li>\n<li class=\"\">启用配置文件，需要在 <code>/etc/nginx/sites-enabled/</code>目录下有对应配置文件的映射。</li>\n</ol></div></div>\n<ol start=\"3\">\n<li class=\"\">新增配置文件</li>\n</ol>\n<div class=\"language-nginx codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_X2iu\">/etc/nginx/sites-available/timeline</div><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-nginx codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">server {</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    listen 80;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    server_name blog.biulight.cn;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    # 映射/timeline到指定目录</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    location /timeline {</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        alias /var/www/timeline;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        index index.html index.htm;</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    }</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></span></code></pre></div></div>\n<ol start=\"2\">\n<li class=\"\">启用配置文件</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 建立软连接，启用新增的配置文件</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ln -s /etc/nginx/sites-available/timeline /etc/nginx/sites-enabled/timeline</span><br></span></code></pre></div></div>\n<ol start=\"3\">\n<li class=\"\">测试配置文件</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 修改完配置后，运行以下命令检查语法是否正确</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo nginx -t</span><br></span></code></pre></div></div>\n<ol start=\"4\">\n<li class=\"\">重新加载 Nginx 服务</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># 如果测试通过，重新加载Nginx以应用更改：</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo systemctl reload nginx</span><br></span></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"使用-lets-encrypt-证书\">使用 <code>Let's Encrypt</code> 证书<a href=\"https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E4%BD%BF%E7%94%A8-lets-encrypt-%E8%AF%81%E4%B9%A6\" class=\"hash-link\" aria-label=\"使用-lets-encrypt-证书的直接链接\" title=\"使用-lets-encrypt-证书的直接链接\" translate=\"no\">​</a></h3>\n<div class=\"theme-admonition theme-admonition-tip admonition_FTDX alert alert--success\"><div class=\"admonitionHeading_WkVZ\"><span class=\"admonitionIcon_uy2W\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>提示</div><div class=\"admonitionContent_ESmc\"><p>ubuntu 安装查看<a href=\"https://certbot.eff.org/instructions?ws=nginx&amp;os=snap\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">certbot</a>官方文档</p></div></div>\n<p>使用<strong>certbot</strong>自动生成证书</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo certbot --nginx</span><br></span></code></pre></div></div>",
            "url": "https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github",
            "title": "使用Github自动交付前端制品",
            "summary": "怎样通过 Github 自动交付前端编译产物？",
            "date_modified": "2025-02-05T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "Linux",
                "Ubuntu",
                "Deploy",
                "GitHub",
                "CI/CD"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim",
            "content_html": "<p>Ubuntu 怎样安装指定版本的 Neovim?</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"背景\">背景<a href=\"https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim#%E8%83%8C%E6%99%AF\" class=\"hash-link\" aria-label=\"背景的直接链接\" title=\"背景的直接链接\" translate=\"no\">​</a></h2>\n<p>由于 apt 中只有 Neovim(v0.72)的安装包。想使用新版需要自己安装</p>\n<blockquote>\n<p>以 <code>v0.9.5</code> 版本为例</p>\n</blockquote>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"方案一\">方案一<a href=\"https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim#%E6%96%B9%E6%A1%88%E4%B8%80\" class=\"hash-link\" aria-label=\"方案一的直接链接\" title=\"方案一的直接链接\" translate=\"no\">​</a></h2>\n<ol>\n<li class=\"\">去 <a href=\"https://github.com/neovim/neovim/releases\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Github</a> 上查看指定版本的压缩包,并下载</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo wget https://github.com/neovim/neovim/releases/download/v0.9.5/nvim-linux64.tar.gz</span><br></span></code></pre></div></div>\n<ol start=\"2\">\n<li class=\"\">解压缩 <code>nvim-linux64</code> 至 <code>/opt</code> 目录</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo tar -C /opt -xzf nvim-linux64.tar.gz</span><br></span></code></pre></div></div>\n<ol start=\"3\">\n<li class=\"\">add this to your shell config (~/.bashrc, ~/.zshrc, ...):</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">export PATH=\"$PATH:/opt/nvim-linux64/bin\"</span><br></span></code></pre></div></div>\n<ol start=\"4\">\n<li class=\"\">reload shell config</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">source ~/.bashrc</span><br></span></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"方案二\">方案二<a href=\"https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim#%E6%96%B9%E6%A1%88%E4%BA%8C\" class=\"hash-link\" aria-label=\"方案二的直接链接\" title=\"方案二的直接链接\" translate=\"no\">​</a></h2>\n<p>使用 <code>snap</code> 包管理工具安装</p>\n<div class=\"language-bash codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sudo snap install nvim --classisc</span><br></span></code></pre></div></div>",
            "url": "https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim",
            "title": "Install Neovim",
            "summary": "Ubuntu 怎样安装指定版本的 Neovim?",
            "date_modified": "2025-02-04T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "Linux",
                "Ubuntu",
                "Neovim"
            ]
        },
        {
            "id": "https://blog.biulight.top/timeline/blog/2024/08/10/document.title",
            "content_html": "<p>埋点监控 SDK <code>ENTER</code> 事件怎样获取到页面标题？</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"背景\">背景<a href=\"https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E8%83%8C%E6%99%AF\" class=\"hash-link\" aria-label=\"背景的直接链接\" title=\"背景的直接链接\" translate=\"no\">​</a></h2>\n<p>使用 <code>window.onload</code>、<code>hashchange</code> 事件获取 <code>document.title</code> 时，某些场景出现异常，值为空</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"分析\">分析<a href=\"https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E5%88%86%E6%9E%90\" class=\"hash-link\" aria-label=\"分析的直接链接\" title=\"分析的直接链接\" translate=\"no\">​</a></h2>\n<p>单页面应用，<code>document.title</code> 存在异步设置的场景（如页面是后管配置的，需要等接口返回页面标题）</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"方案\">方案<a href=\"https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E6%96%B9%E6%A1%88\" class=\"hash-link\" aria-label=\"方案的直接链接\" title=\"方案的直接链接\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"场景一多页面应用title-写在-html-文件里\">场景一：多页面应用，<code>title</code> 写在 html 文件里；<a href=\"https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E5%9C%BA%E6%99%AF%E4%B8%80%E5%A4%9A%E9%A1%B5%E9%9D%A2%E5%BA%94%E7%94%A8title-%E5%86%99%E5%9C%A8-html-%E6%96%87%E4%BB%B6%E9%87%8C\" class=\"hash-link\" aria-label=\"场景一多页面应用title-写在-html-文件里的直接链接\" title=\"场景一多页面应用title-写在-html-文件里的直接链接\" translate=\"no\">​</a></h3>\n<p><code>window.onload</code> 事件可以正常获取到页面标题</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_dDPi\" id=\"场景二单页面应用title-在页面初始化后设置\">场景二：单页面应用，<code>title</code> 在页面初始化后设置；<a href=\"https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E5%9C%BA%E6%99%AF%E4%BA%8C%E5%8D%95%E9%A1%B5%E9%9D%A2%E5%BA%94%E7%94%A8title-%E5%9C%A8%E9%A1%B5%E9%9D%A2%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%8E%E8%AE%BE%E7%BD%AE\" class=\"hash-link\" aria-label=\"场景二单页面应用title-在页面初始化后设置的直接链接\" title=\"场景二单页面应用title-在页面初始化后设置的直接链接\" translate=\"no\">​</a></h3>\n<blockquote>\n<p>借助 <code>MutationObserver</code> 观察 <code>title</code> 元素更新</p>\n</blockquote>\n<div class=\"language-js codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-js codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">function</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">watchForTitleChanges</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token parameter\">callback</span><span class=\"token parameter punctuation\" style=\"color:#393A34\">,</span><span class=\"token parameter\"> type</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"color:#00009f\">let</span><span class=\"token plain\"> ele </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:#36acaa\">document</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">getElementsByTagName</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"title\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token operator\" style=\"color:#393A34\">?.</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">!</span><span class=\"token plain\">ele</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ele </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:#36acaa\">document</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">createElement</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"title\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token dom variable\" style=\"color:#36acaa\">document</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token property-access\">head</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">appendChild</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ele</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"color:#00009f\">let</span><span class=\"token plain\"> previousTitle </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:#36acaa\">document</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token property-access\">title</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"color:#00009f\">const</span><span class=\"token plain\"> observer </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\">MutationObserver</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token parameter\">mutationList</span><span class=\"token parameter punctuation\" style=\"color:#393A34\">,</span><span class=\"token parameter\"> observer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:#393A34\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">const</span><span class=\"token plain\"> title </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:#36acaa\">document</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token property-access\">title</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">previousTitle </span><span class=\"token operator\" style=\"color:#393A34\">!==</span><span class=\"token plain\"> title</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      previousTitle </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> title</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      callback</span><span class=\"token operator\" style=\"color:#393A34\">?.</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">title</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token keyword control-flow\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">type </span><span class=\"token operator\" style=\"color:#393A34\">===</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"once\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> observer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">disconnect</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  observer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">observe</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ele</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\" style=\"color:#36acaa\">childList</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">// 非仅监听一次更新时，返回取消监听函数</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">type </span><span class=\"token operator\" style=\"color:#393A34\">!==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"once\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"color:#00009f\">return</span><span class=\"token plain\"> observer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token property-access\">disconnect</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></span></code></pre></div></div>\n<p>完美实现如下：</p>\n<div class=\"language-js codeBlockContainer_oFpl theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_VBZe\"><pre tabindex=\"0\" class=\"prism-code language-js codeBlock_sDfG thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_FP2B\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\">// 页面初次加载时，在 load 事件中注册 watch</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token dom variable\" style=\"color:#36acaa\">window</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">addEventListener</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"load\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:#393A34\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">watchForTitleChanges</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token parameter\">title</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:#393A34\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token console class-name\">console</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">log</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"title发生了变化\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> title</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"once\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">// 路由切换时，在 hashchange 事件中注册 watch</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token dom variable\" style=\"color:#36acaa\">window</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">addEventListener</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"hashchange\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:#393A34\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">watchForTitleChanges</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token parameter\">title</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:#393A34\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token console class-name\">console</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token method function property-access\" style=\"color:#d73a49\">log</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"title发生了变化\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> title</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"once\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><br></span></code></pre></div></div>",
            "url": "https://blog.biulight.top/timeline/blog/2024/08/10/document.title",
            "title": "Watch document.title",
            "summary": "埋点监控 SDK ENTER 事件怎样获取到页面标题？",
            "date_modified": "2024-08-10T00:00:00.000Z",
            "author": {
                "name": "feilx",
                "url": "https://github.com/biulight"
            },
            "tags": [
                "Web",
                "Supervisory control"
            ]
        }
    ]
}