<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>首页 on 武成龙</title><link>https://orangon.github.io/</link><description>Recent content in 首页 on 武成龙</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Fri, 07 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://orangon.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>云原生地理空间智能计算系统</title><link>https://orangon.github.io/projects/geospatial-computing-platform/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://orangon.github.io/projects/geospatial-computing-platform/</guid><description>&lt;h2 id="项目概述"&gt;项目概述
&lt;/h2&gt;&lt;p&gt;国家重点研发计划课题「云原生地理空间智能计算系统」前端模块，负责地理空间分析可视化前端开发，基于 &lt;strong&gt;React + Mapbox + Bpmn.js&lt;/strong&gt; 构建知识驱动的智能地理计算交互界面。&lt;/p&gt;
&lt;h2 id="核心职责"&gt;核心职责
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;智能地理计算交互界面&lt;/strong&gt;：构建模型编排画布，基于 Bpmn.js 实现地理计算工作流的可视化建模与拖拽编排&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多源异构模型管理&lt;/strong&gt;：解决 200+ 地理计算模型的元数据渲染问题，支持模型检索、参数配置与调用链路可视化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间数据可视化&lt;/strong&gt;：基于 Mapbox 实现多维地理空间数据的交互式展示与分析&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent 驱动改造&lt;/strong&gt;：后期将系统改造为 Agent 驱动架构，基于 LangChain 实现通过对话交互完成基于知识库、数据库和模型库的三位一体地理建模（GEO-TRINITY）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="技术亮点"&gt;技术亮点
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;设计统一的模型元数据 Schema，适配 200+ 多源异构地理计算模型&lt;/li&gt;
&lt;li&gt;基于 Bpmn.js 扩展自定义建模元素，实现地学领域专用的流程编排器&lt;/li&gt;
&lt;li&gt;高性能地图渲染方案，支持大规模空间分析结果的可视化展示&lt;/li&gt;
&lt;li&gt;GEO-TRINITY 三位一体建模：知识库 + 数据库 + 模型库智能编排，自然语言驱动地理建模&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="技术栈"&gt;技术栈
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端框架&lt;/strong&gt;：React + TypeScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地图引擎&lt;/strong&gt;：Mapbox GL JS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程编排&lt;/strong&gt;：Bpmn.js&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地图服务&lt;/strong&gt;：GeoServer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间数据&lt;/strong&gt;：PostGIS&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>纳福猫舍智能信息管理系统</title><link>https://orangon.github.io/projects/nafu-cattery-system/</link><pubDate>Wed, 01 Oct 2025 00:00:00 +0000</pubDate><guid>https://orangon.github.io/projects/nafu-cattery-system/</guid><description>&lt;h2 id="项目概述"&gt;项目概述
&lt;/h2&gt;&lt;p&gt;为山东省邹城市纳福猫舍独立开发的全栈智能信息管理系统，包含 &lt;strong&gt;Web 后台管理系统（Next.js）&lt;/strong&gt; 与 &lt;strong&gt;店铺助手小程序（Uni-app）&lt;/strong&gt; 双端，实现猫舍日常运营的全面数字化。&lt;/p&gt;
&lt;h2 id="核心功能"&gt;核心功能
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;猫咪档案管理&lt;/strong&gt;：完整的猫咪信息登记、血统追溯、健康记录与成长追踪&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;品种管理&lt;/strong&gt;：多品种猫咪的标准化信息模板，支持自定义品种属性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;门店管理&lt;/strong&gt;：多门店统一管理，库存、订单、客户信息一站式维护&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 智能录入&lt;/strong&gt;：集成大语言模型，支持自然语言描述自动填充结构化档案，大幅降低信息录入成本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;售前售后支持&lt;/strong&gt;：小程序方便客户浏览猫咪信息，Web 后台辅助店主高效管理，显著提升售前沟通与售后服务效率&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="技术亮点"&gt;技术亮点
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;独立完成全栈架构设计与开发，前端到后端到部署全链路&lt;/li&gt;
&lt;li&gt;LLM 辅助信息录入，将传统手工填表效率提升 5 倍以上&lt;/li&gt;
&lt;li&gt;双端统一数据模型，小程序与 Web 后台实时同步&lt;/li&gt;
&lt;li&gt;已上线使用并获得店主认可，实际解决猫舍日常运营痛点&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="技术栈"&gt;技术栈
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Web 前端&lt;/strong&gt;：Next.js + React&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小程序&lt;/strong&gt;：Uni-app (Vue)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端&lt;/strong&gt;：Next.js API Routes + Prisma ORM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库&lt;/strong&gt;：PostgreSQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 能力&lt;/strong&gt;：LLM API 集成&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>AICV 简历王小程序</title><link>https://orangon.github.io/projects/aicv-resume-miniapp/</link><pubDate>Fri, 01 Aug 2025 00:00:00 +0000</pubDate><guid>https://orangon.github.io/projects/aicv-resume-miniapp/</guid><description>&lt;h2 id="项目概述"&gt;项目概述
&lt;/h2&gt;&lt;p&gt;智悦云创（湖南）科技有限公司旗下智能简历小程序，参与产品定义并主导整体技术方案设计，带领 3 名前端实习生从 0 到 1 完成开发交付。&lt;/p&gt;
&lt;h2 id="核心功能"&gt;核心功能
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简历智能评估&lt;/strong&gt;：调用自研简历评估算法接口，从排版、内容、匹配度等多维度对简历进行评分&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;智能优化建议&lt;/strong&gt;：基于评估结果，自动生成针对性的简历优化建议和改进方案&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完整闭环流程&lt;/strong&gt;：用户上传简历 → 系统智能评估 → 输出优化建议，全流程自动化&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我的职责"&gt;我的职责
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;产品定义&lt;/strong&gt;：参与产品定位与功能规划，输出核心交互流程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术方案&lt;/strong&gt;：负责小程序整体技术架构设计，制定敏捷迭代计划并分配开发任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队管理&lt;/strong&gt;：带领 3 名前端实习生，把控代码质量与交付节奏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持续优化&lt;/strong&gt;：小程序上线后服务 2000+ 用户，基于用户反馈完成 3 轮功能迭代&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="技术栈"&gt;技术栈
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端&lt;/strong&gt;：Uni-app + Vue&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平台&lt;/strong&gt;：微信小程序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端服务&lt;/strong&gt;：自研简历评估算法 API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;项目管理&lt;/strong&gt;：敏捷开发 + 迭代规划&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>智能企业人才评估系统</title><link>https://orangon.github.io/projects/talent-assessment-system/</link><pubDate>Sun, 01 Jun 2025 00:00:00 +0000</pubDate><guid>https://orangon.github.io/projects/talent-assessment-system/</guid><description>&lt;h2 id="项目概述"&gt;项目概述
&lt;/h2&gt;&lt;p&gt;智悦云创（湖南）科技有限公司面向大型企业招聘场景的智能人才评估系统。负责核心模块（简历评估）的方案设计与前端整体技术架构，保障前端交付质量。&lt;/p&gt;
&lt;h2 id="核心功能"&gt;核心功能
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;批量简历评估&lt;/strong&gt;：支持 HR 批量上传简历，系统自动完成多维度智能评估与打分排序，已处理 1000+ 份简历&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自反馈进化&lt;/strong&gt;：评估结果可被人工复核和修正，系统基于反馈持续优化评估模型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;企业级权限管理&lt;/strong&gt;：多角色权限体系，适配企业 HR、部门主管、管理员等不同角色&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我的职责"&gt;我的职责
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心模块设计&lt;/strong&gt;：负责简历评估模块的方案设计，输出落地技术文档，指导后续开发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端架构&lt;/strong&gt;：负责 React 前端整体技术架构设计，制定组件规范与工程化标准&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;质量把控&lt;/strong&gt;：保障前端交付质量，建立 Code Review 机制与自动化测试体系&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档沉淀&lt;/strong&gt;：输出技术方案文档与接口规范，降低团队协作成本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="技术栈"&gt;技术栈
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端框架&lt;/strong&gt;：React + TypeScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI 组件库&lt;/strong&gt;：Ant Design&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态管理&lt;/strong&gt;：Zustand&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程化&lt;/strong&gt;：Vite + ESLint + Prettier&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试&lt;/strong&gt;：Vitest + Playwright&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>给 RAG Agent 加流式输出：SSE 从设计到落地</title><link>https://orangon.github.io/posts/2026-08-07-sse-streaming-chat/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-08-07-sse-streaming-chat/</guid><description>&lt;p&gt;给一个基于 FastAPI + LangChain/LangGraph 的 RAG Agent 加上&amp;quot;打字机&amp;quot;式流式输出。本文按&lt;strong&gt;自顶向下&lt;/strong&gt;的顺序记录：从方案选型、协议设计，到后端/前端实现，最后是一段曲折的踩坑——&lt;strong&gt;dev 代理的 gzip 压缩会把流式悄悄变成一次性响应&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;先说结论：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;流式对话用 SSE（Server-Sent Events）就够了&lt;/strong&gt;——方案一（SSE 专用接口）成本可控、标准通用、能满足&amp;quot;打字机效果&amp;quot;的核心诉求。关键实现要点有三个：后端用 &lt;code&gt;agent.astream(stream_mode=&amp;quot;messages&amp;quot;)&lt;/code&gt; 逐 token 产出、前端用原生 &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;ReadableStream&lt;/code&gt; 解析（不能用 EventSource，因为要 POST + Authorization）、以及&lt;strong&gt;必须让响应头带 &lt;code&gt;Cache-Control: no-cache, no-transform&lt;/code&gt;&lt;/strong&gt;，否则开发代理的 gzip 会把流缓冲成一大块。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="一为什么需要流式"&gt;一、为什么需要流式
&lt;/h2&gt;&lt;p&gt;LLM 生成一段回复通常要 3~10 秒，Agent 场景还会先调工具检索，首字延迟更明显。如果没有流式，用户点击发送后只能盯着转圈，体验很差；而流式下首个 token 往往几百毫秒就到了，后续内容逐字出现，感知延迟大幅下降。&lt;/p&gt;
&lt;p&gt;在动手前先明确约束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;现有接口 &lt;code&gt;POST /chat&lt;/code&gt; 一次性返回 JSON，前端 &lt;code&gt;await&lt;/code&gt; 后整段渲染；&lt;/li&gt;
&lt;li&gt;后端已有 &lt;code&gt;create_react_agent(llm, tools)&lt;/code&gt; 的 ReAct Agent（含 RAG 检索工具和 MCP 工具）；&lt;/li&gt;
&lt;li&gt;前端是 React + umi（axios 封装），需要增量渲染能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二方案选型sse--websocket--ndjson"&gt;二、方案选型：SSE / WebSocket / NDJSON
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;SSE（方案一）&lt;/th&gt;
					&lt;th&gt;WebSocket（方案二）&lt;/th&gt;
					&lt;th&gt;NDJSON（方案三）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;实现成本&lt;/td&gt;
					&lt;td&gt;中&lt;/td&gt;
					&lt;td&gt;高&lt;/td&gt;
					&lt;td&gt;低&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;前端改动&lt;/td&gt;
					&lt;td&gt;中（换 fetch）&lt;/td&gt;
					&lt;td&gt;大（WS 客户端）&lt;/td&gt;
					&lt;td&gt;小（axios 参数）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;标准性 / 通用性&lt;/td&gt;
					&lt;td&gt;高&lt;/td&gt;
					&lt;td&gt;中&lt;/td&gt;
					&lt;td&gt;低&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;双向交互（停止 / 确认）&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
					&lt;td&gt;是&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;代理 / 负载均衡兼容&lt;/td&gt;
					&lt;td&gt;好&lt;/td&gt;
					&lt;td&gt;需要特殊支持&lt;/td&gt;
					&lt;td&gt;好（但怕压缩）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;适用场景&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;通用聊天流式，首选&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;需要 Agent 交互控制&lt;/td&gt;
					&lt;td&gt;快速验证 / 最小改动&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选择 SSE 的决定性理由：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单向流正是聊天场景需要的&lt;/strong&gt;——服务端推，客户端收，不需要上行通道；&lt;/li&gt;
