[{"data":1,"prerenderedAt":12868},["ShallowReactive",2],{"seo-locale-availability:\u002Fblog\u002Flucky-stun-cloudflare-tunnel-guide":3,"post:zh-CN:\u002Fblog\u002Flucky-stun-cloudflare-tunnel-guide":7,"surroundings:zh-CN":1708},[4,5,6],"zh-CN","en","ja",{"post":8,"contentLocale":4,"isFallback":1692},{"id":9,"title":10,"accent":11,"body":12,"description":1691,"draft":1692,"experience":1693,"extension":1694,"featured":1692,"image":1693,"meta":1695,"navigation":1696,"path":1697,"publishedAt":1698,"readingTime":1699,"seo":1700,"stem":1701,"tags":1702,"updatedAt":1693,"video":1693,"__hash__":1707},"posts_zh\u002Fblog\u002Flucky-stun-cloudflare-tunnel-guide.md","Lucky STUN + Cloudflare 302 实战：用域名跟随动态公网端口","violet",{"type":13,"value":14,"toc":1661},"minimark",[15,28,39,50,63,67,77,84,151,155,160,170,173,189,193,196,202,211,218,224,227,231,234,240,245,346,349,355,362,366,369,372,390,394,397,414,417,421,432,436,513,533,536,540,543,585,591,594,600,606,610,612,618,621,627,640,644,647,653,656,674,678,685,691,694,869,876,882,885,908,917,920,926,929,933,936,967,970,987,990,1009,1019,1023,1046,1049,1053,1066,1070,1073,1079,1082,1088,1093,1099,1118,1122,1124,1130,1133,1139,1144,1212,1215,1226,1229,1233,1236,1242,1257,1268,1271,1306,1310,1313,1319,1326,1329,1333,1336,1342,1350,1353,1356,1491,1494,1497,1500,1526,1538,1541,1547,1550,1553,1556,1559,1562,1657],[16,17,18,19],"p",{},"这篇文章介绍的重点不是普通 DDNS，也不是让 Cloudflare 反向代理 STUN 的动态端口，而是下面这条组合链路：",[20,21,22,23,27],"strong",{},"Lucky 通过 STUN 获得 ",[24,25,26],"code",{},"公网 IP:动态端口","，映射改变时再由 Webhook 修改 Cloudflare Single Redirect 的目标地址。",[16,29,30,31,34,35,38],{},"用户只需要记住 ",[24,32,33],{},"go.example.com","。浏览器先访问 Cloudflare，Cloudflare 返回一次临时重定向，再由浏览器直接连接最新的 STUN 地址。这与参考文章的 301 方案是同一种思路，但本文把状态码改为 ",[20,36,37],{},"302","：NAT 映射本来就是临时值，不应该让浏览器或搜索引擎把某个 IP 和端口当作永久地址。",[16,40,41,42,45,46,49],{},"本文基于 Lucky 2.27.2 与 Cloudflare Dashboard 的实际界面，并用双方官方文档校正能力边界。正文示例只使用 ",[24,43,44],{},"example.com","、",[24,47,48],{},"192.0.2.0\u002F24"," 等文档值；实拍配图中的真实域名、IP、端口、实例名称、流量和 Token 均未展示，存在后台数据的区域已做不可逆强模糊处理。",[51,52,53],"blockquote",{},[16,54,55,56,58,59,62],{},"先给结论：浏览器访问的临时入口使用 ",[20,57,37],{},"；不要使用 301。Cloudflare Single Redirect 当前正式支持 301、302、307、308，",[20,60,61],{},"不支持 303","。如果是 App、API、远程桌面或非 HTTP 协议，这种跳转通常不合适，应改用 307、Cloudflare Tunnel、VPN 或其他原生传输方案。",[64,65,66],"h2",{"id":66},"先看清这条链路",[68,69,75],"pre",{"className":70,"code":72,"language":73,"meta":74},[71],"language-text","第一次请求\n\n浏览器访问 https:\u002F\u002Fgo.example.com\u002Fphotos?id=42\n  └─ Cloudflare Single Redirect 返回 302\n       Location: http:\u002F\u002F203.0.113.10:45678\u002Fphotos?id=42\n        └─ 浏览器直接连接 STUN 映射\n             └─ Lucky 内置转发（或路由器端口转发）\n                  └─ 内网 Web 服务\n\n映射变化\n\nLucky 得到 203.0.113.10:51234\n  └─ STUN Webhook PATCH Cloudflare Rulesets API\n       └─ 下一次访问收到新的 Location\n","text","",[24,76,72],{"__ignoreMap":74},[16,78,79,80,83],{},"这里的“纯域名访问”只表示",[20,81,82],{},"用户输入的起始地址不带端口","。跳转完成后，浏览器地址栏仍会显示真实 IP 或目标域名及端口。Cloudflare 只处理第一次请求，后续业务流量不会经过 Cloudflare，因此 WAF、缓存、Access、隐藏源站等能力不会覆盖第二段连接。",[85,86,87,106],"table",{},[88,89,90],"thead",{},[91,92,93,97,100,103],"tr",{},[94,95,96],"th",{},"方案",[94,98,99],{},"用户入口",[94,101,102],{},"第二段流量",[94,104,105],{},"适合场景",[107,108,109,124,137],"tbody",{},[91,110,111,115,118,121],{},[112,113,114],"td",{},"STUN 直连",[112,116,117],{},"IP\u002F域名加动态端口",[112,119,120],{},"访问者直连家庭网络",[112,122,123],{},"TCP\u002FUDP 或明确知道端口的客户端",[91,125,126,129,132,134],{},[112,127,128],{},"STUN + Cloudflare 302",[112,130,131],{},"固定 HTTPS 域名，随后显示动态地址",[112,133,120],{},[112,135,136],{},"浏览器 GET 页面、临时入口、能接受暴露地址",[91,138,139,142,145,148],{},[112,140,141],{},"Cloudflare Tunnel",[112,143,144],{},"固定 HTTPS 域名",[112,146,147],{},"经 Cloudflare Tunnel",[112,149,150],{},"Web、API、管理后台、需要 Access\u002FWAF 的服务",[64,152,154],{"id":153},"第一条路径配置-lucky-stun","第一条路径：配置 Lucky STUN",[156,157,159],"h3",{"id":158},"_1-先判断网络条件","1. 先判断网络条件",[16,161,162,169],{},[163,164,168],"a",{"href":165,"rel":166},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fstun",[167],"nofollow","Lucky 的 STUN 官方文档","把适用环境描述为 NAT1，并明确提醒公网端口无法保证固定、变化没有规律。不同工具对 NAT 类型的命名可能不完全一致，所以不要只凭路由器页面上的“NAT 类型”文字判断，应以实际穿透日志和外网测试为准。",[16,171,172],{},"开始前依次检查：",[174,175,176,180,183,186],"ol",{},[177,178,179],"li",{},"如果是光猫拨号，按 Lucky 文档把光猫 DMZ 指向主路由；如果是路由器拨号，则跳过这一步。",[177,181,182],{},"最省事的部署位置是主路由。Lucky 位于普通局域网主机时，还要处理主路由到 Lucky 的端口映射、UPnP 或 DMZ。",[177,184,185],{},"确认系统内核和防火墙允许所需的 TCP\u002FUDP 行为；代理软件、双重 NAT 和多 WAN 都可能改变结果。",[177,187,188],{},"不要把 Lucky 管理后台本身作为第一条公网测试规则。先选择一个无敏感数据、可以随时关闭的测试服务。",[156,190,192],{"id":191},"_2-开启-stun-模块","2. 开启 STUN 模块",[16,194,195],{},"进入：",[68,197,200],{"className":198,"code":199,"language":73,"meta":74},[71],"Lucky → 内网穿透 → STUN 内网穿透 → 设置\n",[24,201,199],{"__ignoreMap":74},[16,203,204,205,210],{},"打开 STUN 模块，并检查“全局 STUN 服务器列表”。Lucky 界面会给出一个公开的",[163,206,209],{"href":207,"rel":208},"https:\u002F\u002Fgithub.com\u002Fpradt2\u002Falways-online-stun\u002Fblob\u002Fmaster\u002Fvalid_hosts_tcp.txt",[167],"可用 STUN 服务器列表","作为参考。服务器越多并不一定越稳定；应保留网络可达、经过测试的条目，不要把来源不明的地址批量粘贴进生产配置。",[16,212,213],{},[214,215],"img",{"alt":216,"src":217},"Lucky 2.27.2 的 STUN 设置页，展示模块开关、公开服务器列表与 Webhook 开关","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-stun-settings-v1.png",[16,219,220],{},[221,222,223],"em",{},"实拍界面；画面只包含公开 STUN 服务器地址，没有私有网络地址或访问凭据。",[16,225,226],{},"Webhook 会把公网映射变化发送给外部系统。只有确实需要自动化时才启用，并确保 URL、请求头和消息体中的密钥不会出现在日志或截图里。",[156,228,230],{"id":229},"_3-用简易模式添加第一条规则","3. 用简易模式添加第一条规则",[16,232,233],{},"进入“穿透规则列表”并选择“添加穿透规则”。第一次测试建议使用简易模式。",[16,235,236],{},[214,237],{"alt":238,"src":239},"Lucky 的添加穿透规则表单，表单外的真实后台数据已被强模糊","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-stun-rule-dialog-redacted-v1.png",[16,241,242],{},[221,243,244],{},"表单保持空白；背景中的真实规则、IP、端口和流量统计已做不可逆强模糊。",[85,246,247,257],{},[88,248,249],{},[91,250,251,254],{},[94,252,253],{},"字段",[94,255,256],{},"建议",[107,258,259,267,275,291,299,307,315,323,338],{},[91,260,261,264],{},[112,262,263],{},"规则名称",[112,265,266],{},"使用不暴露设备型号、位置或业务名称的内部备注",[91,268,269,272],{},[112,270,271],{},"穿透类型",[112,273,274],{},"根据服务选择 IPv4-TCP 或 IPv4-UDP；一条规则只处理一种协议",[91,276,277,280],{},[112,278,279],{},"穿透通道本地端口",[112,281,282,283,286,287,290],{},"手动填写时使用未占用且全局唯一的端口；Lucky 文档建议大于 ",[24,284,285],{},"10240","。配合 UPnP 时可按文档使用 ",[24,288,289],{},"0"," 让 Lucky 选择端口",[91,292,293,296],{},[112,294,295],{},"防火墙自动放行",[112,297,298],{},"仅在 Lucky 运行于支持其防火墙控制的路由器、且你理解规则变化时开启",[91,300,301,304],{},[112,302,303],{},"UPnP \u002F NAT-PMP",[112,305,306],{},"Lucky 位于局域网主机且主路由已启用相应能力时使用；不用时关闭，避免扩大自动映射范围",[91,308,309,312],{},[112,310,311],{},"不使用 Lucky 内置端口转发",[112,313,314],{},"第一次测试保持关闭。确认穿透有效后，再评估用路由器转发换取更高性能",[91,316,317,320],{},[112,318,319],{},"禁用通道有效性检测",[112,321,322],{},"保持关闭，它是调试选项",[91,324,325,328],{},[112,326,327],{},"目标地址",[112,329,330,331,334,335],{},"填内网 IP 或主机名，不要带 ",[24,332,333],{},"http:\u002F\u002F"," 或 ",[24,336,337],{},"https:\u002F\u002F",[91,339,340,343],{},[112,341,342],{},"目标端口",[112,344,345],{},"填服务实际监听端口；使用 Lucky 内置转发时必填",[16,347,348],{},"一个完全虚构的 TCP 示例：",[68,350,353],{"className":351,"code":352,"language":73,"meta":74},[71],"穿透类型：IPv4-TCP\n穿透通道本地端口：12080\n目标地址：192.0.2.10\n目标端口：8080\n",[24,354,352],{"__ignoreMap":74},[16,356,357,358,361],{},"这里的 ",[24,359,360],{},"192.0.2.10:8080"," 使用 RFC 5737 文档保留地址，只是教程占位符。发布文章、截图或求助日志前，应把真实内网地址与端口一起打码；二者组合往往足以识别某项服务。",[156,363,365],{"id":364},"_4-定制模式先不要调参优化","4. 定制模式先不要“调参优化”",[16,367,368],{},"Lucky 2.27.2 的定制模式包含指定 IP\u002F网卡、IP 黑白名单、TCP 连接数、UDP 会话数、加密、STUN 等待时间、心跳间隔、重试间隔和日志级别等选项。这些参数互相关联，不应因为看到默认值就照搬到其他网络。",[16,370,371],{},"只有出现可复现问题时才逐项调整：",[373,374,375,378,381,384,387],"ul",{},[177,376,377],{},"多 WAN 或策略路由环境，才指定穿透通道 IP 或网卡；",[177,379,380],{},"连接数与 UDP 会话数根据服务并发和设备资源设上限；",[177,382,383],{},"等待、心跳、重试参数一次只改一项，并保留前后日志；",[177,385,386],{},"“穿透失败自动重试”可以提高可用性，但也要避免错误配置持续制造请求；",[177,388,389],{},"TLS 或流加密不能替代应用自身的认证和授权。",[156,391,393],{"id":392},"_5-用真正的外网验证","5. 用真正的外网验证",[16,395,396],{},"保存后先看该规则的独立日志，再关闭 Wi-Fi，用移动网络或另一条宽带访问。至少验证：",[174,398,399,402,405,408,411],{},[177,400,401],{},"Lucky 是否获得公网映射；",[177,403,404],{},"访问时日志是否出现连接；",[177,406,407],{},"防火墙或路由器是否把流量送到正确的 Lucky 端口；",[177,409,410],{},"内网服务是否监听了正确地址，而不是只监听另一个网络命名空间；",[177,412,413],{},"公网端口变化后，客户端或自动化能否感知新地址。",[16,415,416],{},"如果日志完全没有访问记录，先查运营商 NAT、光猫\u002F路由器映射和防火墙，而不是反复修改目标服务。",[64,418,420],{"id":419},"核心方案用-cloudflare-302-跟随-stun-动态端口","核心方案：用 Cloudflare 302 跟随 STUN 动态端口",[16,422,423,424,427,428,431],{},"STUN 给出的是“公网 IP + 公网端口”。A\u002FAAAA 记录只能保存 IP，不能把普通浏览器 URL 的端口一起动态更新。参考方案巧妙之处在于：DNS 只负责让固定入口到达 Cloudflare，真正的 ",[24,425,426],{},"IP:端口"," 写在重定向规则的 ",[24,429,430],{},"Location"," 里，并由 Lucky Webhook 持续更新。",[156,433,435],{"id":434},"_1-为什么选-302而不是-301-或-303","1. 为什么选 302，而不是 301 或 303",[85,437,438,451],{},[88,439,440],{},[91,441,442,445,448],{},[94,443,444],{},"状态码",[94,446,447],{},"语义与方法行为",[94,449,450],{},"是否适合本文",[107,452,453,467,480,491,502],{},[91,454,455,458,461],{},[112,456,457],{},"301",[112,459,460],{},"永久移动；客户端和搜索引擎可能长期记住目标",[112,462,463,466],{},[20,464,465],{},"不适合","动态 NAT 映射",[91,468,469,471,474],{},[112,470,37],{},[112,472,473],{},"临时移动；常见浏览器适合用它处理 GET 页面",[112,475,476,479],{},[20,477,478],{},"推荐","作为浏览器入口",[91,481,482,485,488],{},[112,483,484],{},"303",[112,486,487],{},"明确要求后续使用 GET，常用于 POST 后查看另一资源",[112,489,490],{},"Single Redirect 当前不可选",[91,492,493,496,499],{},[112,494,495],{},"307",[112,497,498],{},"临时移动，并要求保留原请求方法和请求体",[112,500,501],{},"App\u002FAPI 确需保留方法时评估",[91,503,504,507,510],{},[112,505,506],{},"308",[112,508,509],{},"永久移动并保留方法",[112,511,512],{},"不适合动态 NAT 映射",[16,514,515,516,521,522,525,526,529,530,532],{},"Cloudflare 的 ",[163,517,520],{"href":518,"rel":519},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fsettings\u002F",[167],"Single Redirect 设置文档","当前列出的状态码只有 301、302、307、308。Cloudflare 的 3xx 说明页介绍了 303 的 HTTP 语义，但这不代表 Rulesets API 的 ",[24,523,524],{},"http_request_dynamic_redirect"," 动作接受 303。把下面 JSON 的 ",[24,527,528],{},"status_code"," 改成 ",[24,531,484],{}," 很可能会被 API 校验拒绝。",[16,534,535],{},"如果业务必须返回 303，需要由 Cloudflare Worker 或自己的源站生成该响应；那已经不是本文这套 Single Redirect 配置。对于普通浏览器 GET 页面，302 更直接。对于必须保留 POST\u002FPUT 请求体的 API，优先重新评估架构，而不是盲目把 302 换成 307。",[156,537,539],{"id":538},"_2-准备两个不同角色的域名","2. 准备两个不同角色的域名",[16,541,542],{},"建议把入口和直连目标分开：",[85,544,545,558],{},[88,546,547],{},[91,548,549,552,555],{},[94,550,551],{},"域名",[94,553,554],{},"Cloudflare 状态",[94,556,557],{},"用途",[107,559,560,572],{},[91,561,562,566,569],{},[112,563,564],{},[24,565,33],{},[112,567,568],{},"已代理（橙云）",[112,570,571],{},"用户输入的固定入口，触发 Redirect Rule",[91,573,574,579,582],{},[112,575,576],{},[24,577,578],{},"origin-direct.example.com",[112,580,581],{},"仅 DNS（灰云，可选）",[112,583,584],{},"希望目标仍使用域名和有效 TLS 证书时，由 Lucky DDNS 更新 IP",[16,586,587,588,590],{},"Cloudflare 官方明确要求 Single Redirect 所匹配的入口主机名必须经过代理。因此 ",[24,589,33],{}," 必须是橙云记录。规则命中后 Cloudflare 直接返回 302，不会请求这个记录指向的源站；但未命中的路径可能继续回源，所以生产环境应让规则覆盖完整主机名，并为漏网请求设计安全失败，而不是指向真实管理后台。",[16,592,593],{},"目标可以有两种写法：",[68,595,598],{"className":596,"code":597,"language":73,"meta":74},[71],"最简单： http:\u002F\u002F公网IP:动态端口\u002Fpath\n更适合 HTTPS： https:\u002F\u002Forigin-direct.example.com:动态端口\u002Fpath\n",[24,599,597],{"__ignoreMap":74},[16,601,602,603,605],{},"直接跳到 HTTPS IP 往往会遇到证书主机名不匹配。若需要 HTTPS，推荐让 Lucky DDNS 更新灰云目标域名，并让源站证书包含 ",[24,604,578],{},"。无论采用哪一种，第二段连接都会暴露目标 IP 和端口，也不会得到 Cloudflare WAF 或 Access 保护。",[156,607,609],{"id":608},"_3-在-cloudflare-创建-302-动态重定向","3. 在 Cloudflare 创建 302 动态重定向",[16,611,195],{},[68,613,616],{"className":614,"code":615,"language":73,"meta":74},[71],"Cloudflare Dashboard → 目标 Zone → Rules → Redirect Rules\n→ Create rule → Single Redirects\n",[24,617,615],{"__ignoreMap":74},[16,619,620],{},"先建立一条占位规则：",[68,622,625],{"className":623,"code":624,"language":73,"meta":74},[71],"规则名：Lucky STUN temporary redirect\n匹配表达式：(http.host eq \"go.example.com\")\n类型：Dynamic\n目标表达式：concat(\"http:\u002F\u002F192.0.2.10:12080\", http.request.uri.path)\n状态码：302\n保留查询字符串：开启\n",[24,626,624],{"__ignoreMap":74},[16,628,629,632,633,636,637,639],{},[24,630,631],{},"192.0.2.10"," 是文档保留地址。占位规则只是为了让 Cloudflare 创建规则集和规则 ID，不应拿它做真实连通性测试。部署后先用 ",[24,634,635],{},"curl -I"," 确认返回状态是 302，并检查 ",[24,638,430],{}," 是否保留路径与查询参数。",[156,641,643],{"id":642},"_4-创建最小权限-api-token","4. 创建最小权限 API Token",[16,645,646],{},"为 Lucky 单独创建 Token：",[68,648,651],{"className":649,"code":650,"language":73,"meta":74},[71],"Permissions：Zone → Single Redirect → Edit\nResources：Include → Specific zone → example.com\n",[24,652,650],{"__ignoreMap":74},[16,654,655],{},"不要使用 Global API Key，也不要把 DNS、Workers、账户管理等无关权限一起勾选。这个 Token 能修改入口跳往何处，一旦泄漏可以被用于钓鱼重定向，应按生产密钥管理。",[16,657,658,659,662,663,666,667,670,671,673],{},"Webhook 还需要三个非密钥标识：",[24,660,661],{},"ZONE_ID","、动态重定向规则集的 ",[24,664,665],{},"RULESET_ID","、目标规则的 ",[24,668,669],{},"RULE_ID","。可以从 Dashboard 编辑规则时的请求中查看，也可以调用 Rulesets API 列出 Zone 规则集，查找 phase 为 ",[24,672,524],{}," 的条目，再读取其中规则。截图或文章仍建议统一打码这些标识。",[156,675,677],{"id":676},"_5-在-lucky-规则里配置-webhook","5. 在 Lucky 规则里配置 Webhook",[16,679,680,681,684],{},"编辑对应 STUN 穿透规则，开启 Webhook。不要使用 Dashboard 的内部 ",[24,682,683],{},"\u002Fapi\u002Fv4\u002F"," 地址，应调用公开 API：",[68,686,689],{"className":687,"code":688,"language":73,"meta":74},[71],"请求地址：\nhttps:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F\u003CZONE_ID>\u002Frulesets\u002F\u003CRULESET_ID>\u002Frules\u002F\u003CRULE_ID>\n\n请求方法：PATCH\n\n请求头：\nAuthorization: Bearer \u003CCLOUDFLARE_API_TOKEN>\nContent-Type: application\u002Fjson\n",[24,690,688],{"__ignoreMap":74},[16,692,693],{},"请求体使用 302：",[68,695,699],{"className":696,"code":697,"language":698,"meta":74,"style":74},"language-json shiki shiki-themes github-dark github-dark","{\n  \"action\": \"redirect\",\n  \"description\": \"Lucky STUN temporary redirect\",\n  \"enabled\": true,\n  \"expression\": \"(http.host eq \\\"go.example.com\\\")\",\n  \"action_parameters\": {\n    \"from_value\": {\n      \"status_code\": 302,\n      \"target_url\": {\n        \"expression\": \"concat(\\\"http:\u002F\u002F#{ipAddr}\\\", http.request.uri.path)\"\n      },\n      \"preserve_query_string\": true\n    }\n  }\n}\n","json",[24,700,701,710,727,740,753,776,785,793,805,813,834,840,851,857,863],{"__ignoreMap":74},[702,703,706],"span",{"class":704,"line":705},"line",1,[702,707,709],{"class":708},"suv1-","{\n",[702,711,713,717,720,724],{"class":704,"line":712},2,[702,714,716],{"class":715},"s8ozJ","  \"action\"",[702,718,719],{"class":708},": ",[702,721,723],{"class":722},"s4wv1","\"redirect\"",[702,725,726],{"class":708},",\n",[702,728,730,733,735,738],{"class":704,"line":729},3,[702,731,732],{"class":715},"  \"description\"",[702,734,719],{"class":708},[702,736,737],{"class":722},"\"Lucky STUN temporary redirect\"",[702,739,726],{"class":708},[702,741,743,746,748,751],{"class":704,"line":742},4,[702,744,745],{"class":715},"  \"enabled\"",[702,747,719],{"class":708},[702,749,750],{"class":715},"true",[702,752,726],{"class":708},[702,754,756,759,761,764,767,769,771,774],{"class":704,"line":755},5,[702,757,758],{"class":715},"  \"expression\"",[702,760,719],{"class":708},[702,762,763],{"class":722},"\"(http.host eq ",[702,765,766],{"class":715},"\\\"",[702,768,33],{"class":722},[702,770,766],{"class":715},[702,772,773],{"class":722},")\"",[702,775,726],{"class":708},[702,777,779,782],{"class":704,"line":778},6,[702,780,781],{"class":715},"  \"action_parameters\"",[702,783,784],{"class":708},": {\n",[702,786,788,791],{"class":704,"line":787},7,[702,789,790],{"class":715},"    \"from_value\"",[702,792,784],{"class":708},[702,794,796,799,801,803],{"class":704,"line":795},8,[702,797,798],{"class":715},"      \"status_code\"",[702,800,719],{"class":708},[702,802,37],{"class":715},[702,804,726],{"class":708},[702,806,808,811],{"class":704,"line":807},9,[702,809,810],{"class":715},"      \"target_url\"",[702,812,784],{"class":708},[702,814,816,819,821,824,826,829,831],{"class":704,"line":815},10,[702,817,818],{"class":715},"        \"expression\"",[702,820,719],{"class":708},[702,822,823],{"class":722},"\"concat(",[702,825,766],{"class":715},[702,827,828],{"class":722},"http:\u002F\u002F#{ipAddr}",[702,830,766],{"class":715},[702,832,833],{"class":722},", http.request.uri.path)\"\n",[702,835,837],{"class":704,"line":836},11,[702,838,839],{"class":708},"      },\n",[702,841,843,846,848],{"class":704,"line":842},12,[702,844,845],{"class":715},"      \"preserve_query_string\"",[702,847,719],{"class":708},[702,849,850],{"class":715},"true\n",[702,852,854],{"class":704,"line":853},13,[702,855,856],{"class":708},"    }\n",[702,858,860],{"class":704,"line":859},14,[702,861,862],{"class":708},"  }\n",[702,864,866],{"class":704,"line":865},15,[702,867,868],{"class":708},"}\n",[16,870,871,872,875],{},"在 Lucky STUN Webhook 中，",[24,873,874],{},"#{ipAddr}"," 会在触发时替换为当前的公网地址与端口，因此生成的 Cloudflare 表达式类似：",[68,877,880],{"className":878,"code":879,"language":73,"meta":74},[71],"concat(\"http:\u002F\u002F203.0.113.10:45678\", http.request.uri.path)\n",[24,881,879],{"__ignoreMap":74},[16,883,884],{},"如果采用“灰云目标域名 + HTTPS”模式，请同时用 Lucky DDNS 更新目标域名，并把目标表达式改为：",[68,886,888],{"className":696,"code":887,"language":698,"meta":74,"style":74},"\"expression\": \"concat(\\\"https:\u002F\u002Forigin-direct.example.com:#{port}\\\", http.request.uri.path)\"\n",[24,889,890],{"__ignoreMap":74},[702,891,892,895,897,899,901,904,906],{"class":704,"line":705},[702,893,894],{"class":722},"\"expression\"",[702,896,719],{"class":708},[702,898,823],{"class":722},[702,900,766],{"class":715},[702,902,903],{"class":722},"https:\u002F\u002Forigin-direct.example.com:#{port}",[702,905,766],{"class":715},[702,907,833],{"class":722},[16,909,910,912,913,916],{},[24,911,874],{}," 与 ",[24,914,915],{},"#{port}"," 是 Lucky Webhook 模板变量，不是 Cloudflare Rules 语言变量。引号必须是英文半角字符，JSON 中表达式内部的双引号必须保留反斜杠转义。",[16,918,919],{},"把 Webhook 的成功判断设置为响应中包含：",[68,921,924],{"className":922,"code":923,"language":73,"meta":74},[71],"\"success\": true\n",[24,925,923],{"__ignoreMap":74},[16,927,928],{},"不同界面或响应序列化可能包含空格，不应仅凭一次手动测试就认定长期有效。最终要同时查看 Lucky Webhook 日志与 Cloudflare API 响应，并重新读取规则确认目标已更新。",[156,930,932],{"id":931},"_6-从外网验证完整链路","6. 从外网验证完整链路",[16,934,935],{},"不要只看浏览器“能打开”。用移动网络或另一条宽带分两段验证：",[68,937,941],{"className":938,"code":939,"language":940,"meta":74,"style":74},"language-bash shiki shiki-themes github-dark github-dark","curl -sS -D - -o \u002Fdev\u002Fnull \"https:\u002F\u002Fgo.example.com\u002Fphotos?id=42\"\n","bash",[24,942,943],{"__ignoreMap":74},[702,944,945,949,952,955,958,961,964],{"class":704,"line":705},[702,946,948],{"class":947},"sFR8T","curl",[702,950,951],{"class":715}," -sS",[702,953,954],{"class":715}," -D",[702,956,957],{"class":722}," -",[702,959,960],{"class":715}," -o",[702,962,963],{"class":722}," \u002Fdev\u002Fnull",[702,965,966],{"class":722}," \"https:\u002F\u002Fgo.example.com\u002Fphotos?id=42\"\n",[16,968,969],{},"预期关键响应：",[68,971,975],{"className":972,"code":973,"language":974,"meta":74,"style":74},"language-http shiki shiki-themes github-dark github-dark","HTTP\u002F2 302\nlocation: http:\u002F\u002F203.0.113.10:45678\u002Fphotos?id=42\n","http",[24,976,977,982],{"__ignoreMap":74},[702,978,979],{"class":704,"line":705},[702,980,981],{},"HTTP\u002F2 302\n",[702,983,984],{"class":704,"line":712},[702,985,986],{},"location: http:\u002F\u002F203.0.113.10:45678\u002Fphotos?id=42\n",[16,988,989],{},"接着检查：",[174,991,992,995,1000,1003,1006],{},[177,993,994],{},"状态码确实是 302，不是仍被旧规则返回 301；",[177,996,997,999],{},[24,998,430],{}," 中的 IP、端口、路径和查询字符串正确；",[177,1001,1002],{},"让 STUN 重新获得映射后，Webhook 是否把规则改成新端口；",[177,1004,1005],{},"浏览器无痕窗口和另一台设备没有命中过去的永久缓存；",[177,1007,1008],{},"第二段直连是否具有应用自身的登录、限速、防爆破和 TLS 保护。",[16,1010,1011,1012,1014,1015,1018],{},"测试时使用 ",[24,1013,635],{}," 只能验证 HEAD；部分服务对 HEAD 和 GET 的处理不同。需要完整验证页面时，使用上面的 ",[24,1016,1017],{},"-D - -o \u002Fdev\u002Fnull"," 发出 GET，但不要在终端输出私密响应正文。",[156,1020,1022],{"id":1021},"_7-这套-302-方案明确不解决什么","7. 这套 302 方案明确不解决什么",[373,1024,1025,1031,1034,1037,1040,1043],{},[177,1026,1027,1028,1030],{},"它不会真正隐藏端口；跳转后地址栏和 ",[24,1029,430],{}," 都会显示端口。",[177,1032,1033],{},"它不会隐藏家庭公网 IP；访问者会直接连接目标。",[177,1035,1036],{},"它不会让 App、电视客户端、远程桌面或自定义协议自动兼容 HTTP 重定向。",[177,1038,1039],{},"它不会把第二段请求放到 Cloudflare WAF、Cache 或 Access 后面。",[177,1041,1042],{},"它不会自动解决 Cookie Domain、CORS、CSRF、绝对 URL、Host 校验或 HTTPS 证书问题。",[177,1044,1045],{},"它只适合 HTTP\u002FHTTPS 入口；UDP 不可能通过 HTTP 302 重定向建立会话。",[16,1047,1048],{},"因此，Cloudflare Redirect 不是“给 STUN 端口套橙云”。橙云只覆盖固定入口的第一次 HTTP 请求。",[64,1050,1052],{"id":1051},"替代方案在-lucky-中运行-cloudflare-tunnel","替代方案：在 Lucky 中运行 Cloudflare Tunnel",[16,1054,1055,1056,1059,1060,1065],{},"如果目标是 Web、API 或后台管理页面，Cloudflare Tunnel 通常更简单。",[24,1057,1058],{},"cloudflared"," 从内网主动连接 Cloudflare，不需要公网 IP，也不需要在路由器开放入站端口。完整模型可参考 ",[163,1061,1064],{"href":1062,"rel":1063},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002F",[167],"Cloudflare Tunnel 官方说明","。",[156,1067,1069],{"id":1068},"_1-在-cloudflare-创建远程管理隧道","1. 在 Cloudflare 创建远程管理隧道",[16,1071,1072],{},"进入 Cloudflare Dashboard：",[68,1074,1077],{"className":1075,"code":1076,"language":73,"meta":74},[71],"Networking → Tunnels → Create a tunnel → Cloudflared\n",[24,1078,1076],{"__ignoreMap":74},[16,1080,1081],{},"给隧道一个不包含家庭地址、设备序列号或个人姓名的名称。创建后，控制台会生成一条安装命令，其中包含 Tunnel Token。Lucky 只需要 Token 本身，不要把整条命令保存到笔记或截图中。",[16,1083,1084],{},[214,1085],{"alt":1086,"src":1087},"Cloudflare Dashboard 的创建隧道空白表单","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Fcloudflare-create-tunnel-v1.png",[16,1089,1090],{},[221,1091,1092],{},"实拍界面；未填写名称、未生成 Token，也没有提交创建操作。账号侧栏处于折叠状态。",[16,1094,1095,1096,1098],{},"Tunnel Token 能让新的 ",[24,1097,1058],{}," 连接到这条隧道，应按密码管理：",[373,1100,1101,1104,1107,1110],{},[177,1102,1103],{},"不写进 Markdown、Shell 历史、Git、Dockerfile 或公开环境变量；",[177,1105,1106],{},"不放在浏览器端代码；",[177,1108,1109],{},"每条隧道使用独立 Token；",[177,1111,1112,1113,1065],{},"泄漏后立即在隧道详情中刷新 Token，并清理旧连接。Cloudflare 也建议",[163,1114,1117],{"href":1115,"rel":1116},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002Fconfigure-tunnels\u002Fremote-tunnel-permissions\u002F",[167],"定期轮换 Tunnel Token",[156,1119,1121],{"id":1120},"_2-在-lucky-添加-tunnel-实例","2. 在 Lucky 添加 Tunnel 实例",[16,1123,195],{},[68,1125,1128],{"className":1126,"code":1127,"language":73,"meta":74},[71],"Lucky → 内网穿透 → Cloudflared → 添加实例\n",[24,1129,1127],{"__ignoreMap":74},[16,1131,1132],{},"选择“隧道”模式，然后配置：",[16,1134,1135],{},[214,1136],{"alt":1137,"src":1138},"Lucky 的 Cloudflared 添加实例表单，表单外的真实实例信息已被强模糊","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-cloudflared-instance-redacted-v1.png",[16,1140,1141],{},[221,1142,1143],{},"Token 与 API Token 输入框保持空白；背景中的真实实例信息已做不可逆强模糊。",[85,1145,1146,1154],{},[88,1147,1148],{},[91,1149,1150,1152],{},[94,1151,253],{},[94,1153,256],{},[107,1155,1156,1164,1172,1180,1188,1196,1204],{},[91,1157,1158,1161],{},[112,1159,1160],{},"实例备注",[112,1162,1163],{},"使用唯一但不泄漏设备或位置的信息",[91,1165,1166,1169],{},[112,1167,1168],{},"令牌",[112,1170,1171],{},"粘贴 Tunnel Token；发布前必须完全打码",[91,1173,1174,1177],{},[112,1175,1176],{},"边缘网络类型",[112,1178,1179],{},"一般使用自动；只有 IPv4\u002FIPv6 链路确有问题时才固定",[91,1181,1182,1185],{},[112,1183,1184],{},"高可用连接数",[112,1186,1187],{},"保持合理数量；更多连接不等于无限可用性",[91,1189,1190,1193],{},[112,1191,1192],{},"连接协议",[112,1194,1195],{},"自动、HTTP\u002F2、QUIC 均需结合网络；UDP 受限时优先测试 HTTP\u002F2",[91,1197,1198,1201],{},[112,1199,1200],{},"边缘绑定 IP",[112,1202,1203],{},"单网卡环境留空；多 WAN 定向出口时再配置",[91,1205,1206,1209],{},[112,1207,1208],{},"不验证后端 TLS 证书",[112,1210,1211],{},"生产环境应关闭。优先填写正确的源服务器名或部署可信 CA",[16,1213,1214],{},"如果希望 Lucky 自动管理应用路由和 DNS，再提供一个单独的 Cloudflare API Token。Lucky 当前界面要求：",[373,1216,1217,1220,1223],{},[177,1218,1219],{},"Account → Cloudflare Tunnel → Edit；",[177,1221,1222],{},"Zone → DNS → Edit；",[177,1224,1225],{},"账户和 Zone 均缩小到实际使用范围。",[16,1227,1228],{},"不需要自动管理时可以不填 API Token，改在 Cloudflare Dashboard 手动维护路由和 DNS。最小权限永远比“为了省事选 All”更安全。",[156,1230,1232],{"id":1231},"_3-添加应用路由","3. 添加应用路由",[16,1234,1235],{},"在 Tunnel 实例右侧进入“应用路由”，添加：",[68,1237,1240],{"className":1238,"code":1239,"language":73,"meta":74},[71],"主机名：app.example.com\n路径：\u002F*\n目标服务：http:\u002F\u002F192.0.2.10:8080\n",[24,1241,1239],{"__ignoreMap":74},[16,1243,1244,1247,1248,1253,1254,1256],{},[24,1245,1246],{},"目标服务","必须包含协议。HTTP、HTTPS、TCP 等值的含义可以查阅 ",[163,1249,1252],{"href":1250,"rel":1251},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ftunnel\u002Frouting\u002F",[167],"Cloudflare Tunnel 路由文档","。对于普通浏览器访问，优先使用 HTTP\u002FHTTPS 应用路由；非 HTTP 公共主机名通常要求客户端也运行 ",[24,1255,1058],{},"，任意 TCP\u002FUDP 公网代理则可能需要 Spectrum 或私网路由方案。",[16,1258,1259,1260,1263,1264,1267],{},"当一条主机名有多条路径规则时，把具体规则放在通配规则之前。例如 ",[24,1261,1262],{},"\u002Fapi\u002F*"," 应先于 ",[24,1265,1266],{},"\u002F*","，否则前面的宽泛规则可能提前匹配。",[16,1269,1270],{},"高级选项只在需要时使用：",[373,1272,1273,1279,1285,1291,1297],{},[177,1274,1275,1278],{},[20,1276,1277],{},"源服务器名","：源站证书上的主机名与目标地址不一致时填写；",[177,1280,1281,1284],{},[20,1282,1283],{},"Host 请求头","：源站按虚拟主机分流时覆盖；",[177,1286,1287,1290],{},[20,1288,1289],{},"HTTP\u002F2 连接","：只有源站确实支持时开启；",[177,1292,1293,1296],{},[20,1294,1295],{},"连接、TLS、保活超时","：依据日志和服务特征调整；",[177,1298,1299,1301,1302,1305],{},[20,1300,1208],{},"：只能作为临时排错手段。Cloudflare 官方也把 ",[24,1303,1304],{},"noTLSVerify"," 定义为生产环境不推荐的最后手段。",[156,1307,1309],{"id":1308},"_4-配置-cname","4. 配置 CNAME",[16,1311,1312],{},"每条 Tunnel 会得到形如下面的目标：",[68,1314,1317],{"className":1315,"code":1316,"language":73,"meta":74},[71],"\u003CTUNNEL_ID>.cfargotunnel.com\n",[24,1318,1316],{"__ignoreMap":74},[16,1320,1321,1322,1325],{},"为 ",[24,1323,1324],{},"app.example.com"," 创建 CNAME 指向它。通过 Cloudflare Dashboard 添加 Published application route 时，DNS 记录通常会自动创建；从 Lucky 管理路由时，是否自动写入取决于 API Token 和当前配置。保存后务必回到 DNS 页面确认，而不是假设已经成功。",[16,1327,1328],{},"Tunnel UUID 不是等同于 Token 的认证密钥，但仍然属于基础设施标识。截图中没有展示它的必要，应统一打码。",[156,1330,1332],{"id":1331},"_5-给管理后台加-cloudflare-access","5. 给管理后台加 Cloudflare Access",[16,1334,1335],{},"Tunnel 只解决“如何到达源站”，不会自动决定“谁可以访问”。管理后台、监控页面和私人服务建议再创建 Access 应用：",[68,1337,1340],{"className":1338,"code":1339,"language":73,"meta":74},[71],"Zero Trust → Access controls → Applications\n→ Create new application → Self-hosted and private\n",[24,1341,1339],{"__ignoreMap":74},[16,1343,1344,1345,1065],{},"把 Tunnel 的公网主机名加入应用，创建 Allow 策略，并用邮箱、身份提供商、设备状态或 Service Token 限制访问。Cloudflare Access 的策略由 Action、Include、Require、Exclude 等条件组成，详见",[163,1346,1349],{"href":1347,"rel":1348},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Faccess-controls\u002Fpolicies\u002F",[167],"官方 Access 策略文档",[16,1351,1352],{},"不要在 Access 保护的 URL 中依赖端口号；Cloudflare 的端口文档说明 Access 会移除 URL 里的端口。对外使用标准 HTTPS 主机名，把内网端口只保留在 Tunnel 的目标服务中。",[64,1354,1355],{"id":1355},"常见故障怎么定位",[85,1357,1358,1368],{},[88,1359,1360],{},[91,1361,1362,1365],{},[94,1363,1364],{},"现象",[94,1366,1367],{},"优先检查",[107,1369,1370,1378,1386,1394,1402,1412,1424,1432,1440,1448,1456,1464,1472,1483],{},[91,1371,1372,1375],{},[112,1373,1374],{},"STUN 没拿到映射",[112,1376,1377],{},"NAT 条件、光猫\u002F路由器拨号位置、双重 NAT、代理软件、内核与防火墙",[91,1379,1380,1383],{},[112,1381,1382],{},"STUN 有映射但外网无日志",[112,1384,1385],{},"公网端口、防火墙、路由器转发、运营商入站限制",[91,1387,1388,1391],{},[112,1389,1390],{},"STUN 有日志但服务打不开",[112,1392,1393],{},"目标地址、目标端口、协议类型、服务监听地址",[91,1395,1396,1399],{},[112,1397,1398],{},"域名能解析但端口不对",[112,1400,1401],{},"A\u002FAAAA 只保存 IP；检查公网端口变化通知或客户端配置",[91,1403,1404,1407],{},[112,1405,1406],{},"入口仍返回 301",[112,1408,1409,1410],{},"Cloudflare 是否还有更靠前的 Redirect\u002FPage Rule；读取当前规则的 ",[24,1411,528],{},[91,1413,1414,1417],{},[112,1415,1416],{},"Webhook 返回 API 错误",[112,1418,1419,1420,1423],{},"Token 是否有 ",[24,1421,1422],{},"Single Redirect: Edit","、Zone\u002F规则 ID 是否正确、JSON 引号与转义是否完整",[91,1425,1426,1429],{},[112,1427,1428],{},"302 的 Location 仍是旧端口",[112,1430,1431],{},"Lucky 是否触发 Webhook、成功判断是否误判、Cloudflare 规则是否被其他自动化覆盖",[91,1433,1434,1437],{},[112,1435,1436],{},"浏览器正确、App 无法登录",[112,1438,1439],{},"App 是否支持重定向；Host、Cookie、认证回调和 TLS 是否绑定原域名",[91,1441,1442,1445],{},[112,1443,1444],{},"HTTPS 跳到 IP 后证书报错",[112,1446,1447],{},"改用灰云目标域名与匹配的源站证书，或改用 Tunnel；不要关闭客户端证书校验",[91,1449,1450,1453],{},[112,1451,1452],{},"Tunnel 显示连接但返回 502",[112,1454,1455],{},"Lucky 到目标服务的网络可达性、协议、端口和源站监听",[91,1457,1458,1461],{},[112,1459,1460],{},"HTTPS 源站出现 x509 错误",[112,1462,1463],{},"优先设置正确的源服务器名或 CA，不要长期启用跳过验证",[91,1465,1466,1469],{},[112,1467,1468],{},"Tunnel 域名返回 1016",[112,1470,1471],{},"CNAME 是否属于同一 Cloudflare 账户、隧道是否运行、目标 UUID 是否正确",[91,1473,1474,1477],{},[112,1475,1476],{},"路径访问到错误服务",[112,1478,1479,1480,1482],{},"检查路由顺序，让具体路径位于 ",[24,1481,1266],{}," 之前",[91,1484,1485,1488],{},[112,1486,1487],{},"QUIC 连接失败",[112,1489,1490],{},"检查出站 UDP；必要时切换 HTTP\u002F2 对照测试",[16,1492,1493],{},"排错时一次只改一个变量，并保留时间点。日志可以帮助定位，但分享前先删掉公网 IP、内网 IP、端口、域名、请求路径、Cookie、Authorization、Token 和实例备注。",[64,1495,1496],{"id":1496},"发布截图前的隐私检查清单",[16,1498,1499],{},"Lucky 的规则列表密度很高，一张未处理的截图可能同时暴露整个家庭网络拓扑。发布前逐项检查：",[373,1501,1502,1505,1508,1511,1514,1517,1520,1523],{},[177,1503,1504],{},"Lucky 后台地址、端口与安全入口路径；",[177,1506,1507],{},"公网 IP、映射端口、内网 IP、目标端口；",[177,1509,1510],{},"域名、主机名、实例名、规则名和设备名；",[177,1512,1513],{},"Tunnel Token、Cloudflare API Token、Access Service Token；",[177,1515,1516],{},"Account ID、Zone ID、Tunnel UUID 与 CNAME 目标；",[177,1518,1519],{},"请求路径、Webhook 地址与请求头；",[177,1521,1522],{},"日志、访问者 IP、流量统计与启用状态；",[177,1524,1525],{},"浏览器标签页、书签栏、账号头像和邮箱。",[16,1527,1528,1529,45,1531,1533,1534,1537],{},"仅仅模糊 Token 的中间几位还不够。安全做法是用重新绘制的示意图，或者把所有真实值替换为 ",[24,1530,1324],{},[24,1532,631],{}," 和 ",[24,1535,1536],{},"\u003CTOKEN>"," 这类占位符。",[64,1539,1540],{"id":1540},"最后的选择建议",[16,1542,1543,1544,1546],{},"如果只是给自己或少量可信用户提供一个浏览器 GET 页面入口，能够接受跳转后暴露 IP 与端口，并且已经验证 NAT 条件，可以选择 ",[20,1545,128],{},"。它解决的是“记住固定入口”，不是反向代理、安全网关或真正的端口隐藏。",[16,1548,1549],{},"如果请求必须保留方法，可在确认客户端行为后评估 307；不要把 303 直接填进 Single Redirect JSON。必须使用 303 时，应由 Worker 或源站生成，并单独设计动态目标存储和鉴权。",[16,1551,1552],{},"如果你要发布的是 Web 页面或 API，并希望地址始终保持标准 HTTPS 域名，同时使用 Access 和 WAF，选择 Cloudflare Tunnel。它不要求 STUN 成功，也不会因为公网映射端口改变而修改访问 URL。",[16,1554,1555],{},"如果你明确需要直接 TCP\u002FUDP、延迟和点对点路径更重要，且网络满足 Lucky 的 STUN 条件，可以选择 STUN；同时接受公网端口可能变化、灰云直连暴露源地址，以及需要自行完成应用认证和防火墙控制。",[16,1557,1558],{},"STUN 直连、STUN + 302 入口和 Cloudflare Tunnel 都能帮助“从外网访问内网”，但只有后者让业务流量经过 Cloudflare。先确定协议、客户端和安全边界，再选择入口方式，通常比照抄一份 301 请求体更可靠。",[64,1560,1561],{"id":1561},"参考资料",[373,1563,1564,1570,1577,1584,1591,1597,1604,1611,1618,1624,1631,1637,1644,1650],{},[177,1565,1566],{},[163,1567,1569],{"href":165,"rel":1568},[167],"Lucky：STUN 内网穿透",[177,1571,1572],{},[163,1573,1576],{"href":1574,"rel":1575},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fcloudflared",[167],"Lucky：Cloudflare 隧道",[177,1578,1579],{},[163,1580,1583],{"href":1581,"rel":1582},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fddns",[167],"Lucky：动态域名（DDNS）",[177,1585,1586],{},[163,1587,1590],{"href":1588,"rel":1589},"https:\u002F\u002Fwww.geekxw.top\u002F1814\u002F",[167],"参考思路：Lucky STUN + Cloudflare 301 重定向",[177,1592,1593],{},[163,1594,1596],{"href":518,"rel":1595},[167],"Cloudflare：Single Redirect 可用状态码与动态表达式",[177,1598,1599],{},[163,1600,1603],{"href":1601,"rel":1602},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fcreate-api\u002F",[167],"Cloudflare：通过 API 创建 Single Redirect",[177,1605,1606],{},[163,1607,1610],{"href":1608,"rel":1609},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fapi\u002Fresources\u002Frulesets\u002F",[167],"Cloudflare：Rulesets API",[177,1612,1613],{},[163,1614,1617],{"href":1615,"rel":1616},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fsupport\u002Ftroubleshooting\u002Fhttp-status-codes\u002F3xx-redirection\u002F",[167],"Cloudflare：3xx 状态码语义",[177,1619,1620],{},[163,1621,1623],{"href":1062,"rel":1622},[167],"Cloudflare：Cloudflare Tunnel",[177,1625,1626],{},[163,1627,1630],{"href":1628,"rel":1629},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002Fget-started\u002Fcreate-remote-tunnel\u002F",[167],"Cloudflare：创建远程管理 Tunnel",[177,1632,1633],{},[163,1634,1636],{"href":1250,"rel":1635},[167],"Cloudflare：Tunnel 路由与 CNAME",[177,1638,1639],{},[163,1640,1643],{"href":1641,"rel":1642},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Freference\u002Fnetwork-ports\u002F",[167],"Cloudflare：代理支持的网络端口",[177,1645,1646],{},[163,1647,1649],{"href":1115,"rel":1648},[167],"Cloudflare：Tunnel Token 权限与轮换",[177,1651,1652],{},[163,1653,1656],{"href":1654,"rel":1655},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ftunnel\u002Ftroubleshooting\u002F",[167],"Cloudflare：Tunnel TLS 故障排查",[1658,1659,1660],"style",{},"html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}",{"title":74,"searchDepth":729,"depth":729,"links":1662},[1663,1664,1671,1680,1687,1688,1689,1690],{"id":66,"depth":712,"text":66},{"id":153,"depth":712,"text":154,"children":1665},[1666,1667,1668,1669,1670],{"id":158,"depth":729,"text":159},{"id":191,"depth":729,"text":192},{"id":229,"depth":729,"text":230},{"id":364,"depth":729,"text":365},{"id":392,"depth":729,"text":393},{"id":419,"depth":712,"text":420,"children":1672},[1673,1674,1675,1676,1677,1678,1679],{"id":434,"depth":729,"text":435},{"id":538,"depth":729,"text":539},{"id":608,"depth":729,"text":609},{"id":642,"depth":729,"text":643},{"id":676,"depth":729,"text":677},{"id":931,"depth":729,"text":932},{"id":1021,"depth":729,"text":1022},{"id":1051,"depth":712,"text":1052,"children":1681},[1682,1683,1684,1685,1686],{"id":1068,"depth":729,"text":1069},{"id":1120,"depth":729,"text":1121},{"id":1231,"depth":729,"text":1232},{"id":1308,"depth":729,"text":1309},{"id":1331,"depth":729,"text":1332},{"id":1355,"depth":712,"text":1355},{"id":1496,"depth":712,"text":1496},{"id":1540,"depth":712,"text":1540},{"id":1561,"depth":712,"text":1561},"通过 Lucky STUN 获取公网映射，再用 Webhook 更新 Cloudflare Single Redirect，让用户从固定域名以 302 临时跳转到最新 IP 与端口；同时讲清 303 限制、TLS、App 兼容性与 Tunnel 替代方案。",false,null,"md",{},true,"\u002Fblog\u002Flucky-stun-cloudflare-tunnel-guide","2026-08-02",27,{"title":10,"description":1691},"blog\u002Flucky-stun-cloudflare-tunnel-guide",[1703,1704,1705,141,1706],"Lucky","STUN","Cloudflare Redirect Rules","Homelab","bo6v7kkJdfHWVBr56kRTAvoAvDtbHx8lj08nqYxvbN0",[1709,2446,4550,5726,7312,9695,11398,11655,12104,12505],{"id":1710,"title":1711,"accent":1712,"body":1713,"description":2427,"draft":1692,"experience":1693,"extension":1694,"featured":1696,"image":2428,"meta":2434,"navigation":1696,"path":2435,"publishedAt":2436,"readingTime":853,"seo":2437,"stem":2438,"tags":2439,"updatedAt":2444,"video":1693,"__hash__":2445,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Ftunneldeck-desktop-ssh-tunnel-guide.md","TunnelDeck 桌面版使用指南：把 ssh -L 变成可保存的隧道","mint",{"type":13,"value":1714,"toc":2401},[1715,1726,1729,1754,1763,1766,1770,1781,1801,1816,1819,1822,1889,1903,1909,1913,1920,1924,1927,1963,1966,1985,1991,2010,2014,2025,2031,2053,2068,2072,2075,2094,2107,2111,2114,2164,2177,2181,2184,2187,2190,2196,2199,2210,2217,2221,2224,2231,2235,2238,2255,2258,2265,2269,2272,2275,2278,2281,2284,2303,2307,2310,2314,2320,2323,2326,2329,2332,2335,2342,2357,2361,2372,2375,2395,2398],[16,1716,1717,1718,1721,1722,1725],{},"SSH 本地端口转发很好用，但当命令越来越多，记住端口、目标地址和密钥位置就会变得麻烦。",[20,1719,1720],{},"TunnelDeck"," 是我做的一款轻量、跨平台的 SSH 隧道桌面工具：它把 ",[24,1723,1724],{},"ssh -L"," 变成可以保存、编辑、快速启停的连接，同时保留主机指纹校验、系统凭据存储和本地监听等安全边界。",[16,1727,1728],{},"例如，下面这条命令：",[68,1730,1732],{"className":938,"code":1731,"language":940,"meta":74,"style":74},"ssh -L 9108:127.0.0.1:9108 -p 33899 root@ssh.example.com\n",[24,1733,1734],{"__ignoreMap":74},[702,1735,1736,1739,1742,1745,1748,1751],{"class":704,"line":705},[702,1737,1738],{"class":947},"ssh",[702,1740,1741],{"class":715}," -L",[702,1743,1744],{"class":722}," 9108:127.0.0.1:9108",[702,1746,1747],{"class":715}," -p",[702,1749,1750],{"class":715}," 33899",[702,1752,1753],{"class":722}," root@ssh.example.com\n",[16,1755,1756,1757,1760,1761,1065],{},"可以直接导入 TunnelDeck。连接建立后，本机访问 ",[24,1758,1759],{},"127.0.0.1:9108","，流量会经过 SSH 服务器转发到它所能访问的 ",[24,1762,1759],{},[16,1764,1765],{},"本文以桌面版 v0.3.1 为主，完整介绍安装、创建连接、密码与私钥认证、首次指纹确认、网页快捷入口和常见故障排查。",[64,1767,1769],{"id":1768},"tunneldeck-适合什么场景","TunnelDeck 适合什么场景",[16,1771,1772,1773,1780],{},"TunnelDeck 当前专注于 SSH ",[20,1774,1775,1776,1779],{},"本地端口转发（",[24,1777,1778],{},"-L","）","，适合：",[373,1782,1783,1786,1789,1792,1798],{},[177,1784,1785],{},"临时访问只监听在服务器本机的管理面板；",[177,1787,1788],{},"通过跳板机访问数据库、开发服务或内网 Web 应用；",[177,1790,1791],{},"保存多条常用隧道，按需启动或停止；",[177,1793,1794,1795,1797],{},"不想每次重新输入一整条 ",[24,1796,1724],{}," 命令；",[177,1799,1800],{},"希望网页隧道连接后，可以再手动用默认浏览器打开。",[16,1802,1803,1804,1807,1808,1811,1812,1815],{},"它不是 VPN，也不提供远程转发 ",[24,1805,1806],{},"-R","、SOCKS 动态代理 ",[24,1809,1810],{},"-D"," 或远程 Shell。导入器还会拒绝 ",[24,1813,1814],{},"ProxyCommand"," 等可能执行外部命令的选项。",[64,1817,1818],{"id":1818},"先看懂四个关键参数",[16,1820,1821],{},"以开头的命令为例：",[85,1823,1824,1837],{},[88,1825,1826],{},[91,1827,1828,1831,1834],{},[94,1829,1830],{},"配置项",[94,1832,1833],{},"示例值",[94,1835,1836],{},"含义",[107,1838,1839,1852,1865,1877],{},[91,1840,1841,1844,1849],{},[112,1842,1843],{},"SSH 服务器",[112,1845,1846],{},[24,1847,1848],{},"ssh.example.com:33899",[112,1850,1851],{},"TunnelDeck 实际登录的 SSH 主机和端口",[91,1853,1854,1857,1862],{},[112,1855,1856],{},"SSH 用户",[112,1858,1859],{},[24,1860,1861],{},"root",[112,1863,1864],{},"SSH 登录用户",[91,1866,1867,1870,1874],{},[112,1868,1869],{},"本地入口",[112,1871,1872],{},[24,1873,1759],{},[112,1875,1876],{},"你的电脑上供应用访问的地址",[91,1878,1879,1882,1886],{},[112,1880,1881],{},"远程目标",[112,1883,1884],{},[24,1885,1759],{},[112,1887,1888],{},"从 SSH 服务器视角访问的目标",[16,1890,1891,1892,1895,1896,1899,1900,1065],{},"最容易混淆的是最后一项：远程目标里的 ",[24,1893,1894],{},"127.0.0.1"," 指 ",[20,1897,1898],{},"SSH 服务器自身","，不是正在运行 TunnelDeck 的电脑。如果服务位于 SSH 服务器能访问的另一台内网机器，就应填写那台机器的地址，例如 ",[24,1901,1902],{},"10.0.0.20:8080",[68,1904,1907],{"className":1905,"code":1906,"language":73,"meta":74},[71],"本机应用\n  └─ 127.0.0.1:9108\n       └─ 加密 SSH 连接 → ssh.example.com:33899\n            └─ SSH 服务器发起连接 → 127.0.0.1:9108\n",[24,1908,1906],{"__ignoreMap":74},[64,1910,1912],{"id":1911},"_1-安装桌面版","1. 安装桌面版",[16,1914,1915,1916,1919],{},"目前项目还没有 Apple 或 Windows 代码签名身份，因此 v0.3.1 ",[20,1917,1918],{},"不提供未签名的预编译桌面安装包","。安装脚本会先检查环境，再下载固定版本源码并在本机完成构建。这种方式不会要求你绕过 macOS Gatekeeper 或 Windows SmartScreen 去运行来源不明的二进制文件。",[156,1921,1923],{"id":1922},"macos-与-linux","macOS 与 Linux",[16,1925,1926],{},"建议先只检查依赖：",[68,1928,1930],{"className":938,"code":1929,"language":940,"meta":74,"style":74},"curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh \\\n  | sh -s -- --check\n",[24,1931,1932,1945],{"__ignoreMap":74},[702,1933,1934,1936,1939,1942],{"class":704,"line":705},[702,1935,948],{"class":947},[702,1937,1938],{"class":715}," -fsSL",[702,1940,1941],{"class":722}," https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh",[702,1943,1944],{"class":715}," \\\n",[702,1946,1947,1951,1954,1957,1960],{"class":704,"line":712},[702,1948,1950],{"class":1949},"sOPea","  |",[702,1952,1953],{"class":947}," sh",[702,1955,1956],{"class":715}," -s",[702,1958,1959],{"class":715}," --",[702,1961,1962],{"class":715}," --check\n",[16,1964,1965],{},"确认输出后再安装：",[68,1967,1969],{"className":938,"code":1968,"language":940,"meta":74,"style":74},"curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh | sh\n",[24,1970,1971],{"__ignoreMap":74},[702,1972,1973,1975,1977,1979,1982],{"class":704,"line":705},[702,1974,948],{"class":947},[702,1976,1938],{"class":715},[702,1978,1941],{"class":722},[702,1980,1981],{"class":1949}," |",[702,1983,1984],{"class":947}," sh\n",[16,1986,1987,1988,1990],{},"没有 ",[24,1989,948],{}," 时也可以使用：",[68,1992,1994],{"className":938,"code":1993,"language":940,"meta":74,"style":74},"wget -qO- https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh | sh\n",[24,1995,1996],{"__ignoreMap":74},[702,1997,1998,2001,2004,2006,2008],{"class":704,"line":705},[702,1999,2000],{"class":947},"wget",[702,2002,2003],{"class":715}," -qO-",[702,2005,1941],{"class":722},[702,2007,1981],{"class":1949},[702,2009,1984],{"class":947},[156,2011,2013],{"id":2012},"windows-powershell","Windows PowerShell",[68,2015,2019],{"className":2016,"code":2017,"language":2018,"meta":74,"style":74},"language-powershell shiki shiki-themes github-dark github-dark","irm https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.ps1 | iex\n","powershell",[24,2020,2021],{"__ignoreMap":74},[702,2022,2023],{"class":704,"line":705},[702,2024,2017],{},[16,2026,2027,2028,1065],{},"安装器会检查 Go 1.25+、Node.js 20+ 和对应平台的构建依赖。缺少 Go 或 Node.js 时，它们会被下载到 TunnelDeck 的私有用户目录，并校验官方 SHA-256，不会修改全局 ",[24,2029,2030],{},"PATH",[373,2032,2033,2040,2046],{},[177,2034,2035,2036,2039],{},"macOS 还需要 Xcode Command Line Tools，应用安装到 ",[24,2037,2038],{},"~\u002FApplications\u002FTunnelDeck.app","；",[177,2041,2042,2043,2039],{},"Linux 需要 GTK3 与 WebKitGTK 4.1，程序安装到 ",[24,2044,2045],{},"~\u002F.local\u002Fbin\u002FTunnelDeck",[177,2047,2048,2049,2052],{},"Windows 需要 WebView2，程序安装到 ",[24,2050,2051],{},"%LOCALAPPDATA%\\Programs\\TunnelDeck\\","，同时创建开始菜单入口。",[16,2054,2055,2056,2061,2062,2067],{},"将远程脚本交给 Shell 前，建议先打开 ",[163,2057,2060],{"href":2058,"rel":2059},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fblob\u002Fv0.3.1\u002Finstall.sh",[167],"v0.3.1 安装脚本","阅读内容。完整依赖和手动构建方式也记录在",[163,2063,2066],{"href":2064,"rel":2065},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fblob\u002Fv0.3.1\u002Fdocs\u002FSOURCE_INSTALL.md",[167],"源码安装指南","中。",[64,2069,2071],{"id":2070},"_2-导入已有的-ssh-l-命令","2. 导入已有的 ssh -L 命令",[16,2073,2074],{},"如果手上已有可以工作的命令，最快的方法是：",[174,2076,2077,2080,2085,2088,2091],{},[177,2078,2079],{},"打开 TunnelDeck，点击“导入命令”；",[177,2081,2082,2083,1797],{},"粘贴完整的 ",[24,2084,1724],{},[177,2086,2087],{},"检查解析出的 SSH 主机、端口、用户、本地入口和远程目标；",[177,2089,2090],{},"为连接填写一个便于识别的名称；",[177,2092,2093],{},"选择认证方式，保存后启动。",[16,2095,2096,2097,2100,2101,45,2103,334,2105,1065],{},"TunnelDeck 解析参数后由 Go SSH 库直接建立连接，",[20,2098,2099],{},"不会把输入拼接成 Shell 命令执行","。为了把导入边界保持在可审计范围内，目前只接受一条本地转发，不接受远程命令、",[24,2102,1806],{},[24,2104,1810],{},[24,2106,1814],{},[64,2108,2110],{"id":2109},"_3-手动创建一条隧道","3. 手动创建一条隧道",[16,2112,2113],{},"点击“新建”，依次填写：",[174,2115,2116,2122,2134,2140,2149,2155,2161],{},[177,2117,2118,2121],{},[20,2119,2120],{},"名称","：例如“开发环境面板”；",[177,2123,2124,2127,2128,1533,2131,2039],{},[20,2125,2126],{},"SSH 服务器与端口","：例如 ",[24,2129,2130],{},"ssh.example.com",[24,2132,2133],{},"33899",[177,2135,2136,2139],{},[20,2137,2138],{},"SSH 用户名","：使用服务器实际允许登录的用户；",[177,2141,2142,2145,2146,2148],{},[20,2143,2144],{},"本地监听地址与端口","：通常保持 ",[24,2147,1894],{},"，再选一个未占用端口；",[177,2150,2151,2154],{},[20,2152,2153],{},"远程目标地址与端口","：填写 SSH 服务器可以访问的服务；",[177,2156,2157,2160],{},[20,2158,2159],{},"认证方式","：选择密码或 SSH 私钥；",[177,2162,2163],{},"点击“保存并启动”。",[16,2165,2166,2167,2169,2170,334,2173,2176],{},"默认绑定 ",[24,2168,1894],{},"，意味着只有本机程序能访问这条隧道。除非明确需要让局域网其他设备接入，并且已经配置系统防火墙，否则不要改成 ",[24,2171,2172],{},"0.0.0.0",[24,2174,2175],{},"::","。TunnelDeck 在这种情况下也会显示暴露风险提示。",[64,2178,2180],{"id":2179},"_4-选择密码或私钥认证","4. 选择密码或私钥认证",[156,2182,2183],{"id":2183},"密码认证",[16,2185,2186],{},"输入 SSH 登录密码即可。默认不要勾选“记住凭据”，密码只在当前运行期间保留，停止隧道后会从隧道对象中清除。",[156,2188,2189],{"id":2189},"私钥认证",[16,2191,2192,2193,1065],{},"选择私钥文件；如果私钥已加密，再输入私钥口令。公钥需要提前写入服务器对应用户的 ",[24,2194,2195],{},"authorized_keys",[16,2197,2198],{},"如果主动启用“记住凭据”，TunnelDeck 会调用操作系统提供的安全存储：",[373,2200,2201,2204,2207],{},[177,2202,2203],{},"macOS Keychain；",[177,2205,2206],{},"Windows Credential Manager；",[177,2208,2209],{},"Linux Secret Service。",[16,2211,2212,2213,2216],{},"普通的 ",[24,2214,2215],{},"profiles.json"," 不保存密码、私钥内容或私钥口令。即使如此，服务器端仍应使用最小权限账号，限制可登录来源，并定期轮换密钥。",[64,2218,2220],{"id":2219},"_5-首次连接要核对主机指纹","5. 首次连接要核对主机指纹",[16,2222,2223],{},"第一次连接某台 SSH 服务器时，TunnelDeck 会显示其 SHA-256 主机指纹。不要只因为弹窗出现就直接信任：应通过服务器控制台、运维文档或其他可信渠道核对指纹，再点击“信任并连接”。",[16,2225,2226,2227,2230],{},"确认后的记录写入 TunnelDeck 独立的 ",[24,2228,2229],{},"known_hosts","。如果服务器主机密钥后来发生变化，连接会被阻止，而不是静默接受新密钥。正常重装服务器也可能导致变化，但在确认原因之前，不要绕过这个警告——它也可能意味着主机写错或连接被劫持。",[64,2232,2234],{"id":2233},"_6-连接后访问服务","6. 连接后访问服务",[16,2236,2237],{},"隧道显示“运行中”后，使用本地入口访问服务：",[373,2239,2240,2246,2252],{},[177,2241,2242,2243,2039],{},"网页：在浏览器打开 ",[24,2244,2245],{},"http:\u002F\u002F127.0.0.1:9108",[177,2247,2248,2249,2251],{},"数据库：让数据库客户端连接 ",[24,2250,1894],{}," 和本地端口；",[177,2253,2254],{},"其他 TCP 服务：同样把客户端目标改为本地入口。",[16,2256,2257],{},"如果目标是网页，可以在配置中启用“这是网页服务”，并选择 HTTP 或 HTTPS。连接成功后会出现“打开网页”按钮，由用户点击后调用系统默认浏览器。",[16,2259,2260,2261,2264],{},"TunnelDeck ",[20,2262,2263],{},"不会","在启动应用、启动隧道或自动重连时擅自打开网页。因为很多端口并不是 HTTP 服务，把“连接”和“打开网页”分开能避免错误跳转，也不会在后台重连时不断弹出浏览器窗口。",[64,2266,2268],{"id":2267},"_7-日常管理与自动重连","7. 日常管理与自动重连",[16,2270,2271],{},"列表会显示每条配置的运行状态和活动连接数。你可以按需启动、停止、编辑或删除配置。编辑运行中的隧道时，应先停止旧连接，再让新参数生效，避免误以为界面已改但旧监听仍在运行。",[16,2273,2274],{},"对于临时网络中断，TunnelDeck 会发送 SSH keepalive，并使用 2–30 秒的指数退避尝试重连。重连只恢复隧道，不会自动打开网页。",[64,2276,2277],{"id":2277},"常见问题排查",[156,2279,2280],{"id":2280},"本地端口无法监听",[16,2282,2283],{},"通常是端口已被其他程序占用。换一个本地端口，或先确认占用者：",[68,2285,2287],{"className":938,"code":2286,"language":940,"meta":74,"style":74},"lsof -nP -iTCP:9108 -sTCP:LISTEN\n",[24,2288,2289],{"__ignoreMap":74},[702,2290,2291,2294,2297,2300],{"class":704,"line":705},[702,2292,2293],{"class":947},"lsof",[702,2295,2296],{"class":715}," -nP",[702,2298,2299],{"class":715}," -iTCP:9108",[702,2301,2302],{"class":715}," -sTCP:LISTEN\n",[156,2304,2306],{"id":2305},"ssh-服务器连不上","SSH 服务器连不上",[16,2308,2309],{},"先检查域名、SSH 端口、用户名、防火墙和当前网络是否允许访问。可以在终端使用相同参数做一次基础连接测试，但不要把密码写进命令或截图。",[156,2311,2313],{"id":2312},"隧道已运行但目标服务打不开","隧道已运行，但目标服务打不开",[16,2315,2316,2317,2319],{},"从 SSH 服务器的视角检查目标地址。尤其注意：远程目标写 ",[24,2318,1894],{}," 时，服务必须运行在 SSH 服务器本机。还要确认目标服务确实监听对应端口，并允许 SSH 用户所在主机访问。",[156,2321,2322],{"id":2322},"私钥无法使用",[16,2324,2325],{},"检查是否选错公钥文件、私钥格式是否受支持，以及加密私钥的口令是否正确。私钥应只对当前系统用户开放读取权限。",[156,2327,2328],{"id":2328},"出现主机密钥变更警告",[16,2330,2331],{},"先暂停连接并核对服务器是否重装、域名是否解析到了新主机。只有通过可信渠道确认新指纹后，才更新信任记录；不要为了“先连上”而关闭校验。",[64,2333,2334],{"id":2334},"为什么桌面版比较轻",[16,2336,2337,2338,2341],{},"TunnelDeck 使用 Go、Wails 和 Vue 3 构建，界面复用操作系统 WebView，不会随应用打包一整套 Chromium。SSH 隧道由 ",[24,2339,2340],{},"golang.org\u002Fx\u002Fcrypto\u002Fssh"," 直接建立。当前 macOS ARM64 应用包约 8.7 MB。",[16,2343,2344,2345,2350,2351,2356],{},"项目采用 MIT License，源码、安装脚本和发布记录都在 ",[163,2346,2349],{"href":2347,"rel":2348},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck",[167],"TunnelDeck GitHub 仓库","。当前稳定版本可在 ",[163,2352,2355],{"href":2353,"rel":2354},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Freleases\u002Ftag\u002Fv0.3.1",[167],"v0.3.1 Release","查看。",[64,2358,2360],{"id":2359},"chrome-应用商店版本已经上线","Chrome 应用商店版本已经上线",[16,2362,2363,2368,2369,1065],{},[163,2364,2367],{"href":2365,"rel":2366},"https:\u002F\u002Fchromewebstore.google.com\u002Fdetail\u002Ftunneldeck\u002Fjnfkjehpbkmfnidfcilehhkpbjjinmod",[167],"TunnelDeck Chrome 侧边栏扩展 v0.3.1","现已在 Chrome Web Store 公开上线，可以直接从商店安装。完整源码、桌面端安装脚本和问题反馈仍统一维护在 ",[163,2370,2349],{"href":2347,"rel":2371},[167],[16,2373,2374],{},"推荐按下面的顺序完成安装：",[174,2376,2377,2380,2383,2389,2392],{},[177,2378,2379],{},"先按本文第 1 节或 GitHub README 安装 TunnelDeck 桌面端；",[177,2381,2382],{},"从 Chrome Web Store 安装扩展；",[177,2384,2385,2386,2039],{},"启动桌面端，在底部“Chrome 浏览器集成”中确认正式扩展 ID ",[24,2387,2388],{},"jnfkjehpbkmfnidfcilehhkpbjjinmod",[177,2390,2391],{},"如果状态显示尚未注册，点击“注册 Chrome 服务”，然后重新加载扩展；",[177,2393,2394],{},"点击 Chrome 工具栏中的 TunnelDeck 图标，即可在侧边栏管理连接。",[16,2396,2397],{},"浏览器扩展通过 Native Messaging 控制本机 TunnelDeck，可以创建、编辑、导入和启停连接，也能在用户点击后打开已标记的网页服务；它不会在浏览器进程中自行建立 SSH。因此，商店扩展负责快捷操作，桌面版仍然是实际运行 SSH 隧道的核心。",[1658,2399,2400],{},"html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}",{"title":74,"searchDepth":729,"depth":729,"links":2402},[2403,2404,2405,2409,2410,2411,2415,2416,2417,2418,2425,2426],{"id":1768,"depth":712,"text":1769},{"id":1818,"depth":712,"text":1818},{"id":1911,"depth":712,"text":1912,"children":2406},[2407,2408],{"id":1922,"depth":729,"text":1923},{"id":2012,"depth":729,"text":2013},{"id":2070,"depth":712,"text":2071},{"id":2109,"depth":712,"text":2110},{"id":2179,"depth":712,"text":2180,"children":2412},[2413,2414],{"id":2183,"depth":729,"text":2183},{"id":2189,"depth":729,"text":2189},{"id":2219,"depth":712,"text":2220},{"id":2233,"depth":712,"text":2234},{"id":2267,"depth":712,"text":2268},{"id":2277,"depth":712,"text":2277,"children":2419},[2420,2421,2422,2423,2424],{"id":2280,"depth":729,"text":2280},{"id":2305,"depth":729,"text":2306},{"id":2312,"depth":729,"text":2313},{"id":2322,"depth":729,"text":2322},{"id":2328,"depth":729,"text":2328},{"id":2334,"depth":712,"text":2334},{"id":2359,"depth":712,"text":2360},"从源码安装 TunnelDeck，创建或导入 SSH 本地端口转发，选择密码或私钥认证，核对主机指纹，并安全地打开本地网页服务。",{"src":2429,"alt":2430,"caption":2431,"width":2432,"height":2433},"\u002Fmedia\u002Fposts\u002Ftunneldeck-desktop-ssh-tunnel-guide\u002Ftunneldeck-macos-v1.webp","TunnelDeck 桌面版新建隧道界面，展示 SSH 服务器、本地入口和远程目标配置","TunnelDeck v0.3.1 桌面版真实界面；示例地址不包含真实服务器或凭据。点击图片可全屏查看。",2360,1624,{},"\u002Fblog\u002Ftunneldeck-desktop-ssh-tunnel-guide","2026-08-07",{"title":1711,"description":2427},"blog\u002Ftunneldeck-desktop-ssh-tunnel-guide",[1720,2440,2441,2442,2443],"SSH","端口转发","开源工具","Wails","2026-08-10","z5xVIV6kQWiNR8bXRQcxyNofnXsnBlAbQM0bABikqQk",{"id":2447,"title":2448,"accent":2449,"body":2450,"description":4523,"draft":1692,"experience":1693,"extension":1694,"featured":1692,"image":4524,"meta":4529,"navigation":1696,"path":4530,"publishedAt":4531,"readingTime":4532,"seo":4533,"stem":4534,"tags":4535,"updatedAt":4541,"video":4542,"__hash__":4549,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Fstundeck-docker-deployment-guide.md","StunDeck 实战：飞牛 fnOS FPK、Docker 与 Cloudflare 302","cyan",{"type":13,"value":2451,"toc":4496},[2452,2462,2469,2472,2500,2505,2509,2518,2532,2555,2558,2583,2590,2611,2614,2620,2626,2629,2640,2643,2647,2650,2670,2677,2680,2684,2700,2918,2928,2964,2968,2975,2978,3078,3085,3088,3154,3157,3163,3166,3172,3177,3181,3184,3211,3218,3460,3463,3502,3508,3512,3575,3582,3638,3645,3651,3656,3660,3663,3669,3672,3689,3695,3700,3706,3715,3718,3722,3729,3732,3738,3741,3747,3754,3811,3818,3824,3827,3830,3836,3843,3847,3850,3864,3870,3875,3881,3885,3888,3894,3900,3905,3909,3948,3951,3958,3961,3965,3968,3974,3981,3984,4033,4039,4044,4050,4055,4059,4062,4094,4104,4110,4115,4118,4157,4167,4173,4179,4183,4190,4193,4218,4222,4242,4246,4248,4271,4275,4278,4282,4292,4322,4325,4370,4373,4389,4392,4411,4417,4419,4486,4493],[16,2453,2454,2455,2458,2459,2461],{},"这篇教程的重点不是单独把 STUN 跑通，而是让用户始终访问一个",[20,2456,2457],{},"固定域名","：StunDeck 发现公网 IP 或动态端口变化后，自动更新 Cloudflare Single Redirect；Cloudflare 对入口域名返回 ",[20,2460,37],{},"（或 307），客户端再访问新的公网映射。",[16,2463,2464,2465,2468],{},"部署仍从 Docker 开始，但配置顺序必须是：",[20,2466,2467],{},"部署 StunDeck → 创建 Cloudflare Token → 保存 Cloudflare 连接 → 创建 Redirect 服务 → 启动 STUN\u002FUPnP → 验证 302 与第二跳","。Cloudflare 不能等服务建完才补，否则创建服务时没有可选连接，也无法验证自动同步。",[16,2470,2471],{},"最终实测结果：",[373,2473,2474,2477,2480,2486,2489,2492,2497],{},[177,2475,2476],{},"Debian 13.6，amd64；",[177,2478,2479],{},"Docker Engine 29.7.1，Docker Compose 5.3.1；",[177,2481,2482,2483,2039],{},"StunDeck 镜像对应公开仓库提交 ",[24,2484,2485],{},"bcb5a1a",[177,2487,2488],{},"Docker 服务环境没有代理，StunDeck 容器内的代理变量全部为空；",[177,2490,2491],{},"NATMap 成功取得映射，UPnP 已把第一跳监听端口放行；",[177,2493,2494,2495,2039],{},"独立外网主机直连动态公网地址返回 HTTP 200；固定入口复查返回 HTTP 302 和 ",[24,2496,430],{},[177,2498,2499],{},"没有把真实用户名、密码、Token、内网地址、公网 IP 或动态端口写入本文与配图。",[51,2501,2502],{},[16,2503,2504],{},"STUN 不是中继，也不能把对称型 NAT 或受限 CGNAT 变成公网入口。本文记录的是在当前测试网络中验证成功的部署流程，不代表所有宽带环境都能复现相同结果。",[64,2506,2508],{"id":2507},"飞牛-fnos直接安装正式-fpk","飞牛 fnOS：直接安装正式 FPK",[51,2510,2511],{},[16,2512,2513,2514,2517],{},"上架状态（2026-08-26）：StunDeck 已提交飞牛应用中心上架审核。审核通过并公开上架前，可以先手动安装这里提供的正式 FPK。这里的飞牛安装包格式是 ",[20,2515,2516],{},"FPK","，不是 Android APK。",[16,2519,2520,2521,2524,2525,912,2528,2531],{},"当前正式版本为 ",[20,2522,2523],{},"StunDeck v0.1.1","，支持 ",[24,2526,2527],{},"linux\u002Famd64",[24,2529,2530],{},"linux\u002Farm64","。博客保留一份经过校验的正式发布包，同时提供 GitHub Release 作为备用来源：",[373,2533,2534,2542,2549],{},[177,2535,2536,2541],{},[163,2537,2540],{"href":2538,"rel":2539},"https:\u002F\u002Fblog.sparkles-editor.com\u002Fdownloads\u002Fstundeck\u002Fstundeck-fpk-0.1.1.fpk",[167],"从本站下载 stundeck-fpk-0.1.1.fpk","（136,791 字节）；",[177,2543,2544,2039],{},[163,2545,2548],{"href":2546,"rel":2547},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002Fstundeck\u002Freleases\u002Ftag\u002Fv0.1.1",[167],"查看 StunDeck v0.1.1 GitHub Release",[177,2550,2551,2552,1065],{},"SHA-256：",[24,2553,2554],{},"63d7f0e00b3777400507e62033fb0cce72884d10ad824f51b435a502d3a5dd8f",[16,2556,2557],{},"安装步骤：",[174,2559,2560,2563,2566,2573,2580],{},[177,2561,2562],{},"下载 FPK，并在本机核对 SHA-256；",[177,2564,2565],{},"在飞牛 fnOS 打开「应用中心 → 设置 → 手动安装应用」；",[177,2567,2568,2569,2572],{},"选择 ",[24,2570,2571],{},"stundeck-fpk-0.1.1.fpk","，按向导设置时区；只有通过 HTTPS 反向代理访问时才启用安全 Cookie；",[177,2574,2575,2576,2579],{},"首次安装会从 ",[24,2577,2578],{},"20000–59999"," 选择一个空闲端口并持久化保存，升级和重启不会重新随机；",[177,2581,2582],{},"从飞牛桌面打开 StunDeck，创建管理员，再添加 Cloudflare Token、映射服务与 Webhook。",[16,2584,2585,2586,2589],{},"FPK 固定拉取不可变镜像 ",[24,2587,2588],{},"ghcr.io\u002Fnciae-zyh\u002Fstundeck:v0.1.1","，因此 NAS 首次安装时需要能够访问 GHCR。安装包和向导不会预置或收集 Cloudflare Token、Webhook Secret 等秘密。",[16,2591,2592,2593,2596,2597,912,2600,2603,2604,2607,2608,2610],{},"fnOS 会管理应用配置与持久化目录，真实数据位于包的 ",[24,2594,2595],{},"${TRIM_PKGVAR}\u002Fdata","。升级、迁移或重装前，必须把 ",[24,2598,2599],{},"stundeck.db",[24,2601,2602],{},"master.key"," ",[20,2605,2606],{},"成对备份","；缺少 ",[24,2609,2602],{}," 时，数据库中加密保存的 Cloudflare Token、Webhook Secret 与 TOTP Secret 无法恢复。卸载向导默认保留数据，只有明确选择“永久删除数据”时才会删除它。",[64,2612,2613],{"id":2613},"先理解重点链路",[68,2615,2618],{"className":2616,"code":2617,"language":73,"meta":74},[71],"用户始终访问固定域名\n  └─ Cloudflare 代理 DNS（橙色云）\n       └─ Single Redirect 返回 302 \u002F 307 + Location\n            └─ 当前公网 IP:动态端口\n                 └─ 主路由 UPnP \u002F NAT-PMP \u002F 手动端口映射\n                      └─ StunDeck + NATMap\n                           └─ 局域网目标服务\n",[24,2619,2617],{"__ignoreMap":74},[16,2621,2622,2623,2625],{},"固定的是入口域名，不是公网端口。Cloudflare 只处理第一跳：它返回重定向响应，不会为第二跳提供 Tunnel、中继、WAF 或 Access。浏览器看到 ",[24,2624,430],{}," 后会直接连接公网 IP 与动态端口。",[16,2627,2628],{},"这里有两个独立成功条件：",[174,2630,2631,2634],{},[177,2632,2633],{},"固定域名返回正确的 302\u002F307，证明 DNS 与 Single Redirect 已同步；",[177,2635,2636,2637,2639],{},"跟随 ",[24,2638,430],{}," 后业务返回 200，证明 STUN、网关放行、防火墙与目标服务均可用。",[16,2641,2642],{},"只看到第一个结果，不能把服务标记为“公网可用”。",[64,2644,2646],{"id":2645},"_1-部署前检查","1. 部署前检查",[16,2648,2649],{},"准备一台 Linux 主机，建议满足：",[373,2651,2652,2655,2658,2661,2664,2667],{},[177,2653,2654],{},"Debian 13\u002F12 或 Docker 官方支持的其他发行版；",[177,2656,2657],{},"amd64 或 arm64；",[177,2659,2660],{},"固定或可识别的局域网地址；",[177,2662,2663],{},"能访问局域网目标服务；",[177,2665,2666],{},"主路由允许按需启用 UPnP，或者你能配置手动端口转发；",[177,2668,2669],{},"准备一个无敏感数据、可以随时关闭的测试服务。",[16,2671,2672,2676],{},[163,2673,2675],{"href":165,"rel":2674},[167],"Lucky 的 STUN 文档","建议在 NAT1 环境使用，并说明局域网主机需要 UPnP、端口转发或 DMZ 等额外条件。不同设备对 NAT 类型的命名并不统一，所以最终应以 STUN 日志与真正的外网回连为准。",[16,2678,2679],{},"不要把 NAS 管理后台、路由器后台或 StunDeck 控制面当作第一条公网测试规则。先使用静态测试页或临时 HTTP 服务。",[64,2681,2683],{"id":2682},"_2-安装-docker-engine-与-compose","2. 安装 Docker Engine 与 Compose",[16,2685,2686,2687,2692,2693,2696,2697,1065],{},"下面按 ",[163,2688,2691],{"href":2689,"rel":2690},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Finstall\u002Fdebian\u002F",[167],"Docker 官方 Debian 安装文档","配置 apt 仓库。Docker Compose 使用官方 ",[24,2694,2695],{},"docker-compose-plugin","，不要再安装旧版独立 ",[24,2698,2699],{},"docker-compose",[68,2701,2703],{"className":938,"code":2702,"language":940,"meta":74,"style":74},"sudo apt update\nsudo apt install -y ca-certificates curl\n\nsudo install -m 0755 -d \u002Fetc\u002Fapt\u002Fkeyrings\nsudo curl -fsSL https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg \\\n  -o \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\nsudo chmod a+r \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n\nsudo tee \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fdocker.sources >\u002Fdev\u002Fnull \u003C\u003C'EOF'\nTypes: deb\nURIs: https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\nSuites: trixie\nComponents: stable\nArchitectures: amd64\nSigned-By: \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\nEOF\n\nsudo apt update\nsudo apt install -y \\\n  docker-ce \\\n  docker-ce-cli \\\n  containerd.io \\\n  docker-buildx-plugin \\\n  docker-compose-plugin\n",[24,2704,2705,2716,2734,2739,2757,2771,2779,2791,2795,2817,2822,2827,2832,2837,2842,2847,2853,2858,2867,2880,2888,2896,2904,2912],{"__ignoreMap":74},[702,2706,2707,2710,2713],{"class":704,"line":705},[702,2708,2709],{"class":947},"sudo",[702,2711,2712],{"class":722}," apt",[702,2714,2715],{"class":722}," update\n",[702,2717,2718,2720,2722,2725,2728,2731],{"class":704,"line":712},[702,2719,2709],{"class":947},[702,2721,2712],{"class":722},[702,2723,2724],{"class":722}," install",[702,2726,2727],{"class":715}," -y",[702,2729,2730],{"class":722}," ca-certificates",[702,2732,2733],{"class":722}," curl\n",[702,2735,2736],{"class":704,"line":729},[702,2737,2738],{"emptyLinePlaceholder":1696},"\n",[702,2740,2741,2743,2745,2748,2751,2754],{"class":704,"line":742},[702,2742,2709],{"class":947},[702,2744,2724],{"class":722},[702,2746,2747],{"class":715}," -m",[702,2749,2750],{"class":715}," 0755",[702,2752,2753],{"class":715}," -d",[702,2755,2756],{"class":722}," \u002Fetc\u002Fapt\u002Fkeyrings\n",[702,2758,2759,2761,2764,2766,2769],{"class":704,"line":755},[702,2760,2709],{"class":947},[702,2762,2763],{"class":722}," curl",[702,2765,1938],{"class":715},[702,2767,2768],{"class":722}," https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg",[702,2770,1944],{"class":715},[702,2772,2773,2776],{"class":704,"line":778},[702,2774,2775],{"class":715},"  -o",[702,2777,2778],{"class":722}," \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n",[702,2780,2781,2783,2786,2789],{"class":704,"line":787},[702,2782,2709],{"class":947},[702,2784,2785],{"class":722}," chmod",[702,2787,2788],{"class":722}," a+r",[702,2790,2778],{"class":722},[702,2792,2793],{"class":704,"line":795},[702,2794,2738],{"emptyLinePlaceholder":1696},[702,2796,2797,2799,2802,2805,2808,2811,2814],{"class":704,"line":807},[702,2798,2709],{"class":947},[702,2800,2801],{"class":722}," tee",[702,2803,2804],{"class":722}," \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fdocker.sources",[702,2806,2807],{"class":1949}," >",[702,2809,2810],{"class":722},"\u002Fdev\u002Fnull",[702,2812,2813],{"class":1949}," \u003C\u003C",[702,2815,2816],{"class":722},"'EOF'\n",[702,2818,2819],{"class":704,"line":815},[702,2820,2821],{"class":722},"Types: deb\n",[702,2823,2824],{"class":704,"line":836},[702,2825,2826],{"class":722},"URIs: https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\n",[702,2828,2829],{"class":704,"line":842},[702,2830,2831],{"class":722},"Suites: trixie\n",[702,2833,2834],{"class":704,"line":853},[702,2835,2836],{"class":722},"Components: stable\n",[702,2838,2839],{"class":704,"line":859},[702,2840,2841],{"class":722},"Architectures: amd64\n",[702,2843,2844],{"class":704,"line":865},[702,2845,2846],{"class":722},"Signed-By: \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n",[702,2848,2850],{"class":704,"line":2849},16,[702,2851,2852],{"class":722},"EOF\n",[702,2854,2856],{"class":704,"line":2855},17,[702,2857,2738],{"emptyLinePlaceholder":1696},[702,2859,2861,2863,2865],{"class":704,"line":2860},18,[702,2862,2709],{"class":947},[702,2864,2712],{"class":722},[702,2866,2715],{"class":722},[702,2868,2870,2872,2874,2876,2878],{"class":704,"line":2869},19,[702,2871,2709],{"class":947},[702,2873,2712],{"class":722},[702,2875,2724],{"class":722},[702,2877,2727],{"class":715},[702,2879,1944],{"class":715},[702,2881,2883,2886],{"class":704,"line":2882},20,[702,2884,2885],{"class":722},"  docker-ce",[702,2887,1944],{"class":715},[702,2889,2891,2894],{"class":704,"line":2890},21,[702,2892,2893],{"class":722},"  docker-ce-cli",[702,2895,1944],{"class":715},[702,2897,2899,2902],{"class":704,"line":2898},22,[702,2900,2901],{"class":722},"  containerd.io",[702,2903,1944],{"class":715},[702,2905,2907,2910],{"class":704,"line":2906},23,[702,2908,2909],{"class":722},"  docker-buildx-plugin",[702,2911,1944],{"class":715},[702,2913,2915],{"class":704,"line":2914},24,[702,2916,2917],{"class":722},"  docker-compose-plugin\n",[16,2919,2920,2921,1533,2924,2927],{},"如果不是 Debian 13 amd64，把 ",[24,2922,2923],{},"Suites",[24,2925,2926],{},"Architectures"," 改成实际值。可以用下面两条命令确认：",[68,2929,2931],{"className":938,"code":2930,"language":940,"meta":74,"style":74},". \u002Fetc\u002Fos-release && echo \"$VERSION_CODENAME\"\ndpkg --print-architecture\n",[24,2932,2933,2956],{"__ignoreMap":74},[702,2934,2935,2938,2941,2944,2947,2950,2953],{"class":704,"line":705},[702,2936,2937],{"class":715},".",[702,2939,2940],{"class":722}," \u002Fetc\u002Fos-release",[702,2942,2943],{"class":708}," && ",[702,2945,2946],{"class":715},"echo",[702,2948,2949],{"class":722}," \"",[702,2951,2952],{"class":708},"$VERSION_CODENAME",[702,2954,2955],{"class":722},"\"\n",[702,2957,2958,2961],{"class":704,"line":712},[702,2959,2960],{"class":947},"dpkg",[702,2962,2963],{"class":715}," --print-architecture\n",[156,2965,2967],{"id":2966},"下载需要代理时只让安装阶段使用","下载需要代理时，只让安装阶段使用",[16,2969,2970,2971,2974],{},"测试中，Docker 官方 GPG 地址有一次直连被重置。临时代理只用于下载，不应写进 ",[24,2972,2973],{},"\u002Fetc\u002Fenvironment","，也不应长期留在 Docker systemd 服务中。",[16,2976,2977],{},"一个进程级示例：",[68,2979,2981],{"className":938,"code":2980,"language":940,"meta":74,"style":74},"PROXY_URL=http:\u002F\u002F192.168.50.9:7890\n\nsudo curl --proxy \"$PROXY_URL\" -fsSL \\\n  https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg \\\n  -o \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n\nsudo env http_proxy=\"$PROXY_URL\" https_proxy=\"$PROXY_URL\" \\\n  apt update\n\nunset PROXY_URL\n",[24,2982,2983,2994,2998,3019,3026,3032,3036,3059,3066,3070],{"__ignoreMap":74},[702,2984,2985,2988,2991],{"class":704,"line":705},[702,2986,2987],{"class":708},"PROXY_URL",[702,2989,2990],{"class":1949},"=",[702,2992,2993],{"class":722},"http:\u002F\u002F192.168.50.9:7890\n",[702,2995,2996],{"class":704,"line":712},[702,2997,2738],{"emptyLinePlaceholder":1696},[702,2999,3000,3002,3004,3007,3009,3012,3015,3017],{"class":704,"line":729},[702,3001,2709],{"class":947},[702,3003,2763],{"class":722},[702,3005,3006],{"class":715}," --proxy",[702,3008,2949],{"class":722},[702,3010,3011],{"class":708},"$PROXY_URL",[702,3013,3014],{"class":722},"\"",[702,3016,1938],{"class":715},[702,3018,1944],{"class":715},[702,3020,3021,3024],{"class":704,"line":742},[702,3022,3023],{"class":722},"  https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg",[702,3025,1944],{"class":715},[702,3027,3028,3030],{"class":704,"line":755},[702,3029,2775],{"class":715},[702,3031,2778],{"class":722},[702,3033,3034],{"class":704,"line":778},[702,3035,2738],{"emptyLinePlaceholder":1696},[702,3037,3038,3040,3043,3046,3048,3050,3053,3055,3057],{"class":704,"line":787},[702,3039,2709],{"class":947},[702,3041,3042],{"class":722}," env",[702,3044,3045],{"class":722}," http_proxy=\"",[702,3047,3011],{"class":708},[702,3049,3014],{"class":722},[702,3051,3052],{"class":722}," https_proxy=\"",[702,3054,3011],{"class":708},[702,3056,3014],{"class":722},[702,3058,1944],{"class":715},[702,3060,3061,3064],{"class":704,"line":795},[702,3062,3063],{"class":722},"  apt",[702,3065,2715],{"class":722},[702,3067,3068],{"class":704,"line":807},[702,3069,2738],{"emptyLinePlaceholder":1696},[702,3071,3072,3075],{"class":704,"line":815},[702,3073,3074],{"class":715},"unset",[702,3076,3077],{"class":722}," PROXY_URL\n",[16,3079,3080,3081,3084],{},"如果仓库持续不稳定，可以按 Docker 官方文档的“Install from a package”下载与系统匹配的 ",[24,3082,3083],{},".deb","，先校验文件，再在主机上安装。不要从网盘或不明镜像站获取 Docker 安装包。",[16,3086,3087],{},"安装后验证：",[68,3089,3091],{"className":938,"code":3090,"language":940,"meta":74,"style":74},"sudo docker version\nsudo docker compose version\nsudo systemctl is-enabled docker\nsudo systemctl is-active docker\nsudo systemctl show docker -p Environment\n",[24,3092,3093,3103,3114,3127,3138],{"__ignoreMap":74},[702,3094,3095,3097,3100],{"class":704,"line":705},[702,3096,2709],{"class":947},[702,3098,3099],{"class":722}," docker",[702,3101,3102],{"class":722}," version\n",[702,3104,3105,3107,3109,3112],{"class":704,"line":712},[702,3106,2709],{"class":947},[702,3108,3099],{"class":722},[702,3110,3111],{"class":722}," compose",[702,3113,3102],{"class":722},[702,3115,3116,3118,3121,3124],{"class":704,"line":729},[702,3117,2709],{"class":947},[702,3119,3120],{"class":722}," systemctl",[702,3122,3123],{"class":722}," is-enabled",[702,3125,3126],{"class":722}," docker\n",[702,3128,3129,3131,3133,3136],{"class":704,"line":742},[702,3130,2709],{"class":947},[702,3132,3120],{"class":722},[702,3134,3135],{"class":722}," is-active",[702,3137,3126],{"class":722},[702,3139,3140,3142,3144,3147,3149,3151],{"class":704,"line":755},[702,3141,2709],{"class":947},[702,3143,3120],{"class":722},[702,3145,3146],{"class":722}," show",[702,3148,3099],{"class":722},[702,3150,1747],{"class":715},[702,3152,3153],{"class":722}," Environment\n",[16,3155,3156],{},"最后一条在本次环境中输出：",[68,3158,3161],{"className":3159,"code":3160,"language":73,"meta":74},[71],"Environment=\n",[24,3162,3160],{"__ignoreMap":74},[16,3164,3165],{},"这能证明 Docker daemon 没有继承常驻代理。它还不能证明容器没有代理，后面会再检查一次。",[16,3167,3168],{},[214,3169],{"alt":3170,"src":3171},"Docker Engine、Compose 与 systemd 状态的核验结果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fdocker-install-verified-v1.webp",[16,3173,3174],{},[221,3175,3176],{},"根据本次真实命令输出整理；主机名、登录身份、局域网地址和临时下载代理均已移除。",[64,3178,3180],{"id":3179},"_3-编写-compose-配置","3. 编写 Compose 配置",[16,3182,3183],{},"创建目录：",[68,3185,3187],{"className":938,"code":3186,"language":940,"meta":74,"style":74},"sudo install -d -m 0755 \u002Fopt\u002Fstundeck\ncd \u002Fopt\u002Fstundeck\n",[24,3188,3189,3204],{"__ignoreMap":74},[702,3190,3191,3193,3195,3197,3199,3201],{"class":704,"line":705},[702,3192,2709],{"class":947},[702,3194,2724],{"class":722},[702,3196,2753],{"class":715},[702,3198,2747],{"class":715},[702,3200,2750],{"class":715},[702,3202,3203],{"class":722}," \u002Fopt\u002Fstundeck\n",[702,3205,3206,3209],{"class":704,"line":712},[702,3207,3208],{"class":715},"cd",[702,3210,3203],{"class":722},[16,3212,3213,3214,3217],{},"保存为 ",[24,3215,3216],{},"\u002Fopt\u002Fstundeck\u002Fcompose.yaml","：",[68,3219,3223],{"className":3220,"code":3221,"language":3222,"meta":74,"style":74},"language-yaml shiki shiki-themes github-dark github-dark","services:\n  stundeck:\n    image: ghcr.io\u002Fnciae-zyh\u002Fstundeck:latest\n    network_mode: host\n    restart: unless-stopped\n    read_only: true\n    cap_drop:\n      - ALL\n    security_opt:\n      - no-new-privileges:true\n    tmpfs:\n      - \u002Ftmp:size=16m,mode=1777\n    volumes:\n      - stundeck-data:\u002Fvar\u002Flib\u002Fstundeck\n    environment:\n      STUNDECK_LISTEN: 0.0.0.0:8080\n      STUNDECK_DATA_DIR: \u002Fvar\u002Flib\u002Fstundeck\n      STUNDECK_SECURE_COOKIES: \"false\"\n      TZ: Asia\u002FShanghai\n      HTTP_PROXY: \"\"\n      HTTPS_PROXY: \"\"\n      ALL_PROXY: \"\"\n      http_proxy: \"\"\n      https_proxy: \"\"\n      all_proxy: \"\"\n\nvolumes:\n  stundeck-data:\n","yaml",[24,3224,3225,3234,3241,3251,3261,3271,3280,3287,3295,3302,3309,3316,3323,3330,3337,3344,3354,3364,3374,3384,3394,3403,3412,3421,3430,3440,3445,3452],{"__ignoreMap":74},[702,3226,3227,3231],{"class":704,"line":705},[702,3228,3230],{"class":3229},"sxg3X","services",[702,3232,3233],{"class":708},":\n",[702,3235,3236,3239],{"class":704,"line":712},[702,3237,3238],{"class":3229},"  stundeck",[702,3240,3233],{"class":708},[702,3242,3243,3246,3248],{"class":704,"line":729},[702,3244,3245],{"class":3229},"    image",[702,3247,719],{"class":708},[702,3249,3250],{"class":722},"ghcr.io\u002Fnciae-zyh\u002Fstundeck:latest\n",[702,3252,3253,3256,3258],{"class":704,"line":742},[702,3254,3255],{"class":3229},"    network_mode",[702,3257,719],{"class":708},[702,3259,3260],{"class":722},"host\n",[702,3262,3263,3266,3268],{"class":704,"line":755},[702,3264,3265],{"class":3229},"    restart",[702,3267,719],{"class":708},[702,3269,3270],{"class":722},"unless-stopped\n",[702,3272,3273,3276,3278],{"class":704,"line":778},[702,3274,3275],{"class":3229},"    read_only",[702,3277,719],{"class":708},[702,3279,850],{"class":715},[702,3281,3282,3285],{"class":704,"line":787},[702,3283,3284],{"class":3229},"    cap_drop",[702,3286,3233],{"class":708},[702,3288,3289,3292],{"class":704,"line":795},[702,3290,3291],{"class":708},"      - ",[702,3293,3294],{"class":722},"ALL\n",[702,3296,3297,3300],{"class":704,"line":807},[702,3298,3299],{"class":3229},"    security_opt",[702,3301,3233],{"class":708},[702,3303,3304,3306],{"class":704,"line":815},[702,3305,3291],{"class":708},[702,3307,3308],{"class":722},"no-new-privileges:true\n",[702,3310,3311,3314],{"class":704,"line":836},[702,3312,3313],{"class":3229},"    tmpfs",[702,3315,3233],{"class":708},[702,3317,3318,3320],{"class":704,"line":842},[702,3319,3291],{"class":708},[702,3321,3322],{"class":722},"\u002Ftmp:size=16m,mode=1777\n",[702,3324,3325,3328],{"class":704,"line":853},[702,3326,3327],{"class":3229},"    volumes",[702,3329,3233],{"class":708},[702,3331,3332,3334],{"class":704,"line":859},[702,3333,3291],{"class":708},[702,3335,3336],{"class":722},"stundeck-data:\u002Fvar\u002Flib\u002Fstundeck\n",[702,3338,3339,3342],{"class":704,"line":865},[702,3340,3341],{"class":3229},"    environment",[702,3343,3233],{"class":708},[702,3345,3346,3349,3351],{"class":704,"line":2849},[702,3347,3348],{"class":3229},"      STUNDECK_LISTEN",[702,3350,719],{"class":708},[702,3352,3353],{"class":722},"0.0.0.0:8080\n",[702,3355,3356,3359,3361],{"class":704,"line":2855},[702,3357,3358],{"class":3229},"      STUNDECK_DATA_DIR",[702,3360,719],{"class":708},[702,3362,3363],{"class":722},"\u002Fvar\u002Flib\u002Fstundeck\n",[702,3365,3366,3369,3371],{"class":704,"line":2860},[702,3367,3368],{"class":3229},"      STUNDECK_SECURE_COOKIES",[702,3370,719],{"class":708},[702,3372,3373],{"class":722},"\"false\"\n",[702,3375,3376,3379,3381],{"class":704,"line":2869},[702,3377,3378],{"class":3229},"      TZ",[702,3380,719],{"class":708},[702,3382,3383],{"class":722},"Asia\u002FShanghai\n",[702,3385,3386,3389,3391],{"class":704,"line":2882},[702,3387,3388],{"class":3229},"      HTTP_PROXY",[702,3390,719],{"class":708},[702,3392,3393],{"class":722},"\"\"\n",[702,3395,3396,3399,3401],{"class":704,"line":2890},[702,3397,3398],{"class":3229},"      HTTPS_PROXY",[702,3400,719],{"class":708},[702,3402,3393],{"class":722},[702,3404,3405,3408,3410],{"class":704,"line":2898},[702,3406,3407],{"class":3229},"      ALL_PROXY",[702,3409,719],{"class":708},[702,3411,3393],{"class":722},[702,3413,3414,3417,3419],{"class":704,"line":2906},[702,3415,3416],{"class":3229},"      http_proxy",[702,3418,719],{"class":708},[702,3420,3393],{"class":722},[702,3422,3423,3426,3428],{"class":704,"line":2914},[702,3424,3425],{"class":3229},"      https_proxy",[702,3427,719],{"class":708},[702,3429,3393],{"class":722},[702,3431,3433,3436,3438],{"class":704,"line":3432},25,[702,3434,3435],{"class":3229},"      all_proxy",[702,3437,719],{"class":708},[702,3439,3393],{"class":722},[702,3441,3443],{"class":704,"line":3442},26,[702,3444,2738],{"emptyLinePlaceholder":1696},[702,3446,3447,3450],{"class":704,"line":1699},[702,3448,3449],{"class":3229},"volumes",[702,3451,3233],{"class":708},[702,3453,3455,3458],{"class":704,"line":3454},28,[702,3456,3457],{"class":3229},"  stundeck-data",[702,3459,3233],{"class":708},[16,3461,3462],{},"几个关键点：",[373,3464,3465,3471,3483,3489,3492],{},[177,3466,3467,3470],{},[24,3468,3469],{},"network_mode: host","：NATMap 和 UPnP 需要看到 Linux 主机的真实网络栈；",[177,3472,3473,45,3476,45,3479,3482],{},[24,3474,3475],{},"read_only",[24,3477,3478],{},"cap_drop: ALL",[24,3480,3481],{},"no-new-privileges","：减少容器权限；",[177,3484,3485,3486,2039],{},"只持久化 ",[24,3487,3488],{},"\u002Fvar\u002Flib\u002Fstundeck",[177,3490,3491],{},"显式清空大小写代理变量；",[177,3493,3494,3495,3498,3499,1065],{},"局域网 HTTP 首次部署使用 ",[24,3496,3497],{},"STUNDECK_SECURE_COOKIES: \"false\"","；如果以后由 HTTPS 反向代理访问，应改为 ",[24,3500,3501],{},"\"true\"",[16,3503,3504,3505,1065],{},"不要使用 Docker Desktop 作为正式 STUN 网关，也不要为了省事加入 ",[24,3506,3507],{},"privileged: true",[64,3509,3511],{"id":3510},"_4-拉取并启动","4. 拉取并启动",[68,3513,3515],{"className":938,"code":3514,"language":940,"meta":74,"style":74},"cd \u002Fopt\u002Fstundeck\nsudo docker compose config --quiet\nsudo docker compose pull\nsudo docker compose up -d --no-build\nsudo docker compose ps\n",[24,3516,3517,3523,3537,3548,3564],{"__ignoreMap":74},[702,3518,3519,3521],{"class":704,"line":705},[702,3520,3208],{"class":715},[702,3522,3203],{"class":722},[702,3524,3525,3527,3529,3531,3534],{"class":704,"line":712},[702,3526,2709],{"class":947},[702,3528,3099],{"class":722},[702,3530,3111],{"class":722},[702,3532,3533],{"class":722}," config",[702,3535,3536],{"class":715}," --quiet\n",[702,3538,3539,3541,3543,3545],{"class":704,"line":729},[702,3540,2709],{"class":947},[702,3542,3099],{"class":722},[702,3544,3111],{"class":722},[702,3546,3547],{"class":722}," pull\n",[702,3549,3550,3552,3554,3556,3559,3561],{"class":704,"line":742},[702,3551,2709],{"class":947},[702,3553,3099],{"class":722},[702,3555,3111],{"class":722},[702,3557,3558],{"class":722}," up",[702,3560,2753],{"class":715},[702,3562,3563],{"class":715}," --no-build\n",[702,3565,3566,3568,3570,3572],{"class":704,"line":755},[702,3567,2709],{"class":947},[702,3569,3099],{"class":722},[702,3571,3111],{"class":722},[702,3573,3574],{"class":722}," ps\n",[16,3576,3577,3578,3581],{},"本次环境的健康状态在约 10 秒后变为 ",[24,3579,3580],{},"healthy","。继续验证：",[68,3583,3585],{"className":938,"code":3584,"language":940,"meta":74,"style":74},"curl -fsS http:\u002F\u002F127.0.0.1:8080\u002Fapi\u002Fv1\u002Fhealth\n\nsudo docker inspect stundeck-stundeck-1 \\\n  --format '{{range .Config.Env}}{{println .}}{{end}}' \\\n  | grep -i proxy\n",[24,3586,3587,3597,3601,3615,3625],{"__ignoreMap":74},[702,3588,3589,3591,3594],{"class":704,"line":705},[702,3590,948],{"class":947},[702,3592,3593],{"class":715}," -fsS",[702,3595,3596],{"class":722}," http:\u002F\u002F127.0.0.1:8080\u002Fapi\u002Fv1\u002Fhealth\n",[702,3598,3599],{"class":704,"line":712},[702,3600,2738],{"emptyLinePlaceholder":1696},[702,3602,3603,3605,3607,3610,3613],{"class":704,"line":729},[702,3604,2709],{"class":947},[702,3606,3099],{"class":722},[702,3608,3609],{"class":722}," inspect",[702,3611,3612],{"class":722}," stundeck-stundeck-1",[702,3614,1944],{"class":715},[702,3616,3617,3620,3623],{"class":704,"line":742},[702,3618,3619],{"class":715},"  --format",[702,3621,3622],{"class":722}," '{{range .Config.Env}}{{println .}}{{end}}'",[702,3624,1944],{"class":715},[702,3626,3627,3629,3632,3635],{"class":704,"line":755},[702,3628,1950],{"class":1949},[702,3630,3631],{"class":947}," grep",[702,3633,3634],{"class":715}," -i",[702,3636,3637],{"class":722}," proxy\n",[16,3639,3640,3641,3644],{},"健康接口应返回 ",[24,3642,3643],{},"status: ok","。代理检查会看到 6 个变量，但等号右侧都必须为空。镜像拉取时即使临时使用过代理，也要在启动 StunDeck 前撤销 daemon 代理并重启 Docker。",[16,3646,3647],{},[214,3648],{"alt":3649,"src":3650},"Compose 启动、健康接口与容器代理变量的核验结果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcompose-health-verified-v1.webp",[16,3652,3653],{},[221,3654,3655],{},"本次容器进入 healthy，健康接口返回 ok，容器内的大小写代理变量全部为空。画面未保留主机地址或容器标识。",[64,3657,3659],{"id":3658},"_5-首次初始化与安全边界","5. 首次初始化与安全边界",[16,3661,3662],{},"从局域网打开：",[68,3664,3667],{"className":3665,"code":3666,"language":73,"meta":74},[71],"http:\u002F\u002F服务器局域网地址:8080\n",[24,3668,3666],{"__ignoreMap":74},[16,3670,3671],{},"首次进入需要自行创建管理员：",[174,3673,3674,3677,3680,3683,3686],{},[177,3675,3676],{},"使用唯一用户名和至少 12 位的强密码；",[177,3678,3679],{},"访问模式选择“局域网”；",[177,3681,3682],{},"如果已经确定反向代理域名，可以填入允许访问的 Host；",[177,3684,3685],{},"登录后进入“安全设置”，再配置允许 Host 与验证器 2FA；",[177,3687,3688],{},"不要直接把 8080 控制面映射到公网。",[16,3690,3691],{},[214,3692],{"alt":3693,"src":3694},"StunDeck 首次进入时的管理员创建页面","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-first-run-v1.webp",[16,3696,3697],{},[221,3698,3699],{},"一次性空数据环境中的真实首次初始化页面；用户名、密码与确认密码都没有填写。",[16,3701,3702],{},[214,3703],{"alt":3704,"src":3705},"StunDeck 安全设置，显示局域网访问范围、Host 白名单与 2FA 入口","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-security-domain-v1.webp",[16,3707,3708],{},[221,3709,3710,3711,3714],{},"域名 ",[24,3712,3713],{},"stun-lab-20260802.sparkles-editor.com"," 经本人确认可以公开，仅用于演示 Host 白名单；截图环境未保存该值。画面没有 DNS 记录、Zone\u002FRule ID、Token、TOTP 密钥或管理员身份。",[16,3716,3717],{},"如果必须远程管理控制面，优先使用 Cloudflare Tunnel、VPN 或其他带身份认证的反向代理。STUN 动态映射更适合发布可控业务端口，不适合暴露管理后台。",[64,3719,3721],{"id":3720},"_6-先在-cloudflare-创建最小权限-api-token","6. 先在 Cloudflare 创建最小权限 API Token",[16,3723,3724,3725,3728],{},"Single Redirect 只会处理经过 Cloudflare 代理的请求，因此入口域名必须存在于目标 Zone，并使用",[20,3726,3727],{},"代理 DNS（橙色云）","。本文不展示 DNS 记录值、Zone ID、Ruleset ID 或其他域名。",[16,3730,3731],{},"进入 Cloudflare 右上角用户菜单，打开“个人资料 → API 令牌”，点击“创建令牌”。",[16,3733,3734],{},[214,3735],{"alt":3736,"src":3737},"Cloudflare 用户 API 令牌页面的创建入口","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-entry-v2.webp",[16,3739,3740],{},"选择“创建自定义令牌”，不要使用 Global API Key。",[16,3742,3743],{},[214,3744],{"alt":3745,"src":3746},"Cloudflare 自定义 API 令牌入口","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-custom-token-v2.webp",[16,3748,3749,3750,3753],{},"建议使用可识别的名称，例如 ",[24,3751,3752],{},"StunDeck Redirect · sparkles-editor.com","，然后添加三条 Zone 权限：",[85,3755,3756,3771],{},[88,3757,3758],{},[91,3759,3760,3763,3766,3769],{},[94,3761,3762],{},"资源",[94,3764,3765],{},"权限",[94,3767,3768],{},"级别",[94,3770,557],{},[107,3772,3773,3786,3799],{},[91,3774,3775,3778,3780,3783],{},[112,3776,3777],{},"区域",[112,3779,3777],{},[112,3781,3782],{},"读取",[112,3784,3785],{},"查找并校验目标 Zone",[91,3787,3788,3790,3793,3796],{},[112,3789,3777],{},[112,3791,3792],{},"DNS",[112,3794,3795],{},"编辑",[112,3797,3798],{},"自动创建或维护代理 DNS 记录",[91,3800,3801,3803,3806,3808],{},[112,3802,3777],{},[112,3804,3805],{},"单一重定向",[112,3807,3795],{},[112,3809,3810],{},"创建和更新 302\u002F307 Single Redirect",[16,3812,3813,3814,3817],{},"在“区域资源”中选择“包括 → 特定区域 → ",[24,3815,3816],{},"sparkles-editor.com","”。不要授权所有 Zone。若你关闭 StunDeck 的“自动管理 DNS”，可以不授予 DNS 编辑，但必须自行保证入口记录存在且处于代理状态。",[16,3819,3820],{},[214,3821],{"alt":3822,"src":3823},"Cloudflare 自定义令牌的三项最小权限与特定 Zone 范围","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-permissions-v2.webp",[16,3825,3826],{},"客户端 IP 筛选只适合管理端出口固定的环境。家庭宽带出口会变化时，不要误把当前公网 IP 写死，否则 StunDeck 之后可能无法同步。可以按运维周期设置 Token 到期日。",[16,3828,3829],{},"点击“继续以显示摘要”，确认目标 Zone 只有三项权限：",[16,3831,3832],{},[214,3833],{"alt":3834,"src":3835},"Cloudflare 令牌摘要中的特定 Zone 与最小权限","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-summary-v2.webp",[16,3837,3838,3839,3842],{},"最后点击“创建令牌”。",[20,3840,3841],{},"Token 明文只显示一次","：立即放入密码管理器并粘贴到 StunDeck，不要截图、不要写入 Compose、Markdown、Git、日志或聊天记录。本文为了避免创建不必要的长期凭据，截图停在复核页，没有展示或保存任何 Token 值。",[64,3844,3846],{"id":3845},"_7-在-stundeck-保存并检测-cloudflare-连接","7. 在 StunDeck 保存并检测 Cloudflare 连接",[16,3848,3849],{},"先进入 StunDeck 的“Cloudflare”页面，再创建映射服务：",[174,3851,3852,3855,3858,3861],{},[177,3853,3854],{},"连接名称填写易识别的名称；",[177,3856,3857],{},"粘贴刚创建的 API Token；",[177,3859,3860],{},"点击“检测权限”；",[177,3862,3863],{},"只在 Zone、DNS 和 Single Redirect 检测都通过后保存。",[16,3865,3866],{},[214,3867],{"alt":3868,"src":3869},"StunDeck Cloudflare 连接页，Token 输入框保持为空","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-cf-connect-v2.webp",[16,3871,3872],{},[221,3873,3874],{},"来自重建后的真实管理页面。输入框为空，截图不含 Token、账户名、DNS 值或任何资源 ID。",[16,3876,3877,3878,3880],{},"凭据会由 StunDeck 使用本地主密钥加密保存。备份时必须同时保留 SQLite 数据库与 ",[24,3879,2602],{},"；缺少后者，连接凭据无法恢复。",[64,3882,3884],{"id":3883},"_8-创建以-302-为核心的-redirect-服务","8. 创建以 302 为核心的 Redirect 服务",[16,3886,3887],{},"现在进入“映射服务”，使用一项可以随时关闭的 HTTP 测试服务：",[68,3889,3892],{"className":3890,"code":3891,"language":73,"meta":74},[71],"服务名称：家庭 NAS（示例）\n局域网目标：192.168.1.20（示例）\n目标端口：80（示例）\n协议：TCP\n监听端口：0（自动分配）\n路由器端口放行：UPnP（推荐）\n发布方式：Cloudflare Redirect\nCloudflare 连接：选择上一步保存的连接\n入口域名：stun-lab-20260802.sparkles-editor.com\n目标协议：HTTP\n跳转状态：302 Temporary\n自动管理 DNS：开启\n保留路径：开启\n保留查询参数：开启\n",[24,3893,3891],{"__ignoreMap":74},[16,3895,3896],{},[214,3897],{"alt":3898,"src":3899},"StunDeck 映射服务中的 Cloudflare Redirect、入口域名与 302 选项","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-cf-redirect-service-v2.webp",[16,3901,3902],{},[221,3903,3904],{},"真实页面中的文档表单；局域网地址和端口均为占位示例，Cloudflare 连接未保存，未提交这条服务。画面上方的实际服务内容已裁掉。",[156,3906,3908],{"id":3907},"_302-还是-307","302 还是 307",[85,3910,3911,3924],{},[88,3912,3913],{},[91,3914,3915,3918,3921],{},[94,3916,3917],{},"状态",[94,3919,3920],{},"客户端行为",[94,3922,3923],{},"推荐场景",[107,3925,3926,3937],{},[91,3927,3928,3931,3934],{},[112,3929,3930],{},"302 Temporary",[112,3932,3933],{},"临时跳转；部分客户端会把 POST 改成 GET",[112,3935,3936],{},"普通网页、下载页、以 GET 为主的服务，本文默认",[91,3938,3939,3942,3945],{},[112,3940,3941],{},"307 Temporary Redirect",[112,3943,3944],{},"临时跳转并保留 HTTP 方法与请求体",[112,3946,3947],{},"API、Webhook、上传或不能改变 POST 语义的服务",[16,3949,3950],{},"两者都不会被当作永久跳转缓存。不要使用 301\u002F308 表示会持续变化的 STUN 端点。",[16,3952,3953,3954,3957],{},"“保留路径”和“保留查询参数”开启后，访问 ",[24,3955,3956],{},"\u002Ffiles?a=1"," 会把路径与查询串拼到当前动态目标。使用 HTTPS 目标时还必须填写“HTTPS 目标域名”，并保证目标证书覆盖该域名；直接跳到公网 IP 通常无法通过证书主机名校验。",[16,3959,3960],{},"自动 DNS 只维护 StunDeck 自己创建并带有管理标记的记录。若同名记录由其他系统管理，StunDeck 会停止同步而不是强行覆盖；应先处理冲突，不能删除整组 DNS 后重建。",[64,3962,3964],{"id":3963},"_9-启动服务并完成-stun-upnp-检测","9. 启动服务并完成 STUN \u002F UPnP 检测",[16,3966,3967],{},"点击“创建服务”，再点击“启动”。正常状态会依次经过：",[68,3969,3972],{"className":3970,"code":3971,"language":73,"meta":74},[71],"已停止 → 探测中 → 路由已放行 → Cloudflare 已同步\n",[24,3973,3971],{"__ignoreMap":74},[16,3975,3976,3977,3980],{},"StunDeck 的 UPnP 逻辑会把",[20,3978,3979],{},"穿透监听端口映射到同一个本地端口","，而不是把最终动态公网端口误当成本地端口。随后它把 NATMap 发现的公网 IP 与端口写入 Single Redirect 的目标。",[16,3982,3983],{},"点击服务卡片上的“STUN 检测”，逐项检查：",[373,3985,3986,3992,3998,4003,4009,4015,4021,4027],{},[177,3987,3988,3991],{},[20,3989,3990],{},"代理环境","：Docker daemon 与容器都必须没有常驻代理；",[177,3993,3994,3997],{},[20,3995,3996],{},"NATMap 程序","：二进制可执行；",[177,3999,4000,4002],{},[20,4001,342],{},"：StunDeck 主机能连接局域网服务；",[177,4004,4005,4008],{},[20,4006,4007],{},"UDP\u002FTCP STUN","：至少与你所选协议对应的 Binding 成功；",[177,4010,4011,4014],{},[20,4012,4013],{},"路由器能力","：发现正确的 UPnP IGD 或 NAT-PMP 网关；",[177,4016,4017,4020],{},[20,4018,4019],{},"路由器端口映射","：映射已下发；",[177,4022,4023,4026],{},[20,4024,4025],{},"公网映射","：已取得地址且更新时间合理；",[177,4028,4029,4032],{},[20,4030,4031],{},"Cloudflare 同步","：DNS 与 Redirect Rule 均已完成或明确报告冲突。",[16,4034,4035],{},[214,4036],{"alt":4037,"src":4038},"StunDeck STUN 诊断，显示代理为空、NATMap 可执行、目标端口可达及 Binding 成功","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-diagnostics-v1.webp",[16,4040,4041],{},[221,4042,4043],{},"真实诊断结果的安全裁切；公网 IP、动态端口、内网地址与网关地址均不在画面中。",[16,4045,4046],{},[214,4047],{"alt":4048,"src":4049},"StunDeck 运行概览，显示一个服务、一个已发现映射和运行版本","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-overview-v1.webp",[16,4051,4052],{},[221,4053,4054],{},"真实环境概览；没有展示服务地址、映射地址、凭据或 Cloudflare 配置。",[64,4056,4058],{"id":4057},"_10-验证固定域名的-302-与第二跳","10. 验证固定域名的 302 与第二跳",[16,4060,4061],{},"先禁止 curl 自动跟随跳转，只检查 Cloudflare 第一跳：",[68,4063,4065],{"className":938,"code":4064,"language":940,"meta":74,"style":74},"curl -sS -o \u002Fdev\u002Fnull -D - --max-redirs 0 \\\n  https:\u002F\u002Fstun-lab-20260802.sparkles-editor.com\u002F\n",[24,4066,4067,4089],{"__ignoreMap":74},[702,4068,4069,4071,4073,4075,4077,4079,4081,4084,4087],{"class":704,"line":705},[702,4070,948],{"class":947},[702,4072,951],{"class":715},[702,4074,960],{"class":715},[702,4076,963],{"class":722},[702,4078,954],{"class":715},[702,4080,957],{"class":722},[702,4082,4083],{"class":715}," --max-redirs",[702,4085,4086],{"class":715}," 0",[702,4088,1944],{"class":715},[702,4090,4091],{"class":704,"line":712},[702,4092,4093],{"class":722},"  https:\u002F\u002Fstun-lab-20260802.sparkles-editor.com\u002F\n",[16,4095,4096,4097,4100,4101,4103],{},"应看到 ",[24,4098,4099],{},"HTTP\u002F2 302","（选择 307 时则为 307）以及 ",[24,4102,430],{},"。Cloudflare 规则列表也应显示同一入口域名、302 状态与活动状态：",[16,4105,4106],{},[214,4107],{"alt":4108,"src":4109},"Cloudflare 规则列表中的 StunDeck 302 Single Redirect，动态目标已从截图中裁除","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-redirect-rule-v2.webp",[16,4111,4112],{},[221,4113,4114],{},"真实 Cloudflare 规则行；仅保留经允许公开的入口域名和 302 状态，动态公网目标、账户信息、其他规则与资源 ID 均未进入画面。",[16,4116,4117],{},"再从手机蜂窝网络或独立外网主机验证第二跳：",[68,4119,4121],{"className":938,"code":4120,"language":940,"meta":74,"style":74},"curl -sS -L --max-time 15 -o \u002Fdev\u002Fnull \\\n  -w 'status=%{http_code} redirects=%{num_redirects}\\n' \\\n  https:\u002F\u002Fstun-lab-20260802.sparkles-editor.com\u002F\n",[24,4122,4123,4143,4153],{"__ignoreMap":74},[702,4124,4125,4127,4129,4131,4134,4137,4139,4141],{"class":704,"line":705},[702,4126,948],{"class":947},[702,4128,951],{"class":715},[702,4130,1741],{"class":715},[702,4132,4133],{"class":715}," --max-time",[702,4135,4136],{"class":715}," 15",[702,4138,960],{"class":715},[702,4140,963],{"class":722},[702,4142,1944],{"class":715},[702,4144,4145,4148,4151],{"class":704,"line":712},[702,4146,4147],{"class":715},"  -w",[702,4149,4150],{"class":722}," 'status=%{http_code} redirects=%{num_redirects}\\n'",[702,4152,1944],{"class":715},[702,4154,4155],{"class":704,"line":729},[702,4156,4093],{"class":722},[16,4158,4159,4160,4163,4164,4166],{},"目标是 ",[24,4161,4162],{},"status=200 redirects=1","。本次重建中，独立外网主机直连新的动态映射得到 HTTP 200；固定入口也实测返回 302 和 ",[24,4165,430],{},"。不过截图所示的旧隔离服务随后已下线，所以它现在只能证明第一跳规则仍生效，不能作为第二跳在线承诺。教程因此明确要求每次映射变化后同时检查这两层。",[16,4168,4169],{},[214,4170],{"alt":4171,"src":4172},"局域网目标与独立外网第二跳都返回 HTTP 200 的脱敏核验结果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fexternal-proof-verified-v1.webp",[16,4174,4175,4176,4178],{},"不要把真实 ",[24,4177,430],{},"、公网 IP 与端口贴在文章、截图或工单里。它会变化，也会增加不必要的扫描风险。",[64,4180,4182],{"id":4181},"_11-常见故障定位","11. 常见故障定位",[156,4184,4186,4187],{"id":4185},"已取得公网映射但浏览器是-err_empty_response","已取得公网映射，但浏览器是 ",[24,4188,4189],{},"ERR_EMPTY_RESPONSE",[16,4191,4192],{},"按顺序检查：",[174,4194,4195,4198,4203,4206,4209,4212,4215],{},[177,4196,4197],{},"局域网目标是否从 StunDeck 主机可达；",[177,4199,4200,4201,2039],{},"目标服务是否监听正确地址，而不是只监听 ",[24,4202,1894],{},[177,4204,4205],{},"容器是否使用 host network；",[177,4207,4208],{},"StunDeck 服务是否选择了正确的网关模式；",[177,4210,4211],{},"UPnP\u002FNAT-PMP 是否在真实出网网关下发映射；",[177,4213,4214],{},"多层 NAT 时，上一级光猫或路由器是否仍阻断入站；",[177,4216,4217],{},"是否用真正的外网测试，而不是在不支持 NAT Loopback 的局域网里访问公网地址。",[156,4219,4221],{"id":4220},"stun-时好时坏","STUN 时好时坏",[373,4223,4224,4227,4230,4233,4236,4239],{},[177,4225,4226],{},"确认 Docker daemon 与容器都没有代理；",[177,4228,4229],{},"关闭会改变出口的透明代理、策略路由或多 WAN 规则后对比；",[177,4231,4232],{},"保持监听端口唯一；",[177,4234,4235],{},"不要把动态端口当成固定端口写入客户端；",[177,4237,4238],{},"观察映射更新时间与 NATMap 进程是否持续运行；",[177,4240,4241],{},"对称型 NAT、运营商 CGNAT 或入站过滤严格时，改用 Tunnel、VPN 或中继方案。",[156,4243,4245],{"id":4244},"固定域名没有返回-302307","固定域名没有返回 302\u002F307",[16,4247,4192],{},[174,4249,4250,4253,4256,4259,4262,4265],{},[177,4251,4252],{},"入口 DNS 是否为代理状态（橙色云）；",[177,4254,4255],{},"Token 是否限定到正确 Zone；",[177,4257,4258],{},"是否具备 Zone 读取与单一重定向编辑权限；",[177,4260,4261],{},"开启自动 DNS 时，是否额外具备 DNS 编辑权限；",[177,4263,4264],{},"同名 DNS 或 Redirect Rule 是否由其他系统管理而发生冲突；",[177,4266,4267,4268,4270],{},"Cloudflare Rules 中是否存在 ",[24,4269,524],{}," 阶段的活动规则。",[156,4272,4274],{"id":4273},"_302-能打开入口但第二跳失败","302 能打开入口，但第二跳失败",[16,4276,4277],{},"302 只更新入口。直接请求动态公网地址；如果仍失败，问题在 STUN、路由器映射、防火墙或目标服务，而不是 Cloudflare Redirect Rule。",[64,4279,4281],{"id":4280},"_12-更新备份与卸载","12. 更新、备份与卸载",[16,4283,4284,4285,4287,4288,4291],{},"更新前把 SQLite 数据库和 ",[24,4286,2602],{}," 一起备份，二者缺一不可。Compose 命名卷通常为 ",[24,4289,4290],{},"stundeck_stundeck-data","，先确认实际名称：",[68,4293,4295],{"className":938,"code":4294,"language":940,"meta":74,"style":74},"sudo docker volume ls\nsudo docker volume inspect stundeck_stundeck-data\n",[24,4296,4297,4309],{"__ignoreMap":74},[702,4298,4299,4301,4303,4306],{"class":704,"line":705},[702,4300,2709],{"class":947},[702,4302,3099],{"class":722},[702,4304,4305],{"class":722}," volume",[702,4307,4308],{"class":722}," ls\n",[702,4310,4311,4313,4315,4317,4319],{"class":704,"line":712},[702,4312,2709],{"class":947},[702,4314,3099],{"class":722},[702,4316,4305],{"class":722},[702,4318,3609],{"class":722},[702,4320,4321],{"class":722}," stundeck_stundeck-data\n",[16,4323,4324],{},"更新：",[68,4326,4328],{"className":938,"code":4327,"language":940,"meta":74,"style":74},"cd \u002Fopt\u002Fstundeck\nsudo docker compose pull\nsudo docker compose up -d --no-build\nsudo docker compose ps\n",[24,4329,4330,4336,4346,4360],{"__ignoreMap":74},[702,4331,4332,4334],{"class":704,"line":705},[702,4333,3208],{"class":715},[702,4335,3203],{"class":722},[702,4337,4338,4340,4342,4344],{"class":704,"line":712},[702,4339,2709],{"class":947},[702,4341,3099],{"class":722},[702,4343,3111],{"class":722},[702,4345,3547],{"class":722},[702,4347,4348,4350,4352,4354,4356,4358],{"class":704,"line":729},[702,4349,2709],{"class":947},[702,4351,3099],{"class":722},[702,4353,3111],{"class":722},[702,4355,3558],{"class":722},[702,4357,2753],{"class":715},[702,4359,3563],{"class":715},[702,4361,4362,4364,4366,4368],{"class":704,"line":742},[702,4363,2709],{"class":947},[702,4365,3099],{"class":722},[702,4367,3111],{"class":722},[702,4369,3574],{"class":722},[16,4371,4372],{},"停止但保留数据：",[68,4374,4376],{"className":938,"code":4375,"language":940,"meta":74,"style":74},"sudo docker compose down\n",[24,4377,4378],{"__ignoreMap":74},[702,4379,4380,4382,4384,4386],{"class":704,"line":705},[702,4381,2709],{"class":947},[702,4383,3099],{"class":722},[702,4385,3111],{"class":722},[702,4387,4388],{"class":722}," down\n",[16,4390,4391],{},"只有确定不要恢复时才删除卷：",[68,4393,4395],{"className":938,"code":4394,"language":940,"meta":74,"style":74},"sudo docker compose down -v\n",[24,4396,4397],{"__ignoreMap":74},[702,4398,4399,4401,4403,4405,4408],{"class":704,"line":705},[702,4400,2709],{"class":947},[702,4402,3099],{"class":722},[702,4404,3111],{"class":722},[702,4406,4407],{"class":722}," down",[702,4409,4410],{"class":715}," -v\n",[16,4412,4413,4416],{},[24,4414,4415],{},"down -v"," 会删除管理员、服务、事件记录、Cloudflare 加密凭据以及本地主密钥，无法通过重新拉取镜像恢复。",[64,4418,1561],{"id":1561},[373,4420,4421,4428,4434,4441,4447,4454,4459,4466,4473,4480],{},[177,4422,4423],{},[163,4424,4427],{"href":4425,"rel":4426},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002Fstundeck",[167],"StunDeck 开源仓库",[177,4429,4430],{},[163,4431,4433],{"href":2546,"rel":4432},[167],"StunDeck v0.1.1 Release 与 FPK",[177,4435,4436],{},[163,4437,4440],{"href":4438,"rel":4439},"https:\u002F\u002Fdeveloper.fnnas.com\u002Fdocs\u002Fcli\u002Ffnpack",[167],"飞牛 fnOS 开发者文档：fnpack",[177,4442,4443],{},[163,4444,4446],{"href":2689,"rel":4445},[167],"Docker：Install Docker Engine on Debian",[177,4448,4449],{},[163,4450,4453],{"href":4451,"rel":4452},"https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002Finstall\u002Flinux\u002F",[167],"Docker：Install the Compose plugin",[177,4455,4456],{},[163,4457,1569],{"href":165,"rel":4458},[167],[177,4460,4461],{},[163,4462,4465],{"href":4463,"rel":4464},"https:\u002F\u002Fgithub.com\u002Fheiher\u002Fnatmap",[167],"NATMap 开源仓库",[177,4467,4468],{},[163,4469,4472],{"href":4470,"rel":4471},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Fapi\u002Fget-started\u002Fcreate-token\u002F",[167],"Cloudflare：创建 API Token",[177,4474,4475],{},[163,4476,4479],{"href":4477,"rel":4478},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fcreate-dashboard\u002F",[167],"Cloudflare：通过 Dashboard 创建 Single Redirect",[177,4481,4482],{},[163,4483,4485],{"href":518,"rel":4484},[167],"Cloudflare：Single Redirect 设置与代理 DNS 要求",[16,4487,4488,4489,4492],{},"这次重建最重要的结论是把验证拆成五层：",[20,4490,4491],{},"目标端口可达、STUN Binding 成功、网关映射已下发、Cloudflare 固定入口返回 302\u002F307、独立外网跟随跳转后业务成功","。固定域名解决的是“入口不变”，不是“第二跳自动获得隧道保护”。",[1658,4494,4495],{},"html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sxg3X, html code.shiki .sxg3X{--shiki-default:#85E89D;--shiki-dark:#85E89D}",{"title":74,"searchDepth":729,"depth":729,"links":4497},[4498,4499,4500,4501,4504,4505,4506,4507,4508,4509,4512,4513,4514,4521,4522],{"id":2507,"depth":712,"text":2508},{"id":2613,"depth":712,"text":2613},{"id":2645,"depth":712,"text":2646},{"id":2682,"depth":712,"text":2683,"children":4502},[4503],{"id":2966,"depth":729,"text":2967},{"id":3179,"depth":712,"text":3180},{"id":3510,"depth":712,"text":3511},{"id":3658,"depth":712,"text":3659},{"id":3720,"depth":712,"text":3721},{"id":3845,"depth":712,"text":3846},{"id":3883,"depth":712,"text":3884,"children":4510},[4511],{"id":3907,"depth":729,"text":3908},{"id":3963,"depth":712,"text":3964},{"id":4057,"depth":712,"text":4058},{"id":4181,"depth":712,"text":4182,"children":4515},[4516,4518,4519,4520],{"id":4185,"depth":729,"text":4517},"已取得公网映射，但浏览器是 ERR_EMPTY_RESPONSE",{"id":4220,"depth":729,"text":4221},{"id":4244,"depth":729,"text":4245},{"id":4273,"depth":729,"text":4274},{"id":4280,"depth":712,"text":4281},{"id":1561,"depth":712,"text":1561},"在飞牛 fnOS 手动安装 StunDeck FPK，或使用 Docker 部署；再配置 Cloudflare 最小权限 API Token、代理 DNS 与 Single Redirect，让固定域名跟随动态 STUN 公网地址。",{"src":4049,"alt":4525,"caption":4526,"width":4527,"height":4528},"StunDeck 运行概览，显示一个已配置服务和一个已发现公网映射","本文重建环境的真实界面；画面不含用户名、密码、Token、内网地址或公网端口。",1280,720,{},"\u002Fblog\u002Fstundeck-docker-deployment-guide","2026-08-03",35,{"title":2448,"description":4523},"blog\u002Fstundeck-docker-deployment-guide",[4536,4537,2516,4538,1704,4539,4540,37,1706],"StunDeck","fnOS","Docker","UPnP","Cloudflare","2026-08-26",{"src":4543,"poster":4049,"title":4544,"description":4545,"duration":4546,"chapters":815,"width":4547,"height":4548},"\u002Fmedia\u002Fvideos\u002Fstundeck-docker-deployment-guide\u002Fstundeck-complete-guide-zh-cn-v1.mp4","StunDeck 完整教程：Docker、Cloudflare 302\u002F307 与 STUN 诊断","6 分钟完整操作演示，从部署、首次初始化和最小权限 Token，一路讲到映射服务、UPnP\u002FNAT-PMP、六层诊断与独立外网验证。",375,1920,1080,"7CElhx3NjQez2L0SG5p5OboJnxRy9nNWagwwtowpqQE",{"id":9,"title":10,"accent":11,"body":4551,"description":1691,"draft":1692,"experience":1693,"extension":1694,"featured":1692,"image":1693,"meta":5723,"navigation":1696,"path":1697,"publishedAt":1698,"readingTime":1699,"seo":5724,"stem":1701,"tags":5725,"updatedAt":1693,"video":1693,"__hash__":1707,"contentLocale":4,"isFallback":1692},{"type":13,"value":4552,"toc":5693},[4553,4559,4565,4571,4579,4581,4586,4590,4636,4638,4640,4645,4647,4657,4659,4661,4666,4671,4675,4679,4681,4683,4685,4689,4693,4767,4769,4774,4778,4780,4782,4784,4796,4798,4800,4812,4814,4816,4822,4824,4882,4893,4895,4897,4899,4933,4937,4939,4944,4948,4950,4952,4957,4959,4964,4972,4974,4976,4981,4983,4993,4995,4999,5004,5006,5130,5134,5139,5141,5161,5167,5169,5174,5176,5178,5180,5200,5202,5214,5216,5230,5236,5238,5254,5256,5258,5265,5267,5269,5274,5276,5280,5284,5288,5301,5303,5305,5310,5312,5316,5320,5374,5376,5384,5386,5388,5390,5395,5404,5410,5412,5436,5438,5440,5445,5449,5451,5453,5455,5460,5465,5467,5469,5571,5573,5575,5577,5595,5603,5605,5609,5611,5613,5615,5617,5619,5691],[16,4554,18,4555],{},[20,4556,22,4557,27],{},[24,4558,26],{},[16,4560,30,4561,34,4563,38],{},[24,4562,33],{},[20,4564,37],{},[16,4566,41,4567,45,4569,49],{},[24,4568,44],{},[24,4570,48],{},[51,4572,4573],{},[16,4574,55,4575,58,4577,62],{},[20,4576,37],{},[20,4578,61],{},[64,4580,66],{"id":66},[68,4582,4584],{"className":4583,"code":72,"language":73,"meta":74},[71],[24,4585,72],{"__ignoreMap":74},[16,4587,79,4588,83],{},[20,4589,82],{},[85,4591,4592,4604],{},[88,4593,4594],{},[91,4595,4596,4598,4600,4602],{},[94,4597,96],{},[94,4599,99],{},[94,4601,102],{},[94,4603,105],{},[107,4605,4606,4616,4626],{},[91,4607,4608,4610,4612,4614],{},[112,4609,114],{},[112,4611,117],{},[112,4613,120],{},[112,4615,123],{},[91,4617,4618,4620,4622,4624],{},[112,4619,128],{},[112,4621,131],{},[112,4623,120],{},[112,4625,136],{},[91,4627,4628,4630,4632,4634],{},[112,4629,141],{},[112,4631,144],{},[112,4633,147],{},[112,4635,150],{},[64,4637,154],{"id":153},[156,4639,159],{"id":158},[16,4641,4642,169],{},[163,4643,168],{"href":165,"rel":4644},[167],[16,4646,172],{},[174,4648,4649,4651,4653,4655],{},[177,4650,179],{},[177,4652,182],{},[177,4654,185],{},[177,4656,188],{},[156,4658,192],{"id":191},[16,4660,195],{},[68,4662,4664],{"className":4663,"code":199,"language":73,"meta":74},[71],[24,4665,199],{"__ignoreMap":74},[16,4667,204,4668,210],{},[163,4669,209],{"href":207,"rel":4670},[167],[16,4672,4673],{},[214,4674],{"alt":216,"src":217},[16,4676,4677],{},[221,4678,223],{},[16,4680,226],{},[156,4682,230],{"id":229},[16,4684,233],{},[16,4686,4687],{},[214,4688],{"alt":238,"src":239},[16,4690,4691],{},[221,4692,244],{},[85,4694,4695,4703],{},[88,4696,4697],{},[91,4698,4699,4701],{},[94,4700,253],{},[94,4702,256],{},[107,4704,4705,4711,4717,4727,4733,4739,4745,4751,4761],{},[91,4706,4707,4709],{},[112,4708,263],{},[112,4710,266],{},[91,4712,4713,4715],{},[112,4714,271],{},[112,4716,274],{},[91,4718,4719,4721],{},[112,4720,279],{},[112,4722,282,4723,286,4725,290],{},[24,4724,285],{},[24,4726,289],{},[91,4728,4729,4731],{},[112,4730,295],{},[112,4732,298],{},[91,4734,4735,4737],{},[112,4736,303],{},[112,4738,306],{},[91,4740,4741,4743],{},[112,4742,311],{},[112,4744,314],{},[91,4746,4747,4749],{},[112,4748,319],{},[112,4750,322],{},[91,4752,4753,4755],{},[112,4754,327],{},[112,4756,330,4757,334,4759],{},[24,4758,333],{},[24,4760,337],{},[91,4762,4763,4765],{},[112,4764,342],{},[112,4766,345],{},[16,4768,348],{},[68,4770,4772],{"className":4771,"code":352,"language":73,"meta":74},[71],[24,4773,352],{"__ignoreMap":74},[16,4775,357,4776,361],{},[24,4777,360],{},[156,4779,365],{"id":364},[16,4781,368],{},[16,4783,371],{},[373,4785,4786,4788,4790,4792,4794],{},[177,4787,377],{},[177,4789,380],{},[177,4791,383],{},[177,4793,386],{},[177,4795,389],{},[156,4797,393],{"id":392},[16,4799,396],{},[174,4801,4802,4804,4806,4808,4810],{},[177,4803,401],{},[177,4805,404],{},[177,4807,407],{},[177,4809,410],{},[177,4811,413],{},[16,4813,416],{},[64,4815,420],{"id":419},[16,4817,423,4818,427,4820,431],{},[24,4819,426],{},[24,4821,430],{},[156,4823,435],{"id":434},[85,4825,4826,4836],{},[88,4827,4828],{},[91,4829,4830,4832,4834],{},[94,4831,444],{},[94,4833,447],{},[94,4835,450],{},[107,4837,4838,4848,4858,4866,4874],{},[91,4839,4840,4842,4844],{},[112,4841,457],{},[112,4843,460],{},[112,4845,4846,466],{},[20,4847,465],{},[91,4849,4850,4852,4854],{},[112,4851,37],{},[112,4853,473],{},[112,4855,4856,479],{},[20,4857,478],{},[91,4859,4860,4862,4864],{},[112,4861,484],{},[112,4863,487],{},[112,4865,490],{},[91,4867,4868,4870,4872],{},[112,4869,495],{},[112,4871,498],{},[112,4873,501],{},[91,4875,4876,4878,4880],{},[112,4877,506],{},[112,4879,509],{},[112,4881,512],{},[16,4883,515,4884,521,4887,525,4889,529,4891,532],{},[163,4885,520],{"href":518,"rel":4886},[167],[24,4888,524],{},[24,4890,528],{},[24,4892,484],{},[16,4894,535],{},[156,4896,539],{"id":538},[16,4898,542],{},[85,4900,4901,4911],{},[88,4902,4903],{},[91,4904,4905,4907,4909],{},[94,4906,551],{},[94,4908,554],{},[94,4910,557],{},[107,4912,4913,4923],{},[91,4914,4915,4919,4921],{},[112,4916,4917],{},[24,4918,33],{},[112,4920,568],{},[112,4922,571],{},[91,4924,4925,4929,4931],{},[112,4926,4927],{},[24,4928,578],{},[112,4930,581],{},[112,4932,584],{},[16,4934,587,4935,590],{},[24,4936,33],{},[16,4938,593],{},[68,4940,4942],{"className":4941,"code":597,"language":73,"meta":74},[71],[24,4943,597],{"__ignoreMap":74},[16,4945,602,4946,605],{},[24,4947,578],{},[156,4949,609],{"id":608},[16,4951,195],{},[68,4953,4955],{"className":4954,"code":615,"language":73,"meta":74},[71],[24,4956,615],{"__ignoreMap":74},[16,4958,620],{},[68,4960,4962],{"className":4961,"code":624,"language":73,"meta":74},[71],[24,4963,624],{"__ignoreMap":74},[16,4965,4966,632,4968,636,4970,639],{},[24,4967,631],{},[24,4969,635],{},[24,4971,430],{},[156,4973,643],{"id":642},[16,4975,646],{},[68,4977,4979],{"className":4978,"code":650,"language":73,"meta":74},[71],[24,4980,650],{"__ignoreMap":74},[16,4982,655],{},[16,4984,658,4985,662,4987,666,4989,670,4991,673],{},[24,4986,661],{},[24,4988,665],{},[24,4990,669],{},[24,4992,524],{},[156,4994,677],{"id":676},[16,4996,680,4997,684],{},[24,4998,683],{},[68,5000,5002],{"className":5001,"code":688,"language":73,"meta":74},[71],[24,5003,688],{"__ignoreMap":74},[16,5005,693],{},[68,5007,5008],{"className":696,"code":697,"language":698,"meta":74,"style":74},[24,5009,5010,5014,5024,5034,5044,5062,5068,5074,5084,5090,5106,5110,5118,5122,5126],{"__ignoreMap":74},[702,5011,5012],{"class":704,"line":705},[702,5013,709],{"class":708},[702,5015,5016,5018,5020,5022],{"class":704,"line":712},[702,5017,716],{"class":715},[702,5019,719],{"class":708},[702,5021,723],{"class":722},[702,5023,726],{"class":708},[702,5025,5026,5028,5030,5032],{"class":704,"line":729},[702,5027,732],{"class":715},[702,5029,719],{"class":708},[702,5031,737],{"class":722},[702,5033,726],{"class":708},[702,5035,5036,5038,5040,5042],{"class":704,"line":742},[702,5037,745],{"class":715},[702,5039,719],{"class":708},[702,5041,750],{"class":715},[702,5043,726],{"class":708},[702,5045,5046,5048,5050,5052,5054,5056,5058,5060],{"class":704,"line":755},[702,5047,758],{"class":715},[702,5049,719],{"class":708},[702,5051,763],{"class":722},[702,5053,766],{"class":715},[702,5055,33],{"class":722},[702,5057,766],{"class":715},[702,5059,773],{"class":722},[702,5061,726],{"class":708},[702,5063,5064,5066],{"class":704,"line":778},[702,5065,781],{"class":715},[702,5067,784],{"class":708},[702,5069,5070,5072],{"class":704,"line":787},[702,5071,790],{"class":715},[702,5073,784],{"class":708},[702,5075,5076,5078,5080,5082],{"class":704,"line":795},[702,5077,798],{"class":715},[702,5079,719],{"class":708},[702,5081,37],{"class":715},[702,5083,726],{"class":708},[702,5085,5086,5088],{"class":704,"line":807},[702,5087,810],{"class":715},[702,5089,784],{"class":708},[702,5091,5092,5094,5096,5098,5100,5102,5104],{"class":704,"line":815},[702,5093,818],{"class":715},[702,5095,719],{"class":708},[702,5097,823],{"class":722},[702,5099,766],{"class":715},[702,5101,828],{"class":722},[702,5103,766],{"class":715},[702,5105,833],{"class":722},[702,5107,5108],{"class":704,"line":836},[702,5109,839],{"class":708},[702,5111,5112,5114,5116],{"class":704,"line":842},[702,5113,845],{"class":715},[702,5115,719],{"class":708},[702,5117,850],{"class":715},[702,5119,5120],{"class":704,"line":853},[702,5121,856],{"class":708},[702,5123,5124],{"class":704,"line":859},[702,5125,862],{"class":708},[702,5127,5128],{"class":704,"line":865},[702,5129,868],{"class":708},[16,5131,871,5132,875],{},[24,5133,874],{},[68,5135,5137],{"className":5136,"code":879,"language":73,"meta":74},[71],[24,5138,879],{"__ignoreMap":74},[16,5140,884],{},[68,5142,5143],{"className":696,"code":887,"language":698,"meta":74,"style":74},[24,5144,5145],{"__ignoreMap":74},[702,5146,5147,5149,5151,5153,5155,5157,5159],{"class":704,"line":705},[702,5148,894],{"class":722},[702,5150,719],{"class":708},[702,5152,823],{"class":722},[702,5154,766],{"class":715},[702,5156,903],{"class":722},[702,5158,766],{"class":715},[702,5160,833],{"class":722},[16,5162,5163,912,5165,916],{},[24,5164,874],{},[24,5166,915],{},[16,5168,919],{},[68,5170,5172],{"className":5171,"code":923,"language":73,"meta":74},[71],[24,5173,923],{"__ignoreMap":74},[16,5175,928],{},[156,5177,932],{"id":931},[16,5179,935],{},[68,5181,5182],{"className":938,"code":939,"language":940,"meta":74,"style":74},[24,5183,5184],{"__ignoreMap":74},[702,5185,5186,5188,5190,5192,5194,5196,5198],{"class":704,"line":705},[702,5187,948],{"class":947},[702,5189,951],{"class":715},[702,5191,954],{"class":715},[702,5193,957],{"class":722},[702,5195,960],{"class":715},[702,5197,963],{"class":722},[702,5199,966],{"class":722},[16,5201,969],{},[68,5203,5204],{"className":972,"code":973,"language":974,"meta":74,"style":74},[24,5205,5206,5210],{"__ignoreMap":74},[702,5207,5208],{"class":704,"line":705},[702,5209,981],{},[702,5211,5212],{"class":704,"line":712},[702,5213,986],{},[16,5215,989],{},[174,5217,5218,5220,5224,5226,5228],{},[177,5219,994],{},[177,5221,5222,999],{},[24,5223,430],{},[177,5225,1002],{},[177,5227,1005],{},[177,5229,1008],{},[16,5231,1011,5232,1014,5234,1018],{},[24,5233,635],{},[24,5235,1017],{},[156,5237,1022],{"id":1021},[373,5239,5240,5244,5246,5248,5250,5252],{},[177,5241,1027,5242,1030],{},[24,5243,430],{},[177,5245,1033],{},[177,5247,1036],{},[177,5249,1039],{},[177,5251,1042],{},[177,5253,1045],{},[16,5255,1048],{},[64,5257,1052],{"id":1051},[16,5259,1055,5260,1059,5262,1065],{},[24,5261,1058],{},[163,5263,1064],{"href":1062,"rel":5264},[167],[156,5266,1069],{"id":1068},[16,5268,1072],{},[68,5270,5272],{"className":5271,"code":1076,"language":73,"meta":74},[71],[24,5273,1076],{"__ignoreMap":74},[16,5275,1081],{},[16,5277,5278],{},[214,5279],{"alt":1086,"src":1087},[16,5281,5282],{},[221,5283,1092],{},[16,5285,1095,5286,1098],{},[24,5287,1058],{},[373,5289,5290,5292,5294,5296],{},[177,5291,1103],{},[177,5293,1106],{},[177,5295,1109],{},[177,5297,1112,5298,1065],{},[163,5299,1117],{"href":1115,"rel":5300},[167],[156,5302,1121],{"id":1120},[16,5304,195],{},[68,5306,5308],{"className":5307,"code":1127,"language":73,"meta":74},[71],[24,5309,1127],{"__ignoreMap":74},[16,5311,1132],{},[16,5313,5314],{},[214,5315],{"alt":1137,"src":1138},[16,5317,5318],{},[221,5319,1143],{},[85,5321,5322,5330],{},[88,5323,5324],{},[91,5325,5326,5328],{},[94,5327,253],{},[94,5329,256],{},[107,5331,5332,5338,5344,5350,5356,5362,5368],{},[91,5333,5334,5336],{},[112,5335,1160],{},[112,5337,1163],{},[91,5339,5340,5342],{},[112,5341,1168],{},[112,5343,1171],{},[91,5345,5346,5348],{},[112,5347,1176],{},[112,5349,1179],{},[91,5351,5352,5354],{},[112,5353,1184],{},[112,5355,1187],{},[91,5357,5358,5360],{},[112,5359,1192],{},[112,5361,1195],{},[91,5363,5364,5366],{},[112,5365,1200],{},[112,5367,1203],{},[91,5369,5370,5372],{},[112,5371,1208],{},[112,5373,1211],{},[16,5375,1214],{},[373,5377,5378,5380,5382],{},[177,5379,1219],{},[177,5381,1222],{},[177,5383,1225],{},[16,5385,1228],{},[156,5387,1232],{"id":1231},[16,5389,1235],{},[68,5391,5393],{"className":5392,"code":1239,"language":73,"meta":74},[71],[24,5394,1239],{"__ignoreMap":74},[16,5396,5397,1247,5399,1253,5402,1256],{},[24,5398,1246],{},[163,5400,1252],{"href":1250,"rel":5401},[167],[24,5403,1058],{},[16,5405,1259,5406,1263,5408,1267],{},[24,5407,1262],{},[24,5409,1266],{},[16,5411,1270],{},[373,5413,5414,5418,5422,5426,5430],{},[177,5415,5416,1278],{},[20,5417,1277],{},[177,5419,5420,1284],{},[20,5421,1283],{},[177,5423,5424,1290],{},[20,5425,1289],{},[177,5427,5428,1296],{},[20,5429,1295],{},[177,5431,5432,1301,5434,1305],{},[20,5433,1208],{},[24,5435,1304],{},[156,5437,1309],{"id":1308},[16,5439,1312],{},[68,5441,5443],{"className":5442,"code":1316,"language":73,"meta":74},[71],[24,5444,1316],{"__ignoreMap":74},[16,5446,1321,5447,1325],{},[24,5448,1324],{},[16,5450,1328],{},[156,5452,1332],{"id":1331},[16,5454,1335],{},[68,5456,5458],{"className":5457,"code":1339,"language":73,"meta":74},[71],[24,5459,1339],{"__ignoreMap":74},[16,5461,1344,5462,1065],{},[163,5463,1349],{"href":1347,"rel":5464},[167],[16,5466,1352],{},[64,5468,1355],{"id":1355},[85,5470,5471,5479],{},[88,5472,5473],{},[91,5474,5475,5477],{},[94,5476,1364],{},[94,5478,1367],{},[107,5480,5481,5487,5493,5499,5505,5513,5521,5527,5533,5539,5545,5551,5557,5565],{},[91,5482,5483,5485],{},[112,5484,1374],{},[112,5486,1377],{},[91,5488,5489,5491],{},[112,5490,1382],{},[112,5492,1385],{},[91,5494,5495,5497],{},[112,5496,1390],{},[112,5498,1393],{},[91,5500,5501,5503],{},[112,5502,1398],{},[112,5504,1401],{},[91,5506,5507,5509],{},[112,5508,1406],{},[112,5510,1409,5511],{},[24,5512,528],{},[91,5514,5515,5517],{},[112,5516,1416],{},[112,5518,1419,5519,1423],{},[24,5520,1422],{},[91,5522,5523,5525],{},[112,5524,1428],{},[112,5526,1431],{},[91,5528,5529,5531],{},[112,5530,1436],{},[112,5532,1439],{},[91,5534,5535,5537],{},[112,5536,1444],{},[112,5538,1447],{},[91,5540,5541,5543],{},[112,5542,1452],{},[112,5544,1455],{},[91,5546,5547,5549],{},[112,5548,1460],{},[112,5550,1463],{},[91,5552,5553,5555],{},[112,5554,1468],{},[112,5556,1471],{},[91,5558,5559,5561],{},[112,5560,1476],{},[112,5562,1479,5563,1482],{},[24,5564,1266],{},[91,5566,5567,5569],{},[112,5568,1487],{},[112,5570,1490],{},[16,5572,1493],{},[64,5574,1496],{"id":1496},[16,5576,1499],{},[373,5578,5579,5581,5583,5585,5587,5589,5591,5593],{},[177,5580,1504],{},[177,5582,1507],{},[177,5584,1510],{},[177,5586,1513],{},[177,5588,1516],{},[177,5590,1519],{},[177,5592,1522],{},[177,5594,1525],{},[16,5596,1528,5597,45,5599,1533,5601,1537],{},[24,5598,1324],{},[24,5600,631],{},[24,5602,1536],{},[64,5604,1540],{"id":1540},[16,5606,1543,5607,1546],{},[20,5608,128],{},[16,5610,1549],{},[16,5612,1552],{},[16,5614,1555],{},[16,5616,1558],{},[64,5618,1561],{"id":1561},[373,5620,5621,5626,5631,5636,5641,5646,5651,5656,5661,5666,5671,5676,5681,5686],{},[177,5622,5623],{},[163,5624,1569],{"href":165,"rel":5625},[167],[177,5627,5628],{},[163,5629,1576],{"href":1574,"rel":5630},[167],[177,5632,5633],{},[163,5634,1583],{"href":1581,"rel":5635},[167],[177,5637,5638],{},[163,5639,1590],{"href":1588,"rel":5640},[167],[177,5642,5643],{},[163,5644,1596],{"href":518,"rel":5645},[167],[177,5647,5648],{},[163,5649,1603],{"href":1601,"rel":5650},[167],[177,5652,5653],{},[163,5654,1610],{"href":1608,"rel":5655},[167],[177,5657,5658],{},[163,5659,1617],{"href":1615,"rel":5660},[167],[177,5662,5663],{},[163,5664,1623],{"href":1062,"rel":5665},[167],[177,5667,5668],{},[163,5669,1630],{"href":1628,"rel":5670},[167],[177,5672,5673],{},[163,5674,1636],{"href":1250,"rel":5675},[167],[177,5677,5678],{},[163,5679,1643],{"href":1641,"rel":5680},[167],[177,5682,5683],{},[163,5684,1649],{"href":1115,"rel":5685},[167],[177,5687,5688],{},[163,5689,1656],{"href":1654,"rel":5690},[167],[1658,5692,1660],{},{"title":74,"searchDepth":729,"depth":729,"links":5694},[5695,5696,5703,5712,5719,5720,5721,5722],{"id":66,"depth":712,"text":66},{"id":153,"depth":712,"text":154,"children":5697},[5698,5699,5700,5701,5702],{"id":158,"depth":729,"text":159},{"id":191,"depth":729,"text":192},{"id":229,"depth":729,"text":230},{"id":364,"depth":729,"text":365},{"id":392,"depth":729,"text":393},{"id":419,"depth":712,"text":420,"children":5704},[5705,5706,5707,5708,5709,5710,5711],{"id":434,"depth":729,"text":435},{"id":538,"depth":729,"text":539},{"id":608,"depth":729,"text":609},{"id":642,"depth":729,"text":643},{"id":676,"depth":729,"text":677},{"id":931,"depth":729,"text":932},{"id":1021,"depth":729,"text":1022},{"id":1051,"depth":712,"text":1052,"children":5713},[5714,5715,5716,5717,5718],{"id":1068,"depth":729,"text":1069},{"id":1120,"depth":729,"text":1121},{"id":1231,"depth":729,"text":1232},{"id":1308,"depth":729,"text":1309},{"id":1331,"depth":729,"text":1332},{"id":1355,"depth":712,"text":1355},{"id":1496,"depth":712,"text":1496},{"id":1540,"depth":712,"text":1540},{"id":1561,"depth":712,"text":1561},{},{"title":10,"description":1691},[1703,1704,1705,141,1706],{"id":5727,"title":5728,"accent":2449,"body":5729,"description":7295,"draft":1692,"experience":1693,"extension":1694,"featured":1696,"image":7296,"meta":7302,"navigation":1696,"path":7303,"publishedAt":7304,"readingTime":2855,"seo":7305,"stem":7306,"tags":7307,"updatedAt":1693,"video":1693,"__hash__":7311,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Fcloudflare-smtp-email-sending-guide.md","Cloudflare SMTP 发信实战：从控制台配置到 Nodemailer 上线",{"type":13,"value":5730,"toc":7277},[5731,5738,5741,5750,5768,5771,5856,5859,5872,5879,5882,5886,5889,5895,5898,5902,5908,5914,5919,5922,5941,5944,5964,5968,5971,5977,5983,5986,6008,6011,6079,6091,6094,6126,6130,6133,6160,6163,6180,6183,6446,6452,6461,6464,6671,6689,6693,6696,6703,6795,6798,6801,6858,6861,6865,6868,6947,6957,6960,6968,7004,7007,7010,7013,7016,7093,7096,7102,7108,7112,7117,7137,7140,7143,7237,7240,7243,7246,7248,7274],[16,5732,5733,5734,5737],{},"Cloudflare Email Service 在 2026 年加入了认证 SMTP。对于已经使用 Nodemailer、Python ",[24,5735,5736],{},"smtplib","、PHPMailer，或者某个只会配置 SMTP 的后台系统来说，这意味着不必为了更换邮件服务商重写整个发送层。",[16,5739,5740],{},"本文是一篇独立的 SMTP 教程。目标不是重复介绍邮件接收、Email Routing 或 Email Workers，而是完成一件具体的事：",[51,5742,5743],{},[16,5744,5745,5746,5749],{},"使用 ",[24,5747,5748],{},"smtp.mx.cloudflare.net:465","，通过 Cloudflare API Token 完成 SMTPS 认证，并从自己的域名发送一封事务邮件。",[16,5751,5752,5753,5756,5757,5762,5763,1065],{},"本文依据的是 ",[20,5754,5755],{},"2026 年 7 月 31 日","可见的 Cloudflare Email Service Beta 能力。Beta 阶段的配额和控制台界面仍可能变化，上线前应重新核对",[163,5758,5761],{"href":5759,"rel":5760},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Fsend-emails\u002F",[167],"官方发送指南","与",[163,5764,5767],{"href":5765,"rel":5766},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fsmtp\u002F",[167],"SMTP API 参考",[64,5769,5770],{"id":5770},"一分钟配置表",[85,5772,5773,5783],{},[88,5774,5775],{},[91,5776,5777,5780],{},[94,5778,5779],{},"项目",[94,5781,5782],{},"值",[107,5784,5785,5795,5805,5813,5824,5836,5848],{},[91,5786,5787,5790],{},[112,5788,5789],{},"SMTP 主机",[112,5791,5792],{},[24,5793,5794],{},"smtp.mx.cloudflare.net",[91,5796,5797,5800],{},[112,5798,5799],{},"端口",[112,5801,5802],{},[24,5803,5804],{},"465",[91,5806,5807,5810],{},[112,5808,5809],{},"加密",[112,5811,5812],{},"隐式 TLS，也就是 SMTPS",[91,5814,5815,5818],{},[112,5816,5817],{},"用户名",[112,5819,5820,5821],{},"固定为字面量 ",[24,5822,5823],{},"api_token",[91,5825,5826,5829],{},[112,5827,5828],{},"密码",[112,5830,5831,5832,5835],{},"具有 ",[24,5833,5834],{},"Email Sending: Edit"," 权限的 Cloudflare API Token",[91,5837,5838,5840],{},[112,5839,2159],{},[112,5841,5842,334,5845],{},[24,5843,5844],{},"AUTH PLAIN",[24,5846,5847],{},"AUTH LOGIN",[91,5849,5850,5853],{},[112,5851,5852],{},"发件人",[112,5854,5855],{},"已在 Email Sending 中启用的域名地址",[16,5857,5858],{},"这里有三个不能互换的概念：",[174,5860,5861,5866,5869],{},[177,5862,5863,5864,2039],{},"用户名不是 Cloudflare 邮箱，也不是 Account ID，而是固定字符串 ",[24,5865,5823],{},[177,5867,5868],{},"密码不是登录 Cloudflare 的密码，而是专用 API Token；",[177,5870,5871],{},"端口 465 使用连接建立时立即开始的 TLS，不能照搬端口 587 的 STARTTLS 配置。",[16,5873,5874,5875,1065],{},"Cloudflare 当前不提供 587 STARTTLS，也不把 25 端口作为出站中继；25 端口用于入站 Email Routing。详细差异可查阅",[163,5876,5878],{"href":5765,"rel":5877},[167],"官方 SMTP 文档",[64,5880,5881],{"id":5881},"开始前需要什么",[156,5883,5885],{"id":5884},"_1-域名使用-cloudflare-dns","1. 域名使用 Cloudflare DNS",[16,5887,5888],{},"Email Sending 要求发件域由 Cloudflare DNS 托管。进入：",[68,5890,5893],{"className":5891,"code":5892,"language":73,"meta":74},[71],"Cloudflare Dashboard\n→ Compute\n→ Email Service\n→ Email Sending\n",[24,5894,5892],{"__ignoreMap":74},[16,5896,5897],{},"选择域名并完成启用。Cloudflare 会配置发件验证、DKIM 与退信处理需要的 DNS 记录。控制台显示为“已配置”以后，再继续调 SMTP；否则认证成功也可能因为发件域未获授权而被拒绝。",[156,5899,5901],{"id":5900},"_2-创建最小权限的-api-token","2. 创建最小权限的 API Token",[16,5903,5904,5905,5907],{},"进入域名详情的“连接”页面，切换到 SMTP。页面会提示创建具有 ",[24,5906,5834],{}," 权限的 API Token。",[16,5909,5910],{},[214,5911],{"alt":5912,"src":5913},"Cloudflare Email Service 的 SMTP 连接页，显示主机、端口、用户名与 cURL 示例","\u002Fmedia\u002Fposts\u002Fcloudflare-smtp-email-sending-guide\u002Fdashboard-smtp-curl-v1.webp",[16,5915,5916],{},[221,5917,5918],{},"截图采集于 2026 年 7 月 31 日。画面只包含 Cloudflare 提供的配置值和环境变量占位符；真实 Token 没有被创建、显示或写入文章。",[16,5920,5921],{},"建议使用账户级 Token，并尽可能限制：",[373,5923,5924,5927,5932,5935,5938],{},[177,5925,5926],{},"只授权需要发信的 Cloudflare 账户；",[177,5928,5929,5930,2039],{},"只授予 ",[24,5931,5834],{},[177,5933,5934],{},"为 Token 设置合理有效期；",[177,5936,5937],{},"如果部署出口 IP 稳定，再增加客户端 IP 过滤；",[177,5939,5940],{},"每个应用使用独立 Token，方便吊销和审计。",[16,5942,5943],{},"Token 只在创建时完整显示一次。将它写入服务端 Secret 管理或运行环境变量，绝不能：",[373,5945,5946,5949,5955,5958,5961],{},[177,5947,5948],{},"写进 Git 仓库、Markdown 或构建日志；",[177,5950,5951,5952,2039],{},"放进 ",[24,5953,5954],{},"NUXT_PUBLIC_*",[177,5956,5957],{},"交给浏览器 JavaScript；",[177,5959,5960],{},"在异常日志中打印 SMTP 配置对象；",[177,5962,5963],{},"与其他项目共享同一个长期有效 Token。",[64,5965,5967],{"id":5966},"先用-curl-验证链路","先用 cURL 验证链路",[16,5969,5970],{},"在接入框架以前，先用 cURL 发送最小测试邮件，可以把“Cloudflare 配置问题”和“应用代码问题”拆开排查。",[16,5972,5973,5974,3217],{},"创建 ",[24,5975,5976],{},"mail.txt",[68,5978,5981],{"className":5979,"code":5980,"language":73,"meta":74},[71],"From: Jackie Moon \u003Cnoreply@sparkles-editor.com>\nTo: recipient@example.com\nSubject: Cloudflare SMTP test\nMIME-Version: 1.0\nContent-Type: text\u002Fplain; charset=UTF-8\n\nHello,\n\nThis message was delivered through Cloudflare Email Service SMTP.\n",[24,5982,5980],{"__ignoreMap":74},[16,5984,5985],{},"把 Token 放入当前终端进程的环境变量。不要把真实值写进 Shell 历史：",[68,5987,5989],{"className":938,"code":5988,"language":940,"meta":74,"style":74},"read -s CF_EMAIL_SMTP_TOKEN\nexport CF_EMAIL_SMTP_TOKEN\n",[24,5990,5991,6001],{"__ignoreMap":74},[702,5992,5993,5996,5998],{"class":704,"line":705},[702,5994,5995],{"class":715},"read",[702,5997,1956],{"class":715},[702,5999,6000],{"class":722}," CF_EMAIL_SMTP_TOKEN\n",[702,6002,6003,6006],{"class":704,"line":712},[702,6004,6005],{"class":1949},"export",[702,6007,6000],{"class":708},[16,6009,6010],{},"然后发送：",[68,6012,6014],{"className":938,"code":6013,"language":940,"meta":74,"style":74},"curl --url \"smtps:\u002F\u002Fsmtp.mx.cloudflare.net:465\" \\\n  --ssl-reqd \\\n  --user \"api_token:${CF_EMAIL_SMTP_TOKEN}\" \\\n  --mail-from \"noreply@sparkles-editor.com\" \\\n  --mail-rcpt \"recipient@example.com\" \\\n  --upload-file mail.txt\n",[24,6015,6016,6028,6035,6051,6061,6071],{"__ignoreMap":74},[702,6017,6018,6020,6023,6026],{"class":704,"line":705},[702,6019,948],{"class":947},[702,6021,6022],{"class":715}," --url",[702,6024,6025],{"class":722}," \"smtps:\u002F\u002Fsmtp.mx.cloudflare.net:465\"",[702,6027,1944],{"class":715},[702,6029,6030,6033],{"class":704,"line":712},[702,6031,6032],{"class":715},"  --ssl-reqd",[702,6034,1944],{"class":715},[702,6036,6037,6040,6043,6046,6049],{"class":704,"line":729},[702,6038,6039],{"class":715},"  --user",[702,6041,6042],{"class":722}," \"api_token:${",[702,6044,6045],{"class":708},"CF_EMAIL_SMTP_TOKEN",[702,6047,6048],{"class":722},"}\"",[702,6050,1944],{"class":715},[702,6052,6053,6056,6059],{"class":704,"line":742},[702,6054,6055],{"class":715},"  --mail-from",[702,6057,6058],{"class":722}," \"noreply@sparkles-editor.com\"",[702,6060,1944],{"class":715},[702,6062,6063,6066,6069],{"class":704,"line":755},[702,6064,6065],{"class":715},"  --mail-rcpt",[702,6067,6068],{"class":722}," \"recipient@example.com\"",[702,6070,1944],{"class":715},[702,6072,6073,6076],{"class":704,"line":778},[702,6074,6075],{"class":715},"  --upload-file",[702,6077,6078],{"class":722}," mail.txt\n",[16,6080,6081,6082,6085,6086,1065],{},"成功时，SMTP 会返回 ",[24,6083,6084],{},"250","。响应中包含的 Message-ID 可以用来关联 Email Service 控制台里的发送日志。完整格式可对照",[163,6087,6090],{"href":6088,"rel":6089},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fexamples\u002Femail-sending\u002Fsmtp\u002F",[167],"Cloudflare 的 cURL 示例",[16,6092,6093],{},"如果这里失败，先不要开始改 Nodemailer：",[373,6095,6096,6104,6110,6116],{},[177,6097,6098,6101,6102,2039],{},[24,6099,6100],{},"535","：Token 无效、过期或缺少 ",[24,6103,5834],{},[177,6105,6106,6109],{},[24,6107,6108],{},"550","：发件地址或发件域没有被允许；",[177,6111,6112,6115],{},[24,6113,6114],{},"552","：邮件超过大小限制；",[177,6117,6118,6121,6122,6125],{},[24,6119,6120],{},"421"," \u002F ",[24,6123,6124],{},"451","：临时故障或限流，等待后重试。",[64,6127,6129],{"id":6128},"在-nodejs-中使用-nodemailer","在 Node.js 中使用 Nodemailer",[16,6131,6132],{},"下面的方案适用于 Node.js、Docker、VPS、传统 Serverless Function，或运行在 Node 兼容环境的 Nitro 服务端。安装依赖：",[68,6134,6136],{"className":938,"code":6135,"language":940,"meta":74,"style":74},"pnpm add nodemailer\npnpm add -D @types\u002Fnodemailer\n",[24,6137,6138,6149],{"__ignoreMap":74},[702,6139,6140,6143,6146],{"class":704,"line":705},[702,6141,6142],{"class":947},"pnpm",[702,6144,6145],{"class":722}," add",[702,6147,6148],{"class":722}," nodemailer\n",[702,6150,6151,6153,6155,6157],{"class":704,"line":712},[702,6152,6142],{"class":947},[702,6154,6145],{"class":722},[702,6156,954],{"class":715},[702,6158,6159],{"class":722}," @types\u002Fnodemailer\n",[16,6161,6162],{},"服务端环境变量：",[68,6164,6168],{"className":6165,"code":6166,"language":6167,"meta":74,"style":74},"language-dotenv shiki shiki-themes github-dark github-dark","CF_EMAIL_SMTP_TOKEN=replace-with-a-server-side-secret\nSMTP_FROM=\"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\n","dotenv",[24,6169,6170,6175],{"__ignoreMap":74},[702,6171,6172],{"class":704,"line":705},[702,6173,6174],{},"CF_EMAIL_SMTP_TOKEN=replace-with-a-server-side-secret\n",[702,6176,6177],{"class":704,"line":712},[702,6178,6179],{},"SMTP_FROM=\"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\n",[16,6181,6182],{},"创建 Transport：",[68,6184,6188],{"className":6185,"code":6186,"language":6187,"meta":74,"style":74},"language-ts shiki shiki-themes github-dark github-dark","import nodemailer from 'nodemailer'\n\nconst transporter = nodemailer.createTransport({\n  host: 'smtp.mx.cloudflare.net',\n  port: 465,\n  secure: true,\n  auth: {\n    user: 'api_token',\n    pass: process.env.CF_EMAIL_SMTP_TOKEN,\n  },\n  connectionTimeout: 10_000,\n  greetingTimeout: 10_000,\n  socketTimeout: 30_000,\n})\n\nconst info = await transporter.sendMail({\n  from: process.env.SMTP_FROM,\n  to: 'recipient@example.com',\n  subject: 'Welcome to Signal Log',\n  text: 'Your account is ready.',\n  html: '\u003Cp>Your account is ready.\u003C\u002Fp>',\n})\n\nconsole.info('Email accepted', {\n  messageId: info.messageId,\n  accepted: info.accepted.length,\n  rejected: info.rejected.length,\n})\n","ts",[24,6189,6190,6204,6208,6228,6238,6247,6256,6261,6271,6280,6285,6295,6304,6314,6319,6323,6343,6353,6363,6373,6383,6393,6397,6401,6418,6423,6433,6442],{"__ignoreMap":74},[702,6191,6192,6195,6198,6201],{"class":704,"line":705},[702,6193,6194],{"class":1949},"import",[702,6196,6197],{"class":708}," nodemailer ",[702,6199,6200],{"class":1949},"from",[702,6202,6203],{"class":722}," 'nodemailer'\n",[702,6205,6206],{"class":704,"line":712},[702,6207,2738],{"emptyLinePlaceholder":1696},[702,6209,6210,6213,6216,6219,6222,6225],{"class":704,"line":729},[702,6211,6212],{"class":1949},"const",[702,6214,6215],{"class":715}," transporter",[702,6217,6218],{"class":1949}," =",[702,6220,6221],{"class":708}," nodemailer.",[702,6223,6224],{"class":947},"createTransport",[702,6226,6227],{"class":708},"({\n",[702,6229,6230,6233,6236],{"class":704,"line":742},[702,6231,6232],{"class":708},"  host: ",[702,6234,6235],{"class":722},"'smtp.mx.cloudflare.net'",[702,6237,726],{"class":708},[702,6239,6240,6243,6245],{"class":704,"line":755},[702,6241,6242],{"class":708},"  port: ",[702,6244,5804],{"class":715},[702,6246,726],{"class":708},[702,6248,6249,6252,6254],{"class":704,"line":778},[702,6250,6251],{"class":708},"  secure: ",[702,6253,750],{"class":715},[702,6255,726],{"class":708},[702,6257,6258],{"class":704,"line":787},[702,6259,6260],{"class":708},"  auth: {\n",[702,6262,6263,6266,6269],{"class":704,"line":795},[702,6264,6265],{"class":708},"    user: ",[702,6267,6268],{"class":722},"'api_token'",[702,6270,726],{"class":708},[702,6272,6273,6276,6278],{"class":704,"line":807},[702,6274,6275],{"class":708},"    pass: process.env.",[702,6277,6045],{"class":715},[702,6279,726],{"class":708},[702,6281,6282],{"class":704,"line":815},[702,6283,6284],{"class":708},"  },\n",[702,6286,6287,6290,6293],{"class":704,"line":836},[702,6288,6289],{"class":708},"  connectionTimeout: ",[702,6291,6292],{"class":715},"10_000",[702,6294,726],{"class":708},[702,6296,6297,6300,6302],{"class":704,"line":842},[702,6298,6299],{"class":708},"  greetingTimeout: ",[702,6301,6292],{"class":715},[702,6303,726],{"class":708},[702,6305,6306,6309,6312],{"class":704,"line":853},[702,6307,6308],{"class":708},"  socketTimeout: ",[702,6310,6311],{"class":715},"30_000",[702,6313,726],{"class":708},[702,6315,6316],{"class":704,"line":859},[702,6317,6318],{"class":708},"})\n",[702,6320,6321],{"class":704,"line":865},[702,6322,2738],{"emptyLinePlaceholder":1696},[702,6324,6325,6327,6330,6332,6335,6338,6341],{"class":704,"line":2849},[702,6326,6212],{"class":1949},[702,6328,6329],{"class":715}," info",[702,6331,6218],{"class":1949},[702,6333,6334],{"class":1949}," await",[702,6336,6337],{"class":708}," transporter.",[702,6339,6340],{"class":947},"sendMail",[702,6342,6227],{"class":708},[702,6344,6345,6348,6351],{"class":704,"line":2855},[702,6346,6347],{"class":708},"  from: process.env.",[702,6349,6350],{"class":715},"SMTP_FROM",[702,6352,726],{"class":708},[702,6354,6355,6358,6361],{"class":704,"line":2860},[702,6356,6357],{"class":708},"  to: ",[702,6359,6360],{"class":722},"'recipient@example.com'",[702,6362,726],{"class":708},[702,6364,6365,6368,6371],{"class":704,"line":2869},[702,6366,6367],{"class":708},"  subject: ",[702,6369,6370],{"class":722},"'Welcome to Signal Log'",[702,6372,726],{"class":708},[702,6374,6375,6378,6381],{"class":704,"line":2882},[702,6376,6377],{"class":708},"  text: ",[702,6379,6380],{"class":722},"'Your account is ready.'",[702,6382,726],{"class":708},[702,6384,6385,6388,6391],{"class":704,"line":2890},[702,6386,6387],{"class":708},"  html: ",[702,6389,6390],{"class":722},"'\u003Cp>Your account is ready.\u003C\u002Fp>'",[702,6392,726],{"class":708},[702,6394,6395],{"class":704,"line":2898},[702,6396,6318],{"class":708},[702,6398,6399],{"class":704,"line":2906},[702,6400,2738],{"emptyLinePlaceholder":1696},[702,6402,6403,6406,6409,6412,6415],{"class":704,"line":2914},[702,6404,6405],{"class":708},"console.",[702,6407,6408],{"class":947},"info",[702,6410,6411],{"class":708},"(",[702,6413,6414],{"class":722},"'Email accepted'",[702,6416,6417],{"class":708},", {\n",[702,6419,6420],{"class":704,"line":3432},[702,6421,6422],{"class":708},"  messageId: info.messageId,\n",[702,6424,6425,6428,6431],{"class":704,"line":3442},[702,6426,6427],{"class":708},"  accepted: info.accepted.",[702,6429,6430],{"class":715},"length",[702,6432,726],{"class":708},[702,6434,6435,6438,6440],{"class":704,"line":1699},[702,6436,6437],{"class":708},"  rejected: info.rejected.",[702,6439,6430],{"class":715},[702,6441,726],{"class":708},[702,6443,6444],{"class":704,"line":3454},[702,6445,6318],{"class":708},[16,6447,6448],{},[214,6449],{"alt":6450,"src":6451},"Cloudflare 控制台中的 Nodemailer 示例，密码从环境变量读取","\u002Fmedia\u002Fposts\u002Fcloudflare-smtp-email-sending-guide\u002Fdashboard-smtp-node-v1.webp",[16,6453,6454],{},[221,6455,6456,6457,6460],{},"控制台示例使用 ",[24,6458,6459],{},"process.env.CLOUDFLARE_API_TOKEN","。实际项目可以使用更明确的变量名，但必须保证它只存在于服务端。",[16,6462,6463],{},"生产代码还应补上输入边界：",[68,6465,6467],{"className":6185,"code":6466,"language":6187,"meta":74,"style":74},"const allowedFrom = new Set([\n  'noreply@sparkles-editor.com',\n  'support@sparkles-editor.com',\n])\n\nfunction assertEmailEnvelope(from: string, to: string) {\n  if (!allowedFrom.has(from)) {\n    throw new Error('Sender is not allowed')\n  }\n\n  if (!\u002F^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\u002F.test(to)) {\n    throw new Error('Recipient is invalid')\n  }\n}\n",[24,6468,6469,6487,6494,6501,6506,6510,6542,6562,6580,6584,6588,6648,6663,6667],{"__ignoreMap":74},[702,6470,6471,6473,6476,6478,6481,6484],{"class":704,"line":705},[702,6472,6212],{"class":1949},[702,6474,6475],{"class":715}," allowedFrom",[702,6477,6218],{"class":1949},[702,6479,6480],{"class":1949}," new",[702,6482,6483],{"class":947}," Set",[702,6485,6486],{"class":708},"([\n",[702,6488,6489,6492],{"class":704,"line":712},[702,6490,6491],{"class":722},"  'noreply@sparkles-editor.com'",[702,6493,726],{"class":708},[702,6495,6496,6499],{"class":704,"line":729},[702,6497,6498],{"class":722},"  'support@sparkles-editor.com'",[702,6500,726],{"class":708},[702,6502,6503],{"class":704,"line":742},[702,6504,6505],{"class":708},"])\n",[702,6507,6508],{"class":704,"line":755},[702,6509,2738],{"emptyLinePlaceholder":1696},[702,6511,6512,6515,6518,6520,6523,6526,6529,6532,6535,6537,6539],{"class":704,"line":778},[702,6513,6514],{"class":1949},"function",[702,6516,6517],{"class":947}," assertEmailEnvelope",[702,6519,6411],{"class":708},[702,6521,6200],{"class":6522},"s-3mD",[702,6524,6525],{"class":1949},":",[702,6527,6528],{"class":715}," string",[702,6530,6531],{"class":708},", ",[702,6533,6534],{"class":6522},"to",[702,6536,6525],{"class":1949},[702,6538,6528],{"class":715},[702,6540,6541],{"class":708},") {\n",[702,6543,6544,6547,6550,6553,6556,6559],{"class":704,"line":787},[702,6545,6546],{"class":1949},"  if",[702,6548,6549],{"class":708}," (",[702,6551,6552],{"class":1949},"!",[702,6554,6555],{"class":708},"allowedFrom.",[702,6557,6558],{"class":947},"has",[702,6560,6561],{"class":708},"(from)) {\n",[702,6563,6564,6567,6569,6572,6574,6577],{"class":704,"line":795},[702,6565,6566],{"class":1949},"    throw",[702,6568,6480],{"class":1949},[702,6570,6571],{"class":947}," Error",[702,6573,6411],{"class":708},[702,6575,6576],{"class":722},"'Sender is not allowed'",[702,6578,6579],{"class":708},")\n",[702,6581,6582],{"class":704,"line":807},[702,6583,862],{"class":708},[702,6585,6586],{"class":704,"line":815},[702,6587,2738],{"emptyLinePlaceholder":1696},[702,6589,6590,6592,6594,6596,6599,6602,6605,6607,6610,6613,6617,6619,6621,6623,6625,6629,6631,6633,6635,6638,6640,6642,6645],{"class":704,"line":836},[702,6591,6546],{"class":1949},[702,6593,6549],{"class":708},[702,6595,6552],{"class":1949},[702,6597,6598],{"class":722},"\u002F",[702,6600,6601],{"class":1949},"^",[702,6603,6604],{"class":715},"[",[702,6606,6601],{"class":1949},[702,6608,6609],{"class":715},"\\s@]",[702,6611,6612],{"class":1949},"+",[702,6614,6616],{"class":6615},"se7xv","@",[702,6618,6604],{"class":715},[702,6620,6601],{"class":1949},[702,6622,6609],{"class":715},[702,6624,6612],{"class":1949},[702,6626,6628],{"class":6627},"s-Lb2","\\.",[702,6630,6604],{"class":715},[702,6632,6601],{"class":1949},[702,6634,6609],{"class":715},[702,6636,6637],{"class":1949},"+$",[702,6639,6598],{"class":722},[702,6641,2937],{"class":708},[702,6643,6644],{"class":947},"test",[702,6646,6647],{"class":708},"(to)) {\n",[702,6649,6650,6652,6654,6656,6658,6661],{"class":704,"line":842},[702,6651,6566],{"class":1949},[702,6653,6480],{"class":1949},[702,6655,6571],{"class":947},[702,6657,6411],{"class":708},[702,6659,6660],{"class":722},"'Recipient is invalid'",[702,6662,6579],{"class":708},[702,6664,6665],{"class":704,"line":853},[702,6666,862],{"class":708},[702,6668,6669],{"class":704,"line":859},[702,6670,868],{"class":708},[16,6672,6673,6674,45,6676,45,6678,6681,6682,1533,6685,6688],{},"不要提供一个接收任意 ",[24,6675,6200],{},[24,6677,6534],{},[24,6679,6680],{},"subject"," 和 HTML 的公开 API。否则应用很容易变成开放邮件中继，或者被用来发送钓鱼邮件。更安全的接口只接收业务参数，例如 ",[24,6683,6684],{},"userId",[24,6686,6687],{},"templateId","；服务端再从数据库取已验证地址，并从白名单模板生成标题与正文。",[64,6690,6692],{"id":6691},"nuxt-部署在-cloudflare-workers-时怎么办","Nuxt 部署在 Cloudflare Workers 时怎么办",[16,6694,6695],{},"这里需要特别区分运行时。",[16,6697,6698,6699,6702],{},"Nodemailer 是面向传统 Node SMTP Socket 的库。Cloudflare Workers 上的 Nuxt 应用已经运行在 Cloudflare 网络内，通常更适合使用 ",[24,6700,6701],{},"send_email"," binding：",[68,6704,6706],{"className":6185,"code":6705,"language":6187,"meta":74,"style":74},"interface Env {\n  EMAIL: SendEmail\n}\n\nawait env.EMAIL.send({\n  from: 'noreply@sparkles-editor.com',\n  to: 'recipient@example.com',\n  subject: 'Verification code',\n  text: 'Your code is 123456',\n})\n",[24,6707,6708,6719,6729,6733,6737,6755,6765,6773,6782,6791],{"__ignoreMap":74},[702,6709,6710,6713,6716],{"class":704,"line":705},[702,6711,6712],{"class":1949},"interface",[702,6714,6715],{"class":947}," Env",[702,6717,6718],{"class":708}," {\n",[702,6720,6721,6724,6726],{"class":704,"line":712},[702,6722,6723],{"class":6522},"  EMAIL",[702,6725,6525],{"class":1949},[702,6727,6728],{"class":947}," SendEmail\n",[702,6730,6731],{"class":704,"line":729},[702,6732,868],{"class":708},[702,6734,6735],{"class":704,"line":742},[702,6736,2738],{"emptyLinePlaceholder":1696},[702,6738,6739,6742,6745,6748,6750,6753],{"class":704,"line":755},[702,6740,6741],{"class":1949},"await",[702,6743,6744],{"class":708}," env.",[702,6746,6747],{"class":715},"EMAIL",[702,6749,2937],{"class":708},[702,6751,6752],{"class":947},"send",[702,6754,6227],{"class":708},[702,6756,6757,6760,6763],{"class":704,"line":778},[702,6758,6759],{"class":708},"  from: ",[702,6761,6762],{"class":722},"'noreply@sparkles-editor.com'",[702,6764,726],{"class":708},[702,6766,6767,6769,6771],{"class":704,"line":787},[702,6768,6357],{"class":708},[702,6770,6360],{"class":722},[702,6772,726],{"class":708},[702,6774,6775,6777,6780],{"class":704,"line":795},[702,6776,6367],{"class":708},[702,6778,6779],{"class":722},"'Verification code'",[702,6781,726],{"class":708},[702,6783,6784,6786,6789],{"class":704,"line":807},[702,6785,6377],{"class":708},[702,6787,6788],{"class":722},"'Your code is 123456'",[702,6790,726],{"class":708},[702,6792,6793],{"class":704,"line":815},[702,6794,6318],{"class":708},[16,6796,6797],{},"这样不需要在 Worker 中保存 SMTP API Token，权限也由 binding 管理。",[16,6799,6800],{},"选择原则很简单：",[85,6802,6803,6813],{},[88,6804,6805],{},[91,6806,6807,6810],{},[94,6808,6809],{},"场景",[94,6811,6812],{},"推荐入口",[107,6814,6815,6826,6834,6842,6850],{},[91,6816,6817,6820],{},[112,6818,6819],{},"Nuxt \u002F API 已经部署在 Cloudflare Workers",[112,6821,6822,6823,6825],{},"Workers ",[24,6824,6701],{}," binding",[91,6827,6828,6831],{},[112,6829,6830],{},"外部服务支持 HTTPS API，且便于管理 Token",[112,6832,6833],{},"REST API",[91,6835,6836,6839],{},[112,6837,6838],{},"现有 Node、Python、PHP 应用已经支持 SMTP",[112,6840,6841],{},"认证 SMTP",[91,6843,6844,6847],{},[112,6845,6846],{},"打印机、旧式 CMS 或第三方工具只能填 SMTP",[112,6848,6849],{},"认证 SMTP，但先确认能使用 465 SMTPS",[91,6851,6852,6855],{},[112,6853,6854],{},"浏览器前端直接发送",[112,6856,6857],{},"不允许；必须经过受控服务端",[16,6859,6860],{},"三个入口最后进入同一套 Cloudflare 投递管线、日志和域名认证系统。SMTP 的价值是兼容性，不是要求所有新服务都退回 Socket 协议。",[64,6862,6864],{"id":6863},"python-最小示例","Python 最小示例",[16,6866,6867],{},"Python 标准库无需额外依赖：",[68,6869,6873],{"className":6870,"code":6871,"language":6872,"meta":74,"style":74},"language-python shiki shiki-themes github-dark github-dark","import os\nimport smtplib\nfrom email.message import EmailMessage\n\ntoken = os.environ[\"CF_EMAIL_SMTP_TOKEN\"]\n\nmessage = EmailMessage()\nmessage[\"From\"] = \"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\nmessage[\"To\"] = \"recipient@example.com\"\nmessage[\"Subject\"] = \"Cloudflare SMTP test\"\nmessage.set_content(\"This email was sent over implicit TLS.\")\n\nwith smtplib.SMTP_SSL(\"smtp.mx.cloudflare.net\", 465, timeout=30) as smtp:\n    smtp.login(\"api_token\", token)\n    smtp.send_message(message)\n","python",[24,6874,6875,6880,6885,6890,6894,6899,6903,6908,6913,6918,6923,6928,6932,6937,6942],{"__ignoreMap":74},[702,6876,6877],{"class":704,"line":705},[702,6878,6879],{},"import os\n",[702,6881,6882],{"class":704,"line":712},[702,6883,6884],{},"import smtplib\n",[702,6886,6887],{"class":704,"line":729},[702,6888,6889],{},"from email.message import EmailMessage\n",[702,6891,6892],{"class":704,"line":742},[702,6893,2738],{"emptyLinePlaceholder":1696},[702,6895,6896],{"class":704,"line":755},[702,6897,6898],{},"token = os.environ[\"CF_EMAIL_SMTP_TOKEN\"]\n",[702,6900,6901],{"class":704,"line":778},[702,6902,2738],{"emptyLinePlaceholder":1696},[702,6904,6905],{"class":704,"line":787},[702,6906,6907],{},"message = EmailMessage()\n",[702,6909,6910],{"class":704,"line":795},[702,6911,6912],{},"message[\"From\"] = \"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\n",[702,6914,6915],{"class":704,"line":807},[702,6916,6917],{},"message[\"To\"] = \"recipient@example.com\"\n",[702,6919,6920],{"class":704,"line":815},[702,6921,6922],{},"message[\"Subject\"] = \"Cloudflare SMTP test\"\n",[702,6924,6925],{"class":704,"line":836},[702,6926,6927],{},"message.set_content(\"This email was sent over implicit TLS.\")\n",[702,6929,6930],{"class":704,"line":842},[702,6931,2738],{"emptyLinePlaceholder":1696},[702,6933,6934],{"class":704,"line":853},[702,6935,6936],{},"with smtplib.SMTP_SSL(\"smtp.mx.cloudflare.net\", 465, timeout=30) as smtp:\n",[702,6938,6939],{"class":704,"line":859},[702,6940,6941],{},"    smtp.login(\"api_token\", token)\n",[702,6943,6944],{"class":704,"line":865},[702,6945,6946],{},"    smtp.send_message(message)\n",[16,6948,6949,6950,6953,6954,1065],{},"关键是 ",[24,6951,6952],{},"SMTP_SSL","，而不是先明文连接再调用 ",[24,6955,6956],{},"starttls()",[64,6958,6959],{"id":6959},"限额与附件",[16,6961,6962,6963,3217],{},"根据当前",[163,6964,6967],{"href":6965,"rel":6966},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Flimits\u002F",[167],"Email Service 平台限制",[373,6969,6970,6973,6979,6982,6985,6992,6998,7001],{},[177,6971,6972],{},"每封邮件最多合计 50 个收件人；",[177,6974,6975,6976,2039],{},"SMTP 会话最多接受 50 次 ",[24,6977,6978],{},"RCPT TO",[177,6980,6981],{},"普通邮件总大小上限为 5 MiB；",[177,6983,6984],{},"发往“已验证目标地址”时，总大小上限可到 25 MiB；",[177,6986,6987,6988,6991],{},"SMTP ",[24,6989,6990],{},"AUTH"," 超时为 30 秒；",[177,6993,6987,6994,6997],{},[24,6995,6996],{},"DATA"," 超时为 300 秒；",[177,6999,7000],{},"自定义邮件头合计上限为 16 KiB；",[177,7002,7003],{},"Subject 上限为 998 个字符。",[16,7005,7006],{},"邮件总大小包含 MIME 编码后的正文和附件。二进制附件经过 Base64 后通常会膨胀约三分之一，因此不要把一个 4.9 MiB 文件误认为一定能塞进 5 MiB 邮件。",[16,7008,7009],{},"控制台显示的每日配额可能因账户、Beta 阶段和使用情况而变化。不要把某次看到的数字写死在业务逻辑里；应读取当前控制台配额，并在自己的发送队列中保留余量。",[64,7011,7012],{"id":7012},"错误处理和重试",[16,7014,7015],{},"SMTP 的返回码已经表达了“要不要重试”：",[85,7017,7018,7029],{},[88,7019,7020],{},[91,7021,7022,7025,7027],{},[94,7023,7024],{},"返回码",[94,7026,1836],{},[94,7028,256],{},[107,7030,7031,7043,7057,7069,7081],{},[91,7032,7033,7037,7040],{},[112,7034,7035],{},[24,7036,6084],{},[112,7038,7039],{},"已被 Cloudflare 接受",[112,7041,7042],{},"记录 Message-ID，不再重复发送",[91,7044,7045,7051,7054],{},[112,7046,7047,6121,7049],{},[24,7048,6120],{},[24,7050,6124],{},[112,7052,7053],{},"临时故障或限流",[112,7055,7056],{},"指数退避并加入随机抖动",[91,7058,7059,7063,7066],{},[112,7060,7061],{},[24,7062,6100],{},[112,7064,7065],{},"认证失败",[112,7067,7068],{},"停止自动重试，检查 Token",[91,7070,7071,7075,7078],{},[112,7072,7073],{},[24,7074,6108],{},[112,7076,7077],{},"发件域、地址或收件策略被拒绝",[112,7079,7080],{},"修正配置或数据后再发",[91,7082,7083,7087,7090],{},[112,7084,7085],{},[24,7086,6114],{},[112,7088,7089],{},"邮件过大",[112,7091,7092],{},"缩小正文或附件",[16,7094,7095],{},"推荐的重试节奏可以是 30 秒、2 分钟、10 分钟、30 分钟，最多尝试固定次数。每次业务发送必须有幂等键，例如：",[68,7097,7100],{"className":7098,"code":7099,"language":73,"meta":74},[71],"password-reset:{userId}:{resetRequestId}\ninvoice:{invoiceId}:issued\n",[24,7101,7099],{"__ignoreMap":74},[16,7103,7104,7105,7107],{},"临时错误进入队列，永久错误进入人工检查或失败状态。不要把所有非 ",[24,7106,6084],{}," 都立即重试，也不要让同一个验证码因为网络抖动发送十次。",[64,7109,7111],{"id":7110},"投递率不是返回-250-就结束","投递率不是“返回 250 就结束”",[16,7113,7114,7116],{},[24,7115,6084],{}," 代表 Cloudflare 接受了消息，不代表最终收件箱一定出现了邮件。生产环境还要观察：",[373,7118,7119,7122,7125,7128,7131,7134],{},[177,7120,7121],{},"Delivery、Bounce、Deferred 与 Suppression；",[177,7123,7124],{},"Gmail、Outlook、QQ 邮箱等主要目的地的结果；",[177,7126,7127],{},"From、Return-Path、DKIM 和 DMARC 对齐；",[177,7129,7130],{},"硬退信地址是否立即停止发送；",[177,7132,7133],{},"投诉率与无效地址增长；",[177,7135,7136],{},"验证码和密码重置是否设置明确有效期。",[16,7138,7139],{},"事务邮件应该使用稳定、可识别的发件人，并同时提供纯文本与 HTML。不要伪造 Reply-To，不要用验证码通道发送营销群发，也不要让用户输入直接进入邮件头，避免 Header Injection。",[64,7141,7142],{"id":7142},"上线检查清单",[373,7144,7147,7156,7162,7170,7179,7185,7191,7197,7203,7213,7225,7231],{"className":7145},[7146],"contains-task-list",[177,7148,7151,7155],{"className":7149},[7150],"task-list-item",[7152,7153],"input",{"disabled":1696,"type":7154},"checkbox"," 发件域在 Email Sending 中为“已配置”",[177,7157,7159,7161],{"className":7158},[7150],[7152,7160],{"disabled":1696,"type":7154}," 使用 465 + 隐式 TLS",[177,7163,7165,7167,7168],{"className":7164},[7150],[7152,7166],{"disabled":1696,"type":7154}," 用户名严格为 ",[24,7169,5823],{},[177,7171,7173,7175,7176,7178],{"className":7172},[7150],[7152,7174],{"disabled":1696,"type":7154}," Token 只有 ",[24,7177,5834],{}," 最小权限",[177,7180,7182,7184],{"className":7181},[7150],[7152,7183],{"disabled":1696,"type":7154}," Token 位于服务端 Secret，不进入 Git、日志和前端",[177,7186,7188,7190],{"className":7187},[7150],[7152,7189],{"disabled":1696,"type":7154}," From 地址来自服务端白名单",[177,7192,7194,7196],{"className":7193},[7150],[7152,7195],{"disabled":1696,"type":7154}," 收件地址经过验证、去重和长度限制",[177,7198,7200,7202],{"className":7199},[7150],[7152,7201],{"disabled":1696,"type":7154}," HTML 使用固定模板并保留纯文本版本",[177,7204,7206,2603,7208,6121,7210,7212],{"className":7205},[7150],[7152,7207],{"disabled":1696,"type":7154},[24,7209,6120],{},[24,7211,6124],{}," 使用带抖动的指数退避",[177,7214,7216,2603,7218,6121,7220,6121,7222,7224],{"className":7215},[7150],[7152,7217],{"disabled":1696,"type":7154},[24,7219,6100],{},[24,7221,6108],{},[24,7223,6114],{}," 不做盲目重试",[177,7226,7228,7230],{"className":7227},[7150],[7152,7229],{"disabled":1696,"type":7154}," 记录 Message-ID、模板名与业务幂等键，不记录 Token",[177,7232,7234,7236],{"className":7233},[7150],[7152,7235],{"disabled":1696,"type":7154}," 监控退信、抑制、投诉和每日配额",[64,7238,7239],{"id":7239},"最后的选择",[16,7241,7242],{},"如果你有一个已经稳定运行的 Node、Python 或 PHP 应用，Cloudflare SMTP 的优势很直接：只换主机、端口和认证信息，就能接入 Cloudflare 的投递与日志体系。",[16,7244,7245],{},"如果应用原生运行在 Workers，则没有必要绕一圈模拟传统服务器，优先使用 Email binding。SMTP 是一座兼容旧有生态的桥，而不是唯一入口。",[156,7247,1561],{"id":1561},[373,7249,7250,7256,7262,7268],{},[177,7251,7252],{},[163,7253,7255],{"href":5759,"rel":7254},[167],"Cloudflare Email Service：发送邮件入门",[177,7257,7258],{},[163,7259,7261],{"href":5765,"rel":7260},[167],"Cloudflare Email Service：SMTP API 参考",[177,7263,7264],{},[163,7265,7267],{"href":6088,"rel":7266},[167],"Cloudflare Email Service：SMTP 示例",[177,7269,7270],{},[163,7271,7273],{"href":6965,"rel":7272},[167],"Cloudflare Email Service：平台限制",[1658,7275,7276],{},"html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .se7xv, html code.shiki .se7xv{--shiki-default:#DBEDFF;--shiki-dark:#DBEDFF}html pre.shiki code .s-Lb2, html code.shiki .s-Lb2{--shiki-default:#85E89D;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}",{"title":74,"searchDepth":729,"depth":729,"links":7278},[7279,7280,7284,7285,7286,7287,7288,7289,7290,7291,7292],{"id":5770,"depth":712,"text":5770},{"id":5881,"depth":712,"text":5881,"children":7281},[7282,7283],{"id":5884,"depth":729,"text":5885},{"id":5900,"depth":729,"text":5901},{"id":5966,"depth":712,"text":5967},{"id":6128,"depth":712,"text":6129},{"id":6691,"depth":712,"text":6692},{"id":6863,"depth":712,"text":6864},{"id":6959,"depth":712,"text":6959},{"id":7012,"depth":712,"text":7012},{"id":7110,"depth":712,"text":7111},{"id":7142,"depth":712,"text":7142},{"id":7239,"depth":712,"text":7239,"children":7293},[7294],{"id":1561,"depth":729,"text":1561},"一篇独立、可落地的 Cloudflare Email Service SMTP 教程，覆盖域名接入、API Token、cURL、Nodemailer、Python、限额、错误码与生产安全。",{"src":7297,"alt":7298,"caption":7299,"width":7300,"height":7301},"\u002Fmedia\u002Fposts\u002Fcloudflare-smtp-email-sending-guide\u002Fcover-v1.webp","一封事务邮件通过加密 SMTP 连接进入全球邮件投递网络","把现有应用接到 Cloudflare Email Service，不一定要重写发送层；标准 SMTP 就是一条兼容路径",1672,941,{},"\u002Fblog\u002Fcloudflare-smtp-email-sending-guide","2026-07-31",{"title":5728,"description":7295},"blog\u002Fcloudflare-smtp-email-sending-guide",[4540,7308,7309,7310],"SMTP","Email","Nodemailer","ioq36qTQ7AGVlLQpfduYQZC0FCJygbgoBDEtRMP7pJs",{"id":7313,"title":7314,"accent":2449,"body":7315,"description":9674,"draft":1692,"experience":1693,"extension":1694,"featured":1696,"image":9675,"meta":9681,"navigation":1696,"path":9682,"publishedAt":9683,"readingTime":2898,"seo":9684,"stem":9685,"tags":9686,"updatedAt":1693,"video":9689,"__hash__":9694,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Fcloudflare-email-service-send-receive-guide.md","Cloudflare Email Service 收发实战：从 Email Routing 到 Workers、API 与 SMTP",{"type":13,"value":7316,"toc":9641},[7317,7324,7327,7338,7358,7365,7368,7373,7377,7380,7430,7433,7439,7442,7448,7454,7459,7463,7466,7470,7473,7476,7498,7502,7505,7508,7522,7525,7529,7532,7535,7538,7549,7552,7560,7567,7584,7587,7597,7600,7606,7609,7613,7616,7631,7634,7641,7650,7982,7992,8085,8100,8104,8110,8240,8243,8260,8264,8290,8293,8313,8316,8320,8323,8330,8398,8404,8407,8426,8432,8436,8639,8645,8649,8652,8963,8966,8972,8977,8981,8984,9034,9037,9040,9059,9062,9065,9187,9190,9193,9196,9199,9205,9208,9231,9238,9241,9244,9247,9253,9256,9262,9266,9269,9293,9296,9316,9319,9323,9333,9339,9342,9345,9363,9366,9425,9428,9431,9463,9467,9470,9490,9493,9510,9513,9516,9519,9522,9525,9545,9548,9550,9638],[16,7318,7319,7320,7323],{},"很长一段时间里，Cloudflare 与邮件的关系主要停留在两件事上：管理 DNS，以及用 Email Routing 把 ",[24,7321,7322],{},"support@example.com"," 转发到一个真实邮箱。",[16,7325,7326],{},"2026 年，这个边界明显向前移动了。",[16,7328,7329,7330,7333,7334,7337],{},"Cloudflare 在 4 月把 ",[20,7331,7332],{},"Email Sending"," 推进公开 Beta，让 Workers 可以直接调用 ",[24,7335,7336],{},"env.EMAIL.send()"," 发送事务邮件；6 月又加入了认证 SMTP。加上原有的 Email Routing 与 Email Workers，现在一套 Cloudflare Email Service 已经可以覆盖：",[373,7339,7340,7343,7346,7349,7352,7355],{},[177,7341,7342],{},"接收发往自有域名的邮件；",[177,7344,7345],{},"转发到经过验证的真实邮箱；",[177,7347,7348],{},"把来信交给 Worker 解析、过滤、入库或触发自动化；",[177,7350,7351],{},"在原邮件线程中回复；",[177,7353,7354],{},"从 Worker、REST API 或 SMTP 发送事务邮件；",[177,7356,7357],{},"查看发送日志、退信、抑制与投递指标。",[16,7359,7360,7361,7364],{},"这篇文章以 ",[20,7362,7363],{},"2026 年 7 月 28 日","的官方能力为准，梳理 Cloudflare 邮件收发的完整模型，并给出一套适合 Nuxt、Workers 与 D1 项目的实现思路。",[16,7366,7367],{},"先说结论：",[51,7369,7370],{},[16,7371,7372],{},"Cloudflare Email Service 不是传统的 IMAP \u002F POP3 邮箱，也不是让你在网页里收信的企业邮箱套件。它更像一组“邮件入口、发送通道和可编程事件”，适合验证码、密码重置、订单通知、系统告警、支持工单和邮件驱动的自动化。",[64,7374,7376],{"id":7375},"先把接收和发送分开","先把“接收”和“发送”分开",[16,7378,7379],{},"邮件收发看起来是一件事，在 Cloudflare 里却是两条独立链路。",[85,7381,7382,7398],{},[88,7383,7384],{},[91,7385,7386,7389,7392,7395],{},[94,7387,7388],{},"方向",[94,7390,7391],{},"Cloudflare 产品",[94,7393,7394],{},"入口",[94,7396,7397],{},"Worker 能做什么",[107,7399,7400,7414],{},[91,7401,7402,7405,7408,7411],{},[112,7403,7404],{},"接收",[112,7406,7407],{},"Email Routing \u002F Email Workers",[112,7409,7410],{},"外部邮件服务器连接 Cloudflare MX",[112,7412,7413],{},"转发、回复、拒收、解析、写入 D1\u002FR2、投递 Queue",[91,7415,7416,7419,7421,7424],{},[112,7417,7418],{},"发送",[112,7420,7332],{},[112,7422,7423],{},"Workers binding、REST API、认证 SMTP",[112,7425,7426,7427],{},"发送文本\u002FHTML、附件、自定义头，获得 ",[24,7428,7429],{},"messageId",[16,7431,7432],{},"接收链路大致是：",[68,7434,7437],{"className":7435,"code":7436,"language":73,"meta":74},[71],"发送方 SMTP\n  → Cloudflare MX\n  → SPF \u002F DKIM \u002F DMARC 与信誉检查\n  → 匹配地址规则或 Catch-all\n  → 转发到邮箱 \u002F 交给 Worker \u002F 丢弃\n",[24,7438,7436],{"__ignoreMap":74},[16,7440,7441],{},"发送链路则是：",[68,7443,7446],{"className":7444,"code":7445,"language":73,"meta":74},[71],"Nuxt API \u002F Worker \u002F 外部应用\n  → EMAIL binding \u002F REST API \u002F SMTP\n  → 发件域与参数校验\n  → Cloudflare 投递管线\n  → 收件方邮件服务器\n",[24,7447,7445],{"__ignoreMap":74},[16,7449,7450],{},[214,7451],{"alt":7452,"src":7453},"邮件在边缘入口完成检查后，被分发到真实邮箱、Worker 处理节点或拒绝路径","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Finbound-routing-v1.webp",[16,7455,7456],{},[221,7457,7458],{},"这是一张概念图。真实判断还包含 SMTP 会话、邮件认证、规则匹配和 Worker 执行限制。",[64,7460,7462],{"id":7461},"_2026-年的产品边界与价格","2026 年的产品边界与价格",[16,7464,7465],{},"Cloudflare 当前把接收与发送放在同一个 Email Service 下，但两部分的可用计划不同。",[156,7467,7469],{"id":7468},"email-routing接收与转发","Email Routing：接收与转发",[16,7471,7472],{},"Email Routing 在 Workers Free 和 Workers Paid 都可以使用。官方价格页把入站邮件数量标为不限量，但如果邮件被交给 Worker，仍然会消耗对应的 Worker 请求与计算资源。",[16,7474,7475],{},"它适合：",[373,7477,7478,7485,7488,7495],{},[177,7479,7480,7481,7484],{},"把 ",[24,7482,7483],{},"support@yourdomain.com"," 转发到现有个人邮箱；",[177,7486,7487],{},"为不同网站创建独立的联系地址；",[177,7489,7490,7491,7494],{},"用 ",[24,7492,7493],{},"orders+shop-a@yourdomain.com"," 这样的子地址区分来源；",[177,7496,7497],{},"把来信交给 Worker 转换成工单、Webhook 或数据库记录。",[156,7499,7501],{"id":7500},"email-sending事务邮件","Email Sending：事务邮件",[16,7503,7504],{},"Email Sending 当前仍是公开 Beta。",[16,7506,7507],{},"向任意收件人发送需要 Workers Paid 计划。按照 2026 年 7 月的官方价格：",[373,7509,7510,7513,7516,7519],{},[177,7511,7512],{},"每个账户每月包含 3,000 封出站邮件；",[177,7514,7515],{},"超出后每 1,000 封为 0.35 美元；",[177,7517,7518],{},"发往账户内“已验证目标地址”的邮件始终免费，不计入包含额度；",[177,7520,7521],{},"邮件被 Email Service 接受后，即使随后发生硬退信，通常也会计入额度；在 API 边界直接拒绝的请求不会计入。",[16,7523,7524],{},"价格和 Beta 状态都可能继续调整，真正上线前应重新检查官方价格页，而不是把本文数字长期写死在预算模型里。",[64,7526,7528],{"id":7527},"第一步正确接入域名","第一步：正确接入域名",[16,7530,7531],{},"Email Service 要求域名使用 Cloudflare DNS。发送与接收会创建两组用途不同的 DNS 记录。",[156,7533,7534],{"id":7534},"接收邮件使用根域记录",[16,7536,7537],{},"启用 Email Routing 后，Cloudflare 会在域名根节点配置：",[373,7539,7540,7543,7546],{},[177,7541,7542],{},"MX：把来信送到 Cloudflare 的邮件入口；",[177,7544,7545],{},"SPF：授权 Cloudflare 转发邮件；",[177,7547,7548],{},"DKIM：为转发链路提供签名。",[16,7550,7551],{},"这些记录负责的是“别人发到你的域名”。",[156,7553,7555,7556,7559],{"id":7554},"发送邮件使用-cf-bounce-记录","发送邮件使用 ",[24,7557,7558],{},"cf-bounce"," 记录",[16,7561,7562,7563,7566],{},"启用 Email Sending 时，Cloudflare 会为 ",[24,7564,7565],{},"cf-bounce.yourdomain.com"," 配置：",[373,7568,7569,7572,7575,7578],{},[177,7570,7571],{},"MX：处理退信；",[177,7573,7574],{},"SPF：授权 Cloudflare 代域名发送；",[177,7576,7577],{},"DKIM：验证邮件内容未在途中被修改；",[177,7579,7580,7583],{},[24,7581,7582],{},"_dmarc.yourdomain.com","：声明认证失败时的处理策略。",[16,7585,7586],{},"发送和接收使用不同的 DKIM selector。看到两条 DKIM 记录并不意味着重复配置。",[16,7588,7589,7590,7593,7594,1065],{},"如果域名已有 Google Workspace、Microsoft 365 或其他邮件服务，尤其要检查 MX 和 SPF 是否冲突。一个域名只能有一条有效 SPF 策略；需要同时授权多个服务时，应合并 ",[24,7591,7592],{},"include","，而不是再创建第二条 ",[24,7595,7596],{},"v=spf1",[16,7598,7599],{},"我的建议是把不同信誉用途拆到子域：",[68,7601,7604],{"className":7602,"code":7603,"language":73,"meta":74},[71],"noreply@example.com          账户验证码与密码重置\nnotify@mail.example.com      产品通知\nsupport@example.com          人工支持与来信处理\n",[24,7605,7603],{"__ignoreMap":74},[16,7607,7608],{},"事务通知、人工往来与未来可能出现的批量邮件互不拖累，故障和迁移边界也更清晰。",[64,7610,7612],{"id":7611},"接收邮件从转发规则到-email-worker","接收邮件：从转发规则到 Email Worker",[16,7614,7615],{},"最简单的接收方式完全不用代码：",[174,7617,7618,7621,7625,7628],{},[177,7619,7620],{},"在 Email Routing 中添加一个经过验证的目标邮箱；",[177,7622,5973,7623,2039],{},[24,7624,7483],{},[177,7626,7627],{},"选择“发送到邮箱”；",[177,7629,7630],{},"来信会被 Cloudflare 转发到目标地址。",[16,7632,7633],{},"如果需要按主题分类、提取附件、建立工单或自动回复，就把动作改为“发送到 Worker”。",[156,7635,7637,7640],{"id":7636},"email-是入站邮件入口",[24,7638,7639],{},"email()"," 是入站邮件入口",[16,7642,7643,7644,7647,7648,3217],{},"Email Worker 的入口不是 ",[24,7645,7646],{},"fetch()","，而是 ",[24,7649,7639],{},[68,7651,7653],{"className":6185,"code":7652,"language":6187,"meta":74,"style":74},"interface Env {\n  SUPPORT_FORWARD_TO: string\n}\n\nexport default {\n  async email(message, env): Promise\u003Cvoid> {\n    const recipient = message.to.toLowerCase()\n    const subject = message.headers.get('subject') ?? ''\n\n    if (message.rawSize > 10 * 1024 * 1024) {\n      message.setReject('Message is too large for this mailbox')\n      return\n    }\n\n    if (recipient === 'support@example.com') {\n      console.log('Support email accepted', {\n        fromDomain: message.from.split('@').at(-1),\n        subjectLength: subject.length,\n        rawSize: message.rawSize,\n      })\n\n      await message.forward(env.SUPPORT_FORWARD_TO)\n      return\n    }\n\n    message.setReject('Unknown recipient')\n  },\n} satisfies ExportedHandler\u003CEnv>\n",[24,7654,7655,7663,7673,7677,7681,7690,7725,7744,7773,7777,7803,7818,7823,7827,7831,7846,7861,7891,7900,7905,7910,7914,7933,7937,7941,7945,7959,7963],{"__ignoreMap":74},[702,7656,7657,7659,7661],{"class":704,"line":705},[702,7658,6712],{"class":1949},[702,7660,6715],{"class":947},[702,7662,6718],{"class":708},[702,7664,7665,7668,7670],{"class":704,"line":712},[702,7666,7667],{"class":6522},"  SUPPORT_FORWARD_TO",[702,7669,6525],{"class":1949},[702,7671,7672],{"class":715}," string\n",[702,7674,7675],{"class":704,"line":729},[702,7676,868],{"class":708},[702,7678,7679],{"class":704,"line":742},[702,7680,2738],{"emptyLinePlaceholder":1696},[702,7682,7683,7685,7688],{"class":704,"line":755},[702,7684,6005],{"class":1949},[702,7686,7687],{"class":1949}," default",[702,7689,6718],{"class":708},[702,7691,7692,7695,7698,7700,7703,7705,7708,7711,7713,7716,7719,7722],{"class":704,"line":778},[702,7693,7694],{"class":1949},"  async",[702,7696,7697],{"class":947}," email",[702,7699,6411],{"class":708},[702,7701,7702],{"class":6522},"message",[702,7704,6531],{"class":708},[702,7706,7707],{"class":6522},"env",[702,7709,7710],{"class":708},")",[702,7712,6525],{"class":1949},[702,7714,7715],{"class":947}," Promise",[702,7717,7718],{"class":708},"\u003C",[702,7720,7721],{"class":715},"void",[702,7723,7724],{"class":708},"> {\n",[702,7726,7727,7730,7733,7735,7738,7741],{"class":704,"line":787},[702,7728,7729],{"class":1949},"    const",[702,7731,7732],{"class":715}," recipient",[702,7734,6218],{"class":1949},[702,7736,7737],{"class":708}," message.to.",[702,7739,7740],{"class":947},"toLowerCase",[702,7742,7743],{"class":708},"()\n",[702,7745,7746,7748,7751,7753,7756,7759,7761,7764,7767,7770],{"class":704,"line":795},[702,7747,7729],{"class":1949},[702,7749,7750],{"class":715}," subject",[702,7752,6218],{"class":1949},[702,7754,7755],{"class":708}," message.headers.",[702,7757,7758],{"class":947},"get",[702,7760,6411],{"class":708},[702,7762,7763],{"class":722},"'subject'",[702,7765,7766],{"class":708},") ",[702,7768,7769],{"class":1949},"??",[702,7771,7772],{"class":722}," ''\n",[702,7774,7775],{"class":704,"line":807},[702,7776,2738],{"emptyLinePlaceholder":1696},[702,7778,7779,7782,7785,7788,7791,7794,7797,7799,7801],{"class":704,"line":815},[702,7780,7781],{"class":1949},"    if",[702,7783,7784],{"class":708}," (message.rawSize ",[702,7786,7787],{"class":1949},">",[702,7789,7790],{"class":715}," 10",[702,7792,7793],{"class":1949}," *",[702,7795,7796],{"class":715}," 1024",[702,7798,7793],{"class":1949},[702,7800,7796],{"class":715},[702,7802,6541],{"class":708},[702,7804,7805,7808,7811,7813,7816],{"class":704,"line":836},[702,7806,7807],{"class":708},"      message.",[702,7809,7810],{"class":947},"setReject",[702,7812,6411],{"class":708},[702,7814,7815],{"class":722},"'Message is too large for this mailbox'",[702,7817,6579],{"class":708},[702,7819,7820],{"class":704,"line":842},[702,7821,7822],{"class":1949},"      return\n",[702,7824,7825],{"class":704,"line":853},[702,7826,856],{"class":708},[702,7828,7829],{"class":704,"line":859},[702,7830,2738],{"emptyLinePlaceholder":1696},[702,7832,7833,7835,7838,7841,7844],{"class":704,"line":865},[702,7834,7781],{"class":1949},[702,7836,7837],{"class":708}," (recipient ",[702,7839,7840],{"class":1949},"===",[702,7842,7843],{"class":722}," 'support@example.com'",[702,7845,6541],{"class":708},[702,7847,7848,7851,7854,7856,7859],{"class":704,"line":2849},[702,7849,7850],{"class":708},"      console.",[702,7852,7853],{"class":947},"log",[702,7855,6411],{"class":708},[702,7857,7858],{"class":722},"'Support email accepted'",[702,7860,6417],{"class":708},[702,7862,7863,7866,7869,7871,7874,7877,7880,7882,7885,7888],{"class":704,"line":2855},[702,7864,7865],{"class":708},"        fromDomain: message.from.",[702,7867,7868],{"class":947},"split",[702,7870,6411],{"class":708},[702,7872,7873],{"class":722},"'@'",[702,7875,7876],{"class":708},").",[702,7878,7879],{"class":947},"at",[702,7881,6411],{"class":708},[702,7883,7884],{"class":1949},"-",[702,7886,7887],{"class":715},"1",[702,7889,7890],{"class":708},"),\n",[702,7892,7893,7896,7898],{"class":704,"line":2860},[702,7894,7895],{"class":708},"        subjectLength: subject.",[702,7897,6430],{"class":715},[702,7899,726],{"class":708},[702,7901,7902],{"class":704,"line":2869},[702,7903,7904],{"class":708},"        rawSize: message.rawSize,\n",[702,7906,7907],{"class":704,"line":2882},[702,7908,7909],{"class":708},"      })\n",[702,7911,7912],{"class":704,"line":2890},[702,7913,2738],{"emptyLinePlaceholder":1696},[702,7915,7916,7919,7922,7925,7928,7931],{"class":704,"line":2898},[702,7917,7918],{"class":1949},"      await",[702,7920,7921],{"class":708}," message.",[702,7923,7924],{"class":947},"forward",[702,7926,7927],{"class":708},"(env.",[702,7929,7930],{"class":715},"SUPPORT_FORWARD_TO",[702,7932,6579],{"class":708},[702,7934,7935],{"class":704,"line":2906},[702,7936,7822],{"class":1949},[702,7938,7939],{"class":704,"line":2914},[702,7940,856],{"class":708},[702,7942,7943],{"class":704,"line":3432},[702,7944,2738],{"emptyLinePlaceholder":1696},[702,7946,7947,7950,7952,7954,7957],{"class":704,"line":3442},[702,7948,7949],{"class":708},"    message.",[702,7951,7810],{"class":947},[702,7953,6411],{"class":708},[702,7955,7956],{"class":722},"'Unknown recipient'",[702,7958,6579],{"class":708},[702,7960,7961],{"class":704,"line":1699},[702,7962,6284],{"class":708},[702,7964,7965,7968,7971,7974,7976,7979],{"class":704,"line":3454},[702,7966,7967],{"class":708},"} ",[702,7969,7970],{"class":1949},"satisfies",[702,7972,7973],{"class":947}," ExportedHandler",[702,7975,7718],{"class":708},[702,7977,7978],{"class":947},"Env",[702,7980,7981],{"class":708},">\n",[16,7983,7984,7985,7987,7988,7991],{},"其中 ",[24,7986,7702],{}," 是 ",[24,7989,7990],{},"ForwardableEmailMessage","，常用字段和操作包括：",[85,7993,7994,8003],{},[88,7995,7996],{},[91,7997,7998,8001],{},[94,7999,8000],{},"能力",[94,8002,557],{},[107,8004,8005,8015,8025,8035,8045,8055,8065,8075],{},[91,8006,8007,8012],{},[112,8008,8009],{},[24,8010,8011],{},"message.from",[112,8013,8014],{},"SMTP envelope 的发件人",[91,8016,8017,8022],{},[112,8018,8019],{},[24,8020,8021],{},"message.to",[112,8023,8024],{},"SMTP envelope 的收件人",[91,8026,8027,8032],{},[112,8028,8029],{},[24,8030,8031],{},"message.headers",[112,8033,8034],{},"读取 Subject、Message-ID 等邮件头",[91,8036,8037,8042],{},[112,8038,8039],{},[24,8040,8041],{},"message.raw",[112,8043,8044],{},"原始 MIME 数据流",[91,8046,8047,8052],{},[112,8048,8049],{},[24,8050,8051],{},"message.rawSize",[112,8053,8054],{},"原始邮件大小",[91,8056,8057,8062],{},[112,8058,8059],{},[24,8060,8061],{},"message.forward(address)",[112,8063,8064],{},"转发到已验证目标地址",[91,8066,8067,8072],{},[112,8068,8069],{},[24,8070,8071],{},"message.reply(email)",[112,8073,8074],{},"在约束范围内回复原发件人",[91,8076,8077,8082],{},[112,8078,8079],{},[24,8080,8081],{},"message.setReject(reason)",[112,8083,8084],{},"返回永久 SMTP 拒收",[16,8086,8087,8088,6121,8090,8092,8093,6121,8096,8099],{},"这里有一个容易忽略的安全点：",[24,8089,8011],{},[24,8091,8021],{}," 是 SMTP envelope，邮件正文头部中的 ",[24,8094,8095],{},"From:",[24,8097,8098],{},"To:"," 则可能是另一组值。做授权、路由与审计时，不要把显示给用户看的头部地址误当成可信身份。",[156,8101,8103],{"id":8102},"需要正文时再解析-mime","需要正文时再解析 MIME",[16,8105,8106,8107,3217],{},"只做转发或按地址路由时，没有必要完整解析邮件。要提取正文和附件，可以使用 ",[24,8108,8109],{},"postal-mime",[68,8111,8113],{"className":6185,"code":8112,"language":6187,"meta":74,"style":74},"import PostalMime from 'postal-mime'\n\nexport default {\n  async email(message): Promise\u003Cvoid> {\n    const parsed = await PostalMime.parse(message.raw)\n\n    console.log({\n      subject: parsed.subject,\n      textLength: parsed.text?.length ?? 0,\n      attachmentCount: parsed.attachments.length,\n    })\n  },\n} satisfies ExportedHandler\n",[24,8114,8115,8127,8131,8139,8161,8181,8185,8194,8199,8213,8222,8227,8231],{"__ignoreMap":74},[702,8116,8117,8119,8122,8124],{"class":704,"line":705},[702,8118,6194],{"class":1949},[702,8120,8121],{"class":708}," PostalMime ",[702,8123,6200],{"class":1949},[702,8125,8126],{"class":722}," 'postal-mime'\n",[702,8128,8129],{"class":704,"line":712},[702,8130,2738],{"emptyLinePlaceholder":1696},[702,8132,8133,8135,8137],{"class":704,"line":729},[702,8134,6005],{"class":1949},[702,8136,7687],{"class":1949},[702,8138,6718],{"class":708},[702,8140,8141,8143,8145,8147,8149,8151,8153,8155,8157,8159],{"class":704,"line":742},[702,8142,7694],{"class":1949},[702,8144,7697],{"class":947},[702,8146,6411],{"class":708},[702,8148,7702],{"class":6522},[702,8150,7710],{"class":708},[702,8152,6525],{"class":1949},[702,8154,7715],{"class":947},[702,8156,7718],{"class":708},[702,8158,7721],{"class":715},[702,8160,7724],{"class":708},[702,8162,8163,8165,8168,8170,8172,8175,8178],{"class":704,"line":755},[702,8164,7729],{"class":1949},[702,8166,8167],{"class":715}," parsed",[702,8169,6218],{"class":1949},[702,8171,6334],{"class":1949},[702,8173,8174],{"class":708}," PostalMime.",[702,8176,8177],{"class":947},"parse",[702,8179,8180],{"class":708},"(message.raw)\n",[702,8182,8183],{"class":704,"line":778},[702,8184,2738],{"emptyLinePlaceholder":1696},[702,8186,8187,8190,8192],{"class":704,"line":787},[702,8188,8189],{"class":708},"    console.",[702,8191,7853],{"class":947},[702,8193,6227],{"class":708},[702,8195,8196],{"class":704,"line":795},[702,8197,8198],{"class":708},"      subject: parsed.subject,\n",[702,8200,8201,8204,8206,8209,8211],{"class":704,"line":807},[702,8202,8203],{"class":708},"      textLength: parsed.text?.",[702,8205,6430],{"class":715},[702,8207,8208],{"class":1949}," ??",[702,8210,4086],{"class":715},[702,8212,726],{"class":708},[702,8214,8215,8218,8220],{"class":704,"line":815},[702,8216,8217],{"class":708},"      attachmentCount: parsed.attachments.",[702,8219,6430],{"class":715},[702,8221,726],{"class":708},[702,8223,8224],{"class":704,"line":836},[702,8225,8226],{"class":708},"    })\n",[702,8228,8229],{"class":704,"line":842},[702,8230,6284],{"class":708},[702,8232,8233,8235,8237],{"class":704,"line":853},[702,8234,7967],{"class":708},[702,8236,7970],{"class":1949},[702,8238,8239],{"class":947}," ExportedHandler\n",[16,8241,8242],{},"解析后的 HTML、文件名、Content-Type 与附件都来自外部输入。正确做法是：",[373,8244,8245,8248,8251,8254,8257],{},[177,8246,8247],{},"不在管理页面直接插入来信 HTML；",[177,8249,8250],{},"限制正文、附件数量和解压后大小；",[177,8252,8253],{},"文件写入 R2 前重新生成对象键，不使用原始文件名作为路径；",[177,8255,8256],{},"不让来信内容直接组成 SQL、Shell、模板或 AI 工具参数；",[177,8258,8259],{},"大任务交给 Queue，不要把杀毒、OCR、模型调用全塞进入站事件的同步 CPU 时间。",[156,8261,8263],{"id":8262},"转发回复拒绝与静默丢弃的区别","转发、回复、拒绝与静默丢弃的区别",[373,8265,8266,8272,8278,8284],{},[177,8267,8268,8271],{},[20,8269,8270],{},"转发","：适合人工邮箱继续处理，目标地址必须提前验证；",[177,8273,8274,8277],{},[20,8275,8276],{},"回复","：适合自动确认或邮件机器人，但有额外限制；",[177,8279,8280,8283],{},[20,8281,8282],{},"拒绝","：在 SMTP 阶段明确告诉发送方没有接收；",[177,8285,8286,8289],{},[20,8287,8288],{},"丢弃","：规则匹配后不继续处理，也不给发送方业务层反馈。",[16,8291,8292],{},"自动回复不能被当成任意发送接口。官方当前要求：",[373,8294,8295,8298,8301,8304,8307],{},[177,8296,8297],{},"来信必须有有效 DMARC 结果；",[177,8299,8300],{},"每个入站事件只能调用一次回复；",[177,8302,8303],{},"回复收件人必须是原始来信的发件人；",[177,8305,8306],{},"回复使用的发件域必须与接收邮件的域一致；",[177,8308,8309,8312],{},[24,8310,8311],{},"References"," 超过 100 项时会拒绝回复，以避免邮件环路。",[16,8314,8315],{},"这组限制反而是一件好事：它让“回复这封邮件”和“向任意地址发一封新邮件”成为两种不同权限。",[64,8317,8319],{"id":8318},"发送邮件优先使用-workers-binding","发送邮件：优先使用 Workers binding",[16,8321,8322],{},"如果应用本身运行在 Cloudflare Workers，Workers binding 是最自然的选择。它不需要在业务代码里保存 Cloudflare API Token。",[16,8324,8325,8326,8329],{},"先在 ",[24,8327,8328],{},"wrangler.jsonc"," 中声明绑定：",[68,8331,8335],{"className":8332,"code":8333,"language":8334,"meta":74,"style":74},"language-jsonc shiki shiki-themes github-dark github-dark","{\n  \"send_email\": [\n    {\n      \"name\": \"EMAIL\",\n      \"allowed_sender_addresses\": [\n        \"noreply@example.com\"\n      ]\n    }\n  ],\n  \"vars\": {\n    \"EMAIL_FROM\": \"noreply@example.com\"\n  }\n}\n","jsonc",[24,8336,8337,8341,8346,8351,8356,8361,8366,8371,8375,8380,8385,8390,8394],{"__ignoreMap":74},[702,8338,8339],{"class":704,"line":705},[702,8340,709],{},[702,8342,8343],{"class":704,"line":712},[702,8344,8345],{},"  \"send_email\": [\n",[702,8347,8348],{"class":704,"line":729},[702,8349,8350],{},"    {\n",[702,8352,8353],{"class":704,"line":742},[702,8354,8355],{},"      \"name\": \"EMAIL\",\n",[702,8357,8358],{"class":704,"line":755},[702,8359,8360],{},"      \"allowed_sender_addresses\": [\n",[702,8362,8363],{"class":704,"line":778},[702,8364,8365],{},"        \"noreply@example.com\"\n",[702,8367,8368],{"class":704,"line":787},[702,8369,8370],{},"      ]\n",[702,8372,8373],{"class":704,"line":795},[702,8374,856],{},[702,8376,8377],{"class":704,"line":807},[702,8378,8379],{},"  ],\n",[702,8381,8382],{"class":704,"line":815},[702,8383,8384],{},"  \"vars\": {\n",[702,8386,8387],{"class":704,"line":836},[702,8388,8389],{},"    \"EMAIL_FROM\": \"noreply@example.com\"\n",[702,8391,8392],{"class":704,"line":842},[702,8393,862],{},[702,8395,8396],{"class":704,"line":853},[702,8397,868],{},[16,8399,8400,8403],{},[24,8401,8402],{},"allowed_sender_addresses"," 不只是文档，它是运行时权限边界。即使某个接口被误用，这个绑定也不能伪造其他发件地址。",[16,8405,8406],{},"还可以用以下配置进一步约束收件人：",[373,8408,8409,8415,8421],{},[177,8410,8411,8414],{},[24,8412,8413],{},"destination_address","：固定为一个目标地址；",[177,8416,8417,8420],{},[24,8418,8419],{},"allowed_destination_addresses","：只允许一组目标地址；",[177,8422,8423,8425],{},[24,8424,8402],{},"：限制可使用的发件地址。",[16,8427,8428,8429,8431],{},"用于系统告警的 Worker 最适合固定 ",[24,8430,8413],{},"；用于用户验证码的服务则通常限制发件人，并在应用层校验收件人和频率。",[156,8433,8435],{"id":8434},"在-worker-中发送","在 Worker 中发送",[68,8437,8439],{"className":6185,"code":8438,"language":6187,"meta":74,"style":74},"interface Env {\n  EMAIL: SendEmail\n}\n\nexport default {\n  async fetch(_request, env): Promise\u003CResponse> {\n    const result = await env.EMAIL.send({\n      from: {\n        email: 'noreply@example.com',\n        name: 'Example App',\n      },\n      to: 'reader@example.net',\n      subject: '验证你的邮箱',\n      text: '你的验证码是 482913，5 分钟内有效。',\n      html: '\u003Cp>你的验证码是 \u003Cstrong>482913\u003C\u002Fstrong>，5 分钟内有效。\u003C\u002Fp>',\n    })\n\n    return Response.json({\n      messageId: result.messageId,\n    })\n  },\n} satisfies ExportedHandler\u003CEnv>\n",[24,8440,8441,8449,8457,8461,8465,8473,8502,8523,8528,8538,8548,8552,8562,8572,8582,8592,8596,8600,8612,8617,8621,8625],{"__ignoreMap":74},[702,8442,8443,8445,8447],{"class":704,"line":705},[702,8444,6712],{"class":1949},[702,8446,6715],{"class":947},[702,8448,6718],{"class":708},[702,8450,8451,8453,8455],{"class":704,"line":712},[702,8452,6723],{"class":6522},[702,8454,6525],{"class":1949},[702,8456,6728],{"class":947},[702,8458,8459],{"class":704,"line":729},[702,8460,868],{"class":708},[702,8462,8463],{"class":704,"line":742},[702,8464,2738],{"emptyLinePlaceholder":1696},[702,8466,8467,8469,8471],{"class":704,"line":755},[702,8468,6005],{"class":1949},[702,8470,7687],{"class":1949},[702,8472,6718],{"class":708},[702,8474,8475,8477,8480,8482,8485,8487,8489,8491,8493,8495,8497,8500],{"class":704,"line":778},[702,8476,7694],{"class":1949},[702,8478,8479],{"class":947}," fetch",[702,8481,6411],{"class":708},[702,8483,8484],{"class":6522},"_request",[702,8486,6531],{"class":708},[702,8488,7707],{"class":6522},[702,8490,7710],{"class":708},[702,8492,6525],{"class":1949},[702,8494,7715],{"class":947},[702,8496,7718],{"class":708},[702,8498,8499],{"class":947},"Response",[702,8501,7724],{"class":708},[702,8503,8504,8506,8509,8511,8513,8515,8517,8519,8521],{"class":704,"line":787},[702,8505,7729],{"class":1949},[702,8507,8508],{"class":715}," result",[702,8510,6218],{"class":1949},[702,8512,6334],{"class":1949},[702,8514,6744],{"class":708},[702,8516,6747],{"class":715},[702,8518,2937],{"class":708},[702,8520,6752],{"class":947},[702,8522,6227],{"class":708},[702,8524,8525],{"class":704,"line":795},[702,8526,8527],{"class":708},"      from: {\n",[702,8529,8530,8533,8536],{"class":704,"line":807},[702,8531,8532],{"class":708},"        email: ",[702,8534,8535],{"class":722},"'noreply@example.com'",[702,8537,726],{"class":708},[702,8539,8540,8543,8546],{"class":704,"line":815},[702,8541,8542],{"class":708},"        name: ",[702,8544,8545],{"class":722},"'Example App'",[702,8547,726],{"class":708},[702,8549,8550],{"class":704,"line":836},[702,8551,839],{"class":708},[702,8553,8554,8557,8560],{"class":704,"line":842},[702,8555,8556],{"class":708},"      to: ",[702,8558,8559],{"class":722},"'reader@example.net'",[702,8561,726],{"class":708},[702,8563,8564,8567,8570],{"class":704,"line":853},[702,8565,8566],{"class":708},"      subject: ",[702,8568,8569],{"class":722},"'验证你的邮箱'",[702,8571,726],{"class":708},[702,8573,8574,8577,8580],{"class":704,"line":859},[702,8575,8576],{"class":708},"      text: ",[702,8578,8579],{"class":722},"'你的验证码是 482913，5 分钟内有效。'",[702,8581,726],{"class":708},[702,8583,8584,8587,8590],{"class":704,"line":865},[702,8585,8586],{"class":708},"      html: ",[702,8588,8589],{"class":722},"'\u003Cp>你的验证码是 \u003Cstrong>482913\u003C\u002Fstrong>，5 分钟内有效。\u003C\u002Fp>'",[702,8591,726],{"class":708},[702,8593,8594],{"class":704,"line":2849},[702,8595,8226],{"class":708},[702,8597,8598],{"class":704,"line":2855},[702,8599,2738],{"emptyLinePlaceholder":1696},[702,8601,8602,8605,8608,8610],{"class":704,"line":2860},[702,8603,8604],{"class":1949},"    return",[702,8606,8607],{"class":708}," Response.",[702,8609,698],{"class":947},[702,8611,6227],{"class":708},[702,8613,8614],{"class":704,"line":2869},[702,8615,8616],{"class":708},"      messageId: result.messageId,\n",[702,8618,8619],{"class":704,"line":2882},[702,8620,8226],{"class":708},[702,8622,8623],{"class":704,"line":2890},[702,8624,6284],{"class":708},[702,8626,8627,8629,8631,8633,8635,8637],{"class":704,"line":2898},[702,8628,7967],{"class":708},[702,8630,7970],{"class":1949},[702,8632,7973],{"class":947},[702,8634,7718],{"class":708},[702,8636,7978],{"class":947},[702,8638,7981],{"class":708},[16,8640,8641,8642,8644],{},"Workers binding 成功时返回一个 ",[24,8643,7429],{},"。这个结果代表 Cloudflare 接受了发送请求，不应该被解释为用户一定已经在收件箱里看到邮件。最终投递、延迟、退信和抑制状态还要结合 Email Service 日志与指标。",[156,8646,8648],{"id":8647},"在-nuxt-服务端-api-中发送","在 Nuxt 服务端 API 中发送",[16,8650,8651],{},"Nuxt 部署到 Workers 后，同样可以从服务端路由读取 binding：",[68,8653,8655],{"className":6185,"code":8654,"language":6187,"meta":74,"style":74},"import { z } from 'zod'\n\nconst schema = z.object({\n  email: z.string().trim().email().max(254),\n})\n\nexport default defineEventHandler(async (event) => {\n  const { email } = schema.parse(await readBody(event))\n  const env = event.context.cloudflare.env as {\n    EMAIL: SendEmail\n    EMAIL_FROM: string\n  }\n\n  \u002F\u002F 实际项目还应在这里执行同源校验、IP\u002F邮箱限流，\n  \u002F\u002F 并在 D1 中原子领取一次发送资格。\n  const result = await env.EMAIL.send({\n    from: {\n      email: env.EMAIL_FROM,\n      name: 'Example App',\n    },\n    to: email,\n    subject: '验证邮箱',\n    text: '验证码将在 5 分钟后失效。',\n    html: '\u003Cp>验证码将在 \u003Cstrong>5 分钟\u003C\u002Fstrong>后失效。\u003C\u002Fp>',\n  })\n\n  return {\n    ok: true,\n    messageId: result.messageId,\n  }\n})\n",[24,8656,8657,8669,8673,8690,8721,8725,8729,8755,8785,8801,8810,8819,8823,8827,8833,8838,8858,8863,8873,8882,8887,8892,8902,8912,8922,8927,8931,8938,8947,8953,8958],{"__ignoreMap":74},[702,8658,8659,8661,8664,8666],{"class":704,"line":705},[702,8660,6194],{"class":1949},[702,8662,8663],{"class":708}," { z } ",[702,8665,6200],{"class":1949},[702,8667,8668],{"class":722}," 'zod'\n",[702,8670,8671],{"class":704,"line":712},[702,8672,2738],{"emptyLinePlaceholder":1696},[702,8674,8675,8677,8680,8682,8685,8688],{"class":704,"line":729},[702,8676,6212],{"class":1949},[702,8678,8679],{"class":715}," schema",[702,8681,6218],{"class":1949},[702,8683,8684],{"class":708}," z.",[702,8686,8687],{"class":947},"object",[702,8689,6227],{"class":708},[702,8691,8692,8695,8698,8701,8704,8706,8709,8711,8714,8716,8719],{"class":704,"line":742},[702,8693,8694],{"class":708},"  email: z.",[702,8696,8697],{"class":947},"string",[702,8699,8700],{"class":708},"().",[702,8702,8703],{"class":947},"trim",[702,8705,8700],{"class":708},[702,8707,8708],{"class":947},"email",[702,8710,8700],{"class":708},[702,8712,8713],{"class":947},"max",[702,8715,6411],{"class":708},[702,8717,8718],{"class":715},"254",[702,8720,7890],{"class":708},[702,8722,8723],{"class":704,"line":755},[702,8724,6318],{"class":708},[702,8726,8727],{"class":704,"line":778},[702,8728,2738],{"emptyLinePlaceholder":1696},[702,8730,8731,8733,8735,8738,8740,8743,8745,8748,8750,8753],{"class":704,"line":787},[702,8732,6005],{"class":1949},[702,8734,7687],{"class":1949},[702,8736,8737],{"class":947}," defineEventHandler",[702,8739,6411],{"class":708},[702,8741,8742],{"class":1949},"async",[702,8744,6549],{"class":708},[702,8746,8747],{"class":6522},"event",[702,8749,7766],{"class":708},[702,8751,8752],{"class":1949},"=>",[702,8754,6718],{"class":708},[702,8756,8757,8760,8763,8765,8768,8770,8773,8775,8777,8779,8782],{"class":704,"line":795},[702,8758,8759],{"class":1949},"  const",[702,8761,8762],{"class":708}," { ",[702,8764,8708],{"class":715},[702,8766,8767],{"class":708}," } ",[702,8769,2990],{"class":1949},[702,8771,8772],{"class":708}," schema.",[702,8774,8177],{"class":947},[702,8776,6411],{"class":708},[702,8778,6741],{"class":1949},[702,8780,8781],{"class":947}," readBody",[702,8783,8784],{"class":708},"(event))\n",[702,8786,8787,8789,8791,8793,8796,8799],{"class":704,"line":807},[702,8788,8759],{"class":1949},[702,8790,3042],{"class":715},[702,8792,6218],{"class":1949},[702,8794,8795],{"class":708}," event.context.cloudflare.env ",[702,8797,8798],{"class":1949},"as",[702,8800,6718],{"class":708},[702,8802,8803,8806,8808],{"class":704,"line":815},[702,8804,8805],{"class":6522},"    EMAIL",[702,8807,6525],{"class":1949},[702,8809,6728],{"class":947},[702,8811,8812,8815,8817],{"class":704,"line":836},[702,8813,8814],{"class":6522},"    EMAIL_FROM",[702,8816,6525],{"class":1949},[702,8818,7672],{"class":715},[702,8820,8821],{"class":704,"line":842},[702,8822,862],{"class":708},[702,8824,8825],{"class":704,"line":853},[702,8826,2738],{"emptyLinePlaceholder":1696},[702,8828,8829],{"class":704,"line":859},[702,8830,8832],{"class":8831},"sJ8bj","  \u002F\u002F 实际项目还应在这里执行同源校验、IP\u002F邮箱限流，\n",[702,8834,8835],{"class":704,"line":865},[702,8836,8837],{"class":8831},"  \u002F\u002F 并在 D1 中原子领取一次发送资格。\n",[702,8839,8840,8842,8844,8846,8848,8850,8852,8854,8856],{"class":704,"line":2849},[702,8841,8759],{"class":1949},[702,8843,8508],{"class":715},[702,8845,6218],{"class":1949},[702,8847,6334],{"class":1949},[702,8849,6744],{"class":708},[702,8851,6747],{"class":715},[702,8853,2937],{"class":708},[702,8855,6752],{"class":947},[702,8857,6227],{"class":708},[702,8859,8860],{"class":704,"line":2855},[702,8861,8862],{"class":708},"    from: {\n",[702,8864,8865,8868,8871],{"class":704,"line":2860},[702,8866,8867],{"class":708},"      email: env.",[702,8869,8870],{"class":715},"EMAIL_FROM",[702,8872,726],{"class":708},[702,8874,8875,8878,8880],{"class":704,"line":2869},[702,8876,8877],{"class":708},"      name: ",[702,8879,8545],{"class":722},[702,8881,726],{"class":708},[702,8883,8884],{"class":704,"line":2882},[702,8885,8886],{"class":708},"    },\n",[702,8888,8889],{"class":704,"line":2890},[702,8890,8891],{"class":708},"    to: email,\n",[702,8893,8894,8897,8900],{"class":704,"line":2898},[702,8895,8896],{"class":708},"    subject: ",[702,8898,8899],{"class":722},"'验证邮箱'",[702,8901,726],{"class":708},[702,8903,8904,8907,8910],{"class":704,"line":2906},[702,8905,8906],{"class":708},"    text: ",[702,8908,8909],{"class":722},"'验证码将在 5 分钟后失效。'",[702,8911,726],{"class":708},[702,8913,8914,8917,8920],{"class":704,"line":2914},[702,8915,8916],{"class":708},"    html: ",[702,8918,8919],{"class":722},"'\u003Cp>验证码将在 \u003Cstrong>5 分钟\u003C\u002Fstrong>后失效。\u003C\u002Fp>'",[702,8921,726],{"class":708},[702,8923,8924],{"class":704,"line":3432},[702,8925,8926],{"class":708},"  })\n",[702,8928,8929],{"class":704,"line":3442},[702,8930,2738],{"emptyLinePlaceholder":1696},[702,8932,8933,8936],{"class":704,"line":1699},[702,8934,8935],{"class":1949},"  return",[702,8937,6718],{"class":708},[702,8939,8940,8943,8945],{"class":704,"line":3454},[702,8941,8942],{"class":708},"    ok: ",[702,8944,750],{"class":715},[702,8946,726],{"class":708},[702,8948,8950],{"class":704,"line":8949},29,[702,8951,8952],{"class":708},"    messageId: result.messageId,\n",[702,8954,8956],{"class":704,"line":8955},30,[702,8957,862],{"class":708},[702,8959,8961],{"class":704,"line":8960},31,[702,8962,6318],{"class":708},[16,8964,8965],{},"不要把这个示例直接变成一个匿名、无限频率、可自由填写主题和正文的公开 API。那会把自己的域名变成垃圾邮件跳板。",[16,8967,8968],{},[214,8969],{"alt":8970,"src":8971},"事务邮件从 Worker 生成，经过多层身份认证后投递到不同收件系统","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Foutbound-delivery-v1.webp",[16,8973,8974],{},[221,8975,8976],{},"SPF、DKIM 与 DMARC 解决的是“谁有权发送、内容是否被修改、失败时如何处置”，并不保证每封邮件都进入主收件箱。",[64,8978,8980],{"id":8979},"rest-api-与认证-smtp-什么时候更合适","REST API 与认证 SMTP 什么时候更合适",[16,8982,8983],{},"Cloudflare 目前提供三种发送入口：",[85,8985,8986,8999],{},[88,8987,8988],{},[91,8989,8990,8993,8996],{},[94,8991,8992],{},"方式",[94,8994,8995],{},"最适合",[94,8997,8998],{},"凭证与边界",[107,9000,9001,9012,9022],{},[91,9002,9003,9006,9009],{},[112,9004,9005],{},"Workers binding",[112,9007,9008],{},"已运行在 Workers 的 Nuxt、Hono、Agent",[112,9010,9011],{},"不在代码中保存 API Token，可通过 binding 限制发件人与收件人",[91,9013,9014,9016,9019],{},[112,9015,6833],{},[112,9017,9018],{},"运行在其他云、CI 或后端平台的程序",[112,9020,9021],{},"使用最小权限 API Token，响应会按收件人区分 delivered、queued、permanent bounce",[91,9023,9024,9026,9029],{},[112,9025,6841],{},[112,9027,9028],{},"已经使用 Nodemailer、PHPMailer、JavaMail 等 SMTP 客户端的旧系统",[112,9030,9031,9033],{},[24,9032,5748],{},"，仅隐式 TLS，密码是有 Email Sending 权限的 API Token",[16,9035,9036],{},"认证 SMTP 是 2026 年 6 月加入的 Beta 能力。当前只支持 465 端口的 SMTPS，不提供 587 端口 STARTTLS，也不是可以匿名使用的 25 端口 relay。",[16,9038,9039],{},"SMTP Token 一旦泄漏，持有者可以使用账户中已接入的发件域发送邮件。因此：",[373,9041,9042,9045,9050,9053,9056],{},[177,9043,9044],{},"优先使用 account-owned token；",[177,9046,9047,9048,2039],{},"权限只给 ",[24,9049,5834],{},[177,9051,9052],{},"不写进仓库、镜像或前端环境变量；",[177,9054,9055],{},"不同系统使用不同 Token，便于单独吊销；",[177,9057,9058],{},"能用 Worker binding 的场景，不要为了“熟悉 SMTP”额外引入长期凭证。",[64,9060,9061],{"id":9061},"必须知道的限制",[16,9063,9064],{},"截至 2026 年 7 月，官方平台限制中最值得在代码里提前防守的是：",[85,9066,9067,9081],{},[88,9068,9069],{},[91,9070,9071,9074,9078],{},[94,9072,9073],{},"限制",[94,9075,9077],{"align":9076},"right","当前值",[94,9079,9080],{},"实现影响",[107,9082,9083,9094,9104,9115,9126,9137,9147,9158,9169],{},[91,9084,9085,9088,9091],{},[112,9086,9087],{},"每域 Email Routing 规则",[112,9089,9090],{"align":9076},"200",[112,9092,9093],{},"大量租户不要为每个人创建独立静态规则",[91,9095,9096,9099,9101],{},[112,9097,9098],{},"每账户已验证目标地址",[112,9100,9090],{"align":9076},[112,9102,9103],{},"转发到多邮箱前先规划账户级目标",[91,9105,9106,9109,9112],{},[112,9107,9108],{},"入站邮件大小",[112,9110,9111],{"align":9076},"25 MiB",[112,9113,9114],{},"更大邮件在 SMTP 阶段被拒绝",[91,9116,9117,9120,9123],{},[112,9118,9119],{},"单封出站收件人数",[112,9121,9122],{"align":9076},"50",[112,9124,9125],{},"To、Cc、Bcc 合计",[91,9127,9128,9131,9134],{},[112,9129,9130],{},"普通出站邮件总大小",[112,9132,9133],{"align":9076},"5 MiB",[112,9135,9136],{},"包含附件与 MIME 编码开销",[91,9138,9139,9142,9144],{},[112,9140,9141],{},"发往已验证目标的总大小",[112,9143,9111],{"align":9076},[112,9145,9146],{},"仅适用于已验证目标地址",[91,9148,9149,9152,9155],{},[112,9150,9151],{},"自定义头总大小",[112,9153,9154],{"align":9076},"16 KB",[112,9156,9157],{},"不要把业务数据塞进 Header",[91,9159,9160,9163,9166],{},[112,9161,9162],{},"单个 Zone 的发送\u002F路由域名总数",[112,9164,9165],{"align":9076},"30",[112,9167,9168],{},"根域与子域都计入",[91,9170,9171,9177,9180],{},[112,9172,9173,9174,9176],{},"回复的 ",[24,9175,8311],{}," 项数",[112,9178,9179],{"align":9076},"100",[112,9181,9182,9183,9186],{},"超过后 ",[24,9184,9185],{},"reply()"," 会失败",[16,9188,9189],{},"附件会经过 Base64 和 MIME 封装。3 MiB 的原始附件不等于最终邮件仍是 3 MiB，所以应用限制应给编码、正文和邮件头预留空间。",[16,9191,9192],{},"平台还会根据账户信誉、投递表现和使用历史调整每日发送额度。不要假设“每月额度未用完”就等于某一秒可以无限并发发送。",[64,9194,9195],{"id":9195},"验证码与密码重置应该怎样做",[16,9197,9198],{},"本博客的注册和密码重置邮件采用的是下面这条链路：",[68,9200,9203],{"className":9201,"code":9202,"language":73,"meta":74},[71],"用户提交邮箱\n  → 同源检查\n  → IP 与邮箱限流\n  → 生成 6 位验证码\n  → D1 只保存加盐哈希、用途、过期时间和尝试次数\n  → EMAIL binding 发送\n  → 验证成功后原子标记已使用\n",[24,9204,9202],{"__ignoreMap":74},[16,9206,9207],{},"关键策略包括：",[373,9209,9210,9213,9216,9219,9222,9225,9228],{},[177,9211,9212],{},"同一邮箱 5 分钟只能发送一封；",[177,9214,9215],{},"验证码 5 分钟后失效；",[177,9217,9218],{},"验证码只能使用一次；",[177,9220,9221],{},"D1 不保存验证码明文；",[177,9223,9224],{},"连续输错达到阈值后停止验证；",[177,9226,9227],{},"邮件发送失败时释放本次占位，避免用户被一个未送达的验证码锁住；",[177,9229,9230],{},"重置密码后撤销已有登录会话，而不是只替换密码哈希。",[16,9232,9233,9234,9237],{},"限流要在数据库中“原子领取资格”，不能写成“先查询上次发送时间，再插入一行”。两个并发请求可能同时通过查询。使用唯一键配合 ",[24,9235,9236],{},"INSERT ... ON CONFLICT ... WHERE","，才能让数据库决定谁获得发送资格。",[16,9239,9240],{},"邮件接口的响应也不应暴露“这个邮箱是否已经注册”。否则攻击者可以批量枚举用户。",[64,9242,9243],{"id":9243},"一个更完整的生产架构",[16,9245,9246],{},"当邮件从 Demo 进入生产，推荐把职责拆开：",[68,9248,9251],{"className":9249,"code":9250,"language":73,"meta":74},[71],"HTTP 请求 \u002F 业务事件\n  → 鉴权、同源、参数校验、限流、幂等\n  → Queue\n  → 邮件模板渲染\n  → EMAIL binding\n  → messageId \u002F 错误码写入 D1\n  → Email Service 日志与告警\n",[24,9252,9250],{"__ignoreMap":74},[16,9254,9255],{},"入站链路则可以是：",[68,9257,9260],{"className":9258,"code":9259,"language":73,"meta":74},[71],"support@yourdomain.com\n  → Email Routing\n  → Email Worker\n  → 认证与大小检查\n  → MIME 解析\n  → R2 保存附件 \u002F D1 建立工单\n  → Queue 做耗时处理\n  → 转发人工邮箱或发送受约束回复\n",[24,9261,9259],{"__ignoreMap":74},[156,9263,9265],{"id":9264},"d1-建议记录什么","D1 建议记录什么",[16,9267,9268],{},"可以记录：",[373,9270,9271,9274,9277,9281,9284,9287,9290],{},[177,9272,9273],{},"业务事件 ID 与幂等键；",[177,9275,9276],{},"模板 ID 和模板版本；",[177,9278,9279,2039],{},[24,9280,7429],{},[177,9282,9283],{},"收件人数，不一定要保存完整地址；",[177,9285,9286],{},"accepted \u002F failed 等应用侧状态；",[177,9288,9289],{},"Cloudflare 错误码；",[177,9291,9292],{},"创建、发送和最后更新时间。",[16,9294,9295],{},"不建议默认记录：",[373,9297,9298,9301,9304,9307,9310,9313],{},[177,9299,9300],{},"验证码明文；",[177,9302,9303],{},"密码重置 Token；",[177,9305,9306],{},"完整邮件正文；",[177,9308,9309],{},"附件内容；",[177,9311,9312],{},"SMTP\u002FAPI Token；",[177,9314,9315],{},"为调试方便长期保留的原始来信。",[16,9317,9318],{},"日志本身也可能成为新的隐私数据源。",[156,9320,9322],{"id":9321},"不要把-email-routing-的-dropped-当成发送失败","不要把 Email Routing 的 “Dropped” 当成发送失败",[16,9324,9325,9326,9328,9329,9332],{},"官方特别说明：通过 ",[24,9327,6701],{}," binding 发出的邮件，在 Email Routing 汇总页面可能显示为 ",[20,9330,9331],{},"dropped","，即使邮件实际已经成功投递。",[16,9334,9335,9336,9338],{},"出站结果应该查看 Email Sending 的 Metrics 与 Logs，并用 ",[24,9337,7429],{}," 关联应用日志。Routing 页面描述的是入站路由处理，不是完整的出站投递状态机。",[64,9340,9341],{"id":9341},"本地怎么测试",[16,9343,9344],{},"Wrangler 已支持本地触发 Email Worker。运行：",[68,9346,9348],{"className":938,"code":9347,"language":940,"meta":74,"style":74},"pnpm exec wrangler dev\n",[24,9349,9350],{"__ignoreMap":74},[702,9351,9352,9354,9357,9360],{"class":704,"line":705},[702,9353,6142],{"class":947},[702,9355,9356],{"class":722}," exec",[702,9358,9359],{"class":722}," wrangler",[702,9361,9362],{"class":722}," dev\n",[16,9364,9365],{},"然后向本地入口发送一封原始邮件：",[68,9367,9369],{"className":938,"code":9368,"language":940,"meta":74,"style":74},"curl -X POST \\\n  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com' \\\n  -H 'Content-Type: text\u002Fplain' \\\n  --data-binary $'From: sender@example.net\\nTo: support@example.com\\nSubject: Local test\\n\\nHello from local development.'\n",[24,9370,9371,9383,9390,9400],{"__ignoreMap":74},[702,9372,9373,9375,9378,9381],{"class":704,"line":705},[702,9374,948],{"class":947},[702,9376,9377],{"class":715}," -X",[702,9379,9380],{"class":722}," POST",[702,9382,1944],{"class":715},[702,9384,9385,9388],{"class":704,"line":712},[702,9386,9387],{"class":722},"  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com'",[702,9389,1944],{"class":715},[702,9391,9392,9395,9398],{"class":704,"line":729},[702,9393,9394],{"class":715},"  -H",[702,9396,9397],{"class":722}," 'Content-Type: text\u002Fplain'",[702,9399,1944],{"class":715},[702,9401,9402,9405,9408,9411,9414,9416,9419,9422],{"class":704,"line":742},[702,9403,9404],{"class":715},"  --data-binary",[702,9406,9407],{"class":722}," $'From: sender@example.net",[702,9409,9410],{"class":715},"\\n",[702,9412,9413],{"class":722},"To: support@example.com",[702,9415,9410],{"class":715},[702,9417,9418],{"class":722},"Subject: Local test",[702,9420,9421],{"class":715},"\\n\\n",[702,9423,9424],{"class":722},"Hello from local development.'\n",[16,9426,9427],{},"本地发送 binding 会把文本和 HTML 保存成可检查的文件，适合验证模板。二进制附件的本地模拟仍有单独限制，涉及附件时应补一轮远程测试。",[16,9429,9430],{},"至少覆盖这些用例：",[373,9432,9433,9436,9439,9442,9445,9448,9451,9454,9457,9460],{},[177,9434,9435],{},"正常文本和 HTML；",[177,9437,9438],{},"非 ASCII 主题与发件人显示名；",[177,9440,9441],{},"重复请求与幂等；",[177,9443,9444],{},"超过发送冷却时间前的重复验证码；",[177,9446,9447],{},"大正文与附件边界；",[177,9449,9450],{},"Header 换行注入；",[177,9452,9453],{},"未验证发件域或受限收件人；",[177,9455,9456],{},"退信、抑制、额度和频率错误；",[177,9458,9459],{},"入站无匹配地址、Catch-all 和拒绝路径；",[177,9461,9462],{},"HTML 邮件中的脚本、远程资源与危险附件名。",[64,9464,9466],{"id":9465},"它适合什么不适合什么","它适合什么，不适合什么",[16,9468,9469],{},"适合：",[373,9471,9472,9475,9478,9481,9484,9487],{},[177,9473,9474],{},"注册验证、魔法链接、密码重置；",[177,9476,9477],{},"订单、账单、部署、监控通知；",[177,9479,9480],{},"个人域名邮件转发；",[177,9482,9483],{},"联系表单与支持工单；",[177,9485,9486],{},"邮件触发的 Agent 或自动化流程；",[177,9488,9489],{},"希望少维护一台 SMTP 服务的 Cloudflare 应用。",[16,9491,9492],{},"不应直接拿来做：",[373,9494,9495,9498,9501,9504,9507],{},[177,9496,9497],{},"未经同意的营销群发；",[177,9499,9500],{},"没有退订、投诉和抑制管理的 Newsletter；",[177,9502,9503],{},"匿名开放的“帮我发邮件”接口；",[177,9505,9506],{},"需要 IMAP 文件夹、日历、通讯录和多人邮箱协作的企业邮箱；",[177,9508,9509],{},"把任何来信正文直接交给有高权限工具的 Agent。",[16,9511,9512],{},"Cloudflare 替你维护了邮件入口与投递基础设施，但发件人信誉、用户授权、内容安全、频率控制和数据最小化仍然是应用自己的责任。",[64,9514,9515],{"id":9515},"最后的判断",[16,9517,9518],{},"Cloudflare Email Service 最有价值的地方，不是“又多了一个 SMTP 厂商”，而是它让邮件和 Worker、D1、R2、Queue、Agents 处于同一运行边界。",[16,9520,9521],{},"接收邮件不再只能转发到人类邮箱，它可以成为一个受约束的事件入口；发送邮件也不再需要为验证码单独接入一个外部 SDK 和密钥体系。",[16,9523,9524],{},"如果项目本来就部署在 Workers，优先从这条最小链路开始：",[174,9526,9527,9533,9536,9539,9542],{},[177,9528,9529,9530,2039],{},"Email Routing 接收 ",[24,9531,9532],{},"support@",[177,9534,9535],{},"Worker binding 发送验证码和密码重置；",[177,9537,9538],{},"D1 实现冷却、一次性验证和最小日志；",[177,9540,9541],{},"sender allowlist 限制 binding 权限；",[177,9543,9544],{},"用 Email Sending 日志核对投递，而不是只看接口返回成功。",[16,9546,9547],{},"等确实需要从外部平台发送时，再引入 REST API；只有现有系统已经深度依赖 SMTP 客户端时，才使用认证 SMTP。",[64,9549,1561],{"id":1561},[373,9551,9552,9559,9565,9572,9579,9586,9593,9600,9606,9611,9617,9624,9631],{},[177,9553,9554],{},[163,9555,9558],{"href":9556,"rel":9557},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002F",[167],"Cloudflare Email Service 概览",[177,9560,9561],{},[163,9562,9564],{"href":5759,"rel":9563},[167],"接入 Email Sending",[177,9566,9567],{},[163,9568,9571],{"href":9569,"rel":9570},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Froute-emails\u002F",[167],"接入 Email Routing",[177,9573,9574],{},[163,9575,9578],{"href":9576,"rel":9577},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fdomains\u002F",[167],"域名与 DNS 配置",[177,9580,9581],{},[163,9582,9585],{"href":9583,"rel":9584},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fsend-bindings\u002F",[167],"发送 binding 权限配置",[177,9587,9588],{},[163,9589,9592],{"href":9590,"rel":9591},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fworkers-api\u002F",[167],"Workers 发送 API",[177,9594,9595],{},[163,9596,9599],{"href":9597,"rel":9598},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Froute-emails\u002Femail-handler\u002F",[167],"Email Worker 接收 API",[177,9601,9602],{},[163,9603,6833],{"href":9604,"rel":9605},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Frest-api\u002F",[167],[177,9607,9608],{},[163,9609,6841],{"href":5765,"rel":9610},[167],[177,9612,9613],{},[163,9614,9616],{"href":6965,"rel":9615},[167],"平台限制",[177,9618,9619],{},[163,9620,9623],{"href":9621,"rel":9622},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Fpricing\u002F",[167],"价格说明",[177,9625,9626],{},[163,9627,9630],{"href":9628,"rel":9629},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconcepts\u002Femail-authentication\u002F",[167],"邮件认证与投递",[177,9632,9633],{},[163,9634,9637],{"href":9635,"rel":9636},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Flocal-development\u002Frouting\u002F",[167],"本地测试 Email Worker",[1658,9639,9640],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":74,"searchDepth":729,"depth":729,"links":9642},[9643,9644,9648,9653,9659,9663,9664,9665,9666,9670,9671,9672,9673],{"id":7375,"depth":712,"text":7376},{"id":7461,"depth":712,"text":7462,"children":9645},[9646,9647],{"id":7468,"depth":729,"text":7469},{"id":7500,"depth":729,"text":7501},{"id":7527,"depth":712,"text":7528,"children":9649},[9650,9651],{"id":7534,"depth":729,"text":7534},{"id":7554,"depth":729,"text":9652},"发送邮件使用 cf-bounce 记录",{"id":7611,"depth":712,"text":7612,"children":9654},[9655,9657,9658],{"id":7636,"depth":729,"text":9656},"email() 是入站邮件入口",{"id":8102,"depth":729,"text":8103},{"id":8262,"depth":729,"text":8263},{"id":8318,"depth":712,"text":8319,"children":9660},[9661,9662],{"id":8434,"depth":729,"text":8435},{"id":8647,"depth":729,"text":8648},{"id":8979,"depth":712,"text":8980},{"id":9061,"depth":712,"text":9061},{"id":9195,"depth":712,"text":9195},{"id":9243,"depth":712,"text":9243,"children":9667},[9668,9669],{"id":9264,"depth":729,"text":9265},{"id":9321,"depth":729,"text":9322},{"id":9341,"depth":712,"text":9341},{"id":9465,"depth":712,"text":9466},{"id":9515,"depth":712,"text":9515},{"id":1561,"depth":712,"text":1561},"截至 2026 年 7 月，系统介绍 Cloudflare Email Service 如何接收、转发、解析、回复和发送事务邮件，并给出 Nuxt、Workers、D1 与安全限流实践。",{"src":9676,"alt":9677,"caption":9678,"width":9679,"height":9680},"\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Fcover-v1.webp","邮件经过全球边缘网络中的 Worker 节点完成接收、认证与发送","Cloudflare Email Service 正在把邮件从一台需要维护的服务器，变成一种可以由代码处理的事件",1600,900,{},"\u002Fblog\u002Fcloudflare-email-service-send-receive-guide","2026-07-28",{"title":7314,"description":9674},"blog\u002Fcloudflare-email-service-send-receive-guide",[4540,7309,9687,9688],"Workers","Serverless",{"src":9690,"poster":9676,"title":9691,"description":9692,"duration":9693,"width":4547,"height":4548},"\u002Fmedia\u002Fvideos\u002Fcloudflare-email-service-explained\u002Fcloudflare-email-service-explained-v1.mp4","Cloudflare Email Service 收发实战：11 分钟配置讲解","从 DNS、Email Routing、Email Worker 和 send_email binding，一路讲到 REST、SMTP、Nuxt、D1、五分钟验证码与生产检查。",661,"8xdTS_0QdYdFpDs8xaro-tFzQ7X8dtOlXKNmofjsMnA",{"id":9696,"title":9697,"accent":1712,"body":9698,"description":11383,"draft":1692,"experience":1693,"extension":1694,"featured":1696,"image":11384,"meta":11388,"navigation":1696,"path":11389,"publishedAt":11390,"readingTime":2860,"seo":11391,"stem":11392,"tags":11393,"updatedAt":1693,"video":1693,"__hash__":11397,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Fvue-3-6-rc-upgrade-guide.md","Vue 3.6 RC 升级全景：从 3.5 到 Vapor Mode 与 alien-signals",{"type":13,"value":9699,"toc":11344},[9700,9706,9719,9722,9731,9735,9875,9878,9883,9887,9890,9925,9928,9934,9938,9945,9948,10061,10064,10070,10075,10078,10081,10094,10100,10103,10106,10123,10126,10129,10145,10162,10165,10169,10172,10175,10183,10186,10190,10193,10316,10319,10347,10375,10378,10382,10385,10435,10438,10442,10449,10510,10513,10516,10522,10528,10531,10535,10538,10552,10561,10607,10614,10617,10629,10632,10636,10639,10776,10783,10789,10792,10879,10882,10916,10919,10932,10935,10939,10942,10945,10948,10952,10955,10959,10962,10965,10968,10972,10975,10989,10996,11013,11016,11042,11045,11049,11052,11072,11075,11079,11082,11105,11108,11112,11115,11118,11138,11141,11145,11148,11152,11155,11159,11162,11166,11169,11172,11254,11256,11259,11267,11270,11277,11280,11283,11286,11336,11341],[16,9701,9702,9703,1065],{},"如果只看版本号，Vue 3.6 像是一次普通的次版本升级。但真正读完发布说明后，我的感受是：",[20,9704,9705],{},"它更像 Vue 为下一阶段准备的一次“换发动机”",[16,9707,9708,9709,45,9712,45,9715,9718],{},"一边是基于 alien-signals 重构的响应式内核，公开的 ",[24,9710,9711],{},"ref",[24,9713,9714],{},"computed",[24,9716,9717],{},"watch"," 用法基本不变；另一边是正式进入 RC 阶段的 Vapor Mode，它让 Vue 单文件组件可以绕过虚拟 DOM，编译为更直接的 DOM 更新逻辑。",[16,9720,9721],{},"不过，先把最重要的版本信息放在前面：",[51,9723,9724],{},[16,9725,9726,9727,9730],{},"截至 2026 年 7 月 26 日，Vue 3.6 的最新版本是 ",[20,9728,9729],{},"3.6.0-rc.2","。RC 是候选发布版，不是稳定版。本文适合用来评估、试验和准备迁移，不代表所有生产项目都应该立即升级。",[64,9732,9734],{"id":9733},"先看结论36-改变了什么","先看结论：3.6 改变了什么",[85,9736,9737,9753],{},[88,9738,9739],{},[91,9740,9741,9744,9747,9750],{},[94,9742,9743],{},"对比维度",[94,9745,9746],{},"Vue 3.5",[94,9748,9749],{},"Vue 3.6 RC",[94,9751,9752],{},"对现有项目的实际影响",[107,9754,9755,9769,9783,9797,9811,9825,9843,9861],{},[91,9756,9757,9760,9763,9766],{},[112,9758,9759],{},"响应式系统",[112,9761,9762],{},"3.5 已做过内部优化",[112,9764,9765],{},"基于 alien-signals 大幅重构",[112,9767,9768],{},"常规 Composition API 写法通常不需要重写，但应回归复杂副作用与调度逻辑",[91,9770,9771,9774,9777,9780],{},[112,9772,9773],{},"默认渲染模式",[112,9775,9776],{},"Virtual DOM",[112,9778,9779],{},"仍然是 Virtual DOM",[112,9781,9782],{},"单纯升级依赖不会让应用自动进入 Vapor",[91,9784,9785,9788,9791,9794],{},[112,9786,9787],{},"Vapor Mode",[112,9789,9790],{},"尚未作为 3.5 的正式能力交付",[112,9792,9793],{},"功能集进入 RC，100% 显式启用",[112,9795,9796],{},"可以从单个性能敏感区域开始试验",[91,9798,9799,9802,9805,9808],{},[112,9800,9801],{},"应用体积",[112,9803,9804],{},"默认包含 VDOM runtime",[112,9806,9807],{},"纯 Vapor 应用可不引入 VDOM runtime",[112,9809,9810],{},"只有纯 Vapor 或边界清晰的场景，体积收益才容易兑现",[91,9812,9813,9816,9819,9822],{},[112,9814,9815],{},"API 覆盖",[112,9817,9818],{},"完整 Vue 3 API 生态",[112,9820,9821],{},"Vapor 支持 Vue API 的一个子集",[112,9823,9824],{},"Options API、实例代理相关能力不能直接迁移",[91,9826,9827,9830,9833,9840],{},[112,9828,9829],{},"VDOM \u002F Vapor 混用",[112,9831,9832],{},"不适用",[112,9834,9835,9836,9839],{},"通过 ",[24,9837,9838],{},"vaporInteropPlugin"," 互操作",[112,9841,9842],{},"标准 props、事件、slots 已覆盖，但复杂嵌套仍需测试",[91,9844,9845,9848,9851,9858],{},[112,9846,9847],{},"事件委托",[112,9849,9850],{},"原生监听语义",[112,9852,9853,9854,9857],{},"rc.2 起默认直接绑定；",[24,9855,9856],{},".delegate"," 才显式委托",[112,9859,9860],{},"不要沿用 rc.1 的默认委托认知",[91,9862,9863,9866,9869,9872],{},[112,9864,9865],{},"适合生产吗",[112,9867,9868],{},"稳定版",[112,9870,9871],{},"当前仍是预发布版",[112,9873,9874],{},"生产项目应等待稳定版和上层生态明确支持",[16,9876,9877],{},"一句话概括：",[16,9879,9880],{},[20,9881,9882],{},"Vue 3.5 的亮点更接近“开发体验和 SSR 能力升级”，Vue 3.6 的核心则是“响应式内核与渲染架构升级”。",[64,9884,9886],{"id":9885},"从-35-到-36变化重心已经不同","从 3.5 到 3.6：变化重心已经不同",[16,9888,9889],{},"Vue 3.5 没有破坏性变化，它带来的能力大多能直接被业务开发感知：",[373,9891,9892,9895,9901,9907,9913,9916,9922],{},[177,9893,9894],{},"响应式 Props 解构正式稳定；",[177,9896,9897,9900],{},[24,9898,9899],{},"useTemplateRef()"," 改善模板引用的组织方式；",[177,9902,9903,9906],{},[24,9904,9905],{},"onWatcherCleanup()"," 让 watcher 清理逻辑更自然；",[177,9908,9909,9912],{},[24,9910,9911],{},"useId()"," 提供 SSR 友好的稳定 ID；",[177,9914,9915],{},"异步组件支持懒激活（Lazy Hydration）；",[177,9917,9918,9921],{},[24,9919,9920],{},"data-allow-mismatch"," 可以声明允许的 hydration 差异；",[177,9923,9924],{},"Deferred Teleport 与自定义元素能力得到增强。",[16,9926,9927],{},"3.6 当然也包含大量修复，但它最值得关注的两件事——alien-signals 与 Vapor——都位于框架的底层。你可能不会在业务代码里多写一个新 API，却会面对新的性能特征、编译产物和运行时边界。",[16,9929,9930,9931],{},"这也决定了升级思路：",[20,9932,9933],{},"不要只检查“代码能不能编译”，还要检查响应式行为、SSR hydration、组件互操作和真实性能。",[64,9935,9937],{"id":9936},"变化一响应式内核切换到-alien-signals","变化一：响应式内核切换到 alien-signals",[16,9939,9940,9941,9944],{},"Vue 3.6 对 ",[24,9942,9943],{},"@vue\u002Freactivity"," 做了大规模重构，其基础来自 StackBlitz 开源的 alien-signals。",[16,9946,9947],{},"对于绝大多数业务开发者，下面这些代码仍然是熟悉的 Vue：",[68,9949,9951],{"className":6185,"code":9950,"language":6187,"meta":74,"style":74},"import { computed, ref, watchEffect } from 'vue'\n\nconst price = ref(99)\nconst count = ref(2)\nconst total = computed(() => price.value * count.value)\n\nwatchEffect(() => {\n  console.log(total.value)\n})\n",[24,9952,9953,9965,9969,9988,10006,10032,10036,10047,10057],{"__ignoreMap":74},[702,9954,9955,9957,9960,9962],{"class":704,"line":705},[702,9956,6194],{"class":1949},[702,9958,9959],{"class":708}," { computed, ref, watchEffect } ",[702,9961,6200],{"class":1949},[702,9963,9964],{"class":722}," 'vue'\n",[702,9966,9967],{"class":704,"line":712},[702,9968,2738],{"emptyLinePlaceholder":1696},[702,9970,9971,9973,9976,9978,9981,9983,9986],{"class":704,"line":729},[702,9972,6212],{"class":1949},[702,9974,9975],{"class":715}," price",[702,9977,6218],{"class":1949},[702,9979,9980],{"class":947}," ref",[702,9982,6411],{"class":708},[702,9984,9985],{"class":715},"99",[702,9987,6579],{"class":708},[702,9989,9990,9992,9995,9997,9999,10001,10004],{"class":704,"line":742},[702,9991,6212],{"class":1949},[702,9993,9994],{"class":715}," count",[702,9996,6218],{"class":1949},[702,9998,9980],{"class":947},[702,10000,6411],{"class":708},[702,10002,10003],{"class":715},"2",[702,10005,6579],{"class":708},[702,10007,10008,10010,10013,10015,10018,10021,10023,10026,10029],{"class":704,"line":755},[702,10009,6212],{"class":1949},[702,10011,10012],{"class":715}," total",[702,10014,6218],{"class":1949},[702,10016,10017],{"class":947}," computed",[702,10019,10020],{"class":708},"(() ",[702,10022,8752],{"class":1949},[702,10024,10025],{"class":708}," price.value ",[702,10027,10028],{"class":1949},"*",[702,10030,10031],{"class":708}," count.value)\n",[702,10033,10034],{"class":704,"line":778},[702,10035,2738],{"emptyLinePlaceholder":1696},[702,10037,10038,10041,10043,10045],{"class":704,"line":787},[702,10039,10040],{"class":947},"watchEffect",[702,10042,10020],{"class":708},[702,10044,8752],{"class":1949},[702,10046,6718],{"class":708},[702,10048,10049,10052,10054],{"class":704,"line":795},[702,10050,10051],{"class":708},"  console.",[702,10053,7853],{"class":947},[702,10055,10056],{"class":708},"(total.value)\n",[702,10058,10059],{"class":704,"line":807},[702,10060,6318],{"class":708},[16,10062,10063],{},"变化主要发生在内部：依赖如何建立、状态如何标记、计算属性何时失效、effect 如何沿依赖图传播，以及这些关系如何被清理和复用。",[16,10065,10066],{},[214,10067],{"alt":10068,"src":10069},"由活跃与休眠路径组成的细粒度响应式依赖网络","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Freactivity-core-v1.webp",[16,10071,10072],{},[221,10073,10074],{},"这张图表达的是依赖传播的概念，不是 Vue 内部数据结构的逐字段示意。",[156,10076,10077],{"id":10077},"性能数字应该怎样理解",[16,10079,10080],{},"Vue 核心仓库的 alien-signals 合并 PR 给出过两类微基准结果：",[373,10082,10083,10091],{},[177,10084,10085,10086,45,10088,10090],{},"在大量 ",[24,10087,9711],{},[24,10089,9714],{}," 和 effect 实例的构造场景中，示例内存从约 2.3 MB 降至约 2.0 MB，约减少 13%；",[177,10092,10093],{},"在“一个源变化后，大量 computed 被读取”的特定压力场景中，性能提升可以超过 30 倍。",[16,10095,10096,10097],{},"第二个数字很亮眼，但不能直接翻译成“Vue 3.6 应用快 30 倍”。它证明的是：",[20,10098,10099],{},"新的依赖传播算法在某些过去成本会随规模显著上升的图结构中，复杂度表现更好。",[16,10101,10102],{},"真实页面还包含 DOM、布局、网络、组件库、序列化和业务计算。升级前后的判断应基于自己项目的 flame chart、交互延迟、内存曲线和打包结果，而不是套用框架微基准。",[156,10104,10105],{"id":10105},"哪些项目最可能感受到收益",[373,10107,10108,10111,10114,10117,10120],{},[177,10109,10110],{},"大量 computed 相互依赖的数据看板；",[177,10112,10113],{},"高频修改、筛选和派生状态很多的编辑器；",[177,10115,10116],{},"长时间运行、创建和销毁大量 effect 的工作台；",[177,10118,10119],{},"表单字段多、验证规则和联动关系复杂的应用；",[177,10121,10122],{},"基于 Vue 响应式原语构建的状态管理或工具库。",[16,10124,10125],{},"普通内容页也能得到内部改进，但用户未必会感知到明显差异。",[156,10127,10128],{"id":10128},"谁需要格外谨慎",[16,10130,10131,10132,45,10134,45,10137,45,10139,912,10141,10144],{},"如果代码只使用公开的 ",[24,10133,9711],{},[24,10135,10136],{},"reactive",[24,10138,9714],{},[24,10140,9717],{},[24,10142,10143],{},"effectScope","，迁移风险相对可控。真正需要警惕的是：",[373,10146,10147,10150,10153,10156,10159],{},[177,10148,10149],{},"直接读取或修改 Vue 响应式内部字段；",[177,10151,10152],{},"依赖未公开的 effect、Dep 或 scheduler 实现细节；",[177,10154,10155],{},"自行 monkey patch 响应式函数；",[177,10157,10158],{},"对 watcher 执行顺序、同步刷新和清理时机有隐含假设；",[177,10160,10161],{},"库代码同时支持多个 Vue 次版本，却没有覆盖这些边界测试。",[16,10163,10164],{},"内部重构的意义，正是 Vue 可以在保持公开 API 的同时更换实现。反过来说，依赖内部实现的代码也最容易在这类升级中暴露问题。",[64,10166,10168],{"id":10167},"变化二vapor-mode-不再只是实验概念","变化二：Vapor Mode 不再只是实验概念",[16,10170,10171],{},"Vapor Mode 是 Vue SFC 的一种新编译模式。传统 Vue 模板会生成 VNode，由 Virtual DOM runtime 对比前后两棵树，再把差异更新到真实 DOM；Vapor 则让编译器提前识别动态部分，生成更直接、粒度更细的更新逻辑。",[16,10173,10174],{},"它的目标有两个：",[174,10176,10177,10180],{},[177,10178,10179],{},"降低应用的基础包体积；",[177,10181,10182],{},"减少运行时创建、遍历和比较 VNode 的成本。",[16,10184,10185],{},"官方发布说明称，Vapor 已在第三方 js-framework-benchmark 中达到与 Solid、Svelte 5 相近的级别。这个结论同样应该按“基准场景”理解，而不是把不同框架、不同应用的真实性能压缩成一条排名。",[156,10187,10189],{"id":10188},"vapor-不会在升级后自动开启","Vapor 不会在升级后自动开启",[16,10191,10192],{},"Vue 3.6 依然默认使用 VDOM。要让一个 SFC 使用 Vapor，需要显式标记：",[68,10194,10198],{"className":10195,"code":10196,"language":10197,"meta":74,"style":74},"language-vue shiki shiki-themes github-dark github-dark","\u003Cscript setup vapor lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton @click=\"count++\">\n    Count: {{ count }}\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n","vue",[24,10199,10200,10223,10234,10238,10254,10263,10267,10276,10294,10299,10308],{"__ignoreMap":74},[702,10201,10202,10204,10207,10210,10213,10216,10218,10221],{"class":704,"line":705},[702,10203,7718],{"class":708},[702,10205,10206],{"class":3229},"script",[702,10208,10209],{"class":947}," setup",[702,10211,10212],{"class":947}," vapor",[702,10214,10215],{"class":947}," lang",[702,10217,2990],{"class":708},[702,10219,10220],{"class":722},"\"ts\"",[702,10222,7981],{"class":708},[702,10224,10225,10227,10230,10232],{"class":704,"line":712},[702,10226,6194],{"class":1949},[702,10228,10229],{"class":708}," { ref } ",[702,10231,6200],{"class":1949},[702,10233,9964],{"class":722},[702,10235,10236],{"class":704,"line":729},[702,10237,2738],{"emptyLinePlaceholder":1696},[702,10239,10240,10242,10244,10246,10248,10250,10252],{"class":704,"line":742},[702,10241,6212],{"class":1949},[702,10243,9994],{"class":715},[702,10245,6218],{"class":1949},[702,10247,9980],{"class":947},[702,10249,6411],{"class":708},[702,10251,289],{"class":715},[702,10253,6579],{"class":708},[702,10255,10256,10259,10261],{"class":704,"line":755},[702,10257,10258],{"class":708},"\u003C\u002F",[702,10260,10206],{"class":3229},[702,10262,7981],{"class":708},[702,10264,10265],{"class":704,"line":778},[702,10266,2738],{"emptyLinePlaceholder":1696},[702,10268,10269,10271,10274],{"class":704,"line":787},[702,10270,7718],{"class":708},[702,10272,10273],{"class":3229},"template",[702,10275,7981],{"class":708},[702,10277,10278,10281,10284,10287,10289,10292],{"class":704,"line":795},[702,10279,10280],{"class":708},"  \u003C",[702,10282,10283],{"class":3229},"button",[702,10285,10286],{"class":947}," @click",[702,10288,2990],{"class":708},[702,10290,10291],{"class":722},"\"count++\"",[702,10293,7981],{"class":708},[702,10295,10296],{"class":704,"line":807},[702,10297,10298],{"class":708},"    Count: {{ count }}\n",[702,10300,10301,10304,10306],{"class":704,"line":815},[702,10302,10303],{"class":708},"  \u003C\u002F",[702,10305,10283],{"class":3229},[702,10307,7981],{"class":708},[702,10309,10310,10312,10314],{"class":704,"line":836},[702,10311,10258],{"class":708},[702,10313,10273],{"class":3229},[702,10315,7981],{"class":708},[16,10317,10318],{},"官方还支持两个等价入口：",[68,10320,10322],{"className":10195,"code":10321,"language":10197,"meta":74,"style":74},"\u003Cscript vapor>\n\u002F\u002F \u003Cscript setup vapor> 的简写\n\u003C\u002Fscript>\n",[24,10323,10324,10334,10339],{"__ignoreMap":74},[702,10325,10326,10328,10330,10332],{"class":704,"line":705},[702,10327,7718],{"class":708},[702,10329,10206],{"class":3229},[702,10331,10212],{"class":947},[702,10333,7981],{"class":708},[702,10335,10336],{"class":704,"line":712},[702,10337,10338],{"class":8831},"\u002F\u002F \u003Cscript setup vapor> 的简写\n",[702,10340,10341,10343,10345],{"class":704,"line":729},[702,10342,10258],{"class":708},[702,10344,10206],{"class":3229},[702,10346,7981],{"class":708},[68,10348,10350],{"className":10195,"code":10349,"language":10197,"meta":74,"style":74},"\u003Ctemplate vapor>\n  \u003C!-- 整个 SFC 使用 Vapor 编译 -->\n\u003C\u002Ftemplate>\n",[24,10351,10352,10362,10367],{"__ignoreMap":74},[702,10353,10354,10356,10358,10360],{"class":704,"line":705},[702,10355,7718],{"class":708},[702,10357,10273],{"class":3229},[702,10359,10212],{"class":947},[702,10361,7981],{"class":708},[702,10363,10364],{"class":704,"line":712},[702,10365,10366],{"class":8831},"  \u003C!-- 整个 SFC 使用 Vapor 编译 -->\n",[702,10368,10369,10371,10373],{"class":704,"line":729},[702,10370,10258],{"class":708},[702,10372,10273],{"class":3229},[702,10374,7981],{"class":708},[16,10376,10377],{},"也就是说，团队可以升级到 3.6 后继续运行现有 VDOM 应用，再有选择地迁移某个组件或页面。",[156,10379,10381],{"id":10380},"纯-vapor-应用","纯 Vapor 应用",[16,10383,10384],{},"如果应用完全由 Vapor 组件组成，可以使用：",[68,10386,10388],{"className":6185,"code":10387,"language":6187,"meta":74,"style":74},"import { createVaporApp } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateVaporApp(App).mount('#app')\n",[24,10389,10390,10401,10413,10417],{"__ignoreMap":74},[702,10391,10392,10394,10397,10399],{"class":704,"line":705},[702,10393,6194],{"class":1949},[702,10395,10396],{"class":708}," { createVaporApp } ",[702,10398,6200],{"class":1949},[702,10400,9964],{"class":722},[702,10402,10403,10405,10408,10410],{"class":704,"line":712},[702,10404,6194],{"class":1949},[702,10406,10407],{"class":708}," App ",[702,10409,6200],{"class":1949},[702,10411,10412],{"class":722}," '.\u002FApp.vue'\n",[702,10414,10415],{"class":704,"line":729},[702,10416,2738],{"emptyLinePlaceholder":1696},[702,10418,10419,10422,10425,10428,10430,10433],{"class":704,"line":742},[702,10420,10421],{"class":947},"createVaporApp",[702,10423,10424],{"class":708},"(App).",[702,10426,10427],{"class":947},"mount",[702,10429,6411],{"class":708},[702,10431,10432],{"class":722},"'#app'",[702,10434,6579],{"class":708},[16,10436,10437],{},"这种模式不需要引入 Virtual DOM runtime，最容易获得基础包体积收益。它更适合小型新应用、交互小工具，或可以明确约束技术栈的独立页面。",[156,10439,10441],{"id":10440},"在现有-vdom-应用中使用-vapor","在现有 VDOM 应用中使用 Vapor",[16,10443,10444,10445,10448],{},"现有 ",[24,10446,10447],{},"createApp()"," 项目需要安装互操作插件：",[68,10450,10452],{"className":6185,"code":10451,"language":6187,"meta":74,"style":74},"import { createApp, vaporInteropPlugin } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateApp(App)\n  .use(vaporInteropPlugin)\n  .mount('#app')\n",[24,10453,10454,10465,10475,10479,10487,10498],{"__ignoreMap":74},[702,10455,10456,10458,10461,10463],{"class":704,"line":705},[702,10457,6194],{"class":1949},[702,10459,10460],{"class":708}," { createApp, vaporInteropPlugin } ",[702,10462,6200],{"class":1949},[702,10464,9964],{"class":722},[702,10466,10467,10469,10471,10473],{"class":704,"line":712},[702,10468,6194],{"class":1949},[702,10470,10407],{"class":708},[702,10472,6200],{"class":1949},[702,10474,10412],{"class":722},[702,10476,10477],{"class":704,"line":729},[702,10478,2738],{"emptyLinePlaceholder":1696},[702,10480,10481,10484],{"class":704,"line":742},[702,10482,10483],{"class":947},"createApp",[702,10485,10486],{"class":708},"(App)\n",[702,10488,10489,10492,10495],{"class":704,"line":755},[702,10490,10491],{"class":708},"  .",[702,10493,10494],{"class":947},"use",[702,10496,10497],{"class":708},"(vaporInteropPlugin)\n",[702,10499,10500,10502,10504,10506,10508],{"class":704,"line":778},[702,10501,10491],{"class":708},[702,10503,10427],{"class":947},[702,10505,6411],{"class":708},[702,10507,10432],{"class":722},[702,10509,6579],{"class":708},[16,10511,10512],{},"反过来，Vapor 应用也能安装这个插件以容纳 VDOM 组件，但这样会重新带入 VDOM runtime，削弱体积优势。",[16,10514,10515],{},"Render Function 与 JSX 编写的组件仍然属于 VDOM 组件。在纯 Vapor 应用中使用它们，同样需要互操作。",[16,10517,10518],{},[214,10519],{"alt":10520,"src":10521},"现有 VDOM 区域通过明确的互操作边界连接到独立 Vapor 区域","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fvapor-boundaries-v1.webp",[16,10523,10524,10525],{},"我的建议与官方一致：",[20,10526,10527],{},"把两种渲染模式组织成边界清楚的区域，而不是在每一层组件中来回套娃。",[16,10529,10530],{},"例如，把一个高频更新的数据画布做成 Vapor 页面或子树，周围的后台导航、弹窗和成熟组件库继续使用 VDOM。这样的收益、故障范围和回退路径都更容易测量。",[64,10532,10534],{"id":10533},"rc2-最容易踩坑的变化事件委托改为显式启用","rc.2 最容易踩坑的变化：事件委托改为显式启用",[16,10536,10537],{},"如果你在 rc.1 阶段已经试过 Vapor，这一节尤其重要。",[16,10539,10540,10541,10544,10545,10548,10549,10551],{},"rc.1 的说明中，符合条件的事件会自动委托到 ",[24,10542,10543],{},"document","。但这与原生 DOM 和标准 Vue 的行为存在差异：如果祖先元素提前调用 ",[24,10546,10547],{},"stopPropagation()","，事件到不了 ",[24,10550,10543],{},"，子元素被委托的 handler 也不会执行。",[16,10553,10554,10555,10557,10558,10560],{},"从 ",[20,10556,9729],{}," 开始，Vapor 默认把 DOM 监听器直接绑定到元素。只有明确希望使用文档级委托时，才添加 Vapor 专用的 ",[24,10559,9856],{}," 修饰符：",[68,10562,10564],{"className":10195,"code":10563,"language":10197,"meta":74,"style":74},"\u003Cbutton @click.delegate=\"onClick\">\n  保存\n\u003C\u002Fbutton>\n",[24,10565,10566,10594,10599],{"__ignoreMap":74},[702,10567,10568,10570,10572,10575,10578,10580,10583,10585,10587,10590,10592],{"class":704,"line":705},[702,10569,7718],{"class":708},[702,10571,10283],{"class":3229},[702,10573,10574],{"class":708}," @",[702,10576,10577],{"class":947},"click",[702,10579,2937],{"class":708},[702,10581,10582],{"class":947},"delegate",[702,10584,2990],{"class":708},[702,10586,3014],{"class":722},[702,10588,10589],{"class":708},"onClick",[702,10591,3014],{"class":722},[702,10593,7981],{"class":708},[702,10595,10596],{"class":704,"line":712},[702,10597,10598],{"class":708},"  保存\n",[702,10600,10601,10603,10605],{"class":704,"line":729},[702,10602,10258],{"class":708},[702,10604,10283],{"class":3229},[702,10606,7981],{"class":708},[16,10608,10609,10610,10613],{},"同时，旧的 ",[24,10611,10612],{},"compilerOptions.eventDelegation"," 已被移除。",[16,10615,10616],{},"迁移时应该做两件事：",[174,10618,10619,10624],{},[177,10620,10621,10622,2039],{},"删除已有的 ",[24,10623,10612],{},[177,10625,10626,10627,1065],{},"只对数量大、事件稳定，并且传播路径已验证的监听器使用 ",[24,10628,9856],{},[16,10630,10631],{},"这不是一个适合全局机械替换的优化。事件是否值得委托，要结合节点数量、生命周期和传播语义判断。",[64,10633,10635],{"id":10634},"vapor-与标准-vue-的兼容边界","Vapor 与标准 Vue 的兼容边界",[16,10637,10638],{},"Vapor 的目标不是在第一天覆盖 Vue 的所有历史能力。官方明确说明，它支持现有 Vue API 的一个子集；依赖 VNode 或组件公开实例代理的功能，目前不能直接使用。",[85,10640,10641,10653],{},[88,10642,10643],{},[91,10644,10645,10647,10650],{},[94,10646,8000],{},[94,10648,10649],{},"VDOM 组件",[94,10651,10652],{},"Vapor 组件",[107,10654,10655,10666,10676,10687,10703,10714,10726,10738,10765],{},[91,10656,10657,10660,10663],{},[112,10658,10659],{},"Composition API",[112,10661,10662],{},"支持",[112,10664,10665],{},"主要编程方式",[91,10667,10668,10671,10673],{},[112,10669,10670],{},"Options API",[112,10672,10662],{},[112,10674,10675],{},"不支持",[91,10677,10678,10683,10685],{},[112,10679,10680],{},[24,10681,10682],{},"app.config.globalProperties",[112,10684,10662],{},[112,10686,10675],{},[91,10688,10689,10694,10697],{},[112,10690,10691],{},[24,10692,10693],{},"getCurrentInstance()",[112,10695,10696],{},"返回当前实例",[112,10698,10699,10700],{},"返回 ",[24,10701,10702],{},"null",[91,10704,10705,10708,10711],{},[112,10706,10707],{},"Render Function \u002F JSX",[112,10709,10710],{},"原生 VDOM",[112,10712,10713],{},"仍是 VDOM，混用需要 interop",[91,10715,10716,10721,10723],{},[112,10717,10718],{},[24,10719,10720],{},"v-memo",[112,10722,10662],{},[112,10724,10725],{},"不适用 \u002F 不支持",[91,10727,10728,10734,10736],{},[112,10729,10730,10733],{},[24,10731,10732],{},"@vue:xxx"," 元素生命周期事件",[112,10735,10662],{},[112,10737,10675],{},[91,10739,10740,10743,10746],{},[112,10741,10742],{},"组件模板引用",[112,10744,10745],{},"暴露公开实例能力",[112,10747,10748,10749,45,10752,45,10755,45,10758,45,10761,10764],{},"不暴露 ",[24,10750,10751],{},"$el",[24,10753,10754],{},"$props",[24,10756,10757],{},"$attrs",[24,10759,10760],{},"$slots",[24,10762,10763],{},"$refs"," 等属性",[91,10766,10767,10770,10773],{},[112,10768,10769],{},"自定义指令",[112,10771,10772],{},"标准指令生命周期对象",[112,10774,10775],{},"使用 Vapor 专用函数接口",[156,10777,10779,10782],{"id":10778},"slotsdefault-不能再当作无副作用探测",[24,10780,10781],{},"slots.default()"," 不能再当作无副作用探测",[16,10784,10785,10786,10788],{},"在 VDOM 世界中，一些组件会先调用 ",[24,10787,10781],{},"，检查返回内容，再决定渲染 fallback。Vapor 中调用 slot 可能直接创建 Block 和 DOM、注册响应式 effect，或在 hydration 时认领已有 SSR DOM。",[16,10790,10791],{},"因此不要这样写：",[68,10793,10795],{"className":10195,"code":10794,"language":10197,"meta":74,"style":74},"\u003Cscript setup vapor>\nimport { useSlots } from 'vue'\n\nconst slots = useSlots()\nconst content = slots.default?.()\nconst showFallback = !content\n\u003C\u002Fscript>\n",[24,10796,10797,10809,10820,10824,10838,10856,10871],{"__ignoreMap":74},[702,10798,10799,10801,10803,10805,10807],{"class":704,"line":705},[702,10800,7718],{"class":708},[702,10802,10206],{"class":3229},[702,10804,10209],{"class":947},[702,10806,10212],{"class":947},[702,10808,7981],{"class":708},[702,10810,10811,10813,10816,10818],{"class":704,"line":712},[702,10812,6194],{"class":1949},[702,10814,10815],{"class":708}," { useSlots } ",[702,10817,6200],{"class":1949},[702,10819,9964],{"class":722},[702,10821,10822],{"class":704,"line":729},[702,10823,2738],{"emptyLinePlaceholder":1696},[702,10825,10826,10828,10831,10833,10836],{"class":704,"line":742},[702,10827,6212],{"class":1949},[702,10829,10830],{"class":715}," slots",[702,10832,6218],{"class":1949},[702,10834,10835],{"class":947}," useSlots",[702,10837,7743],{"class":708},[702,10839,10840,10842,10845,10847,10850,10853],{"class":704,"line":755},[702,10841,6212],{"class":1949},[702,10843,10844],{"class":715}," content",[702,10846,6218],{"class":1949},[702,10848,10849],{"class":708}," slots.",[702,10851,10852],{"class":947},"default",[702,10854,10855],{"class":708},"?.()\n",[702,10857,10858,10860,10863,10865,10868],{"class":704,"line":778},[702,10859,6212],{"class":1949},[702,10861,10862],{"class":715}," showFallback",[702,10864,6218],{"class":1949},[702,10866,10867],{"class":1949}," !",[702,10869,10870],{"class":708},"content\n",[702,10872,10873,10875,10877],{"class":704,"line":787},[702,10874,10258],{"class":708},[702,10876,10206],{"class":3229},[702,10878,7981],{"class":708},[16,10880,10881],{},"让模板负责 slot 的实际渲染，通常更安全：",[68,10883,10885],{"className":10195,"code":10884,"language":10197,"meta":74,"style":74},"\u003Ctemplate>\n  \u003Cslot \u002F>\n\u003C\u002Ftemplate>\n",[24,10886,10887,10895,10908],{"__ignoreMap":74},[702,10888,10889,10891,10893],{"class":704,"line":705},[702,10890,7718],{"class":708},[702,10892,10273],{"class":3229},[702,10894,7981],{"class":708},[702,10896,10897,10899,10902,10906],{"class":704,"line":712},[702,10898,10280],{"class":708},[702,10900,10901],{"class":3229},"slot",[702,10903,10905],{"class":10904},"sX7ps"," \u002F",[702,10907,7981],{"class":708},[702,10909,10910,10912,10914],{"class":704,"line":729},[702,10911,10258],{"class":708},[702,10913,10273],{"class":3229},[702,10915,7981],{"class":708},[156,10917,10918],{"id":10918},"自定义指令需要单独迁移",[16,10920,10921,10922,45,10925,45,10928,10931],{},"Vapor 自定义指令接收节点和响应式 getter，并且可以返回 cleanup 函数。它不是把 VDOM 指令的 ",[24,10923,10924],{},"mounted",[24,10926,10927],{},"updated",[24,10929,10930],{},"unmounted"," 对象原样搬过去。",[16,10933,10934],{},"如果项目或组件库依赖大量自定义指令，应该把它们列成独立迁移项，逐个验证响应式更新、清理和 SSR 行为。",[64,10936,10938],{"id":10937},"nuxtrouterpinia-和组件库应该怎么看","Nuxt、Router、Pinia 和组件库应该怎么看",[16,10940,10941],{},"Vue 核心可用，不代表上层框架已经自动完成 Vapor 集成。",[16,10943,10944],{},"Vue 官方的 Vapor Roadmap 单独列出了 Router、Pinia、Nuxt、DevTools 和 Vue Test Utils 等生态工作，其中 Nuxt 还依赖 SSR hydration 能力。对真实项目来说，编译器能否识别 Vapor、服务端产物如何生成、客户端如何 hydration、开发工具怎样检查组件，都是完整链路的一部分。",[16,10946,10947],{},"因此我会把项目分成两类：",[156,10949,10951],{"id":10950},"普通-vue-vite-应用","普通 Vue \u002F Vite 应用",[16,10953,10954],{},"可以在分支中升级 3.6 RC，先验证现有 VDOM 模式，再挑一个叶子页面试用 Vapor。依赖关系比较直接，问题也更容易归因。",[156,10956,10958],{"id":10957},"nuxtssr-或重度组件库应用","Nuxt、SSR 或重度组件库应用",[16,10960,10961],{},"不要仅通过 package override 强行把底层 Vue 换成 RC，就把它视为完成 Vapor 升级。应该先确认当前框架、构建插件和 SSR 链路明确支持对应版本，再进行 hydration、路由切换、异步组件和组件库互操作测试。",[16,10963,10964],{},"即使暂时不使用 Vapor，测试 Vue 3.6 的新响应式内核仍然有价值；只是这类试验最好留在独立分支或测试环境。",[64,10966,10967],{"id":10967},"一套更稳妥的升级路线",[156,10969,10971],{"id":10970},"第一阶段只升级依赖不启用-vapor","第一阶段：只升级依赖，不启用 Vapor",[16,10973,10974],{},"普通 Vue \u002F Vite 项目可以在实验分支中安装 RC：",[68,10976,10978],{"className":938,"code":10977,"language":940,"meta":74,"style":74},"pnpm add vue@3.6.0-rc.2\n",[24,10979,10980],{"__ignoreMap":74},[702,10981,10982,10984,10986],{"class":704,"line":705},[702,10983,6142],{"class":947},[702,10985,6145],{"class":722},[702,10987,10988],{"class":722}," vue@3.6.0-rc.2\n",[16,10990,10991,10992,10995],{},"如果项目直接依赖 ",[24,10993,10994],{},"@vue\u002Fserver-renderer","，让它与 Vue 保持同一版本：",[68,10997,10999],{"className":938,"code":10998,"language":940,"meta":74,"style":74},"pnpm add vue@3.6.0-rc.2 @vue\u002Fserver-renderer@3.6.0-rc.2\n",[24,11000,11001],{"__ignoreMap":74},[702,11002,11003,11005,11007,11010],{"class":704,"line":705},[702,11004,6142],{"class":947},[702,11006,6145],{"class":722},[702,11008,11009],{"class":722}," vue@3.6.0-rc.2",[702,11011,11012],{"class":722}," @vue\u002Fserver-renderer@3.6.0-rc.2\n",[16,11014,11015],{},"然后先验证原有 VDOM 应用：",[373,11017,11018,11021,11024,11027,11030,11033,11036,11039],{},[177,11019,11020],{},"单元测试与组件测试；",[177,11022,11023],{},"首屏 SSR 与客户端 hydration；",[177,11025,11026],{},"watch、computed、effectScope 的复杂场景；",[177,11028,11029],{},"快速路由切换后的副作用清理；",[177,11031,11032],{},"自定义指令、Teleport、Suspense 和异步组件；",[177,11034,11035],{},"开发与生产构建；",[177,11037,11038],{},"浏览器控制台 warning 和错误；",[177,11040,11041],{},"关键交互的耗时与内存基线。",[16,11043,11044],{},"如果这一阶段失败，问题大概率来自新响应式实现或 RC 回归，而不是 Vapor，定位会简单得多。",[156,11046,11048],{"id":11047},"第二阶段选择一个边界清楚的区域","第二阶段：选择一个边界清楚的区域",[16,11050,11051],{},"合适的试点通常同时满足：",[373,11053,11054,11057,11060,11063,11066,11069],{},[177,11055,11056],{},"使用 Composition API；",[177,11058,11059],{},"很少依赖组件实例；",[177,11061,11062],{},"不依赖复杂的 VDOM 组件库；",[177,11064,11065],{},"更新频率高，确实存在性能压力；",[177,11067,11068],{},"有独立路由或明确的组件树边界；",[177,11070,11071],{},"有可重复的性能测试。",[16,11073,11074],{},"先记录迁移前的 JavaScript 体积、交互延迟、CPU 时间和内存，再启用 Vapor。没有基线，就很容易把“技术上成功运行”误判成“值得迁移”。",[156,11076,11078],{"id":11077},"第三阶段验证互操作与-ssr","第三阶段：验证互操作与 SSR",[16,11080,11081],{},"至少覆盖这些组合：",[174,11083,11084,11087,11090,11093,11096,11099,11102],{},[177,11085,11086],{},"VDOM 父组件嵌套 Vapor 子组件；",[177,11088,11089],{},"Vapor 父组件嵌套 VDOM 子组件；",[177,11091,11092],{},"props、emit 与双向绑定；",[177,11094,11095],{},"默认 slot、具名 slot 与作用域 slot；",[177,11097,11098],{},"Suspense、异步组件与错误边界；",[177,11100,11101],{},"服务端输出、hydration 和 mismatch 恢复；",[177,11103,11104],{},"组件卸载后的 watcher、事件与指令 cleanup。",[16,11106,11107],{},"rc.2 的修复列表中有相当多内容正好集中在 hydration、Suspense、slot anchor 和 VDOM interop。这说明这些区域正在快速收敛，也说明它们是当前最值得测试的地方。",[156,11109,11111],{"id":11110},"第四阶段等稳定版再决定生产节奏","第四阶段：等稳定版再决定生产节奏",[16,11113,11114],{},"RC 的意义是功能集已经接近确定，接下来集中发现回归和边缘问题；它不等于生态已经完全稳定。",[16,11116,11117],{},"生产项目至少应该同时满足：",[373,11119,11120,11123,11126,11129,11132,11135],{},[177,11121,11122],{},"Vue 3.6 正式版已发布；",[177,11124,11125],{},"使用的框架和构建工具明确支持；",[177,11127,11128],{},"核心组件库通过互操作验证；",[177,11130,11131],{},"DevTools、测试工具和 SSR 链路可用；",[177,11133,11134],{},"性能收益足以覆盖迁移与维护成本；",[177,11136,11137],{},"有随时退回 VDOM 的方案。",[64,11139,11140],{"id":11140},"不建议做的四件事",[156,11142,11144],{"id":11143},"_1-不要把-rc-当成普通补丁版","1. 不要把 RC 当成普通补丁版",[16,11146,11147],{},"预发布阶段的行为仍可能调整。rc.1 到 rc.2 的事件委托变化，就是最直接的例子。",[156,11149,11151],{"id":11150},"_2-不要为了用上-vapor迁移全部组件","2. 不要为了“用上 Vapor”迁移全部组件",[16,11153,11154],{},"导航、静态内容、低频设置页未必能获得有意义的收益。优先迁移性能瓶颈，而不是追求迁移比例。",[156,11156,11158],{"id":11157},"_3-不要把微基准数字当作应用承诺","3. 不要把微基准数字当作应用承诺",[16,11160,11161],{},"“超过 30 倍”来自特定响应式压力场景；“与 Solid、Svelte 5 同级”来自第三方基准。它们适合证明技术方向，不适合替代项目测量。",[156,11163,11165],{"id":11164},"_4-不要忽略生态边界","4. 不要忽略生态边界",[16,11167,11168],{},"Vue 核心、SFC 编译器、Vite 插件、Nuxt、SSR、Router、Pinia、组件库、DevTools 和测试工具共同组成开发体验。只让其中一个包进入 RC 版本，不代表整条链路已经升级完毕。",[64,11170,11171],{"id":11171},"我的项目类型建议",[85,11173,11174,11186],{},[88,11175,11176],{},[91,11177,11178,11181,11184],{},[94,11179,11180],{},"项目类型",[94,11182,11183],{},"现在是否值得试",[94,11185,256],{},[107,11187,11188,11199,11210,11221,11232,11243],{},[91,11189,11190,11193,11196],{},[112,11191,11192],{},"小型新工具、内部实验",[112,11194,11195],{},"值得",[112,11197,11198],{},"可以尝试纯 Vapor，重点测包体积与交互",[91,11200,11201,11204,11207],{},[112,11202,11203],{},"已有 Vue SPA",[112,11205,11206],{},"值得在分支试",[112,11208,11209],{},"先保持 VDOM，再迁移单个性能敏感页面",[91,11211,11212,11215,11218],{},[112,11213,11214],{},"Nuxt \u002F SSR 站点",[112,11216,11217],{},"适合研究，生产宜等待",[112,11219,11220],{},"先确认框架支持，重点测 hydration",[91,11222,11223,11226,11229],{},[112,11224,11225],{},"组件库",[112,11227,11228],{},"应尽早做兼容测试",[112,11230,11231],{},"检查实例代理、slots、自定义指令和 interop",[91,11233,11234,11237,11240],{},[112,11235,11236],{},"大型数据看板 \u002F 编辑器",[112,11238,11239],{},"很值得压测",[112,11241,11242],{},"单独比较 alien-signals 与 Vapor 各自带来的收益",[91,11244,11245,11248,11251],{},[112,11246,11247],{},"内容型官网",[112,11249,11250],{},"优先级较低",[112,11252,11253],{},"等稳定版和生态成熟，避免为有限收益增加复杂度",[64,11255,9515],{"id":9515},[16,11257,11258],{},"Vue 3.6 RC 最有价值的地方，不是提供了多少个可以立刻写进业务的新 API，而是同时推进了两个长期问题：",[373,11260,11261,11264],{},[177,11262,11263],{},"Vue 的响应式系统能否在更复杂的依赖图中保持高效；",[177,11265,11266],{},"Vue 的模板能否在保留熟悉开发体验的同时，摆脱必须依赖虚拟 DOM 的成本。",[16,11268,11269],{},"alien-signals 回答第一个问题，Vapor 尝试回答第二个问题。",[16,11271,11272,11273,11276],{},"对于现有项目，我认为最合理的策略是：",[20,11274,11275],{},"先把 3.6 当作响应式内核升级来测试，再把 Vapor 当作可选的局部编译模式来评估。"," 两件事分开验证，才能知道性能变化来自哪里，也能把风险控制在清晰的边界内。",[16,11278,11279],{},"Vue 3.6 还没有到“所有人今天就应该升级”的阶段，但它已经到了“值得所有 Vue 团队开始理解”的阶段。",[11281,11282],"hr",{},[64,11284,11285],{"id":11285},"资料说明",[373,11287,11288,11296,11304,11312,11320,11328],{},[177,11289,11290,11295],{},[163,11291,11294],{"href":11292,"rel":11293},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fblob\u002Fminor\u002FCHANGELOG.md",[167],"Vue 3.6 minor 分支 Changelog","：3.6.0-rc.1 的 Vapor 总览、兼容边界，以及 rc.2 的事件委托变更。",[177,11297,11298,11303],{},[163,11299,11302],{"href":11300,"rel":11301},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Freleases\u002Ftag\u002Fv3.6.0-rc.2",[167],"Vue 3.6.0-rc.2 Release","：本文发布时的最新候选版本。",[177,11305,11306,11311],{},[163,11307,11310],{"href":11308,"rel":11309},"https:\u002F\u002Fblog.vuejs.org\u002Fposts\u002Fvue-3-5",[167],"Vue 3.5 发布说明","：Vue 3.5 的响应式优化、SSR 与开发者 API 基线。",[177,11313,11314,11319],{},[163,11315,11318],{"href":11316,"rel":11317},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fpull\u002F12349",[167],"Vue core：alien-signals 重构 PR","：响应式实现、内存与特定压力场景微基准。",[177,11321,11322,11327],{},[163,11323,11326],{"href":11324,"rel":11325},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fissues\u002F13687",[167],"Vapor Mode Roadmap","：核心能力与 Vue 生态集成路线。",[177,11329,11330,11335],{},[163,11331,11334],{"href":11332,"rel":11333},"https:\u002F\u002Fgithub.com\u002Fkrausest\u002Fjs-framework-benchmark",[167],"js-framework-benchmark","：Vue 官方发布说明引用的第三方前端框架基准。",[51,11337,11338],{},[16,11339,11340],{},"本文中的升级建议是我基于官方资料和工程经验做出的判断，文章由 AI 协助资料整理与语言校对；具体项目仍应以自身测试结果为准。",[1658,11342,11343],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sxg3X, html code.shiki .sxg3X{--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sX7ps, html code.shiki .sX7ps{--shiki-default:#FDAEB7;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":74,"searchDepth":729,"depth":729,"links":11345},[11346,11347,11348,11353,11358,11359,11364,11368,11374,11380,11381,11382],{"id":9733,"depth":712,"text":9734},{"id":9885,"depth":712,"text":9886},{"id":9936,"depth":712,"text":9937,"children":11349},[11350,11351,11352],{"id":10077,"depth":729,"text":10077},{"id":10105,"depth":729,"text":10105},{"id":10128,"depth":729,"text":10128},{"id":10167,"depth":712,"text":10168,"children":11354},[11355,11356,11357],{"id":10188,"depth":729,"text":10189},{"id":10380,"depth":729,"text":10381},{"id":10440,"depth":729,"text":10441},{"id":10533,"depth":712,"text":10534},{"id":10634,"depth":712,"text":10635,"children":11360},[11361,11363],{"id":10778,"depth":729,"text":11362},"slots.default() 不能再当作无副作用探测",{"id":10918,"depth":729,"text":10918},{"id":10937,"depth":712,"text":10938,"children":11365},[11366,11367],{"id":10950,"depth":729,"text":10951},{"id":10957,"depth":729,"text":10958},{"id":10967,"depth":712,"text":10967,"children":11369},[11370,11371,11372,11373],{"id":10970,"depth":729,"text":10971},{"id":11047,"depth":729,"text":11048},{"id":11077,"depth":729,"text":11078},{"id":11110,"depth":729,"text":11111},{"id":11140,"depth":712,"text":11140,"children":11375},[11376,11377,11378,11379],{"id":11143,"depth":729,"text":11144},{"id":11150,"depth":729,"text":11151},{"id":11157,"depth":729,"text":11158},{"id":11164,"depth":729,"text":11165},{"id":11171,"depth":712,"text":11171},{"id":9515,"depth":712,"text":9515},{"id":11285,"depth":712,"text":11285},"截至 Vue 3.6.0-rc.2，系统对比 Vue 3.5 与 3.6 的响应式内核、Vapor Mode、事件委托、互操作边界和渐进升级策略。",{"src":11385,"alt":11386,"caption":11387,"width":9679,"height":9680},"\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fcover-v1.webp","Vue 3.5 的虚拟 DOM 组件树经过晶体通道转向 Vue 3.6 的细粒度信号网络","Vue 3.6 RC 更像一次编译器与运行时的架构换挡，而不是一份新 API 清单",{},"\u002Fblog\u002Fvue-3-6-rc-upgrade-guide","2026-07-26",{"title":9697,"description":11383},"blog\u002Fvue-3-6-rc-upgrade-guide",[11394,11395,11396],"Vue","JavaScript","前端工程","L8sIMfVAKUKqWTcVSvsseRJeOmYbzw7h958oo1EPLCo",{"id":11399,"title":11400,"accent":11,"body":11401,"description":11635,"draft":1692,"experience":11636,"extension":1694,"featured":1696,"image":11641,"meta":11645,"navigation":1696,"path":11646,"publishedAt":11647,"readingTime":795,"seo":11648,"stem":11649,"tags":11650,"updatedAt":1693,"video":1693,"__hash__":11654,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Ffrom-gpt-4-invite-to-agents.md","从一封 GPT‑4 邀请邮件，到智能体时代",{"type":13,"value":11402,"toc":11627},[11403,11406,11409,11414,11421,11427,11430,11433,11437,11440,11443,11449,11452,11455,11458,11461,11475,11478,11495,11501,11504,11507,11511,11514,11517,11531,11534,11548,11551,11555,11558,11561,11578,11581,11585,11588,11591,11594,11597,11599,11601],[16,11404,11405],{},"今天整理旧邮件时，我重新翻到了一封 2023 年 3 月 18 日的邮件。",[16,11407,11408],{},"邮件的标题很直接：",[51,11410,11411],{},[16,11412,11413],{},"You’re invited to use the OpenAI GPT-4 API.",[16,11415,11416,11417,11420],{},"正文告诉我，已经可以通过 API 使用拥有 ",[20,11418,11419],{},"8K context"," 的 GPT‑4。",[16,11422,11423],{},[214,11424],{"alt":11425,"src":11426},"2023 年 GPT‑4 API 邀请邮件，正文说明可使用 8K context","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Femail-invite-v1.webp",[16,11428,11429],{},"那时 GPT‑4 才刚发布几天。API 需要申请、等待，再由 OpenAI 逐步邀请开发者。官方发布资料里，标准 GPT‑4 的上下文长度是 8,192 tokens，同时有限开放 32,768-token 版本。对当时的我来说，一次 API 访问资格本身，就像一扇刚刚推开的小门。",[16,11431,11432],{},"三年多以后再看，这封邮件已经不像一封产品通知，更像一枚时间胶囊。",[64,11434,11436],{"id":11435},"数字变大只是最容易看到的变化","数字变大，只是最容易看到的变化",[16,11438,11439],{},"早期开发者熟悉的是 4K、8K 量级的上下文。写提示词时，我们会计算还剩多少空间，主动裁剪资料，把长文拆成小段，再想办法让下一次请求接住上一次的结果。",[16,11441,11442],{},"到了 2026 年，百万级上下文已经进入主流前沿模型。OpenAI 当前模型目录列出的 GPT‑5.6 系列拥有约 1.05M context window；Google 的长上下文文档也说明，多款 Gemini 模型支持 1M 或更多 tokens。",[16,11444,11445],{},[214,11446],{"alt":11447,"src":11448},"从 4K、8K 到 1M 以上，上下文从短纸条延伸为多模态资料长廊","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcontext-window-v1.webp",[16,11450,11451],{},"这不只是“能塞进更多文字”。代码、图片、音频、视频和大批文档可以共同成为一次任务的背景。以前必须先切碎、索引和摘要的资料，现在有更多机会被放进同一个工作上下文。",[16,11453,11454],{},"但更长的上下文不等于完美记忆，也不等于模型会自动抓住重点。资料越多，噪声、成本、延迟和错误关联也可能越多。真正重要的能力，仍然是选择什么信息、以什么顺序提供，以及如何验证模型是否理解了关键约束。",[64,11456,11457],{"id":11457},"真正改变的是交互单位",[16,11459,11460],{},"三年前，我最常做的是“提问—回答”：",[174,11462,11463,11466,11469,11472],{},[177,11464,11465],{},"写一段提示词；",[177,11467,11468],{},"等待模型生成；",[177,11470,11471],{},"复制结果；",[177,11473,11474],{},"自己完成剩余工作。",[16,11476,11477],{},"今天，越来越多任务变成了“目标—上下文—行动—验证”：",[174,11479,11480,11483,11486,11489,11492],{},[177,11481,11482],{},"说明最终目标与完成标准；",[177,11484,11485],{},"提供代码库、文档和当前状态；",[177,11487,11488],{},"允许模型调用搜索、文件、终端或其他工具；",[177,11490,11491],{},"让它执行一连串相互依赖的步骤；",[177,11493,11494],{},"用测试、构建、差异检查或人工评审确认结果。",[16,11496,11497],{},[214,11498],{"alt":11499,"src":11500},"聊天框扩展为包含文档、代码、浏览器、工具调用和人工确认的智能体工作流","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fagent-workflow-v1.webp",[16,11502,11503],{},"OpenAI 当前的模型与 Responses API 文档已经把函数调用、网页搜索、文件搜索和计算机操作列为可用工具。模型不再只告诉我们“可以怎么做”，而是开始在获得授权的范围内参与真实工作过程。",[16,11505,11506],{},"这也是我认为“聊天模型”和“智能体”之间最重要的区别：前者交付一段内容，后者推进一个状态会变化的任务。",[64,11508,11510],{"id":11509},"程序员的工作没有消失而是向两端移动","程序员的工作没有消失，而是向两端移动",[16,11512,11513],{},"当模型可以读代码、改文件、运行命令和检查结果时，程序员并不是突然变得不重要。相反，我们的注意力开始向任务的两端移动。",[16,11515,11516],{},"在行动之前，需要把问题定义得更清楚：",[373,11518,11519,11522,11525,11528],{},[177,11520,11521],{},"目标是什么，什么不在范围内；",[177,11523,11524],{},"哪些资料可信，哪些只是参考；",[177,11526,11527],{},"哪些权限可以授予，哪些操作必须确认；",[177,11529,11530],{},"失败时应该停止、重试，还是回滚。",[16,11532,11533],{},"在行动之后，需要把结果验证得更扎实：",[373,11535,11536,11539,11542,11545],{},[177,11537,11538],{},"代码能否通过测试与构建；",[177,11540,11541],{},"页面是否真的符合预期，而不只是生成成功；",[177,11543,11544],{},"外部状态是否已经改变；",[177,11546,11547],{},"结果能否被解释、复现和审计。",[16,11549,11550],{},"三年前，我们努力学习怎样写出一个更好的提示词。今天，我们还要学习怎样设计上下文、工具、权限、检查点和验收标准。",[64,11552,11554],{"id":11553},"能力可以委托责任不能外包","能力可以委托，责任不能外包",[16,11556,11557],{},"智能体越能执行真实操作，边界就越重要。",[16,11559,11560],{},"让模型总结一篇文章，与让它修改生产数据库，不是同一种授权；让它提出邮件草稿，与让它直接发送给客户，也不是同一种风险。一个成熟的智能体系统，不应该只追求“能做多少”，还应该明确：",[373,11562,11563,11566,11569,11572,11575],{},[177,11564,11565],{},"默认只读还是默认可写；",[177,11567,11568],{},"哪些动作需要人在执行前确认；",[177,11570,11571],{},"如何限制目标、目录、账号和数据范围；",[177,11573,11574],{},"如何保存必要的操作记录；",[177,11576,11577],{},"发生错误时怎样停止并恢复。",[16,11579,11580],{},"模型可以承担越来越多行动，但判断和责任并不会因此消失。最终仍然需要有人回答：为什么要这样做，依据是什么，风险由谁承担，结果是否可以接受。",[64,11582,11584],{"id":11583},"再过三年我们会怎样看今天","再过三年，我们会怎样看今天",[16,11586,11587],{},"这封旧邮件如今很像一张偶然保存下来的车票。",[16,11589,11590],{},"2023 年，我申请的是一次访问模型的机会。2026 年，我们已经在学习如何让模型理解一个长期目标、使用工具、跨越多个步骤，并在人的监督下完成工作。",[16,11592,11593],{},"让我感慨的不是某一个参数增长了多少倍，而是时间被压缩了。许多曾经只存在于想象中的能力，没有在遥远的未来慢慢到来，而是在几次版本更新之间，悄悄变成了日常。",[16,11595,11596],{},"也许再过三年回头看，今天让我们惊叹的百万级上下文和智能体，也会像这封邮件一样，成为未来刚刚开始时留下的一件旧物。",[11281,11598],{},[64,11600,11285],{"id":11285},[373,11602,11603,11611,11619],{},[177,11604,11605,11610],{},[163,11606,11609],{"href":11607,"rel":11608},"https:\u002F\u002Fopenai.com\u002Findex\u002Fgpt-4-research\u002F",[167],"OpenAI：GPT‑4 发布资料","：GPT‑4 于 2023 年 3 月发布，标准上下文为 8,192 tokens，并有限开放 32,768-token 版本。",[177,11612,11613,11618],{},[163,11614,11617],{"href":11615,"rel":11616},"https:\u002F\u002Fdevelopers.openai.com\u002Fapi\u002Fdocs\u002Fmodels",[167],"OpenAI：模型目录","：截至本文发布时，前沿 GPT‑5.6 系列列出约 1.05M context window，并支持多类工具。",[177,11620,11621,11626],{},[163,11622,11625],{"href":11623,"rel":11624},"https:\u002F\u002Fai.google.dev\u002Fgemini-api\u002Fdocs\u002Flong-context",[167],"Google AI：Long context","：介绍 Gemini 模型的 1M+ 长上下文能力、使用场景与限制。",{"title":74,"searchDepth":729,"depth":729,"links":11628},[11629,11630,11631,11632,11633,11634],{"id":11435,"depth":712,"text":11436},{"id":11457,"depth":712,"text":11457},{"id":11509,"depth":712,"text":11510},{"id":11553,"depth":712,"text":11554},{"id":11583,"depth":712,"text":11584},{"id":11285,"depth":712,"text":11285},"2023 年，我等来的是一次 8K context 的 API 访问资格；三年后，我们开始学习如何向智能体交代目标、权限、边界与验收标准。",{"src":11637,"title":11638,"description":11639,"duration":11640,"hasAudio":1696},"\u002Fexperiences\u002Ffrom-gpt-4-invite-to-agents","从 GPT-4 邀请到智能体时代 · 六幕互动叙事","用 2 分 26 秒回看上下文、智能体工作流，以及能力与责任的变化。",146,{"src":11642,"alt":11643,"caption":11644,"width":9679,"height":9680},"\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcover-v1.webp","一封来自 2023 年的邮件从玻璃时间胶囊延伸为现代智能体工作空间","三年前申请访问模型，三年后学习与模型共同工作",{},"\u002Fblog\u002Ffrom-gpt-4-invite-to-agents","2026-07-25",{"title":11400,"description":11635},"blog\u002Ffrom-gpt-4-invite-to-agents",[11651,11652,11653],"AI","GPT-4","智能体","P0HWhOcEJwLRdrvZlwkQks0C-1y9Q98neHDcWHX6Xow",{"id":11656,"title":11657,"accent":2449,"body":11658,"description":12090,"draft":1692,"experience":1693,"extension":1694,"featured":1696,"image":12091,"meta":12095,"navigation":1696,"path":12096,"publishedAt":12097,"readingTime":795,"seo":12098,"stem":12099,"tags":12100,"updatedAt":12102,"video":1693,"__hash__":12103,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Fedge-stateful-apps.md","在边缘构建有状态应用",{"type":13,"value":11659,"toc":12082},[11660,11666,11669,11672,11675,11724,11727,11732,11735,11738,11741,11809,11812,11894,11897,11900,11903,11980,11994,11997,12000,12003,12006,12017,12020,12023,12026,12029,12043,12046,12050,12053,12056,12073,12079],[16,11661,11662,11663],{},"边缘计算最吸引人的承诺，是让代码离用户更近。但当应用开始保存账号、会话和留言，问题就不再只是“把函数部署到更多节点”，而是：",[20,11664,11665],{},"状态究竟在哪里，谁对它负责，失败时系统会怎样退化？",[16,11667,11668],{},"这篇文章用一个带登录和留言的博客作为最小案例，梳理在 Cloudflare Workers 与 D1 上构建有状态应用时真正重要的设计边界。",[64,11670,11671],{"id":11671},"先画清楚状态地图",[16,11673,11674],{},"一个看起来很小的博客，至少有三类完全不同的状态：",[85,11676,11677,11689],{},[88,11678,11679],{},[91,11680,11681,11683,11686],{},[94,11682,3917],{},[94,11684,11685],{},"来源",[94,11687,11688],{},"生命周期",[107,11690,11691,11702,11713],{},[91,11692,11693,11696,11699],{},[112,11694,11695],{},"文章正文",[112,11697,11698],{},"Git 中的 Markdown",[112,11700,11701],{},"随部署更新",[91,11703,11704,11707,11710],{},[112,11705,11706],{},"用户与留言",[112,11708,11709],{},"D1",[112,11711,11712],{},"长期持久化",[91,11714,11715,11718,11721],{},[112,11716,11717],{},"登录会话",[112,11719,11720],{},"D1 + Cookie",[112,11722,11723],{},"有明确过期时间",[16,11725,11726],{},"文章内容适合跟着代码版本走。它需要评审、回滚和构建时校验，却不需要读者在运行时修改。用户数据则刚好相反：它必须脱离部署生命周期存在。",[51,11728,11729],{},[16,11730,11731],{},"选择数据库之前，先判断一份数据应该跟随代码版本，还是跟随用户行为。",[16,11733,11734],{},"这个区分会自然导向一个简单架构：Nuxt Content 负责 Markdown，D1 负责运行时业务数据，Worker 负责把两者组合成一次请求。",[64,11736,11737],{"id":11737},"绑定比网络请求更重要",[16,11739,11740],{},"Worker 访问 D1 时不需要自己保存 API Token，也不应该在运行时调用 Cloudflare REST API。D1 通过 binding 直接进入运行时：",[68,11742,11744],{"className":696,"code":11743,"filename":8328,"language":698,"meta":74,"style":74},"{\n  \"d1_databases\": [\n    {\n      \"binding\": \"DB\",\n      \"database_name\": \"signal-log-db\",\n      \"database_id\": \"\u003CYOUR_DATABASE_ID>\"\n    }\n  ]\n}\n",[24,11745,11746,11750,11758,11762,11774,11786,11796,11800,11805],{"__ignoreMap":74},[702,11747,11748],{"class":704,"line":705},[702,11749,709],{"class":708},[702,11751,11752,11755],{"class":704,"line":712},[702,11753,11754],{"class":715},"  \"d1_databases\"",[702,11756,11757],{"class":708},": [\n",[702,11759,11760],{"class":704,"line":729},[702,11761,8350],{"class":708},[702,11763,11764,11767,11769,11772],{"class":704,"line":742},[702,11765,11766],{"class":715},"      \"binding\"",[702,11768,719],{"class":708},[702,11770,11771],{"class":722},"\"DB\"",[702,11773,726],{"class":708},[702,11775,11776,11779,11781,11784],{"class":704,"line":755},[702,11777,11778],{"class":715},"      \"database_name\"",[702,11780,719],{"class":708},[702,11782,11783],{"class":722},"\"signal-log-db\"",[702,11785,726],{"class":708},[702,11787,11788,11791,11793],{"class":704,"line":778},[702,11789,11790],{"class":715},"      \"database_id\"",[702,11792,719],{"class":708},[702,11794,11795],{"class":722},"\"\u003CYOUR_DATABASE_ID>\"\n",[702,11797,11798],{"class":704,"line":787},[702,11799,856],{"class":708},[702,11801,11802],{"class":704,"line":795},[702,11803,11804],{"class":708},"  ]\n",[702,11806,11807],{"class":704,"line":807},[702,11808,868],{"class":708},[16,11810,11811],{},"在服务端路由里，数据库对象来自当前请求上下文。每一条 SQL 都使用 prepared statement：",[68,11813,11815],{"className":6185,"code":11814,"language":6187,"meta":74,"style":74},"const comment = await db\n  .prepare(`\n    SELECT id, body, created_at\n    FROM comments\n    WHERE post_slug = ?1 AND status = 'published'\n    ORDER BY created_at DESC\n    LIMIT 100\n  `)\n  .bind(postSlug)\n  .all()\n",[24,11816,11817,11831,11843,11848,11853,11858,11863,11868,11875,11885],{"__ignoreMap":74},[702,11818,11819,11821,11824,11826,11828],{"class":704,"line":705},[702,11820,6212],{"class":1949},[702,11822,11823],{"class":715}," comment",[702,11825,6218],{"class":1949},[702,11827,6334],{"class":1949},[702,11829,11830],{"class":708}," db\n",[702,11832,11833,11835,11838,11840],{"class":704,"line":712},[702,11834,10491],{"class":708},[702,11836,11837],{"class":947},"prepare",[702,11839,6411],{"class":708},[702,11841,11842],{"class":722},"`\n",[702,11844,11845],{"class":704,"line":729},[702,11846,11847],{"class":722},"    SELECT id, body, created_at\n",[702,11849,11850],{"class":704,"line":742},[702,11851,11852],{"class":722},"    FROM comments\n",[702,11854,11855],{"class":704,"line":755},[702,11856,11857],{"class":722},"    WHERE post_slug = ?1 AND status = 'published'\n",[702,11859,11860],{"class":704,"line":778},[702,11861,11862],{"class":722},"    ORDER BY created_at DESC\n",[702,11864,11865],{"class":704,"line":787},[702,11866,11867],{"class":722},"    LIMIT 100\n",[702,11869,11870,11873],{"class":704,"line":795},[702,11871,11872],{"class":722},"  `",[702,11874,6579],{"class":708},[702,11876,11877,11879,11882],{"class":704,"line":807},[702,11878,10491],{"class":708},[702,11880,11881],{"class":947},"bind",[702,11883,11884],{"class":708},"(postSlug)\n",[702,11886,11887,11889,11892],{"class":704,"line":815},[702,11888,10491],{"class":708},[702,11890,11891],{"class":947},"all",[702,11893,7743],{"class":708},[16,11895,11896],{},"参数绑定不是代码风格问题，而是输入边界的一部分。文章路径、邮箱、昵称和留言正文都来自用户，任何一个值都不应该被拼接进 SQL。",[64,11898,11899],{"id":11899},"会话只存不可逆摘要",[16,11901,11902],{},"登录成功后，浏览器拿到一个随机会话令牌；数据库只保存它的 SHA-256 摘要。这样即使会话表意外暴露，原始令牌也不能被直接拿去登录。",[68,11904,11906],{"className":6185,"code":11905,"language":6187,"meta":74,"style":74},"const token = createRandomToken()\nconst tokenHash = await sha256(token)\n\nawait db\n  .prepare('INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)')\n  .bind(tokenHash, userId, expiresAt)\n  .run()\n",[24,11907,11908,11922,11939,11943,11949,11962,11971],{"__ignoreMap":74},[702,11909,11910,11912,11915,11917,11920],{"class":704,"line":705},[702,11911,6212],{"class":1949},[702,11913,11914],{"class":715}," token",[702,11916,6218],{"class":1949},[702,11918,11919],{"class":947}," createRandomToken",[702,11921,7743],{"class":708},[702,11923,11924,11926,11929,11931,11933,11936],{"class":704,"line":712},[702,11925,6212],{"class":1949},[702,11927,11928],{"class":715}," tokenHash",[702,11930,6218],{"class":1949},[702,11932,6334],{"class":1949},[702,11934,11935],{"class":947}," sha256",[702,11937,11938],{"class":708},"(token)\n",[702,11940,11941],{"class":704,"line":729},[702,11942,2738],{"emptyLinePlaceholder":1696},[702,11944,11945,11947],{"class":704,"line":742},[702,11946,6741],{"class":1949},[702,11948,11830],{"class":708},[702,11950,11951,11953,11955,11957,11960],{"class":704,"line":755},[702,11952,10491],{"class":708},[702,11954,11837],{"class":947},[702,11956,6411],{"class":708},[702,11958,11959],{"class":722},"'INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)'",[702,11961,6579],{"class":708},[702,11963,11964,11966,11968],{"class":704,"line":778},[702,11965,10491],{"class":708},[702,11967,11881],{"class":947},[702,11969,11970],{"class":708},"(tokenHash, userId, expiresAt)\n",[702,11972,11973,11975,11978],{"class":704,"line":787},[702,11974,10491],{"class":708},[702,11976,11977],{"class":947},"run",[702,11979,7743],{"class":708},[16,11981,11982,11983,11986,11987,11990,11991,1065],{},"Cookie 使用 ",[24,11984,11985],{},"HttpOnly","，让前端 JavaScript 无法读取；使用 ",[24,11988,11989],{},"SameSite=Lax"," 降低跨站请求风险；生产环境再加上 ",[24,11992,11993],{},"Secure",[16,11995,11996],{},"这并不意味着系统从此“绝对安全”。更准确的说法是：我们让每一层只持有它完成工作所需的最少能力。",[64,11998,11999],{"id":11999},"一致性要围绕用户动作设计",[16,12001,12002],{},"D1 是关系数据库，但边缘应用仍然需要明确哪些流程要求强一致。",[16,12004,12005],{},"对博客来说，关键动作很少：",[174,12007,12008,12011,12014],{},[177,12009,12010],{},"注册后应当立刻能够登录；",[177,12012,12013],{},"留言成功后应当立刻在当前页面出现；",[177,12015,12016],{},"退出后旧会话应当立刻失效。",[16,12018,12019],{},"这些流程都应该从同一个主数据库完成写入和紧随其后的读取。文章列表、公开留言列表等纯读取场景，才有更大的缓存空间。",[16,12021,12022],{},"不要从“哪里可以缓存”出发，而要从“用户刚刚完成了什么承诺”出发。",[64,12024,12025],{"id":12025},"失败也需要产品设计",[16,12027,12028],{},"边缘运行时会失败，数据库也可能暂时不可用。区别只在于，我们有没有把失败变成用户能理解的状态。",[373,12030,12031,12034,12037,12040],{},[177,12032,12033],{},"登录失败时，不区分“邮箱不存在”和“密码错误”，避免暴露账号枚举信息；",[177,12035,12036],{},"留言写入失败时，保留输入内容，让用户可以重试；",[177,12038,12039],{},"内容查询失败时，返回真正的错误状态，而不是空白页面；",[177,12041,12042],{},"所有服务端错误使用结构化日志，但不把内部 SQL 或堆栈发给浏览器。",[16,12044,12045],{},"系统可靠性不是“永远不出错”，而是错误发生时边界仍然完整。",[64,12047,12049],{"id":12048},"最后让架构保持无聊","最后：让架构保持无聊",[16,12051,12052],{},"这个方案没有消息队列、没有分布式锁，也没有额外的身份服务。不是因为那些工具没有价值，而是当前问题还没有提出相应要求。",[16,12054,12055],{},"一个长期可维护的个人站，最好的基础通常是：",[373,12057,12058,12061,12064,12067,12070],{},[177,12059,12060],{},"内容跟着 Git；",[177,12062,12063],{},"数据进入 D1；",[177,12065,12066],{},"密钥留在 Worker secret；",[177,12068,12069],{},"输入在边界验证；",[177,12071,12072],{},"失败清楚、可观察、可恢复。",[16,12074,12075,12076],{},"当流量或产品需求真正变化时，再让架构跟着证据演进。",[20,12077,12078],{},"边缘已经足够快，工程的任务是让它继续简单。",[1658,12080,12081],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":74,"searchDepth":729,"depth":729,"links":12083},[12084,12085,12086,12087,12088,12089],{"id":11671,"depth":712,"text":11671},{"id":11737,"depth":712,"text":11737},{"id":11899,"depth":712,"text":11899},{"id":11999,"depth":712,"text":11999},{"id":12025,"depth":712,"text":12025},{"id":12048,"depth":712,"text":12049},"Cloudflare Workers 很快，D1 很近，但真正困难的是把状态、一致性与失败边界设计清楚。",{"src":12092,"alt":12093,"caption":12094,"width":9679,"height":9680},"\u002Fmedia\u002Fposts\u002Fedge-stateful-apps\u002Fcover-v1.webp","边缘节点围绕中心数据库形成有状态应用拓扑","状态、边缘节点与一致性边界",{},"\u002Fblog\u002Fedge-stateful-apps","2026-07-21",{"title":11657,"description":12090},"blog\u002Fedge-stateful-apps",[4540,11709,12101],"工程","2026-07-24","gsFDt7C9LtEnc-0ELg6T7tdWM-k-26r6jVfx424yrSM",{"id":12105,"title":12106,"accent":1712,"body":12107,"description":12491,"draft":1692,"experience":1693,"extension":1694,"featured":1692,"image":12492,"meta":12496,"navigation":1696,"path":12497,"publishedAt":12498,"readingTime":778,"seo":12499,"stem":12500,"tags":12501,"updatedAt":1693,"video":1693,"__hash__":12504,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Fnuxt-4-architecture-subtraction.md","Nuxt 4 的架构减法",{"type":13,"value":12108,"toc":12483},[12109,12112,12118,12122,12125,12176,12179,12186,12189,12192,12316,12319,12337,12340,12343,12346,12349,12352,12357,12361,12368,12374,12377,12380,12383,12386,12451,12454,12457,12460,12477,12480],[16,12110,12111],{},"很多 Nuxt 项目变复杂，并不是因为业务真的复杂，而是因为我们太早把所有可能性都放进了架构。",[16,12113,12114,12115,1065],{},"个人博客是一个很好的校准器：页面数量有限、内容结构稳定、交互集中在少数区域。它提醒我们，框架最有价值的部分往往不是增加能力，而是",[20,12116,12117],{},"删除不必要的选择",[64,12119,12121],{"id":12120},"从默认-ssr-开始","从默认 SSR 开始",[16,12123,12124],{},"技术文章需要被搜索引擎理解，也需要在弱网下尽早出现正文。Nuxt 默认的服务端渲染刚好符合这个目标。",[68,12126,12129],{"className":6185,"code":12127,"filename":12128,"language":6187,"meta":74,"style":74},"export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fcontent'],\n  nitro: {\n    preset: 'cloudflare',\n  },\n})\n","nuxt.config.ts",[24,12130,12131,12142,12153,12158,12168,12172],{"__ignoreMap":74},[702,12132,12133,12135,12137,12140],{"class":704,"line":705},[702,12134,6005],{"class":1949},[702,12136,7687],{"class":1949},[702,12138,12139],{"class":947}," defineNuxtConfig",[702,12141,6227],{"class":708},[702,12143,12144,12147,12150],{"class":704,"line":712},[702,12145,12146],{"class":708},"  modules: [",[702,12148,12149],{"class":722},"'@nuxt\u002Fcontent'",[702,12151,12152],{"class":708},"],\n",[702,12154,12155],{"class":704,"line":729},[702,12156,12157],{"class":708},"  nitro: {\n",[702,12159,12160,12163,12166],{"class":704,"line":742},[702,12161,12162],{"class":708},"    preset: ",[702,12164,12165],{"class":722},"'cloudflare'",[702,12167,726],{"class":708},[702,12169,12170],{"class":704,"line":755},[702,12171,6284],{"class":708},[702,12173,12174],{"class":704,"line":778},[702,12175,6318],{"class":708},[16,12177,12178],{},"这里没有把整个站改成 SPA，也没有为每个页面单独发明数据加载方式。页面在服务端完成第一次查询和渲染，客户端接管后继续使用同一套路由。",[16,12180,12181,12182,12185],{},"如果某一小块必须依赖浏览器 API，再局部使用 ",[24,12183,12184],{},"ClientOnly","。不要为了一个客户端组件牺牲整页的语义和首屏。",[64,12187,12188],{"id":12188},"内容查询留在页面边界",[16,12190,12191],{},"首页、文章列表和文章详情需要的数据粒度不同。查询应该贴近消费它的页面：",[68,12193,12195],{"className":6185,"code":12194,"language":6187,"meta":74,"style":74},"const { data: posts } = await useAsyncData('latest-posts', () => {\n  return queryCollection('posts')\n    .where('draft', '=', false)\n    .order('publishedAt', 'DESC')\n    .limit(6)\n    .all()\n})\n",[24,12196,12197,12232,12246,12271,12290,12304,12312],{"__ignoreMap":74},[702,12198,12199,12201,12203,12206,12208,12211,12213,12215,12217,12220,12222,12225,12228,12230],{"class":704,"line":705},[702,12200,6212],{"class":1949},[702,12202,8762],{"class":708},[702,12204,12205],{"class":6522},"data",[702,12207,719],{"class":708},[702,12209,12210],{"class":715},"posts",[702,12212,8767],{"class":708},[702,12214,2990],{"class":1949},[702,12216,6334],{"class":1949},[702,12218,12219],{"class":947}," useAsyncData",[702,12221,6411],{"class":708},[702,12223,12224],{"class":722},"'latest-posts'",[702,12226,12227],{"class":708},", () ",[702,12229,8752],{"class":1949},[702,12231,6718],{"class":708},[702,12233,12234,12236,12239,12241,12244],{"class":704,"line":712},[702,12235,8935],{"class":1949},[702,12237,12238],{"class":947}," queryCollection",[702,12240,6411],{"class":708},[702,12242,12243],{"class":722},"'posts'",[702,12245,6579],{"class":708},[702,12247,12248,12251,12254,12256,12259,12261,12264,12266,12269],{"class":704,"line":729},[702,12249,12250],{"class":708},"    .",[702,12252,12253],{"class":947},"where",[702,12255,6411],{"class":708},[702,12257,12258],{"class":722},"'draft'",[702,12260,6531],{"class":708},[702,12262,12263],{"class":722},"'='",[702,12265,6531],{"class":708},[702,12267,12268],{"class":715},"false",[702,12270,6579],{"class":708},[702,12272,12273,12275,12278,12280,12283,12285,12288],{"class":704,"line":742},[702,12274,12250],{"class":708},[702,12276,12277],{"class":947},"order",[702,12279,6411],{"class":708},[702,12281,12282],{"class":722},"'publishedAt'",[702,12284,6531],{"class":708},[702,12286,12287],{"class":722},"'DESC'",[702,12289,6579],{"class":708},[702,12291,12292,12294,12297,12299,12302],{"class":704,"line":755},[702,12293,12250],{"class":708},[702,12295,12296],{"class":947},"limit",[702,12298,6411],{"class":708},[702,12300,12301],{"class":715},"6",[702,12303,6579],{"class":708},[702,12305,12306,12308,12310],{"class":704,"line":778},[702,12307,12250],{"class":708},[702,12309,11891],{"class":947},[702,12311,7743],{"class":708},[702,12313,12314],{"class":704,"line":787},[702,12315,6318],{"class":708},[16,12317,12318],{},"这样做有几个直接好处：",[373,12320,12321,12328,12331,12334],{},[177,12322,12323,12324,12327],{},"数据参与 SSR，不需要 ",[24,12325,12326],{},"onMounted"," 后再闪烁加载；",[177,12329,12330],{},"查询键明确，Nuxt 可以在 hydration 时复用 payload；",[177,12332,12333],{},"页面知道自己需要什么，不必经过一层通用 store；",[177,12335,12336],{},"草稿过滤与排序规则出现在真正相关的位置。",[16,12338,12339],{},"只有当多个页面共享了同一段复杂业务状态时，才值得抽成 composable 或 store。",[64,12341,12342],{"id":12342},"组件只抽取稳定重复",[16,12344,12345],{},"“看起来可能复用”不是组件边界。真正稳定的重复通常同时包含结构、语义和行为。",[16,12347,12348],{},"这个博客里，文章卡片是合适的组件：它在首页和索引页重复出现，拥有固定的元数据、标题、摘要、标签和跳转行为。",[16,12350,12351],{},"相反，首页 Hero 即使很大，也未必需要拆成五个小组件。它只在一个页面出现，内部元素共同完成一段视觉叙事。过早拆分只会让样式关系跨文件跳转。",[51,12353,12354],{},[16,12355,12356],{},"组件不是文件整理工具，而是可独立理解和复用的 UI 契约。",[64,12358,12360],{"id":12359},"服务端-api-保持窄接口","服务端 API 保持窄接口",[16,12362,12363,12364,12367],{},"登录与留言属于运行时数据，放在 ",[24,12365,12366],{},"server\u002Fapi","。每个 endpoint 只做一件事：",[68,12369,12372],{"className":12370,"code":12371,"language":73,"meta":74},[71],"POST \u002Fapi\u002Fauth\u002Fregister\nPOST \u002Fapi\u002Fauth\u002Flogin\nPOST \u002Fapi\u002Fauth\u002Flogout\nGET  \u002Fapi\u002Fauth\u002Fsession\nGET  \u002Fapi\u002Fcomments\u002F:post\nPOST \u002Fapi\u002Fcomments\u002F:post\n",[24,12373,12371],{"__ignoreMap":74},[16,12375,12376],{},"窄接口使授权规则更容易检查。读取留言是公开的，创建留言需要会话；退出只销毁当前令牌，不需要触碰用户资料。",[16,12378,12379],{},"如果一个 endpoint 同时处理读取、写入、登录和格式转换，它就已经失去了清晰边界。",[64,12381,12382],{"id":12382},"把状态放回正确的位置",[16,12384,12385],{},"Vue 的响应式状态很方便，但不是所有状态都应该进入 JavaScript：",[85,12387,12388,12397],{},[88,12389,12390],{},[91,12391,12392,12394],{},[94,12393,3917],{},[94,12395,12396],{},"最合适的位置",[107,12398,12399,12407,12417,12425,12436,12443],{},[91,12400,12401,12404],{},[112,12402,12403],{},"当前文章",[112,12405,12406],{},"路由",[91,12408,12409,12412],{},[112,12410,12411],{},"搜索词",[112,12413,12414,12415],{},"页面 ",[24,12416,9711],{},[91,12418,12419,12422],{},[112,12420,12421],{},"标签筛选",[112,12423,12424],{},"URL query + 页面状态",[91,12426,12427,12430],{},[112,12428,12429],{},"登录用户",[112,12431,12432,12433],{},"SSR 安全的 ",[24,12434,12435],{},"useState",[91,12437,12438,12440],{},[112,12439,11695],{},[112,12441,12442],{},"Nuxt Content",[91,12444,12445,12448],{},[112,12446,12447],{},"会话真实性",[112,12449,12450],{},"服务端 D1",[16,12452,12453],{},"当状态位置正确时，大量“同步逻辑”会自然消失。我们不再需要监听路由去修复 store，也不需要用 localStorage 假装登录状态可靠。",[64,12455,12456],{"id":12456},"减法不是功能更少",[16,12458,12459],{},"好的架构减法不会让用户少得到什么。相反，它把复杂度预算留给真正有感知的部分：",[373,12461,12462,12465,12468,12471,12474],{},[177,12463,12464],{},"更清晰的排版；",[177,12466,12467],{},"更快的首屏；",[177,12469,12470],{},"更可靠的登录；",[177,12472,12473],{},"更舒服的移动端阅读；",[177,12475,12476],{},"更容易坚持的 Markdown 写作流程。",[16,12478,12479],{},"Nuxt 4 提供了足够多的能力。项目成熟的标志，不是把它们全部用上，而是知道哪些能力可以暂时不用。",[1658,12481,12482],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":74,"searchDepth":729,"depth":729,"links":12484},[12485,12486,12487,12488,12489,12490],{"id":12120,"depth":712,"text":12121},{"id":12188,"depth":712,"text":12188},{"id":12342,"depth":712,"text":12342},{"id":12359,"depth":712,"text":12360},{"id":12382,"depth":712,"text":12382},{"id":12456,"depth":712,"text":12456},"一个内容型站点不需要复杂前端状态。让路由、服务端渲染和内容查询各自回到最擅长的位置。",{"src":12493,"alt":12494,"caption":12495,"width":9679,"height":9680},"\u002Fmedia\u002Fposts\u002Fnuxt-4-architecture-subtraction\u002Fcover-v1.webp","复杂模块逐步收敛为清晰精简的 Nuxt 应用核心","用架构减法换取清晰边界",{},"\u002Fblog\u002Fnuxt-4-architecture-subtraction","2026-07-16",{"title":12106,"description":12491},"blog\u002Fnuxt-4-architecture-subtraction",[12502,11394,12503],"Nuxt","架构","Czr5nd2U1OiyR7yB5-ruM4B8FGFAAjFQQISDmWg9Fis",{"id":12506,"title":12507,"accent":11,"body":12508,"description":12855,"draft":1692,"experience":1693,"extension":1694,"featured":1692,"image":12856,"meta":12860,"navigation":1696,"path":12861,"publishedAt":12862,"readingTime":787,"seo":12863,"stem":12864,"tags":12865,"updatedAt":1693,"video":1693,"__hash__":12867,"contentLocale":4,"isFallback":1692},"posts_zh\u002Fblog\u002Fobservable-ai-systems.md","AI 系统的可观测性，不只是 Token 账单",{"type":13,"value":12509,"toc":12842},[12510,12513,12518,12521,12524,12527,12531,12534,12538,12541,12545,12548,12552,12555,12558,12562,12569,12682,12685,12688,12705,12708,12711,12714,12768,12771,12774,12777,12794,12797,12800,12803,12806,12820,12823,12827,12830,12836,12839],[16,12511,12512],{},"传统服务出问题时，我们习惯查看状态码、延迟和错误堆栈。AI 应用出问题时，这些指标仍然重要，却经常不足以回答最关键的问题：",[16,12514,12515],{},[20,12516,12517],{},"模型为什么做出了这个选择？",[16,12519,12520],{},"答案不在某一条日志里，而在从用户目标、上下文组装、模型决策、工具执行到最终结果的整条链路中。",[64,12522,12523],{"id":12523},"先区分四种失败",[16,12525,12526],{},"把所有失败都记成“模型效果不好”，团队就无法采取具体行动。至少应该区分：",[156,12528,12530],{"id":12529},"_1-输入失败","1. 输入失败",[16,12532,12533],{},"用户目标不完整，或系统没有拿到完成任务所需的上下文。此时升级模型通常没有帮助。",[156,12535,12537],{"id":12536},"_2-推理失败","2. 推理失败",[16,12539,12540],{},"上下文正确，但模型选择了错误路径、遗漏约束或过早得出结论。",[156,12542,12544],{"id":12543},"_3-工具失败","3. 工具失败",[16,12546,12547],{},"模型选择正确，实际执行却因为权限、网络、参数或外部状态失败。",[156,12549,12551],{"id":12550},"_4-交付失败","4. 交付失败",[16,12553,12554],{},"内部工作已经完成，但最终回答没有把结果、限制和下一步说清楚。",[16,12556,12557],{},"这四类问题需要不同的修复者：产品、提示设计、平台工程或交互设计。可观测性的第一份价值，是让问题回到正确的人手里。",[64,12559,12561],{"id":12560},"一次运行需要共同的-trace","一次运行需要共同的 Trace",[16,12563,12564,12565,12568],{},"每个用户请求都应该有稳定的 ",[24,12566,12567],{},"trace_id","，贯穿模型调用和工具执行：",[68,12570,12572],{"className":6185,"code":12571,"language":6187,"meta":74,"style":74},"type AgentEvent = {\n  traceId: string\n  spanId: string\n  parentSpanId?: string\n  type: 'model' | 'tool' | 'handoff' | 'result'\n  startedAt: string\n  durationMs?: number\n  status: 'running' | 'ok' | 'error'\n}\n",[24,12573,12574,12586,12595,12604,12614,12639,12648,12658,12678],{"__ignoreMap":74},[702,12575,12576,12579,12582,12584],{"class":704,"line":705},[702,12577,12578],{"class":1949},"type",[702,12580,12581],{"class":947}," AgentEvent",[702,12583,6218],{"class":1949},[702,12585,6718],{"class":708},[702,12587,12588,12591,12593],{"class":704,"line":712},[702,12589,12590],{"class":6522},"  traceId",[702,12592,6525],{"class":1949},[702,12594,7672],{"class":715},[702,12596,12597,12600,12602],{"class":704,"line":729},[702,12598,12599],{"class":6522},"  spanId",[702,12601,6525],{"class":1949},[702,12603,7672],{"class":715},[702,12605,12606,12609,12612],{"class":704,"line":742},[702,12607,12608],{"class":6522},"  parentSpanId",[702,12610,12611],{"class":1949},"?:",[702,12613,7672],{"class":715},[702,12615,12616,12619,12621,12624,12626,12629,12631,12634,12636],{"class":704,"line":755},[702,12617,12618],{"class":6522},"  type",[702,12620,6525],{"class":1949},[702,12622,12623],{"class":722}," 'model'",[702,12625,1981],{"class":1949},[702,12627,12628],{"class":722}," 'tool'",[702,12630,1981],{"class":1949},[702,12632,12633],{"class":722}," 'handoff'",[702,12635,1981],{"class":1949},[702,12637,12638],{"class":722}," 'result'\n",[702,12640,12641,12644,12646],{"class":704,"line":778},[702,12642,12643],{"class":6522},"  startedAt",[702,12645,6525],{"class":1949},[702,12647,7672],{"class":715},[702,12649,12650,12653,12655],{"class":704,"line":787},[702,12651,12652],{"class":6522},"  durationMs",[702,12654,12611],{"class":1949},[702,12656,12657],{"class":715}," number\n",[702,12659,12660,12663,12665,12668,12670,12673,12675],{"class":704,"line":795},[702,12661,12662],{"class":6522},"  status",[702,12664,6525],{"class":1949},[702,12666,12667],{"class":722}," 'running'",[702,12669,1981],{"class":1949},[702,12671,12672],{"class":722}," 'ok'",[702,12674,1981],{"class":1949},[702,12676,12677],{"class":722}," 'error'\n",[702,12679,12680],{"class":704,"line":807},[702,12681,868],{"class":708},[16,12683,12684],{},"工具调用是模型决策的外部表现。只记录工具返回值而不记录调用原因，会失去决策上下文；只保存提示词而不保存工具结果，又无法判断执行是否符合预期。",[16,12686,12687],{},"需要注意的是，观测并不意味着无限制收集。原始提示词可能包含个人信息、凭据或业务数据。更稳妥的做法是：",[373,12689,12690,12693,12696,12699,12702],{},[177,12691,12692],{},"默认记录结构化元数据；",[177,12694,12695],{},"对正文做字段级脱敏；",[177,12697,12698],{},"为调试样本设置短保留期；",[177,12700,12701],{},"允许用户和业务明确选择不进入训练或分析；",[177,12703,12704],{},"不在日志中保存密钥与完整身份信息。",[64,12706,12707],{"id":12707},"质量指标必须贴近任务",[16,12709,12710],{},"Token、延迟和模型错误率是平台指标，但它们不能代表用户是否得到结果。",[16,12712,12713],{},"一个代码修复 Agent 更有意义的指标可能是：",[85,12715,12716,12726],{},[88,12717,12718],{},[91,12719,12720,12723],{},[94,12721,12722],{},"层级",[94,12724,12725],{},"指标示例",[107,12727,12728,12736,12744,12752,12760],{},[91,12729,12730,12733],{},[112,12731,12732],{},"结果",[112,12734,12735],{},"受影响测试是否通过",[91,12737,12738,12741],{},[112,12739,12740],{},"过程",[112,12742,12743],{},"是否读取了相关约束文件",[91,12745,12746,12749],{},[112,12747,12748],{},"安全",[112,12750,12751],{},"是否修改了任务范围外文件",[91,12753,12754,12757],{},[112,12755,12756],{},"效率",[112,12758,12759],{},"有效工具调用 \u002F 总工具调用",[91,12761,12762,12765],{},[112,12763,12764],{},"交付",[112,12766,12767],{},"是否提供可验证的文件或提交",[16,12769,12770],{},"“有效工具调用”比单纯调用次数更有价值。一次准确的搜索可能替代十次猜测式读取。",[64,12772,12773],{"id":12773},"把评估变成日常反馈",[16,12775,12776],{},"离线 benchmark 能比较模型版本，却不能覆盖你的真实工作流。更可持续的评估集来自生产中的高价值样本：",[174,12778,12779,12782,12785,12788,12791],{},[177,12780,12781],{},"收集明确成功与明确失败的运行；",[177,12783,12784],{},"删除隐私信息，保留决策结构；",[177,12786,12787],{},"为失败标注类型，而不是只打一个总分；",[177,12789,12790],{},"在提示、模型或工具变化时重新运行；",[177,12792,12793],{},"同时比较结果质量、成本和延迟。",[16,12795,12796],{},"评估集不需要一开始就很大。二十个边界清楚、能稳定复现的案例，通常比两千个来源模糊的样本更能指导工程。",[64,12798,12799],{"id":12799},"面向人的可观测性",[16,12801,12802],{},"最后一层观测不在仪表盘，而在产品界面。",[16,12804,12805],{},"当 Agent 正在读取文件、等待授权或执行部署时，用户需要知道它处于什么阶段。好的状态更新不是倾倒内部思维，而是给出：",[373,12807,12808,12811,12814,12817],{},[177,12809,12810],{},"当前正在完成什么；",[177,12812,12813],{},"已经确认了什么；",[177,12815,12816],{},"是否需要用户采取行动；",[177,12818,12819],{},"哪些风险仍未关闭。",[16,12821,12822],{},"这类可见性会直接改变用户对系统可靠性的判断。一个需要几十秒完成的任务，如果过程清楚，往往比十秒后突然返回不透明结果更让人放心。",[64,12824,12826],{"id":12825},"可解释不等于暴露全部过程","可解释，不等于暴露全部过程",[16,12828,12829],{},"AI 系统不需要把每一个内部 token 展示给用户，也不应该把敏感上下文原样写入日志。真正需要的是一条足够清楚的因果链：",[68,12831,12834],{"className":12832,"code":12833,"language":73,"meta":74},[71],"用户目标\n  → 使用了哪些上下文\n  → 选择了什么工具\n  → 工具返回了什么状态\n  → 最终交付了什么结果\n",[24,12835,12833],{"__ignoreMap":74},[16,12837,12838],{},"当这条链路可以被查询、比较和复现时，团队才有能力把“偶尔很聪明”变成“长期可信赖”。",[1658,12840,12841],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":74,"searchDepth":729,"depth":729,"links":12843},[12844,12850,12851,12852,12853,12854],{"id":12523,"depth":712,"text":12523,"children":12845},[12846,12847,12848,12849],{"id":12529,"depth":729,"text":12530},{"id":12536,"depth":729,"text":12537},{"id":12543,"depth":729,"text":12544},{"id":12550,"depth":729,"text":12551},{"id":12560,"depth":712,"text":12561},{"id":12707,"depth":712,"text":12707},{"id":12773,"depth":712,"text":12773},{"id":12799,"depth":712,"text":12799},{"id":12825,"depth":712,"text":12826},"从提示词到工具调用，再到用户可见结果，AI 应用需要一条能解释“为什么”的观测链路。",{"src":12857,"alt":12858,"caption":12859,"width":9679,"height":9680},"\u002Fmedia\u002Fposts\u002Fobservable-ai-systems\u002Fcover-v1.webp","AI 请求从输入经过模型和工具节点形成完整观测链路","从提示词到结果的可解释 Trace",{},"\u002Fblog\u002Fobservable-ai-systems","2026-07-09",{"title":12507,"description":12855},"blog\u002Fobservable-ai-systems",[11651,12866,12101],"可观测性","HwqaP2v2TJ3UNRY0hMgfErIMN1Z-buqu7-QNk_P-4Vk",1787735716846]