&lt;li&gt;HTTP 协议、任何代理/CDN 都能透传，生产部署成本最低；&lt;/li&gt;
&lt;li&gt;保留原有 &lt;code&gt;POST /chat&lt;/code&gt; 不动，新端点并行存在，向后兼容；&lt;/li&gt;
&lt;li&gt;把事件对象设计成与传输协议解耦（&lt;code&gt;{&amp;quot;type&amp;quot;, &amp;quot;content&amp;quot;, ...}&lt;/code&gt;），未来要升级 WebSocket 或加&amp;quot;停止生成&amp;quot;功能时，只换传输层即可。&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;一个容易踩的坑：&lt;code&gt;EventSource&lt;/code&gt;（浏览器原生 SSE API）&lt;strong&gt;只支持 GET&lt;/strong&gt;，而我们的接口需要 POST + Authorization 请求头，所以前端必须用 &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;ReadableStream&lt;/code&gt; 手动解析 SSE 帧。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="三后端设计事件协议"&gt;三、后端设计：事件协议
&lt;/h2&gt;&lt;p&gt;流式接口把原来的一次性 JSON 响应拆成一串事件，每个事件一行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;type&amp;#34;: &amp;#34;text&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;地理信息系统（GIS&amp;#34;} # 增量文本，前端追加渲染
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;type&amp;#34;: &amp;#34;tool&amp;#34;, &amp;#34;name&amp;#34;: &amp;#34;info_retriever&amp;#34;} # 工具调用，可展示&amp;#34;正在检索…&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;type&amp;#34;: &amp;#34;done&amp;#34;, &amp;#34;response&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;session_id&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;message_count&amp;#34;: 6} # 结束
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;type&amp;#34;: &amp;#34;error&amp;#34;, &amp;#34;code&amp;#34;: 5010, &amp;#34;message&amp;#34;: &amp;#34;聊天调用超时&amp;#34;} # 出错
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SSE 帧格式是 &lt;code&gt;data: &amp;lt;JSON&amp;gt;\n\n&lt;/code&gt;，一次连接推多个事件，最后自然结束。&lt;/p&gt;
&lt;h2 id="四后端实现"&gt;四、后端实现
&lt;/h2&gt;&lt;h3 id="41-服务层async-generator-产出事件"&gt;4.1 服务层：async generator 产出事件
&lt;/h3&gt;&lt;p&gt;核心是 LangGraph 的 &lt;code&gt;agent.astream(..., stream_mode=&amp;quot;messages&amp;quot;)&lt;/code&gt;，它逐 token 产出 &lt;code&gt;(message_chunk, metadata)&lt;/code&gt;。这里有个非常优雅的特性：&lt;strong&gt;工具调用阶段产出的 AIMessageChunk 内容为空（只有 tool_calls），自然被 &lt;code&gt;elif content:&lt;/code&gt; 过滤掉；ToolMessage 单独透出为 tool 事件；最终答案逐 token 到达&lt;/strong&gt;。这样&amp;quot;文本流&amp;quot;和&amp;quot;工具事件流&amp;quot;自动分离。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chat_model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_build_tools_and_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;use_memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;db_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mcp_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mcp_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_create_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_model_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_react_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_meta&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;messages&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;callbacks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt; &lt;span class="c1"&gt;# 工具审计回调不变&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;stream_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;messages&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;msg_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# ToolMessage → 工具事件&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# AIMessageChunk → 增量文本&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# 纯对话直接流式&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;顺带一个重构收益：把&amp;quot;构建工具 + 消息列表&amp;quot;抽成 &lt;code&gt;_build_tools_and_messages()&lt;/code&gt;，普通 &lt;code&gt;/chat&lt;/code&gt; 和流式 &lt;code&gt;/chat/stream&lt;/code&gt; 共享，避免两套逻辑漂移。&lt;/p&gt;
&lt;h3 id="42-路由层streamingresponse--完整生命周期"&gt;4.2 路由层：StreamingResponse + 完整生命周期
&lt;/h3&gt;&lt;p&gt;流式不能牺牲原有保障，所以生命周期和一次性接口完全对齐：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先落库&lt;/strong&gt;：用户消息、ChatRun 审计记录在流开始前写入（超时/断线也能恢复用户输入）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流转&lt;/strong&gt;：把 generator 包进 &lt;code&gt;StreamingResponse(media_type=&amp;quot;text/event-stream&amp;quot;)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流结束落库&lt;/strong&gt;：AI 回复、会话标题、记忆、&lt;code&gt;finish_chat_run(&amp;quot;succeeded&amp;quot;)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时&lt;/strong&gt;：用 &lt;code&gt;asyncio.wait_for(iterator.__anext__(), timeout=...)&lt;/code&gt; 对每个 chunk 设超时，超时发 error 事件并记 &lt;code&gt;timed_out&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;断线/取消&lt;/strong&gt;：&lt;code&gt;except asyncio.CancelledError&lt;/code&gt; / &lt;code&gt;finally&lt;/code&gt; 兜底记 &lt;code&gt;cancelled&lt;/code&gt;，防止审计记录永远卡在 running。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_sse_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/chat/stream&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... 会话校验、先落库用户消息、create_chat_run ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;event_generator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;# 内部 while 循环 + asyncio.wait_for 逐块消费&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text/event-stream&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Cache-Control&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;no-cache, no-transform&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# 关键！见踩坑部分&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;X-Accel-Buffering&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;no&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Connection&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;keep-alive&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="五前端实现"&gt;五、前端实现
&lt;/h2&gt;&lt;h3 id="51-用-fetch-解析-sse"&gt;5.1 用 fetch 解析 SSE
&lt;/h3&gt;&lt;p&gt;为什么不用 &lt;code&gt;EventSource&lt;/code&gt;：它只支持 GET，且无法自定义 Authorization 头。用 &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;ReadableStream&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;chatStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;: &lt;span class="kt"&gt;Record&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Content-Type&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;application/json&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;token&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Authorization&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`Bearer &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/llm/v1/chat/stream&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;: &lt;span class="kt"&gt;JSON.stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`HTTP &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;响应体不可用&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;utf-8&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;: &lt;span class="kt"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sepIdx&lt;/span&gt;: &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sepIdx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n\n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 按空行切分 SSE 帧
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sepIdx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sepIdx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;data: &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;[DONE]&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 交给 React 增量渲染
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意：SSE 帧可能跨网络分块到达，所以必须用 &lt;code&gt;buffer&lt;/code&gt; 拼接后按 &lt;code&gt;\n\n&lt;/code&gt; 切分，不能假设一次 &lt;code&gt;read()&lt;/code&gt; 就是一个完整事件。&lt;/p&gt;
&lt;h3 id="52-增量渲染"&gt;5.2 增量渲染
&lt;/h3&gt;&lt;p&gt;状态更新只改最后一条 AI 消息：首个 text 事件创建消息（标记 &lt;code&gt;streaming&lt;/code&gt;），后续事件原地追加内容。配合一个闪烁光标，就是完整的打字机效果。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;streamedText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;streamMsgId&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chatStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;text&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;streamedText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamMsgId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;streamMsgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;: &lt;span class="kt"&gt;streamMsgId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;ai&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;: &lt;span class="kt"&gt;streamedText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;streaming&lt;/span&gt;: &lt;span class="kt"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;streamMsgId&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;: &lt;span class="kt"&gt;streamedText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ev.type === &amp;#39;tool&amp;#39; 可展示&amp;#34;正在检索…&amp;#34;；ev.type === &amp;#39;error&amp;#39; 提示错误
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="六踩坑记录gzip-把流式变成一次性响应"&gt;六、踩坑记录：gzip 把流式变成一次性响应
&lt;/h2&gt;&lt;p&gt;这是本次最值得记录的一环。&lt;/p&gt;
&lt;h3 id="现象"&gt;现象
&lt;/h3&gt;&lt;p&gt;后端直连、甚至通过 dev 代理用 curl 测试，事件都是&lt;strong&gt;逐块到达&lt;/strong&gt;的（实测 31 个事件分布在 10~16 秒内）。但浏览器里就是&lt;strong&gt;等全部回复结束后一次性渲染&lt;/strong&gt;，流式完全没有效果。&lt;/p&gt;
&lt;h3 id="定位"&gt;定位
&lt;/h3&gt;&lt;p&gt;在浏览器 Network 面板看 &lt;code&gt;chat/stream&lt;/code&gt; 响应头，发现两个可疑项：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Content-Encoding: gzip
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Powered-By: Express
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;X-Powered-By: Express&lt;/code&gt; 说明请求经过了 Umi dev 代理（Express 中间件）。&lt;strong&gt;Express 的 &lt;code&gt;compression&lt;/code&gt; 中间件默认对接受 gzip 的客户端启用压缩&lt;/strong&gt;——浏览器请求头带 &lt;code&gt;Accept-Encoding: gzip, deflate, br&lt;/code&gt;，而 curl 不带，所以 curl 正常、浏览器异常，就是这个差异造成的假象。&lt;/p&gt;
&lt;p&gt;为什么 gzip 会破坏流式？&lt;code&gt;compression&lt;/code&gt; 中间件把响应包进 zlib 流，zlib 默认攒够一个块（约 16KB）才输出。对一段几百字的回复，&lt;strong&gt;数据会在流结束时一次性 flush&lt;/strong&gt;，浏览器拿到的就是一整块压缩数据，解压后一次性渲染。&lt;/p&gt;
&lt;h3 id="修复cache-control-no-cache-no-transform"&gt;修复：&lt;code&gt;Cache-Control: no-cache, no-transform&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Umi 3 的 dev CLI 把 &lt;code&gt;compress: true&lt;/code&gt; 写死，&lt;code&gt;devServer.compress = false&lt;/code&gt; 覆盖不掉。最稳妥的方案是&lt;strong&gt;让后端对这个响应显式声明&amp;quot;不可转换&amp;quot;&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Cache-Control&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;no-cache, no-transform&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;no-transform&lt;/code&gt; 是 &lt;a class="link" href="https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.4" target="_blank" rel="noopener"
 &gt;RFC 7234 §5.2.2.4&lt;/a&gt; 定义的标准指令。Express &lt;code&gt;compression&lt;/code&gt; 中间件的 &lt;code&gt;shouldTransform()&lt;/code&gt; 正是检查它：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cacheControlNoTransformRegExp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/(?:^|,)\s*?no-transform\s*?(?:,|$)/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;shouldTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cacheControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Cache-Control&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Don&amp;#39;t compress for Cache-Control: no-transform
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cacheControl&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cacheControlNoTransformRegExp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheControl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;命中后中间件直接跳过压缩，流就能原样逐块透传给浏览器。&lt;strong&gt;实测对比&lt;/strong&gt;：同一代理，请求 &lt;code&gt;/umi.js&lt;/code&gt;（普通资源）带 &lt;code&gt;Accept-Encoding: gzip&lt;/code&gt; 返回 &lt;code&gt;Content-Encoding: gzip&lt;/code&gt;；而 SSE 响应带 &lt;code&gt;no-transform&lt;/code&gt; 后不再被压缩。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;顺带说明：&lt;code&gt;X-Accel-Buffering: no&lt;/code&gt; 是 nginx 的概念，对 Express 代理无效；真正让压缩中间件退场的是 &lt;code&gt;no-transform&lt;/code&gt;。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="七验证方法"&gt;七、验证方法
&lt;/h2&gt;&lt;p&gt;不要靠&amp;quot;感觉&amp;quot;，用带时间戳的实测脚本验证流是否真的逐块到达：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;urllib.request&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth_headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# 逐行读取，天然带到达时间信息&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;data: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;+&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;6.0f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;判读标准：&lt;strong&gt;事件间隔呈几十到几百毫秒的分布、且总耗时接近模型生成时长&lt;/strong&gt;（而不是第一帧就 0ms、最后一帧才全部出现）→ 流式生效。&lt;/p&gt;
&lt;h2 id="八小结与后续优化"&gt;八、小结与后续优化
&lt;/h2&gt;&lt;p&gt;这套实现的三个关键决策，值得带走：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;协议选 SSE&lt;/strong&gt;：单向聊天场景下，比 WebSocket 少 90% 的复杂度，且代理友好；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;stream_mode=&amp;quot;messages&amp;quot;&lt;/code&gt; 让文本流与工具事件自动分离&lt;/strong&gt;，Agent 的&amp;quot;思考-调用-作答&amp;quot;过程对前端透明化；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;no-transform&lt;/code&gt; 是流式接口的&amp;quot;免死金牌&amp;quot;&lt;/strong&gt;——任何中间件只要尊重标准，就不会吃掉你的流。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后续可选优化方向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;取消生成&lt;/strong&gt;：加一个 AbortController + 后端取消信号（这需要双向通道，届时再考虑 WebSocket 或独立的 cancel 端点）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具事件可视化&lt;/strong&gt;：前端把 &lt;code&gt;tool&lt;/code&gt; 事件渲染成&amp;quot;正在检索知识库…&amp;ldquo;的步骤条；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent 状态持久化&lt;/strong&gt;：用 LangGraph Checkpointer（SQLite/Postgres）替代内存 dict，跨请求保存 Agent 状态；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重连语义&lt;/strong&gt;：SSE 标准支持 &lt;code&gt;Last-Event-ID&lt;/code&gt;，可配合请求 ID 实现断线续传。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Apifox 调试 SSE 接口</title><link>https://orangon.github.io/posts/2026-08-05-apifox-sse-debug/</link><pubDate>Wed, 05 Aug 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-08-05-apifox-sse-debug/</guid><description>&lt;p&gt;SSE 广泛用于 AI 流式响应。Apifox（≥2.3.10）原生支持：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新建 HTTP 接口，填写地址、设置返回类型后发送。&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;text/event-stream&lt;/code&gt; 时自动解析 SSE 事件。&lt;/li&gt;
&lt;li&gt;时间线视图实时查看，内置 OpenAI / Gemini / Claude 格式自动合并。&lt;/li&gt;
&lt;li&gt;自定义格式配置 JSONPath 提取规则，或用后置脚本处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;参考：&lt;a class="link" href="https://docs.apifox.com/sse/" target="_blank" rel="noopener"
 &gt;Apifox 官方文档 - SSE 调试&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="编辑返回类型" class="gallery-image" data-flex-basis="296px" data-flex-grow="123" height="972" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://orangon.github.io/posts/2026-08-05-apifox-sse-debug/image.png" srcset="https://orangon.github.io/posts/2026-08-05-apifox-sse-debug/image_hu_11c06180f7563468.png 800w, https://orangon.github.io/posts/2026-08-05-apifox-sse-debug/image.png 1202w" width="1202"&gt;&lt;/p&gt;</description></item><item><title>Zed自定义Git命令</title><link>https://orangon.github.io/posts/2026-07-31-zed-git-commands/</link><pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-07-31-zed-git-commands/</guid><description>&lt;h1 id="zed自定义git命令"&gt;Zed自定义Git命令
&lt;/h1&gt;
 &lt;blockquote&gt;
 &lt;p&gt;适用环境：Zed 编辑器 + Git Graph / Git Panel · 配置位置：&lt;code&gt;~/.config/zed/tasks.json&lt;/code&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="一核心结论"&gt;一、核心结论
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;用 &lt;code&gt;tags: [&amp;quot;git-command&amp;quot;]&lt;/code&gt; 定义任务，配合 &lt;code&gt;$ZED_GIT_*&lt;/code&gt; 变量，把「查看、派生、分支操作」三类高频 Git 操作变成右键即用的菜单命令，从此告别手敲 SHA 和分支名。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一份配置，三个收益：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;零记忆&lt;/strong&gt;：不用记 SHA、不用复制粘贴，右键即点即用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零输入&lt;/strong&gt;：分支名 / 标签名 / 提交号全部由变量自动注入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零切换&lt;/strong&gt;：不离开编辑器，git 操作全部在 Zed 的终端里完成&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="二三大机制"&gt;二、三大机制
&lt;/h2&gt;&lt;p&gt;理解这三件事，后面所有配置都是它们的排列组合。&lt;/p&gt;
&lt;h3 id="21-入口命令从哪来"&gt;2.1 入口：命令从哪来
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;任务定义在&lt;strong&gt;全局&lt;/strong&gt; &lt;code&gt;tasks.json&lt;/code&gt;（工作区级 &lt;code&gt;.zed/tasks.json&lt;/code&gt; 暂不支持）&lt;/li&gt;
&lt;li&gt;带 &lt;code&gt;git-command&lt;/code&gt; 标签的任务会出现在 &lt;strong&gt;Git Graph&lt;/strong&gt; 的右键菜单里：
&lt;ul&gt;
&lt;li&gt;右键&lt;strong&gt;提交行&lt;/strong&gt; → 提交菜单（Commit 菜单）&lt;/li&gt;
&lt;li&gt;右键&lt;strong&gt;分支 / 远程引用 / 标签 chip&lt;/strong&gt; → 引用菜单（Ref 菜单）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="22-变量能自动注入什么"&gt;2.2 变量：能自动注入什么
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;变量&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;可用菜单&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$ZED_GIT_SHA&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;选中提交的完整 SHA&lt;/td&gt;
					&lt;td&gt;Commit + Ref&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$ZED_GIT_SHA_SHORT&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;短 SHA（7 位）&lt;/td&gt;
					&lt;td&gt;Commit + Ref&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$ZED_GIT_REPOSITORY_NAME&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;仓库名&lt;/td&gt;
					&lt;td&gt;Commit + Ref&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$ZED_GIT_REPOSITORY_PATH&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;仓库绝对路径（默认 cwd）&lt;/td&gt;
					&lt;td&gt;Commit + Ref&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$ZED_GIT_REF&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;点击的引用名（分支/远程/标签）&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;仅 Ref 菜单&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;

 &lt;blockquote&gt;
 &lt;p&gt;编辑器类变量（&lt;code&gt;$ZED_FILE&lt;/code&gt;、&lt;code&gt;$ZED_SELECTED_TEXT&lt;/code&gt; 等）在 Git 任务里&lt;strong&gt;不会提供&lt;/strong&gt;，引用它们的任务会被自动过滤。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="23-过滤规则任务怎么隐身"&gt;2.3 过滤规则：任务怎么&amp;quot;隐身&amp;quot;
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;任务里用了 &lt;code&gt;$ZED_GIT_REF&lt;/code&gt; → 只在右键 chip 时出现&lt;/li&gt;
&lt;li&gt;任务里只用 &lt;code&gt;$ZED_GIT_SHA&lt;/code&gt; → 提交菜单和引用菜单都出现&lt;/li&gt;
&lt;li&gt;任务不用任何变量 → 两种菜单都出现&lt;/li&gt;
&lt;li&gt;任务用了不存在的变量 → &lt;strong&gt;不出现&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;结论：用变量本身做&amp;quot;菜单路由&amp;quot;，想限定场景就引用对应变量。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="三三种写法与两个大坑"&gt;三、三种写法与两个大坑
&lt;/h2&gt;&lt;h3 id="31-三种写法"&gt;3.1 三种写法
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;写法&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
					&lt;th&gt;示例&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;A. 纯参数&lt;/strong&gt;：&lt;code&gt;command&lt;/code&gt; + &lt;code&gt;args&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;参数无空格、无特殊字符&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;git branch -d $ZED_GIT_REF&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;B. 整行命令&lt;/strong&gt;：整条 shell 命令放 &lt;code&gt;command&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;含管道 / 重定向 / 交互输入&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;git show $ZED_GIT_SHA | pbcopy&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;C. tty 交互&lt;/strong&gt;：&lt;code&gt;/dev/tty&lt;/code&gt; + &lt;code&gt;read&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;需要现场输入（起名 / 改名）&lt;/td&gt;
					&lt;td&gt;见 3.3&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="32-大坑一args-会被拆碎"&gt;3.2 大坑一：args 会被&amp;quot;拆碎&amp;quot;
&lt;/h3&gt;&lt;p&gt;Zed 会把 &lt;code&gt;command&lt;/code&gt; 和所有 &lt;code&gt;args&lt;/code&gt; &lt;strong&gt;用空格拼成一条命令行&lt;/strong&gt;交给登录 shell 执行，&lt;strong&gt;不负责转义&lt;/strong&gt;。所以：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;args&amp;#34;: [&amp;#34;-c&amp;#34;, &amp;#34;printf &amp;#39;Branch name: &amp;#39; &amp;gt; /dev/tty; ...&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;拼接后变成 &lt;code&gt;zsh -c printf 'Branch name: ' &amp;gt; /dev/tty; ...&lt;/code&gt;，&lt;code&gt;-c&lt;/code&gt; 后面只跟了 &lt;code&gt;printf&lt;/code&gt; 一个词 → 报 &lt;code&gt;zsh: printf:1: not enough arguments&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对策&lt;/strong&gt;：含空格/引号/特殊符号的命令一律用&lt;strong&gt;写法 B（整行命令）&lt;/strong&gt;，让登录 shell 原样执行，不经过拼接。&lt;/p&gt;
&lt;h3 id="33-大坑二交互输入要绑-devtty"&gt;3.3 大坑二：交互输入要绑 &lt;code&gt;/dev/tty&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;任务跑在 PTY 里，标准输入不一定是终端。输入提示的标准姿势：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Create tag at: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;printf &amp;#39;Tag name: &amp;#39; &amp;gt; /dev/tty; IFS= read -r name &amp;lt; /dev/tty; [ -n \&amp;#34;$name\&amp;#34; ] &amp;amp;&amp;amp; git tag \&amp;#34;$name\&amp;#34; \&amp;#34;$ZED_GIT_SHA\&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;三段式：&lt;strong&gt;打印提示到 tty → 从 tty 读入 → 非空才执行&lt;/strong&gt;。注意这里整行命令在 &lt;code&gt;command&lt;/code&gt; 字段，天然规避了大坑一。&lt;/p&gt;
&lt;h3 id="34-平台差异"&gt;3.4 平台差异
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pbcopy&lt;/code&gt;（复制到剪贴板）仅 macOS；Linux 换 &lt;code&gt;xclip -selection clipboard&lt;/code&gt; 或 &lt;code&gt;wl-copy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open -a Fork&lt;/code&gt; 依赖 Fork 应用，其他平台可换成 GitHub/GitLab 链接&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="四命令清单"&gt;四、命令清单
&lt;/h2&gt;&lt;h3 id="41-提交右键族commit-菜单-看取派查"&gt;4.1 提交右键族（Commit 菜单）—— 看、取、派、查
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;命令&lt;/th&gt;
					&lt;th&gt;作用&lt;/th&gt;
					&lt;th&gt;备注&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Cherry-pick&lt;/td&gt;
					&lt;td&gt;摘取提交&lt;/td&gt;
					&lt;td&gt;已有基础版；&lt;code&gt;-x&lt;/code&gt; 版会记录来源&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Revert&lt;/td&gt;
					&lt;td&gt;撤销提交&lt;/td&gt;
					&lt;td&gt;已有；&lt;code&gt;-m 1&lt;/code&gt; 版专用于合并提交&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Show&lt;/td&gt;
					&lt;td&gt;查看提交 + 文件统计&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;git show --stat&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Diff vs parent&lt;/td&gt;
					&lt;td&gt;只看该提交的改动&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;git diff sha^ sha&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Branches containing&lt;/td&gt;
					&lt;td&gt;哪些分支包含此提交&lt;/td&gt;
					&lt;td&gt;排查漏合并神器&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Copy patch&lt;/td&gt;
					&lt;td&gt;整段 patch 进剪贴板&lt;/td&gt;
					&lt;td&gt;macOS 需 &lt;code&gt;pbcopy&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Create fixup&lt;/td&gt;
					&lt;td&gt;对旧提交打补丁标记&lt;/td&gt;
					&lt;td&gt;配合 &lt;code&gt;rebase --autosquash&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Create tag at&lt;/td&gt;
					&lt;td&gt;在提交上打标签&lt;/td&gt;
					&lt;td&gt;tty 交互输入&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Bisect bad / good&lt;/td&gt;
					&lt;td&gt;二分定位回归&lt;/td&gt;
					&lt;td&gt;右键提交直接标记&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="42-分支标签右键族ref-菜单-引用的增删改查"&gt;4.2 分支标签右键族（Ref 菜单）—— 引用的增删改查
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;命令&lt;/th&gt;
					&lt;th&gt;作用&lt;/th&gt;
					&lt;th&gt;备注&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Delete branch&lt;/td&gt;
					&lt;td&gt;删除本地分支&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;-d&lt;/code&gt; 仅删已合并，&lt;code&gt;-D&lt;/code&gt; 强删&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Delete tag&lt;/td&gt;
					&lt;td&gt;删除本地标签&lt;/td&gt;
					&lt;td&gt;可选配&amp;quot;删远程标签&amp;quot;版&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Check out&lt;/td&gt;
					&lt;td&gt;切换分支&lt;/td&gt;
					&lt;td&gt;官方示例同款&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Merge&lt;/td&gt;
					&lt;td&gt;并入当前分支&lt;/td&gt;
					&lt;td&gt;注意当前分支被保护&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Rebase onto&lt;/td&gt;
					&lt;td&gt;当前分支变基到它&lt;/td&gt;
					&lt;td&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Diff vs HEAD&lt;/td&gt;
					&lt;td&gt;与当前分支的差异统计&lt;/td&gt;
					&lt;td&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Log&lt;/td&gt;
					&lt;td&gt;看该引用最近 15 条&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;--graph&lt;/code&gt; 带拓扑&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Push -u origin&lt;/td&gt;
					&lt;td&gt;推送并建立上游&lt;/td&gt;
					&lt;td&gt;新建分支后常用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Rename&lt;/td&gt;
					&lt;td&gt;重命名分支&lt;/td&gt;
					&lt;td&gt;tty 交互输入&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="43-通用族两种菜单都会出现-谨慎添加"&gt;4.3 通用族（两种菜单都会出现）—— 谨慎添加
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;git status&lt;/code&gt;、&lt;code&gt;git fetch --prune&lt;/code&gt; 这类无变量任务两种菜单都显示，容易把菜单刷爆，&lt;strong&gt;按需添加&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="五完整配置可直接粘贴"&gt;五、完整配置（可直接粘贴）
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;合并进 &lt;code&gt;~/.config/zed/tasks.json&lt;/code&gt; 的数组里即可。其中 &lt;code&gt;Create branch at&lt;/code&gt; 已修复为整行写法（规避大坑一）。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Cherry-pick&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cherry-pick&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Cherry-pick (-x): $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cherry-pick&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-x&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Revert&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;revert&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--no-edit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Revert merge: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;revert&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-m&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--no-edit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Show: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;show&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--stat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Branches containing: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;branch&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--contains&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Diff vs parent: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;diff&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA^&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Create fixup: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;commit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--fixup&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Copy patch: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git show $ZED_GIT_SHA | pbcopy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Create tag at: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;printf &amp;#39;Tag name: &amp;#39; &amp;gt; /dev/tty; IFS= read -r name &amp;lt; /dev/tty; [ -n \&amp;#34;$name\&amp;#34; ] &amp;amp;&amp;amp; git tag \&amp;#34;$name\&amp;#34; \&amp;#34;$ZED_GIT_SHA\&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bisect bad: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bisect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bad&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bisect good: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bisect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;good&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Create branch at: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;printf &amp;#39;Branch name: &amp;#39; &amp;gt; /dev/tty; IFS= read -r name &amp;lt; /dev/tty; [ -n \&amp;#34;$name\&amp;#34; ] &amp;amp;&amp;amp; git branch \&amp;#34;$name\&amp;#34; \&amp;#34;$ZED_GIT_SHA\&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;reveal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;always&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;use_new_terminal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Open in Fork&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;zsh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;open -a Fork . &amp;amp;&amp;amp; sleep 2 &amp;amp;&amp;amp; open \&amp;#34;fork://repo/$ZED_GIT_REPOSITORY_NAME/commit/$ZED_GIT_SHA\&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Reset --soft to: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;reset&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--soft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Reset --hard to: $ZED_GIT_SHA_SHORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;reset&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--hard&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_SHA&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Delete branch: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;branch&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Delete tag: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;tag&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Delete remote tag: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;push&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;origin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--delete&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;refs/tags/$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Check out: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;checkout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Merge: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;merge&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Rebase onto: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;rebase&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Diff vs HEAD: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;diff&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--stat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;HEAD&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Log: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;log&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--oneline&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--graph&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-15&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Push -u origin: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;push&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-u&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;origin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Rename: $ZED_GIT_REF&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;printf &amp;#39;New name: &amp;#39; &amp;gt; /dev/tty; IFS= read -r name &amp;lt; /dev/tty; [ -n \&amp;#34;$name\&amp;#34; ] &amp;amp;&amp;amp; git branch -m \&amp;#34;$ZED_GIT_REF\&amp;#34; \&amp;#34;$name\&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;git-command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="六收尾三条心法"&gt;六、收尾：三条心法
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;写法优先&lt;/strong&gt;：拿不准就整行写 &lt;code&gt;command&lt;/code&gt;，别跟 args 拼接较劲&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变量即路由&lt;/strong&gt;：菜单放哪，由变量决定，别硬塞&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标签醒目&lt;/strong&gt;：&lt;code&gt;Delete branch:&lt;/code&gt; / &lt;code&gt;Delete tag:&lt;/code&gt; 前缀写清楚，右键前瞄一眼就不手滑&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;记得刷新Git树验证命令效果，如tag添加和删除需要刷新后才能看到效果。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://zed.dev/docs/tasks?highlight=git%20custom#custom-git-commands" target="_blank" rel="noopener"
 &gt;Zed Docs — Custom Git Commands&lt;/a&gt; — 官方文档，涵盖 &lt;code&gt;git-command&lt;/code&gt; 标签、Git 专属变量（&lt;code&gt;$ZED_GIT_SHA&lt;/code&gt;、&lt;code&gt;$ZED_GIT_REF&lt;/code&gt; 等）、变量过滤规则和示例配置。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>接手老旧项目不用改公共gitignore：用git exclude静默忽略AI配置文件</title><link>https://orangon.github.io/posts/2026-07-24-git-exclude-ai-config/</link><pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-07-24-git-exclude-ai-config/</guid><description>&lt;h1 id="接手老旧项目不用改公共gitignore用git-exclude静默忽略ai配置文件"&gt;接手老旧项目不用改公共gitignore：用git exclude静默忽略AI配置文件
&lt;/h1&gt;&lt;p&gt;接手维护年代久远的历史项目时，很多人会借助 Coding Agent （例如Claude Code，Codex）生成 &lt;code&gt;CLAUDE.md&lt;/code&gt;、&lt;code&gt;AGENT.md&lt;/code&gt;，用来定制本地代码辅助规则。
这类文件只服务于我个人本地AI开发，不需要同步给团队、不能提交到远程仓库。但历史仓库的 &lt;code&gt;.gitignore&lt;/code&gt; 是团队统一维护的，随意新增规则会打乱原有规范，不适合修改；全局gitignore又会让所有项目都屏蔽这两个文件，灵活性太差。&lt;/p&gt;
&lt;p&gt;这时最佳方案：使用 &lt;strong&gt;git exclude&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="极简使用步骤"&gt;极简使用步骤
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;进入项目根目录，创建对应目录（不存在则自动新建）&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p .git/info
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="2"&gt;
&lt;li&gt;编辑私有忽略文件 &lt;code&gt;.git/info/exclude&lt;/code&gt;，写入两行规则&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CLAUDE.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AGENT.md
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;保存后即时生效，无需额外Git命令。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="核心优势"&gt;核心优势
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;文件存放在 &lt;code&gt;.git&lt;/code&gt; 目录内，不会被Git追踪、推送远程，仅当前本地这份仓库生效；&lt;/li&gt;
&lt;li&gt;全程不动仓库里任何公开文件，&lt;code&gt;.gitignore&lt;/code&gt; 零改动，不会影响团队所有人；&lt;/li&gt;
&lt;li&gt;语法和gitignore完全一致，上手无学习成本。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="简单选型对比"&gt;简单选型对比
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;项目&lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt;：改公共配置，全员生效，适合团队通用文件，此处不合适；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局gitignore&lt;/strong&gt;：全电脑所有仓库统一屏蔽，会误伤其他需要保留CLAUDE.md的项目；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;git exclude&lt;/strong&gt;：单仓库、个人私有屏蔽，完美适配本地AI配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="结尾小结"&gt;结尾小结
&lt;/h2&gt;&lt;p&gt;所有仅自己使用、不方便改动团队忽略清单的本地文件，优先交给git exclude管理。&lt;/p&gt;</description></item><item><title>用Code Server随时随地打开你的VS Code</title><link>https://orangon.github.io/posts/2026-07-24-code-server-vscode-anywhere/</link><pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-07-24-code-server-vscode-anywhere/</guid><description>&lt;h1 id="用code-server随时随地打开你的vs-code"&gt;用Code Server随时随地打开你的VS Code
&lt;/h1&gt;&lt;p&gt;想把家里电脑变成一个永远在线的开发环境，出门用 iPad、公司电脑、酒店商务机，只要能上网就能打开「自己那台 VS Code」继续写代码？开源项目 &lt;a class="link" href="https://github.com/coder/code-server" target="_blank" rel="noopener"
 &gt;code-server&lt;/a&gt; 就是干这个的：它在你的电脑上跑一个服务，把完整的 VS Code 编辑器通过浏览器渲染出来，任何联网设备打开网址就是熟悉的编辑器界面。&lt;/p&gt;
&lt;p&gt;下面按「需求描述 → 解决方案 → 配置过程」三步走。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="需求描述"&gt;需求描述
&lt;/h2&gt;&lt;p&gt;典型场景很明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码、环境、插件、终端全都装在自己电脑上，不想到处同步；&lt;/li&gt;
&lt;li&gt;不想被设备绑死——iPad、别人的 Mac、网吧 Windows，只要能开浏览器就想直接接着写；&lt;/li&gt;
&lt;li&gt;不想为了&amp;quot;换个地方写代码&amp;quot;去买云服务器、搞 SSH 隧道、装客户端。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;换句话说，目标是：&lt;strong&gt;把电脑上的 VS Code 变成一个网址，任何联网设备访问即用&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="解决方案"&gt;解决方案
&lt;/h2&gt;&lt;p&gt;code-server 会以服务形式跑在你的电脑上，监听一个端口，对外提供 HTTPS 的编辑器页面。访问端不需要装任何东西，浏览器即是客户端。&lt;/p&gt;
&lt;p&gt;对外暴露服务时需要一张 SSL 证书，让浏览器走 &lt;code&gt;https://&lt;/code&gt; 而不是被拦截的 &lt;code&gt;http://&lt;/code&gt;。code-server 官方在文档里给出了几种证书路线，其中最省事的是 &lt;strong&gt;自签名证书（self-signed certificate）&lt;/strong&gt;，前提是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的电脑（实例）需要能被互联网访问到；&lt;/li&gt;
&lt;li&gt;你&lt;strong&gt;不想&lt;/strong&gt;买域名、也不想用 SSH 端口转发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要提前知道自签名证书的几个限制（来自官方文档，务必留意）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;iPad 不支持自签名证书&lt;/strong&gt;，iPad 上直接访问会失败，详细情况见官方 &lt;code&gt;ipad.md&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;自签名证书可能给 code-server 带来一些意外的小问题；&lt;/li&gt;
&lt;li&gt;因此官方把它定位成「&lt;strong&gt;最后手段（last resort）&lt;/strong&gt;」——只有在你确实不想买域名 / 无法做端口转发时才用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你能接受这些限制，自签名这条路就足够轻量，继续看配置过程。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="配置过程"&gt;配置过程
&lt;/h2&gt;&lt;p&gt;以下命令基于 code-server 官方文档，目标是在电脑（实例）上启用自签名证书、监听 443 端口，然后通过 &lt;code&gt;https://&amp;lt;你的IP&amp;gt;&lt;/code&gt; 访问。&lt;/p&gt;
&lt;h3 id="1-ssh-进入你的电脑--实例"&gt;1. SSH 进入你的电脑 / 实例
&lt;/h3&gt;&lt;p&gt;确认这台机器对公网开放了 HTTP/HTTPS 流量（防火墙、安全组放行 443）。&lt;/p&gt;
&lt;h3 id="2-修改-code-server-配置"&gt;2. 修改 code-server 配置
&lt;/h3&gt;&lt;p&gt;code-server 的配置文件在 &lt;code&gt;~/.config/code-server/config.yaml&lt;/code&gt;。用官方给的两条 &lt;code&gt;sed&lt;/code&gt; 一键改：开启自签名证书、把监听地址从本机回环改成全网卡 + 443 端口。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 把 &amp;#34;cert: false&amp;#34; 改成 &amp;#34;cert: true&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sed -i.bak &lt;span class="s1"&gt;&amp;#39;s/cert: false/cert: true/&amp;#39;&lt;/span&gt; ~/.config/code-server/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 把 &amp;#34;bind-addr: 127.0.0.1:8080&amp;#34; 改成 &amp;#34;bind-addr: 0.0.0.0:443&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sed -i.bak &lt;span class="s1"&gt;&amp;#39;s/bind-addr: 127.0.0.1:8080/bind-addr: 0.0.0.0:443/&amp;#39;&lt;/span&gt; ~/.config/code-server/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 允许 code-server 监听 443 这个特权端口&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo setcap &lt;span class="nv"&gt;cap_net_bind_service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;+ep /usr/lib/code-server/lib/node
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;code&gt;sed -i.bak&lt;/code&gt; 会同时生成一个 &lt;code&gt;.bak&lt;/code&gt; 备份，方便回滚。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="3-重启-code-server"&gt;3. 重启 code-server
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart code-server@&lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-浏览器访问"&gt;4. 浏览器访问
&lt;/h3&gt;&lt;p&gt;现在在任意联网设备的浏览器里打开：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;https://&amp;lt;你的实例IP&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;就会看到 code-server 的登录页，输入 &lt;code&gt;config.yaml&lt;/code&gt; 里的密码即可进入编辑器。&lt;/p&gt;
&lt;h3 id="5-可选消除自签名证书的浏览器警告"&gt;5. （可选）消除自签名证书的浏览器警告
&lt;/h3&gt;&lt;p&gt;自签名证书浏览器会报「不安全」并弹警告。官方推荐用 &lt;a class="link" href="https://github.com/FiloSottile/mkcert" target="_blank" rel="noopener"
 &gt;mkcert&lt;/a&gt; 生成一张被本机操作系统信任的自签名证书，然后把证书路径填进 code-server 配置里的 &lt;code&gt;cert&lt;/code&gt; 和 &lt;code&gt;cert-key&lt;/code&gt; 两个字段即可消除警告。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;注意：mkcert 让本机信任这张证书，并不等于让 iPad / 其他设备也信任。跨设备无警告访问仍然需要一张正规证书（域名 + Let&amp;rsquo;s Encrypt 等）。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="小结"&gt;小结
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;code-server = 把本机 VS Code 变成网址，浏览器即客户端；&lt;/li&gt;
&lt;li&gt;自签名证书路线最省事，但 &lt;strong&gt;iPad 不支持&lt;/strong&gt;、可能伴随小问题，仅当不想买域名 / 不想端口转发时使用；&lt;/li&gt;
&lt;li&gt;核心就改两行配置（&lt;code&gt;cert: true&lt;/code&gt;、&lt;code&gt;bind-addr: 0.0.0.0:443&lt;/code&gt;）+ 一条 &lt;code&gt;setcap&lt;/code&gt; + 重启，之后 &lt;code&gt;https://&amp;lt;IP&amp;gt;&lt;/code&gt; 即可访问。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Monorepo 按变更自动部署</title><link>https://orangon.github.io/posts/2026-07-10-monorepo-auto-deploy/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-07-10-monorepo-auto-deploy/</guid><description>&lt;h1 id="monorepo-项目的按变更自动部署"&gt;Monorepo 项目的按变更自动部署
&lt;/h1&gt;&lt;p&gt;当一个仓库里放了多个子项目，它们共享同一套底层代码，每次推送如果全部重新构建和部署，既慢又浪费构建额度。更好的做法是：&lt;strong&gt;改了哪个项目，就只部署哪个项目&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="共享代码的困境"&gt;共享代码的困境
&lt;/h2&gt;&lt;p&gt;典型的 monorepo 目录结构：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;my-project/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── apps/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── web/ # 主站
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── admin/ # 后台
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── docs/ # 文档站
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── packages/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── shared-ui/ # 公共组件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── utils/ # 工具函数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── package.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;apps/&lt;/code&gt; 下的三个项目都依赖 &lt;code&gt;packages/&lt;/code&gt; 里的公共代码。公共代码一改，三个项目都可能受影响；但只改 &lt;code&gt;admin&lt;/code&gt; 的业务页面，&lt;code&gt;web&lt;/code&gt; 和 &lt;code&gt;docs&lt;/code&gt; 就没必要动。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="思路"&gt;思路
&lt;/h2&gt;&lt;p&gt;在 GitHub Actions 里加一个&amp;quot;变更检测&amp;quot;步骤，判断这次提交改了哪些目录。如果只改了 &lt;code&gt;apps/web&lt;/code&gt;，就只构建和部署 &lt;code&gt;web&lt;/code&gt;；如果改了 &lt;code&gt;packages/utils&lt;/code&gt;，所有依赖它的项目都触发部署。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;推送代码 → 检测哪些目录变了 → 只构建和部署变更涉及的项目 → 未变更的跳过
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="示例"&gt;示例
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deploy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;main]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 第一步：检测变更&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;detect-changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;web&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ steps.changes.outputs.web }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ steps.changes.outputs.admin }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ steps.changes.outputs.docs }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fetch-depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;dorny/paths-filter@v2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;changes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; web:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;apps/web/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;packages/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; admin:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;apps/admin/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;packages/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docs:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;apps/docs/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;packages/**&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 第二步：按条件部署&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy-web&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;detect-changes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;needs.detect-changes.outputs.web == &amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ... 构建和部署 web&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy-admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;detect-changes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;needs.detect-changes.outputs.admin == &amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ... 构建和部署 admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy-docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;detect-changes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;needs.detect-changes.outputs.docs == &amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ... 构建和部署 docs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="关键设计"&gt;关键设计
&lt;/h2&gt;&lt;h3 id="变更规则怎么写"&gt;变更规则怎么写
&lt;/h3&gt;&lt;p&gt;每个子项目的 &lt;code&gt;filters&lt;/code&gt; 要同时包含自己的目录和公共包目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; web:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;apps/web/**&amp;#39; # 自己的业务代码
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;packages/**&amp;#39; # 公共依赖
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; admin:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;apps/admin/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;packages/**&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样公共包一改，所有子项目都会触发部署，不会遗漏。&lt;/p&gt;
&lt;p&gt;如果公共包内部也可以区分为&amp;quot;仅 A 项目用&amp;quot;和&amp;quot;B、C 共用&amp;quot;，可以写得更细：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; web:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;apps/web/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;packages/shared-ui/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;packages/utils/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; admin:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;apps/admin/**&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; - &amp;#39;packages/utils/**&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="条件跳过"&gt;条件跳过
&lt;/h3&gt;&lt;p&gt;通过 &lt;code&gt;needs&lt;/code&gt; 拿到上一步的输出结果，用 &lt;code&gt;if&lt;/code&gt; 决定是否执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;deploy-admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;detect-changes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;needs.detect-changes.outputs.admin == &amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;没变化的项目这个 Job 直接跳过，不浪费 CI 时间。&lt;/p&gt;
&lt;h3 id="支持手动全量部署"&gt;支持手动全量部署
&lt;/h3&gt;&lt;p&gt;加一个手动触发入口，允许绕开变更检测，强制部署所有项目：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;main]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy_all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;强制全量部署&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;boolean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后在 Job 的条件里加判断：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; needs.detect-changes.outputs.admin == &amp;#39;true&amp;#39; ||
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; github.event.inputs.deploy_all == &amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>前端自动部署模式对比</title><link>https://orangon.github.io/posts/2026-07-10-frontend-deploy-modes/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-07-10-frontend-deploy-modes/</guid><description>&lt;h1 id="前端自动部署的几种方式"&gt;前端自动部署的几种方式
&lt;/h1&gt;&lt;p&gt;维护项目的一大痛点就是希望推送代码后自动触发更新部署，对此可以采用 GitHub Actions 设置自动部署的工作流（Workflow），对于前端来说有三种主流做法：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;&lt;/th&gt;
					&lt;th&gt;直传文件&lt;/th&gt;
					&lt;th&gt;服务器构建&lt;/th&gt;
					&lt;th&gt;推送仓库拉取&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;构建位置&lt;/td&gt;
					&lt;td&gt;CI&lt;/td&gt;
					&lt;td&gt;服务器&lt;/td&gt;
					&lt;td&gt;CI&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;传输内容&lt;/td&gt;
					&lt;td&gt;静态产物&lt;/td&gt;
					&lt;td&gt;完整源码&lt;/td&gt;
					&lt;td&gt;镜像&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;服务器需装&lt;/td&gt;
					&lt;td&gt;Web 服务&lt;/td&gt;
					&lt;td&gt;Docker + 构建工具&lt;/td&gt;
					&lt;td&gt;仅 Docker&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多服务器部署&lt;/td&gt;
					&lt;td&gt;逐个上传&lt;/td&gt;
					&lt;td&gt;逐个构建&lt;/td&gt;
					&lt;td&gt;各自拉取同一镜像&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;回滚方式&lt;/td&gt;
					&lt;td&gt;切备份目录&lt;/td&gt;
					&lt;td&gt;切旧镜像&lt;/td&gt;
					&lt;td&gt;拉指定 tag 镜像&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;主要局限&lt;/td&gt;
					&lt;td&gt;只能静态文件&lt;/td&gt;
					&lt;td&gt;服务器负担重、多台不一致&lt;/td&gt;
					&lt;td&gt;需维护镜像仓库&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三种模式遵循同一个核心逻辑：&lt;strong&gt;构建 → 替换线上 → 出问题可回退&lt;/strong&gt;。区别只在于构建在哪发生、中间产物是什么。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="模式一ci-构建--直传文件"&gt;模式一：CI 构建 + 直传文件
&lt;/h2&gt;&lt;h3 id="思路"&gt;思路
&lt;/h3&gt;&lt;p&gt;这是最直接的方式。在 CI 里装依赖、跑构建，把产出的静态文件传到服务器站点目录，用新旧目录互换上线。但它的前提是：&lt;strong&gt;构建产物是静态文件&lt;/strong&gt;，丢到 Web 服务器就能跑。如果项目需要以容器方式运行，光传文件没用，得走 Docker 构建。&lt;/p&gt;
&lt;h3 id="流程"&gt;流程
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;推送代码 → CI 构建 → SCP 传文件到服务器 → 备份旧目录 → 替换为新文件 → 刷新服务
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="示例"&gt;示例
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deploy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;main]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/setup-node@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;node-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;20&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;npm&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;npm ci&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;npm run build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;appleboy/scp-action@v0.1.7&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.HOST }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.USERNAME }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.PRIVATE_KEY }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dist/*&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/www/site/dist_new/&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;strip_components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.HOST }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.USERNAME }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.PRIVATE_KEY }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; cd /www/site
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; BACKUP=&amp;#34;backup_$(date +%Y%m%d_%H%M%S)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; mkdir -p &amp;#34;$BACKUP&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; find . -maxdepth 1 -mindepth 1 ! -name &amp;#39;dist_new&amp;#39; ! -name &amp;#39;backup_*&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; -exec mv {} &amp;#34;$BACKUP&amp;#34;/ \;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; cp -r dist_new/* . &amp;amp;&amp;amp; rm -rf dist_new
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ls -dt backup_* | tail -n +6 | xargs -r rm -rf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; nginx -s reload&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="模式二传源码--服务器构建"&gt;模式二：传源码 + 服务器构建
&lt;/h2&gt;&lt;h3 id="思路-1"&gt;思路
&lt;/h3&gt;&lt;p&gt;模式一只能传最终的静态产物，碰上需要完整源码环境才能构建的项目（比如容器化部署）就行不通。模式二把&lt;strong&gt;整个仓库源码传到服务器&lt;/strong&gt;，在服务器上现场构建、打包成镜像再启动。但这种方式的问题是：服务器必须装上全套构建工具，而且如果有好几台服务器，每台都得各自构建一遍，费时且结果可能不一致。&lt;/p&gt;
&lt;h3 id="流程-1"&gt;流程
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;推送代码 → SCP 传源码 → 服务器上构建镜像 → 停旧容器 → 启新容器
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="示例-1"&gt;示例
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deploy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;main]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;appleboy/scp-action@v0.1.7&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.HOST }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.USERNAME }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.PRIVATE_KEY }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/opt/project/build/&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;strip_components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.HOST }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.USERNAME }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.PRIVATE_KEY }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; cd /opt/project/build
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker build -t my-app:latest .
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker stop my-app 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker rm my-app 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker run -d --name my-app -p 3000:80 --restart=always my-app:latest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker image prune -f&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="模式三ci-构建镜像--推送仓库--服务器拉取"&gt;模式三：CI 构建镜像 + 推送仓库 + 服务器拉取
&lt;/h2&gt;&lt;h3 id="思路-2"&gt;思路
&lt;/h3&gt;&lt;p&gt;模式二让服务器承担了构建任务，服务器多了就成问题。模式三把&lt;strong&gt;构建和部署彻底分开&lt;/strong&gt;：CI 里构建好镜像，推到镜像仓库；服务器不再参与构建，只需要拉取镜像、替换容器。这样每台服务器拿到的都是同一个镜像，构建环境统一在 CI 里，服务器只管运行。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;可以考虑 GitHub Container Registry 托管镜像，其与 Actions 无缝集成，登录直接用 &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; 即可。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="流程-2"&gt;流程
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;推送代码 → CI 构建镜像 → 推送到镜像仓库 → 服务器拉取镜像 → 停旧容器 → 启新容器
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="示例-2"&gt;示例
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deploy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;main]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build-and-push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;docker/login-action@v3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ghcr.io&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ github.actor }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;docker/build-push-action@v5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ghcr.io/${{ github.repository }}:latest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ghcr.io/${{ github.repository }}:${{ github.sha }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;build-and-push&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.HOST }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.USERNAME }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.PRIVATE_KEY }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker pull ghcr.io/${{ github.repository }}:latest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker stop my-app 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker rm my-app 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker run -d \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --name my-app \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; -p 3000:80 \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --restart=always \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ghcr.io/${{ github.repository }}:latest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker image prune -f&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>后端框架选型：Fastify / FastAPI / Spring Boot / Go 怎么选？</title><link>https://orangon.github.io/posts/2026-06-30-web-framework-selection/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-06-30-web-framework-selection/</guid><description>&lt;p&gt;我在为一个小型信息管理 Web 项目做后端选型，候选有 Node.js (Fastify)、Python (FastAPI)、Java (Spring Boot)、Go (Gin)。为了不被&amp;quot;听说&amp;quot;带节奏，我读了两篇风格迥异的性能对比文章，把结论提炼出来，并给出针对这类项目的明确推荐。&lt;/p&gt;
&lt;p&gt;先说结论：&lt;strong&gt;对小型信息管理系统，我会首选 Python (FastAPI)；如果团队是 JS/TS 背景，选 Fastify；只有当你在意单二进制部署或预期流量会快速增长时，才需要认真考虑 Go (Gin)。Spring Boot 对这个量级偏重，除非你本来就是 Java 栈。&lt;/strong&gt; 下面是依据。&lt;/p&gt;
&lt;h2 id="一第一篇travis-luong-的单机-wrk-基准测试"&gt;一、第一篇：Travis Luong 的单机 wrk 基准测试
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;来源：&lt;a class="link" href="https://www.travisluong.com/fastapi-vs-fastify-vs-spring-boot-vs-gin-benchmark/" target="_blank" rel="noopener"
 &gt;FastAPI vs Fastify vs Spring Boot vs Gin Benchmark&lt;/a&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这篇是相对&amp;quot;实验室&amp;quot;式的测试：每个框架写一个接口，从 PostgreSQL 查 100 行数据返回 JSON，用 wrk 压 10 秒。作者非常坦诚，多次更新修正了&amp;quot;进程数不对等&amp;quot;等测试缺陷。它的价值不在于绝对数字，而在于揭示了&lt;strong&gt;配置对性能的放大效应&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代表性排名（req/sec，摘自作者的最终榜单）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;排名&lt;/th&gt;
					&lt;th&gt;配置&lt;/th&gt;
					&lt;th&gt;req/sec&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;Spring Boot + &lt;strong&gt;jdbc（原生）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;7886&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2&lt;/td&gt;
					&lt;td&gt;Go + &lt;strong&gt;pgx&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;7517&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;3&lt;/td&gt;
					&lt;td&gt;Go + pg + 连接池调优&lt;/td&gt;
					&lt;td&gt;7388&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;4&lt;/td&gt;
					&lt;td&gt;FastAPI + &lt;strong&gt;asyncpg + ujson + gunicorn 8w&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;4831&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;5&lt;/td&gt;
					&lt;td&gt;Fastify + pg + cluster 8w（关日志）&lt;/td&gt;
					&lt;td&gt;4622&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;8&lt;/td&gt;
					&lt;td&gt;Express + pg + cluster 8w&lt;/td&gt;
					&lt;td&gt;4145&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;10&lt;/td&gt;
					&lt;td&gt;Gin + database/sql + lib/pq&lt;/td&gt;
					&lt;td&gt;2966&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;12&lt;/td&gt;
					&lt;td&gt;Fastify + pg（单进程）&lt;/td&gt;
					&lt;td&gt;2184&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;20&lt;/td&gt;
					&lt;td&gt;Spring Boot + &lt;strong&gt;JPA（ORM）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;844&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;作者自己提炼的几点，比排名本身更值得记：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;FastAPI 开箱并不快&lt;/strong&gt;——必须配上 &lt;code&gt;asyncpg&lt;/code&gt; 异步驱动才能发挥，光这一点就能拉开数倍差距。&lt;/li&gt;
&lt;li&gt;即使用了 asyncpg，&lt;strong&gt;还要换更快的 JSON 库（orjson/ujson）&lt;/strong&gt; 才能追上 Node.js。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原生 SQL 转 JSON 远快于 ORM&lt;/strong&gt;——最直观的证据：同样是 Spring Boot，用 jdbc 是 7886，换成 JPA 直接掉到 844，差了近 10 倍。框架的&amp;quot;慢&amp;quot;往往来自 ORM 层，而不是框架本身。&lt;/li&gt;
&lt;li&gt;编译型语言（Java/Go）在同等配置下确实比解释型快。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志会拖性能&lt;/strong&gt;——Fastify 关掉日志后明显提速。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;这篇告诉我们&amp;quot;配置 &amp;gt; 选型&amp;quot;——同一个框架，会不会调，性能能差一个数量级。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="二第二篇10-亿请求下的生存测试"&gt;二、第二篇：10 亿请求下的&amp;quot;生存测试&amp;quot;
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;来源：&lt;a class="link" href="https://www.51cto.com/article/816124.html" target="_blank" rel="noopener"
 &gt;五大主流 Web 框架真实性能对比：10 亿请求下谁能幸存？&lt;/a&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这篇更&amp;quot;生产化&amp;quot;：谷歌云 4 核 16G、Docker 部署、PostgreSQL 连接池，用 wrk2 把流量从 100 RPS 一路爬到 10 万 RPS，同时观察吞吐、延迟、内存、CPU、错误率。&lt;/p&gt;
&lt;p&gt;核心数据：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;框架&lt;/th&gt;
					&lt;th&gt;持续稳定 RPS&lt;/th&gt;
					&lt;th&gt;峰值内存&lt;/th&gt;
					&lt;th&gt;99% 延迟&lt;/th&gt;
					&lt;th&gt;CPU 峰值&lt;/th&gt;
					&lt;th&gt;错误率(10万RPS)&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Rust (Actix-Web)&lt;/td&gt;
					&lt;td&gt;110,000&lt;/td&gt;
					&lt;td&gt;250 MB&lt;/td&gt;
					&lt;td&gt;7 ms&lt;/td&gt;
					&lt;td&gt;75%&lt;/td&gt;
					&lt;td&gt;0.01%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Go (Gin)&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;105,000&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;190 MB&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;10 ms&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;68%&lt;/td&gt;
					&lt;td&gt;0.03%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Node.js (Fastify)&lt;/td&gt;
					&lt;td&gt;60,000&lt;/td&gt;
					&lt;td&gt;650 MB&lt;/td&gt;
					&lt;td&gt;35 ms&lt;/td&gt;
					&lt;td&gt;82%&lt;/td&gt;
					&lt;td&gt;0.5%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Java (Spring Boot)&lt;/td&gt;
					&lt;td&gt;40,000&lt;/td&gt;
					&lt;td&gt;1.4 GB&lt;/td&gt;
					&lt;td&gt;50 ms&lt;/td&gt;
					&lt;td&gt;88%&lt;/td&gt;
					&lt;td&gt;0.3%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Python (FastAPI)&lt;/td&gt;
					&lt;td&gt;8,000&lt;/td&gt;
					&lt;td&gt;1.2 GB&lt;/td&gt;
					&lt;td&gt;150 ms&lt;/td&gt;
					&lt;td&gt;100%&lt;/td&gt;
					&lt;td&gt;15%&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;作者的&amp;quot;幸存者&amp;quot;分级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;绝对幸存者 —— Go (Gin)&lt;/strong&gt;：性能、开发效率、生态三者最平衡，像&amp;quot;AK47&amp;quot;，易上手、耐造。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极限生存者 —— Rust (Actix)&lt;/strong&gt;：性能天花板，但学习曲线陡，需专业团队。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主流生存者 —— Fastify / Spring Boot&lt;/strong&gt;：覆盖大多数公司的常规战场。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;限定生存者 —— FastAPI&lt;/strong&gt;：仅在低并发&amp;quot;安全区&amp;quot;（&amp;lt; 1 万 RPS）表现良好，适合数据管道、AI 模型接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;这篇把极限高并发的天花板拉了出来——系统级语言（Go/Rust）是降维打击，但前提是你真的有那么多流量。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="三两篇放在一起看"&gt;三、两篇放在一起看
&lt;/h2&gt;&lt;p&gt;两篇文章结论高度一致，但视角互补：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能天花板&lt;/strong&gt;：Go ≈ Java(原生) &amp;gt; Node &amp;gt; Python。压到极限时，编译型语言的优势会随并发上升而放大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源效率&lt;/strong&gt;：Go 最省（190MB），Java 最重（1.4GB + 启动 12 秒）。Python 在高并发下反而吃内存（1.2GB）且错误率飙升。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;易用性 / 开发效率&lt;/strong&gt;：恰好与性能排序相反——FastAPI 开发最快、自动文档最香，但抗并发最弱。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关键陷阱：&lt;strong&gt;别把高并发结论直接套到小项目上。&lt;/strong&gt; 第二篇自己也写明，FastAPI 的&amp;quot;安全区&amp;quot;是 &amp;lt; 1 万 RPS——而一个小型信息管理系统，日常峰值大概率在几十到几百 RPS，离这个阈值有两个数量级。在这种流量下，框架本身的吞吐根本不是瓶颈，&lt;strong&gt;开发速度、可维护性、文档自动生成&lt;/strong&gt;才是。&lt;/p&gt;
&lt;h2 id="四针对小型信息管理系统的推荐"&gt;四、针对&amp;quot;小型信息管理系统&amp;quot;的推荐
&lt;/h2&gt;&lt;p&gt;信息管理系统的典型特征：以 CRUD 为主、中低并发、前后端分离、追求快速交付。这类项目的真正成本不在服务器，而在&lt;strong&gt;开发和对接的人力&lt;/strong&gt;。所以选型权重应该是：开发效率 &amp;gt; 文档/对接 &amp;gt; 部署运维 &amp;gt; 极限性能。&lt;/p&gt;
&lt;p&gt;按这个权重，决策如下：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;你的情况&lt;/th&gt;
					&lt;th&gt;推荐&lt;/th&gt;
					&lt;th&gt;理由&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;想最快交付、要自动 API 文档给前端&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;类型提示 + 自动 OpenAPI 文档，信息管理系统的 CRUD 接口几乎&amp;quot;写模型即得文档&amp;quot;；&amp;lt;1万 RPS 安全区绰绰有余&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;团队是 JS/TS 背景，想前后端同语言&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Fastify&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;比 Express 快、schema 校验内置，前后端类型可复用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;追求单二进制部署、运维简单，或预期流量会涨&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Go (Gin)&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;编译成一个可执行文件扔服务器即可，无运行时依赖；两篇文章里都是&amp;quot;绝对幸存者&amp;quot;，未来不会遇到性能墙&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;已有 Java 栈 / 复杂事务和权限&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Spring Boot&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;生态最全，但启动慢、内存重，对小项目是&amp;quot;用航母运快递&amp;quot;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我的最终选择是 &lt;strong&gt;FastAPI&lt;/strong&gt;。原因很实在：信息管理系统的价值在于&amp;quot;把数据管起来、把接口暴露清楚&amp;quot;，FastAPI 的 Pydantic 模型 + 自动文档把这两件事的成本压到了最低；性能上配 &lt;code&gt;asyncpg + orjson&lt;/code&gt;（第一篇验证可到 ~4800 req/s）对小型系统完全是冗余的富余。等哪天流量真涨上来，再按第二篇的指引用 Go 重写核心接口，成本也可控——技术选型从来不是非黑即白，而是匹配当下的战场。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考来源：&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Travis Luong — &lt;a class="link" href="https://www.travisluong.com/fastapi-vs-fastify-vs-spring-boot-vs-gin-benchmark/" target="_blank" rel="noopener"
 &gt;FastAPI vs Fastify vs Spring Boot vs Gin Benchmark&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;51CTO — &lt;a class="link" href="https://www.51cto.com/article/816124.html" target="_blank" rel="noopener"
 &gt;五大主流 Web 框架真实性能对比：10 亿请求下谁能幸存？&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>派生模型</title><link>https://orangon.github.io/posts/2026-03-01/</link><pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-03-01/</guid><description>&lt;h1 id="派生模型"&gt;派生模型
&lt;/h1&gt;&lt;p&gt;派生模型，是一个我师兄博士论文用到的科研术语的简写，指的是能派生出所需数据的模型。得到的数据叫做派生数据。但是存在歧义：派生是模型的目的，数据是派生的结果，但容易被理解为动宾结构，即模型是派生的结果。&lt;/p&gt;
&lt;p&gt;如果翻译为英文就没有这个问题，不是被派生的模型 derived model，而是用于派生数据的模型 model for deriving data。就像生成模型，可以理解为生成内容的模型，可以生成文本/图片，还可以理解为有一个东西生成了模型。&lt;/p&gt;
&lt;p&gt;派生不应该修饰模型，应该修饰数据，所以不应该这样简写。&lt;/p&gt;</description></item><item><title>AI 应该只是工具</title><link>https://orangon.github.io/posts/2026-02-15/</link><pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-02-15/</guid><description>&lt;h1 id="ai应该只是工具辅助人类"&gt;AI应该只是工具，辅助人类
&lt;/h1&gt;&lt;p&gt;只要LLM的底层还是transformer，那么它的本质仍是预测下一个词元（token）。这虽然能让它通过图灵测试，但是不代表它拥有和人类等同的智能。注意我说的是等同，不是同等。等同是完全相同，而同等是指同一水平。图灵测试结果就是通过与否，而没有人关心是如何通过的。就像高考，即使同一套试卷都考了600分，但得分点也不是完全相同的，而只能说有同等水平。&lt;/p&gt;
&lt;p&gt;细究起来，LLM和人脑差异巨大，说明LLM的智能和人类的智能在底层上就是截然不同的。&lt;/p&gt;
&lt;p&gt;那么我们也不能把AI Agent（智能体）和人类等同起来。我能接受AI Agent和人都是智能体，但不能接受他们是同一种智能体。换句话说，我不会把AI当做人，也不会反过来。&lt;/p&gt;
&lt;p&gt;我不会把AI当做有情感的人，不会把它当做我的伙伴或者下属，更遑论恋人。因为我无论怎么攻击他，怎么辱骂他，他也不会真正愤怒或者罢工，它也不会生病，除了打打喷嚏——而这通常是因为光顾它的人太多了，服务繁忙。&lt;/p&gt;</description></item><item><title>横切与竖切</title><link>https://orangon.github.io/posts/2026-02-14-stratification/</link><pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-02-14-stratification/</guid><description>&lt;h1 id="横切与竖切"&gt;横切与竖切
&lt;/h1&gt;&lt;p&gt;有一天千海看完动画EVA，冒出来一个想法：人类要是一个整体就好了，几十亿人共用一个大脑，一副身体，这样就能消除人们之间的隔阂。没有猜忌，没有误解的世界该多好啊，所有人都能齐心协力做同一件事。&lt;/p&gt;
&lt;p&gt;但现实并非如此，每个人是独立的个体，有各自的大脑和身体，这就注定了人与人之间的想法千差万别。&lt;/p&gt;
&lt;p&gt;于是要想与他人达成合作，千海必须了解他人。&lt;/p&gt;
&lt;p&gt;但又不可能去认识、了解每个人的所有想法，那太累了。&lt;/p&gt;
&lt;p&gt;千海自然而然地想到，通过一些标签给人分类，这不就方便认识他人了么？&lt;/p&gt;
&lt;p&gt;人以类聚物以群分，根据一个人的标签，就可以大概了解TA的性格和习惯，然后推测TA的行为。&lt;/p&gt;
&lt;p&gt;那么如何给人分类呢？好比切蛋糕，可以竖切，也可以横切。&lt;/p&gt;
&lt;p&gt;假如面前有一块榴莲千层蛋糕，竖切是一视同仁，每个人都能吃到顶层的榴莲、奶油和底层的面包；而横切则是把顶层和底层给分开，这显然是一种不平等的切法。&lt;/p&gt;
&lt;p&gt;千海观察到：&lt;/p&gt;
&lt;p&gt;典型的竖切有：星座、生肖、血型、人种、性别。
最近比较火的竖切有：MBTI、居然人与竟然人。&lt;/p&gt;
&lt;p&gt;典型的横切有：阶级、阶层、学历、职称。
最近比较火的横切有：苹果人与安卓人（已封杀）。&lt;/p&gt;
&lt;p&gt;然后千海意识到，现实中是横切与竖切并存，如果只看到竖切或只看到横切，那么难免过于片面。&lt;/p&gt;</description></item><item><title>走，吃面去！🍜</title><link>https://orangon.github.io/posts/2026-02-03-noodles/</link><pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-02-03-noodles/</guid><description>&lt;h1 id="走吃面去"&gt;走，吃面去！🍜
&lt;/h1&gt;&lt;h2 id="新面店"&gt;新面店
&lt;/h2&gt;&lt;p&gt;地下一层开了一家新面店，主打是现做牛肉面。我发现大家都喜欢“现做“二字，八成是受了西贝的影响。&lt;/p&gt;
&lt;p&gt;这是几周以来最暖和的一个冬日，我约了一个朋友到新面店，想聊聊创业。我们扫开点餐二维码一看，怎么就三款面条，其他都是小吃？不管他了，我的原则就是先吃招牌菜。如果招牌菜都难以下咽，其他的菜更不用试了——这是我在披萨店点了炸鸡之后得出的教训。&lt;/p&gt;
&lt;p&gt;现在还不到12点，客流量还不大。此时店里居然只有吧台一样的区域还有座位，没有小桌了，有点尴尬。我们找了地方坐下，发现桌面上有个白色圆柱状的盒子，我打开一摸，居然是抽式湿巾，有点意思。我们脱了羽绒服，忽然发现不知道放哪，回头一看，有专门的衣架，挺有意思。&lt;/p&gt;
&lt;p&gt;我们最终点了两份招牌面，面的名字花里胡哨一长串儿，我没记住。我怀疑他们服务员也记不住。&lt;/p&gt;
&lt;p&gt;客源不断，我看到几个熟悉的人在等位。&lt;/p&gt;
&lt;p&gt;我们一边等面，一边聊创业团队的上限与下限。也就是说，一个创业团队应该怎么配置，才能保证团队既有发展空间，又有托底能力。&lt;/p&gt;
&lt;h2 id="上限与下限"&gt;上限与下限
&lt;/h2&gt;&lt;p&gt;我天天在听创始人在吹嘘产品的前景，就像天天打冰美式鸡血，有点疲了。&lt;/p&gt;
&lt;p&gt;“包装和吹嘘之间，是不是应该有个分界线？“我举起左手然后是右手，表示这两个概念。&lt;/p&gt;
&lt;p&gt;“创始人对外讲的时候，为了拉投资就必须吹啊。投资人看的都是发展前景，不吹大点怎么能叫前景呢。“&lt;/p&gt;
&lt;p&gt;“但是也不能一直吹，不落地吧。“&lt;/p&gt;
&lt;p&gt;“你说得对，所以团队里还应该有一个人能托底。“他用右手自下而上做了一个托举的手势。&lt;/p&gt;
&lt;p&gt;“托底······“&lt;/p&gt;
&lt;p&gt;面上桌了，居然只上了一碗。“怎么是一碗一碗做的啊？“&lt;/p&gt;
&lt;p&gt;“他这后厨就两个灶坑，肯定做不快。“&lt;/p&gt;
&lt;p&gt;透过玻璃确实能看到只有两名厨子在翻炒。&lt;/p&gt;
&lt;p&gt;我们背后传过来一个声音：“还有没有座，要不要排队啊？“&lt;/p&gt;
&lt;p&gt;面前的服务员皱着眉头，伸出手指着门口说：“你问下那个服务员。“&lt;/p&gt;
&lt;p&gt;我们收回目光，继续聊。&lt;/p&gt;
&lt;p&gt;“从装修和菜品等等这些地方能看出来，这家店的老板审美是很好的。但我大胆猜测一下，这位老板可能缺乏创业经验。“&lt;/p&gt;
&lt;p&gt;“你是说这可能是他第一次做餐饮品牌？为什么这么说？“我抛出这个问题，还没意识到答案就藏在刚才的场景里。&lt;/p&gt;
&lt;p&gt;“别看他们现在刚开业，红红火火的。但是他们的服务员体系不成熟，刚才客人问要不要排队，这位服务员自己不知道，居然还要问其他服务员。还有就是出餐时间超过了15分钟。这两点就很难保证未来持续有健康的客流量。“&lt;/p&gt;
&lt;p&gt;魔鬼细节啊！初出茅庐的老板很难做到面面俱到，往往没意识到基本的业务不能低于及格线。&lt;/p&gt;
&lt;p&gt;“确实，如果我是一个人来的话，等这么久早都没耐心了，可能以后就不来了。“&lt;/p&gt;
&lt;p&gt;“这就是下限，要有人能保证培训的服务员是成熟的，保证15分钟以内上菜，而且是香喷喷的，这样才能达到及格线。毕竟客户的耐心非常有限。“&lt;/p&gt;
&lt;p&gt;“审美能影响一家店的上限，而服务员体系、出餐速度能决定它的下限。那么对于互联网或者AI公司来说，上限是吹出去的故事，下限是什么呢？我想应该是能保证项目拆分到各个阶段的具体任务，每个任务谁来负责，每个阶段交付什么。“&lt;/p&gt;
&lt;p&gt;“你说得对，就是要对每个节点有把控能力。这是工程化能力。“&lt;/p&gt;
&lt;p&gt;面上齐了，这面条好像是用了特殊的酱料炒过，吃起来香喷喷的。&lt;/p&gt;
&lt;p&gt;吃面！&lt;/p&gt;
&lt;h2 id="第一产业"&gt;第一产业
&lt;/h2&gt;&lt;p&gt;第一产业其实是最赚钱的，不仅是农业，还有林业、畜牧业、渔业。&lt;/p&gt;
&lt;p&gt;因为只要给饲料、养料就能长大，投入产出比是很高的。就看愿不愿意脱下长衫。&lt;/p&gt;
&lt;p&gt;我姐在老家小县城里，专门把猫咪卖到大城市里的打工人。已经起飞了，从繁育到猫咖，线上线下联动，还开了宠物摄影业务。&lt;/p&gt;
&lt;p&gt;她22年毕业，很早就开始养猫，然后在老家一边备考一边给宠物店打工帮忙。后来店长不干了，她把摊子接过来，引进了优质品种，逐渐把生意盘活了。&lt;/p&gt;
&lt;p&gt;不但自己有收入，还带上了她妈妈一起干。还在本地找了个会摄影的小哥，拉上大学闺蜜在苏州开了分店。养活了四口人呢。&lt;/p&gt;
&lt;p&gt;我想这和她喜欢小动物的天性也是分不开的。&lt;/p&gt;
&lt;p&gt;要是我也有一门小生意该多好啊！&lt;/p&gt;
&lt;h2 id="a-出来b-就死了我还活得好好的"&gt;A 出来，B 就死了？我还活得好好的！
&lt;/h2&gt;&lt;p&gt;AI 时代有自己的震惊体。&lt;/p&gt;
&lt;p&gt;Gemini 3 横空出世，前端已死！&lt;/p&gt;
&lt;p&gt;看多了真是腻了，祛魅了。&lt;/p&gt;</description></item><item><title>Entrepreneur Review 创业复盘</title><link>https://orangon.github.io/posts/2026-01-26-entrepreneur/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2026-01-26-entrepreneur/</guid><description>&lt;h1 id="entrepreneur-review-创业复盘"&gt;Entrepreneur Review 创业复盘
&lt;/h1&gt;&lt;p&gt;创业是什么？我在研究生选修课上学习到：创业是将知识变现。但是通过参与一次&lt;em&gt;尚未成功&lt;/em&gt;的创业实践，我对创业的理解更加深刻，现在我认为&lt;strong&gt;创业是利用极其有限的资源，将知识快速变现的游戏&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;资源包括人才、启动资金、技术、人脉、设备&lt;/strong&gt;。在 AI 创业领域，人才既包括技术人员（如程序员），也包括管理人员（如项目经理），甚至是顾问（如法律顾问）。启动资金可以是自筹，也可以来自投资（如天使投资人）或贷款。技术指的是生产产品需要的技能（如编码技能）。人脉指生意合作伙伴、校友、亲友，是可以拉来投资、人才、渠道等的贵人。设备就是生产产品的设施或工具，最常见的就是服务器、个人电脑、显示器、键鼠等等，当然现在的 coding agent 也算。&lt;strong&gt;在创业早期，这些资源往往都是极其有限的，可能只有两三个核心员工，几万块钱启动资金，稀少的人脉，以及几台配置一般的笔记本电脑。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而知识变现指的就是利用已有知识生产产品，然后通过营销渠道卖出去，获得一定的利润&lt;/strong&gt;。这里的知识主要来自于书籍、论文、博客，之前知识被高等学校、图书馆等垄断，现在互联网和 AI 的爆炸发展也带来了知识的普及，旧时王谢堂前燕，飞入寻常百姓家。而营销渠道相比 30 年前互联网刚兴起的时候已经大为不同。那时候把自己的网页挂到互联网上，大家就能通过搜索引擎看到你的网页，随之看到你的产品。所以流量是浏览器带来的，联系客户的方式通常是邮箱。但现在，流量是推荐算法带来的，联系客户的方式则变成了社交平台、通讯软件。获得利润的方式除了买断，也发展出了打赏、平台分成、广告费用、以及会员订阅费用等多种方式，真是百花齐放。之所以我还加了一个“快速”限定词，是因为&lt;strong&gt;现在的市场变化很快，研发周期越来越短，科技产品层出不穷，你如果不够快，就只能被同行干掉，或者只能不断追求差异化而推迟发布产品。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;尤其要注意的是，正因为初创公司规模很小，资源非常有限，所以更应该追求快速的变现，快速的回本&lt;/strong&gt;。大公司资源相对充裕，可以游刃有余地进行知识变现，具体来说就是前期投入大量的人力物力，用很长的时间研发早期不追求利润的产品，而用其他产品的利润或者投资来补贴。当然，如果你有天降贵人拉来大量的投资，你也可以不追求快速变现。我主要是想说，&lt;strong&gt;什么样的体量干什么样的事&lt;/strong&gt;，3个人的团队和 300 人的团队很难一直保持在同一起跑线上。如果老板说只用 3 个人就可以干掉 DeepSeek，那他该吃点溜溜梅了，他没事吧？&lt;/p&gt;
&lt;h2 id="我的角色"&gt;我的角色
&lt;/h2&gt;&lt;p&gt;下面按照时间顺序梳理一下我在团队中担任过的角色。&lt;/p&gt;
&lt;h3 id="前端小兵"&gt;前端小兵
&lt;/h3&gt;&lt;p&gt;我最开始只会React，由于业务需要，接触了 Vue 和 Uniapp。现在，常见的组件库（Tdesign、Ant design、ChatUI）和全栈框架（Next.js ）我都熟悉了。我先后维护着许多团队的前端项目，比如简历优化小程序、智能体记忆云平台。&lt;/p&gt;
&lt;p&gt;后来 CEO 拿到了投资，开始激进地招募实习生，他认为每一个实习生配一个 Cursor，其产出效率就相当于一个高级工程师。“反正有了 AI ，活儿都很简单了，谁干不是干呢，嗯哼？”&lt;/p&gt;
&lt;h3 id="前端组长"&gt;前端组长
&lt;/h3&gt;&lt;p&gt;公司来了有十几名后端实习生和 3 名前端实习生。CEO 每天的任务就是 10 点钟起来给这些实习生布置任务，相当于要给 20 个智能体下任务指令。但是他验收不过来，而且到后来，实习生常常因为没有任务做而无法上岗。于是开始让小组长来拆任务和验收任务。&lt;/p&gt;
&lt;p&gt;我代替 CEO 给 3 名前端实习生拆任务，他们是小黄、小姜和小鹏。我们共同用 uniapp 开发小程序。一开始我还是干劲十足的，因为得到了管理别人的职能。但是整个工作真的特别累，因为我成了一个保姆，每天和三个人沟通非常繁琐，沟通成本太高了，我也受不了。于是我把 Uniapp 小程序项目给项目经理掌握进度，我只负责审阅代码、合并发布。小黄是大三学生，干劲很足，但是干了2个月国庆节就到期离职了。小姜是本科毕业回到老家，线上接活维持生计，性格比较闷，不会主动找人协助，因为家庭原因暂时退出过，后来又继续做前端。小鹏是计算机专业研二，性格一开始比较闷，后来变得主动了些。他由前端转全栈干了挺长一段时间，到了寒假也离职了。&lt;/p&gt;
&lt;p&gt;随着业务调整，简历小程序被逐渐放弃，我转向维护一个智能体记忆云平台的控制台、开发文档、落地页的单仓项目（Monorepo）。&lt;/p&gt;
&lt;h3 id="mcp-服务维护者"&gt;MCP 服务维护者
&lt;/h3&gt;&lt;p&gt;我负责把写好的记忆云服务包裹成 LLM 可以直接调用的服务，这是一种新的产品形态，直接面向开发者。可以发布到各大开发平台的 MCP 市场，用户通过订阅获得密钥，然后开发者就可以持续收费，躺着挣钱。&lt;/p&gt;
&lt;h2 id="我得到的教训"&gt;我得到的教训
&lt;/h2&gt;&lt;h3 id="技术"&gt;技术
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;一、谨慎使用 Uniapp 开发&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;尽管 Uniapp 声称通过编译可以实现“一次开发，到处运行”，但实际开发过程中发现，用它写的小程序体量一大就变得难以维护。&lt;/p&gt;
&lt;p&gt;这主要是因为 H5、微信开发者工具、各个手机客户端都有不同的渲染逻辑，注意，安卓、iOS、鸿蒙也有不同的渲染逻辑！这意味着任何一个交互程度略复杂的组件，都要花大量时间精力来打磨，来适配不同终端。而当组件出现意料之外的行为，也很难判断和底层渲染逻辑有什么关系，只能靠 AI 一遍一遍抽卡修复。&lt;/p&gt;
&lt;p&gt;因此我明白为什么 Uniapp 的组件生态很少，而且都很简单。怪不得以前听说 Uniapp 只能用来做玩具。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;二、人必须终身学习&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了不被日渐强大的 AI 牵着鼻子走，就必须向先进的 AI 学习，不然项目埋了雷都不知道。因为现在的 coding 模型经常会给出看似正确的代码，但是经不起细看，可能不符合全局逻辑；修复 bug 方面，经常给出临时性的补丁而忽略了长期维护，造成了技术债务。&lt;/p&gt;
&lt;h3 id="管理"&gt;管理
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;一、进度管理永远是一件难事&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;假如你是给工作项/需求排期的人，那么你不但要知道研发的水平，而且还要了解需求的复杂程度&lt;/strong&gt;。这样才能给合适的人安排合适的需求，保证迭代/项目按时交付。如果给实习生安排了过难的任务，可能会打击积极性，而且拖慢任务进度；反过来，假如给一个熟练的研发安排了一些初级需求，可能他很快就做完了，然后就没有事情做。&lt;/p&gt;
&lt;p&gt;（1）了解研发。你要知道，研发的基础能力除了编码能力，还包括对需求的理解能力、沟通对齐的能力。也就是说，拿到一个需求描述之后，不论是口头的还是书面的，研发是否能迅速拆解成清晰的步骤，是否能对模糊的部分提出疑问，并且主动去找人对接。对技术的掌握倒是其次，因为 AI 的能力越来越强，架子搭好了很快就能实现。&lt;/p&gt;
&lt;p&gt;麻烦就麻烦在，基础能力是会变化的。研发可以从过往实现中学习成长，从而越来越熟练，效率越来越高；但也可能因为没有及时积淀，导致能力滑坡。你要及时跟进研发的能力状态。对研发更高的要求是，用优雅的方法而非复杂的方法实现需求。&lt;/p&gt;
&lt;p&gt;（2）了解需求。需求的复杂度倒不会因为熟练程度而变化，但需求有合理与否的区别。这是因为资源并非无限，你需要判断在时间和研发力量有限的情况下，到底哪些需求值得当下去做。&lt;/p&gt;
&lt;p&gt;要使用何种方式来判断，来排序？这就是你要精进的技能点。&lt;/p&gt;
&lt;p&gt;很遗憾，我不认为现在团队里有人点出了进度管理的高级技能点。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;二、职责太多不是好事&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;进度管理是一种职责，其实谁都可以来承担，当有专人做进度管理时，这就成了一种职位：项目经理。&lt;/p&gt;
&lt;p&gt;但初创公司的资源非常有限，大家都希望尽量用更少的人做更多的事，于是大家通常都是多面手，也就是一个人承担多个角色的职责。比如项目经理同时是研发人员，或者产品经理同时是市场营销， CEO 还可以兼任财务。&lt;/p&gt;
&lt;p&gt;问题在于，如果一个人身上集中了太多的职责，比如 CEO 兼任了所有的高管职位，那会带来灾难的。&lt;/p&gt;
&lt;p&gt;拿我自己来说，假如我担任了过多职责，首先我的作息会无法保证。我必须牺牲部分睡眠时间才能处理这么多事务，这会导致作息混乱，影响我的身心健康。比如心脏、肝脏受损，甚至是鼻炎。长期下去，我的活力会慢慢丧失，身体机能下降，内分泌失调。例如熬灯夜战时，皮质醇持续分泌，导致我误以为夜里效率高。&lt;/p&gt;
&lt;p&gt;同时，我每天还要和小伙伴对接，于是团队沟通就会变得非常低效，因为别人不知道我什么时候在线。如果我凌晨3点才睡，下午下午1点才醒，而其他人作息是正常的。那么我收不到上午的消息，只能下午回复消息——我们失去了半天的沟通窗口！&lt;/p&gt;
&lt;p&gt;更麻烦的是，我也不能保证每天作息的稳定，因为有时候我要在白天见客户，下午补觉。而且我也不能要求别人的作息和我一样，这不现实。我只能在约定的会议时间内尽量上线，同时一有空就处理工作消息（异步）。&lt;/p&gt;
&lt;p&gt;这时候我要怎么办呢？难道要每天都把自己的时间窗口列出来，告诉团队小伙伴？问题是困意袭来的时候我根本控制不住自己的眼皮。还有一个办法就是放权，但是我放权之后只是减少了沟通次数——我仍然需要和组长沟通呀！&lt;/p&gt;
&lt;p&gt;还有，我不可能在所有领域都有很高的决策水平，就算我是创始人，我也不可能面面俱到（否则就不需要团队了）。&lt;/p&gt;
&lt;p&gt;当我活力逐渐丧失，每天要和海量的人打交道，再加上和 AI 聊天久了，我会认为一两句话描述需求和下判断是 ok 的！&lt;/p&gt;
&lt;p&gt;到最后，我难以做出任何长远决策，哪怕是下一周该干什么，都以“没有时间“来逃避。&lt;/p&gt;
&lt;h3 id="人才"&gt;人才
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;宁要一个诸葛亮，也不要三个臭皮匠&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;刘关张也相当于早期创业团队，他们当时也不会想找三个臭皮匠来做军师吧。&lt;/p&gt;
&lt;p&gt;而且人越多，沟通效率其实越低，沟通成本越高。&lt;/p&gt;
&lt;h3 id="情怀"&gt;情怀
&lt;/h3&gt;&lt;p&gt;情怀可以打动一部分人，但不能打动所有人，这是因为性格不同。&lt;/p&gt;
&lt;h3 id="人脉"&gt;人脉
&lt;/h3&gt;&lt;p&gt;更好的学校，意味着更好的平台和更牛逼的校友。我要是能早点明白这个道理，也许我高中或者大学期间会更努力一些。&lt;/p&gt;
&lt;h3 id="设备"&gt;设备
&lt;/h3&gt;&lt;p&gt;苹果公司的 Mac 系列产品在 AI 时代获得了巨大成功，这是因为它的定位是面向创意性工作的，所以它的生态位和 Windows 不同，现在的生成式 AI 恰恰是解放了之前由于技术门槛过高而不能实现想法的一类人。如果这类人拥有一台 MacBook，那真的是如虎添翼。别的不说，Mac 的类 Linux 环境使得许多命令可以顺畅执行，而 Windows 就麻烦得多。而且 Mac 运行 Docker 也更丝滑，不需要安装 WSL。&lt;/p&gt;
&lt;p&gt;可恶的是，内存涨价带动了一波苹果产品全线涨价，换电脑真是肉疼。&lt;/p&gt;
&lt;h2 id="我得到的经验"&gt;我得到的经验
&lt;/h2&gt;&lt;h3 id="主动出击推销自己"&gt;主动出击，推销自己
&lt;/h3&gt;&lt;p&gt;恰恰因为一个人做不成大生意，所以有无数的团队渴求人才。而每个人的简历上肯定都有一两个闪光点，遇到气味相投的团队时，把最好的一面拿出来，相信自己！&lt;/p&gt;
&lt;h3 id="识别人才善用人才"&gt;识别人才，善用人才
&lt;/h3&gt;&lt;p&gt;把团队成员看成一种资源，不同的人有不同的性格和能力，需要有不同的利用方式。&lt;/p&gt;
&lt;h3 id="把团队的变成自己的"&gt;把团队的变成自己的
&lt;/h3&gt;&lt;p&gt;假如你已经有团队了，一定一定要把团队的能力沉淀、转化为自己的能力。无论是写文档、写代码，还是沟通协作。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结
&lt;/h2&gt;&lt;p&gt;我在技术方面已经走得够远了，是时候把技术变现了。&lt;/p&gt;
&lt;p&gt;当我认为我不能从团队中学到更多时，我就可以抽身了。&lt;/p&gt;
&lt;p&gt;更新于2026年7月10日。&lt;/p&gt;</description></item><item><title>为 Windows 右键添加 VS Code 快捷打开方式</title><link>https://orangon.github.io/posts/2025-10-19-windows-right-click/</link><pubDate>Sun, 19 Oct 2025 00:00:00 +0000</pubDate><guid>https://orangon.github.io/posts/2025-10-19-windows-right-click/</guid><description>&lt;h1 id="为-windows-右键添加-vs-code-快捷打开方式"&gt;为 Windows 右键添加 VS Code 快捷打开方式
&lt;/h1&gt;&lt;p&gt;本文介绍如何在 Windows 右键菜单中添加使用 VS Code 快速打开文件和文件夹的方法。&lt;/p&gt;
&lt;p&gt;共可添加三种右键菜单功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;右键点击文件夹&lt;/strong&gt;：在文件夹上右键，选择&amp;quot;通过VS Code打开&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;右键点击文件&lt;/strong&gt;：在单个文件上右键，选择&amp;quot;通过VS Code打开&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;右键点击空白处&lt;/strong&gt;：在文件夹内空白处右键，选择&amp;quot;通过VS Code打开当前文件夹&amp;quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;方法有两种，直接重装 VS Code 并勾选安装选项，或者通过注册表手动添加。&lt;/p&gt;
&lt;h2 id="方法一重新安装-vs-code推荐"&gt;方法一：重新安装 VS Code（推荐）
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;下载最新的 VS Code 安装包&lt;/li&gt;
&lt;li&gt;运行安装程序时，确保勾选&amp;quot;&lt;strong&gt;将&amp;rsquo;通过Code 打开&amp;rsquo;操作添加到Windows资源管理器上下文菜单&lt;/strong&gt;&amp;ldquo;选项&lt;/li&gt;
&lt;li&gt;完成安装后即可在右键菜单中看到 VS Code 选项&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="方法二修改注册表"&gt;方法二：修改注册表
&lt;/h2&gt;&lt;p&gt;如果你不想重新安装，可以通过修改注册表，分为手动添加和使用注册表文件：&lt;/p&gt;
&lt;h3 id="手动操作"&gt;手动操作：
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;打开注册表编辑器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按 &lt;code&gt;Win + R&lt;/code&gt; 键，输入 &lt;code&gt;regedit&lt;/code&gt;，回车&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为文件夹添加右键菜单&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导航到：&lt;code&gt;HKEY_CLASSES_ROOT\Directory\shell&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;shell&lt;/code&gt; 上右键 → 新建 → 项，命名为 &lt;code&gt;VSCode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在右侧窗口双击&amp;quot;默认&amp;rdquo;，输入显示的名称，如&amp;quot;通过VS Code打开&amp;quot;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;VSCode&lt;/code&gt; 上右键 → 新建 → 项，命名为 &lt;code&gt;command&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在右侧窗口双击&amp;quot;默认&amp;quot;，输入 VS Code 的安装路径：
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;C:\Users\你的用户名\AppData\Local\Programs\Microsoft VS Code\Code.exe&amp;#34; -n &amp;#34;%V&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;C:\Users\你的用户名\AppData\Local\Programs\Microsoft VS Code\Code.exe&amp;#34; -r &amp;#34;%V&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为文件添加右键菜单&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导航到：&lt;code&gt;HKEY_CLASSES_ROOT\*\shell&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重复上述步骤 2 的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为文件夹空白处添加右键菜单&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导航到：&lt;code&gt;HKEY_CLASSES_ROOT\Directory\Background\shell&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重复上述步骤 2 的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;-n 是使用新窗囗打开，-r 是使用当前窗口打开&lt;/li&gt;
&lt;li&gt;更改了不需要重启电脑&lt;/li&gt;
&lt;li&gt;注意大小写，双引号（英文双引号），句号（英文）空格，路径（自身电脑 vscode 打开路径）&lt;/li&gt;
&lt;li&gt;OpenWithVSCode 是右键显示的名称，可自行更改， command 是启动命令，不加会无法执行， Icon 是图标，也可自行更改&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;常见 VS Code 安装路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C:\Users\用户名\AppData\Local\Programs\Microsoft VS Code\Code.exe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C:\Program Files\Microsoft VS Code\Code.exe&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="使用注册表文件"&gt;使用注册表文件：
&lt;/h3&gt;&lt;p&gt;创建一个 &lt;code&gt;.reg&lt;/code&gt; 文件，内容如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-reg" data-lang="reg"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Windows Registry Editor Version 5.00
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;; 右键文件夹用VS Code打开&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;HKEY_CLASSES_ROOT&lt;/span&gt;&lt;span class="k"&gt;\Directory\shell\VSCode]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;@&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;通过VS Code打开&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;&amp;#34;Icon&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;C:\\Users\\你的用户名\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;HKEY_CLASSES_ROOT&lt;/span&gt;&lt;span class="k"&gt;\Directory\shell\VSCode\command]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;@&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\&amp;#34;C:\\Users\\你的用户名\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe\&amp;#34; \&amp;#34;%V\&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;; 右键文件用VS Code打开&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;HKEY_CLASSES_ROOT&lt;/span&gt;&lt;span class="k"&gt;\*\shell\VSCode]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;@&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;通过VS Code打开&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;&amp;#34;Icon&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;C:\\Users\\你的用户名\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;HKEY_CLASSES_ROOT&lt;/span&gt;&lt;span class="k"&gt;\*\shell\VSCode\command]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;@&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\&amp;#34;C:\\Users\\你的用户名\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe\&amp;#34; \&amp;#34;%V\&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;; 右键文件夹空白处用VS Code打开当前文件夹&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;HKEY_CLASSES_ROOT&lt;/span&gt;&lt;span class="k"&gt;\Directory\Background\shell\VSCode]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;@&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;通过VS Code打开当前文件夹&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;&amp;#34;Icon&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;C:\\Users\\你的用户名\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;HKEY_CLASSES_ROOT&lt;/span&gt;&lt;span class="k"&gt;\Directory\Background\shell\VSCode\command]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;@&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\&amp;#34;C:\\Users\\你的用户名\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe\&amp;#34; \&amp;#34;%V\&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;将文件保存为 &lt;code&gt;.reg&lt;/code&gt; 格式，双击运行即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：请根据你的实际 VS Code 安装路径修改上述路径中的用户名部分。&lt;/p&gt;
&lt;h2 id="windows-11-右键菜单配置"&gt;Windows 11 右键菜单配置
&lt;/h2&gt;&lt;p&gt;在 Windows 11 中，右键菜单分为两种模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;现代模式&lt;/strong&gt;：右键显示简化菜单，需要点击&amp;quot;显示更多选项&amp;quot;才能看到完整菜单&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经典模式&lt;/strong&gt;：直接显示完整的传统右键菜单&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用传统的注册表方法无法直接添加到现代菜单中，导致 VS Code 选项只出现在&amp;quot;显示更多选项&amp;quot;中。对此，不想折腾的话可以直接使用 Shift + 右键 打开完整菜单，或者一劳永逸地切换到经典模式。最后，还有一种待验证的方法是使用 PowerToys 工具来管理现代菜单。&lt;/p&gt;
&lt;h3 id="切换到经典模式"&gt;切换到经典模式
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;打开注册表编辑器&lt;/li&gt;
&lt;li&gt;导航到：&lt;code&gt;HKEY_CURRENT_USER\Software\Classes\CLSID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新建项：&lt;code&gt;{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在该项下新建项：&lt;code&gt;InprocServer32&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改&amp;quot;默认&amp;quot;值为空字符串即可&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="使用-powertoys-管理现代菜单"&gt;使用 PowerToys 管理现代菜单
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;PowerToys&lt;/strong&gt;（微软官方工具）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 GitHub 或 Microsoft Store 安装 PowerToys&lt;/li&gt;
&lt;li&gt;在设置中寻找右键菜单管理功能&lt;/li&gt;
&lt;li&gt;添加 VS Code 到现代菜单&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>