[{"data":1,"prerenderedAt":13265},["ShallowReactive",2],{"seo-locale-availability:\u002Fblog\u002Fstundeck-docker-deployment-guide":3,"post:zh-CN:\u002Fblog\u002Fstundeck-docker-deployment-guide":7,"surroundings:zh-CN":2196},[4,5,6],"zh-CN","en","ja",{"post":8,"contentLocale":4,"isFallback":2165},{"id":9,"title":10,"accent":11,"body":12,"description":2164,"draft":2165,"experience":2166,"extension":2167,"featured":2165,"image":2168,"meta":2173,"navigation":331,"path":2174,"publishedAt":2175,"readingTime":2176,"seo":2177,"stem":2178,"tags":2179,"updatedAt":2187,"video":2188,"__hash__":2195},"posts_zh\u002Fblog\u002Fstundeck-docker-deployment-guide.md","StunDeck 实战：飞牛 fnOS FPK、Docker 与 Cloudflare 302","cyan",{"type":13,"value":14,"toc":2137},"minimark",[15,28,35,38,71,77,82,91,106,132,135,161,168,189,192,202,208,211,222,225,229,232,252,260,263,267,283,527,538,575,580,587,590,690,697,700,767,770,776,779,786,792,796,799,826,833,1078,1081,1121,1127,1131,1194,1201,1259,1266,1272,1277,1281,1284,1290,1293,1310,1316,1321,1327,1336,1339,1343,1350,1353,1359,1362,1368,1375,1439,1446,1452,1455,1458,1464,1471,1475,1478,1492,1498,1503,1509,1513,1516,1522,1528,1533,1537,1576,1579,1586,1589,1593,1596,1602,1609,1612,1662,1668,1673,1679,1684,1688,1691,1728,1738,1744,1749,1752,1792,1802,1808,1814,1818,1825,1828,1854,1858,1878,1882,1884,1908,1912,1915,1919,1929,1959,1962,2007,2010,2026,2029,2048,2054,2057,2126,2133],[16,17,18,19,23,24,27],"p",{},"这篇教程的重点不是单独把 STUN 跑通，而是让用户始终访问一个",[20,21,22],"strong",{},"固定域名","：StunDeck 发现公网 IP 或动态端口变化后，自动更新 Cloudflare Single Redirect；Cloudflare 对入口域名返回 ",[20,25,26],{},"302","（或 307），客户端再访问新的公网映射。",[16,29,30,31,34],{},"部署仍从 Docker 开始，但配置顺序必须是：",[20,32,33],{},"部署 StunDeck → 创建 Cloudflare Token → 保存 Cloudflare 连接 → 创建 Redirect 服务 → 启动 STUN\u002FUPnP → 验证 302 与第二跳","。Cloudflare 不能等服务建完才补，否则创建服务时没有可选连接，也无法验证自动同步。",[16,36,37],{},"最终实测结果：",[39,40,41,45,48,56,59,62,68],"ul",{},[42,43,44],"li",{},"Debian 13.6，amd64；",[42,46,47],{},"Docker Engine 29.7.1，Docker Compose 5.3.1；",[42,49,50,51,55],{},"StunDeck 镜像对应公开仓库提交 ",[52,53,54],"code",{},"bcb5a1a","；",[42,57,58],{},"Docker 服务环境没有代理，StunDeck 容器内的代理变量全部为空；",[42,60,61],{},"NATMap 成功取得映射，UPnP 已把第一跳监听端口放行；",[42,63,64,65,55],{},"独立外网主机直连动态公网地址返回 HTTP 200；固定入口复查返回 HTTP 302 和 ",[52,66,67],{},"Location",[42,69,70],{},"没有把真实用户名、密码、Token、内网地址、公网 IP 或动态端口写入本文与配图。",[72,73,74],"blockquote",{},[16,75,76],{},"STUN 不是中继，也不能把对称型 NAT 或受限 CGNAT 变成公网入口。本文记录的是在当前测试网络中验证成功的部署流程，不代表所有宽带环境都能复现相同结果。",[78,79,81],"h2",{"id":80},"飞牛-fnos直接安装正式-fpk","飞牛 fnOS：直接安装正式 FPK",[72,83,84],{},[16,85,86,87,90],{},"上架状态（2026-08-26）：StunDeck 已提交飞牛应用中心上架审核。审核通过并公开上架前，可以先手动安装这里提供的正式 FPK。这里的飞牛安装包格式是 ",[20,88,89],{},"FPK","，不是 Android APK。",[16,92,93,94,97,98,101,102,105],{},"当前正式版本为 ",[20,95,96],{},"StunDeck v0.1.1","，支持 ",[52,99,100],{},"linux\u002Famd64"," 与 ",[52,103,104],{},"linux\u002Farm64","。博客保留一份经过校验的正式发布包，同时提供 GitHub Release 作为备用来源：",[39,107,108,118,125],{},[42,109,110,117],{},[111,112,116],"a",{"href":113,"rel":114},"https:\u002F\u002Fblog.sparkles-editor.com\u002Fdownloads\u002Fstundeck\u002Fstundeck-fpk-0.1.1.fpk",[115],"nofollow","从本站下载 stundeck-fpk-0.1.1.fpk","（136,791 字节）；",[42,119,120,55],{},[111,121,124],{"href":122,"rel":123},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002Fstundeck\u002Freleases\u002Ftag\u002Fv0.1.1",[115],"查看 StunDeck v0.1.1 GitHub Release",[42,126,127,128,131],{},"SHA-256：",[52,129,130],{},"63d7f0e00b3777400507e62033fb0cce72884d10ad824f51b435a502d3a5dd8f","。",[16,133,134],{},"安装步骤：",[136,137,138,141,144,151,158],"ol",{},[42,139,140],{},"下载 FPK，并在本机核对 SHA-256；",[42,142,143],{},"在飞牛 fnOS 打开「应用中心 → 设置 → 手动安装应用」；",[42,145,146,147,150],{},"选择 ",[52,148,149],{},"stundeck-fpk-0.1.1.fpk","，按向导设置时区；只有通过 HTTPS 反向代理访问时才启用安全 Cookie；",[42,152,153,154,157],{},"首次安装会从 ",[52,155,156],{},"20000–59999"," 选择一个空闲端口并持久化保存，升级和重启不会重新随机；",[42,159,160],{},"从飞牛桌面打开 StunDeck，创建管理员，再添加 Cloudflare Token、映射服务与 Webhook。",[16,162,163,164,167],{},"FPK 固定拉取不可变镜像 ",[52,165,166],{},"ghcr.io\u002Fnciae-zyh\u002Fstundeck:v0.1.1","，因此 NAS 首次安装时需要能够访问 GHCR。安装包和向导不会预置或收集 Cloudflare Token、Webhook Secret 等秘密。",[16,169,170,171,174,175,101,178,181,182,185,186,188],{},"fnOS 会管理应用配置与持久化目录，真实数据位于包的 ",[52,172,173],{},"${TRIM_PKGVAR}\u002Fdata","。升级、迁移或重装前，必须把 ",[52,176,177],{},"stundeck.db",[52,179,180],{},"master.key"," ",[20,183,184],{},"成对备份","；缺少 ",[52,187,180],{}," 时，数据库中加密保存的 Cloudflare Token、Webhook Secret 与 TOTP Secret 无法恢复。卸载向导默认保留数据，只有明确选择“永久删除数据”时才会删除它。",[78,190,191],{"id":191},"先理解重点链路",[193,194,200],"pre",{"className":195,"code":197,"language":198,"meta":199},[196],"language-text","用户始终访问固定域名\n  └─ Cloudflare 代理 DNS（橙色云）\n       └─ Single Redirect 返回 302 \u002F 307 + Location\n            └─ 当前公网 IP:动态端口\n                 └─ 主路由 UPnP \u002F NAT-PMP \u002F 手动端口映射\n                      └─ StunDeck + NATMap\n                           └─ 局域网目标服务\n","text","",[52,201,197],{"__ignoreMap":199},[16,203,204,205,207],{},"固定的是入口域名，不是公网端口。Cloudflare 只处理第一跳：它返回重定向响应，不会为第二跳提供 Tunnel、中继、WAF 或 Access。浏览器看到 ",[52,206,67],{}," 后会直接连接公网 IP 与动态端口。",[16,209,210],{},"这里有两个独立成功条件：",[136,212,213,216],{},[42,214,215],{},"固定域名返回正确的 302\u002F307，证明 DNS 与 Single Redirect 已同步；",[42,217,218,219,221],{},"跟随 ",[52,220,67],{}," 后业务返回 200，证明 STUN、网关放行、防火墙与目标服务均可用。",[16,223,224],{},"只看到第一个结果，不能把服务标记为“公网可用”。",[78,226,228],{"id":227},"_1-部署前检查","1. 部署前检查",[16,230,231],{},"准备一台 Linux 主机，建议满足：",[39,233,234,237,240,243,246,249],{},[42,235,236],{},"Debian 13\u002F12 或 Docker 官方支持的其他发行版；",[42,238,239],{},"amd64 或 arm64；",[42,241,242],{},"固定或可识别的局域网地址；",[42,244,245],{},"能访问局域网目标服务；",[42,247,248],{},"主路由允许按需启用 UPnP，或者你能配置手动端口转发；",[42,250,251],{},"准备一个无敏感数据、可以随时关闭的测试服务。",[16,253,254,259],{},[111,255,258],{"href":256,"rel":257},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fstun",[115],"Lucky 的 STUN 文档","建议在 NAT1 环境使用，并说明局域网主机需要 UPnP、端口转发或 DMZ 等额外条件。不同设备对 NAT 类型的命名并不统一，所以最终应以 STUN 日志与真正的外网回连为准。",[16,261,262],{},"不要把 NAS 管理后台、路由器后台或 StunDeck 控制面当作第一条公网测试规则。先使用静态测试页或临时 HTTP 服务。",[78,264,266],{"id":265},"_2-安装-docker-engine-与-compose","2. 安装 Docker Engine 与 Compose",[16,268,269,270,275,276,279,280,131],{},"下面按 ",[111,271,274],{"href":272,"rel":273},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Finstall\u002Fdebian\u002F",[115],"Docker 官方 Debian 安装文档","配置 apt 仓库。Docker Compose 使用官方 ",[52,277,278],{},"docker-compose-plugin","，不要再安装旧版独立 ",[52,281,282],{},"docker-compose",[193,284,288],{"className":285,"code":286,"language":287,"meta":199,"style":199},"language-bash shiki shiki-themes github-dark github-dark","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","bash",[52,289,290,306,326,333,352,369,378,391,396,420,426,432,438,444,450,456,462,467,476,489,497,505,513,521],{"__ignoreMap":199},[291,292,295,299,303],"span",{"class":293,"line":294},"line",1,[291,296,298],{"class":297},"sFR8T","sudo",[291,300,302],{"class":301},"s4wv1"," apt",[291,304,305],{"class":301}," update\n",[291,307,309,311,313,316,320,323],{"class":293,"line":308},2,[291,310,298],{"class":297},[291,312,302],{"class":301},[291,314,315],{"class":301}," install",[291,317,319],{"class":318},"s8ozJ"," -y",[291,321,322],{"class":301}," ca-certificates",[291,324,325],{"class":301}," curl\n",[291,327,329],{"class":293,"line":328},3,[291,330,332],{"emptyLinePlaceholder":331},true,"\n",[291,334,336,338,340,343,346,349],{"class":293,"line":335},4,[291,337,298],{"class":297},[291,339,315],{"class":301},[291,341,342],{"class":318}," -m",[291,344,345],{"class":318}," 0755",[291,347,348],{"class":318}," -d",[291,350,351],{"class":301}," \u002Fetc\u002Fapt\u002Fkeyrings\n",[291,353,355,357,360,363,366],{"class":293,"line":354},5,[291,356,298],{"class":297},[291,358,359],{"class":301}," curl",[291,361,362],{"class":318}," -fsSL",[291,364,365],{"class":301}," https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg",[291,367,368],{"class":318}," \\\n",[291,370,372,375],{"class":293,"line":371},6,[291,373,374],{"class":318},"  -o",[291,376,377],{"class":301}," \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n",[291,379,381,383,386,389],{"class":293,"line":380},7,[291,382,298],{"class":297},[291,384,385],{"class":301}," chmod",[291,387,388],{"class":301}," a+r",[291,390,377],{"class":301},[291,392,394],{"class":293,"line":393},8,[291,395,332],{"emptyLinePlaceholder":331},[291,397,399,401,404,407,411,414,417],{"class":293,"line":398},9,[291,400,298],{"class":297},[291,402,403],{"class":301}," tee",[291,405,406],{"class":301}," \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fdocker.sources",[291,408,410],{"class":409},"sOPea"," >",[291,412,413],{"class":301},"\u002Fdev\u002Fnull",[291,415,416],{"class":409}," \u003C\u003C",[291,418,419],{"class":301},"'EOF'\n",[291,421,423],{"class":293,"line":422},10,[291,424,425],{"class":301},"Types: deb\n",[291,427,429],{"class":293,"line":428},11,[291,430,431],{"class":301},"URIs: https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\n",[291,433,435],{"class":293,"line":434},12,[291,436,437],{"class":301},"Suites: trixie\n",[291,439,441],{"class":293,"line":440},13,[291,442,443],{"class":301},"Components: stable\n",[291,445,447],{"class":293,"line":446},14,[291,448,449],{"class":301},"Architectures: amd64\n",[291,451,453],{"class":293,"line":452},15,[291,454,455],{"class":301},"Signed-By: \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n",[291,457,459],{"class":293,"line":458},16,[291,460,461],{"class":301},"EOF\n",[291,463,465],{"class":293,"line":464},17,[291,466,332],{"emptyLinePlaceholder":331},[291,468,470,472,474],{"class":293,"line":469},18,[291,471,298],{"class":297},[291,473,302],{"class":301},[291,475,305],{"class":301},[291,477,479,481,483,485,487],{"class":293,"line":478},19,[291,480,298],{"class":297},[291,482,302],{"class":301},[291,484,315],{"class":301},[291,486,319],{"class":318},[291,488,368],{"class":318},[291,490,492,495],{"class":293,"line":491},20,[291,493,494],{"class":301},"  docker-ce",[291,496,368],{"class":318},[291,498,500,503],{"class":293,"line":499},21,[291,501,502],{"class":301},"  docker-ce-cli",[291,504,368],{"class":318},[291,506,508,511],{"class":293,"line":507},22,[291,509,510],{"class":301},"  containerd.io",[291,512,368],{"class":318},[291,514,516,519],{"class":293,"line":515},23,[291,517,518],{"class":301},"  docker-buildx-plugin",[291,520,368],{"class":318},[291,522,524],{"class":293,"line":523},24,[291,525,526],{"class":301},"  docker-compose-plugin\n",[16,528,529,530,533,534,537],{},"如果不是 Debian 13 amd64，把 ",[52,531,532],{},"Suites"," 和 ",[52,535,536],{},"Architectures"," 改成实际值。可以用下面两条命令确认：",[193,539,541],{"className":285,"code":540,"language":287,"meta":199,"style":199},". \u002Fetc\u002Fos-release && echo \"$VERSION_CODENAME\"\ndpkg --print-architecture\n",[52,542,543,567],{"__ignoreMap":199},[291,544,545,548,551,555,558,561,564],{"class":293,"line":294},[291,546,547],{"class":318},".",[291,549,550],{"class":301}," \u002Fetc\u002Fos-release",[291,552,554],{"class":553},"suv1-"," && ",[291,556,557],{"class":318},"echo",[291,559,560],{"class":301}," \"",[291,562,563],{"class":553},"$VERSION_CODENAME",[291,565,566],{"class":301},"\"\n",[291,568,569,572],{"class":293,"line":308},[291,570,571],{"class":297},"dpkg",[291,573,574],{"class":318}," --print-architecture\n",[576,577,579],"h3",{"id":578},"下载需要代理时只让安装阶段使用","下载需要代理时，只让安装阶段使用",[16,581,582,583,586],{},"测试中，Docker 官方 GPG 地址有一次直连被重置。临时代理只用于下载，不应写进 ",[52,584,585],{},"\u002Fetc\u002Fenvironment","，也不应长期留在 Docker systemd 服务中。",[16,588,589],{},"一个进程级示例：",[193,591,593],{"className":285,"code":592,"language":287,"meta":199,"style":199},"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",[52,594,595,606,610,631,638,644,648,671,678,682],{"__ignoreMap":199},[291,596,597,600,603],{"class":293,"line":294},[291,598,599],{"class":553},"PROXY_URL",[291,601,602],{"class":409},"=",[291,604,605],{"class":301},"http:\u002F\u002F192.168.50.9:7890\n",[291,607,608],{"class":293,"line":308},[291,609,332],{"emptyLinePlaceholder":331},[291,611,612,614,616,619,621,624,627,629],{"class":293,"line":328},[291,613,298],{"class":297},[291,615,359],{"class":301},[291,617,618],{"class":318}," --proxy",[291,620,560],{"class":301},[291,622,623],{"class":553},"$PROXY_URL",[291,625,626],{"class":301},"\"",[291,628,362],{"class":318},[291,630,368],{"class":318},[291,632,633,636],{"class":293,"line":335},[291,634,635],{"class":301},"  https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg",[291,637,368],{"class":318},[291,639,640,642],{"class":293,"line":354},[291,641,374],{"class":318},[291,643,377],{"class":301},[291,645,646],{"class":293,"line":371},[291,647,332],{"emptyLinePlaceholder":331},[291,649,650,652,655,658,660,662,665,667,669],{"class":293,"line":380},[291,651,298],{"class":297},[291,653,654],{"class":301}," env",[291,656,657],{"class":301}," http_proxy=\"",[291,659,623],{"class":553},[291,661,626],{"class":301},[291,663,664],{"class":301}," https_proxy=\"",[291,666,623],{"class":553},[291,668,626],{"class":301},[291,670,368],{"class":318},[291,672,673,676],{"class":293,"line":393},[291,674,675],{"class":301},"  apt",[291,677,305],{"class":301},[291,679,680],{"class":293,"line":398},[291,681,332],{"emptyLinePlaceholder":331},[291,683,684,687],{"class":293,"line":422},[291,685,686],{"class":318},"unset",[291,688,689],{"class":301}," PROXY_URL\n",[16,691,692,693,696],{},"如果仓库持续不稳定，可以按 Docker 官方文档的“Install from a package”下载与系统匹配的 ",[52,694,695],{},".deb","，先校验文件，再在主机上安装。不要从网盘或不明镜像站获取 Docker 安装包。",[16,698,699],{},"安装后验证：",[193,701,703],{"className":285,"code":702,"language":287,"meta":199,"style":199},"sudo docker version\nsudo docker compose version\nsudo systemctl is-enabled docker\nsudo systemctl is-active docker\nsudo systemctl show docker -p Environment\n",[52,704,705,715,726,739,750],{"__ignoreMap":199},[291,706,707,709,712],{"class":293,"line":294},[291,708,298],{"class":297},[291,710,711],{"class":301}," docker",[291,713,714],{"class":301}," version\n",[291,716,717,719,721,724],{"class":293,"line":308},[291,718,298],{"class":297},[291,720,711],{"class":301},[291,722,723],{"class":301}," compose",[291,725,714],{"class":301},[291,727,728,730,733,736],{"class":293,"line":328},[291,729,298],{"class":297},[291,731,732],{"class":301}," systemctl",[291,734,735],{"class":301}," is-enabled",[291,737,738],{"class":301}," docker\n",[291,740,741,743,745,748],{"class":293,"line":335},[291,742,298],{"class":297},[291,744,732],{"class":301},[291,746,747],{"class":301}," is-active",[291,749,738],{"class":301},[291,751,752,754,756,759,761,764],{"class":293,"line":354},[291,753,298],{"class":297},[291,755,732],{"class":301},[291,757,758],{"class":301}," show",[291,760,711],{"class":301},[291,762,763],{"class":318}," -p",[291,765,766],{"class":301}," Environment\n",[16,768,769],{},"最后一条在本次环境中输出：",[193,771,774],{"className":772,"code":773,"language":198,"meta":199},[196],"Environment=\n",[52,775,773],{"__ignoreMap":199},[16,777,778],{},"这能证明 Docker daemon 没有继承常驻代理。它还不能证明容器没有代理，后面会再检查一次。",[16,780,781],{},[782,783],"img",{"alt":784,"src":785},"Docker Engine、Compose 与 systemd 状态的核验结果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fdocker-install-verified-v1.webp",[16,787,788],{},[789,790,791],"em",{},"根据本次真实命令输出整理；主机名、登录身份、局域网地址和临时下载代理均已移除。",[78,793,795],{"id":794},"_3-编写-compose-配置","3. 编写 Compose 配置",[16,797,798],{},"创建目录：",[193,800,802],{"className":285,"code":801,"language":287,"meta":199,"style":199},"sudo install -d -m 0755 \u002Fopt\u002Fstundeck\ncd \u002Fopt\u002Fstundeck\n",[52,803,804,819],{"__ignoreMap":199},[291,805,806,808,810,812,814,816],{"class":293,"line":294},[291,807,298],{"class":297},[291,809,315],{"class":301},[291,811,348],{"class":318},[291,813,342],{"class":318},[291,815,345],{"class":318},[291,817,818],{"class":301}," \u002Fopt\u002Fstundeck\n",[291,820,821,824],{"class":293,"line":308},[291,822,823],{"class":318},"cd",[291,825,818],{"class":301},[16,827,828,829,832],{},"保存为 ",[52,830,831],{},"\u002Fopt\u002Fstundeck\u002Fcompose.yaml","：",[193,834,838],{"className":835,"code":836,"language":837,"meta":199,"style":199},"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",[52,839,840,849,856,867,877,887,897,904,912,919,926,933,940,947,954,961,971,981,991,1001,1011,1020,1029,1038,1047,1057,1062,1070],{"__ignoreMap":199},[291,841,842,846],{"class":293,"line":294},[291,843,845],{"class":844},"sxg3X","services",[291,847,848],{"class":553},":\n",[291,850,851,854],{"class":293,"line":308},[291,852,853],{"class":844},"  stundeck",[291,855,848],{"class":553},[291,857,858,861,864],{"class":293,"line":328},[291,859,860],{"class":844},"    image",[291,862,863],{"class":553},": ",[291,865,866],{"class":301},"ghcr.io\u002Fnciae-zyh\u002Fstundeck:latest\n",[291,868,869,872,874],{"class":293,"line":335},[291,870,871],{"class":844},"    network_mode",[291,873,863],{"class":553},[291,875,876],{"class":301},"host\n",[291,878,879,882,884],{"class":293,"line":354},[291,880,881],{"class":844},"    restart",[291,883,863],{"class":553},[291,885,886],{"class":301},"unless-stopped\n",[291,888,889,892,894],{"class":293,"line":371},[291,890,891],{"class":844},"    read_only",[291,893,863],{"class":553},[291,895,896],{"class":318},"true\n",[291,898,899,902],{"class":293,"line":380},[291,900,901],{"class":844},"    cap_drop",[291,903,848],{"class":553},[291,905,906,909],{"class":293,"line":393},[291,907,908],{"class":553},"      - ",[291,910,911],{"class":301},"ALL\n",[291,913,914,917],{"class":293,"line":398},[291,915,916],{"class":844},"    security_opt",[291,918,848],{"class":553},[291,920,921,923],{"class":293,"line":422},[291,922,908],{"class":553},[291,924,925],{"class":301},"no-new-privileges:true\n",[291,927,928,931],{"class":293,"line":428},[291,929,930],{"class":844},"    tmpfs",[291,932,848],{"class":553},[291,934,935,937],{"class":293,"line":434},[291,936,908],{"class":553},[291,938,939],{"class":301},"\u002Ftmp:size=16m,mode=1777\n",[291,941,942,945],{"class":293,"line":440},[291,943,944],{"class":844},"    volumes",[291,946,848],{"class":553},[291,948,949,951],{"class":293,"line":446},[291,950,908],{"class":553},[291,952,953],{"class":301},"stundeck-data:\u002Fvar\u002Flib\u002Fstundeck\n",[291,955,956,959],{"class":293,"line":452},[291,957,958],{"class":844},"    environment",[291,960,848],{"class":553},[291,962,963,966,968],{"class":293,"line":458},[291,964,965],{"class":844},"      STUNDECK_LISTEN",[291,967,863],{"class":553},[291,969,970],{"class":301},"0.0.0.0:8080\n",[291,972,973,976,978],{"class":293,"line":464},[291,974,975],{"class":844},"      STUNDECK_DATA_DIR",[291,977,863],{"class":553},[291,979,980],{"class":301},"\u002Fvar\u002Flib\u002Fstundeck\n",[291,982,983,986,988],{"class":293,"line":469},[291,984,985],{"class":844},"      STUNDECK_SECURE_COOKIES",[291,987,863],{"class":553},[291,989,990],{"class":301},"\"false\"\n",[291,992,993,996,998],{"class":293,"line":478},[291,994,995],{"class":844},"      TZ",[291,997,863],{"class":553},[291,999,1000],{"class":301},"Asia\u002FShanghai\n",[291,1002,1003,1006,1008],{"class":293,"line":491},[291,1004,1005],{"class":844},"      HTTP_PROXY",[291,1007,863],{"class":553},[291,1009,1010],{"class":301},"\"\"\n",[291,1012,1013,1016,1018],{"class":293,"line":499},[291,1014,1015],{"class":844},"      HTTPS_PROXY",[291,1017,863],{"class":553},[291,1019,1010],{"class":301},[291,1021,1022,1025,1027],{"class":293,"line":507},[291,1023,1024],{"class":844},"      ALL_PROXY",[291,1026,863],{"class":553},[291,1028,1010],{"class":301},[291,1030,1031,1034,1036],{"class":293,"line":515},[291,1032,1033],{"class":844},"      http_proxy",[291,1035,863],{"class":553},[291,1037,1010],{"class":301},[291,1039,1040,1043,1045],{"class":293,"line":523},[291,1041,1042],{"class":844},"      https_proxy",[291,1044,863],{"class":553},[291,1046,1010],{"class":301},[291,1048,1050,1053,1055],{"class":293,"line":1049},25,[291,1051,1052],{"class":844},"      all_proxy",[291,1054,863],{"class":553},[291,1056,1010],{"class":301},[291,1058,1060],{"class":293,"line":1059},26,[291,1061,332],{"emptyLinePlaceholder":331},[291,1063,1065,1068],{"class":293,"line":1064},27,[291,1066,1067],{"class":844},"volumes",[291,1069,848],{"class":553},[291,1071,1073,1076],{"class":293,"line":1072},28,[291,1074,1075],{"class":844},"  stundeck-data",[291,1077,848],{"class":553},[16,1079,1080],{},"几个关键点：",[39,1082,1083,1089,1102,1108,1111],{},[42,1084,1085,1088],{},[52,1086,1087],{},"network_mode: host","：NATMap 和 UPnP 需要看到 Linux 主机的真实网络栈；",[42,1090,1091,1094,1095,1094,1098,1101],{},[52,1092,1093],{},"read_only","、",[52,1096,1097],{},"cap_drop: ALL",[52,1099,1100],{},"no-new-privileges","：减少容器权限；",[42,1103,1104,1105,55],{},"只持久化 ",[52,1106,1107],{},"\u002Fvar\u002Flib\u002Fstundeck",[42,1109,1110],{},"显式清空大小写代理变量；",[42,1112,1113,1114,1117,1118,131],{},"局域网 HTTP 首次部署使用 ",[52,1115,1116],{},"STUNDECK_SECURE_COOKIES: \"false\"","；如果以后由 HTTPS 反向代理访问，应改为 ",[52,1119,1120],{},"\"true\"",[16,1122,1123,1124,131],{},"不要使用 Docker Desktop 作为正式 STUN 网关，也不要为了省事加入 ",[52,1125,1126],{},"privileged: true",[78,1128,1130],{"id":1129},"_4-拉取并启动","4. 拉取并启动",[193,1132,1134],{"className":285,"code":1133,"language":287,"meta":199,"style":199},"cd \u002Fopt\u002Fstundeck\nsudo docker compose config --quiet\nsudo docker compose pull\nsudo docker compose up -d --no-build\nsudo docker compose ps\n",[52,1135,1136,1142,1156,1167,1183],{"__ignoreMap":199},[291,1137,1138,1140],{"class":293,"line":294},[291,1139,823],{"class":318},[291,1141,818],{"class":301},[291,1143,1144,1146,1148,1150,1153],{"class":293,"line":308},[291,1145,298],{"class":297},[291,1147,711],{"class":301},[291,1149,723],{"class":301},[291,1151,1152],{"class":301}," config",[291,1154,1155],{"class":318}," --quiet\n",[291,1157,1158,1160,1162,1164],{"class":293,"line":328},[291,1159,298],{"class":297},[291,1161,711],{"class":301},[291,1163,723],{"class":301},[291,1165,1166],{"class":301}," pull\n",[291,1168,1169,1171,1173,1175,1178,1180],{"class":293,"line":335},[291,1170,298],{"class":297},[291,1172,711],{"class":301},[291,1174,723],{"class":301},[291,1176,1177],{"class":301}," up",[291,1179,348],{"class":318},[291,1181,1182],{"class":318}," --no-build\n",[291,1184,1185,1187,1189,1191],{"class":293,"line":354},[291,1186,298],{"class":297},[291,1188,711],{"class":301},[291,1190,723],{"class":301},[291,1192,1193],{"class":301}," ps\n",[16,1195,1196,1197,1200],{},"本次环境的健康状态在约 10 秒后变为 ",[52,1198,1199],{},"healthy","。继续验证：",[193,1202,1204],{"className":285,"code":1203,"language":287,"meta":199,"style":199},"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",[52,1205,1206,1217,1221,1235,1245],{"__ignoreMap":199},[291,1207,1208,1211,1214],{"class":293,"line":294},[291,1209,1210],{"class":297},"curl",[291,1212,1213],{"class":318}," -fsS",[291,1215,1216],{"class":301}," http:\u002F\u002F127.0.0.1:8080\u002Fapi\u002Fv1\u002Fhealth\n",[291,1218,1219],{"class":293,"line":308},[291,1220,332],{"emptyLinePlaceholder":331},[291,1222,1223,1225,1227,1230,1233],{"class":293,"line":328},[291,1224,298],{"class":297},[291,1226,711],{"class":301},[291,1228,1229],{"class":301}," inspect",[291,1231,1232],{"class":301}," stundeck-stundeck-1",[291,1234,368],{"class":318},[291,1236,1237,1240,1243],{"class":293,"line":335},[291,1238,1239],{"class":318},"  --format",[291,1241,1242],{"class":301}," '{{range .Config.Env}}{{println .}}{{end}}'",[291,1244,368],{"class":318},[291,1246,1247,1250,1253,1256],{"class":293,"line":354},[291,1248,1249],{"class":409},"  |",[291,1251,1252],{"class":297}," grep",[291,1254,1255],{"class":318}," -i",[291,1257,1258],{"class":301}," proxy\n",[16,1260,1261,1262,1265],{},"健康接口应返回 ",[52,1263,1264],{},"status: ok","。代理检查会看到 6 个变量，但等号右侧都必须为空。镜像拉取时即使临时使用过代理，也要在启动 StunDeck 前撤销 daemon 代理并重启 Docker。",[16,1267,1268],{},[782,1269],{"alt":1270,"src":1271},"Compose 启动、健康接口与容器代理变量的核验结果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcompose-health-verified-v1.webp",[16,1273,1274],{},[789,1275,1276],{},"本次容器进入 healthy，健康接口返回 ok，容器内的大小写代理变量全部为空。画面未保留主机地址或容器标识。",[78,1278,1280],{"id":1279},"_5-首次初始化与安全边界","5. 首次初始化与安全边界",[16,1282,1283],{},"从局域网打开：",[193,1285,1288],{"className":1286,"code":1287,"language":198,"meta":199},[196],"http:\u002F\u002F服务器局域网地址:8080\n",[52,1289,1287],{"__ignoreMap":199},[16,1291,1292],{},"首次进入需要自行创建管理员：",[136,1294,1295,1298,1301,1304,1307],{},[42,1296,1297],{},"使用唯一用户名和至少 12 位的强密码；",[42,1299,1300],{},"访问模式选择“局域网”；",[42,1302,1303],{},"如果已经确定反向代理域名，可以填入允许访问的 Host；",[42,1305,1306],{},"登录后进入“安全设置”，再配置允许 Host 与验证器 2FA；",[42,1308,1309],{},"不要直接把 8080 控制面映射到公网。",[16,1311,1312],{},[782,1313],{"alt":1314,"src":1315},"StunDeck 首次进入时的管理员创建页面","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-first-run-v1.webp",[16,1317,1318],{},[789,1319,1320],{},"一次性空数据环境中的真实首次初始化页面；用户名、密码与确认密码都没有填写。",[16,1322,1323],{},[782,1324],{"alt":1325,"src":1326},"StunDeck 安全设置，显示局域网访问范围、Host 白名单与 2FA 入口","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-security-domain-v1.webp",[16,1328,1329],{},[789,1330,1331,1332,1335],{},"域名 ",[52,1333,1334],{},"stun-lab-20260802.sparkles-editor.com"," 经本人确认可以公开，仅用于演示 Host 白名单；截图环境未保存该值。画面没有 DNS 记录、Zone\u002FRule ID、Token、TOTP 密钥或管理员身份。",[16,1337,1338],{},"如果必须远程管理控制面，优先使用 Cloudflare Tunnel、VPN 或其他带身份认证的反向代理。STUN 动态映射更适合发布可控业务端口，不适合暴露管理后台。",[78,1340,1342],{"id":1341},"_6-先在-cloudflare-创建最小权限-api-token","6. 先在 Cloudflare 创建最小权限 API Token",[16,1344,1345,1346,1349],{},"Single Redirect 只会处理经过 Cloudflare 代理的请求，因此入口域名必须存在于目标 Zone，并使用",[20,1347,1348],{},"代理 DNS（橙色云）","。本文不展示 DNS 记录值、Zone ID、Ruleset ID 或其他域名。",[16,1351,1352],{},"进入 Cloudflare 右上角用户菜单，打开“个人资料 → API 令牌”，点击“创建令牌”。",[16,1354,1355],{},[782,1356],{"alt":1357,"src":1358},"Cloudflare 用户 API 令牌页面的创建入口","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-entry-v2.webp",[16,1360,1361],{},"选择“创建自定义令牌”，不要使用 Global API Key。",[16,1363,1364],{},[782,1365],{"alt":1366,"src":1367},"Cloudflare 自定义 API 令牌入口","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-custom-token-v2.webp",[16,1369,1370,1371,1374],{},"建议使用可识别的名称，例如 ",[52,1372,1373],{},"StunDeck Redirect · sparkles-editor.com","，然后添加三条 Zone 权限：",[1376,1377,1378,1397],"table",{},[1379,1380,1381],"thead",{},[1382,1383,1384,1388,1391,1394],"tr",{},[1385,1386,1387],"th",{},"资源",[1385,1389,1390],{},"权限",[1385,1392,1393],{},"级别",[1385,1395,1396],{},"用途",[1398,1399,1400,1414,1427],"tbody",{},[1382,1401,1402,1406,1408,1411],{},[1403,1404,1405],"td",{},"区域",[1403,1407,1405],{},[1403,1409,1410],{},"读取",[1403,1412,1413],{},"查找并校验目标 Zone",[1382,1415,1416,1418,1421,1424],{},[1403,1417,1405],{},[1403,1419,1420],{},"DNS",[1403,1422,1423],{},"编辑",[1403,1425,1426],{},"自动创建或维护代理 DNS 记录",[1382,1428,1429,1431,1434,1436],{},[1403,1430,1405],{},[1403,1432,1433],{},"单一重定向",[1403,1435,1423],{},[1403,1437,1438],{},"创建和更新 302\u002F307 Single Redirect",[16,1440,1441,1442,1445],{},"在“区域资源”中选择“包括 → 特定区域 → ",[52,1443,1444],{},"sparkles-editor.com","”。不要授权所有 Zone。若你关闭 StunDeck 的“自动管理 DNS”，可以不授予 DNS 编辑，但必须自行保证入口记录存在且处于代理状态。",[16,1447,1448],{},[782,1449],{"alt":1450,"src":1451},"Cloudflare 自定义令牌的三项最小权限与特定 Zone 范围","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-permissions-v2.webp",[16,1453,1454],{},"客户端 IP 筛选只适合管理端出口固定的环境。家庭宽带出口会变化时，不要误把当前公网 IP 写死，否则 StunDeck 之后可能无法同步。可以按运维周期设置 Token 到期日。",[16,1456,1457],{},"点击“继续以显示摘要”，确认目标 Zone 只有三项权限：",[16,1459,1460],{},[782,1461],{"alt":1462,"src":1463},"Cloudflare 令牌摘要中的特定 Zone 与最小权限","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-summary-v2.webp",[16,1465,1466,1467,1470],{},"最后点击“创建令牌”。",[20,1468,1469],{},"Token 明文只显示一次","：立即放入密码管理器并粘贴到 StunDeck，不要截图、不要写入 Compose、Markdown、Git、日志或聊天记录。本文为了避免创建不必要的长期凭据，截图停在复核页，没有展示或保存任何 Token 值。",[78,1472,1474],{"id":1473},"_7-在-stundeck-保存并检测-cloudflare-连接","7. 在 StunDeck 保存并检测 Cloudflare 连接",[16,1476,1477],{},"先进入 StunDeck 的“Cloudflare”页面，再创建映射服务：",[136,1479,1480,1483,1486,1489],{},[42,1481,1482],{},"连接名称填写易识别的名称；",[42,1484,1485],{},"粘贴刚创建的 API Token；",[42,1487,1488],{},"点击“检测权限”；",[42,1490,1491],{},"只在 Zone、DNS 和 Single Redirect 检测都通过后保存。",[16,1493,1494],{},[782,1495],{"alt":1496,"src":1497},"StunDeck Cloudflare 连接页，Token 输入框保持为空","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-cf-connect-v2.webp",[16,1499,1500],{},[789,1501,1502],{},"来自重建后的真实管理页面。输入框为空，截图不含 Token、账户名、DNS 值或任何资源 ID。",[16,1504,1505,1506,1508],{},"凭据会由 StunDeck 使用本地主密钥加密保存。备份时必须同时保留 SQLite 数据库与 ",[52,1507,180],{},"；缺少后者，连接凭据无法恢复。",[78,1510,1512],{"id":1511},"_8-创建以-302-为核心的-redirect-服务","8. 创建以 302 为核心的 Redirect 服务",[16,1514,1515],{},"现在进入“映射服务”，使用一项可以随时关闭的 HTTP 测试服务：",[193,1517,1520],{"className":1518,"code":1519,"language":198,"meta":199},[196],"服务名称：家庭 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",[52,1521,1519],{"__ignoreMap":199},[16,1523,1524],{},[782,1525],{"alt":1526,"src":1527},"StunDeck 映射服务中的 Cloudflare Redirect、入口域名与 302 选项","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-cf-redirect-service-v2.webp",[16,1529,1530],{},[789,1531,1532],{},"真实页面中的文档表单；局域网地址和端口均为占位示例，Cloudflare 连接未保存，未提交这条服务。画面上方的实际服务内容已裁掉。",[576,1534,1536],{"id":1535},"_302-还是-307","302 还是 307",[1376,1538,1539,1552],{},[1379,1540,1541],{},[1382,1542,1543,1546,1549],{},[1385,1544,1545],{},"状态",[1385,1547,1548],{},"客户端行为",[1385,1550,1551],{},"推荐场景",[1398,1553,1554,1565],{},[1382,1555,1556,1559,1562],{},[1403,1557,1558],{},"302 Temporary",[1403,1560,1561],{},"临时跳转；部分客户端会把 POST 改成 GET",[1403,1563,1564],{},"普通网页、下载页、以 GET 为主的服务，本文默认",[1382,1566,1567,1570,1573],{},[1403,1568,1569],{},"307 Temporary Redirect",[1403,1571,1572],{},"临时跳转并保留 HTTP 方法与请求体",[1403,1574,1575],{},"API、Webhook、上传或不能改变 POST 语义的服务",[16,1577,1578],{},"两者都不会被当作永久跳转缓存。不要使用 301\u002F308 表示会持续变化的 STUN 端点。",[16,1580,1581,1582,1585],{},"“保留路径”和“保留查询参数”开启后，访问 ",[52,1583,1584],{},"\u002Ffiles?a=1"," 会把路径与查询串拼到当前动态目标。使用 HTTPS 目标时还必须填写“HTTPS 目标域名”，并保证目标证书覆盖该域名；直接跳到公网 IP 通常无法通过证书主机名校验。",[16,1587,1588],{},"自动 DNS 只维护 StunDeck 自己创建并带有管理标记的记录。若同名记录由其他系统管理，StunDeck 会停止同步而不是强行覆盖；应先处理冲突，不能删除整组 DNS 后重建。",[78,1590,1592],{"id":1591},"_9-启动服务并完成-stun-upnp-检测","9. 启动服务并完成 STUN \u002F UPnP 检测",[16,1594,1595],{},"点击“创建服务”，再点击“启动”。正常状态会依次经过：",[193,1597,1600],{"className":1598,"code":1599,"language":198,"meta":199},[196],"已停止 → 探测中 → 路由已放行 → Cloudflare 已同步\n",[52,1601,1599],{"__ignoreMap":199},[16,1603,1604,1605,1608],{},"StunDeck 的 UPnP 逻辑会把",[20,1606,1607],{},"穿透监听端口映射到同一个本地端口","，而不是把最终动态公网端口误当成本地端口。随后它把 NATMap 发现的公网 IP 与端口写入 Single Redirect 的目标。",[16,1610,1611],{},"点击服务卡片上的“STUN 检测”，逐项检查：",[39,1613,1614,1620,1626,1632,1638,1644,1650,1656],{},[42,1615,1616,1619],{},[20,1617,1618],{},"代理环境","：Docker daemon 与容器都必须没有常驻代理；",[42,1621,1622,1625],{},[20,1623,1624],{},"NATMap 程序","：二进制可执行；",[42,1627,1628,1631],{},[20,1629,1630],{},"目标端口","：StunDeck 主机能连接局域网服务；",[42,1633,1634,1637],{},[20,1635,1636],{},"UDP\u002FTCP STUN","：至少与你所选协议对应的 Binding 成功；",[42,1639,1640,1643],{},[20,1641,1642],{},"路由器能力","：发现正确的 UPnP IGD 或 NAT-PMP 网关；",[42,1645,1646,1649],{},[20,1647,1648],{},"路由器端口映射","：映射已下发；",[42,1651,1652,1655],{},[20,1653,1654],{},"公网映射","：已取得地址且更新时间合理；",[42,1657,1658,1661],{},[20,1659,1660],{},"Cloudflare 同步","：DNS 与 Redirect Rule 均已完成或明确报告冲突。",[16,1663,1664],{},[782,1665],{"alt":1666,"src":1667},"StunDeck STUN 诊断，显示代理为空、NATMap 可执行、目标端口可达及 Binding 成功","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-diagnostics-v1.webp",[16,1669,1670],{},[789,1671,1672],{},"真实诊断结果的安全裁切；公网 IP、动态端口、内网地址与网关地址均不在画面中。",[16,1674,1675],{},[782,1676],{"alt":1677,"src":1678},"StunDeck 运行概览，显示一个服务、一个已发现映射和运行版本","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-overview-v1.webp",[16,1680,1681],{},[789,1682,1683],{},"真实环境概览；没有展示服务地址、映射地址、凭据或 Cloudflare 配置。",[78,1685,1687],{"id":1686},"_10-验证固定域名的-302-与第二跳","10. 验证固定域名的 302 与第二跳",[16,1689,1690],{},"先禁止 curl 自动跟随跳转，只检查 Cloudflare 第一跳：",[193,1692,1694],{"className":285,"code":1693,"language":287,"meta":199,"style":199},"curl -sS -o \u002Fdev\u002Fnull -D - --max-redirs 0 \\\n  https:\u002F\u002Fstun-lab-20260802.sparkles-editor.com\u002F\n",[52,1695,1696,1723],{"__ignoreMap":199},[291,1697,1698,1700,1703,1706,1709,1712,1715,1718,1721],{"class":293,"line":294},[291,1699,1210],{"class":297},[291,1701,1702],{"class":318}," -sS",[291,1704,1705],{"class":318}," -o",[291,1707,1708],{"class":301}," \u002Fdev\u002Fnull",[291,1710,1711],{"class":318}," -D",[291,1713,1714],{"class":301}," -",[291,1716,1717],{"class":318}," --max-redirs",[291,1719,1720],{"class":318}," 0",[291,1722,368],{"class":318},[291,1724,1725],{"class":293,"line":308},[291,1726,1727],{"class":301},"  https:\u002F\u002Fstun-lab-20260802.sparkles-editor.com\u002F\n",[16,1729,1730,1731,1734,1735,1737],{},"应看到 ",[52,1732,1733],{},"HTTP\u002F2 302","（选择 307 时则为 307）以及 ",[52,1736,67],{},"。Cloudflare 规则列表也应显示同一入口域名、302 状态与活动状态：",[16,1739,1740],{},[782,1741],{"alt":1742,"src":1743},"Cloudflare 规则列表中的 StunDeck 302 Single Redirect，动态目标已从截图中裁除","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-redirect-rule-v2.webp",[16,1745,1746],{},[789,1747,1748],{},"真实 Cloudflare 规则行；仅保留经允许公开的入口域名和 302 状态，动态公网目标、账户信息、其他规则与资源 ID 均未进入画面。",[16,1750,1751],{},"再从手机蜂窝网络或独立外网主机验证第二跳：",[193,1753,1755],{"className":285,"code":1754,"language":287,"meta":199,"style":199},"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",[52,1756,1757,1778,1788],{"__ignoreMap":199},[291,1758,1759,1761,1763,1766,1769,1772,1774,1776],{"class":293,"line":294},[291,1760,1210],{"class":297},[291,1762,1702],{"class":318},[291,1764,1765],{"class":318}," -L",[291,1767,1768],{"class":318}," --max-time",[291,1770,1771],{"class":318}," 15",[291,1773,1705],{"class":318},[291,1775,1708],{"class":301},[291,1777,368],{"class":318},[291,1779,1780,1783,1786],{"class":293,"line":308},[291,1781,1782],{"class":318},"  -w",[291,1784,1785],{"class":301}," 'status=%{http_code} redirects=%{num_redirects}\\n'",[291,1787,368],{"class":318},[291,1789,1790],{"class":293,"line":328},[291,1791,1727],{"class":301},[16,1793,1794,1795,1798,1799,1801],{},"目标是 ",[52,1796,1797],{},"status=200 redirects=1","。本次重建中，独立外网主机直连新的动态映射得到 HTTP 200；固定入口也实测返回 302 和 ",[52,1800,67],{},"。不过截图所示的旧隔离服务随后已下线，所以它现在只能证明第一跳规则仍生效，不能作为第二跳在线承诺。教程因此明确要求每次映射变化后同时检查这两层。",[16,1803,1804],{},[782,1805],{"alt":1806,"src":1807},"局域网目标与独立外网第二跳都返回 HTTP 200 的脱敏核验结果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fexternal-proof-verified-v1.webp",[16,1809,1810,1811,1813],{},"不要把真实 ",[52,1812,67],{},"、公网 IP 与端口贴在文章、截图或工单里。它会变化，也会增加不必要的扫描风险。",[78,1815,1817],{"id":1816},"_11-常见故障定位","11. 常见故障定位",[576,1819,1821,1822],{"id":1820},"已取得公网映射但浏览器是-err_empty_response","已取得公网映射，但浏览器是 ",[52,1823,1824],{},"ERR_EMPTY_RESPONSE",[16,1826,1827],{},"按顺序检查：",[136,1829,1830,1833,1839,1842,1845,1848,1851],{},[42,1831,1832],{},"局域网目标是否从 StunDeck 主机可达；",[42,1834,1835,1836,55],{},"目标服务是否监听正确地址，而不是只监听 ",[52,1837,1838],{},"127.0.0.1",[42,1840,1841],{},"容器是否使用 host network；",[42,1843,1844],{},"StunDeck 服务是否选择了正确的网关模式；",[42,1846,1847],{},"UPnP\u002FNAT-PMP 是否在真实出网网关下发映射；",[42,1849,1850],{},"多层 NAT 时，上一级光猫或路由器是否仍阻断入站；",[42,1852,1853],{},"是否用真正的外网测试，而不是在不支持 NAT Loopback 的局域网里访问公网地址。",[576,1855,1857],{"id":1856},"stun-时好时坏","STUN 时好时坏",[39,1859,1860,1863,1866,1869,1872,1875],{},[42,1861,1862],{},"确认 Docker daemon 与容器都没有代理；",[42,1864,1865],{},"关闭会改变出口的透明代理、策略路由或多 WAN 规则后对比；",[42,1867,1868],{},"保持监听端口唯一；",[42,1870,1871],{},"不要把动态端口当成固定端口写入客户端；",[42,1873,1874],{},"观察映射更新时间与 NATMap 进程是否持续运行；",[42,1876,1877],{},"对称型 NAT、运营商 CGNAT 或入站过滤严格时，改用 Tunnel、VPN 或中继方案。",[576,1879,1881],{"id":1880},"固定域名没有返回-302307","固定域名没有返回 302\u002F307",[16,1883,1827],{},[136,1885,1886,1889,1892,1895,1898,1901],{},[42,1887,1888],{},"入口 DNS 是否为代理状态（橙色云）；",[42,1890,1891],{},"Token 是否限定到正确 Zone；",[42,1893,1894],{},"是否具备 Zone 读取与单一重定向编辑权限；",[42,1896,1897],{},"开启自动 DNS 时，是否额外具备 DNS 编辑权限；",[42,1899,1900],{},"同名 DNS 或 Redirect Rule 是否由其他系统管理而发生冲突；",[42,1902,1903,1904,1907],{},"Cloudflare Rules 中是否存在 ",[52,1905,1906],{},"http_request_dynamic_redirect"," 阶段的活动规则。",[576,1909,1911],{"id":1910},"_302-能打开入口但第二跳失败","302 能打开入口，但第二跳失败",[16,1913,1914],{},"302 只更新入口。直接请求动态公网地址；如果仍失败，问题在 STUN、路由器映射、防火墙或目标服务，而不是 Cloudflare Redirect Rule。",[78,1916,1918],{"id":1917},"_12-更新备份与卸载","12. 更新、备份与卸载",[16,1920,1921,1922,1924,1925,1928],{},"更新前把 SQLite 数据库和 ",[52,1923,180],{}," 一起备份，二者缺一不可。Compose 命名卷通常为 ",[52,1926,1927],{},"stundeck_stundeck-data","，先确认实际名称：",[193,1930,1932],{"className":285,"code":1931,"language":287,"meta":199,"style":199},"sudo docker volume ls\nsudo docker volume inspect stundeck_stundeck-data\n",[52,1933,1934,1946],{"__ignoreMap":199},[291,1935,1936,1938,1940,1943],{"class":293,"line":294},[291,1937,298],{"class":297},[291,1939,711],{"class":301},[291,1941,1942],{"class":301}," volume",[291,1944,1945],{"class":301}," ls\n",[291,1947,1948,1950,1952,1954,1956],{"class":293,"line":308},[291,1949,298],{"class":297},[291,1951,711],{"class":301},[291,1953,1942],{"class":301},[291,1955,1229],{"class":301},[291,1957,1958],{"class":301}," stundeck_stundeck-data\n",[16,1960,1961],{},"更新：",[193,1963,1965],{"className":285,"code":1964,"language":287,"meta":199,"style":199},"cd \u002Fopt\u002Fstundeck\nsudo docker compose pull\nsudo docker compose up -d --no-build\nsudo docker compose ps\n",[52,1966,1967,1973,1983,1997],{"__ignoreMap":199},[291,1968,1969,1971],{"class":293,"line":294},[291,1970,823],{"class":318},[291,1972,818],{"class":301},[291,1974,1975,1977,1979,1981],{"class":293,"line":308},[291,1976,298],{"class":297},[291,1978,711],{"class":301},[291,1980,723],{"class":301},[291,1982,1166],{"class":301},[291,1984,1985,1987,1989,1991,1993,1995],{"class":293,"line":328},[291,1986,298],{"class":297},[291,1988,711],{"class":301},[291,1990,723],{"class":301},[291,1992,1177],{"class":301},[291,1994,348],{"class":318},[291,1996,1182],{"class":318},[291,1998,1999,2001,2003,2005],{"class":293,"line":335},[291,2000,298],{"class":297},[291,2002,711],{"class":301},[291,2004,723],{"class":301},[291,2006,1193],{"class":301},[16,2008,2009],{},"停止但保留数据：",[193,2011,2013],{"className":285,"code":2012,"language":287,"meta":199,"style":199},"sudo docker compose down\n",[52,2014,2015],{"__ignoreMap":199},[291,2016,2017,2019,2021,2023],{"class":293,"line":294},[291,2018,298],{"class":297},[291,2020,711],{"class":301},[291,2022,723],{"class":301},[291,2024,2025],{"class":301}," down\n",[16,2027,2028],{},"只有确定不要恢复时才删除卷：",[193,2030,2032],{"className":285,"code":2031,"language":287,"meta":199,"style":199},"sudo docker compose down -v\n",[52,2033,2034],{"__ignoreMap":199},[291,2035,2036,2038,2040,2042,2045],{"class":293,"line":294},[291,2037,298],{"class":297},[291,2039,711],{"class":301},[291,2041,723],{"class":301},[291,2043,2044],{"class":301}," down",[291,2046,2047],{"class":318}," -v\n",[16,2049,2050,2053],{},[52,2051,2052],{},"down -v"," 会删除管理员、服务、事件记录、Cloudflare 加密凭据以及本地主密钥，无法通过重新拉取镜像恢复。",[78,2055,2056],{"id":2056},"参考资料",[39,2058,2059,2066,2072,2079,2085,2092,2098,2105,2112,2119],{},[42,2060,2061],{},[111,2062,2065],{"href":2063,"rel":2064},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002Fstundeck",[115],"StunDeck 开源仓库",[42,2067,2068],{},[111,2069,2071],{"href":122,"rel":2070},[115],"StunDeck v0.1.1 Release 与 FPK",[42,2073,2074],{},[111,2075,2078],{"href":2076,"rel":2077},"https:\u002F\u002Fdeveloper.fnnas.com\u002Fdocs\u002Fcli\u002Ffnpack",[115],"飞牛 fnOS 开发者文档：fnpack",[42,2080,2081],{},[111,2082,2084],{"href":272,"rel":2083},[115],"Docker：Install Docker Engine on Debian",[42,2086,2087],{},[111,2088,2091],{"href":2089,"rel":2090},"https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002Finstall\u002Flinux\u002F",[115],"Docker：Install the Compose plugin",[42,2093,2094],{},[111,2095,2097],{"href":256,"rel":2096},[115],"Lucky：STUN 内网穿透",[42,2099,2100],{},[111,2101,2104],{"href":2102,"rel":2103},"https:\u002F\u002Fgithub.com\u002Fheiher\u002Fnatmap",[115],"NATMap 开源仓库",[42,2106,2107],{},[111,2108,2111],{"href":2109,"rel":2110},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Fapi\u002Fget-started\u002Fcreate-token\u002F",[115],"Cloudflare：创建 API Token",[42,2113,2114],{},[111,2115,2118],{"href":2116,"rel":2117},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fcreate-dashboard\u002F",[115],"Cloudflare：通过 Dashboard 创建 Single Redirect",[42,2120,2121],{},[111,2122,2125],{"href":2123,"rel":2124},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fsettings\u002F",[115],"Cloudflare：Single Redirect 设置与代理 DNS 要求",[16,2127,2128,2129,2132],{},"这次重建最重要的结论是把验证拆成五层：",[20,2130,2131],{},"目标端口可达、STUN Binding 成功、网关映射已下发、Cloudflare 固定入口返回 302\u002F307、独立外网跟随跳转后业务成功","。固定域名解决的是“入口不变”，不是“第二跳自动获得隧道保护”。",[2134,2135,2136],"style",{},"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":199,"searchDepth":328,"depth":328,"links":2138},[2139,2140,2141,2142,2145,2146,2147,2148,2149,2150,2153,2154,2155,2162,2163],{"id":80,"depth":308,"text":81},{"id":191,"depth":308,"text":191},{"id":227,"depth":308,"text":228},{"id":265,"depth":308,"text":266,"children":2143},[2144],{"id":578,"depth":328,"text":579},{"id":794,"depth":308,"text":795},{"id":1129,"depth":308,"text":1130},{"id":1279,"depth":308,"text":1280},{"id":1341,"depth":308,"text":1342},{"id":1473,"depth":308,"text":1474},{"id":1511,"depth":308,"text":1512,"children":2151},[2152],{"id":1535,"depth":328,"text":1536},{"id":1591,"depth":308,"text":1592},{"id":1686,"depth":308,"text":1687},{"id":1816,"depth":308,"text":1817,"children":2156},[2157,2159,2160,2161],{"id":1820,"depth":328,"text":2158},"已取得公网映射，但浏览器是 ERR_EMPTY_RESPONSE",{"id":1856,"depth":328,"text":1857},{"id":1880,"depth":328,"text":1881},{"id":1910,"depth":328,"text":1911},{"id":1917,"depth":308,"text":1918},{"id":2056,"depth":308,"text":2056},"在飞牛 fnOS 手动安装 StunDeck FPK，或使用 Docker 部署；再配置 Cloudflare 最小权限 API Token、代理 DNS 与 Single Redirect，让固定域名跟随动态 STUN 公网地址。",false,null,"md",{"src":1678,"alt":2169,"caption":2170,"width":2171,"height":2172},"StunDeck 运行概览，显示一个已配置服务和一个已发现公网映射","本文重建环境的真实界面；画面不含用户名、密码、Token、内网地址或公网端口。",1280,720,{},"\u002Fblog\u002Fstundeck-docker-deployment-guide","2026-08-03",35,{"title":10,"description":2164},"blog\u002Fstundeck-docker-deployment-guide",[2180,2181,89,2182,2183,2184,2185,26,2186],"StunDeck","fnOS","Docker","STUN","UPnP","Cloudflare","Homelab","2026-08-26",{"src":2189,"poster":1678,"title":2190,"description":2191,"duration":2192,"chapters":422,"width":2193,"height":2194},"\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",[2197,2927,4500,6123,7709,10092,11795,12052,12501,12902],{"id":2198,"title":2199,"accent":2200,"body":2201,"description":2908,"draft":2165,"experience":2166,"extension":2167,"featured":331,"image":2909,"meta":2915,"navigation":331,"path":2916,"publishedAt":2917,"readingTime":440,"seo":2918,"stem":2919,"tags":2920,"updatedAt":2925,"video":2166,"__hash__":2926,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Ftunneldeck-desktop-ssh-tunnel-guide.md","TunnelDeck 桌面版使用指南：把 ssh -L 变成可保存的隧道","mint",{"type":13,"value":2202,"toc":2882},[2203,2214,2217,2240,2249,2252,2256,2267,2287,2302,2305,2308,2375,2388,2394,2398,2405,2409,2412,2444,2447,2466,2472,2491,2495,2506,2512,2533,2548,2552,2555,2574,2588,2592,2595,2645,2658,2662,2665,2668,2671,2677,2680,2691,2698,2702,2705,2712,2716,2719,2736,2739,2746,2750,2753,2756,2759,2762,2765,2784,2788,2791,2795,2801,2804,2807,2810,2813,2816,2823,2838,2842,2853,2856,2876,2879],[16,2204,2205,2206,2209,2210,2213],{},"SSH 本地端口转发很好用，但当命令越来越多，记住端口、目标地址和密钥位置就会变得麻烦。",[20,2207,2208],{},"TunnelDeck"," 是我做的一款轻量、跨平台的 SSH 隧道桌面工具：它把 ",[52,2211,2212],{},"ssh -L"," 变成可以保存、编辑、快速启停的连接，同时保留主机指纹校验、系统凭据存储和本地监听等安全边界。",[16,2215,2216],{},"例如，下面这条命令：",[193,2218,2220],{"className":285,"code":2219,"language":287,"meta":199,"style":199},"ssh -L 9108:127.0.0.1:9108 -p 33899 root@ssh.example.com\n",[52,2221,2222],{"__ignoreMap":199},[291,2223,2224,2227,2229,2232,2234,2237],{"class":293,"line":294},[291,2225,2226],{"class":297},"ssh",[291,2228,1765],{"class":318},[291,2230,2231],{"class":301}," 9108:127.0.0.1:9108",[291,2233,763],{"class":318},[291,2235,2236],{"class":318}," 33899",[291,2238,2239],{"class":301}," root@ssh.example.com\n",[16,2241,2242,2243,2246,2247,131],{},"可以直接导入 TunnelDeck。连接建立后，本机访问 ",[52,2244,2245],{},"127.0.0.1:9108","，流量会经过 SSH 服务器转发到它所能访问的 ",[52,2248,2245],{},[16,2250,2251],{},"本文以桌面版 v0.3.1 为主，完整介绍安装、创建连接、密码与私钥认证、首次指纹确认、网页快捷入口和常见故障排查。",[78,2253,2255],{"id":2254},"tunneldeck-适合什么场景","TunnelDeck 适合什么场景",[16,2257,2258,2259,2266],{},"TunnelDeck 当前专注于 SSH ",[20,2260,2261,2262,2265],{},"本地端口转发（",[52,2263,2264],{},"-L","）","，适合：",[39,2268,2269,2272,2275,2278,2284],{},[42,2270,2271],{},"临时访问只监听在服务器本机的管理面板；",[42,2273,2274],{},"通过跳板机访问数据库、开发服务或内网 Web 应用；",[42,2276,2277],{},"保存多条常用隧道，按需启动或停止；",[42,2279,2280,2281,2283],{},"不想每次重新输入一整条 ",[52,2282,2212],{}," 命令；",[42,2285,2286],{},"希望网页隧道连接后，可以再手动用默认浏览器打开。",[16,2288,2289,2290,2293,2294,2297,2298,2301],{},"它不是 VPN，也不提供远程转发 ",[52,2291,2292],{},"-R","、SOCKS 动态代理 ",[52,2295,2296],{},"-D"," 或远程 Shell。导入器还会拒绝 ",[52,2299,2300],{},"ProxyCommand"," 等可能执行外部命令的选项。",[78,2303,2304],{"id":2304},"先看懂四个关键参数",[16,2306,2307],{},"以开头的命令为例：",[1376,2309,2310,2323],{},[1379,2311,2312],{},[1382,2313,2314,2317,2320],{},[1385,2315,2316],{},"配置项",[1385,2318,2319],{},"示例值",[1385,2321,2322],{},"含义",[1398,2324,2325,2338,2351,2363],{},[1382,2326,2327,2330,2335],{},[1403,2328,2329],{},"SSH 服务器",[1403,2331,2332],{},[52,2333,2334],{},"ssh.example.com:33899",[1403,2336,2337],{},"TunnelDeck 实际登录的 SSH 主机和端口",[1382,2339,2340,2343,2348],{},[1403,2341,2342],{},"SSH 用户",[1403,2344,2345],{},[52,2346,2347],{},"root",[1403,2349,2350],{},"SSH 登录用户",[1382,2352,2353,2356,2360],{},[1403,2354,2355],{},"本地入口",[1403,2357,2358],{},[52,2359,2245],{},[1403,2361,2362],{},"你的电脑上供应用访问的地址",[1382,2364,2365,2368,2372],{},[1403,2366,2367],{},"远程目标",[1403,2369,2370],{},[52,2371,2245],{},[1403,2373,2374],{},"从 SSH 服务器视角访问的目标",[16,2376,2377,2378,2380,2381,2384,2385,131],{},"最容易混淆的是最后一项：远程目标里的 ",[52,2379,1838],{}," 指 ",[20,2382,2383],{},"SSH 服务器自身","，不是正在运行 TunnelDeck 的电脑。如果服务位于 SSH 服务器能访问的另一台内网机器，就应填写那台机器的地址，例如 ",[52,2386,2387],{},"10.0.0.20:8080",[193,2389,2392],{"className":2390,"code":2391,"language":198,"meta":199},[196],"本机应用\n  └─ 127.0.0.1:9108\n       └─ 加密 SSH 连接 → ssh.example.com:33899\n            └─ SSH 服务器发起连接 → 127.0.0.1:9108\n",[52,2393,2391],{"__ignoreMap":199},[78,2395,2397],{"id":2396},"_1-安装桌面版","1. 安装桌面版",[16,2399,2400,2401,2404],{},"目前项目还没有 Apple 或 Windows 代码签名身份，因此 v0.3.1 ",[20,2402,2403],{},"不提供未签名的预编译桌面安装包","。安装脚本会先检查环境，再下载固定版本源码并在本机完成构建。这种方式不会要求你绕过 macOS Gatekeeper 或 Windows SmartScreen 去运行来源不明的二进制文件。",[576,2406,2408],{"id":2407},"macos-与-linux","macOS 与 Linux",[16,2410,2411],{},"建议先只检查依赖：",[193,2413,2415],{"className":285,"code":2414,"language":287,"meta":199,"style":199},"curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh \\\n  | sh -s -- --check\n",[52,2416,2417,2428],{"__ignoreMap":199},[291,2418,2419,2421,2423,2426],{"class":293,"line":294},[291,2420,1210],{"class":297},[291,2422,362],{"class":318},[291,2424,2425],{"class":301}," https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh",[291,2427,368],{"class":318},[291,2429,2430,2432,2435,2438,2441],{"class":293,"line":308},[291,2431,1249],{"class":409},[291,2433,2434],{"class":297}," sh",[291,2436,2437],{"class":318}," -s",[291,2439,2440],{"class":318}," --",[291,2442,2443],{"class":318}," --check\n",[16,2445,2446],{},"确认输出后再安装：",[193,2448,2450],{"className":285,"code":2449,"language":287,"meta":199,"style":199},"curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh | sh\n",[52,2451,2452],{"__ignoreMap":199},[291,2453,2454,2456,2458,2460,2463],{"class":293,"line":294},[291,2455,1210],{"class":297},[291,2457,362],{"class":318},[291,2459,2425],{"class":301},[291,2461,2462],{"class":409}," |",[291,2464,2465],{"class":297}," sh\n",[16,2467,2468,2469,2471],{},"没有 ",[52,2470,1210],{}," 时也可以使用：",[193,2473,2475],{"className":285,"code":2474,"language":287,"meta":199,"style":199},"wget -qO- https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh | sh\n",[52,2476,2477],{"__ignoreMap":199},[291,2478,2479,2482,2485,2487,2489],{"class":293,"line":294},[291,2480,2481],{"class":297},"wget",[291,2483,2484],{"class":318}," -qO-",[291,2486,2425],{"class":301},[291,2488,2462],{"class":409},[291,2490,2465],{"class":297},[576,2492,2494],{"id":2493},"windows-powershell","Windows PowerShell",[193,2496,2500],{"className":2497,"code":2498,"language":2499,"meta":199,"style":199},"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",[52,2501,2502],{"__ignoreMap":199},[291,2503,2504],{"class":293,"line":294},[291,2505,2498],{},[16,2507,2508,2509,131],{},"安装器会检查 Go 1.25+、Node.js 20+ 和对应平台的构建依赖。缺少 Go 或 Node.js 时，它们会被下载到 TunnelDeck 的私有用户目录，并校验官方 SHA-256，不会修改全局 ",[52,2510,2511],{},"PATH",[39,2513,2514,2520,2526],{},[42,2515,2516,2517,55],{},"macOS 还需要 Xcode Command Line Tools，应用安装到 ",[52,2518,2519],{},"~\u002FApplications\u002FTunnelDeck.app",[42,2521,2522,2523,55],{},"Linux 需要 GTK3 与 WebKitGTK 4.1，程序安装到 ",[52,2524,2525],{},"~\u002F.local\u002Fbin\u002FTunnelDeck",[42,2527,2528,2529,2532],{},"Windows 需要 WebView2，程序安装到 ",[52,2530,2531],{},"%LOCALAPPDATA%\\Programs\\TunnelDeck\\","，同时创建开始菜单入口。",[16,2534,2535,2536,2541,2542,2547],{},"将远程脚本交给 Shell 前，建议先打开 ",[111,2537,2540],{"href":2538,"rel":2539},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fblob\u002Fv0.3.1\u002Finstall.sh",[115],"v0.3.1 安装脚本","阅读内容。完整依赖和手动构建方式也记录在",[111,2543,2546],{"href":2544,"rel":2545},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fblob\u002Fv0.3.1\u002Fdocs\u002FSOURCE_INSTALL.md",[115],"源码安装指南","中。",[78,2549,2551],{"id":2550},"_2-导入已有的-ssh-l-命令","2. 导入已有的 ssh -L 命令",[16,2553,2554],{},"如果手上已有可以工作的命令，最快的方法是：",[136,2556,2557,2560,2565,2568,2571],{},[42,2558,2559],{},"打开 TunnelDeck，点击“导入命令”；",[42,2561,2562,2563,2283],{},"粘贴完整的 ",[52,2564,2212],{},[42,2566,2567],{},"检查解析出的 SSH 主机、端口、用户、本地入口和远程目标；",[42,2569,2570],{},"为连接填写一个便于识别的名称；",[42,2572,2573],{},"选择认证方式，保存后启动。",[16,2575,2576,2577,2580,2581,1094,2583,2585,2586,131],{},"TunnelDeck 解析参数后由 Go SSH 库直接建立连接，",[20,2578,2579],{},"不会把输入拼接成 Shell 命令执行","。为了把导入边界保持在可审计范围内，目前只接受一条本地转发，不接受远程命令、",[52,2582,2292],{},[52,2584,2296],{}," 或 ",[52,2587,2300],{},[78,2589,2591],{"id":2590},"_3-手动创建一条隧道","3. 手动创建一条隧道",[16,2593,2594],{},"点击“新建”，依次填写：",[136,2596,2597,2603,2615,2621,2630,2636,2642],{},[42,2598,2599,2602],{},[20,2600,2601],{},"名称","：例如“开发环境面板”；",[42,2604,2605,2608,2609,533,2612,55],{},[20,2606,2607],{},"SSH 服务器与端口","：例如 ",[52,2610,2611],{},"ssh.example.com",[52,2613,2614],{},"33899",[42,2616,2617,2620],{},[20,2618,2619],{},"SSH 用户名","：使用服务器实际允许登录的用户；",[42,2622,2623,2626,2627,2629],{},[20,2624,2625],{},"本地监听地址与端口","：通常保持 ",[52,2628,1838],{},"，再选一个未占用端口；",[42,2631,2632,2635],{},[20,2633,2634],{},"远程目标地址与端口","：填写 SSH 服务器可以访问的服务；",[42,2637,2638,2641],{},[20,2639,2640],{},"认证方式","：选择密码或 SSH 私钥；",[42,2643,2644],{},"点击“保存并启动”。",[16,2646,2647,2648,2650,2651,2585,2654,2657],{},"默认绑定 ",[52,2649,1838],{},"，意味着只有本机程序能访问这条隧道。除非明确需要让局域网其他设备接入，并且已经配置系统防火墙，否则不要改成 ",[52,2652,2653],{},"0.0.0.0",[52,2655,2656],{},"::","。TunnelDeck 在这种情况下也会显示暴露风险提示。",[78,2659,2661],{"id":2660},"_4-选择密码或私钥认证","4. 选择密码或私钥认证",[576,2663,2664],{"id":2664},"密码认证",[16,2666,2667],{},"输入 SSH 登录密码即可。默认不要勾选“记住凭据”，密码只在当前运行期间保留，停止隧道后会从隧道对象中清除。",[576,2669,2670],{"id":2670},"私钥认证",[16,2672,2673,2674,131],{},"选择私钥文件；如果私钥已加密，再输入私钥口令。公钥需要提前写入服务器对应用户的 ",[52,2675,2676],{},"authorized_keys",[16,2678,2679],{},"如果主动启用“记住凭据”，TunnelDeck 会调用操作系统提供的安全存储：",[39,2681,2682,2685,2688],{},[42,2683,2684],{},"macOS Keychain；",[42,2686,2687],{},"Windows Credential Manager；",[42,2689,2690],{},"Linux Secret Service。",[16,2692,2693,2694,2697],{},"普通的 ",[52,2695,2696],{},"profiles.json"," 不保存密码、私钥内容或私钥口令。即使如此，服务器端仍应使用最小权限账号，限制可登录来源，并定期轮换密钥。",[78,2699,2701],{"id":2700},"_5-首次连接要核对主机指纹","5. 首次连接要核对主机指纹",[16,2703,2704],{},"第一次连接某台 SSH 服务器时，TunnelDeck 会显示其 SHA-256 主机指纹。不要只因为弹窗出现就直接信任：应通过服务器控制台、运维文档或其他可信渠道核对指纹，再点击“信任并连接”。",[16,2706,2707,2708,2711],{},"确认后的记录写入 TunnelDeck 独立的 ",[52,2709,2710],{},"known_hosts","。如果服务器主机密钥后来发生变化，连接会被阻止，而不是静默接受新密钥。正常重装服务器也可能导致变化，但在确认原因之前，不要绕过这个警告——它也可能意味着主机写错或连接被劫持。",[78,2713,2715],{"id":2714},"_6-连接后访问服务","6. 连接后访问服务",[16,2717,2718],{},"隧道显示“运行中”后，使用本地入口访问服务：",[39,2720,2721,2727,2733],{},[42,2722,2723,2724,55],{},"网页：在浏览器打开 ",[52,2725,2726],{},"http:\u002F\u002F127.0.0.1:9108",[42,2728,2729,2730,2732],{},"数据库：让数据库客户端连接 ",[52,2731,1838],{}," 和本地端口；",[42,2734,2735],{},"其他 TCP 服务：同样把客户端目标改为本地入口。",[16,2737,2738],{},"如果目标是网页，可以在配置中启用“这是网页服务”，并选择 HTTP 或 HTTPS。连接成功后会出现“打开网页”按钮，由用户点击后调用系统默认浏览器。",[16,2740,2741,2742,2745],{},"TunnelDeck ",[20,2743,2744],{},"不会","在启动应用、启动隧道或自动重连时擅自打开网页。因为很多端口并不是 HTTP 服务，把“连接”和“打开网页”分开能避免错误跳转，也不会在后台重连时不断弹出浏览器窗口。",[78,2747,2749],{"id":2748},"_7-日常管理与自动重连","7. 日常管理与自动重连",[16,2751,2752],{},"列表会显示每条配置的运行状态和活动连接数。你可以按需启动、停止、编辑或删除配置。编辑运行中的隧道时，应先停止旧连接，再让新参数生效，避免误以为界面已改但旧监听仍在运行。",[16,2754,2755],{},"对于临时网络中断，TunnelDeck 会发送 SSH keepalive，并使用 2–30 秒的指数退避尝试重连。重连只恢复隧道，不会自动打开网页。",[78,2757,2758],{"id":2758},"常见问题排查",[576,2760,2761],{"id":2761},"本地端口无法监听",[16,2763,2764],{},"通常是端口已被其他程序占用。换一个本地端口，或先确认占用者：",[193,2766,2768],{"className":285,"code":2767,"language":287,"meta":199,"style":199},"lsof -nP -iTCP:9108 -sTCP:LISTEN\n",[52,2769,2770],{"__ignoreMap":199},[291,2771,2772,2775,2778,2781],{"class":293,"line":294},[291,2773,2774],{"class":297},"lsof",[291,2776,2777],{"class":318}," -nP",[291,2779,2780],{"class":318}," -iTCP:9108",[291,2782,2783],{"class":318}," -sTCP:LISTEN\n",[576,2785,2787],{"id":2786},"ssh-服务器连不上","SSH 服务器连不上",[16,2789,2790],{},"先检查域名、SSH 端口、用户名、防火墙和当前网络是否允许访问。可以在终端使用相同参数做一次基础连接测试，但不要把密码写进命令或截图。",[576,2792,2794],{"id":2793},"隧道已运行但目标服务打不开","隧道已运行，但目标服务打不开",[16,2796,2797,2798,2800],{},"从 SSH 服务器的视角检查目标地址。尤其注意：远程目标写 ",[52,2799,1838],{}," 时，服务必须运行在 SSH 服务器本机。还要确认目标服务确实监听对应端口，并允许 SSH 用户所在主机访问。",[576,2802,2803],{"id":2803},"私钥无法使用",[16,2805,2806],{},"检查是否选错公钥文件、私钥格式是否受支持，以及加密私钥的口令是否正确。私钥应只对当前系统用户开放读取权限。",[576,2808,2809],{"id":2809},"出现主机密钥变更警告",[16,2811,2812],{},"先暂停连接并核对服务器是否重装、域名是否解析到了新主机。只有通过可信渠道确认新指纹后，才更新信任记录；不要为了“先连上”而关闭校验。",[78,2814,2815],{"id":2815},"为什么桌面版比较轻",[16,2817,2818,2819,2822],{},"TunnelDeck 使用 Go、Wails 和 Vue 3 构建，界面复用操作系统 WebView，不会随应用打包一整套 Chromium。SSH 隧道由 ",[52,2820,2821],{},"golang.org\u002Fx\u002Fcrypto\u002Fssh"," 直接建立。当前 macOS ARM64 应用包约 8.7 MB。",[16,2824,2825,2826,2831,2832,2837],{},"项目采用 MIT License，源码、安装脚本和发布记录都在 ",[111,2827,2830],{"href":2828,"rel":2829},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck",[115],"TunnelDeck GitHub 仓库","。当前稳定版本可在 ",[111,2833,2836],{"href":2834,"rel":2835},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Freleases\u002Ftag\u002Fv0.3.1",[115],"v0.3.1 Release","查看。",[78,2839,2841],{"id":2840},"chrome-应用商店版本已经上线","Chrome 应用商店版本已经上线",[16,2843,2844,2849,2850,131],{},[111,2845,2848],{"href":2846,"rel":2847},"https:\u002F\u002Fchromewebstore.google.com\u002Fdetail\u002Ftunneldeck\u002Fjnfkjehpbkmfnidfcilehhkpbjjinmod",[115],"TunnelDeck Chrome 侧边栏扩展 v0.3.1","现已在 Chrome Web Store 公开上线，可以直接从商店安装。完整源码、桌面端安装脚本和问题反馈仍统一维护在 ",[111,2851,2830],{"href":2828,"rel":2852},[115],[16,2854,2855],{},"推荐按下面的顺序完成安装：",[136,2857,2858,2861,2864,2870,2873],{},[42,2859,2860],{},"先按本文第 1 节或 GitHub README 安装 TunnelDeck 桌面端；",[42,2862,2863],{},"从 Chrome Web Store 安装扩展；",[42,2865,2866,2867,55],{},"启动桌面端，在底部“Chrome 浏览器集成”中确认正式扩展 ID ",[52,2868,2869],{},"jnfkjehpbkmfnidfcilehhkpbjjinmod",[42,2871,2872],{},"如果状态显示尚未注册，点击“注册 Chrome 服务”，然后重新加载扩展；",[42,2874,2875],{},"点击 Chrome 工具栏中的 TunnelDeck 图标，即可在侧边栏管理连接。",[16,2877,2878],{},"浏览器扩展通过 Native Messaging 控制本机 TunnelDeck，可以创建、编辑、导入和启停连接，也能在用户点击后打开已标记的网页服务；它不会在浏览器进程中自行建立 SSH。因此，商店扩展负责快捷操作，桌面版仍然是实际运行 SSH 隧道的核心。",[2134,2880,2881],{},"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":199,"searchDepth":328,"depth":328,"links":2883},[2884,2885,2886,2890,2891,2892,2896,2897,2898,2899,2906,2907],{"id":2254,"depth":308,"text":2255},{"id":2304,"depth":308,"text":2304},{"id":2396,"depth":308,"text":2397,"children":2887},[2888,2889],{"id":2407,"depth":328,"text":2408},{"id":2493,"depth":328,"text":2494},{"id":2550,"depth":308,"text":2551},{"id":2590,"depth":308,"text":2591},{"id":2660,"depth":308,"text":2661,"children":2893},[2894,2895],{"id":2664,"depth":328,"text":2664},{"id":2670,"depth":328,"text":2670},{"id":2700,"depth":308,"text":2701},{"id":2714,"depth":308,"text":2715},{"id":2748,"depth":308,"text":2749},{"id":2758,"depth":308,"text":2758,"children":2900},[2901,2902,2903,2904,2905],{"id":2761,"depth":328,"text":2761},{"id":2786,"depth":328,"text":2787},{"id":2793,"depth":328,"text":2794},{"id":2803,"depth":328,"text":2803},{"id":2809,"depth":328,"text":2809},{"id":2815,"depth":308,"text":2815},{"id":2840,"depth":308,"text":2841},"从源码安装 TunnelDeck，创建或导入 SSH 本地端口转发，选择密码或私钥认证，核对主机指纹，并安全地打开本地网页服务。",{"src":2910,"alt":2911,"caption":2912,"width":2913,"height":2914},"\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":2199,"description":2908},"blog\u002Ftunneldeck-desktop-ssh-tunnel-guide",[2208,2921,2922,2923,2924],"SSH","端口转发","开源工具","Wails","2026-08-10","z5xVIV6kQWiNR8bXRQcxyNofnXsnBlAbQM0bABikqQk",{"id":9,"title":10,"accent":11,"body":2928,"description":2164,"draft":2165,"experience":2166,"extension":2167,"featured":2165,"image":4495,"meta":4496,"navigation":331,"path":2174,"publishedAt":2175,"readingTime":2176,"seo":4497,"stem":2178,"tags":4498,"updatedAt":2187,"video":4499,"__hash__":2195,"contentLocale":4,"isFallback":2165},{"type":13,"value":2929,"toc":4469},[2930,2936,2940,2942,2962,2966,2968,2974,2982,2998,3000,3016,3020,3032,3034,3039,3043,3045,3053,3055,3057,3059,3073,3078,3080,3082,3091,3263,3269,3295,3297,3301,3303,3389,3393,3395,3451,3453,3458,3460,3464,3468,3470,3472,3496,3500,3698,3700,3726,3730,3732,3788,3792,3838,3842,3846,3850,3852,3854,3859,3861,3873,3877,3881,3885,3891,3893,3895,3899,3901,3905,3907,3911,3915,3961,3965,3969,3971,3973,3977,3981,3983,3985,3995,3999,4003,4007,4009,4011,4016,4020,4024,4026,4056,4058,4062,4064,4066,4068,4073,4077,4079,4113,4117,4121,4125,4129,4131,4133,4161,4167,4171,4175,4177,4211,4217,4221,4225,4227,4231,4233,4251,4253,4267,4269,4271,4287,4289,4291,4293,4299,4325,4327,4371,4373,4387,4389,4405,4409,4411,4463,4467],[16,2931,18,2932,23,2934,27],{},[20,2933,22],{},[20,2935,26],{},[16,2937,30,2938,34],{},[20,2939,33],{},[16,2941,37],{},[39,2943,2944,2946,2948,2952,2954,2956,2960],{},[42,2945,44],{},[42,2947,47],{},[42,2949,50,2950,55],{},[52,2951,54],{},[42,2953,58],{},[42,2955,61],{},[42,2957,64,2958,55],{},[52,2959,67],{},[42,2961,70],{},[72,2963,2964],{},[16,2965,76],{},[78,2967,81],{"id":80},[72,2969,2970],{},[16,2971,86,2972,90],{},[20,2973,89],{},[16,2975,93,2976,97,2978,101,2980,105],{},[20,2977,96],{},[52,2979,100],{},[52,2981,104],{},[39,2983,2984,2989,2994],{},[42,2985,2986,117],{},[111,2987,116],{"href":113,"rel":2988},[115],[42,2990,2991,55],{},[111,2992,124],{"href":122,"rel":2993},[115],[42,2995,127,2996,131],{},[52,2997,130],{},[16,2999,134],{},[136,3001,3002,3004,3006,3010,3014],{},[42,3003,140],{},[42,3005,143],{},[42,3007,146,3008,150],{},[52,3009,149],{},[42,3011,153,3012,157],{},[52,3013,156],{},[42,3015,160],{},[16,3017,163,3018,167],{},[52,3019,166],{},[16,3021,170,3022,174,3024,101,3026,181,3028,185,3030,188],{},[52,3023,173],{},[52,3025,177],{},[52,3027,180],{},[20,3029,184],{},[52,3031,180],{},[78,3033,191],{"id":191},[193,3035,3037],{"className":3036,"code":197,"language":198,"meta":199},[196],[52,3038,197],{"__ignoreMap":199},[16,3040,204,3041,207],{},[52,3042,67],{},[16,3044,210],{},[136,3046,3047,3049],{},[42,3048,215],{},[42,3050,218,3051,221],{},[52,3052,67],{},[16,3054,224],{},[78,3056,228],{"id":227},[16,3058,231],{},[39,3060,3061,3063,3065,3067,3069,3071],{},[42,3062,236],{},[42,3064,239],{},[42,3066,242],{},[42,3068,245],{},[42,3070,248],{},[42,3072,251],{},[16,3074,3075,259],{},[111,3076,258],{"href":256,"rel":3077},[115],[16,3079,262],{},[78,3081,266],{"id":265},[16,3083,269,3084,275,3087,279,3089,131],{},[111,3085,274],{"href":272,"rel":3086},[115],[52,3088,278],{},[52,3090,282],{},[193,3092,3093],{"className":285,"code":286,"language":287,"meta":199,"style":199},[52,3094,3095,3103,3117,3121,3135,3147,3153,3163,3167,3183,3187,3191,3195,3199,3203,3207,3211,3215,3223,3235,3241,3247,3253,3259],{"__ignoreMap":199},[291,3096,3097,3099,3101],{"class":293,"line":294},[291,3098,298],{"class":297},[291,3100,302],{"class":301},[291,3102,305],{"class":301},[291,3104,3105,3107,3109,3111,3113,3115],{"class":293,"line":308},[291,3106,298],{"class":297},[291,3108,302],{"class":301},[291,3110,315],{"class":301},[291,3112,319],{"class":318},[291,3114,322],{"class":301},[291,3116,325],{"class":301},[291,3118,3119],{"class":293,"line":328},[291,3120,332],{"emptyLinePlaceholder":331},[291,3122,3123,3125,3127,3129,3131,3133],{"class":293,"line":335},[291,3124,298],{"class":297},[291,3126,315],{"class":301},[291,3128,342],{"class":318},[291,3130,345],{"class":318},[291,3132,348],{"class":318},[291,3134,351],{"class":301},[291,3136,3137,3139,3141,3143,3145],{"class":293,"line":354},[291,3138,298],{"class":297},[291,3140,359],{"class":301},[291,3142,362],{"class":318},[291,3144,365],{"class":301},[291,3146,368],{"class":318},[291,3148,3149,3151],{"class":293,"line":371},[291,3150,374],{"class":318},[291,3152,377],{"class":301},[291,3154,3155,3157,3159,3161],{"class":293,"line":380},[291,3156,298],{"class":297},[291,3158,385],{"class":301},[291,3160,388],{"class":301},[291,3162,377],{"class":301},[291,3164,3165],{"class":293,"line":393},[291,3166,332],{"emptyLinePlaceholder":331},[291,3168,3169,3171,3173,3175,3177,3179,3181],{"class":293,"line":398},[291,3170,298],{"class":297},[291,3172,403],{"class":301},[291,3174,406],{"class":301},[291,3176,410],{"class":409},[291,3178,413],{"class":301},[291,3180,416],{"class":409},[291,3182,419],{"class":301},[291,3184,3185],{"class":293,"line":422},[291,3186,425],{"class":301},[291,3188,3189],{"class":293,"line":428},[291,3190,431],{"class":301},[291,3192,3193],{"class":293,"line":434},[291,3194,437],{"class":301},[291,3196,3197],{"class":293,"line":440},[291,3198,443],{"class":301},[291,3200,3201],{"class":293,"line":446},[291,3202,449],{"class":301},[291,3204,3205],{"class":293,"line":452},[291,3206,455],{"class":301},[291,3208,3209],{"class":293,"line":458},[291,3210,461],{"class":301},[291,3212,3213],{"class":293,"line":464},[291,3214,332],{"emptyLinePlaceholder":331},[291,3216,3217,3219,3221],{"class":293,"line":469},[291,3218,298],{"class":297},[291,3220,302],{"class":301},[291,3222,305],{"class":301},[291,3224,3225,3227,3229,3231,3233],{"class":293,"line":478},[291,3226,298],{"class":297},[291,3228,302],{"class":301},[291,3230,315],{"class":301},[291,3232,319],{"class":318},[291,3234,368],{"class":318},[291,3236,3237,3239],{"class":293,"line":491},[291,3238,494],{"class":301},[291,3240,368],{"class":318},[291,3242,3243,3245],{"class":293,"line":499},[291,3244,502],{"class":301},[291,3246,368],{"class":318},[291,3248,3249,3251],{"class":293,"line":507},[291,3250,510],{"class":301},[291,3252,368],{"class":318},[291,3254,3255,3257],{"class":293,"line":515},[291,3256,518],{"class":301},[291,3258,368],{"class":318},[291,3260,3261],{"class":293,"line":523},[291,3262,526],{"class":301},[16,3264,529,3265,533,3267,537],{},[52,3266,532],{},[52,3268,536],{},[193,3270,3271],{"className":285,"code":540,"language":287,"meta":199,"style":199},[52,3272,3273,3289],{"__ignoreMap":199},[291,3274,3275,3277,3279,3281,3283,3285,3287],{"class":293,"line":294},[291,3276,547],{"class":318},[291,3278,550],{"class":301},[291,3280,554],{"class":553},[291,3282,557],{"class":318},[291,3284,560],{"class":301},[291,3286,563],{"class":553},[291,3288,566],{"class":301},[291,3290,3291,3293],{"class":293,"line":308},[291,3292,571],{"class":297},[291,3294,574],{"class":318},[576,3296,579],{"id":578},[16,3298,582,3299,586],{},[52,3300,585],{},[16,3302,589],{},[193,3304,3305],{"className":285,"code":592,"language":287,"meta":199,"style":199},[52,3306,3307,3315,3319,3337,3343,3349,3353,3373,3379,3383],{"__ignoreMap":199},[291,3308,3309,3311,3313],{"class":293,"line":294},[291,3310,599],{"class":553},[291,3312,602],{"class":409},[291,3314,605],{"class":301},[291,3316,3317],{"class":293,"line":308},[291,3318,332],{"emptyLinePlaceholder":331},[291,3320,3321,3323,3325,3327,3329,3331,3333,3335],{"class":293,"line":328},[291,3322,298],{"class":297},[291,3324,359],{"class":301},[291,3326,618],{"class":318},[291,3328,560],{"class":301},[291,3330,623],{"class":553},[291,3332,626],{"class":301},[291,3334,362],{"class":318},[291,3336,368],{"class":318},[291,3338,3339,3341],{"class":293,"line":335},[291,3340,635],{"class":301},[291,3342,368],{"class":318},[291,3344,3345,3347],{"class":293,"line":354},[291,3346,374],{"class":318},[291,3348,377],{"class":301},[291,3350,3351],{"class":293,"line":371},[291,3352,332],{"emptyLinePlaceholder":331},[291,3354,3355,3357,3359,3361,3363,3365,3367,3369,3371],{"class":293,"line":380},[291,3356,298],{"class":297},[291,3358,654],{"class":301},[291,3360,657],{"class":301},[291,3362,623],{"class":553},[291,3364,626],{"class":301},[291,3366,664],{"class":301},[291,3368,623],{"class":553},[291,3370,626],{"class":301},[291,3372,368],{"class":318},[291,3374,3375,3377],{"class":293,"line":393},[291,3376,675],{"class":301},[291,3378,305],{"class":301},[291,3380,3381],{"class":293,"line":398},[291,3382,332],{"emptyLinePlaceholder":331},[291,3384,3385,3387],{"class":293,"line":422},[291,3386,686],{"class":318},[291,3388,689],{"class":301},[16,3390,692,3391,696],{},[52,3392,695],{},[16,3394,699],{},[193,3396,3397],{"className":285,"code":702,"language":287,"meta":199,"style":199},[52,3398,3399,3407,3417,3427,3437],{"__ignoreMap":199},[291,3400,3401,3403,3405],{"class":293,"line":294},[291,3402,298],{"class":297},[291,3404,711],{"class":301},[291,3406,714],{"class":301},[291,3408,3409,3411,3413,3415],{"class":293,"line":308},[291,3410,298],{"class":297},[291,3412,711],{"class":301},[291,3414,723],{"class":301},[291,3416,714],{"class":301},[291,3418,3419,3421,3423,3425],{"class":293,"line":328},[291,3420,298],{"class":297},[291,3422,732],{"class":301},[291,3424,735],{"class":301},[291,3426,738],{"class":301},[291,3428,3429,3431,3433,3435],{"class":293,"line":335},[291,3430,298],{"class":297},[291,3432,732],{"class":301},[291,3434,747],{"class":301},[291,3436,738],{"class":301},[291,3438,3439,3441,3443,3445,3447,3449],{"class":293,"line":354},[291,3440,298],{"class":297},[291,3442,732],{"class":301},[291,3444,758],{"class":301},[291,3446,711],{"class":301},[291,3448,763],{"class":318},[291,3450,766],{"class":301},[16,3452,769],{},[193,3454,3456],{"className":3455,"code":773,"language":198,"meta":199},[196],[52,3457,773],{"__ignoreMap":199},[16,3459,778],{},[16,3461,3462],{},[782,3463],{"alt":784,"src":785},[16,3465,3466],{},[789,3467,791],{},[78,3469,795],{"id":794},[16,3471,798],{},[193,3473,3474],{"className":285,"code":801,"language":287,"meta":199,"style":199},[52,3475,3476,3490],{"__ignoreMap":199},[291,3477,3478,3480,3482,3484,3486,3488],{"class":293,"line":294},[291,3479,298],{"class":297},[291,3481,315],{"class":301},[291,3483,348],{"class":318},[291,3485,342],{"class":318},[291,3487,345],{"class":318},[291,3489,818],{"class":301},[291,3491,3492,3494],{"class":293,"line":308},[291,3493,823],{"class":318},[291,3495,818],{"class":301},[16,3497,828,3498,832],{},[52,3499,831],{},[193,3501,3502],{"className":835,"code":836,"language":837,"meta":199,"style":199},[52,3503,3504,3510,3516,3524,3532,3540,3548,3554,3560,3566,3572,3578,3584,3590,3596,3602,3610,3618,3626,3634,3642,3650,3658,3666,3674,3682,3686,3692],{"__ignoreMap":199},[291,3505,3506,3508],{"class":293,"line":294},[291,3507,845],{"class":844},[291,3509,848],{"class":553},[291,3511,3512,3514],{"class":293,"line":308},[291,3513,853],{"class":844},[291,3515,848],{"class":553},[291,3517,3518,3520,3522],{"class":293,"line":328},[291,3519,860],{"class":844},[291,3521,863],{"class":553},[291,3523,866],{"class":301},[291,3525,3526,3528,3530],{"class":293,"line":335},[291,3527,871],{"class":844},[291,3529,863],{"class":553},[291,3531,876],{"class":301},[291,3533,3534,3536,3538],{"class":293,"line":354},[291,3535,881],{"class":844},[291,3537,863],{"class":553},[291,3539,886],{"class":301},[291,3541,3542,3544,3546],{"class":293,"line":371},[291,3543,891],{"class":844},[291,3545,863],{"class":553},[291,3547,896],{"class":318},[291,3549,3550,3552],{"class":293,"line":380},[291,3551,901],{"class":844},[291,3553,848],{"class":553},[291,3555,3556,3558],{"class":293,"line":393},[291,3557,908],{"class":553},[291,3559,911],{"class":301},[291,3561,3562,3564],{"class":293,"line":398},[291,3563,916],{"class":844},[291,3565,848],{"class":553},[291,3567,3568,3570],{"class":293,"line":422},[291,3569,908],{"class":553},[291,3571,925],{"class":301},[291,3573,3574,3576],{"class":293,"line":428},[291,3575,930],{"class":844},[291,3577,848],{"class":553},[291,3579,3580,3582],{"class":293,"line":434},[291,3581,908],{"class":553},[291,3583,939],{"class":301},[291,3585,3586,3588],{"class":293,"line":440},[291,3587,944],{"class":844},[291,3589,848],{"class":553},[291,3591,3592,3594],{"class":293,"line":446},[291,3593,908],{"class":553},[291,3595,953],{"class":301},[291,3597,3598,3600],{"class":293,"line":452},[291,3599,958],{"class":844},[291,3601,848],{"class":553},[291,3603,3604,3606,3608],{"class":293,"line":458},[291,3605,965],{"class":844},[291,3607,863],{"class":553},[291,3609,970],{"class":301},[291,3611,3612,3614,3616],{"class":293,"line":464},[291,3613,975],{"class":844},[291,3615,863],{"class":553},[291,3617,980],{"class":301},[291,3619,3620,3622,3624],{"class":293,"line":469},[291,3621,985],{"class":844},[291,3623,863],{"class":553},[291,3625,990],{"class":301},[291,3627,3628,3630,3632],{"class":293,"line":478},[291,3629,995],{"class":844},[291,3631,863],{"class":553},[291,3633,1000],{"class":301},[291,3635,3636,3638,3640],{"class":293,"line":491},[291,3637,1005],{"class":844},[291,3639,863],{"class":553},[291,3641,1010],{"class":301},[291,3643,3644,3646,3648],{"class":293,"line":499},[291,3645,1015],{"class":844},[291,3647,863],{"class":553},[291,3649,1010],{"class":301},[291,3651,3652,3654,3656],{"class":293,"line":507},[291,3653,1024],{"class":844},[291,3655,863],{"class":553},[291,3657,1010],{"class":301},[291,3659,3660,3662,3664],{"class":293,"line":515},[291,3661,1033],{"class":844},[291,3663,863],{"class":553},[291,3665,1010],{"class":301},[291,3667,3668,3670,3672],{"class":293,"line":523},[291,3669,1042],{"class":844},[291,3671,863],{"class":553},[291,3673,1010],{"class":301},[291,3675,3676,3678,3680],{"class":293,"line":1049},[291,3677,1052],{"class":844},[291,3679,863],{"class":553},[291,3681,1010],{"class":301},[291,3683,3684],{"class":293,"line":1059},[291,3685,332],{"emptyLinePlaceholder":331},[291,3687,3688,3690],{"class":293,"line":1064},[291,3689,1067],{"class":844},[291,3691,848],{"class":553},[291,3693,3694,3696],{"class":293,"line":1072},[291,3695,1075],{"class":844},[291,3697,848],{"class":553},[16,3699,1080],{},[39,3701,3702,3706,3714,3718,3720],{},[42,3703,3704,1088],{},[52,3705,1087],{},[42,3707,3708,1094,3710,1094,3712,1101],{},[52,3709,1093],{},[52,3711,1097],{},[52,3713,1100],{},[42,3715,1104,3716,55],{},[52,3717,1107],{},[42,3719,1110],{},[42,3721,1113,3722,1117,3724,131],{},[52,3723,1116],{},[52,3725,1120],{},[16,3727,1123,3728,131],{},[52,3729,1126],{},[78,3731,1130],{"id":1129},[193,3733,3734],{"className":285,"code":1133,"language":287,"meta":199,"style":199},[52,3735,3736,3742,3754,3764,3778],{"__ignoreMap":199},[291,3737,3738,3740],{"class":293,"line":294},[291,3739,823],{"class":318},[291,3741,818],{"class":301},[291,3743,3744,3746,3748,3750,3752],{"class":293,"line":308},[291,3745,298],{"class":297},[291,3747,711],{"class":301},[291,3749,723],{"class":301},[291,3751,1152],{"class":301},[291,3753,1155],{"class":318},[291,3755,3756,3758,3760,3762],{"class":293,"line":328},[291,3757,298],{"class":297},[291,3759,711],{"class":301},[291,3761,723],{"class":301},[291,3763,1166],{"class":301},[291,3765,3766,3768,3770,3772,3774,3776],{"class":293,"line":335},[291,3767,298],{"class":297},[291,3769,711],{"class":301},[291,3771,723],{"class":301},[291,3773,1177],{"class":301},[291,3775,348],{"class":318},[291,3777,1182],{"class":318},[291,3779,3780,3782,3784,3786],{"class":293,"line":354},[291,3781,298],{"class":297},[291,3783,711],{"class":301},[291,3785,723],{"class":301},[291,3787,1193],{"class":301},[16,3789,1196,3790,1200],{},[52,3791,1199],{},[193,3793,3794],{"className":285,"code":1203,"language":287,"meta":199,"style":199},[52,3795,3796,3804,3808,3820,3828],{"__ignoreMap":199},[291,3797,3798,3800,3802],{"class":293,"line":294},[291,3799,1210],{"class":297},[291,3801,1213],{"class":318},[291,3803,1216],{"class":301},[291,3805,3806],{"class":293,"line":308},[291,3807,332],{"emptyLinePlaceholder":331},[291,3809,3810,3812,3814,3816,3818],{"class":293,"line":328},[291,3811,298],{"class":297},[291,3813,711],{"class":301},[291,3815,1229],{"class":301},[291,3817,1232],{"class":301},[291,3819,368],{"class":318},[291,3821,3822,3824,3826],{"class":293,"line":335},[291,3823,1239],{"class":318},[291,3825,1242],{"class":301},[291,3827,368],{"class":318},[291,3829,3830,3832,3834,3836],{"class":293,"line":354},[291,3831,1249],{"class":409},[291,3833,1252],{"class":297},[291,3835,1255],{"class":318},[291,3837,1258],{"class":301},[16,3839,1261,3840,1265],{},[52,3841,1264],{},[16,3843,3844],{},[782,3845],{"alt":1270,"src":1271},[16,3847,3848],{},[789,3849,1276],{},[78,3851,1280],{"id":1279},[16,3853,1283],{},[193,3855,3857],{"className":3856,"code":1287,"language":198,"meta":199},[196],[52,3858,1287],{"__ignoreMap":199},[16,3860,1292],{},[136,3862,3863,3865,3867,3869,3871],{},[42,3864,1297],{},[42,3866,1300],{},[42,3868,1303],{},[42,3870,1306],{},[42,3872,1309],{},[16,3874,3875],{},[782,3876],{"alt":1314,"src":1315},[16,3878,3879],{},[789,3880,1320],{},[16,3882,3883],{},[782,3884],{"alt":1325,"src":1326},[16,3886,3887],{},[789,3888,1331,3889,1335],{},[52,3890,1334],{},[16,3892,1338],{},[78,3894,1342],{"id":1341},[16,3896,1345,3897,1349],{},[20,3898,1348],{},[16,3900,1352],{},[16,3902,3903],{},[782,3904],{"alt":1357,"src":1358},[16,3906,1361],{},[16,3908,3909],{},[782,3910],{"alt":1366,"src":1367},[16,3912,1370,3913,1374],{},[52,3914,1373],{},[1376,3916,3917,3929],{},[1379,3918,3919],{},[1382,3920,3921,3923,3925,3927],{},[1385,3922,1387],{},[1385,3924,1390],{},[1385,3926,1393],{},[1385,3928,1396],{},[1398,3930,3931,3941,3951],{},[1382,3932,3933,3935,3937,3939],{},[1403,3934,1405],{},[1403,3936,1405],{},[1403,3938,1410],{},[1403,3940,1413],{},[1382,3942,3943,3945,3947,3949],{},[1403,3944,1405],{},[1403,3946,1420],{},[1403,3948,1423],{},[1403,3950,1426],{},[1382,3952,3953,3955,3957,3959],{},[1403,3954,1405],{},[1403,3956,1433],{},[1403,3958,1423],{},[1403,3960,1438],{},[16,3962,1441,3963,1445],{},[52,3964,1444],{},[16,3966,3967],{},[782,3968],{"alt":1450,"src":1451},[16,3970,1454],{},[16,3972,1457],{},[16,3974,3975],{},[782,3976],{"alt":1462,"src":1463},[16,3978,1466,3979,1470],{},[20,3980,1469],{},[78,3982,1474],{"id":1473},[16,3984,1477],{},[136,3986,3987,3989,3991,3993],{},[42,3988,1482],{},[42,3990,1485],{},[42,3992,1488],{},[42,3994,1491],{},[16,3996,3997],{},[782,3998],{"alt":1496,"src":1497},[16,4000,4001],{},[789,4002,1502],{},[16,4004,1505,4005,1508],{},[52,4006,180],{},[78,4008,1512],{"id":1511},[16,4010,1515],{},[193,4012,4014],{"className":4013,"code":1519,"language":198,"meta":199},[196],[52,4015,1519],{"__ignoreMap":199},[16,4017,4018],{},[782,4019],{"alt":1526,"src":1527},[16,4021,4022],{},[789,4023,1532],{},[576,4025,1536],{"id":1535},[1376,4027,4028,4038],{},[1379,4029,4030],{},[1382,4031,4032,4034,4036],{},[1385,4033,1545],{},[1385,4035,1548],{},[1385,4037,1551],{},[1398,4039,4040,4048],{},[1382,4041,4042,4044,4046],{},[1403,4043,1558],{},[1403,4045,1561],{},[1403,4047,1564],{},[1382,4049,4050,4052,4054],{},[1403,4051,1569],{},[1403,4053,1572],{},[1403,4055,1575],{},[16,4057,1578],{},[16,4059,1581,4060,1585],{},[52,4061,1584],{},[16,4063,1588],{},[78,4065,1592],{"id":1591},[16,4067,1595],{},[193,4069,4071],{"className":4070,"code":1599,"language":198,"meta":199},[196],[52,4072,1599],{"__ignoreMap":199},[16,4074,1604,4075,1608],{},[20,4076,1607],{},[16,4078,1611],{},[39,4080,4081,4085,4089,4093,4097,4101,4105,4109],{},[42,4082,4083,1619],{},[20,4084,1618],{},[42,4086,4087,1625],{},[20,4088,1624],{},[42,4090,4091,1631],{},[20,4092,1630],{},[42,4094,4095,1637],{},[20,4096,1636],{},[42,4098,4099,1643],{},[20,4100,1642],{},[42,4102,4103,1649],{},[20,4104,1648],{},[42,4106,4107,1655],{},[20,4108,1654],{},[42,4110,4111,1661],{},[20,4112,1660],{},[16,4114,4115],{},[782,4116],{"alt":1666,"src":1667},[16,4118,4119],{},[789,4120,1672],{},[16,4122,4123],{},[782,4124],{"alt":1677,"src":1678},[16,4126,4127],{},[789,4128,1683],{},[78,4130,1687],{"id":1686},[16,4132,1690],{},[193,4134,4135],{"className":285,"code":1693,"language":287,"meta":199,"style":199},[52,4136,4137,4157],{"__ignoreMap":199},[291,4138,4139,4141,4143,4145,4147,4149,4151,4153,4155],{"class":293,"line":294},[291,4140,1210],{"class":297},[291,4142,1702],{"class":318},[291,4144,1705],{"class":318},[291,4146,1708],{"class":301},[291,4148,1711],{"class":318},[291,4150,1714],{"class":301},[291,4152,1717],{"class":318},[291,4154,1720],{"class":318},[291,4156,368],{"class":318},[291,4158,4159],{"class":293,"line":308},[291,4160,1727],{"class":301},[16,4162,1730,4163,1734,4165,1737],{},[52,4164,1733],{},[52,4166,67],{},[16,4168,4169],{},[782,4170],{"alt":1742,"src":1743},[16,4172,4173],{},[789,4174,1748],{},[16,4176,1751],{},[193,4178,4179],{"className":285,"code":1754,"language":287,"meta":199,"style":199},[52,4180,4181,4199,4207],{"__ignoreMap":199},[291,4182,4183,4185,4187,4189,4191,4193,4195,4197],{"class":293,"line":294},[291,4184,1210],{"class":297},[291,4186,1702],{"class":318},[291,4188,1765],{"class":318},[291,4190,1768],{"class":318},[291,4192,1771],{"class":318},[291,4194,1705],{"class":318},[291,4196,1708],{"class":301},[291,4198,368],{"class":318},[291,4200,4201,4203,4205],{"class":293,"line":308},[291,4202,1782],{"class":318},[291,4204,1785],{"class":301},[291,4206,368],{"class":318},[291,4208,4209],{"class":293,"line":328},[291,4210,1727],{"class":301},[16,4212,1794,4213,1798,4215,1801],{},[52,4214,1797],{},[52,4216,67],{},[16,4218,4219],{},[782,4220],{"alt":1806,"src":1807},[16,4222,1810,4223,1813],{},[52,4224,67],{},[78,4226,1817],{"id":1816},[576,4228,1821,4229],{"id":1820},[52,4230,1824],{},[16,4232,1827],{},[136,4234,4235,4237,4241,4243,4245,4247,4249],{},[42,4236,1832],{},[42,4238,1835,4239,55],{},[52,4240,1838],{},[42,4242,1841],{},[42,4244,1844],{},[42,4246,1847],{},[42,4248,1850],{},[42,4250,1853],{},[576,4252,1857],{"id":1856},[39,4254,4255,4257,4259,4261,4263,4265],{},[42,4256,1862],{},[42,4258,1865],{},[42,4260,1868],{},[42,4262,1871],{},[42,4264,1874],{},[42,4266,1877],{},[576,4268,1881],{"id":1880},[16,4270,1827],{},[136,4272,4273,4275,4277,4279,4281,4283],{},[42,4274,1888],{},[42,4276,1891],{},[42,4278,1894],{},[42,4280,1897],{},[42,4282,1900],{},[42,4284,1903,4285,1907],{},[52,4286,1906],{},[576,4288,1911],{"id":1910},[16,4290,1914],{},[78,4292,1918],{"id":1917},[16,4294,1921,4295,1924,4297,1928],{},[52,4296,180],{},[52,4298,1927],{},[193,4300,4301],{"className":285,"code":1931,"language":287,"meta":199,"style":199},[52,4302,4303,4313],{"__ignoreMap":199},[291,4304,4305,4307,4309,4311],{"class":293,"line":294},[291,4306,298],{"class":297},[291,4308,711],{"class":301},[291,4310,1942],{"class":301},[291,4312,1945],{"class":301},[291,4314,4315,4317,4319,4321,4323],{"class":293,"line":308},[291,4316,298],{"class":297},[291,4318,711],{"class":301},[291,4320,1942],{"class":301},[291,4322,1229],{"class":301},[291,4324,1958],{"class":301},[16,4326,1961],{},[193,4328,4329],{"className":285,"code":1964,"language":287,"meta":199,"style":199},[52,4330,4331,4337,4347,4361],{"__ignoreMap":199},[291,4332,4333,4335],{"class":293,"line":294},[291,4334,823],{"class":318},[291,4336,818],{"class":301},[291,4338,4339,4341,4343,4345],{"class":293,"line":308},[291,4340,298],{"class":297},[291,4342,711],{"class":301},[291,4344,723],{"class":301},[291,4346,1166],{"class":301},[291,4348,4349,4351,4353,4355,4357,4359],{"class":293,"line":328},[291,4350,298],{"class":297},[291,4352,711],{"class":301},[291,4354,723],{"class":301},[291,4356,1177],{"class":301},[291,4358,348],{"class":318},[291,4360,1182],{"class":318},[291,4362,4363,4365,4367,4369],{"class":293,"line":335},[291,4364,298],{"class":297},[291,4366,711],{"class":301},[291,4368,723],{"class":301},[291,4370,1193],{"class":301},[16,4372,2009],{},[193,4374,4375],{"className":285,"code":2012,"language":287,"meta":199,"style":199},[52,4376,4377],{"__ignoreMap":199},[291,4378,4379,4381,4383,4385],{"class":293,"line":294},[291,4380,298],{"class":297},[291,4382,711],{"class":301},[291,4384,723],{"class":301},[291,4386,2025],{"class":301},[16,4388,2028],{},[193,4390,4391],{"className":285,"code":2031,"language":287,"meta":199,"style":199},[52,4392,4393],{"__ignoreMap":199},[291,4394,4395,4397,4399,4401,4403],{"class":293,"line":294},[291,4396,298],{"class":297},[291,4398,711],{"class":301},[291,4400,723],{"class":301},[291,4402,2044],{"class":301},[291,4404,2047],{"class":318},[16,4406,4407,2053],{},[52,4408,2052],{},[78,4410,2056],{"id":2056},[39,4412,4413,4418,4423,4428,4433,4438,4443,4448,4453,4458],{},[42,4414,4415],{},[111,4416,2065],{"href":2063,"rel":4417},[115],[42,4419,4420],{},[111,4421,2071],{"href":122,"rel":4422},[115],[42,4424,4425],{},[111,4426,2078],{"href":2076,"rel":4427},[115],[42,4429,4430],{},[111,4431,2084],{"href":272,"rel":4432},[115],[42,4434,4435],{},[111,4436,2091],{"href":2089,"rel":4437},[115],[42,4439,4440],{},[111,4441,2097],{"href":256,"rel":4442},[115],[42,4444,4445],{},[111,4446,2104],{"href":2102,"rel":4447},[115],[42,4449,4450],{},[111,4451,2111],{"href":2109,"rel":4452},[115],[42,4454,4455],{},[111,4456,2118],{"href":2116,"rel":4457},[115],[42,4459,4460],{},[111,4461,2125],{"href":2123,"rel":4462},[115],[16,4464,2128,4465,2132],{},[20,4466,2131],{},[2134,4468,2136],{},{"title":199,"searchDepth":328,"depth":328,"links":4470},[4471,4472,4473,4474,4477,4478,4479,4480,4481,4482,4485,4486,4487,4493,4494],{"id":80,"depth":308,"text":81},{"id":191,"depth":308,"text":191},{"id":227,"depth":308,"text":228},{"id":265,"depth":308,"text":266,"children":4475},[4476],{"id":578,"depth":328,"text":579},{"id":794,"depth":308,"text":795},{"id":1129,"depth":308,"text":1130},{"id":1279,"depth":308,"text":1280},{"id":1341,"depth":308,"text":1342},{"id":1473,"depth":308,"text":1474},{"id":1511,"depth":308,"text":1512,"children":4483},[4484],{"id":1535,"depth":328,"text":1536},{"id":1591,"depth":308,"text":1592},{"id":1686,"depth":308,"text":1687},{"id":1816,"depth":308,"text":1817,"children":4488},[4489,4490,4491,4492],{"id":1820,"depth":328,"text":2158},{"id":1856,"depth":328,"text":1857},{"id":1880,"depth":328,"text":1881},{"id":1910,"depth":328,"text":1911},{"id":1917,"depth":308,"text":1918},{"id":2056,"depth":308,"text":2056},{"src":1678,"alt":2169,"caption":2170,"width":2171,"height":2172},{},{"title":10,"description":2164},[2180,2181,89,2182,2183,2184,2185,26,2186],{"src":2189,"poster":1678,"title":2190,"description":2191,"duration":2192,"chapters":422,"width":2193,"height":2194},{"id":4501,"title":4502,"accent":4503,"body":4504,"description":6113,"draft":2165,"experience":2166,"extension":2167,"featured":2165,"image":2166,"meta":6114,"navigation":331,"path":6115,"publishedAt":6116,"readingTime":1064,"seo":6117,"stem":6118,"tags":6119,"updatedAt":2166,"video":2166,"__hash__":6122,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Flucky-stun-cloudflare-tunnel-guide.md","Lucky STUN + Cloudflare 302 实战：用域名跟随动态公网端口","violet",{"type":13,"value":4505,"toc":6083},[4506,4516,4526,4536,4548,4551,4557,4564,4625,4629,4633,4640,4643,4657,4661,4664,4670,4679,4685,4690,4693,4697,4700,4706,4711,4810,4813,4819,4826,4830,4833,4836,4853,4857,4860,4877,4880,4884,4894,4898,4975,4993,4996,5000,5003,5044,5050,5053,5059,5065,5069,5071,5077,5080,5086,5099,5103,5106,5112,5115,5133,5137,5144,5150,5153,5306,5313,5319,5322,5345,5353,5356,5362,5365,5369,5372,5394,5397,5414,5417,5436,5446,5450,5473,5476,5480,5492,5496,5499,5505,5508,5514,5519,5525,5544,5548,5550,5556,5559,5565,5570,5638,5641,5652,5655,5659,5662,5668,5683,5694,5697,5732,5736,5739,5745,5752,5755,5759,5762,5768,5776,5779,5782,5917,5920,5923,5926,5952,5963,5966,5972,5975,5978,5981,5984,5986,6080],[16,4507,4508,4509],{},"这篇文章介绍的重点不是普通 DDNS，也不是让 Cloudflare 反向代理 STUN 的动态端口，而是下面这条组合链路：",[20,4510,4511,4512,4515],{},"Lucky 通过 STUN 获得 ",[52,4513,4514],{},"公网 IP:动态端口","，映射改变时再由 Webhook 修改 Cloudflare Single Redirect 的目标地址。",[16,4517,4518,4519,4522,4523,4525],{},"用户只需要记住 ",[52,4520,4521],{},"go.example.com","。浏览器先访问 Cloudflare，Cloudflare 返回一次临时重定向，再由浏览器直接连接最新的 STUN 地址。这与参考文章的 301 方案是同一种思路，但本文把状态码改为 ",[20,4524,26],{},"：NAT 映射本来就是临时值，不应该让浏览器或搜索引擎把某个 IP 和端口当作永久地址。",[16,4527,4528,4529,1094,4532,4535],{},"本文基于 Lucky 2.27.2 与 Cloudflare Dashboard 的实际界面，并用双方官方文档校正能力边界。正文示例只使用 ",[52,4530,4531],{},"example.com",[52,4533,4534],{},"192.0.2.0\u002F24"," 等文档值；实拍配图中的真实域名、IP、端口、实例名称、流量和 Token 均未展示，存在后台数据的区域已做不可逆强模糊处理。",[72,4537,4538],{},[16,4539,4540,4541,4543,4544,4547],{},"先给结论：浏览器访问的临时入口使用 ",[20,4542,26],{},"；不要使用 301。Cloudflare Single Redirect 当前正式支持 301、302、307、308，",[20,4545,4546],{},"不支持 303","。如果是 App、API、远程桌面或非 HTTP 协议，这种跳转通常不合适，应改用 307、Cloudflare Tunnel、VPN 或其他原生传输方案。",[78,4549,4550],{"id":4550},"先看清这条链路",[193,4552,4555],{"className":4553,"code":4554,"language":198,"meta":199},[196],"第一次请求\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",[52,4556,4554],{"__ignoreMap":199},[16,4558,4559,4560,4563],{},"这里的“纯域名访问”只表示",[20,4561,4562],{},"用户输入的起始地址不带端口","。跳转完成后，浏览器地址栏仍会显示真实 IP 或目标域名及端口。Cloudflare 只处理第一次请求，后续业务流量不会经过 Cloudflare，因此 WAF、缓存、Access、隐藏源站等能力不会覆盖第二段连接。",[1376,4565,4566,4582],{},[1379,4567,4568],{},[1382,4569,4570,4573,4576,4579],{},[1385,4571,4572],{},"方案",[1385,4574,4575],{},"用户入口",[1385,4577,4578],{},"第二段流量",[1385,4580,4581],{},"适合场景",[1398,4583,4584,4598,4611],{},[1382,4585,4586,4589,4592,4595],{},[1403,4587,4588],{},"STUN 直连",[1403,4590,4591],{},"IP\u002F域名加动态端口",[1403,4593,4594],{},"访问者直连家庭网络",[1403,4596,4597],{},"TCP\u002FUDP 或明确知道端口的客户端",[1382,4599,4600,4603,4606,4608],{},[1403,4601,4602],{},"STUN + Cloudflare 302",[1403,4604,4605],{},"固定 HTTPS 域名，随后显示动态地址",[1403,4607,4594],{},[1403,4609,4610],{},"浏览器 GET 页面、临时入口、能接受暴露地址",[1382,4612,4613,4616,4619,4622],{},[1403,4614,4615],{},"Cloudflare Tunnel",[1403,4617,4618],{},"固定 HTTPS 域名",[1403,4620,4621],{},"经 Cloudflare Tunnel",[1403,4623,4624],{},"Web、API、管理后台、需要 Access\u002FWAF 的服务",[78,4626,4628],{"id":4627},"第一条路径配置-lucky-stun","第一条路径：配置 Lucky STUN",[576,4630,4632],{"id":4631},"_1-先判断网络条件","1. 先判断网络条件",[16,4634,4635,4639],{},[111,4636,4638],{"href":256,"rel":4637},[115],"Lucky 的 STUN 官方文档","把适用环境描述为 NAT1，并明确提醒公网端口无法保证固定、变化没有规律。不同工具对 NAT 类型的命名可能不完全一致，所以不要只凭路由器页面上的“NAT 类型”文字判断，应以实际穿透日志和外网测试为准。",[16,4641,4642],{},"开始前依次检查：",[136,4644,4645,4648,4651,4654],{},[42,4646,4647],{},"如果是光猫拨号，按 Lucky 文档把光猫 DMZ 指向主路由；如果是路由器拨号，则跳过这一步。",[42,4649,4650],{},"最省事的部署位置是主路由。Lucky 位于普通局域网主机时，还要处理主路由到 Lucky 的端口映射、UPnP 或 DMZ。",[42,4652,4653],{},"确认系统内核和防火墙允许所需的 TCP\u002FUDP 行为；代理软件、双重 NAT 和多 WAN 都可能改变结果。",[42,4655,4656],{},"不要把 Lucky 管理后台本身作为第一条公网测试规则。先选择一个无敏感数据、可以随时关闭的测试服务。",[576,4658,4660],{"id":4659},"_2-开启-stun-模块","2. 开启 STUN 模块",[16,4662,4663],{},"进入：",[193,4665,4668],{"className":4666,"code":4667,"language":198,"meta":199},[196],"Lucky → 内网穿透 → STUN 内网穿透 → 设置\n",[52,4669,4667],{"__ignoreMap":199},[16,4671,4672,4673,4678],{},"打开 STUN 模块，并检查“全局 STUN 服务器列表”。Lucky 界面会给出一个公开的",[111,4674,4677],{"href":4675,"rel":4676},"https:\u002F\u002Fgithub.com\u002Fpradt2\u002Falways-online-stun\u002Fblob\u002Fmaster\u002Fvalid_hosts_tcp.txt",[115],"可用 STUN 服务器列表","作为参考。服务器越多并不一定越稳定；应保留网络可达、经过测试的条目，不要把来源不明的地址批量粘贴进生产配置。",[16,4680,4681],{},[782,4682],{"alt":4683,"src":4684},"Lucky 2.27.2 的 STUN 设置页，展示模块开关、公开服务器列表与 Webhook 开关","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-stun-settings-v1.png",[16,4686,4687],{},[789,4688,4689],{},"实拍界面；画面只包含公开 STUN 服务器地址，没有私有网络地址或访问凭据。",[16,4691,4692],{},"Webhook 会把公网映射变化发送给外部系统。只有确实需要自动化时才启用，并确保 URL、请求头和消息体中的密钥不会出现在日志或截图里。",[576,4694,4696],{"id":4695},"_3-用简易模式添加第一条规则","3. 用简易模式添加第一条规则",[16,4698,4699],{},"进入“穿透规则列表”并选择“添加穿透规则”。第一次测试建议使用简易模式。",[16,4701,4702],{},[782,4703],{"alt":4704,"src":4705},"Lucky 的添加穿透规则表单，表单外的真实后台数据已被强模糊","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-stun-rule-dialog-redacted-v1.png",[16,4707,4708],{},[789,4709,4710],{},"表单保持空白；背景中的真实规则、IP、端口和流量统计已做不可逆强模糊。",[1376,4712,4713,4723],{},[1379,4714,4715],{},[1382,4716,4717,4720],{},[1385,4718,4719],{},"字段",[1385,4721,4722],{},"建议",[1398,4724,4725,4733,4741,4757,4765,4773,4781,4789,4803],{},[1382,4726,4727,4730],{},[1403,4728,4729],{},"规则名称",[1403,4731,4732],{},"使用不暴露设备型号、位置或业务名称的内部备注",[1382,4734,4735,4738],{},[1403,4736,4737],{},"穿透类型",[1403,4739,4740],{},"根据服务选择 IPv4-TCP 或 IPv4-UDP；一条规则只处理一种协议",[1382,4742,4743,4746],{},[1403,4744,4745],{},"穿透通道本地端口",[1403,4747,4748,4749,4752,4753,4756],{},"手动填写时使用未占用且全局唯一的端口；Lucky 文档建议大于 ",[52,4750,4751],{},"10240","。配合 UPnP 时可按文档使用 ",[52,4754,4755],{},"0"," 让 Lucky 选择端口",[1382,4758,4759,4762],{},[1403,4760,4761],{},"防火墙自动放行",[1403,4763,4764],{},"仅在 Lucky 运行于支持其防火墙控制的路由器、且你理解规则变化时开启",[1382,4766,4767,4770],{},[1403,4768,4769],{},"UPnP \u002F NAT-PMP",[1403,4771,4772],{},"Lucky 位于局域网主机且主路由已启用相应能力时使用；不用时关闭，避免扩大自动映射范围",[1382,4774,4775,4778],{},[1403,4776,4777],{},"不使用 Lucky 内置端口转发",[1403,4779,4780],{},"第一次测试保持关闭。确认穿透有效后，再评估用路由器转发换取更高性能",[1382,4782,4783,4786],{},[1403,4784,4785],{},"禁用通道有效性检测",[1403,4787,4788],{},"保持关闭，它是调试选项",[1382,4790,4791,4794],{},[1403,4792,4793],{},"目标地址",[1403,4795,4796,4797,2585,4800],{},"填内网 IP 或主机名，不要带 ",[52,4798,4799],{},"http:\u002F\u002F",[52,4801,4802],{},"https:\u002F\u002F",[1382,4804,4805,4807],{},[1403,4806,1630],{},[1403,4808,4809],{},"填服务实际监听端口；使用 Lucky 内置转发时必填",[16,4811,4812],{},"一个完全虚构的 TCP 示例：",[193,4814,4817],{"className":4815,"code":4816,"language":198,"meta":199},[196],"穿透类型：IPv4-TCP\n穿透通道本地端口：12080\n目标地址：192.0.2.10\n目标端口：8080\n",[52,4818,4816],{"__ignoreMap":199},[16,4820,4821,4822,4825],{},"这里的 ",[52,4823,4824],{},"192.0.2.10:8080"," 使用 RFC 5737 文档保留地址，只是教程占位符。发布文章、截图或求助日志前，应把真实内网地址与端口一起打码；二者组合往往足以识别某项服务。",[576,4827,4829],{"id":4828},"_4-定制模式先不要调参优化","4. 定制模式先不要“调参优化”",[16,4831,4832],{},"Lucky 2.27.2 的定制模式包含指定 IP\u002F网卡、IP 黑白名单、TCP 连接数、UDP 会话数、加密、STUN 等待时间、心跳间隔、重试间隔和日志级别等选项。这些参数互相关联，不应因为看到默认值就照搬到其他网络。",[16,4834,4835],{},"只有出现可复现问题时才逐项调整：",[39,4837,4838,4841,4844,4847,4850],{},[42,4839,4840],{},"多 WAN 或策略路由环境，才指定穿透通道 IP 或网卡；",[42,4842,4843],{},"连接数与 UDP 会话数根据服务并发和设备资源设上限；",[42,4845,4846],{},"等待、心跳、重试参数一次只改一项，并保留前后日志；",[42,4848,4849],{},"“穿透失败自动重试”可以提高可用性，但也要避免错误配置持续制造请求；",[42,4851,4852],{},"TLS 或流加密不能替代应用自身的认证和授权。",[576,4854,4856],{"id":4855},"_5-用真正的外网验证","5. 用真正的外网验证",[16,4858,4859],{},"保存后先看该规则的独立日志，再关闭 Wi-Fi，用移动网络或另一条宽带访问。至少验证：",[136,4861,4862,4865,4868,4871,4874],{},[42,4863,4864],{},"Lucky 是否获得公网映射；",[42,4866,4867],{},"访问时日志是否出现连接；",[42,4869,4870],{},"防火墙或路由器是否把流量送到正确的 Lucky 端口；",[42,4872,4873],{},"内网服务是否监听了正确地址，而不是只监听另一个网络命名空间；",[42,4875,4876],{},"公网端口变化后，客户端或自动化能否感知新地址。",[16,4878,4879],{},"如果日志完全没有访问记录，先查运营商 NAT、光猫\u002F路由器映射和防火墙，而不是反复修改目标服务。",[78,4881,4883],{"id":4882},"核心方案用-cloudflare-302-跟随-stun-动态端口","核心方案：用 Cloudflare 302 跟随 STUN 动态端口",[16,4885,4886,4887,4890,4891,4893],{},"STUN 给出的是“公网 IP + 公网端口”。A\u002FAAAA 记录只能保存 IP，不能把普通浏览器 URL 的端口一起动态更新。参考方案巧妙之处在于：DNS 只负责让固定入口到达 Cloudflare，真正的 ",[52,4888,4889],{},"IP:端口"," 写在重定向规则的 ",[52,4892,67],{}," 里，并由 Lucky Webhook 持续更新。",[576,4895,4897],{"id":4896},"_1-为什么选-302而不是-301-或-303","1. 为什么选 302，而不是 301 或 303",[1376,4899,4900,4913],{},[1379,4901,4902],{},[1382,4903,4904,4907,4910],{},[1385,4905,4906],{},"状态码",[1385,4908,4909],{},"语义与方法行为",[1385,4911,4912],{},"是否适合本文",[1398,4914,4915,4929,4942,4953,4964],{},[1382,4916,4917,4920,4923],{},[1403,4918,4919],{},"301",[1403,4921,4922],{},"永久移动；客户端和搜索引擎可能长期记住目标",[1403,4924,4925,4928],{},[20,4926,4927],{},"不适合","动态 NAT 映射",[1382,4930,4931,4933,4936],{},[1403,4932,26],{},[1403,4934,4935],{},"临时移动；常见浏览器适合用它处理 GET 页面",[1403,4937,4938,4941],{},[20,4939,4940],{},"推荐","作为浏览器入口",[1382,4943,4944,4947,4950],{},[1403,4945,4946],{},"303",[1403,4948,4949],{},"明确要求后续使用 GET，常用于 POST 后查看另一资源",[1403,4951,4952],{},"Single Redirect 当前不可选",[1382,4954,4955,4958,4961],{},[1403,4956,4957],{},"307",[1403,4959,4960],{},"临时移动，并要求保留原请求方法和请求体",[1403,4962,4963],{},"App\u002FAPI 确需保留方法时评估",[1382,4965,4966,4969,4972],{},[1403,4967,4968],{},"308",[1403,4970,4971],{},"永久移动并保留方法",[1403,4973,4974],{},"不适合动态 NAT 映射",[16,4976,4977,4978,4982,4983,4985,4986,4989,4990,4992],{},"Cloudflare 的 ",[111,4979,4981],{"href":2123,"rel":4980},[115],"Single Redirect 设置文档","当前列出的状态码只有 301、302、307、308。Cloudflare 的 3xx 说明页介绍了 303 的 HTTP 语义，但这不代表 Rulesets API 的 ",[52,4984,1906],{}," 动作接受 303。把下面 JSON 的 ",[52,4987,4988],{},"status_code"," 改成 ",[52,4991,4946],{}," 很可能会被 API 校验拒绝。",[16,4994,4995],{},"如果业务必须返回 303，需要由 Cloudflare Worker 或自己的源站生成该响应；那已经不是本文这套 Single Redirect 配置。对于普通浏览器 GET 页面，302 更直接。对于必须保留 POST\u002FPUT 请求体的 API，优先重新评估架构，而不是盲目把 302 换成 307。",[576,4997,4999],{"id":4998},"_2-准备两个不同角色的域名","2. 准备两个不同角色的域名",[16,5001,5002],{},"建议把入口和直连目标分开：",[1376,5004,5005,5017],{},[1379,5006,5007],{},[1382,5008,5009,5012,5015],{},[1385,5010,5011],{},"域名",[1385,5013,5014],{},"Cloudflare 状态",[1385,5016,1396],{},[1398,5018,5019,5031],{},[1382,5020,5021,5025,5028],{},[1403,5022,5023],{},[52,5024,4521],{},[1403,5026,5027],{},"已代理（橙云）",[1403,5029,5030],{},"用户输入的固定入口，触发 Redirect Rule",[1382,5032,5033,5038,5041],{},[1403,5034,5035],{},[52,5036,5037],{},"origin-direct.example.com",[1403,5039,5040],{},"仅 DNS（灰云，可选）",[1403,5042,5043],{},"希望目标仍使用域名和有效 TLS 证书时，由 Lucky DDNS 更新 IP",[16,5045,5046,5047,5049],{},"Cloudflare 官方明确要求 Single Redirect 所匹配的入口主机名必须经过代理。因此 ",[52,5048,4521],{}," 必须是橙云记录。规则命中后 Cloudflare 直接返回 302，不会请求这个记录指向的源站；但未命中的路径可能继续回源，所以生产环境应让规则覆盖完整主机名，并为漏网请求设计安全失败，而不是指向真实管理后台。",[16,5051,5052],{},"目标可以有两种写法：",[193,5054,5057],{"className":5055,"code":5056,"language":198,"meta":199},[196],"最简单： http:\u002F\u002F公网IP:动态端口\u002Fpath\n更适合 HTTPS： https:\u002F\u002Forigin-direct.example.com:动态端口\u002Fpath\n",[52,5058,5056],{"__ignoreMap":199},[16,5060,5061,5062,5064],{},"直接跳到 HTTPS IP 往往会遇到证书主机名不匹配。若需要 HTTPS，推荐让 Lucky DDNS 更新灰云目标域名，并让源站证书包含 ",[52,5063,5037],{},"。无论采用哪一种，第二段连接都会暴露目标 IP 和端口，也不会得到 Cloudflare WAF 或 Access 保护。",[576,5066,5068],{"id":5067},"_3-在-cloudflare-创建-302-动态重定向","3. 在 Cloudflare 创建 302 动态重定向",[16,5070,4663],{},[193,5072,5075],{"className":5073,"code":5074,"language":198,"meta":199},[196],"Cloudflare Dashboard → 目标 Zone → Rules → Redirect Rules\n→ Create rule → Single Redirects\n",[52,5076,5074],{"__ignoreMap":199},[16,5078,5079],{},"先建立一条占位规则：",[193,5081,5084],{"className":5082,"code":5083,"language":198,"meta":199},[196],"规则名：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",[52,5085,5083],{"__ignoreMap":199},[16,5087,5088,5091,5092,5095,5096,5098],{},[52,5089,5090],{},"192.0.2.10"," 是文档保留地址。占位规则只是为了让 Cloudflare 创建规则集和规则 ID，不应拿它做真实连通性测试。部署后先用 ",[52,5093,5094],{},"curl -I"," 确认返回状态是 302，并检查 ",[52,5097,67],{}," 是否保留路径与查询参数。",[576,5100,5102],{"id":5101},"_4-创建最小权限-api-token","4. 创建最小权限 API Token",[16,5104,5105],{},"为 Lucky 单独创建 Token：",[193,5107,5110],{"className":5108,"code":5109,"language":198,"meta":199},[196],"Permissions：Zone → Single Redirect → Edit\nResources：Include → Specific zone → example.com\n",[52,5111,5109],{"__ignoreMap":199},[16,5113,5114],{},"不要使用 Global API Key，也不要把 DNS、Workers、账户管理等无关权限一起勾选。这个 Token 能修改入口跳往何处，一旦泄漏可以被用于钓鱼重定向，应按生产密钥管理。",[16,5116,5117,5118,5121,5122,5125,5126,5129,5130,5132],{},"Webhook 还需要三个非密钥标识：",[52,5119,5120],{},"ZONE_ID","、动态重定向规则集的 ",[52,5123,5124],{},"RULESET_ID","、目标规则的 ",[52,5127,5128],{},"RULE_ID","。可以从 Dashboard 编辑规则时的请求中查看，也可以调用 Rulesets API 列出 Zone 规则集，查找 phase 为 ",[52,5131,1906],{}," 的条目，再读取其中规则。截图或文章仍建议统一打码这些标识。",[576,5134,5136],{"id":5135},"_5-在-lucky-规则里配置-webhook","5. 在 Lucky 规则里配置 Webhook",[16,5138,5139,5140,5143],{},"编辑对应 STUN 穿透规则，开启 Webhook。不要使用 Dashboard 的内部 ",[52,5141,5142],{},"\u002Fapi\u002Fv4\u002F"," 地址，应调用公开 API：",[193,5145,5148],{"className":5146,"code":5147,"language":198,"meta":199},[196],"请求地址：\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",[52,5149,5147],{"__ignoreMap":199},[16,5151,5152],{},"请求体使用 302：",[193,5154,5158],{"className":5155,"code":5156,"language":5157,"meta":199,"style":199},"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",[52,5159,5160,5165,5178,5190,5202,5224,5232,5239,5250,5257,5277,5282,5291,5296,5301],{"__ignoreMap":199},[291,5161,5162],{"class":293,"line":294},[291,5163,5164],{"class":553},"{\n",[291,5166,5167,5170,5172,5175],{"class":293,"line":308},[291,5168,5169],{"class":318},"  \"action\"",[291,5171,863],{"class":553},[291,5173,5174],{"class":301},"\"redirect\"",[291,5176,5177],{"class":553},",\n",[291,5179,5180,5183,5185,5188],{"class":293,"line":328},[291,5181,5182],{"class":318},"  \"description\"",[291,5184,863],{"class":553},[291,5186,5187],{"class":301},"\"Lucky STUN temporary redirect\"",[291,5189,5177],{"class":553},[291,5191,5192,5195,5197,5200],{"class":293,"line":335},[291,5193,5194],{"class":318},"  \"enabled\"",[291,5196,863],{"class":553},[291,5198,5199],{"class":318},"true",[291,5201,5177],{"class":553},[291,5203,5204,5207,5209,5212,5215,5217,5219,5222],{"class":293,"line":354},[291,5205,5206],{"class":318},"  \"expression\"",[291,5208,863],{"class":553},[291,5210,5211],{"class":301},"\"(http.host eq ",[291,5213,5214],{"class":318},"\\\"",[291,5216,4521],{"class":301},[291,5218,5214],{"class":318},[291,5220,5221],{"class":301},")\"",[291,5223,5177],{"class":553},[291,5225,5226,5229],{"class":293,"line":371},[291,5227,5228],{"class":318},"  \"action_parameters\"",[291,5230,5231],{"class":553},": {\n",[291,5233,5234,5237],{"class":293,"line":380},[291,5235,5236],{"class":318},"    \"from_value\"",[291,5238,5231],{"class":553},[291,5240,5241,5244,5246,5248],{"class":293,"line":393},[291,5242,5243],{"class":318},"      \"status_code\"",[291,5245,863],{"class":553},[291,5247,26],{"class":318},[291,5249,5177],{"class":553},[291,5251,5252,5255],{"class":293,"line":398},[291,5253,5254],{"class":318},"      \"target_url\"",[291,5256,5231],{"class":553},[291,5258,5259,5262,5264,5267,5269,5272,5274],{"class":293,"line":422},[291,5260,5261],{"class":318},"        \"expression\"",[291,5263,863],{"class":553},[291,5265,5266],{"class":301},"\"concat(",[291,5268,5214],{"class":318},[291,5270,5271],{"class":301},"http:\u002F\u002F#{ipAddr}",[291,5273,5214],{"class":318},[291,5275,5276],{"class":301},", http.request.uri.path)\"\n",[291,5278,5279],{"class":293,"line":428},[291,5280,5281],{"class":553},"      },\n",[291,5283,5284,5287,5289],{"class":293,"line":434},[291,5285,5286],{"class":318},"      \"preserve_query_string\"",[291,5288,863],{"class":553},[291,5290,896],{"class":318},[291,5292,5293],{"class":293,"line":440},[291,5294,5295],{"class":553},"    }\n",[291,5297,5298],{"class":293,"line":446},[291,5299,5300],{"class":553},"  }\n",[291,5302,5303],{"class":293,"line":452},[291,5304,5305],{"class":553},"}\n",[16,5307,5308,5309,5312],{},"在 Lucky STUN Webhook 中，",[52,5310,5311],{},"#{ipAddr}"," 会在触发时替换为当前的公网地址与端口，因此生成的 Cloudflare 表达式类似：",[193,5314,5317],{"className":5315,"code":5316,"language":198,"meta":199},[196],"concat(\"http:\u002F\u002F203.0.113.10:45678\", http.request.uri.path)\n",[52,5318,5316],{"__ignoreMap":199},[16,5320,5321],{},"如果采用“灰云目标域名 + HTTPS”模式，请同时用 Lucky DDNS 更新目标域名，并把目标表达式改为：",[193,5323,5325],{"className":5155,"code":5324,"language":5157,"meta":199,"style":199},"\"expression\": \"concat(\\\"https:\u002F\u002Forigin-direct.example.com:#{port}\\\", http.request.uri.path)\"\n",[52,5326,5327],{"__ignoreMap":199},[291,5328,5329,5332,5334,5336,5338,5341,5343],{"class":293,"line":294},[291,5330,5331],{"class":301},"\"expression\"",[291,5333,863],{"class":553},[291,5335,5266],{"class":301},[291,5337,5214],{"class":318},[291,5339,5340],{"class":301},"https:\u002F\u002Forigin-direct.example.com:#{port}",[291,5342,5214],{"class":318},[291,5344,5276],{"class":301},[16,5346,5347,101,5349,5352],{},[52,5348,5311],{},[52,5350,5351],{},"#{port}"," 是 Lucky Webhook 模板变量，不是 Cloudflare Rules 语言变量。引号必须是英文半角字符，JSON 中表达式内部的双引号必须保留反斜杠转义。",[16,5354,5355],{},"把 Webhook 的成功判断设置为响应中包含：",[193,5357,5360],{"className":5358,"code":5359,"language":198,"meta":199},[196],"\"success\": true\n",[52,5361,5359],{"__ignoreMap":199},[16,5363,5364],{},"不同界面或响应序列化可能包含空格，不应仅凭一次手动测试就认定长期有效。最终要同时查看 Lucky Webhook 日志与 Cloudflare API 响应，并重新读取规则确认目标已更新。",[576,5366,5368],{"id":5367},"_6-从外网验证完整链路","6. 从外网验证完整链路",[16,5370,5371],{},"不要只看浏览器“能打开”。用移动网络或另一条宽带分两段验证：",[193,5373,5375],{"className":285,"code":5374,"language":287,"meta":199,"style":199},"curl -sS -D - -o \u002Fdev\u002Fnull \"https:\u002F\u002Fgo.example.com\u002Fphotos?id=42\"\n",[52,5376,5377],{"__ignoreMap":199},[291,5378,5379,5381,5383,5385,5387,5389,5391],{"class":293,"line":294},[291,5380,1210],{"class":297},[291,5382,1702],{"class":318},[291,5384,1711],{"class":318},[291,5386,1714],{"class":301},[291,5388,1705],{"class":318},[291,5390,1708],{"class":301},[291,5392,5393],{"class":301}," \"https:\u002F\u002Fgo.example.com\u002Fphotos?id=42\"\n",[16,5395,5396],{},"预期关键响应：",[193,5398,5402],{"className":5399,"code":5400,"language":5401,"meta":199,"style":199},"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",[52,5403,5404,5409],{"__ignoreMap":199},[291,5405,5406],{"class":293,"line":294},[291,5407,5408],{},"HTTP\u002F2 302\n",[291,5410,5411],{"class":293,"line":308},[291,5412,5413],{},"location: http:\u002F\u002F203.0.113.10:45678\u002Fphotos?id=42\n",[16,5415,5416],{},"接着检查：",[136,5418,5419,5422,5427,5430,5433],{},[42,5420,5421],{},"状态码确实是 302，不是仍被旧规则返回 301；",[42,5423,5424,5426],{},[52,5425,67],{}," 中的 IP、端口、路径和查询字符串正确；",[42,5428,5429],{},"让 STUN 重新获得映射后，Webhook 是否把规则改成新端口；",[42,5431,5432],{},"浏览器无痕窗口和另一台设备没有命中过去的永久缓存；",[42,5434,5435],{},"第二段直连是否具有应用自身的登录、限速、防爆破和 TLS 保护。",[16,5437,5438,5439,5441,5442,5445],{},"测试时使用 ",[52,5440,5094],{}," 只能验证 HEAD；部分服务对 HEAD 和 GET 的处理不同。需要完整验证页面时，使用上面的 ",[52,5443,5444],{},"-D - -o \u002Fdev\u002Fnull"," 发出 GET，但不要在终端输出私密响应正文。",[576,5447,5449],{"id":5448},"_7-这套-302-方案明确不解决什么","7. 这套 302 方案明确不解决什么",[39,5451,5452,5458,5461,5464,5467,5470],{},[42,5453,5454,5455,5457],{},"它不会真正隐藏端口；跳转后地址栏和 ",[52,5456,67],{}," 都会显示端口。",[42,5459,5460],{},"它不会隐藏家庭公网 IP；访问者会直接连接目标。",[42,5462,5463],{},"它不会让 App、电视客户端、远程桌面或自定义协议自动兼容 HTTP 重定向。",[42,5465,5466],{},"它不会把第二段请求放到 Cloudflare WAF、Cache 或 Access 后面。",[42,5468,5469],{},"它不会自动解决 Cookie Domain、CORS、CSRF、绝对 URL、Host 校验或 HTTPS 证书问题。",[42,5471,5472],{},"它只适合 HTTP\u002FHTTPS 入口；UDP 不可能通过 HTTP 302 重定向建立会话。",[16,5474,5475],{},"因此，Cloudflare Redirect 不是“给 STUN 端口套橙云”。橙云只覆盖固定入口的第一次 HTTP 请求。",[78,5477,5479],{"id":5478},"替代方案在-lucky-中运行-cloudflare-tunnel","替代方案：在 Lucky 中运行 Cloudflare Tunnel",[16,5481,5482,5483,5486,5487,131],{},"如果目标是 Web、API 或后台管理页面，Cloudflare Tunnel 通常更简单。",[52,5484,5485],{},"cloudflared"," 从内网主动连接 Cloudflare，不需要公网 IP，也不需要在路由器开放入站端口。完整模型可参考 ",[111,5488,5491],{"href":5489,"rel":5490},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002F",[115],"Cloudflare Tunnel 官方说明",[576,5493,5495],{"id":5494},"_1-在-cloudflare-创建远程管理隧道","1. 在 Cloudflare 创建远程管理隧道",[16,5497,5498],{},"进入 Cloudflare Dashboard：",[193,5500,5503],{"className":5501,"code":5502,"language":198,"meta":199},[196],"Networking → Tunnels → Create a tunnel → Cloudflared\n",[52,5504,5502],{"__ignoreMap":199},[16,5506,5507],{},"给隧道一个不包含家庭地址、设备序列号或个人姓名的名称。创建后，控制台会生成一条安装命令，其中包含 Tunnel Token。Lucky 只需要 Token 本身，不要把整条命令保存到笔记或截图中。",[16,5509,5510],{},[782,5511],{"alt":5512,"src":5513},"Cloudflare Dashboard 的创建隧道空白表单","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Fcloudflare-create-tunnel-v1.png",[16,5515,5516],{},[789,5517,5518],{},"实拍界面；未填写名称、未生成 Token，也没有提交创建操作。账号侧栏处于折叠状态。",[16,5520,5521,5522,5524],{},"Tunnel Token 能让新的 ",[52,5523,5485],{}," 连接到这条隧道，应按密码管理：",[39,5526,5527,5530,5533,5536],{},[42,5528,5529],{},"不写进 Markdown、Shell 历史、Git、Dockerfile 或公开环境变量；",[42,5531,5532],{},"不放在浏览器端代码；",[42,5534,5535],{},"每条隧道使用独立 Token；",[42,5537,5538,5539,131],{},"泄漏后立即在隧道详情中刷新 Token，并清理旧连接。Cloudflare 也建议",[111,5540,5543],{"href":5541,"rel":5542},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002Fconfigure-tunnels\u002Fremote-tunnel-permissions\u002F",[115],"定期轮换 Tunnel Token",[576,5545,5547],{"id":5546},"_2-在-lucky-添加-tunnel-实例","2. 在 Lucky 添加 Tunnel 实例",[16,5549,4663],{},[193,5551,5554],{"className":5552,"code":5553,"language":198,"meta":199},[196],"Lucky → 内网穿透 → Cloudflared → 添加实例\n",[52,5555,5553],{"__ignoreMap":199},[16,5557,5558],{},"选择“隧道”模式，然后配置：",[16,5560,5561],{},[782,5562],{"alt":5563,"src":5564},"Lucky 的 Cloudflared 添加实例表单，表单外的真实实例信息已被强模糊","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-cloudflared-instance-redacted-v1.png",[16,5566,5567],{},[789,5568,5569],{},"Token 与 API Token 输入框保持空白；背景中的真实实例信息已做不可逆强模糊。",[1376,5571,5572,5580],{},[1379,5573,5574],{},[1382,5575,5576,5578],{},[1385,5577,4719],{},[1385,5579,4722],{},[1398,5581,5582,5590,5598,5606,5614,5622,5630],{},[1382,5583,5584,5587],{},[1403,5585,5586],{},"实例备注",[1403,5588,5589],{},"使用唯一但不泄漏设备或位置的信息",[1382,5591,5592,5595],{},[1403,5593,5594],{},"令牌",[1403,5596,5597],{},"粘贴 Tunnel Token；发布前必须完全打码",[1382,5599,5600,5603],{},[1403,5601,5602],{},"边缘网络类型",[1403,5604,5605],{},"一般使用自动；只有 IPv4\u002FIPv6 链路确有问题时才固定",[1382,5607,5608,5611],{},[1403,5609,5610],{},"高可用连接数",[1403,5612,5613],{},"保持合理数量；更多连接不等于无限可用性",[1382,5615,5616,5619],{},[1403,5617,5618],{},"连接协议",[1403,5620,5621],{},"自动、HTTP\u002F2、QUIC 均需结合网络；UDP 受限时优先测试 HTTP\u002F2",[1382,5623,5624,5627],{},[1403,5625,5626],{},"边缘绑定 IP",[1403,5628,5629],{},"单网卡环境留空；多 WAN 定向出口时再配置",[1382,5631,5632,5635],{},[1403,5633,5634],{},"不验证后端 TLS 证书",[1403,5636,5637],{},"生产环境应关闭。优先填写正确的源服务器名或部署可信 CA",[16,5639,5640],{},"如果希望 Lucky 自动管理应用路由和 DNS，再提供一个单独的 Cloudflare API Token。Lucky 当前界面要求：",[39,5642,5643,5646,5649],{},[42,5644,5645],{},"Account → Cloudflare Tunnel → Edit；",[42,5647,5648],{},"Zone → DNS → Edit；",[42,5650,5651],{},"账户和 Zone 均缩小到实际使用范围。",[16,5653,5654],{},"不需要自动管理时可以不填 API Token，改在 Cloudflare Dashboard 手动维护路由和 DNS。最小权限永远比“为了省事选 All”更安全。",[576,5656,5658],{"id":5657},"_3-添加应用路由","3. 添加应用路由",[16,5660,5661],{},"在 Tunnel 实例右侧进入“应用路由”，添加：",[193,5663,5666],{"className":5664,"code":5665,"language":198,"meta":199},[196],"主机名：app.example.com\n路径：\u002F*\n目标服务：http:\u002F\u002F192.0.2.10:8080\n",[52,5667,5665],{"__ignoreMap":199},[16,5669,5670,5673,5674,5679,5680,5682],{},[52,5671,5672],{},"目标服务","必须包含协议。HTTP、HTTPS、TCP 等值的含义可以查阅 ",[111,5675,5678],{"href":5676,"rel":5677},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ftunnel\u002Frouting\u002F",[115],"Cloudflare Tunnel 路由文档","。对于普通浏览器访问，优先使用 HTTP\u002FHTTPS 应用路由；非 HTTP 公共主机名通常要求客户端也运行 ",[52,5681,5485],{},"，任意 TCP\u002FUDP 公网代理则可能需要 Spectrum 或私网路由方案。",[16,5684,5685,5686,5689,5690,5693],{},"当一条主机名有多条路径规则时，把具体规则放在通配规则之前。例如 ",[52,5687,5688],{},"\u002Fapi\u002F*"," 应先于 ",[52,5691,5692],{},"\u002F*","，否则前面的宽泛规则可能提前匹配。",[16,5695,5696],{},"高级选项只在需要时使用：",[39,5698,5699,5705,5711,5717,5723],{},[42,5700,5701,5704],{},[20,5702,5703],{},"源服务器名","：源站证书上的主机名与目标地址不一致时填写；",[42,5706,5707,5710],{},[20,5708,5709],{},"Host 请求头","：源站按虚拟主机分流时覆盖；",[42,5712,5713,5716],{},[20,5714,5715],{},"HTTP\u002F2 连接","：只有源站确实支持时开启；",[42,5718,5719,5722],{},[20,5720,5721],{},"连接、TLS、保活超时","：依据日志和服务特征调整；",[42,5724,5725,5727,5728,5731],{},[20,5726,5634],{},"：只能作为临时排错手段。Cloudflare 官方也把 ",[52,5729,5730],{},"noTLSVerify"," 定义为生产环境不推荐的最后手段。",[576,5733,5735],{"id":5734},"_4-配置-cname","4. 配置 CNAME",[16,5737,5738],{},"每条 Tunnel 会得到形如下面的目标：",[193,5740,5743],{"className":5741,"code":5742,"language":198,"meta":199},[196],"\u003CTUNNEL_ID>.cfargotunnel.com\n",[52,5744,5742],{"__ignoreMap":199},[16,5746,5747,5748,5751],{},"为 ",[52,5749,5750],{},"app.example.com"," 创建 CNAME 指向它。通过 Cloudflare Dashboard 添加 Published application route 时，DNS 记录通常会自动创建；从 Lucky 管理路由时，是否自动写入取决于 API Token 和当前配置。保存后务必回到 DNS 页面确认，而不是假设已经成功。",[16,5753,5754],{},"Tunnel UUID 不是等同于 Token 的认证密钥，但仍然属于基础设施标识。截图中没有展示它的必要，应统一打码。",[576,5756,5758],{"id":5757},"_5-给管理后台加-cloudflare-access","5. 给管理后台加 Cloudflare Access",[16,5760,5761],{},"Tunnel 只解决“如何到达源站”，不会自动决定“谁可以访问”。管理后台、监控页面和私人服务建议再创建 Access 应用：",[193,5763,5766],{"className":5764,"code":5765,"language":198,"meta":199},[196],"Zero Trust → Access controls → Applications\n→ Create new application → Self-hosted and private\n",[52,5767,5765],{"__ignoreMap":199},[16,5769,5770,5771,131],{},"把 Tunnel 的公网主机名加入应用，创建 Allow 策略，并用邮箱、身份提供商、设备状态或 Service Token 限制访问。Cloudflare Access 的策略由 Action、Include、Require、Exclude 等条件组成，详见",[111,5772,5775],{"href":5773,"rel":5774},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Faccess-controls\u002Fpolicies\u002F",[115],"官方 Access 策略文档",[16,5777,5778],{},"不要在 Access 保护的 URL 中依赖端口号；Cloudflare 的端口文档说明 Access 会移除 URL 里的端口。对外使用标准 HTTPS 主机名，把内网端口只保留在 Tunnel 的目标服务中。",[78,5780,5781],{"id":5781},"常见故障怎么定位",[1376,5783,5784,5794],{},[1379,5785,5786],{},[1382,5787,5788,5791],{},[1385,5789,5790],{},"现象",[1385,5792,5793],{},"优先检查",[1398,5795,5796,5804,5812,5820,5828,5838,5850,5858,5866,5874,5882,5890,5898,5909],{},[1382,5797,5798,5801],{},[1403,5799,5800],{},"STUN 没拿到映射",[1403,5802,5803],{},"NAT 条件、光猫\u002F路由器拨号位置、双重 NAT、代理软件、内核与防火墙",[1382,5805,5806,5809],{},[1403,5807,5808],{},"STUN 有映射但外网无日志",[1403,5810,5811],{},"公网端口、防火墙、路由器转发、运营商入站限制",[1382,5813,5814,5817],{},[1403,5815,5816],{},"STUN 有日志但服务打不开",[1403,5818,5819],{},"目标地址、目标端口、协议类型、服务监听地址",[1382,5821,5822,5825],{},[1403,5823,5824],{},"域名能解析但端口不对",[1403,5826,5827],{},"A\u002FAAAA 只保存 IP；检查公网端口变化通知或客户端配置",[1382,5829,5830,5833],{},[1403,5831,5832],{},"入口仍返回 301",[1403,5834,5835,5836],{},"Cloudflare 是否还有更靠前的 Redirect\u002FPage Rule；读取当前规则的 ",[52,5837,4988],{},[1382,5839,5840,5843],{},[1403,5841,5842],{},"Webhook 返回 API 错误",[1403,5844,5845,5846,5849],{},"Token 是否有 ",[52,5847,5848],{},"Single Redirect: Edit","、Zone\u002F规则 ID 是否正确、JSON 引号与转义是否完整",[1382,5851,5852,5855],{},[1403,5853,5854],{},"302 的 Location 仍是旧端口",[1403,5856,5857],{},"Lucky 是否触发 Webhook、成功判断是否误判、Cloudflare 规则是否被其他自动化覆盖",[1382,5859,5860,5863],{},[1403,5861,5862],{},"浏览器正确、App 无法登录",[1403,5864,5865],{},"App 是否支持重定向；Host、Cookie、认证回调和 TLS 是否绑定原域名",[1382,5867,5868,5871],{},[1403,5869,5870],{},"HTTPS 跳到 IP 后证书报错",[1403,5872,5873],{},"改用灰云目标域名与匹配的源站证书，或改用 Tunnel；不要关闭客户端证书校验",[1382,5875,5876,5879],{},[1403,5877,5878],{},"Tunnel 显示连接但返回 502",[1403,5880,5881],{},"Lucky 到目标服务的网络可达性、协议、端口和源站监听",[1382,5883,5884,5887],{},[1403,5885,5886],{},"HTTPS 源站出现 x509 错误",[1403,5888,5889],{},"优先设置正确的源服务器名或 CA，不要长期启用跳过验证",[1382,5891,5892,5895],{},[1403,5893,5894],{},"Tunnel 域名返回 1016",[1403,5896,5897],{},"CNAME 是否属于同一 Cloudflare 账户、隧道是否运行、目标 UUID 是否正确",[1382,5899,5900,5903],{},[1403,5901,5902],{},"路径访问到错误服务",[1403,5904,5905,5906,5908],{},"检查路由顺序，让具体路径位于 ",[52,5907,5692],{}," 之前",[1382,5910,5911,5914],{},[1403,5912,5913],{},"QUIC 连接失败",[1403,5915,5916],{},"检查出站 UDP；必要时切换 HTTP\u002F2 对照测试",[16,5918,5919],{},"排错时一次只改一个变量，并保留时间点。日志可以帮助定位，但分享前先删掉公网 IP、内网 IP、端口、域名、请求路径、Cookie、Authorization、Token 和实例备注。",[78,5921,5922],{"id":5922},"发布截图前的隐私检查清单",[16,5924,5925],{},"Lucky 的规则列表密度很高，一张未处理的截图可能同时暴露整个家庭网络拓扑。发布前逐项检查：",[39,5927,5928,5931,5934,5937,5940,5943,5946,5949],{},[42,5929,5930],{},"Lucky 后台地址、端口与安全入口路径；",[42,5932,5933],{},"公网 IP、映射端口、内网 IP、目标端口；",[42,5935,5936],{},"域名、主机名、实例名、规则名和设备名；",[42,5938,5939],{},"Tunnel Token、Cloudflare API Token、Access Service Token；",[42,5941,5942],{},"Account ID、Zone ID、Tunnel UUID 与 CNAME 目标；",[42,5944,5945],{},"请求路径、Webhook 地址与请求头；",[42,5947,5948],{},"日志、访问者 IP、流量统计与启用状态；",[42,5950,5951],{},"浏览器标签页、书签栏、账号头像和邮箱。",[16,5953,5954,5955,1094,5957,533,5959,5962],{},"仅仅模糊 Token 的中间几位还不够。安全做法是用重新绘制的示意图，或者把所有真实值替换为 ",[52,5956,5750],{},[52,5958,5090],{},[52,5960,5961],{},"\u003CTOKEN>"," 这类占位符。",[78,5964,5965],{"id":5965},"最后的选择建议",[16,5967,5968,5969,5971],{},"如果只是给自己或少量可信用户提供一个浏览器 GET 页面入口，能够接受跳转后暴露 IP 与端口，并且已经验证 NAT 条件，可以选择 ",[20,5970,4602],{},"。它解决的是“记住固定入口”，不是反向代理、安全网关或真正的端口隐藏。",[16,5973,5974],{},"如果请求必须保留方法，可在确认客户端行为后评估 307；不要把 303 直接填进 Single Redirect JSON。必须使用 303 时，应由 Worker 或源站生成，并单独设计动态目标存储和鉴权。",[16,5976,5977],{},"如果你要发布的是 Web 页面或 API，并希望地址始终保持标准 HTTPS 域名，同时使用 Access 和 WAF，选择 Cloudflare Tunnel。它不要求 STUN 成功，也不会因为公网映射端口改变而修改访问 URL。",[16,5979,5980],{},"如果你明确需要直接 TCP\u002FUDP、延迟和点对点路径更重要，且网络满足 Lucky 的 STUN 条件，可以选择 STUN；同时接受公网端口可能变化、灰云直连暴露源地址，以及需要自行完成应用认证和防火墙控制。",[16,5982,5983],{},"STUN 直连、STUN + 302 入口和 Cloudflare Tunnel 都能帮助“从外网访问内网”，但只有后者让业务流量经过 Cloudflare。先确定协议、客户端和安全边界，再选择入口方式，通常比照抄一份 301 请求体更可靠。",[78,5985,2056],{"id":2056},[39,5987,5988,5993,6000,6007,6014,6020,6027,6034,6041,6047,6054,6060,6067,6073],{},[42,5989,5990],{},[111,5991,2097],{"href":256,"rel":5992},[115],[42,5994,5995],{},[111,5996,5999],{"href":5997,"rel":5998},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fcloudflared",[115],"Lucky：Cloudflare 隧道",[42,6001,6002],{},[111,6003,6006],{"href":6004,"rel":6005},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fddns",[115],"Lucky：动态域名（DDNS）",[42,6008,6009],{},[111,6010,6013],{"href":6011,"rel":6012},"https:\u002F\u002Fwww.geekxw.top\u002F1814\u002F",[115],"参考思路：Lucky STUN + Cloudflare 301 重定向",[42,6015,6016],{},[111,6017,6019],{"href":2123,"rel":6018},[115],"Cloudflare：Single Redirect 可用状态码与动态表达式",[42,6021,6022],{},[111,6023,6026],{"href":6024,"rel":6025},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fcreate-api\u002F",[115],"Cloudflare：通过 API 创建 Single Redirect",[42,6028,6029],{},[111,6030,6033],{"href":6031,"rel":6032},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fapi\u002Fresources\u002Frulesets\u002F",[115],"Cloudflare：Rulesets API",[42,6035,6036],{},[111,6037,6040],{"href":6038,"rel":6039},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fsupport\u002Ftroubleshooting\u002Fhttp-status-codes\u002F3xx-redirection\u002F",[115],"Cloudflare：3xx 状态码语义",[42,6042,6043],{},[111,6044,6046],{"href":5489,"rel":6045},[115],"Cloudflare：Cloudflare Tunnel",[42,6048,6049],{},[111,6050,6053],{"href":6051,"rel":6052},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002Fget-started\u002Fcreate-remote-tunnel\u002F",[115],"Cloudflare：创建远程管理 Tunnel",[42,6055,6056],{},[111,6057,6059],{"href":5676,"rel":6058},[115],"Cloudflare：Tunnel 路由与 CNAME",[42,6061,6062],{},[111,6063,6066],{"href":6064,"rel":6065},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Freference\u002Fnetwork-ports\u002F",[115],"Cloudflare：代理支持的网络端口",[42,6068,6069],{},[111,6070,6072],{"href":5541,"rel":6071},[115],"Cloudflare：Tunnel Token 权限与轮换",[42,6074,6075],{},[111,6076,6079],{"href":6077,"rel":6078},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ftunnel\u002Ftroubleshooting\u002F",[115],"Cloudflare：Tunnel TLS 故障排查",[2134,6081,6082],{},"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":199,"searchDepth":328,"depth":328,"links":6084},[6085,6086,6093,6102,6109,6110,6111,6112],{"id":4550,"depth":308,"text":4550},{"id":4627,"depth":308,"text":4628,"children":6087},[6088,6089,6090,6091,6092],{"id":4631,"depth":328,"text":4632},{"id":4659,"depth":328,"text":4660},{"id":4695,"depth":328,"text":4696},{"id":4828,"depth":328,"text":4829},{"id":4855,"depth":328,"text":4856},{"id":4882,"depth":308,"text":4883,"children":6094},[6095,6096,6097,6098,6099,6100,6101],{"id":4896,"depth":328,"text":4897},{"id":4998,"depth":328,"text":4999},{"id":5067,"depth":328,"text":5068},{"id":5101,"depth":328,"text":5102},{"id":5135,"depth":328,"text":5136},{"id":5367,"depth":328,"text":5368},{"id":5448,"depth":328,"text":5449},{"id":5478,"depth":308,"text":5479,"children":6103},[6104,6105,6106,6107,6108],{"id":5494,"depth":328,"text":5495},{"id":5546,"depth":328,"text":5547},{"id":5657,"depth":328,"text":5658},{"id":5734,"depth":328,"text":5735},{"id":5757,"depth":328,"text":5758},{"id":5781,"depth":308,"text":5781},{"id":5922,"depth":308,"text":5922},{"id":5965,"depth":308,"text":5965},{"id":2056,"depth":308,"text":2056},"通过 Lucky STUN 获取公网映射，再用 Webhook 更新 Cloudflare Single Redirect，让用户从固定域名以 302 临时跳转到最新 IP 与端口；同时讲清 303 限制、TLS、App 兼容性与 Tunnel 替代方案。",{},"\u002Fblog\u002Flucky-stun-cloudflare-tunnel-guide","2026-08-02",{"title":4502,"description":6113},"blog\u002Flucky-stun-cloudflare-tunnel-guide",[6120,2183,6121,4615,2186],"Lucky","Cloudflare Redirect Rules","bo6v7kkJdfHWVBr56kRTAvoAvDtbHx8lj08nqYxvbN0",{"id":6124,"title":6125,"accent":11,"body":6126,"description":7692,"draft":2165,"experience":2166,"extension":2167,"featured":331,"image":7693,"meta":7699,"navigation":331,"path":7700,"publishedAt":7701,"readingTime":464,"seo":7702,"stem":7703,"tags":7704,"updatedAt":2166,"video":2166,"__hash__":7708,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Fcloudflare-smtp-email-sending-guide.md","Cloudflare SMTP 发信实战：从控制台配置到 Nodemailer 上线",{"type":13,"value":6127,"toc":7674},[6128,6135,6138,6147,6165,6168,6253,6256,6269,6276,6279,6283,6286,6292,6295,6299,6305,6311,6316,6319,6338,6341,6361,6365,6368,6374,6380,6383,6405,6408,6476,6488,6491,6523,6527,6530,6557,6560,6577,6580,6843,6849,6858,6861,7068,7086,7090,7093,7100,7192,7195,7198,7255,7258,7262,7265,7344,7354,7357,7365,7401,7404,7407,7410,7413,7490,7493,7499,7505,7509,7514,7534,7537,7540,7634,7637,7640,7643,7645,7671],[16,6129,6130,6131,6134],{},"Cloudflare Email Service 在 2026 年加入了认证 SMTP。对于已经使用 Nodemailer、Python ",[52,6132,6133],{},"smtplib","、PHPMailer，或者某个只会配置 SMTP 的后台系统来说，这意味着不必为了更换邮件服务商重写整个发送层。",[16,6136,6137],{},"本文是一篇独立的 SMTP 教程。目标不是重复介绍邮件接收、Email Routing 或 Email Workers，而是完成一件具体的事：",[72,6139,6140],{},[16,6141,6142,6143,6146],{},"使用 ",[52,6144,6145],{},"smtp.mx.cloudflare.net:465","，通过 Cloudflare API Token 完成 SMTPS 认证，并从自己的域名发送一封事务邮件。",[16,6148,6149,6150,6153,6154,6159,6160,131],{},"本文依据的是 ",[20,6151,6152],{},"2026 年 7 月 31 日","可见的 Cloudflare Email Service Beta 能力。Beta 阶段的配额和控制台界面仍可能变化，上线前应重新核对",[111,6155,6158],{"href":6156,"rel":6157},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Fsend-emails\u002F",[115],"官方发送指南","与",[111,6161,6164],{"href":6162,"rel":6163},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fsmtp\u002F",[115],"SMTP API 参考",[78,6166,6167],{"id":6167},"一分钟配置表",[1376,6169,6170,6180],{},[1379,6171,6172],{},[1382,6173,6174,6177],{},[1385,6175,6176],{},"项目",[1385,6178,6179],{},"值",[1398,6181,6182,6192,6202,6210,6221,6233,6245],{},[1382,6183,6184,6187],{},[1403,6185,6186],{},"SMTP 主机",[1403,6188,6189],{},[52,6190,6191],{},"smtp.mx.cloudflare.net",[1382,6193,6194,6197],{},[1403,6195,6196],{},"端口",[1403,6198,6199],{},[52,6200,6201],{},"465",[1382,6203,6204,6207],{},[1403,6205,6206],{},"加密",[1403,6208,6209],{},"隐式 TLS，也就是 SMTPS",[1382,6211,6212,6215],{},[1403,6213,6214],{},"用户名",[1403,6216,6217,6218],{},"固定为字面量 ",[52,6219,6220],{},"api_token",[1382,6222,6223,6226],{},[1403,6224,6225],{},"密码",[1403,6227,6228,6229,6232],{},"具有 ",[52,6230,6231],{},"Email Sending: Edit"," 权限的 Cloudflare API Token",[1382,6234,6235,6237],{},[1403,6236,2640],{},[1403,6238,6239,2585,6242],{},[52,6240,6241],{},"AUTH PLAIN",[52,6243,6244],{},"AUTH LOGIN",[1382,6246,6247,6250],{},[1403,6248,6249],{},"发件人",[1403,6251,6252],{},"已在 Email Sending 中启用的域名地址",[16,6254,6255],{},"这里有三个不能互换的概念：",[136,6257,6258,6263,6266],{},[42,6259,6260,6261,55],{},"用户名不是 Cloudflare 邮箱，也不是 Account ID，而是固定字符串 ",[52,6262,6220],{},[42,6264,6265],{},"密码不是登录 Cloudflare 的密码，而是专用 API Token；",[42,6267,6268],{},"端口 465 使用连接建立时立即开始的 TLS，不能照搬端口 587 的 STARTTLS 配置。",[16,6270,6271,6272,131],{},"Cloudflare 当前不提供 587 STARTTLS，也不把 25 端口作为出站中继；25 端口用于入站 Email Routing。详细差异可查阅",[111,6273,6275],{"href":6162,"rel":6274},[115],"官方 SMTP 文档",[78,6277,6278],{"id":6278},"开始前需要什么",[576,6280,6282],{"id":6281},"_1-域名使用-cloudflare-dns","1. 域名使用 Cloudflare DNS",[16,6284,6285],{},"Email Sending 要求发件域由 Cloudflare DNS 托管。进入：",[193,6287,6290],{"className":6288,"code":6289,"language":198,"meta":199},[196],"Cloudflare Dashboard\n→ Compute\n→ Email Service\n→ Email Sending\n",[52,6291,6289],{"__ignoreMap":199},[16,6293,6294],{},"选择域名并完成启用。Cloudflare 会配置发件验证、DKIM 与退信处理需要的 DNS 记录。控制台显示为“已配置”以后，再继续调 SMTP；否则认证成功也可能因为发件域未获授权而被拒绝。",[576,6296,6298],{"id":6297},"_2-创建最小权限的-api-token","2. 创建最小权限的 API Token",[16,6300,6301,6302,6304],{},"进入域名详情的“连接”页面，切换到 SMTP。页面会提示创建具有 ",[52,6303,6231],{}," 权限的 API Token。",[16,6306,6307],{},[782,6308],{"alt":6309,"src":6310},"Cloudflare Email Service 的 SMTP 连接页，显示主机、端口、用户名与 cURL 示例","\u002Fmedia\u002Fposts\u002Fcloudflare-smtp-email-sending-guide\u002Fdashboard-smtp-curl-v1.webp",[16,6312,6313],{},[789,6314,6315],{},"截图采集于 2026 年 7 月 31 日。画面只包含 Cloudflare 提供的配置值和环境变量占位符；真实 Token 没有被创建、显示或写入文章。",[16,6317,6318],{},"建议使用账户级 Token，并尽可能限制：",[39,6320,6321,6324,6329,6332,6335],{},[42,6322,6323],{},"只授权需要发信的 Cloudflare 账户；",[42,6325,6326,6327,55],{},"只授予 ",[52,6328,6231],{},[42,6330,6331],{},"为 Token 设置合理有效期；",[42,6333,6334],{},"如果部署出口 IP 稳定，再增加客户端 IP 过滤；",[42,6336,6337],{},"每个应用使用独立 Token，方便吊销和审计。",[16,6339,6340],{},"Token 只在创建时完整显示一次。将它写入服务端 Secret 管理或运行环境变量，绝不能：",[39,6342,6343,6346,6352,6355,6358],{},[42,6344,6345],{},"写进 Git 仓库、Markdown 或构建日志；",[42,6347,6348,6349,55],{},"放进 ",[52,6350,6351],{},"NUXT_PUBLIC_*",[42,6353,6354],{},"交给浏览器 JavaScript；",[42,6356,6357],{},"在异常日志中打印 SMTP 配置对象；",[42,6359,6360],{},"与其他项目共享同一个长期有效 Token。",[78,6362,6364],{"id":6363},"先用-curl-验证链路","先用 cURL 验证链路",[16,6366,6367],{},"在接入框架以前，先用 cURL 发送最小测试邮件，可以把“Cloudflare 配置问题”和“应用代码问题”拆开排查。",[16,6369,6370,6371,832],{},"创建 ",[52,6372,6373],{},"mail.txt",[193,6375,6378],{"className":6376,"code":6377,"language":198,"meta":199},[196],"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",[52,6379,6377],{"__ignoreMap":199},[16,6381,6382],{},"把 Token 放入当前终端进程的环境变量。不要把真实值写进 Shell 历史：",[193,6384,6386],{"className":285,"code":6385,"language":287,"meta":199,"style":199},"read -s CF_EMAIL_SMTP_TOKEN\nexport CF_EMAIL_SMTP_TOKEN\n",[52,6387,6388,6398],{"__ignoreMap":199},[291,6389,6390,6393,6395],{"class":293,"line":294},[291,6391,6392],{"class":318},"read",[291,6394,2437],{"class":318},[291,6396,6397],{"class":301}," CF_EMAIL_SMTP_TOKEN\n",[291,6399,6400,6403],{"class":293,"line":308},[291,6401,6402],{"class":409},"export",[291,6404,6397],{"class":553},[16,6406,6407],{},"然后发送：",[193,6409,6411],{"className":285,"code":6410,"language":287,"meta":199,"style":199},"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",[52,6412,6413,6425,6432,6448,6458,6468],{"__ignoreMap":199},[291,6414,6415,6417,6420,6423],{"class":293,"line":294},[291,6416,1210],{"class":297},[291,6418,6419],{"class":318}," --url",[291,6421,6422],{"class":301}," \"smtps:\u002F\u002Fsmtp.mx.cloudflare.net:465\"",[291,6424,368],{"class":318},[291,6426,6427,6430],{"class":293,"line":308},[291,6428,6429],{"class":318},"  --ssl-reqd",[291,6431,368],{"class":318},[291,6433,6434,6437,6440,6443,6446],{"class":293,"line":328},[291,6435,6436],{"class":318},"  --user",[291,6438,6439],{"class":301}," \"api_token:${",[291,6441,6442],{"class":553},"CF_EMAIL_SMTP_TOKEN",[291,6444,6445],{"class":301},"}\"",[291,6447,368],{"class":318},[291,6449,6450,6453,6456],{"class":293,"line":335},[291,6451,6452],{"class":318},"  --mail-from",[291,6454,6455],{"class":301}," \"noreply@sparkles-editor.com\"",[291,6457,368],{"class":318},[291,6459,6460,6463,6466],{"class":293,"line":354},[291,6461,6462],{"class":318},"  --mail-rcpt",[291,6464,6465],{"class":301}," \"recipient@example.com\"",[291,6467,368],{"class":318},[291,6469,6470,6473],{"class":293,"line":371},[291,6471,6472],{"class":318},"  --upload-file",[291,6474,6475],{"class":301}," mail.txt\n",[16,6477,6478,6479,6482,6483,131],{},"成功时，SMTP 会返回 ",[52,6480,6481],{},"250","。响应中包含的 Message-ID 可以用来关联 Email Service 控制台里的发送日志。完整格式可对照",[111,6484,6487],{"href":6485,"rel":6486},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fexamples\u002Femail-sending\u002Fsmtp\u002F",[115],"Cloudflare 的 cURL 示例",[16,6489,6490],{},"如果这里失败，先不要开始改 Nodemailer：",[39,6492,6493,6501,6507,6513],{},[42,6494,6495,6498,6499,55],{},[52,6496,6497],{},"535","：Token 无效、过期或缺少 ",[52,6500,6231],{},[42,6502,6503,6506],{},[52,6504,6505],{},"550","：发件地址或发件域没有被允许；",[42,6508,6509,6512],{},[52,6510,6511],{},"552","：邮件超过大小限制；",[42,6514,6515,6518,6519,6522],{},[52,6516,6517],{},"421"," \u002F ",[52,6520,6521],{},"451","：临时故障或限流，等待后重试。",[78,6524,6526],{"id":6525},"在-nodejs-中使用-nodemailer","在 Node.js 中使用 Nodemailer",[16,6528,6529],{},"下面的方案适用于 Node.js、Docker、VPS、传统 Serverless Function，或运行在 Node 兼容环境的 Nitro 服务端。安装依赖：",[193,6531,6533],{"className":285,"code":6532,"language":287,"meta":199,"style":199},"pnpm add nodemailer\npnpm add -D @types\u002Fnodemailer\n",[52,6534,6535,6546],{"__ignoreMap":199},[291,6536,6537,6540,6543],{"class":293,"line":294},[291,6538,6539],{"class":297},"pnpm",[291,6541,6542],{"class":301}," add",[291,6544,6545],{"class":301}," nodemailer\n",[291,6547,6548,6550,6552,6554],{"class":293,"line":308},[291,6549,6539],{"class":297},[291,6551,6542],{"class":301},[291,6553,1711],{"class":318},[291,6555,6556],{"class":301}," @types\u002Fnodemailer\n",[16,6558,6559],{},"服务端环境变量：",[193,6561,6565],{"className":6562,"code":6563,"language":6564,"meta":199,"style":199},"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",[52,6566,6567,6572],{"__ignoreMap":199},[291,6568,6569],{"class":293,"line":294},[291,6570,6571],{},"CF_EMAIL_SMTP_TOKEN=replace-with-a-server-side-secret\n",[291,6573,6574],{"class":293,"line":308},[291,6575,6576],{},"SMTP_FROM=\"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\n",[16,6578,6579],{},"创建 Transport：",[193,6581,6585],{"className":6582,"code":6583,"language":6584,"meta":199,"style":199},"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",[52,6586,6587,6601,6605,6625,6635,6644,6653,6658,6668,6677,6682,6692,6701,6711,6716,6720,6740,6750,6760,6770,6780,6790,6794,6798,6815,6820,6830,6839],{"__ignoreMap":199},[291,6588,6589,6592,6595,6598],{"class":293,"line":294},[291,6590,6591],{"class":409},"import",[291,6593,6594],{"class":553}," nodemailer ",[291,6596,6597],{"class":409},"from",[291,6599,6600],{"class":301}," 'nodemailer'\n",[291,6602,6603],{"class":293,"line":308},[291,6604,332],{"emptyLinePlaceholder":331},[291,6606,6607,6610,6613,6616,6619,6622],{"class":293,"line":328},[291,6608,6609],{"class":409},"const",[291,6611,6612],{"class":318}," transporter",[291,6614,6615],{"class":409}," =",[291,6617,6618],{"class":553}," nodemailer.",[291,6620,6621],{"class":297},"createTransport",[291,6623,6624],{"class":553},"({\n",[291,6626,6627,6630,6633],{"class":293,"line":335},[291,6628,6629],{"class":553},"  host: ",[291,6631,6632],{"class":301},"'smtp.mx.cloudflare.net'",[291,6634,5177],{"class":553},[291,6636,6637,6640,6642],{"class":293,"line":354},[291,6638,6639],{"class":553},"  port: ",[291,6641,6201],{"class":318},[291,6643,5177],{"class":553},[291,6645,6646,6649,6651],{"class":293,"line":371},[291,6647,6648],{"class":553},"  secure: ",[291,6650,5199],{"class":318},[291,6652,5177],{"class":553},[291,6654,6655],{"class":293,"line":380},[291,6656,6657],{"class":553},"  auth: {\n",[291,6659,6660,6663,6666],{"class":293,"line":393},[291,6661,6662],{"class":553},"    user: ",[291,6664,6665],{"class":301},"'api_token'",[291,6667,5177],{"class":553},[291,6669,6670,6673,6675],{"class":293,"line":398},[291,6671,6672],{"class":553},"    pass: process.env.",[291,6674,6442],{"class":318},[291,6676,5177],{"class":553},[291,6678,6679],{"class":293,"line":422},[291,6680,6681],{"class":553},"  },\n",[291,6683,6684,6687,6690],{"class":293,"line":428},[291,6685,6686],{"class":553},"  connectionTimeout: ",[291,6688,6689],{"class":318},"10_000",[291,6691,5177],{"class":553},[291,6693,6694,6697,6699],{"class":293,"line":434},[291,6695,6696],{"class":553},"  greetingTimeout: ",[291,6698,6689],{"class":318},[291,6700,5177],{"class":553},[291,6702,6703,6706,6709],{"class":293,"line":440},[291,6704,6705],{"class":553},"  socketTimeout: ",[291,6707,6708],{"class":318},"30_000",[291,6710,5177],{"class":553},[291,6712,6713],{"class":293,"line":446},[291,6714,6715],{"class":553},"})\n",[291,6717,6718],{"class":293,"line":452},[291,6719,332],{"emptyLinePlaceholder":331},[291,6721,6722,6724,6727,6729,6732,6735,6738],{"class":293,"line":458},[291,6723,6609],{"class":409},[291,6725,6726],{"class":318}," info",[291,6728,6615],{"class":409},[291,6730,6731],{"class":409}," await",[291,6733,6734],{"class":553}," transporter.",[291,6736,6737],{"class":297},"sendMail",[291,6739,6624],{"class":553},[291,6741,6742,6745,6748],{"class":293,"line":464},[291,6743,6744],{"class":553},"  from: process.env.",[291,6746,6747],{"class":318},"SMTP_FROM",[291,6749,5177],{"class":553},[291,6751,6752,6755,6758],{"class":293,"line":469},[291,6753,6754],{"class":553},"  to: ",[291,6756,6757],{"class":301},"'recipient@example.com'",[291,6759,5177],{"class":553},[291,6761,6762,6765,6768],{"class":293,"line":478},[291,6763,6764],{"class":553},"  subject: ",[291,6766,6767],{"class":301},"'Welcome to Signal Log'",[291,6769,5177],{"class":553},[291,6771,6772,6775,6778],{"class":293,"line":491},[291,6773,6774],{"class":553},"  text: ",[291,6776,6777],{"class":301},"'Your account is ready.'",[291,6779,5177],{"class":553},[291,6781,6782,6785,6788],{"class":293,"line":499},[291,6783,6784],{"class":553},"  html: ",[291,6786,6787],{"class":301},"'\u003Cp>Your account is ready.\u003C\u002Fp>'",[291,6789,5177],{"class":553},[291,6791,6792],{"class":293,"line":507},[291,6793,6715],{"class":553},[291,6795,6796],{"class":293,"line":515},[291,6797,332],{"emptyLinePlaceholder":331},[291,6799,6800,6803,6806,6809,6812],{"class":293,"line":523},[291,6801,6802],{"class":553},"console.",[291,6804,6805],{"class":297},"info",[291,6807,6808],{"class":553},"(",[291,6810,6811],{"class":301},"'Email accepted'",[291,6813,6814],{"class":553},", {\n",[291,6816,6817],{"class":293,"line":1049},[291,6818,6819],{"class":553},"  messageId: info.messageId,\n",[291,6821,6822,6825,6828],{"class":293,"line":1059},[291,6823,6824],{"class":553},"  accepted: info.accepted.",[291,6826,6827],{"class":318},"length",[291,6829,5177],{"class":553},[291,6831,6832,6835,6837],{"class":293,"line":1064},[291,6833,6834],{"class":553},"  rejected: info.rejected.",[291,6836,6827],{"class":318},[291,6838,5177],{"class":553},[291,6840,6841],{"class":293,"line":1072},[291,6842,6715],{"class":553},[16,6844,6845],{},[782,6846],{"alt":6847,"src":6848},"Cloudflare 控制台中的 Nodemailer 示例，密码从环境变量读取","\u002Fmedia\u002Fposts\u002Fcloudflare-smtp-email-sending-guide\u002Fdashboard-smtp-node-v1.webp",[16,6850,6851],{},[789,6852,6853,6854,6857],{},"控制台示例使用 ",[52,6855,6856],{},"process.env.CLOUDFLARE_API_TOKEN","。实际项目可以使用更明确的变量名，但必须保证它只存在于服务端。",[16,6859,6860],{},"生产代码还应补上输入边界：",[193,6862,6864],{"className":6582,"code":6863,"language":6584,"meta":199,"style":199},"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",[52,6865,6866,6884,6891,6898,6903,6907,6939,6959,6977,6981,6985,7045,7060,7064],{"__ignoreMap":199},[291,6867,6868,6870,6873,6875,6878,6881],{"class":293,"line":294},[291,6869,6609],{"class":409},[291,6871,6872],{"class":318}," allowedFrom",[291,6874,6615],{"class":409},[291,6876,6877],{"class":409}," new",[291,6879,6880],{"class":297}," Set",[291,6882,6883],{"class":553},"([\n",[291,6885,6886,6889],{"class":293,"line":308},[291,6887,6888],{"class":301},"  'noreply@sparkles-editor.com'",[291,6890,5177],{"class":553},[291,6892,6893,6896],{"class":293,"line":328},[291,6894,6895],{"class":301},"  'support@sparkles-editor.com'",[291,6897,5177],{"class":553},[291,6899,6900],{"class":293,"line":335},[291,6901,6902],{"class":553},"])\n",[291,6904,6905],{"class":293,"line":354},[291,6906,332],{"emptyLinePlaceholder":331},[291,6908,6909,6912,6915,6917,6920,6923,6926,6929,6932,6934,6936],{"class":293,"line":371},[291,6910,6911],{"class":409},"function",[291,6913,6914],{"class":297}," assertEmailEnvelope",[291,6916,6808],{"class":553},[291,6918,6597],{"class":6919},"s-3mD",[291,6921,6922],{"class":409},":",[291,6924,6925],{"class":318}," string",[291,6927,6928],{"class":553},", ",[291,6930,6931],{"class":6919},"to",[291,6933,6922],{"class":409},[291,6935,6925],{"class":318},[291,6937,6938],{"class":553},") {\n",[291,6940,6941,6944,6947,6950,6953,6956],{"class":293,"line":380},[291,6942,6943],{"class":409},"  if",[291,6945,6946],{"class":553}," (",[291,6948,6949],{"class":409},"!",[291,6951,6952],{"class":553},"allowedFrom.",[291,6954,6955],{"class":297},"has",[291,6957,6958],{"class":553},"(from)) {\n",[291,6960,6961,6964,6966,6969,6971,6974],{"class":293,"line":393},[291,6962,6963],{"class":409},"    throw",[291,6965,6877],{"class":409},[291,6967,6968],{"class":297}," Error",[291,6970,6808],{"class":553},[291,6972,6973],{"class":301},"'Sender is not allowed'",[291,6975,6976],{"class":553},")\n",[291,6978,6979],{"class":293,"line":398},[291,6980,5300],{"class":553},[291,6982,6983],{"class":293,"line":422},[291,6984,332],{"emptyLinePlaceholder":331},[291,6986,6987,6989,6991,6993,6996,6999,7002,7004,7007,7010,7014,7016,7018,7020,7022,7026,7028,7030,7032,7035,7037,7039,7042],{"class":293,"line":428},[291,6988,6943],{"class":409},[291,6990,6946],{"class":553},[291,6992,6949],{"class":409},[291,6994,6995],{"class":301},"\u002F",[291,6997,6998],{"class":409},"^",[291,7000,7001],{"class":318},"[",[291,7003,6998],{"class":409},[291,7005,7006],{"class":318},"\\s@]",[291,7008,7009],{"class":409},"+",[291,7011,7013],{"class":7012},"se7xv","@",[291,7015,7001],{"class":318},[291,7017,6998],{"class":409},[291,7019,7006],{"class":318},[291,7021,7009],{"class":409},[291,7023,7025],{"class":7024},"s-Lb2","\\.",[291,7027,7001],{"class":318},[291,7029,6998],{"class":409},[291,7031,7006],{"class":318},[291,7033,7034],{"class":409},"+$",[291,7036,6995],{"class":301},[291,7038,547],{"class":553},[291,7040,7041],{"class":297},"test",[291,7043,7044],{"class":553},"(to)) {\n",[291,7046,7047,7049,7051,7053,7055,7058],{"class":293,"line":434},[291,7048,6963],{"class":409},[291,7050,6877],{"class":409},[291,7052,6968],{"class":297},[291,7054,6808],{"class":553},[291,7056,7057],{"class":301},"'Recipient is invalid'",[291,7059,6976],{"class":553},[291,7061,7062],{"class":293,"line":440},[291,7063,5300],{"class":553},[291,7065,7066],{"class":293,"line":446},[291,7067,5305],{"class":553},[16,7069,7070,7071,1094,7073,1094,7075,7078,7079,533,7082,7085],{},"不要提供一个接收任意 ",[52,7072,6597],{},[52,7074,6931],{},[52,7076,7077],{},"subject"," 和 HTML 的公开 API。否则应用很容易变成开放邮件中继，或者被用来发送钓鱼邮件。更安全的接口只接收业务参数，例如 ",[52,7080,7081],{},"userId",[52,7083,7084],{},"templateId","；服务端再从数据库取已验证地址，并从白名单模板生成标题与正文。",[78,7087,7089],{"id":7088},"nuxt-部署在-cloudflare-workers-时怎么办","Nuxt 部署在 Cloudflare Workers 时怎么办",[16,7091,7092],{},"这里需要特别区分运行时。",[16,7094,7095,7096,7099],{},"Nodemailer 是面向传统 Node SMTP Socket 的库。Cloudflare Workers 上的 Nuxt 应用已经运行在 Cloudflare 网络内，通常更适合使用 ",[52,7097,7098],{},"send_email"," binding：",[193,7101,7103],{"className":6582,"code":7102,"language":6584,"meta":199,"style":199},"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",[52,7104,7105,7116,7126,7130,7134,7152,7162,7170,7179,7188],{"__ignoreMap":199},[291,7106,7107,7110,7113],{"class":293,"line":294},[291,7108,7109],{"class":409},"interface",[291,7111,7112],{"class":297}," Env",[291,7114,7115],{"class":553}," {\n",[291,7117,7118,7121,7123],{"class":293,"line":308},[291,7119,7120],{"class":6919},"  EMAIL",[291,7122,6922],{"class":409},[291,7124,7125],{"class":297}," SendEmail\n",[291,7127,7128],{"class":293,"line":328},[291,7129,5305],{"class":553},[291,7131,7132],{"class":293,"line":335},[291,7133,332],{"emptyLinePlaceholder":331},[291,7135,7136,7139,7142,7145,7147,7150],{"class":293,"line":354},[291,7137,7138],{"class":409},"await",[291,7140,7141],{"class":553}," env.",[291,7143,7144],{"class":318},"EMAIL",[291,7146,547],{"class":553},[291,7148,7149],{"class":297},"send",[291,7151,6624],{"class":553},[291,7153,7154,7157,7160],{"class":293,"line":371},[291,7155,7156],{"class":553},"  from: ",[291,7158,7159],{"class":301},"'noreply@sparkles-editor.com'",[291,7161,5177],{"class":553},[291,7163,7164,7166,7168],{"class":293,"line":380},[291,7165,6754],{"class":553},[291,7167,6757],{"class":301},[291,7169,5177],{"class":553},[291,7171,7172,7174,7177],{"class":293,"line":393},[291,7173,6764],{"class":553},[291,7175,7176],{"class":301},"'Verification code'",[291,7178,5177],{"class":553},[291,7180,7181,7183,7186],{"class":293,"line":398},[291,7182,6774],{"class":553},[291,7184,7185],{"class":301},"'Your code is 123456'",[291,7187,5177],{"class":553},[291,7189,7190],{"class":293,"line":422},[291,7191,6715],{"class":553},[16,7193,7194],{},"这样不需要在 Worker 中保存 SMTP API Token，权限也由 binding 管理。",[16,7196,7197],{},"选择原则很简单：",[1376,7199,7200,7210],{},[1379,7201,7202],{},[1382,7203,7204,7207],{},[1385,7205,7206],{},"场景",[1385,7208,7209],{},"推荐入口",[1398,7211,7212,7223,7231,7239,7247],{},[1382,7213,7214,7217],{},[1403,7215,7216],{},"Nuxt \u002F API 已经部署在 Cloudflare Workers",[1403,7218,7219,7220,7222],{},"Workers ",[52,7221,7098],{}," binding",[1382,7224,7225,7228],{},[1403,7226,7227],{},"外部服务支持 HTTPS API，且便于管理 Token",[1403,7229,7230],{},"REST API",[1382,7232,7233,7236],{},[1403,7234,7235],{},"现有 Node、Python、PHP 应用已经支持 SMTP",[1403,7237,7238],{},"认证 SMTP",[1382,7240,7241,7244],{},[1403,7242,7243],{},"打印机、旧式 CMS 或第三方工具只能填 SMTP",[1403,7245,7246],{},"认证 SMTP，但先确认能使用 465 SMTPS",[1382,7248,7249,7252],{},[1403,7250,7251],{},"浏览器前端直接发送",[1403,7253,7254],{},"不允许；必须经过受控服务端",[16,7256,7257],{},"三个入口最后进入同一套 Cloudflare 投递管线、日志和域名认证系统。SMTP 的价值是兼容性，不是要求所有新服务都退回 Socket 协议。",[78,7259,7261],{"id":7260},"python-最小示例","Python 最小示例",[16,7263,7264],{},"Python 标准库无需额外依赖：",[193,7266,7270],{"className":7267,"code":7268,"language":7269,"meta":199,"style":199},"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",[52,7271,7272,7277,7282,7287,7291,7296,7300,7305,7310,7315,7320,7325,7329,7334,7339],{"__ignoreMap":199},[291,7273,7274],{"class":293,"line":294},[291,7275,7276],{},"import os\n",[291,7278,7279],{"class":293,"line":308},[291,7280,7281],{},"import smtplib\n",[291,7283,7284],{"class":293,"line":328},[291,7285,7286],{},"from email.message import EmailMessage\n",[291,7288,7289],{"class":293,"line":335},[291,7290,332],{"emptyLinePlaceholder":331},[291,7292,7293],{"class":293,"line":354},[291,7294,7295],{},"token = os.environ[\"CF_EMAIL_SMTP_TOKEN\"]\n",[291,7297,7298],{"class":293,"line":371},[291,7299,332],{"emptyLinePlaceholder":331},[291,7301,7302],{"class":293,"line":380},[291,7303,7304],{},"message = EmailMessage()\n",[291,7306,7307],{"class":293,"line":393},[291,7308,7309],{},"message[\"From\"] = \"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\n",[291,7311,7312],{"class":293,"line":398},[291,7313,7314],{},"message[\"To\"] = \"recipient@example.com\"\n",[291,7316,7317],{"class":293,"line":422},[291,7318,7319],{},"message[\"Subject\"] = \"Cloudflare SMTP test\"\n",[291,7321,7322],{"class":293,"line":428},[291,7323,7324],{},"message.set_content(\"This email was sent over implicit TLS.\")\n",[291,7326,7327],{"class":293,"line":434},[291,7328,332],{"emptyLinePlaceholder":331},[291,7330,7331],{"class":293,"line":440},[291,7332,7333],{},"with smtplib.SMTP_SSL(\"smtp.mx.cloudflare.net\", 465, timeout=30) as smtp:\n",[291,7335,7336],{"class":293,"line":446},[291,7337,7338],{},"    smtp.login(\"api_token\", token)\n",[291,7340,7341],{"class":293,"line":452},[291,7342,7343],{},"    smtp.send_message(message)\n",[16,7345,7346,7347,7350,7351,131],{},"关键是 ",[52,7348,7349],{},"SMTP_SSL","，而不是先明文连接再调用 ",[52,7352,7353],{},"starttls()",[78,7355,7356],{"id":7356},"限额与附件",[16,7358,7359,7360,832],{},"根据当前",[111,7361,7364],{"href":7362,"rel":7363},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Flimits\u002F",[115],"Email Service 平台限制",[39,7366,7367,7370,7376,7379,7382,7389,7395,7398],{},[42,7368,7369],{},"每封邮件最多合计 50 个收件人；",[42,7371,7372,7373,55],{},"SMTP 会话最多接受 50 次 ",[52,7374,7375],{},"RCPT TO",[42,7377,7378],{},"普通邮件总大小上限为 5 MiB；",[42,7380,7381],{},"发往“已验证目标地址”时，总大小上限可到 25 MiB；",[42,7383,7384,7385,7388],{},"SMTP ",[52,7386,7387],{},"AUTH"," 超时为 30 秒；",[42,7390,7384,7391,7394],{},[52,7392,7393],{},"DATA"," 超时为 300 秒；",[42,7396,7397],{},"自定义邮件头合计上限为 16 KiB；",[42,7399,7400],{},"Subject 上限为 998 个字符。",[16,7402,7403],{},"邮件总大小包含 MIME 编码后的正文和附件。二进制附件经过 Base64 后通常会膨胀约三分之一，因此不要把一个 4.9 MiB 文件误认为一定能塞进 5 MiB 邮件。",[16,7405,7406],{},"控制台显示的每日配额可能因账户、Beta 阶段和使用情况而变化。不要把某次看到的数字写死在业务逻辑里；应读取当前控制台配额，并在自己的发送队列中保留余量。",[78,7408,7409],{"id":7409},"错误处理和重试",[16,7411,7412],{},"SMTP 的返回码已经表达了“要不要重试”：",[1376,7414,7415,7426],{},[1379,7416,7417],{},[1382,7418,7419,7422,7424],{},[1385,7420,7421],{},"返回码",[1385,7423,2322],{},[1385,7425,4722],{},[1398,7427,7428,7440,7454,7466,7478],{},[1382,7429,7430,7434,7437],{},[1403,7431,7432],{},[52,7433,6481],{},[1403,7435,7436],{},"已被 Cloudflare 接受",[1403,7438,7439],{},"记录 Message-ID，不再重复发送",[1382,7441,7442,7448,7451],{},[1403,7443,7444,6518,7446],{},[52,7445,6517],{},[52,7447,6521],{},[1403,7449,7450],{},"临时故障或限流",[1403,7452,7453],{},"指数退避并加入随机抖动",[1382,7455,7456,7460,7463],{},[1403,7457,7458],{},[52,7459,6497],{},[1403,7461,7462],{},"认证失败",[1403,7464,7465],{},"停止自动重试，检查 Token",[1382,7467,7468,7472,7475],{},[1403,7469,7470],{},[52,7471,6505],{},[1403,7473,7474],{},"发件域、地址或收件策略被拒绝",[1403,7476,7477],{},"修正配置或数据后再发",[1382,7479,7480,7484,7487],{},[1403,7481,7482],{},[52,7483,6511],{},[1403,7485,7486],{},"邮件过大",[1403,7488,7489],{},"缩小正文或附件",[16,7491,7492],{},"推荐的重试节奏可以是 30 秒、2 分钟、10 分钟、30 分钟，最多尝试固定次数。每次业务发送必须有幂等键，例如：",[193,7494,7497],{"className":7495,"code":7496,"language":198,"meta":199},[196],"password-reset:{userId}:{resetRequestId}\ninvoice:{invoiceId}:issued\n",[52,7498,7496],{"__ignoreMap":199},[16,7500,7501,7502,7504],{},"临时错误进入队列，永久错误进入人工检查或失败状态。不要把所有非 ",[52,7503,6481],{}," 都立即重试，也不要让同一个验证码因为网络抖动发送十次。",[78,7506,7508],{"id":7507},"投递率不是返回-250-就结束","投递率不是“返回 250 就结束”",[16,7510,7511,7513],{},[52,7512,6481],{}," 代表 Cloudflare 接受了消息，不代表最终收件箱一定出现了邮件。生产环境还要观察：",[39,7515,7516,7519,7522,7525,7528,7531],{},[42,7517,7518],{},"Delivery、Bounce、Deferred 与 Suppression；",[42,7520,7521],{},"Gmail、Outlook、QQ 邮箱等主要目的地的结果；",[42,7523,7524],{},"From、Return-Path、DKIM 和 DMARC 对齐；",[42,7526,7527],{},"硬退信地址是否立即停止发送；",[42,7529,7530],{},"投诉率与无效地址增长；",[42,7532,7533],{},"验证码和密码重置是否设置明确有效期。",[16,7535,7536],{},"事务邮件应该使用稳定、可识别的发件人，并同时提供纯文本与 HTML。不要伪造 Reply-To，不要用验证码通道发送营销群发，也不要让用户输入直接进入邮件头，避免 Header Injection。",[78,7538,7539],{"id":7539},"上线检查清单",[39,7541,7544,7553,7559,7567,7576,7582,7588,7594,7600,7610,7622,7628],{"className":7542},[7543],"contains-task-list",[42,7545,7548,7552],{"className":7546},[7547],"task-list-item",[7549,7550],"input",{"disabled":331,"type":7551},"checkbox"," 发件域在 Email Sending 中为“已配置”",[42,7554,7556,7558],{"className":7555},[7547],[7549,7557],{"disabled":331,"type":7551}," 使用 465 + 隐式 TLS",[42,7560,7562,7564,7565],{"className":7561},[7547],[7549,7563],{"disabled":331,"type":7551}," 用户名严格为 ",[52,7566,6220],{},[42,7568,7570,7572,7573,7575],{"className":7569},[7547],[7549,7571],{"disabled":331,"type":7551}," Token 只有 ",[52,7574,6231],{}," 最小权限",[42,7577,7579,7581],{"className":7578},[7547],[7549,7580],{"disabled":331,"type":7551}," Token 位于服务端 Secret，不进入 Git、日志和前端",[42,7583,7585,7587],{"className":7584},[7547],[7549,7586],{"disabled":331,"type":7551}," From 地址来自服务端白名单",[42,7589,7591,7593],{"className":7590},[7547],[7549,7592],{"disabled":331,"type":7551}," 收件地址经过验证、去重和长度限制",[42,7595,7597,7599],{"className":7596},[7547],[7549,7598],{"disabled":331,"type":7551}," HTML 使用固定模板并保留纯文本版本",[42,7601,7603,181,7605,6518,7607,7609],{"className":7602},[7547],[7549,7604],{"disabled":331,"type":7551},[52,7606,6517],{},[52,7608,6521],{}," 使用带抖动的指数退避",[42,7611,7613,181,7615,6518,7617,6518,7619,7621],{"className":7612},[7547],[7549,7614],{"disabled":331,"type":7551},[52,7616,6497],{},[52,7618,6505],{},[52,7620,6511],{}," 不做盲目重试",[42,7623,7625,7627],{"className":7624},[7547],[7549,7626],{"disabled":331,"type":7551}," 记录 Message-ID、模板名与业务幂等键，不记录 Token",[42,7629,7631,7633],{"className":7630},[7547],[7549,7632],{"disabled":331,"type":7551}," 监控退信、抑制、投诉和每日配额",[78,7635,7636],{"id":7636},"最后的选择",[16,7638,7639],{},"如果你有一个已经稳定运行的 Node、Python 或 PHP 应用，Cloudflare SMTP 的优势很直接：只换主机、端口和认证信息，就能接入 Cloudflare 的投递与日志体系。",[16,7641,7642],{},"如果应用原生运行在 Workers，则没有必要绕一圈模拟传统服务器，优先使用 Email binding。SMTP 是一座兼容旧有生态的桥，而不是唯一入口。",[576,7644,2056],{"id":2056},[39,7646,7647,7653,7659,7665],{},[42,7648,7649],{},[111,7650,7652],{"href":6156,"rel":7651},[115],"Cloudflare Email Service：发送邮件入门",[42,7654,7655],{},[111,7656,7658],{"href":6162,"rel":7657},[115],"Cloudflare Email Service：SMTP API 参考",[42,7660,7661],{},[111,7662,7664],{"href":6485,"rel":7663},[115],"Cloudflare Email Service：SMTP 示例",[42,7666,7667],{},[111,7668,7670],{"href":7362,"rel":7669},[115],"Cloudflare Email Service：平台限制",[2134,7672,7673],{},"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":199,"searchDepth":328,"depth":328,"links":7675},[7676,7677,7681,7682,7683,7684,7685,7686,7687,7688,7689],{"id":6167,"depth":308,"text":6167},{"id":6278,"depth":308,"text":6278,"children":7678},[7679,7680],{"id":6281,"depth":328,"text":6282},{"id":6297,"depth":328,"text":6298},{"id":6363,"depth":308,"text":6364},{"id":6525,"depth":308,"text":6526},{"id":7088,"depth":308,"text":7089},{"id":7260,"depth":308,"text":7261},{"id":7356,"depth":308,"text":7356},{"id":7409,"depth":308,"text":7409},{"id":7507,"depth":308,"text":7508},{"id":7539,"depth":308,"text":7539},{"id":7636,"depth":308,"text":7636,"children":7690},[7691],{"id":2056,"depth":328,"text":2056},"一篇独立、可落地的 Cloudflare Email Service SMTP 教程，覆盖域名接入、API Token、cURL、Nodemailer、Python、限额、错误码与生产安全。",{"src":7694,"alt":7695,"caption":7696,"width":7697,"height":7698},"\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":6125,"description":7692},"blog\u002Fcloudflare-smtp-email-sending-guide",[2185,7705,7706,7707],"SMTP","Email","Nodemailer","ioq36qTQ7AGVlLQpfduYQZC0FCJygbgoBDEtRMP7pJs",{"id":7710,"title":7711,"accent":11,"body":7712,"description":10071,"draft":2165,"experience":2166,"extension":2167,"featured":331,"image":10072,"meta":10078,"navigation":331,"path":10079,"publishedAt":10080,"readingTime":507,"seo":10081,"stem":10082,"tags":10083,"updatedAt":2166,"video":10086,"__hash__":10091,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Fcloudflare-email-service-send-receive-guide.md","Cloudflare Email Service 收发实战：从 Email Routing 到 Workers、API 与 SMTP",{"type":13,"value":7713,"toc":10038},[7714,7721,7724,7735,7755,7762,7765,7770,7774,7777,7827,7830,7836,7839,7845,7851,7856,7860,7863,7867,7870,7873,7895,7899,7902,7905,7919,7922,7926,7929,7932,7935,7946,7949,7957,7964,7981,7984,7994,7997,8003,8006,8010,8013,8028,8031,8038,8047,8379,8389,8482,8497,8501,8507,8637,8640,8657,8661,8687,8690,8710,8713,8717,8720,8727,8795,8801,8804,8823,8829,8833,9036,9042,9046,9049,9360,9363,9369,9374,9378,9381,9431,9434,9437,9456,9459,9462,9584,9587,9590,9593,9596,9602,9605,9628,9635,9638,9641,9644,9650,9653,9659,9663,9666,9690,9693,9713,9716,9720,9730,9736,9739,9742,9760,9763,9822,9825,9828,9860,9864,9867,9887,9890,9907,9910,9913,9916,9919,9922,9942,9945,9947,10035],[16,7715,7716,7717,7720],{},"很长一段时间里，Cloudflare 与邮件的关系主要停留在两件事上：管理 DNS，以及用 Email Routing 把 ",[52,7718,7719],{},"support@example.com"," 转发到一个真实邮箱。",[16,7722,7723],{},"2026 年，这个边界明显向前移动了。",[16,7725,7726,7727,7730,7731,7734],{},"Cloudflare 在 4 月把 ",[20,7728,7729],{},"Email Sending"," 推进公开 Beta，让 Workers 可以直接调用 ",[52,7732,7733],{},"env.EMAIL.send()"," 发送事务邮件；6 月又加入了认证 SMTP。加上原有的 Email Routing 与 Email Workers，现在一套 Cloudflare Email Service 已经可以覆盖：",[39,7736,7737,7740,7743,7746,7749,7752],{},[42,7738,7739],{},"接收发往自有域名的邮件；",[42,7741,7742],{},"转发到经过验证的真实邮箱；",[42,7744,7745],{},"把来信交给 Worker 解析、过滤、入库或触发自动化；",[42,7747,7748],{},"在原邮件线程中回复；",[42,7750,7751],{},"从 Worker、REST API 或 SMTP 发送事务邮件；",[42,7753,7754],{},"查看发送日志、退信、抑制与投递指标。",[16,7756,7757,7758,7761],{},"这篇文章以 ",[20,7759,7760],{},"2026 年 7 月 28 日","的官方能力为准，梳理 Cloudflare 邮件收发的完整模型，并给出一套适合 Nuxt、Workers 与 D1 项目的实现思路。",[16,7763,7764],{},"先说结论：",[72,7766,7767],{},[16,7768,7769],{},"Cloudflare Email Service 不是传统的 IMAP \u002F POP3 邮箱，也不是让你在网页里收信的企业邮箱套件。它更像一组“邮件入口、发送通道和可编程事件”，适合验证码、密码重置、订单通知、系统告警、支持工单和邮件驱动的自动化。",[78,7771,7773],{"id":7772},"先把接收和发送分开","先把“接收”和“发送”分开",[16,7775,7776],{},"邮件收发看起来是一件事，在 Cloudflare 里却是两条独立链路。",[1376,7778,7779,7795],{},[1379,7780,7781],{},[1382,7782,7783,7786,7789,7792],{},[1385,7784,7785],{},"方向",[1385,7787,7788],{},"Cloudflare 产品",[1385,7790,7791],{},"入口",[1385,7793,7794],{},"Worker 能做什么",[1398,7796,7797,7811],{},[1382,7798,7799,7802,7805,7808],{},[1403,7800,7801],{},"接收",[1403,7803,7804],{},"Email Routing \u002F Email Workers",[1403,7806,7807],{},"外部邮件服务器连接 Cloudflare MX",[1403,7809,7810],{},"转发、回复、拒收、解析、写入 D1\u002FR2、投递 Queue",[1382,7812,7813,7816,7818,7821],{},[1403,7814,7815],{},"发送",[1403,7817,7729],{},[1403,7819,7820],{},"Workers binding、REST API、认证 SMTP",[1403,7822,7823,7824],{},"发送文本\u002FHTML、附件、自定义头，获得 ",[52,7825,7826],{},"messageId",[16,7828,7829],{},"接收链路大致是：",[193,7831,7834],{"className":7832,"code":7833,"language":198,"meta":199},[196],"发送方 SMTP\n  → Cloudflare MX\n  → SPF \u002F DKIM \u002F DMARC 与信誉检查\n  → 匹配地址规则或 Catch-all\n  → 转发到邮箱 \u002F 交给 Worker \u002F 丢弃\n",[52,7835,7833],{"__ignoreMap":199},[16,7837,7838],{},"发送链路则是：",[193,7840,7843],{"className":7841,"code":7842,"language":198,"meta":199},[196],"Nuxt API \u002F Worker \u002F 外部应用\n  → EMAIL binding \u002F REST API \u002F SMTP\n  → 发件域与参数校验\n  → Cloudflare 投递管线\n  → 收件方邮件服务器\n",[52,7844,7842],{"__ignoreMap":199},[16,7846,7847],{},[782,7848],{"alt":7849,"src":7850},"邮件在边缘入口完成检查后，被分发到真实邮箱、Worker 处理节点或拒绝路径","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Finbound-routing-v1.webp",[16,7852,7853],{},[789,7854,7855],{},"这是一张概念图。真实判断还包含 SMTP 会话、邮件认证、规则匹配和 Worker 执行限制。",[78,7857,7859],{"id":7858},"_2026-年的产品边界与价格","2026 年的产品边界与价格",[16,7861,7862],{},"Cloudflare 当前把接收与发送放在同一个 Email Service 下，但两部分的可用计划不同。",[576,7864,7866],{"id":7865},"email-routing接收与转发","Email Routing：接收与转发",[16,7868,7869],{},"Email Routing 在 Workers Free 和 Workers Paid 都可以使用。官方价格页把入站邮件数量标为不限量，但如果邮件被交给 Worker，仍然会消耗对应的 Worker 请求与计算资源。",[16,7871,7872],{},"它适合：",[39,7874,7875,7882,7885,7892],{},[42,7876,7877,7878,7881],{},"把 ",[52,7879,7880],{},"support@yourdomain.com"," 转发到现有个人邮箱；",[42,7883,7884],{},"为不同网站创建独立的联系地址；",[42,7886,7887,7888,7891],{},"用 ",[52,7889,7890],{},"orders+shop-a@yourdomain.com"," 这样的子地址区分来源；",[42,7893,7894],{},"把来信交给 Worker 转换成工单、Webhook 或数据库记录。",[576,7896,7898],{"id":7897},"email-sending事务邮件","Email Sending：事务邮件",[16,7900,7901],{},"Email Sending 当前仍是公开 Beta。",[16,7903,7904],{},"向任意收件人发送需要 Workers Paid 计划。按照 2026 年 7 月的官方价格：",[39,7906,7907,7910,7913,7916],{},[42,7908,7909],{},"每个账户每月包含 3,000 封出站邮件；",[42,7911,7912],{},"超出后每 1,000 封为 0.35 美元；",[42,7914,7915],{},"发往账户内“已验证目标地址”的邮件始终免费，不计入包含额度；",[42,7917,7918],{},"邮件被 Email Service 接受后，即使随后发生硬退信，通常也会计入额度；在 API 边界直接拒绝的请求不会计入。",[16,7920,7921],{},"价格和 Beta 状态都可能继续调整，真正上线前应重新检查官方价格页，而不是把本文数字长期写死在预算模型里。",[78,7923,7925],{"id":7924},"第一步正确接入域名","第一步：正确接入域名",[16,7927,7928],{},"Email Service 要求域名使用 Cloudflare DNS。发送与接收会创建两组用途不同的 DNS 记录。",[576,7930,7931],{"id":7931},"接收邮件使用根域记录",[16,7933,7934],{},"启用 Email Routing 后，Cloudflare 会在域名根节点配置：",[39,7936,7937,7940,7943],{},[42,7938,7939],{},"MX：把来信送到 Cloudflare 的邮件入口；",[42,7941,7942],{},"SPF：授权 Cloudflare 转发邮件；",[42,7944,7945],{},"DKIM：为转发链路提供签名。",[16,7947,7948],{},"这些记录负责的是“别人发到你的域名”。",[576,7950,7952,7953,7956],{"id":7951},"发送邮件使用-cf-bounce-记录","发送邮件使用 ",[52,7954,7955],{},"cf-bounce"," 记录",[16,7958,7959,7960,7963],{},"启用 Email Sending 时，Cloudflare 会为 ",[52,7961,7962],{},"cf-bounce.yourdomain.com"," 配置：",[39,7965,7966,7969,7972,7975],{},[42,7967,7968],{},"MX：处理退信；",[42,7970,7971],{},"SPF：授权 Cloudflare 代域名发送；",[42,7973,7974],{},"DKIM：验证邮件内容未在途中被修改；",[42,7976,7977,7980],{},[52,7978,7979],{},"_dmarc.yourdomain.com","：声明认证失败时的处理策略。",[16,7982,7983],{},"发送和接收使用不同的 DKIM selector。看到两条 DKIM 记录并不意味着重复配置。",[16,7985,7986,7987,7990,7991,131],{},"如果域名已有 Google Workspace、Microsoft 365 或其他邮件服务，尤其要检查 MX 和 SPF 是否冲突。一个域名只能有一条有效 SPF 策略；需要同时授权多个服务时，应合并 ",[52,7988,7989],{},"include","，而不是再创建第二条 ",[52,7992,7993],{},"v=spf1",[16,7995,7996],{},"我的建议是把不同信誉用途拆到子域：",[193,7998,8001],{"className":7999,"code":8000,"language":198,"meta":199},[196],"noreply@example.com          账户验证码与密码重置\nnotify@mail.example.com      产品通知\nsupport@example.com          人工支持与来信处理\n",[52,8002,8000],{"__ignoreMap":199},[16,8004,8005],{},"事务通知、人工往来与未来可能出现的批量邮件互不拖累，故障和迁移边界也更清晰。",[78,8007,8009],{"id":8008},"接收邮件从转发规则到-email-worker","接收邮件：从转发规则到 Email Worker",[16,8011,8012],{},"最简单的接收方式完全不用代码：",[136,8014,8015,8018,8022,8025],{},[42,8016,8017],{},"在 Email Routing 中添加一个经过验证的目标邮箱；",[42,8019,6370,8020,55],{},[52,8021,7880],{},[42,8023,8024],{},"选择“发送到邮箱”；",[42,8026,8027],{},"来信会被 Cloudflare 转发到目标地址。",[16,8029,8030],{},"如果需要按主题分类、提取附件、建立工单或自动回复，就把动作改为“发送到 Worker”。",[576,8032,8034,8037],{"id":8033},"email-是入站邮件入口",[52,8035,8036],{},"email()"," 是入站邮件入口",[16,8039,8040,8041,8044,8045,832],{},"Email Worker 的入口不是 ",[52,8042,8043],{},"fetch()","，而是 ",[52,8046,8036],{},[193,8048,8050],{"className":6582,"code":8049,"language":6584,"meta":199,"style":199},"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",[52,8051,8052,8060,8070,8074,8078,8087,8122,8141,8170,8174,8200,8215,8220,8224,8228,8243,8258,8288,8297,8302,8307,8311,8330,8334,8338,8342,8356,8360],{"__ignoreMap":199},[291,8053,8054,8056,8058],{"class":293,"line":294},[291,8055,7109],{"class":409},[291,8057,7112],{"class":297},[291,8059,7115],{"class":553},[291,8061,8062,8065,8067],{"class":293,"line":308},[291,8063,8064],{"class":6919},"  SUPPORT_FORWARD_TO",[291,8066,6922],{"class":409},[291,8068,8069],{"class":318}," string\n",[291,8071,8072],{"class":293,"line":328},[291,8073,5305],{"class":553},[291,8075,8076],{"class":293,"line":335},[291,8077,332],{"emptyLinePlaceholder":331},[291,8079,8080,8082,8085],{"class":293,"line":354},[291,8081,6402],{"class":409},[291,8083,8084],{"class":409}," default",[291,8086,7115],{"class":553},[291,8088,8089,8092,8095,8097,8100,8102,8105,8108,8110,8113,8116,8119],{"class":293,"line":371},[291,8090,8091],{"class":409},"  async",[291,8093,8094],{"class":297}," email",[291,8096,6808],{"class":553},[291,8098,8099],{"class":6919},"message",[291,8101,6928],{"class":553},[291,8103,8104],{"class":6919},"env",[291,8106,8107],{"class":553},")",[291,8109,6922],{"class":409},[291,8111,8112],{"class":297}," Promise",[291,8114,8115],{"class":553},"\u003C",[291,8117,8118],{"class":318},"void",[291,8120,8121],{"class":553},"> {\n",[291,8123,8124,8127,8130,8132,8135,8138],{"class":293,"line":380},[291,8125,8126],{"class":409},"    const",[291,8128,8129],{"class":318}," recipient",[291,8131,6615],{"class":409},[291,8133,8134],{"class":553}," message.to.",[291,8136,8137],{"class":297},"toLowerCase",[291,8139,8140],{"class":553},"()\n",[291,8142,8143,8145,8148,8150,8153,8156,8158,8161,8164,8167],{"class":293,"line":393},[291,8144,8126],{"class":409},[291,8146,8147],{"class":318}," subject",[291,8149,6615],{"class":409},[291,8151,8152],{"class":553}," message.headers.",[291,8154,8155],{"class":297},"get",[291,8157,6808],{"class":553},[291,8159,8160],{"class":301},"'subject'",[291,8162,8163],{"class":553},") ",[291,8165,8166],{"class":409},"??",[291,8168,8169],{"class":301}," ''\n",[291,8171,8172],{"class":293,"line":398},[291,8173,332],{"emptyLinePlaceholder":331},[291,8175,8176,8179,8182,8185,8188,8191,8194,8196,8198],{"class":293,"line":422},[291,8177,8178],{"class":409},"    if",[291,8180,8181],{"class":553}," (message.rawSize ",[291,8183,8184],{"class":409},">",[291,8186,8187],{"class":318}," 10",[291,8189,8190],{"class":409}," *",[291,8192,8193],{"class":318}," 1024",[291,8195,8190],{"class":409},[291,8197,8193],{"class":318},[291,8199,6938],{"class":553},[291,8201,8202,8205,8208,8210,8213],{"class":293,"line":428},[291,8203,8204],{"class":553},"      message.",[291,8206,8207],{"class":297},"setReject",[291,8209,6808],{"class":553},[291,8211,8212],{"class":301},"'Message is too large for this mailbox'",[291,8214,6976],{"class":553},[291,8216,8217],{"class":293,"line":434},[291,8218,8219],{"class":409},"      return\n",[291,8221,8222],{"class":293,"line":440},[291,8223,5295],{"class":553},[291,8225,8226],{"class":293,"line":446},[291,8227,332],{"emptyLinePlaceholder":331},[291,8229,8230,8232,8235,8238,8241],{"class":293,"line":452},[291,8231,8178],{"class":409},[291,8233,8234],{"class":553}," (recipient ",[291,8236,8237],{"class":409},"===",[291,8239,8240],{"class":301}," 'support@example.com'",[291,8242,6938],{"class":553},[291,8244,8245,8248,8251,8253,8256],{"class":293,"line":458},[291,8246,8247],{"class":553},"      console.",[291,8249,8250],{"class":297},"log",[291,8252,6808],{"class":553},[291,8254,8255],{"class":301},"'Support email accepted'",[291,8257,6814],{"class":553},[291,8259,8260,8263,8266,8268,8271,8274,8277,8279,8282,8285],{"class":293,"line":464},[291,8261,8262],{"class":553},"        fromDomain: message.from.",[291,8264,8265],{"class":297},"split",[291,8267,6808],{"class":553},[291,8269,8270],{"class":301},"'@'",[291,8272,8273],{"class":553},").",[291,8275,8276],{"class":297},"at",[291,8278,6808],{"class":553},[291,8280,8281],{"class":409},"-",[291,8283,8284],{"class":318},"1",[291,8286,8287],{"class":553},"),\n",[291,8289,8290,8293,8295],{"class":293,"line":469},[291,8291,8292],{"class":553},"        subjectLength: subject.",[291,8294,6827],{"class":318},[291,8296,5177],{"class":553},[291,8298,8299],{"class":293,"line":478},[291,8300,8301],{"class":553},"        rawSize: message.rawSize,\n",[291,8303,8304],{"class":293,"line":491},[291,8305,8306],{"class":553},"      })\n",[291,8308,8309],{"class":293,"line":499},[291,8310,332],{"emptyLinePlaceholder":331},[291,8312,8313,8316,8319,8322,8325,8328],{"class":293,"line":507},[291,8314,8315],{"class":409},"      await",[291,8317,8318],{"class":553}," message.",[291,8320,8321],{"class":297},"forward",[291,8323,8324],{"class":553},"(env.",[291,8326,8327],{"class":318},"SUPPORT_FORWARD_TO",[291,8329,6976],{"class":553},[291,8331,8332],{"class":293,"line":515},[291,8333,8219],{"class":409},[291,8335,8336],{"class":293,"line":523},[291,8337,5295],{"class":553},[291,8339,8340],{"class":293,"line":1049},[291,8341,332],{"emptyLinePlaceholder":331},[291,8343,8344,8347,8349,8351,8354],{"class":293,"line":1059},[291,8345,8346],{"class":553},"    message.",[291,8348,8207],{"class":297},[291,8350,6808],{"class":553},[291,8352,8353],{"class":301},"'Unknown recipient'",[291,8355,6976],{"class":553},[291,8357,8358],{"class":293,"line":1064},[291,8359,6681],{"class":553},[291,8361,8362,8365,8368,8371,8373,8376],{"class":293,"line":1072},[291,8363,8364],{"class":553},"} ",[291,8366,8367],{"class":409},"satisfies",[291,8369,8370],{"class":297}," ExportedHandler",[291,8372,8115],{"class":553},[291,8374,8375],{"class":297},"Env",[291,8377,8378],{"class":553},">\n",[16,8380,8381,8382,8384,8385,8388],{},"其中 ",[52,8383,8099],{}," 是 ",[52,8386,8387],{},"ForwardableEmailMessage","，常用字段和操作包括：",[1376,8390,8391,8400],{},[1379,8392,8393],{},[1382,8394,8395,8398],{},[1385,8396,8397],{},"能力",[1385,8399,1396],{},[1398,8401,8402,8412,8422,8432,8442,8452,8462,8472],{},[1382,8403,8404,8409],{},[1403,8405,8406],{},[52,8407,8408],{},"message.from",[1403,8410,8411],{},"SMTP envelope 的发件人",[1382,8413,8414,8419],{},[1403,8415,8416],{},[52,8417,8418],{},"message.to",[1403,8420,8421],{},"SMTP envelope 的收件人",[1382,8423,8424,8429],{},[1403,8425,8426],{},[52,8427,8428],{},"message.headers",[1403,8430,8431],{},"读取 Subject、Message-ID 等邮件头",[1382,8433,8434,8439],{},[1403,8435,8436],{},[52,8437,8438],{},"message.raw",[1403,8440,8441],{},"原始 MIME 数据流",[1382,8443,8444,8449],{},[1403,8445,8446],{},[52,8447,8448],{},"message.rawSize",[1403,8450,8451],{},"原始邮件大小",[1382,8453,8454,8459],{},[1403,8455,8456],{},[52,8457,8458],{},"message.forward(address)",[1403,8460,8461],{},"转发到已验证目标地址",[1382,8463,8464,8469],{},[1403,8465,8466],{},[52,8467,8468],{},"message.reply(email)",[1403,8470,8471],{},"在约束范围内回复原发件人",[1382,8473,8474,8479],{},[1403,8475,8476],{},[52,8477,8478],{},"message.setReject(reason)",[1403,8480,8481],{},"返回永久 SMTP 拒收",[16,8483,8484,8485,6518,8487,8489,8490,6518,8493,8496],{},"这里有一个容易忽略的安全点：",[52,8486,8408],{},[52,8488,8418],{}," 是 SMTP envelope，邮件正文头部中的 ",[52,8491,8492],{},"From:",[52,8494,8495],{},"To:"," 则可能是另一组值。做授权、路由与审计时，不要把显示给用户看的头部地址误当成可信身份。",[576,8498,8500],{"id":8499},"需要正文时再解析-mime","需要正文时再解析 MIME",[16,8502,8503,8504,832],{},"只做转发或按地址路由时，没有必要完整解析邮件。要提取正文和附件，可以使用 ",[52,8505,8506],{},"postal-mime",[193,8508,8510],{"className":6582,"code":8509,"language":6584,"meta":199,"style":199},"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",[52,8511,8512,8524,8528,8536,8558,8578,8582,8591,8596,8610,8619,8624,8628],{"__ignoreMap":199},[291,8513,8514,8516,8519,8521],{"class":293,"line":294},[291,8515,6591],{"class":409},[291,8517,8518],{"class":553}," PostalMime ",[291,8520,6597],{"class":409},[291,8522,8523],{"class":301}," 'postal-mime'\n",[291,8525,8526],{"class":293,"line":308},[291,8527,332],{"emptyLinePlaceholder":331},[291,8529,8530,8532,8534],{"class":293,"line":328},[291,8531,6402],{"class":409},[291,8533,8084],{"class":409},[291,8535,7115],{"class":553},[291,8537,8538,8540,8542,8544,8546,8548,8550,8552,8554,8556],{"class":293,"line":335},[291,8539,8091],{"class":409},[291,8541,8094],{"class":297},[291,8543,6808],{"class":553},[291,8545,8099],{"class":6919},[291,8547,8107],{"class":553},[291,8549,6922],{"class":409},[291,8551,8112],{"class":297},[291,8553,8115],{"class":553},[291,8555,8118],{"class":318},[291,8557,8121],{"class":553},[291,8559,8560,8562,8565,8567,8569,8572,8575],{"class":293,"line":354},[291,8561,8126],{"class":409},[291,8563,8564],{"class":318}," parsed",[291,8566,6615],{"class":409},[291,8568,6731],{"class":409},[291,8570,8571],{"class":553}," PostalMime.",[291,8573,8574],{"class":297},"parse",[291,8576,8577],{"class":553},"(message.raw)\n",[291,8579,8580],{"class":293,"line":371},[291,8581,332],{"emptyLinePlaceholder":331},[291,8583,8584,8587,8589],{"class":293,"line":380},[291,8585,8586],{"class":553},"    console.",[291,8588,8250],{"class":297},[291,8590,6624],{"class":553},[291,8592,8593],{"class":293,"line":393},[291,8594,8595],{"class":553},"      subject: parsed.subject,\n",[291,8597,8598,8601,8603,8606,8608],{"class":293,"line":398},[291,8599,8600],{"class":553},"      textLength: parsed.text?.",[291,8602,6827],{"class":318},[291,8604,8605],{"class":409}," ??",[291,8607,1720],{"class":318},[291,8609,5177],{"class":553},[291,8611,8612,8615,8617],{"class":293,"line":422},[291,8613,8614],{"class":553},"      attachmentCount: parsed.attachments.",[291,8616,6827],{"class":318},[291,8618,5177],{"class":553},[291,8620,8621],{"class":293,"line":428},[291,8622,8623],{"class":553},"    })\n",[291,8625,8626],{"class":293,"line":434},[291,8627,6681],{"class":553},[291,8629,8630,8632,8634],{"class":293,"line":440},[291,8631,8364],{"class":553},[291,8633,8367],{"class":409},[291,8635,8636],{"class":297}," ExportedHandler\n",[16,8638,8639],{},"解析后的 HTML、文件名、Content-Type 与附件都来自外部输入。正确做法是：",[39,8641,8642,8645,8648,8651,8654],{},[42,8643,8644],{},"不在管理页面直接插入来信 HTML；",[42,8646,8647],{},"限制正文、附件数量和解压后大小；",[42,8649,8650],{},"文件写入 R2 前重新生成对象键，不使用原始文件名作为路径；",[42,8652,8653],{},"不让来信内容直接组成 SQL、Shell、模板或 AI 工具参数；",[42,8655,8656],{},"大任务交给 Queue，不要把杀毒、OCR、模型调用全塞进入站事件的同步 CPU 时间。",[576,8658,8660],{"id":8659},"转发回复拒绝与静默丢弃的区别","转发、回复、拒绝与静默丢弃的区别",[39,8662,8663,8669,8675,8681],{},[42,8664,8665,8668],{},[20,8666,8667],{},"转发","：适合人工邮箱继续处理，目标地址必须提前验证；",[42,8670,8671,8674],{},[20,8672,8673],{},"回复","：适合自动确认或邮件机器人，但有额外限制；",[42,8676,8677,8680],{},[20,8678,8679],{},"拒绝","：在 SMTP 阶段明确告诉发送方没有接收；",[42,8682,8683,8686],{},[20,8684,8685],{},"丢弃","：规则匹配后不继续处理，也不给发送方业务层反馈。",[16,8688,8689],{},"自动回复不能被当成任意发送接口。官方当前要求：",[39,8691,8692,8695,8698,8701,8704],{},[42,8693,8694],{},"来信必须有有效 DMARC 结果；",[42,8696,8697],{},"每个入站事件只能调用一次回复；",[42,8699,8700],{},"回复收件人必须是原始来信的发件人；",[42,8702,8703],{},"回复使用的发件域必须与接收邮件的域一致；",[42,8705,8706,8709],{},[52,8707,8708],{},"References"," 超过 100 项时会拒绝回复，以避免邮件环路。",[16,8711,8712],{},"这组限制反而是一件好事：它让“回复这封邮件”和“向任意地址发一封新邮件”成为两种不同权限。",[78,8714,8716],{"id":8715},"发送邮件优先使用-workers-binding","发送邮件：优先使用 Workers binding",[16,8718,8719],{},"如果应用本身运行在 Cloudflare Workers，Workers binding 是最自然的选择。它不需要在业务代码里保存 Cloudflare API Token。",[16,8721,8722,8723,8726],{},"先在 ",[52,8724,8725],{},"wrangler.jsonc"," 中声明绑定：",[193,8728,8732],{"className":8729,"code":8730,"language":8731,"meta":199,"style":199},"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",[52,8733,8734,8738,8743,8748,8753,8758,8763,8768,8772,8777,8782,8787,8791],{"__ignoreMap":199},[291,8735,8736],{"class":293,"line":294},[291,8737,5164],{},[291,8739,8740],{"class":293,"line":308},[291,8741,8742],{},"  \"send_email\": [\n",[291,8744,8745],{"class":293,"line":328},[291,8746,8747],{},"    {\n",[291,8749,8750],{"class":293,"line":335},[291,8751,8752],{},"      \"name\": \"EMAIL\",\n",[291,8754,8755],{"class":293,"line":354},[291,8756,8757],{},"      \"allowed_sender_addresses\": [\n",[291,8759,8760],{"class":293,"line":371},[291,8761,8762],{},"        \"noreply@example.com\"\n",[291,8764,8765],{"class":293,"line":380},[291,8766,8767],{},"      ]\n",[291,8769,8770],{"class":293,"line":393},[291,8771,5295],{},[291,8773,8774],{"class":293,"line":398},[291,8775,8776],{},"  ],\n",[291,8778,8779],{"class":293,"line":422},[291,8780,8781],{},"  \"vars\": {\n",[291,8783,8784],{"class":293,"line":428},[291,8785,8786],{},"    \"EMAIL_FROM\": \"noreply@example.com\"\n",[291,8788,8789],{"class":293,"line":434},[291,8790,5300],{},[291,8792,8793],{"class":293,"line":440},[291,8794,5305],{},[16,8796,8797,8800],{},[52,8798,8799],{},"allowed_sender_addresses"," 不只是文档，它是运行时权限边界。即使某个接口被误用，这个绑定也不能伪造其他发件地址。",[16,8802,8803],{},"还可以用以下配置进一步约束收件人：",[39,8805,8806,8812,8818],{},[42,8807,8808,8811],{},[52,8809,8810],{},"destination_address","：固定为一个目标地址；",[42,8813,8814,8817],{},[52,8815,8816],{},"allowed_destination_addresses","：只允许一组目标地址；",[42,8819,8820,8822],{},[52,8821,8799],{},"：限制可使用的发件地址。",[16,8824,8825,8826,8828],{},"用于系统告警的 Worker 最适合固定 ",[52,8827,8810],{},"；用于用户验证码的服务则通常限制发件人，并在应用层校验收件人和频率。",[576,8830,8832],{"id":8831},"在-worker-中发送","在 Worker 中发送",[193,8834,8836],{"className":6582,"code":8835,"language":6584,"meta":199,"style":199},"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",[52,8837,8838,8846,8854,8858,8862,8870,8899,8920,8925,8935,8945,8949,8959,8969,8979,8989,8993,8997,9009,9014,9018,9022],{"__ignoreMap":199},[291,8839,8840,8842,8844],{"class":293,"line":294},[291,8841,7109],{"class":409},[291,8843,7112],{"class":297},[291,8845,7115],{"class":553},[291,8847,8848,8850,8852],{"class":293,"line":308},[291,8849,7120],{"class":6919},[291,8851,6922],{"class":409},[291,8853,7125],{"class":297},[291,8855,8856],{"class":293,"line":328},[291,8857,5305],{"class":553},[291,8859,8860],{"class":293,"line":335},[291,8861,332],{"emptyLinePlaceholder":331},[291,8863,8864,8866,8868],{"class":293,"line":354},[291,8865,6402],{"class":409},[291,8867,8084],{"class":409},[291,8869,7115],{"class":553},[291,8871,8872,8874,8877,8879,8882,8884,8886,8888,8890,8892,8894,8897],{"class":293,"line":371},[291,8873,8091],{"class":409},[291,8875,8876],{"class":297}," fetch",[291,8878,6808],{"class":553},[291,8880,8881],{"class":6919},"_request",[291,8883,6928],{"class":553},[291,8885,8104],{"class":6919},[291,8887,8107],{"class":553},[291,8889,6922],{"class":409},[291,8891,8112],{"class":297},[291,8893,8115],{"class":553},[291,8895,8896],{"class":297},"Response",[291,8898,8121],{"class":553},[291,8900,8901,8903,8906,8908,8910,8912,8914,8916,8918],{"class":293,"line":380},[291,8902,8126],{"class":409},[291,8904,8905],{"class":318}," result",[291,8907,6615],{"class":409},[291,8909,6731],{"class":409},[291,8911,7141],{"class":553},[291,8913,7144],{"class":318},[291,8915,547],{"class":553},[291,8917,7149],{"class":297},[291,8919,6624],{"class":553},[291,8921,8922],{"class":293,"line":393},[291,8923,8924],{"class":553},"      from: {\n",[291,8926,8927,8930,8933],{"class":293,"line":398},[291,8928,8929],{"class":553},"        email: ",[291,8931,8932],{"class":301},"'noreply@example.com'",[291,8934,5177],{"class":553},[291,8936,8937,8940,8943],{"class":293,"line":422},[291,8938,8939],{"class":553},"        name: ",[291,8941,8942],{"class":301},"'Example App'",[291,8944,5177],{"class":553},[291,8946,8947],{"class":293,"line":428},[291,8948,5281],{"class":553},[291,8950,8951,8954,8957],{"class":293,"line":434},[291,8952,8953],{"class":553},"      to: ",[291,8955,8956],{"class":301},"'reader@example.net'",[291,8958,5177],{"class":553},[291,8960,8961,8964,8967],{"class":293,"line":440},[291,8962,8963],{"class":553},"      subject: ",[291,8965,8966],{"class":301},"'验证你的邮箱'",[291,8968,5177],{"class":553},[291,8970,8971,8974,8977],{"class":293,"line":446},[291,8972,8973],{"class":553},"      text: ",[291,8975,8976],{"class":301},"'你的验证码是 482913，5 分钟内有效。'",[291,8978,5177],{"class":553},[291,8980,8981,8984,8987],{"class":293,"line":452},[291,8982,8983],{"class":553},"      html: ",[291,8985,8986],{"class":301},"'\u003Cp>你的验证码是 \u003Cstrong>482913\u003C\u002Fstrong>，5 分钟内有效。\u003C\u002Fp>'",[291,8988,5177],{"class":553},[291,8990,8991],{"class":293,"line":458},[291,8992,8623],{"class":553},[291,8994,8995],{"class":293,"line":464},[291,8996,332],{"emptyLinePlaceholder":331},[291,8998,8999,9002,9005,9007],{"class":293,"line":469},[291,9000,9001],{"class":409},"    return",[291,9003,9004],{"class":553}," Response.",[291,9006,5157],{"class":297},[291,9008,6624],{"class":553},[291,9010,9011],{"class":293,"line":478},[291,9012,9013],{"class":553},"      messageId: result.messageId,\n",[291,9015,9016],{"class":293,"line":491},[291,9017,8623],{"class":553},[291,9019,9020],{"class":293,"line":499},[291,9021,6681],{"class":553},[291,9023,9024,9026,9028,9030,9032,9034],{"class":293,"line":507},[291,9025,8364],{"class":553},[291,9027,8367],{"class":409},[291,9029,8370],{"class":297},[291,9031,8115],{"class":553},[291,9033,8375],{"class":297},[291,9035,8378],{"class":553},[16,9037,9038,9039,9041],{},"Workers binding 成功时返回一个 ",[52,9040,7826],{},"。这个结果代表 Cloudflare 接受了发送请求，不应该被解释为用户一定已经在收件箱里看到邮件。最终投递、延迟、退信和抑制状态还要结合 Email Service 日志与指标。",[576,9043,9045],{"id":9044},"在-nuxt-服务端-api-中发送","在 Nuxt 服务端 API 中发送",[16,9047,9048],{},"Nuxt 部署到 Workers 后，同样可以从服务端路由读取 binding：",[193,9050,9052],{"className":6582,"code":9051,"language":6584,"meta":199,"style":199},"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",[52,9053,9054,9066,9070,9087,9118,9122,9126,9152,9182,9198,9207,9216,9220,9224,9230,9235,9255,9260,9270,9279,9284,9289,9299,9309,9319,9324,9328,9335,9344,9350,9355],{"__ignoreMap":199},[291,9055,9056,9058,9061,9063],{"class":293,"line":294},[291,9057,6591],{"class":409},[291,9059,9060],{"class":553}," { z } ",[291,9062,6597],{"class":409},[291,9064,9065],{"class":301}," 'zod'\n",[291,9067,9068],{"class":293,"line":308},[291,9069,332],{"emptyLinePlaceholder":331},[291,9071,9072,9074,9077,9079,9082,9085],{"class":293,"line":328},[291,9073,6609],{"class":409},[291,9075,9076],{"class":318}," schema",[291,9078,6615],{"class":409},[291,9080,9081],{"class":553}," z.",[291,9083,9084],{"class":297},"object",[291,9086,6624],{"class":553},[291,9088,9089,9092,9095,9098,9101,9103,9106,9108,9111,9113,9116],{"class":293,"line":335},[291,9090,9091],{"class":553},"  email: z.",[291,9093,9094],{"class":297},"string",[291,9096,9097],{"class":553},"().",[291,9099,9100],{"class":297},"trim",[291,9102,9097],{"class":553},[291,9104,9105],{"class":297},"email",[291,9107,9097],{"class":553},[291,9109,9110],{"class":297},"max",[291,9112,6808],{"class":553},[291,9114,9115],{"class":318},"254",[291,9117,8287],{"class":553},[291,9119,9120],{"class":293,"line":354},[291,9121,6715],{"class":553},[291,9123,9124],{"class":293,"line":371},[291,9125,332],{"emptyLinePlaceholder":331},[291,9127,9128,9130,9132,9135,9137,9140,9142,9145,9147,9150],{"class":293,"line":380},[291,9129,6402],{"class":409},[291,9131,8084],{"class":409},[291,9133,9134],{"class":297}," defineEventHandler",[291,9136,6808],{"class":553},[291,9138,9139],{"class":409},"async",[291,9141,6946],{"class":553},[291,9143,9144],{"class":6919},"event",[291,9146,8163],{"class":553},[291,9148,9149],{"class":409},"=>",[291,9151,7115],{"class":553},[291,9153,9154,9157,9160,9162,9165,9167,9170,9172,9174,9176,9179],{"class":293,"line":393},[291,9155,9156],{"class":409},"  const",[291,9158,9159],{"class":553}," { ",[291,9161,9105],{"class":318},[291,9163,9164],{"class":553}," } ",[291,9166,602],{"class":409},[291,9168,9169],{"class":553}," schema.",[291,9171,8574],{"class":297},[291,9173,6808],{"class":553},[291,9175,7138],{"class":409},[291,9177,9178],{"class":297}," readBody",[291,9180,9181],{"class":553},"(event))\n",[291,9183,9184,9186,9188,9190,9193,9196],{"class":293,"line":398},[291,9185,9156],{"class":409},[291,9187,654],{"class":318},[291,9189,6615],{"class":409},[291,9191,9192],{"class":553}," event.context.cloudflare.env ",[291,9194,9195],{"class":409},"as",[291,9197,7115],{"class":553},[291,9199,9200,9203,9205],{"class":293,"line":422},[291,9201,9202],{"class":6919},"    EMAIL",[291,9204,6922],{"class":409},[291,9206,7125],{"class":297},[291,9208,9209,9212,9214],{"class":293,"line":428},[291,9210,9211],{"class":6919},"    EMAIL_FROM",[291,9213,6922],{"class":409},[291,9215,8069],{"class":318},[291,9217,9218],{"class":293,"line":434},[291,9219,5300],{"class":553},[291,9221,9222],{"class":293,"line":440},[291,9223,332],{"emptyLinePlaceholder":331},[291,9225,9226],{"class":293,"line":446},[291,9227,9229],{"class":9228},"sJ8bj","  \u002F\u002F 实际项目还应在这里执行同源校验、IP\u002F邮箱限流，\n",[291,9231,9232],{"class":293,"line":452},[291,9233,9234],{"class":9228},"  \u002F\u002F 并在 D1 中原子领取一次发送资格。\n",[291,9236,9237,9239,9241,9243,9245,9247,9249,9251,9253],{"class":293,"line":458},[291,9238,9156],{"class":409},[291,9240,8905],{"class":318},[291,9242,6615],{"class":409},[291,9244,6731],{"class":409},[291,9246,7141],{"class":553},[291,9248,7144],{"class":318},[291,9250,547],{"class":553},[291,9252,7149],{"class":297},[291,9254,6624],{"class":553},[291,9256,9257],{"class":293,"line":464},[291,9258,9259],{"class":553},"    from: {\n",[291,9261,9262,9265,9268],{"class":293,"line":469},[291,9263,9264],{"class":553},"      email: env.",[291,9266,9267],{"class":318},"EMAIL_FROM",[291,9269,5177],{"class":553},[291,9271,9272,9275,9277],{"class":293,"line":478},[291,9273,9274],{"class":553},"      name: ",[291,9276,8942],{"class":301},[291,9278,5177],{"class":553},[291,9280,9281],{"class":293,"line":491},[291,9282,9283],{"class":553},"    },\n",[291,9285,9286],{"class":293,"line":499},[291,9287,9288],{"class":553},"    to: email,\n",[291,9290,9291,9294,9297],{"class":293,"line":507},[291,9292,9293],{"class":553},"    subject: ",[291,9295,9296],{"class":301},"'验证邮箱'",[291,9298,5177],{"class":553},[291,9300,9301,9304,9307],{"class":293,"line":515},[291,9302,9303],{"class":553},"    text: ",[291,9305,9306],{"class":301},"'验证码将在 5 分钟后失效。'",[291,9308,5177],{"class":553},[291,9310,9311,9314,9317],{"class":293,"line":523},[291,9312,9313],{"class":553},"    html: ",[291,9315,9316],{"class":301},"'\u003Cp>验证码将在 \u003Cstrong>5 分钟\u003C\u002Fstrong>后失效。\u003C\u002Fp>'",[291,9318,5177],{"class":553},[291,9320,9321],{"class":293,"line":1049},[291,9322,9323],{"class":553},"  })\n",[291,9325,9326],{"class":293,"line":1059},[291,9327,332],{"emptyLinePlaceholder":331},[291,9329,9330,9333],{"class":293,"line":1064},[291,9331,9332],{"class":409},"  return",[291,9334,7115],{"class":553},[291,9336,9337,9340,9342],{"class":293,"line":1072},[291,9338,9339],{"class":553},"    ok: ",[291,9341,5199],{"class":318},[291,9343,5177],{"class":553},[291,9345,9347],{"class":293,"line":9346},29,[291,9348,9349],{"class":553},"    messageId: result.messageId,\n",[291,9351,9353],{"class":293,"line":9352},30,[291,9354,5300],{"class":553},[291,9356,9358],{"class":293,"line":9357},31,[291,9359,6715],{"class":553},[16,9361,9362],{},"不要把这个示例直接变成一个匿名、无限频率、可自由填写主题和正文的公开 API。那会把自己的域名变成垃圾邮件跳板。",[16,9364,9365],{},[782,9366],{"alt":9367,"src":9368},"事务邮件从 Worker 生成，经过多层身份认证后投递到不同收件系统","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Foutbound-delivery-v1.webp",[16,9370,9371],{},[789,9372,9373],{},"SPF、DKIM 与 DMARC 解决的是“谁有权发送、内容是否被修改、失败时如何处置”，并不保证每封邮件都进入主收件箱。",[78,9375,9377],{"id":9376},"rest-api-与认证-smtp-什么时候更合适","REST API 与认证 SMTP 什么时候更合适",[16,9379,9380],{},"Cloudflare 目前提供三种发送入口：",[1376,9382,9383,9396],{},[1379,9384,9385],{},[1382,9386,9387,9390,9393],{},[1385,9388,9389],{},"方式",[1385,9391,9392],{},"最适合",[1385,9394,9395],{},"凭证与边界",[1398,9397,9398,9409,9419],{},[1382,9399,9400,9403,9406],{},[1403,9401,9402],{},"Workers binding",[1403,9404,9405],{},"已运行在 Workers 的 Nuxt、Hono、Agent",[1403,9407,9408],{},"不在代码中保存 API Token，可通过 binding 限制发件人与收件人",[1382,9410,9411,9413,9416],{},[1403,9412,7230],{},[1403,9414,9415],{},"运行在其他云、CI 或后端平台的程序",[1403,9417,9418],{},"使用最小权限 API Token，响应会按收件人区分 delivered、queued、permanent bounce",[1382,9420,9421,9423,9426],{},[1403,9422,7238],{},[1403,9424,9425],{},"已经使用 Nodemailer、PHPMailer、JavaMail 等 SMTP 客户端的旧系统",[1403,9427,9428,9430],{},[52,9429,6145],{},"，仅隐式 TLS，密码是有 Email Sending 权限的 API Token",[16,9432,9433],{},"认证 SMTP 是 2026 年 6 月加入的 Beta 能力。当前只支持 465 端口的 SMTPS，不提供 587 端口 STARTTLS，也不是可以匿名使用的 25 端口 relay。",[16,9435,9436],{},"SMTP Token 一旦泄漏，持有者可以使用账户中已接入的发件域发送邮件。因此：",[39,9438,9439,9442,9447,9450,9453],{},[42,9440,9441],{},"优先使用 account-owned token；",[42,9443,9444,9445,55],{},"权限只给 ",[52,9446,6231],{},[42,9448,9449],{},"不写进仓库、镜像或前端环境变量；",[42,9451,9452],{},"不同系统使用不同 Token，便于单独吊销；",[42,9454,9455],{},"能用 Worker binding 的场景，不要为了“熟悉 SMTP”额外引入长期凭证。",[78,9457,9458],{"id":9458},"必须知道的限制",[16,9460,9461],{},"截至 2026 年 7 月，官方平台限制中最值得在代码里提前防守的是：",[1376,9463,9464,9478],{},[1379,9465,9466],{},[1382,9467,9468,9471,9475],{},[1385,9469,9470],{},"限制",[1385,9472,9474],{"align":9473},"right","当前值",[1385,9476,9477],{},"实现影响",[1398,9479,9480,9491,9501,9512,9523,9534,9544,9555,9566],{},[1382,9481,9482,9485,9488],{},[1403,9483,9484],{},"每域 Email Routing 规则",[1403,9486,9487],{"align":9473},"200",[1403,9489,9490],{},"大量租户不要为每个人创建独立静态规则",[1382,9492,9493,9496,9498],{},[1403,9494,9495],{},"每账户已验证目标地址",[1403,9497,9487],{"align":9473},[1403,9499,9500],{},"转发到多邮箱前先规划账户级目标",[1382,9502,9503,9506,9509],{},[1403,9504,9505],{},"入站邮件大小",[1403,9507,9508],{"align":9473},"25 MiB",[1403,9510,9511],{},"更大邮件在 SMTP 阶段被拒绝",[1382,9513,9514,9517,9520],{},[1403,9515,9516],{},"单封出站收件人数",[1403,9518,9519],{"align":9473},"50",[1403,9521,9522],{},"To、Cc、Bcc 合计",[1382,9524,9525,9528,9531],{},[1403,9526,9527],{},"普通出站邮件总大小",[1403,9529,9530],{"align":9473},"5 MiB",[1403,9532,9533],{},"包含附件与 MIME 编码开销",[1382,9535,9536,9539,9541],{},[1403,9537,9538],{},"发往已验证目标的总大小",[1403,9540,9508],{"align":9473},[1403,9542,9543],{},"仅适用于已验证目标地址",[1382,9545,9546,9549,9552],{},[1403,9547,9548],{},"自定义头总大小",[1403,9550,9551],{"align":9473},"16 KB",[1403,9553,9554],{},"不要把业务数据塞进 Header",[1382,9556,9557,9560,9563],{},[1403,9558,9559],{},"单个 Zone 的发送\u002F路由域名总数",[1403,9561,9562],{"align":9473},"30",[1403,9564,9565],{},"根域与子域都计入",[1382,9567,9568,9574,9577],{},[1403,9569,9570,9571,9573],{},"回复的 ",[52,9572,8708],{}," 项数",[1403,9575,9576],{"align":9473},"100",[1403,9578,9579,9580,9583],{},"超过后 ",[52,9581,9582],{},"reply()"," 会失败",[16,9585,9586],{},"附件会经过 Base64 和 MIME 封装。3 MiB 的原始附件不等于最终邮件仍是 3 MiB，所以应用限制应给编码、正文和邮件头预留空间。",[16,9588,9589],{},"平台还会根据账户信誉、投递表现和使用历史调整每日发送额度。不要假设“每月额度未用完”就等于某一秒可以无限并发发送。",[78,9591,9592],{"id":9592},"验证码与密码重置应该怎样做",[16,9594,9595],{},"本博客的注册和密码重置邮件采用的是下面这条链路：",[193,9597,9600],{"className":9598,"code":9599,"language":198,"meta":199},[196],"用户提交邮箱\n  → 同源检查\n  → IP 与邮箱限流\n  → 生成 6 位验证码\n  → D1 只保存加盐哈希、用途、过期时间和尝试次数\n  → EMAIL binding 发送\n  → 验证成功后原子标记已使用\n",[52,9601,9599],{"__ignoreMap":199},[16,9603,9604],{},"关键策略包括：",[39,9606,9607,9610,9613,9616,9619,9622,9625],{},[42,9608,9609],{},"同一邮箱 5 分钟只能发送一封；",[42,9611,9612],{},"验证码 5 分钟后失效；",[42,9614,9615],{},"验证码只能使用一次；",[42,9617,9618],{},"D1 不保存验证码明文；",[42,9620,9621],{},"连续输错达到阈值后停止验证；",[42,9623,9624],{},"邮件发送失败时释放本次占位，避免用户被一个未送达的验证码锁住；",[42,9626,9627],{},"重置密码后撤销已有登录会话，而不是只替换密码哈希。",[16,9629,9630,9631,9634],{},"限流要在数据库中“原子领取资格”，不能写成“先查询上次发送时间，再插入一行”。两个并发请求可能同时通过查询。使用唯一键配合 ",[52,9632,9633],{},"INSERT ... ON CONFLICT ... WHERE","，才能让数据库决定谁获得发送资格。",[16,9636,9637],{},"邮件接口的响应也不应暴露“这个邮箱是否已经注册”。否则攻击者可以批量枚举用户。",[78,9639,9640],{"id":9640},"一个更完整的生产架构",[16,9642,9643],{},"当邮件从 Demo 进入生产，推荐把职责拆开：",[193,9645,9648],{"className":9646,"code":9647,"language":198,"meta":199},[196],"HTTP 请求 \u002F 业务事件\n  → 鉴权、同源、参数校验、限流、幂等\n  → Queue\n  → 邮件模板渲染\n  → EMAIL binding\n  → messageId \u002F 错误码写入 D1\n  → Email Service 日志与告警\n",[52,9649,9647],{"__ignoreMap":199},[16,9651,9652],{},"入站链路则可以是：",[193,9654,9657],{"className":9655,"code":9656,"language":198,"meta":199},[196],"support@yourdomain.com\n  → Email Routing\n  → Email Worker\n  → 认证与大小检查\n  → MIME 解析\n  → R2 保存附件 \u002F D1 建立工单\n  → Queue 做耗时处理\n  → 转发人工邮箱或发送受约束回复\n",[52,9658,9656],{"__ignoreMap":199},[576,9660,9662],{"id":9661},"d1-建议记录什么","D1 建议记录什么",[16,9664,9665],{},"可以记录：",[39,9667,9668,9671,9674,9678,9681,9684,9687],{},[42,9669,9670],{},"业务事件 ID 与幂等键；",[42,9672,9673],{},"模板 ID 和模板版本；",[42,9675,9676,55],{},[52,9677,7826],{},[42,9679,9680],{},"收件人数，不一定要保存完整地址；",[42,9682,9683],{},"accepted \u002F failed 等应用侧状态；",[42,9685,9686],{},"Cloudflare 错误码；",[42,9688,9689],{},"创建、发送和最后更新时间。",[16,9691,9692],{},"不建议默认记录：",[39,9694,9695,9698,9701,9704,9707,9710],{},[42,9696,9697],{},"验证码明文；",[42,9699,9700],{},"密码重置 Token；",[42,9702,9703],{},"完整邮件正文；",[42,9705,9706],{},"附件内容；",[42,9708,9709],{},"SMTP\u002FAPI Token；",[42,9711,9712],{},"为调试方便长期保留的原始来信。",[16,9714,9715],{},"日志本身也可能成为新的隐私数据源。",[576,9717,9719],{"id":9718},"不要把-email-routing-的-dropped-当成发送失败","不要把 Email Routing 的 “Dropped” 当成发送失败",[16,9721,9722,9723,9725,9726,9729],{},"官方特别说明：通过 ",[52,9724,7098],{}," binding 发出的邮件，在 Email Routing 汇总页面可能显示为 ",[20,9727,9728],{},"dropped","，即使邮件实际已经成功投递。",[16,9731,9732,9733,9735],{},"出站结果应该查看 Email Sending 的 Metrics 与 Logs，并用 ",[52,9734,7826],{}," 关联应用日志。Routing 页面描述的是入站路由处理，不是完整的出站投递状态机。",[78,9737,9738],{"id":9738},"本地怎么测试",[16,9740,9741],{},"Wrangler 已支持本地触发 Email Worker。运行：",[193,9743,9745],{"className":285,"code":9744,"language":287,"meta":199,"style":199},"pnpm exec wrangler dev\n",[52,9746,9747],{"__ignoreMap":199},[291,9748,9749,9751,9754,9757],{"class":293,"line":294},[291,9750,6539],{"class":297},[291,9752,9753],{"class":301}," exec",[291,9755,9756],{"class":301}," wrangler",[291,9758,9759],{"class":301}," dev\n",[16,9761,9762],{},"然后向本地入口发送一封原始邮件：",[193,9764,9766],{"className":285,"code":9765,"language":287,"meta":199,"style":199},"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",[52,9767,9768,9780,9787,9797],{"__ignoreMap":199},[291,9769,9770,9772,9775,9778],{"class":293,"line":294},[291,9771,1210],{"class":297},[291,9773,9774],{"class":318}," -X",[291,9776,9777],{"class":301}," POST",[291,9779,368],{"class":318},[291,9781,9782,9785],{"class":293,"line":308},[291,9783,9784],{"class":301},"  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com'",[291,9786,368],{"class":318},[291,9788,9789,9792,9795],{"class":293,"line":328},[291,9790,9791],{"class":318},"  -H",[291,9793,9794],{"class":301}," 'Content-Type: text\u002Fplain'",[291,9796,368],{"class":318},[291,9798,9799,9802,9805,9808,9811,9813,9816,9819],{"class":293,"line":335},[291,9800,9801],{"class":318},"  --data-binary",[291,9803,9804],{"class":301}," $'From: sender@example.net",[291,9806,9807],{"class":318},"\\n",[291,9809,9810],{"class":301},"To: support@example.com",[291,9812,9807],{"class":318},[291,9814,9815],{"class":301},"Subject: Local test",[291,9817,9818],{"class":318},"\\n\\n",[291,9820,9821],{"class":301},"Hello from local development.'\n",[16,9823,9824],{},"本地发送 binding 会把文本和 HTML 保存成可检查的文件，适合验证模板。二进制附件的本地模拟仍有单独限制，涉及附件时应补一轮远程测试。",[16,9826,9827],{},"至少覆盖这些用例：",[39,9829,9830,9833,9836,9839,9842,9845,9848,9851,9854,9857],{},[42,9831,9832],{},"正常文本和 HTML；",[42,9834,9835],{},"非 ASCII 主题与发件人显示名；",[42,9837,9838],{},"重复请求与幂等；",[42,9840,9841],{},"超过发送冷却时间前的重复验证码；",[42,9843,9844],{},"大正文与附件边界；",[42,9846,9847],{},"Header 换行注入；",[42,9849,9850],{},"未验证发件域或受限收件人；",[42,9852,9853],{},"退信、抑制、额度和频率错误；",[42,9855,9856],{},"入站无匹配地址、Catch-all 和拒绝路径；",[42,9858,9859],{},"HTML 邮件中的脚本、远程资源与危险附件名。",[78,9861,9863],{"id":9862},"它适合什么不适合什么","它适合什么，不适合什么",[16,9865,9866],{},"适合：",[39,9868,9869,9872,9875,9878,9881,9884],{},[42,9870,9871],{},"注册验证、魔法链接、密码重置；",[42,9873,9874],{},"订单、账单、部署、监控通知；",[42,9876,9877],{},"个人域名邮件转发；",[42,9879,9880],{},"联系表单与支持工单；",[42,9882,9883],{},"邮件触发的 Agent 或自动化流程；",[42,9885,9886],{},"希望少维护一台 SMTP 服务的 Cloudflare 应用。",[16,9888,9889],{},"不应直接拿来做：",[39,9891,9892,9895,9898,9901,9904],{},[42,9893,9894],{},"未经同意的营销群发；",[42,9896,9897],{},"没有退订、投诉和抑制管理的 Newsletter；",[42,9899,9900],{},"匿名开放的“帮我发邮件”接口；",[42,9902,9903],{},"需要 IMAP 文件夹、日历、通讯录和多人邮箱协作的企业邮箱；",[42,9905,9906],{},"把任何来信正文直接交给有高权限工具的 Agent。",[16,9908,9909],{},"Cloudflare 替你维护了邮件入口与投递基础设施，但发件人信誉、用户授权、内容安全、频率控制和数据最小化仍然是应用自己的责任。",[78,9911,9912],{"id":9912},"最后的判断",[16,9914,9915],{},"Cloudflare Email Service 最有价值的地方，不是“又多了一个 SMTP 厂商”，而是它让邮件和 Worker、D1、R2、Queue、Agents 处于同一运行边界。",[16,9917,9918],{},"接收邮件不再只能转发到人类邮箱，它可以成为一个受约束的事件入口；发送邮件也不再需要为验证码单独接入一个外部 SDK 和密钥体系。",[16,9920,9921],{},"如果项目本来就部署在 Workers，优先从这条最小链路开始：",[136,9923,9924,9930,9933,9936,9939],{},[42,9925,9926,9927,55],{},"Email Routing 接收 ",[52,9928,9929],{},"support@",[42,9931,9932],{},"Worker binding 发送验证码和密码重置；",[42,9934,9935],{},"D1 实现冷却、一次性验证和最小日志；",[42,9937,9938],{},"sender allowlist 限制 binding 权限；",[42,9940,9941],{},"用 Email Sending 日志核对投递，而不是只看接口返回成功。",[16,9943,9944],{},"等确实需要从外部平台发送时，再引入 REST API；只有现有系统已经深度依赖 SMTP 客户端时，才使用认证 SMTP。",[78,9946,2056],{"id":2056},[39,9948,9949,9956,9962,9969,9976,9983,9990,9997,10003,10008,10014,10021,10028],{},[42,9950,9951],{},[111,9952,9955],{"href":9953,"rel":9954},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002F",[115],"Cloudflare Email Service 概览",[42,9957,9958],{},[111,9959,9961],{"href":6156,"rel":9960},[115],"接入 Email Sending",[42,9963,9964],{},[111,9965,9968],{"href":9966,"rel":9967},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Froute-emails\u002F",[115],"接入 Email Routing",[42,9970,9971],{},[111,9972,9975],{"href":9973,"rel":9974},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fdomains\u002F",[115],"域名与 DNS 配置",[42,9977,9978],{},[111,9979,9982],{"href":9980,"rel":9981},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fsend-bindings\u002F",[115],"发送 binding 权限配置",[42,9984,9985],{},[111,9986,9989],{"href":9987,"rel":9988},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fworkers-api\u002F",[115],"Workers 发送 API",[42,9991,9992],{},[111,9993,9996],{"href":9994,"rel":9995},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Froute-emails\u002Femail-handler\u002F",[115],"Email Worker 接收 API",[42,9998,9999],{},[111,10000,7230],{"href":10001,"rel":10002},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Frest-api\u002F",[115],[42,10004,10005],{},[111,10006,7238],{"href":6162,"rel":10007},[115],[42,10009,10010],{},[111,10011,10013],{"href":7362,"rel":10012},[115],"平台限制",[42,10015,10016],{},[111,10017,10020],{"href":10018,"rel":10019},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Fpricing\u002F",[115],"价格说明",[42,10022,10023],{},[111,10024,10027],{"href":10025,"rel":10026},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconcepts\u002Femail-authentication\u002F",[115],"邮件认证与投递",[42,10029,10030],{},[111,10031,10034],{"href":10032,"rel":10033},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Flocal-development\u002Frouting\u002F",[115],"本地测试 Email Worker",[2134,10036,10037],{},"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":199,"searchDepth":328,"depth":328,"links":10039},[10040,10041,10045,10050,10056,10060,10061,10062,10063,10067,10068,10069,10070],{"id":7772,"depth":308,"text":7773},{"id":7858,"depth":308,"text":7859,"children":10042},[10043,10044],{"id":7865,"depth":328,"text":7866},{"id":7897,"depth":328,"text":7898},{"id":7924,"depth":308,"text":7925,"children":10046},[10047,10048],{"id":7931,"depth":328,"text":7931},{"id":7951,"depth":328,"text":10049},"发送邮件使用 cf-bounce 记录",{"id":8008,"depth":308,"text":8009,"children":10051},[10052,10054,10055],{"id":8033,"depth":328,"text":10053},"email() 是入站邮件入口",{"id":8499,"depth":328,"text":8500},{"id":8659,"depth":328,"text":8660},{"id":8715,"depth":308,"text":8716,"children":10057},[10058,10059],{"id":8831,"depth":328,"text":8832},{"id":9044,"depth":328,"text":9045},{"id":9376,"depth":308,"text":9377},{"id":9458,"depth":308,"text":9458},{"id":9592,"depth":308,"text":9592},{"id":9640,"depth":308,"text":9640,"children":10064},[10065,10066],{"id":9661,"depth":328,"text":9662},{"id":9718,"depth":328,"text":9719},{"id":9738,"depth":308,"text":9738},{"id":9862,"depth":308,"text":9863},{"id":9912,"depth":308,"text":9912},{"id":2056,"depth":308,"text":2056},"截至 2026 年 7 月，系统介绍 Cloudflare Email Service 如何接收、转发、解析、回复和发送事务邮件，并给出 Nuxt、Workers、D1 与安全限流实践。",{"src":10073,"alt":10074,"caption":10075,"width":10076,"height":10077},"\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":7711,"description":10071},"blog\u002Fcloudflare-email-service-send-receive-guide",[2185,7706,10084,10085],"Workers","Serverless",{"src":10087,"poster":10073,"title":10088,"description":10089,"duration":10090,"width":2193,"height":2194},"\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":10093,"title":10094,"accent":2200,"body":10095,"description":11780,"draft":2165,"experience":2166,"extension":2167,"featured":331,"image":11781,"meta":11785,"navigation":331,"path":11786,"publishedAt":11787,"readingTime":469,"seo":11788,"stem":11789,"tags":11790,"updatedAt":2166,"video":2166,"__hash__":11794,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Fvue-3-6-rc-upgrade-guide.md","Vue 3.6 RC 升级全景：从 3.5 到 Vapor Mode 与 alien-signals",{"type":13,"value":10096,"toc":11741},[10097,10103,10116,10119,10128,10132,10272,10275,10280,10284,10287,10322,10325,10331,10335,10342,10345,10458,10461,10467,10472,10475,10478,10491,10497,10500,10503,10520,10523,10526,10542,10559,10562,10566,10569,10572,10580,10583,10587,10590,10713,10716,10744,10772,10775,10779,10782,10832,10835,10839,10846,10907,10910,10913,10919,10925,10928,10932,10935,10949,10958,11004,11011,11014,11026,11029,11033,11036,11173,11180,11186,11189,11276,11279,11313,11316,11329,11332,11336,11339,11342,11345,11349,11352,11356,11359,11362,11365,11369,11372,11386,11393,11410,11413,11439,11442,11446,11449,11469,11472,11476,11479,11502,11505,11509,11512,11515,11535,11538,11542,11545,11549,11552,11556,11559,11563,11566,11569,11651,11653,11656,11664,11667,11674,11677,11680,11683,11733,11738],[16,10098,10099,10100,131],{},"如果只看版本号，Vue 3.6 像是一次普通的次版本升级。但真正读完发布说明后，我的感受是：",[20,10101,10102],{},"它更像 Vue 为下一阶段准备的一次“换发动机”",[16,10104,10105,10106,1094,10109,1094,10112,10115],{},"一边是基于 alien-signals 重构的响应式内核，公开的 ",[52,10107,10108],{},"ref",[52,10110,10111],{},"computed",[52,10113,10114],{},"watch"," 用法基本不变；另一边是正式进入 RC 阶段的 Vapor Mode，它让 Vue 单文件组件可以绕过虚拟 DOM，编译为更直接的 DOM 更新逻辑。",[16,10117,10118],{},"不过，先把最重要的版本信息放在前面：",[72,10120,10121],{},[16,10122,10123,10124,10127],{},"截至 2026 年 7 月 26 日，Vue 3.6 的最新版本是 ",[20,10125,10126],{},"3.6.0-rc.2","。RC 是候选发布版，不是稳定版。本文适合用来评估、试验和准备迁移，不代表所有生产项目都应该立即升级。",[78,10129,10131],{"id":10130},"先看结论36-改变了什么","先看结论：3.6 改变了什么",[1376,10133,10134,10150],{},[1379,10135,10136],{},[1382,10137,10138,10141,10144,10147],{},[1385,10139,10140],{},"对比维度",[1385,10142,10143],{},"Vue 3.5",[1385,10145,10146],{},"Vue 3.6 RC",[1385,10148,10149],{},"对现有项目的实际影响",[1398,10151,10152,10166,10180,10194,10208,10222,10240,10258],{},[1382,10153,10154,10157,10160,10163],{},[1403,10155,10156],{},"响应式系统",[1403,10158,10159],{},"3.5 已做过内部优化",[1403,10161,10162],{},"基于 alien-signals 大幅重构",[1403,10164,10165],{},"常规 Composition API 写法通常不需要重写，但应回归复杂副作用与调度逻辑",[1382,10167,10168,10171,10174,10177],{},[1403,10169,10170],{},"默认渲染模式",[1403,10172,10173],{},"Virtual DOM",[1403,10175,10176],{},"仍然是 Virtual DOM",[1403,10178,10179],{},"单纯升级依赖不会让应用自动进入 Vapor",[1382,10181,10182,10185,10188,10191],{},[1403,10183,10184],{},"Vapor Mode",[1403,10186,10187],{},"尚未作为 3.5 的正式能力交付",[1403,10189,10190],{},"功能集进入 RC，100% 显式启用",[1403,10192,10193],{},"可以从单个性能敏感区域开始试验",[1382,10195,10196,10199,10202,10205],{},[1403,10197,10198],{},"应用体积",[1403,10200,10201],{},"默认包含 VDOM runtime",[1403,10203,10204],{},"纯 Vapor 应用可不引入 VDOM runtime",[1403,10206,10207],{},"只有纯 Vapor 或边界清晰的场景，体积收益才容易兑现",[1382,10209,10210,10213,10216,10219],{},[1403,10211,10212],{},"API 覆盖",[1403,10214,10215],{},"完整 Vue 3 API 生态",[1403,10217,10218],{},"Vapor 支持 Vue API 的一个子集",[1403,10220,10221],{},"Options API、实例代理相关能力不能直接迁移",[1382,10223,10224,10227,10230,10237],{},[1403,10225,10226],{},"VDOM \u002F Vapor 混用",[1403,10228,10229],{},"不适用",[1403,10231,10232,10233,10236],{},"通过 ",[52,10234,10235],{},"vaporInteropPlugin"," 互操作",[1403,10238,10239],{},"标准 props、事件、slots 已覆盖，但复杂嵌套仍需测试",[1382,10241,10242,10245,10248,10255],{},[1403,10243,10244],{},"事件委托",[1403,10246,10247],{},"原生监听语义",[1403,10249,10250,10251,10254],{},"rc.2 起默认直接绑定；",[52,10252,10253],{},".delegate"," 才显式委托",[1403,10256,10257],{},"不要沿用 rc.1 的默认委托认知",[1382,10259,10260,10263,10266,10269],{},[1403,10261,10262],{},"适合生产吗",[1403,10264,10265],{},"稳定版",[1403,10267,10268],{},"当前仍是预发布版",[1403,10270,10271],{},"生产项目应等待稳定版和上层生态明确支持",[16,10273,10274],{},"一句话概括：",[16,10276,10277],{},[20,10278,10279],{},"Vue 3.5 的亮点更接近“开发体验和 SSR 能力升级”，Vue 3.6 的核心则是“响应式内核与渲染架构升级”。",[78,10281,10283],{"id":10282},"从-35-到-36变化重心已经不同","从 3.5 到 3.6：变化重心已经不同",[16,10285,10286],{},"Vue 3.5 没有破坏性变化，它带来的能力大多能直接被业务开发感知：",[39,10288,10289,10292,10298,10304,10310,10313,10319],{},[42,10290,10291],{},"响应式 Props 解构正式稳定；",[42,10293,10294,10297],{},[52,10295,10296],{},"useTemplateRef()"," 改善模板引用的组织方式；",[42,10299,10300,10303],{},[52,10301,10302],{},"onWatcherCleanup()"," 让 watcher 清理逻辑更自然；",[42,10305,10306,10309],{},[52,10307,10308],{},"useId()"," 提供 SSR 友好的稳定 ID；",[42,10311,10312],{},"异步组件支持懒激活（Lazy Hydration）；",[42,10314,10315,10318],{},[52,10316,10317],{},"data-allow-mismatch"," 可以声明允许的 hydration 差异；",[42,10320,10321],{},"Deferred Teleport 与自定义元素能力得到增强。",[16,10323,10324],{},"3.6 当然也包含大量修复，但它最值得关注的两件事——alien-signals 与 Vapor——都位于框架的底层。你可能不会在业务代码里多写一个新 API，却会面对新的性能特征、编译产物和运行时边界。",[16,10326,10327,10328],{},"这也决定了升级思路：",[20,10329,10330],{},"不要只检查“代码能不能编译”，还要检查响应式行为、SSR hydration、组件互操作和真实性能。",[78,10332,10334],{"id":10333},"变化一响应式内核切换到-alien-signals","变化一：响应式内核切换到 alien-signals",[16,10336,10337,10338,10341],{},"Vue 3.6 对 ",[52,10339,10340],{},"@vue\u002Freactivity"," 做了大规模重构，其基础来自 StackBlitz 开源的 alien-signals。",[16,10343,10344],{},"对于绝大多数业务开发者，下面这些代码仍然是熟悉的 Vue：",[193,10346,10348],{"className":6582,"code":10347,"language":6584,"meta":199,"style":199},"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",[52,10349,10350,10362,10366,10385,10403,10429,10433,10444,10454],{"__ignoreMap":199},[291,10351,10352,10354,10357,10359],{"class":293,"line":294},[291,10353,6591],{"class":409},[291,10355,10356],{"class":553}," { computed, ref, watchEffect } ",[291,10358,6597],{"class":409},[291,10360,10361],{"class":301}," 'vue'\n",[291,10363,10364],{"class":293,"line":308},[291,10365,332],{"emptyLinePlaceholder":331},[291,10367,10368,10370,10373,10375,10378,10380,10383],{"class":293,"line":328},[291,10369,6609],{"class":409},[291,10371,10372],{"class":318}," price",[291,10374,6615],{"class":409},[291,10376,10377],{"class":297}," ref",[291,10379,6808],{"class":553},[291,10381,10382],{"class":318},"99",[291,10384,6976],{"class":553},[291,10386,10387,10389,10392,10394,10396,10398,10401],{"class":293,"line":335},[291,10388,6609],{"class":409},[291,10390,10391],{"class":318}," count",[291,10393,6615],{"class":409},[291,10395,10377],{"class":297},[291,10397,6808],{"class":553},[291,10399,10400],{"class":318},"2",[291,10402,6976],{"class":553},[291,10404,10405,10407,10410,10412,10415,10418,10420,10423,10426],{"class":293,"line":354},[291,10406,6609],{"class":409},[291,10408,10409],{"class":318}," total",[291,10411,6615],{"class":409},[291,10413,10414],{"class":297}," computed",[291,10416,10417],{"class":553},"(() ",[291,10419,9149],{"class":409},[291,10421,10422],{"class":553}," price.value ",[291,10424,10425],{"class":409},"*",[291,10427,10428],{"class":553}," count.value)\n",[291,10430,10431],{"class":293,"line":371},[291,10432,332],{"emptyLinePlaceholder":331},[291,10434,10435,10438,10440,10442],{"class":293,"line":380},[291,10436,10437],{"class":297},"watchEffect",[291,10439,10417],{"class":553},[291,10441,9149],{"class":409},[291,10443,7115],{"class":553},[291,10445,10446,10449,10451],{"class":293,"line":393},[291,10447,10448],{"class":553},"  console.",[291,10450,8250],{"class":297},[291,10452,10453],{"class":553},"(total.value)\n",[291,10455,10456],{"class":293,"line":398},[291,10457,6715],{"class":553},[16,10459,10460],{},"变化主要发生在内部：依赖如何建立、状态如何标记、计算属性何时失效、effect 如何沿依赖图传播，以及这些关系如何被清理和复用。",[16,10462,10463],{},[782,10464],{"alt":10465,"src":10466},"由活跃与休眠路径组成的细粒度响应式依赖网络","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Freactivity-core-v1.webp",[16,10468,10469],{},[789,10470,10471],{},"这张图表达的是依赖传播的概念，不是 Vue 内部数据结构的逐字段示意。",[576,10473,10474],{"id":10474},"性能数字应该怎样理解",[16,10476,10477],{},"Vue 核心仓库的 alien-signals 合并 PR 给出过两类微基准结果：",[39,10479,10480,10488],{},[42,10481,10482,10483,1094,10485,10487],{},"在大量 ",[52,10484,10108],{},[52,10486,10111],{}," 和 effect 实例的构造场景中，示例内存从约 2.3 MB 降至约 2.0 MB，约减少 13%；",[42,10489,10490],{},"在“一个源变化后，大量 computed 被读取”的特定压力场景中，性能提升可以超过 30 倍。",[16,10492,10493,10494],{},"第二个数字很亮眼，但不能直接翻译成“Vue 3.6 应用快 30 倍”。它证明的是：",[20,10495,10496],{},"新的依赖传播算法在某些过去成本会随规模显著上升的图结构中，复杂度表现更好。",[16,10498,10499],{},"真实页面还包含 DOM、布局、网络、组件库、序列化和业务计算。升级前后的判断应基于自己项目的 flame chart、交互延迟、内存曲线和打包结果，而不是套用框架微基准。",[576,10501,10502],{"id":10502},"哪些项目最可能感受到收益",[39,10504,10505,10508,10511,10514,10517],{},[42,10506,10507],{},"大量 computed 相互依赖的数据看板；",[42,10509,10510],{},"高频修改、筛选和派生状态很多的编辑器；",[42,10512,10513],{},"长时间运行、创建和销毁大量 effect 的工作台；",[42,10515,10516],{},"表单字段多、验证规则和联动关系复杂的应用；",[42,10518,10519],{},"基于 Vue 响应式原语构建的状态管理或工具库。",[16,10521,10522],{},"普通内容页也能得到内部改进，但用户未必会感知到明显差异。",[576,10524,10525],{"id":10525},"谁需要格外谨慎",[16,10527,10528,10529,1094,10531,1094,10534,1094,10536,101,10538,10541],{},"如果代码只使用公开的 ",[52,10530,10108],{},[52,10532,10533],{},"reactive",[52,10535,10111],{},[52,10537,10114],{},[52,10539,10540],{},"effectScope","，迁移风险相对可控。真正需要警惕的是：",[39,10543,10544,10547,10550,10553,10556],{},[42,10545,10546],{},"直接读取或修改 Vue 响应式内部字段；",[42,10548,10549],{},"依赖未公开的 effect、Dep 或 scheduler 实现细节；",[42,10551,10552],{},"自行 monkey patch 响应式函数；",[42,10554,10555],{},"对 watcher 执行顺序、同步刷新和清理时机有隐含假设；",[42,10557,10558],{},"库代码同时支持多个 Vue 次版本，却没有覆盖这些边界测试。",[16,10560,10561],{},"内部重构的意义，正是 Vue 可以在保持公开 API 的同时更换实现。反过来说，依赖内部实现的代码也最容易在这类升级中暴露问题。",[78,10563,10565],{"id":10564},"变化二vapor-mode-不再只是实验概念","变化二：Vapor Mode 不再只是实验概念",[16,10567,10568],{},"Vapor Mode 是 Vue SFC 的一种新编译模式。传统 Vue 模板会生成 VNode，由 Virtual DOM runtime 对比前后两棵树，再把差异更新到真实 DOM；Vapor 则让编译器提前识别动态部分，生成更直接、粒度更细的更新逻辑。",[16,10570,10571],{},"它的目标有两个：",[136,10573,10574,10577],{},[42,10575,10576],{},"降低应用的基础包体积；",[42,10578,10579],{},"减少运行时创建、遍历和比较 VNode 的成本。",[16,10581,10582],{},"官方发布说明称，Vapor 已在第三方 js-framework-benchmark 中达到与 Solid、Svelte 5 相近的级别。这个结论同样应该按“基准场景”理解，而不是把不同框架、不同应用的真实性能压缩成一条排名。",[576,10584,10586],{"id":10585},"vapor-不会在升级后自动开启","Vapor 不会在升级后自动开启",[16,10588,10589],{},"Vue 3.6 依然默认使用 VDOM。要让一个 SFC 使用 Vapor，需要显式标记：",[193,10591,10595],{"className":10592,"code":10593,"language":10594,"meta":199,"style":199},"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",[52,10596,10597,10620,10631,10635,10651,10660,10664,10673,10691,10696,10705],{"__ignoreMap":199},[291,10598,10599,10601,10604,10607,10610,10613,10615,10618],{"class":293,"line":294},[291,10600,8115],{"class":553},[291,10602,10603],{"class":844},"script",[291,10605,10606],{"class":297}," setup",[291,10608,10609],{"class":297}," vapor",[291,10611,10612],{"class":297}," lang",[291,10614,602],{"class":553},[291,10616,10617],{"class":301},"\"ts\"",[291,10619,8378],{"class":553},[291,10621,10622,10624,10627,10629],{"class":293,"line":308},[291,10623,6591],{"class":409},[291,10625,10626],{"class":553}," { ref } ",[291,10628,6597],{"class":409},[291,10630,10361],{"class":301},[291,10632,10633],{"class":293,"line":328},[291,10634,332],{"emptyLinePlaceholder":331},[291,10636,10637,10639,10641,10643,10645,10647,10649],{"class":293,"line":335},[291,10638,6609],{"class":409},[291,10640,10391],{"class":318},[291,10642,6615],{"class":409},[291,10644,10377],{"class":297},[291,10646,6808],{"class":553},[291,10648,4755],{"class":318},[291,10650,6976],{"class":553},[291,10652,10653,10656,10658],{"class":293,"line":354},[291,10654,10655],{"class":553},"\u003C\u002F",[291,10657,10603],{"class":844},[291,10659,8378],{"class":553},[291,10661,10662],{"class":293,"line":371},[291,10663,332],{"emptyLinePlaceholder":331},[291,10665,10666,10668,10671],{"class":293,"line":380},[291,10667,8115],{"class":553},[291,10669,10670],{"class":844},"template",[291,10672,8378],{"class":553},[291,10674,10675,10678,10681,10684,10686,10689],{"class":293,"line":393},[291,10676,10677],{"class":553},"  \u003C",[291,10679,10680],{"class":844},"button",[291,10682,10683],{"class":297}," @click",[291,10685,602],{"class":553},[291,10687,10688],{"class":301},"\"count++\"",[291,10690,8378],{"class":553},[291,10692,10693],{"class":293,"line":398},[291,10694,10695],{"class":553},"    Count: {{ count }}\n",[291,10697,10698,10701,10703],{"class":293,"line":422},[291,10699,10700],{"class":553},"  \u003C\u002F",[291,10702,10680],{"class":844},[291,10704,8378],{"class":553},[291,10706,10707,10709,10711],{"class":293,"line":428},[291,10708,10655],{"class":553},[291,10710,10670],{"class":844},[291,10712,8378],{"class":553},[16,10714,10715],{},"官方还支持两个等价入口：",[193,10717,10719],{"className":10592,"code":10718,"language":10594,"meta":199,"style":199},"\u003Cscript vapor>\n\u002F\u002F \u003Cscript setup vapor> 的简写\n\u003C\u002Fscript>\n",[52,10720,10721,10731,10736],{"__ignoreMap":199},[291,10722,10723,10725,10727,10729],{"class":293,"line":294},[291,10724,8115],{"class":553},[291,10726,10603],{"class":844},[291,10728,10609],{"class":297},[291,10730,8378],{"class":553},[291,10732,10733],{"class":293,"line":308},[291,10734,10735],{"class":9228},"\u002F\u002F \u003Cscript setup vapor> 的简写\n",[291,10737,10738,10740,10742],{"class":293,"line":328},[291,10739,10655],{"class":553},[291,10741,10603],{"class":844},[291,10743,8378],{"class":553},[193,10745,10747],{"className":10592,"code":10746,"language":10594,"meta":199,"style":199},"\u003Ctemplate vapor>\n  \u003C!-- 整个 SFC 使用 Vapor 编译 -->\n\u003C\u002Ftemplate>\n",[52,10748,10749,10759,10764],{"__ignoreMap":199},[291,10750,10751,10753,10755,10757],{"class":293,"line":294},[291,10752,8115],{"class":553},[291,10754,10670],{"class":844},[291,10756,10609],{"class":297},[291,10758,8378],{"class":553},[291,10760,10761],{"class":293,"line":308},[291,10762,10763],{"class":9228},"  \u003C!-- 整个 SFC 使用 Vapor 编译 -->\n",[291,10765,10766,10768,10770],{"class":293,"line":328},[291,10767,10655],{"class":553},[291,10769,10670],{"class":844},[291,10771,8378],{"class":553},[16,10773,10774],{},"也就是说，团队可以升级到 3.6 后继续运行现有 VDOM 应用，再有选择地迁移某个组件或页面。",[576,10776,10778],{"id":10777},"纯-vapor-应用","纯 Vapor 应用",[16,10780,10781],{},"如果应用完全由 Vapor 组件组成，可以使用：",[193,10783,10785],{"className":6582,"code":10784,"language":6584,"meta":199,"style":199},"import { createVaporApp } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateVaporApp(App).mount('#app')\n",[52,10786,10787,10798,10810,10814],{"__ignoreMap":199},[291,10788,10789,10791,10794,10796],{"class":293,"line":294},[291,10790,6591],{"class":409},[291,10792,10793],{"class":553}," { createVaporApp } ",[291,10795,6597],{"class":409},[291,10797,10361],{"class":301},[291,10799,10800,10802,10805,10807],{"class":293,"line":308},[291,10801,6591],{"class":409},[291,10803,10804],{"class":553}," App ",[291,10806,6597],{"class":409},[291,10808,10809],{"class":301}," '.\u002FApp.vue'\n",[291,10811,10812],{"class":293,"line":328},[291,10813,332],{"emptyLinePlaceholder":331},[291,10815,10816,10819,10822,10825,10827,10830],{"class":293,"line":335},[291,10817,10818],{"class":297},"createVaporApp",[291,10820,10821],{"class":553},"(App).",[291,10823,10824],{"class":297},"mount",[291,10826,6808],{"class":553},[291,10828,10829],{"class":301},"'#app'",[291,10831,6976],{"class":553},[16,10833,10834],{},"这种模式不需要引入 Virtual DOM runtime，最容易获得基础包体积收益。它更适合小型新应用、交互小工具，或可以明确约束技术栈的独立页面。",[576,10836,10838],{"id":10837},"在现有-vdom-应用中使用-vapor","在现有 VDOM 应用中使用 Vapor",[16,10840,10841,10842,10845],{},"现有 ",[52,10843,10844],{},"createApp()"," 项目需要安装互操作插件：",[193,10847,10849],{"className":6582,"code":10848,"language":6584,"meta":199,"style":199},"import { createApp, vaporInteropPlugin } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateApp(App)\n  .use(vaporInteropPlugin)\n  .mount('#app')\n",[52,10850,10851,10862,10872,10876,10884,10895],{"__ignoreMap":199},[291,10852,10853,10855,10858,10860],{"class":293,"line":294},[291,10854,6591],{"class":409},[291,10856,10857],{"class":553}," { createApp, vaporInteropPlugin } ",[291,10859,6597],{"class":409},[291,10861,10361],{"class":301},[291,10863,10864,10866,10868,10870],{"class":293,"line":308},[291,10865,6591],{"class":409},[291,10867,10804],{"class":553},[291,10869,6597],{"class":409},[291,10871,10809],{"class":301},[291,10873,10874],{"class":293,"line":328},[291,10875,332],{"emptyLinePlaceholder":331},[291,10877,10878,10881],{"class":293,"line":335},[291,10879,10880],{"class":297},"createApp",[291,10882,10883],{"class":553},"(App)\n",[291,10885,10886,10889,10892],{"class":293,"line":354},[291,10887,10888],{"class":553},"  .",[291,10890,10891],{"class":297},"use",[291,10893,10894],{"class":553},"(vaporInteropPlugin)\n",[291,10896,10897,10899,10901,10903,10905],{"class":293,"line":371},[291,10898,10888],{"class":553},[291,10900,10824],{"class":297},[291,10902,6808],{"class":553},[291,10904,10829],{"class":301},[291,10906,6976],{"class":553},[16,10908,10909],{},"反过来，Vapor 应用也能安装这个插件以容纳 VDOM 组件，但这样会重新带入 VDOM runtime，削弱体积优势。",[16,10911,10912],{},"Render Function 与 JSX 编写的组件仍然属于 VDOM 组件。在纯 Vapor 应用中使用它们，同样需要互操作。",[16,10914,10915],{},[782,10916],{"alt":10917,"src":10918},"现有 VDOM 区域通过明确的互操作边界连接到独立 Vapor 区域","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fvapor-boundaries-v1.webp",[16,10920,10921,10922],{},"我的建议与官方一致：",[20,10923,10924],{},"把两种渲染模式组织成边界清楚的区域，而不是在每一层组件中来回套娃。",[16,10926,10927],{},"例如，把一个高频更新的数据画布做成 Vapor 页面或子树，周围的后台导航、弹窗和成熟组件库继续使用 VDOM。这样的收益、故障范围和回退路径都更容易测量。",[78,10929,10931],{"id":10930},"rc2-最容易踩坑的变化事件委托改为显式启用","rc.2 最容易踩坑的变化：事件委托改为显式启用",[16,10933,10934],{},"如果你在 rc.1 阶段已经试过 Vapor，这一节尤其重要。",[16,10936,10937,10938,10941,10942,10945,10946,10948],{},"rc.1 的说明中，符合条件的事件会自动委托到 ",[52,10939,10940],{},"document","。但这与原生 DOM 和标准 Vue 的行为存在差异：如果祖先元素提前调用 ",[52,10943,10944],{},"stopPropagation()","，事件到不了 ",[52,10947,10940],{},"，子元素被委托的 handler 也不会执行。",[16,10950,10951,10952,10954,10955,10957],{},"从 ",[20,10953,10126],{}," 开始，Vapor 默认把 DOM 监听器直接绑定到元素。只有明确希望使用文档级委托时，才添加 Vapor 专用的 ",[52,10956,10253],{}," 修饰符：",[193,10959,10961],{"className":10592,"code":10960,"language":10594,"meta":199,"style":199},"\u003Cbutton @click.delegate=\"onClick\">\n  保存\n\u003C\u002Fbutton>\n",[52,10962,10963,10991,10996],{"__ignoreMap":199},[291,10964,10965,10967,10969,10972,10975,10977,10980,10982,10984,10987,10989],{"class":293,"line":294},[291,10966,8115],{"class":553},[291,10968,10680],{"class":844},[291,10970,10971],{"class":553}," @",[291,10973,10974],{"class":297},"click",[291,10976,547],{"class":553},[291,10978,10979],{"class":297},"delegate",[291,10981,602],{"class":553},[291,10983,626],{"class":301},[291,10985,10986],{"class":553},"onClick",[291,10988,626],{"class":301},[291,10990,8378],{"class":553},[291,10992,10993],{"class":293,"line":308},[291,10994,10995],{"class":553},"  保存\n",[291,10997,10998,11000,11002],{"class":293,"line":328},[291,10999,10655],{"class":553},[291,11001,10680],{"class":844},[291,11003,8378],{"class":553},[16,11005,11006,11007,11010],{},"同时，旧的 ",[52,11008,11009],{},"compilerOptions.eventDelegation"," 已被移除。",[16,11012,11013],{},"迁移时应该做两件事：",[136,11015,11016,11021],{},[42,11017,11018,11019,55],{},"删除已有的 ",[52,11020,11009],{},[42,11022,11023,11024,131],{},"只对数量大、事件稳定，并且传播路径已验证的监听器使用 ",[52,11025,10253],{},[16,11027,11028],{},"这不是一个适合全局机械替换的优化。事件是否值得委托，要结合节点数量、生命周期和传播语义判断。",[78,11030,11032],{"id":11031},"vapor-与标准-vue-的兼容边界","Vapor 与标准 Vue 的兼容边界",[16,11034,11035],{},"Vapor 的目标不是在第一天覆盖 Vue 的所有历史能力。官方明确说明，它支持现有 Vue API 的一个子集；依赖 VNode 或组件公开实例代理的功能，目前不能直接使用。",[1376,11037,11038,11050],{},[1379,11039,11040],{},[1382,11041,11042,11044,11047],{},[1385,11043,8397],{},[1385,11045,11046],{},"VDOM 组件",[1385,11048,11049],{},"Vapor 组件",[1398,11051,11052,11063,11073,11084,11100,11111,11123,11135,11162],{},[1382,11053,11054,11057,11060],{},[1403,11055,11056],{},"Composition API",[1403,11058,11059],{},"支持",[1403,11061,11062],{},"主要编程方式",[1382,11064,11065,11068,11070],{},[1403,11066,11067],{},"Options API",[1403,11069,11059],{},[1403,11071,11072],{},"不支持",[1382,11074,11075,11080,11082],{},[1403,11076,11077],{},[52,11078,11079],{},"app.config.globalProperties",[1403,11081,11059],{},[1403,11083,11072],{},[1382,11085,11086,11091,11094],{},[1403,11087,11088],{},[52,11089,11090],{},"getCurrentInstance()",[1403,11092,11093],{},"返回当前实例",[1403,11095,11096,11097],{},"返回 ",[52,11098,11099],{},"null",[1382,11101,11102,11105,11108],{},[1403,11103,11104],{},"Render Function \u002F JSX",[1403,11106,11107],{},"原生 VDOM",[1403,11109,11110],{},"仍是 VDOM，混用需要 interop",[1382,11112,11113,11118,11120],{},[1403,11114,11115],{},[52,11116,11117],{},"v-memo",[1403,11119,11059],{},[1403,11121,11122],{},"不适用 \u002F 不支持",[1382,11124,11125,11131,11133],{},[1403,11126,11127,11130],{},[52,11128,11129],{},"@vue:xxx"," 元素生命周期事件",[1403,11132,11059],{},[1403,11134,11072],{},[1382,11136,11137,11140,11143],{},[1403,11138,11139],{},"组件模板引用",[1403,11141,11142],{},"暴露公开实例能力",[1403,11144,11145,11146,1094,11149,1094,11152,1094,11155,1094,11158,11161],{},"不暴露 ",[52,11147,11148],{},"$el",[52,11150,11151],{},"$props",[52,11153,11154],{},"$attrs",[52,11156,11157],{},"$slots",[52,11159,11160],{},"$refs"," 等属性",[1382,11163,11164,11167,11170],{},[1403,11165,11166],{},"自定义指令",[1403,11168,11169],{},"标准指令生命周期对象",[1403,11171,11172],{},"使用 Vapor 专用函数接口",[576,11174,11176,11179],{"id":11175},"slotsdefault-不能再当作无副作用探测",[52,11177,11178],{},"slots.default()"," 不能再当作无副作用探测",[16,11181,11182,11183,11185],{},"在 VDOM 世界中，一些组件会先调用 ",[52,11184,11178],{},"，检查返回内容，再决定渲染 fallback。Vapor 中调用 slot 可能直接创建 Block 和 DOM、注册响应式 effect，或在 hydration 时认领已有 SSR DOM。",[16,11187,11188],{},"因此不要这样写：",[193,11190,11192],{"className":10592,"code":11191,"language":10594,"meta":199,"style":199},"\u003Cscript setup vapor>\nimport { useSlots } from 'vue'\n\nconst slots = useSlots()\nconst content = slots.default?.()\nconst showFallback = !content\n\u003C\u002Fscript>\n",[52,11193,11194,11206,11217,11221,11235,11253,11268],{"__ignoreMap":199},[291,11195,11196,11198,11200,11202,11204],{"class":293,"line":294},[291,11197,8115],{"class":553},[291,11199,10603],{"class":844},[291,11201,10606],{"class":297},[291,11203,10609],{"class":297},[291,11205,8378],{"class":553},[291,11207,11208,11210,11213,11215],{"class":293,"line":308},[291,11209,6591],{"class":409},[291,11211,11212],{"class":553}," { useSlots } ",[291,11214,6597],{"class":409},[291,11216,10361],{"class":301},[291,11218,11219],{"class":293,"line":328},[291,11220,332],{"emptyLinePlaceholder":331},[291,11222,11223,11225,11228,11230,11233],{"class":293,"line":335},[291,11224,6609],{"class":409},[291,11226,11227],{"class":318}," slots",[291,11229,6615],{"class":409},[291,11231,11232],{"class":297}," useSlots",[291,11234,8140],{"class":553},[291,11236,11237,11239,11242,11244,11247,11250],{"class":293,"line":354},[291,11238,6609],{"class":409},[291,11240,11241],{"class":318}," content",[291,11243,6615],{"class":409},[291,11245,11246],{"class":553}," slots.",[291,11248,11249],{"class":297},"default",[291,11251,11252],{"class":553},"?.()\n",[291,11254,11255,11257,11260,11262,11265],{"class":293,"line":371},[291,11256,6609],{"class":409},[291,11258,11259],{"class":318}," showFallback",[291,11261,6615],{"class":409},[291,11263,11264],{"class":409}," !",[291,11266,11267],{"class":553},"content\n",[291,11269,11270,11272,11274],{"class":293,"line":380},[291,11271,10655],{"class":553},[291,11273,10603],{"class":844},[291,11275,8378],{"class":553},[16,11277,11278],{},"让模板负责 slot 的实际渲染，通常更安全：",[193,11280,11282],{"className":10592,"code":11281,"language":10594,"meta":199,"style":199},"\u003Ctemplate>\n  \u003Cslot \u002F>\n\u003C\u002Ftemplate>\n",[52,11283,11284,11292,11305],{"__ignoreMap":199},[291,11285,11286,11288,11290],{"class":293,"line":294},[291,11287,8115],{"class":553},[291,11289,10670],{"class":844},[291,11291,8378],{"class":553},[291,11293,11294,11296,11299,11303],{"class":293,"line":308},[291,11295,10677],{"class":553},[291,11297,11298],{"class":844},"slot",[291,11300,11302],{"class":11301},"sX7ps"," \u002F",[291,11304,8378],{"class":553},[291,11306,11307,11309,11311],{"class":293,"line":328},[291,11308,10655],{"class":553},[291,11310,10670],{"class":844},[291,11312,8378],{"class":553},[576,11314,11315],{"id":11315},"自定义指令需要单独迁移",[16,11317,11318,11319,1094,11322,1094,11325,11328],{},"Vapor 自定义指令接收节点和响应式 getter，并且可以返回 cleanup 函数。它不是把 VDOM 指令的 ",[52,11320,11321],{},"mounted",[52,11323,11324],{},"updated",[52,11326,11327],{},"unmounted"," 对象原样搬过去。",[16,11330,11331],{},"如果项目或组件库依赖大量自定义指令，应该把它们列成独立迁移项，逐个验证响应式更新、清理和 SSR 行为。",[78,11333,11335],{"id":11334},"nuxtrouterpinia-和组件库应该怎么看","Nuxt、Router、Pinia 和组件库应该怎么看",[16,11337,11338],{},"Vue 核心可用，不代表上层框架已经自动完成 Vapor 集成。",[16,11340,11341],{},"Vue 官方的 Vapor Roadmap 单独列出了 Router、Pinia、Nuxt、DevTools 和 Vue Test Utils 等生态工作，其中 Nuxt 还依赖 SSR hydration 能力。对真实项目来说，编译器能否识别 Vapor、服务端产物如何生成、客户端如何 hydration、开发工具怎样检查组件，都是完整链路的一部分。",[16,11343,11344],{},"因此我会把项目分成两类：",[576,11346,11348],{"id":11347},"普通-vue-vite-应用","普通 Vue \u002F Vite 应用",[16,11350,11351],{},"可以在分支中升级 3.6 RC，先验证现有 VDOM 模式，再挑一个叶子页面试用 Vapor。依赖关系比较直接，问题也更容易归因。",[576,11353,11355],{"id":11354},"nuxtssr-或重度组件库应用","Nuxt、SSR 或重度组件库应用",[16,11357,11358],{},"不要仅通过 package override 强行把底层 Vue 换成 RC，就把它视为完成 Vapor 升级。应该先确认当前框架、构建插件和 SSR 链路明确支持对应版本，再进行 hydration、路由切换、异步组件和组件库互操作测试。",[16,11360,11361],{},"即使暂时不使用 Vapor，测试 Vue 3.6 的新响应式内核仍然有价值；只是这类试验最好留在独立分支或测试环境。",[78,11363,11364],{"id":11364},"一套更稳妥的升级路线",[576,11366,11368],{"id":11367},"第一阶段只升级依赖不启用-vapor","第一阶段：只升级依赖，不启用 Vapor",[16,11370,11371],{},"普通 Vue \u002F Vite 项目可以在实验分支中安装 RC：",[193,11373,11375],{"className":285,"code":11374,"language":287,"meta":199,"style":199},"pnpm add vue@3.6.0-rc.2\n",[52,11376,11377],{"__ignoreMap":199},[291,11378,11379,11381,11383],{"class":293,"line":294},[291,11380,6539],{"class":297},[291,11382,6542],{"class":301},[291,11384,11385],{"class":301}," vue@3.6.0-rc.2\n",[16,11387,11388,11389,11392],{},"如果项目直接依赖 ",[52,11390,11391],{},"@vue\u002Fserver-renderer","，让它与 Vue 保持同一版本：",[193,11394,11396],{"className":285,"code":11395,"language":287,"meta":199,"style":199},"pnpm add vue@3.6.0-rc.2 @vue\u002Fserver-renderer@3.6.0-rc.2\n",[52,11397,11398],{"__ignoreMap":199},[291,11399,11400,11402,11404,11407],{"class":293,"line":294},[291,11401,6539],{"class":297},[291,11403,6542],{"class":301},[291,11405,11406],{"class":301}," vue@3.6.0-rc.2",[291,11408,11409],{"class":301}," @vue\u002Fserver-renderer@3.6.0-rc.2\n",[16,11411,11412],{},"然后先验证原有 VDOM 应用：",[39,11414,11415,11418,11421,11424,11427,11430,11433,11436],{},[42,11416,11417],{},"单元测试与组件测试；",[42,11419,11420],{},"首屏 SSR 与客户端 hydration；",[42,11422,11423],{},"watch、computed、effectScope 的复杂场景；",[42,11425,11426],{},"快速路由切换后的副作用清理；",[42,11428,11429],{},"自定义指令、Teleport、Suspense 和异步组件；",[42,11431,11432],{},"开发与生产构建；",[42,11434,11435],{},"浏览器控制台 warning 和错误；",[42,11437,11438],{},"关键交互的耗时与内存基线。",[16,11440,11441],{},"如果这一阶段失败，问题大概率来自新响应式实现或 RC 回归，而不是 Vapor，定位会简单得多。",[576,11443,11445],{"id":11444},"第二阶段选择一个边界清楚的区域","第二阶段：选择一个边界清楚的区域",[16,11447,11448],{},"合适的试点通常同时满足：",[39,11450,11451,11454,11457,11460,11463,11466],{},[42,11452,11453],{},"使用 Composition API；",[42,11455,11456],{},"很少依赖组件实例；",[42,11458,11459],{},"不依赖复杂的 VDOM 组件库；",[42,11461,11462],{},"更新频率高，确实存在性能压力；",[42,11464,11465],{},"有独立路由或明确的组件树边界；",[42,11467,11468],{},"有可重复的性能测试。",[16,11470,11471],{},"先记录迁移前的 JavaScript 体积、交互延迟、CPU 时间和内存，再启用 Vapor。没有基线，就很容易把“技术上成功运行”误判成“值得迁移”。",[576,11473,11475],{"id":11474},"第三阶段验证互操作与-ssr","第三阶段：验证互操作与 SSR",[16,11477,11478],{},"至少覆盖这些组合：",[136,11480,11481,11484,11487,11490,11493,11496,11499],{},[42,11482,11483],{},"VDOM 父组件嵌套 Vapor 子组件；",[42,11485,11486],{},"Vapor 父组件嵌套 VDOM 子组件；",[42,11488,11489],{},"props、emit 与双向绑定；",[42,11491,11492],{},"默认 slot、具名 slot 与作用域 slot；",[42,11494,11495],{},"Suspense、异步组件与错误边界；",[42,11497,11498],{},"服务端输出、hydration 和 mismatch 恢复；",[42,11500,11501],{},"组件卸载后的 watcher、事件与指令 cleanup。",[16,11503,11504],{},"rc.2 的修复列表中有相当多内容正好集中在 hydration、Suspense、slot anchor 和 VDOM interop。这说明这些区域正在快速收敛，也说明它们是当前最值得测试的地方。",[576,11506,11508],{"id":11507},"第四阶段等稳定版再决定生产节奏","第四阶段：等稳定版再决定生产节奏",[16,11510,11511],{},"RC 的意义是功能集已经接近确定，接下来集中发现回归和边缘问题；它不等于生态已经完全稳定。",[16,11513,11514],{},"生产项目至少应该同时满足：",[39,11516,11517,11520,11523,11526,11529,11532],{},[42,11518,11519],{},"Vue 3.6 正式版已发布；",[42,11521,11522],{},"使用的框架和构建工具明确支持；",[42,11524,11525],{},"核心组件库通过互操作验证；",[42,11527,11528],{},"DevTools、测试工具和 SSR 链路可用；",[42,11530,11531],{},"性能收益足以覆盖迁移与维护成本；",[42,11533,11534],{},"有随时退回 VDOM 的方案。",[78,11536,11537],{"id":11537},"不建议做的四件事",[576,11539,11541],{"id":11540},"_1-不要把-rc-当成普通补丁版","1. 不要把 RC 当成普通补丁版",[16,11543,11544],{},"预发布阶段的行为仍可能调整。rc.1 到 rc.2 的事件委托变化，就是最直接的例子。",[576,11546,11548],{"id":11547},"_2-不要为了用上-vapor迁移全部组件","2. 不要为了“用上 Vapor”迁移全部组件",[16,11550,11551],{},"导航、静态内容、低频设置页未必能获得有意义的收益。优先迁移性能瓶颈，而不是追求迁移比例。",[576,11553,11555],{"id":11554},"_3-不要把微基准数字当作应用承诺","3. 不要把微基准数字当作应用承诺",[16,11557,11558],{},"“超过 30 倍”来自特定响应式压力场景；“与 Solid、Svelte 5 同级”来自第三方基准。它们适合证明技术方向，不适合替代项目测量。",[576,11560,11562],{"id":11561},"_4-不要忽略生态边界","4. 不要忽略生态边界",[16,11564,11565],{},"Vue 核心、SFC 编译器、Vite 插件、Nuxt、SSR、Router、Pinia、组件库、DevTools 和测试工具共同组成开发体验。只让其中一个包进入 RC 版本，不代表整条链路已经升级完毕。",[78,11567,11568],{"id":11568},"我的项目类型建议",[1376,11570,11571,11583],{},[1379,11572,11573],{},[1382,11574,11575,11578,11581],{},[1385,11576,11577],{},"项目类型",[1385,11579,11580],{},"现在是否值得试",[1385,11582,4722],{},[1398,11584,11585,11596,11607,11618,11629,11640],{},[1382,11586,11587,11590,11593],{},[1403,11588,11589],{},"小型新工具、内部实验",[1403,11591,11592],{},"值得",[1403,11594,11595],{},"可以尝试纯 Vapor，重点测包体积与交互",[1382,11597,11598,11601,11604],{},[1403,11599,11600],{},"已有 Vue SPA",[1403,11602,11603],{},"值得在分支试",[1403,11605,11606],{},"先保持 VDOM，再迁移单个性能敏感页面",[1382,11608,11609,11612,11615],{},[1403,11610,11611],{},"Nuxt \u002F SSR 站点",[1403,11613,11614],{},"适合研究，生产宜等待",[1403,11616,11617],{},"先确认框架支持，重点测 hydration",[1382,11619,11620,11623,11626],{},[1403,11621,11622],{},"组件库",[1403,11624,11625],{},"应尽早做兼容测试",[1403,11627,11628],{},"检查实例代理、slots、自定义指令和 interop",[1382,11630,11631,11634,11637],{},[1403,11632,11633],{},"大型数据看板 \u002F 编辑器",[1403,11635,11636],{},"很值得压测",[1403,11638,11639],{},"单独比较 alien-signals 与 Vapor 各自带来的收益",[1382,11641,11642,11645,11648],{},[1403,11643,11644],{},"内容型官网",[1403,11646,11647],{},"优先级较低",[1403,11649,11650],{},"等稳定版和生态成熟，避免为有限收益增加复杂度",[78,11652,9912],{"id":9912},[16,11654,11655],{},"Vue 3.6 RC 最有价值的地方，不是提供了多少个可以立刻写进业务的新 API，而是同时推进了两个长期问题：",[39,11657,11658,11661],{},[42,11659,11660],{},"Vue 的响应式系统能否在更复杂的依赖图中保持高效；",[42,11662,11663],{},"Vue 的模板能否在保留熟悉开发体验的同时，摆脱必须依赖虚拟 DOM 的成本。",[16,11665,11666],{},"alien-signals 回答第一个问题，Vapor 尝试回答第二个问题。",[16,11668,11669,11670,11673],{},"对于现有项目，我认为最合理的策略是：",[20,11671,11672],{},"先把 3.6 当作响应式内核升级来测试，再把 Vapor 当作可选的局部编译模式来评估。"," 两件事分开验证，才能知道性能变化来自哪里，也能把风险控制在清晰的边界内。",[16,11675,11676],{},"Vue 3.6 还没有到“所有人今天就应该升级”的阶段，但它已经到了“值得所有 Vue 团队开始理解”的阶段。",[11678,11679],"hr",{},[78,11681,11682],{"id":11682},"资料说明",[39,11684,11685,11693,11701,11709,11717,11725],{},[42,11686,11687,11692],{},[111,11688,11691],{"href":11689,"rel":11690},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fblob\u002Fminor\u002FCHANGELOG.md",[115],"Vue 3.6 minor 分支 Changelog","：3.6.0-rc.1 的 Vapor 总览、兼容边界，以及 rc.2 的事件委托变更。",[42,11694,11695,11700],{},[111,11696,11699],{"href":11697,"rel":11698},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Freleases\u002Ftag\u002Fv3.6.0-rc.2",[115],"Vue 3.6.0-rc.2 Release","：本文发布时的最新候选版本。",[42,11702,11703,11708],{},[111,11704,11707],{"href":11705,"rel":11706},"https:\u002F\u002Fblog.vuejs.org\u002Fposts\u002Fvue-3-5",[115],"Vue 3.5 发布说明","：Vue 3.5 的响应式优化、SSR 与开发者 API 基线。",[42,11710,11711,11716],{},[111,11712,11715],{"href":11713,"rel":11714},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fpull\u002F12349",[115],"Vue core：alien-signals 重构 PR","：响应式实现、内存与特定压力场景微基准。",[42,11718,11719,11724],{},[111,11720,11723],{"href":11721,"rel":11722},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fissues\u002F13687",[115],"Vapor Mode Roadmap","：核心能力与 Vue 生态集成路线。",[42,11726,11727,11732],{},[111,11728,11731],{"href":11729,"rel":11730},"https:\u002F\u002Fgithub.com\u002Fkrausest\u002Fjs-framework-benchmark",[115],"js-framework-benchmark","：Vue 官方发布说明引用的第三方前端框架基准。",[72,11734,11735],{},[16,11736,11737],{},"本文中的升级建议是我基于官方资料和工程经验做出的判断，文章由 AI 协助资料整理与语言校对；具体项目仍应以自身测试结果为准。",[2134,11739,11740],{},"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":199,"searchDepth":328,"depth":328,"links":11742},[11743,11744,11745,11750,11755,11756,11761,11765,11771,11777,11778,11779],{"id":10130,"depth":308,"text":10131},{"id":10282,"depth":308,"text":10283},{"id":10333,"depth":308,"text":10334,"children":11746},[11747,11748,11749],{"id":10474,"depth":328,"text":10474},{"id":10502,"depth":328,"text":10502},{"id":10525,"depth":328,"text":10525},{"id":10564,"depth":308,"text":10565,"children":11751},[11752,11753,11754],{"id":10585,"depth":328,"text":10586},{"id":10777,"depth":328,"text":10778},{"id":10837,"depth":328,"text":10838},{"id":10930,"depth":308,"text":10931},{"id":11031,"depth":308,"text":11032,"children":11757},[11758,11760],{"id":11175,"depth":328,"text":11759},"slots.default() 不能再当作无副作用探测",{"id":11315,"depth":328,"text":11315},{"id":11334,"depth":308,"text":11335,"children":11762},[11763,11764],{"id":11347,"depth":328,"text":11348},{"id":11354,"depth":328,"text":11355},{"id":11364,"depth":308,"text":11364,"children":11766},[11767,11768,11769,11770],{"id":11367,"depth":328,"text":11368},{"id":11444,"depth":328,"text":11445},{"id":11474,"depth":328,"text":11475},{"id":11507,"depth":328,"text":11508},{"id":11537,"depth":308,"text":11537,"children":11772},[11773,11774,11775,11776],{"id":11540,"depth":328,"text":11541},{"id":11547,"depth":328,"text":11548},{"id":11554,"depth":328,"text":11555},{"id":11561,"depth":328,"text":11562},{"id":11568,"depth":308,"text":11568},{"id":9912,"depth":308,"text":9912},{"id":11682,"depth":308,"text":11682},"截至 Vue 3.6.0-rc.2，系统对比 Vue 3.5 与 3.6 的响应式内核、Vapor Mode、事件委托、互操作边界和渐进升级策略。",{"src":11782,"alt":11783,"caption":11784,"width":10076,"height":10077},"\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":10094,"description":11780},"blog\u002Fvue-3-6-rc-upgrade-guide",[11791,11792,11793],"Vue","JavaScript","前端工程","L8sIMfVAKUKqWTcVSvsseRJeOmYbzw7h958oo1EPLCo",{"id":11796,"title":11797,"accent":4503,"body":11798,"description":12032,"draft":2165,"experience":12033,"extension":2167,"featured":331,"image":12038,"meta":12042,"navigation":331,"path":12043,"publishedAt":12044,"readingTime":393,"seo":12045,"stem":12046,"tags":12047,"updatedAt":2166,"video":2166,"__hash__":12051,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Ffrom-gpt-4-invite-to-agents.md","从一封 GPT‑4 邀请邮件，到智能体时代",{"type":13,"value":11799,"toc":12024},[11800,11803,11806,11811,11818,11824,11827,11830,11834,11837,11840,11846,11849,11852,11855,11858,11872,11875,11892,11898,11901,11904,11908,11911,11914,11928,11931,11945,11948,11952,11955,11958,11975,11978,11982,11985,11988,11991,11994,11996,11998],[16,11801,11802],{},"今天整理旧邮件时，我重新翻到了一封 2023 年 3 月 18 日的邮件。",[16,11804,11805],{},"邮件的标题很直接：",[72,11807,11808],{},[16,11809,11810],{},"You’re invited to use the OpenAI GPT-4 API.",[16,11812,11813,11814,11817],{},"正文告诉我，已经可以通过 API 使用拥有 ",[20,11815,11816],{},"8K context"," 的 GPT‑4。",[16,11819,11820],{},[782,11821],{"alt":11822,"src":11823},"2023 年 GPT‑4 API 邀请邮件，正文说明可使用 8K context","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Femail-invite-v1.webp",[16,11825,11826],{},"那时 GPT‑4 才刚发布几天。API 需要申请、等待，再由 OpenAI 逐步邀请开发者。官方发布资料里，标准 GPT‑4 的上下文长度是 8,192 tokens，同时有限开放 32,768-token 版本。对当时的我来说，一次 API 访问资格本身，就像一扇刚刚推开的小门。",[16,11828,11829],{},"三年多以后再看，这封邮件已经不像一封产品通知，更像一枚时间胶囊。",[78,11831,11833],{"id":11832},"数字变大只是最容易看到的变化","数字变大，只是最容易看到的变化",[16,11835,11836],{},"早期开发者熟悉的是 4K、8K 量级的上下文。写提示词时，我们会计算还剩多少空间，主动裁剪资料，把长文拆成小段，再想办法让下一次请求接住上一次的结果。",[16,11838,11839],{},"到了 2026 年，百万级上下文已经进入主流前沿模型。OpenAI 当前模型目录列出的 GPT‑5.6 系列拥有约 1.05M context window；Google 的长上下文文档也说明，多款 Gemini 模型支持 1M 或更多 tokens。",[16,11841,11842],{},[782,11843],{"alt":11844,"src":11845},"从 4K、8K 到 1M 以上，上下文从短纸条延伸为多模态资料长廊","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcontext-window-v1.webp",[16,11847,11848],{},"这不只是“能塞进更多文字”。代码、图片、音频、视频和大批文档可以共同成为一次任务的背景。以前必须先切碎、索引和摘要的资料，现在有更多机会被放进同一个工作上下文。",[16,11850,11851],{},"但更长的上下文不等于完美记忆，也不等于模型会自动抓住重点。资料越多，噪声、成本、延迟和错误关联也可能越多。真正重要的能力，仍然是选择什么信息、以什么顺序提供，以及如何验证模型是否理解了关键约束。",[78,11853,11854],{"id":11854},"真正改变的是交互单位",[16,11856,11857],{},"三年前，我最常做的是“提问—回答”：",[136,11859,11860,11863,11866,11869],{},[42,11861,11862],{},"写一段提示词；",[42,11864,11865],{},"等待模型生成；",[42,11867,11868],{},"复制结果；",[42,11870,11871],{},"自己完成剩余工作。",[16,11873,11874],{},"今天，越来越多任务变成了“目标—上下文—行动—验证”：",[136,11876,11877,11880,11883,11886,11889],{},[42,11878,11879],{},"说明最终目标与完成标准；",[42,11881,11882],{},"提供代码库、文档和当前状态；",[42,11884,11885],{},"允许模型调用搜索、文件、终端或其他工具；",[42,11887,11888],{},"让它执行一连串相互依赖的步骤；",[42,11890,11891],{},"用测试、构建、差异检查或人工评审确认结果。",[16,11893,11894],{},[782,11895],{"alt":11896,"src":11897},"聊天框扩展为包含文档、代码、浏览器、工具调用和人工确认的智能体工作流","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fagent-workflow-v1.webp",[16,11899,11900],{},"OpenAI 当前的模型与 Responses API 文档已经把函数调用、网页搜索、文件搜索和计算机操作列为可用工具。模型不再只告诉我们“可以怎么做”，而是开始在获得授权的范围内参与真实工作过程。",[16,11902,11903],{},"这也是我认为“聊天模型”和“智能体”之间最重要的区别：前者交付一段内容，后者推进一个状态会变化的任务。",[78,11905,11907],{"id":11906},"程序员的工作没有消失而是向两端移动","程序员的工作没有消失，而是向两端移动",[16,11909,11910],{},"当模型可以读代码、改文件、运行命令和检查结果时，程序员并不是突然变得不重要。相反，我们的注意力开始向任务的两端移动。",[16,11912,11913],{},"在行动之前，需要把问题定义得更清楚：",[39,11915,11916,11919,11922,11925],{},[42,11917,11918],{},"目标是什么，什么不在范围内；",[42,11920,11921],{},"哪些资料可信，哪些只是参考；",[42,11923,11924],{},"哪些权限可以授予，哪些操作必须确认；",[42,11926,11927],{},"失败时应该停止、重试，还是回滚。",[16,11929,11930],{},"在行动之后，需要把结果验证得更扎实：",[39,11932,11933,11936,11939,11942],{},[42,11934,11935],{},"代码能否通过测试与构建；",[42,11937,11938],{},"页面是否真的符合预期，而不只是生成成功；",[42,11940,11941],{},"外部状态是否已经改变；",[42,11943,11944],{},"结果能否被解释、复现和审计。",[16,11946,11947],{},"三年前，我们努力学习怎样写出一个更好的提示词。今天，我们还要学习怎样设计上下文、工具、权限、检查点和验收标准。",[78,11949,11951],{"id":11950},"能力可以委托责任不能外包","能力可以委托，责任不能外包",[16,11953,11954],{},"智能体越能执行真实操作，边界就越重要。",[16,11956,11957],{},"让模型总结一篇文章，与让它修改生产数据库，不是同一种授权；让它提出邮件草稿，与让它直接发送给客户，也不是同一种风险。一个成熟的智能体系统，不应该只追求“能做多少”，还应该明确：",[39,11959,11960,11963,11966,11969,11972],{},[42,11961,11962],{},"默认只读还是默认可写；",[42,11964,11965],{},"哪些动作需要人在执行前确认；",[42,11967,11968],{},"如何限制目标、目录、账号和数据范围；",[42,11970,11971],{},"如何保存必要的操作记录；",[42,11973,11974],{},"发生错误时怎样停止并恢复。",[16,11976,11977],{},"模型可以承担越来越多行动，但判断和责任并不会因此消失。最终仍然需要有人回答：为什么要这样做，依据是什么，风险由谁承担，结果是否可以接受。",[78,11979,11981],{"id":11980},"再过三年我们会怎样看今天","再过三年，我们会怎样看今天",[16,11983,11984],{},"这封旧邮件如今很像一张偶然保存下来的车票。",[16,11986,11987],{},"2023 年，我申请的是一次访问模型的机会。2026 年，我们已经在学习如何让模型理解一个长期目标、使用工具、跨越多个步骤，并在人的监督下完成工作。",[16,11989,11990],{},"让我感慨的不是某一个参数增长了多少倍，而是时间被压缩了。许多曾经只存在于想象中的能力，没有在遥远的未来慢慢到来，而是在几次版本更新之间，悄悄变成了日常。",[16,11992,11993],{},"也许再过三年回头看，今天让我们惊叹的百万级上下文和智能体，也会像这封邮件一样，成为未来刚刚开始时留下的一件旧物。",[11678,11995],{},[78,11997,11682],{"id":11682},[39,11999,12000,12008,12016],{},[42,12001,12002,12007],{},[111,12003,12006],{"href":12004,"rel":12005},"https:\u002F\u002Fopenai.com\u002Findex\u002Fgpt-4-research\u002F",[115],"OpenAI：GPT‑4 发布资料","：GPT‑4 于 2023 年 3 月发布，标准上下文为 8,192 tokens，并有限开放 32,768-token 版本。",[42,12009,12010,12015],{},[111,12011,12014],{"href":12012,"rel":12013},"https:\u002F\u002Fdevelopers.openai.com\u002Fapi\u002Fdocs\u002Fmodels",[115],"OpenAI：模型目录","：截至本文发布时，前沿 GPT‑5.6 系列列出约 1.05M context window，并支持多类工具。",[42,12017,12018,12023],{},[111,12019,12022],{"href":12020,"rel":12021},"https:\u002F\u002Fai.google.dev\u002Fgemini-api\u002Fdocs\u002Flong-context",[115],"Google AI：Long context","：介绍 Gemini 模型的 1M+ 长上下文能力、使用场景与限制。",{"title":199,"searchDepth":328,"depth":328,"links":12025},[12026,12027,12028,12029,12030,12031],{"id":11832,"depth":308,"text":11833},{"id":11854,"depth":308,"text":11854},{"id":11906,"depth":308,"text":11907},{"id":11950,"depth":308,"text":11951},{"id":11980,"depth":308,"text":11981},{"id":11682,"depth":308,"text":11682},"2023 年，我等来的是一次 8K context 的 API 访问资格；三年后，我们开始学习如何向智能体交代目标、权限、边界与验收标准。",{"src":12034,"title":12035,"description":12036,"duration":12037,"hasAudio":331},"\u002Fexperiences\u002Ffrom-gpt-4-invite-to-agents","从 GPT-4 邀请到智能体时代 · 六幕互动叙事","用 2 分 26 秒回看上下文、智能体工作流，以及能力与责任的变化。",146,{"src":12039,"alt":12040,"caption":12041,"width":10076,"height":10077},"\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcover-v1.webp","一封来自 2023 年的邮件从玻璃时间胶囊延伸为现代智能体工作空间","三年前申请访问模型，三年后学习与模型共同工作",{},"\u002Fblog\u002Ffrom-gpt-4-invite-to-agents","2026-07-25",{"title":11797,"description":12032},"blog\u002Ffrom-gpt-4-invite-to-agents",[12048,12049,12050],"AI","GPT-4","智能体","P0HWhOcEJwLRdrvZlwkQks0C-1y9Q98neHDcWHX6Xow",{"id":12053,"title":12054,"accent":11,"body":12055,"description":12487,"draft":2165,"experience":2166,"extension":2167,"featured":331,"image":12488,"meta":12492,"navigation":331,"path":12493,"publishedAt":12494,"readingTime":393,"seo":12495,"stem":12496,"tags":12497,"updatedAt":12499,"video":2166,"__hash__":12500,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Fedge-stateful-apps.md","在边缘构建有状态应用",{"type":13,"value":12056,"toc":12479},[12057,12063,12066,12069,12072,12121,12124,12129,12132,12135,12138,12206,12209,12291,12294,12297,12300,12377,12391,12394,12397,12400,12403,12414,12417,12420,12423,12426,12440,12443,12447,12450,12453,12470,12476],[16,12058,12059,12060],{},"边缘计算最吸引人的承诺，是让代码离用户更近。但当应用开始保存账号、会话和留言，问题就不再只是“把函数部署到更多节点”，而是：",[20,12061,12062],{},"状态究竟在哪里，谁对它负责，失败时系统会怎样退化？",[16,12064,12065],{},"这篇文章用一个带登录和留言的博客作为最小案例，梳理在 Cloudflare Workers 与 D1 上构建有状态应用时真正重要的设计边界。",[78,12067,12068],{"id":12068},"先画清楚状态地图",[16,12070,12071],{},"一个看起来很小的博客，至少有三类完全不同的状态：",[1376,12073,12074,12086],{},[1379,12075,12076],{},[1382,12077,12078,12080,12083],{},[1385,12079,1545],{},[1385,12081,12082],{},"来源",[1385,12084,12085],{},"生命周期",[1398,12087,12088,12099,12110],{},[1382,12089,12090,12093,12096],{},[1403,12091,12092],{},"文章正文",[1403,12094,12095],{},"Git 中的 Markdown",[1403,12097,12098],{},"随部署更新",[1382,12100,12101,12104,12107],{},[1403,12102,12103],{},"用户与留言",[1403,12105,12106],{},"D1",[1403,12108,12109],{},"长期持久化",[1382,12111,12112,12115,12118],{},[1403,12113,12114],{},"登录会话",[1403,12116,12117],{},"D1 + Cookie",[1403,12119,12120],{},"有明确过期时间",[16,12122,12123],{},"文章内容适合跟着代码版本走。它需要评审、回滚和构建时校验，却不需要读者在运行时修改。用户数据则刚好相反：它必须脱离部署生命周期存在。",[72,12125,12126],{},[16,12127,12128],{},"选择数据库之前，先判断一份数据应该跟随代码版本，还是跟随用户行为。",[16,12130,12131],{},"这个区分会自然导向一个简单架构：Nuxt Content 负责 Markdown，D1 负责运行时业务数据，Worker 负责把两者组合成一次请求。",[78,12133,12134],{"id":12134},"绑定比网络请求更重要",[16,12136,12137],{},"Worker 访问 D1 时不需要自己保存 API Token，也不应该在运行时调用 Cloudflare REST API。D1 通过 binding 直接进入运行时：",[193,12139,12141],{"className":5155,"code":12140,"filename":8725,"language":5157,"meta":199,"style":199},"{\n  \"d1_databases\": [\n    {\n      \"binding\": \"DB\",\n      \"database_name\": \"signal-log-db\",\n      \"database_id\": \"\u003CYOUR_DATABASE_ID>\"\n    }\n  ]\n}\n",[52,12142,12143,12147,12155,12159,12171,12183,12193,12197,12202],{"__ignoreMap":199},[291,12144,12145],{"class":293,"line":294},[291,12146,5164],{"class":553},[291,12148,12149,12152],{"class":293,"line":308},[291,12150,12151],{"class":318},"  \"d1_databases\"",[291,12153,12154],{"class":553},": [\n",[291,12156,12157],{"class":293,"line":328},[291,12158,8747],{"class":553},[291,12160,12161,12164,12166,12169],{"class":293,"line":335},[291,12162,12163],{"class":318},"      \"binding\"",[291,12165,863],{"class":553},[291,12167,12168],{"class":301},"\"DB\"",[291,12170,5177],{"class":553},[291,12172,12173,12176,12178,12181],{"class":293,"line":354},[291,12174,12175],{"class":318},"      \"database_name\"",[291,12177,863],{"class":553},[291,12179,12180],{"class":301},"\"signal-log-db\"",[291,12182,5177],{"class":553},[291,12184,12185,12188,12190],{"class":293,"line":371},[291,12186,12187],{"class":318},"      \"database_id\"",[291,12189,863],{"class":553},[291,12191,12192],{"class":301},"\"\u003CYOUR_DATABASE_ID>\"\n",[291,12194,12195],{"class":293,"line":380},[291,12196,5295],{"class":553},[291,12198,12199],{"class":293,"line":393},[291,12200,12201],{"class":553},"  ]\n",[291,12203,12204],{"class":293,"line":398},[291,12205,5305],{"class":553},[16,12207,12208],{},"在服务端路由里，数据库对象来自当前请求上下文。每一条 SQL 都使用 prepared statement：",[193,12210,12212],{"className":6582,"code":12211,"language":6584,"meta":199,"style":199},"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",[52,12213,12214,12228,12240,12245,12250,12255,12260,12265,12272,12282],{"__ignoreMap":199},[291,12215,12216,12218,12221,12223,12225],{"class":293,"line":294},[291,12217,6609],{"class":409},[291,12219,12220],{"class":318}," comment",[291,12222,6615],{"class":409},[291,12224,6731],{"class":409},[291,12226,12227],{"class":553}," db\n",[291,12229,12230,12232,12235,12237],{"class":293,"line":308},[291,12231,10888],{"class":553},[291,12233,12234],{"class":297},"prepare",[291,12236,6808],{"class":553},[291,12238,12239],{"class":301},"`\n",[291,12241,12242],{"class":293,"line":328},[291,12243,12244],{"class":301},"    SELECT id, body, created_at\n",[291,12246,12247],{"class":293,"line":335},[291,12248,12249],{"class":301},"    FROM comments\n",[291,12251,12252],{"class":293,"line":354},[291,12253,12254],{"class":301},"    WHERE post_slug = ?1 AND status = 'published'\n",[291,12256,12257],{"class":293,"line":371},[291,12258,12259],{"class":301},"    ORDER BY created_at DESC\n",[291,12261,12262],{"class":293,"line":380},[291,12263,12264],{"class":301},"    LIMIT 100\n",[291,12266,12267,12270],{"class":293,"line":393},[291,12268,12269],{"class":301},"  `",[291,12271,6976],{"class":553},[291,12273,12274,12276,12279],{"class":293,"line":398},[291,12275,10888],{"class":553},[291,12277,12278],{"class":297},"bind",[291,12280,12281],{"class":553},"(postSlug)\n",[291,12283,12284,12286,12289],{"class":293,"line":422},[291,12285,10888],{"class":553},[291,12287,12288],{"class":297},"all",[291,12290,8140],{"class":553},[16,12292,12293],{},"参数绑定不是代码风格问题，而是输入边界的一部分。文章路径、邮箱、昵称和留言正文都来自用户，任何一个值都不应该被拼接进 SQL。",[78,12295,12296],{"id":12296},"会话只存不可逆摘要",[16,12298,12299],{},"登录成功后，浏览器拿到一个随机会话令牌；数据库只保存它的 SHA-256 摘要。这样即使会话表意外暴露，原始令牌也不能被直接拿去登录。",[193,12301,12303],{"className":6582,"code":12302,"language":6584,"meta":199,"style":199},"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",[52,12304,12305,12319,12336,12340,12346,12359,12368],{"__ignoreMap":199},[291,12306,12307,12309,12312,12314,12317],{"class":293,"line":294},[291,12308,6609],{"class":409},[291,12310,12311],{"class":318}," token",[291,12313,6615],{"class":409},[291,12315,12316],{"class":297}," createRandomToken",[291,12318,8140],{"class":553},[291,12320,12321,12323,12326,12328,12330,12333],{"class":293,"line":308},[291,12322,6609],{"class":409},[291,12324,12325],{"class":318}," tokenHash",[291,12327,6615],{"class":409},[291,12329,6731],{"class":409},[291,12331,12332],{"class":297}," sha256",[291,12334,12335],{"class":553},"(token)\n",[291,12337,12338],{"class":293,"line":328},[291,12339,332],{"emptyLinePlaceholder":331},[291,12341,12342,12344],{"class":293,"line":335},[291,12343,7138],{"class":409},[291,12345,12227],{"class":553},[291,12347,12348,12350,12352,12354,12357],{"class":293,"line":354},[291,12349,10888],{"class":553},[291,12351,12234],{"class":297},[291,12353,6808],{"class":553},[291,12355,12356],{"class":301},"'INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)'",[291,12358,6976],{"class":553},[291,12360,12361,12363,12365],{"class":293,"line":371},[291,12362,10888],{"class":553},[291,12364,12278],{"class":297},[291,12366,12367],{"class":553},"(tokenHash, userId, expiresAt)\n",[291,12369,12370,12372,12375],{"class":293,"line":380},[291,12371,10888],{"class":553},[291,12373,12374],{"class":297},"run",[291,12376,8140],{"class":553},[16,12378,12379,12380,12383,12384,12387,12388,131],{},"Cookie 使用 ",[52,12381,12382],{},"HttpOnly","，让前端 JavaScript 无法读取；使用 ",[52,12385,12386],{},"SameSite=Lax"," 降低跨站请求风险；生产环境再加上 ",[52,12389,12390],{},"Secure",[16,12392,12393],{},"这并不意味着系统从此“绝对安全”。更准确的说法是：我们让每一层只持有它完成工作所需的最少能力。",[78,12395,12396],{"id":12396},"一致性要围绕用户动作设计",[16,12398,12399],{},"D1 是关系数据库，但边缘应用仍然需要明确哪些流程要求强一致。",[16,12401,12402],{},"对博客来说，关键动作很少：",[136,12404,12405,12408,12411],{},[42,12406,12407],{},"注册后应当立刻能够登录；",[42,12409,12410],{},"留言成功后应当立刻在当前页面出现；",[42,12412,12413],{},"退出后旧会话应当立刻失效。",[16,12415,12416],{},"这些流程都应该从同一个主数据库完成写入和紧随其后的读取。文章列表、公开留言列表等纯读取场景，才有更大的缓存空间。",[16,12418,12419],{},"不要从“哪里可以缓存”出发，而要从“用户刚刚完成了什么承诺”出发。",[78,12421,12422],{"id":12422},"失败也需要产品设计",[16,12424,12425],{},"边缘运行时会失败，数据库也可能暂时不可用。区别只在于，我们有没有把失败变成用户能理解的状态。",[39,12427,12428,12431,12434,12437],{},[42,12429,12430],{},"登录失败时，不区分“邮箱不存在”和“密码错误”，避免暴露账号枚举信息；",[42,12432,12433],{},"留言写入失败时，保留输入内容，让用户可以重试；",[42,12435,12436],{},"内容查询失败时，返回真正的错误状态，而不是空白页面；",[42,12438,12439],{},"所有服务端错误使用结构化日志，但不把内部 SQL 或堆栈发给浏览器。",[16,12441,12442],{},"系统可靠性不是“永远不出错”，而是错误发生时边界仍然完整。",[78,12444,12446],{"id":12445},"最后让架构保持无聊","最后：让架构保持无聊",[16,12448,12449],{},"这个方案没有消息队列、没有分布式锁，也没有额外的身份服务。不是因为那些工具没有价值，而是当前问题还没有提出相应要求。",[16,12451,12452],{},"一个长期可维护的个人站，最好的基础通常是：",[39,12454,12455,12458,12461,12464,12467],{},[42,12456,12457],{},"内容跟着 Git；",[42,12459,12460],{},"数据进入 D1；",[42,12462,12463],{},"密钥留在 Worker secret；",[42,12465,12466],{},"输入在边界验证；",[42,12468,12469],{},"失败清楚、可观察、可恢复。",[16,12471,12472,12473],{},"当流量或产品需求真正变化时，再让架构跟着证据演进。",[20,12474,12475],{},"边缘已经足够快，工程的任务是让它继续简单。",[2134,12477,12478],{},"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":199,"searchDepth":328,"depth":328,"links":12480},[12481,12482,12483,12484,12485,12486],{"id":12068,"depth":308,"text":12068},{"id":12134,"depth":308,"text":12134},{"id":12296,"depth":308,"text":12296},{"id":12396,"depth":308,"text":12396},{"id":12422,"depth":308,"text":12422},{"id":12445,"depth":308,"text":12446},"Cloudflare Workers 很快，D1 很近，但真正困难的是把状态、一致性与失败边界设计清楚。",{"src":12489,"alt":12490,"caption":12491,"width":10076,"height":10077},"\u002Fmedia\u002Fposts\u002Fedge-stateful-apps\u002Fcover-v1.webp","边缘节点围绕中心数据库形成有状态应用拓扑","状态、边缘节点与一致性边界",{},"\u002Fblog\u002Fedge-stateful-apps","2026-07-21",{"title":12054,"description":12487},"blog\u002Fedge-stateful-apps",[2185,12106,12498],"工程","2026-07-24","gsFDt7C9LtEnc-0ELg6T7tdWM-k-26r6jVfx424yrSM",{"id":12502,"title":12503,"accent":2200,"body":12504,"description":12888,"draft":2165,"experience":2166,"extension":2167,"featured":2165,"image":12889,"meta":12893,"navigation":331,"path":12894,"publishedAt":12895,"readingTime":371,"seo":12896,"stem":12897,"tags":12898,"updatedAt":2166,"video":2166,"__hash__":12901,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Fnuxt-4-architecture-subtraction.md","Nuxt 4 的架构减法",{"type":13,"value":12505,"toc":12880},[12506,12509,12515,12519,12522,12573,12576,12583,12586,12589,12713,12716,12734,12737,12740,12743,12746,12749,12754,12758,12765,12771,12774,12777,12780,12783,12848,12851,12854,12857,12874,12877],[16,12507,12508],{},"很多 Nuxt 项目变复杂，并不是因为业务真的复杂，而是因为我们太早把所有可能性都放进了架构。",[16,12510,12511,12512,131],{},"个人博客是一个很好的校准器：页面数量有限、内容结构稳定、交互集中在少数区域。它提醒我们，框架最有价值的部分往往不是增加能力，而是",[20,12513,12514],{},"删除不必要的选择",[78,12516,12518],{"id":12517},"从默认-ssr-开始","从默认 SSR 开始",[16,12520,12521],{},"技术文章需要被搜索引擎理解，也需要在弱网下尽早出现正文。Nuxt 默认的服务端渲染刚好符合这个目标。",[193,12523,12526],{"className":6582,"code":12524,"filename":12525,"language":6584,"meta":199,"style":199},"export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fcontent'],\n  nitro: {\n    preset: 'cloudflare',\n  },\n})\n","nuxt.config.ts",[52,12527,12528,12539,12550,12555,12565,12569],{"__ignoreMap":199},[291,12529,12530,12532,12534,12537],{"class":293,"line":294},[291,12531,6402],{"class":409},[291,12533,8084],{"class":409},[291,12535,12536],{"class":297}," defineNuxtConfig",[291,12538,6624],{"class":553},[291,12540,12541,12544,12547],{"class":293,"line":308},[291,12542,12543],{"class":553},"  modules: [",[291,12545,12546],{"class":301},"'@nuxt\u002Fcontent'",[291,12548,12549],{"class":553},"],\n",[291,12551,12552],{"class":293,"line":328},[291,12553,12554],{"class":553},"  nitro: {\n",[291,12556,12557,12560,12563],{"class":293,"line":335},[291,12558,12559],{"class":553},"    preset: ",[291,12561,12562],{"class":301},"'cloudflare'",[291,12564,5177],{"class":553},[291,12566,12567],{"class":293,"line":354},[291,12568,6681],{"class":553},[291,12570,12571],{"class":293,"line":371},[291,12572,6715],{"class":553},[16,12574,12575],{},"这里没有把整个站改成 SPA，也没有为每个页面单独发明数据加载方式。页面在服务端完成第一次查询和渲染，客户端接管后继续使用同一套路由。",[16,12577,12578,12579,12582],{},"如果某一小块必须依赖浏览器 API，再局部使用 ",[52,12580,12581],{},"ClientOnly","。不要为了一个客户端组件牺牲整页的语义和首屏。",[78,12584,12585],{"id":12585},"内容查询留在页面边界",[16,12587,12588],{},"首页、文章列表和文章详情需要的数据粒度不同。查询应该贴近消费它的页面：",[193,12590,12592],{"className":6582,"code":12591,"language":6584,"meta":199,"style":199},"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",[52,12593,12594,12629,12643,12668,12687,12701,12709],{"__ignoreMap":199},[291,12595,12596,12598,12600,12603,12605,12608,12610,12612,12614,12617,12619,12622,12625,12627],{"class":293,"line":294},[291,12597,6609],{"class":409},[291,12599,9159],{"class":553},[291,12601,12602],{"class":6919},"data",[291,12604,863],{"class":553},[291,12606,12607],{"class":318},"posts",[291,12609,9164],{"class":553},[291,12611,602],{"class":409},[291,12613,6731],{"class":409},[291,12615,12616],{"class":297}," useAsyncData",[291,12618,6808],{"class":553},[291,12620,12621],{"class":301},"'latest-posts'",[291,12623,12624],{"class":553},", () ",[291,12626,9149],{"class":409},[291,12628,7115],{"class":553},[291,12630,12631,12633,12636,12638,12641],{"class":293,"line":308},[291,12632,9332],{"class":409},[291,12634,12635],{"class":297}," queryCollection",[291,12637,6808],{"class":553},[291,12639,12640],{"class":301},"'posts'",[291,12642,6976],{"class":553},[291,12644,12645,12648,12651,12653,12656,12658,12661,12663,12666],{"class":293,"line":328},[291,12646,12647],{"class":553},"    .",[291,12649,12650],{"class":297},"where",[291,12652,6808],{"class":553},[291,12654,12655],{"class":301},"'draft'",[291,12657,6928],{"class":553},[291,12659,12660],{"class":301},"'='",[291,12662,6928],{"class":553},[291,12664,12665],{"class":318},"false",[291,12667,6976],{"class":553},[291,12669,12670,12672,12675,12677,12680,12682,12685],{"class":293,"line":335},[291,12671,12647],{"class":553},[291,12673,12674],{"class":297},"order",[291,12676,6808],{"class":553},[291,12678,12679],{"class":301},"'publishedAt'",[291,12681,6928],{"class":553},[291,12683,12684],{"class":301},"'DESC'",[291,12686,6976],{"class":553},[291,12688,12689,12691,12694,12696,12699],{"class":293,"line":354},[291,12690,12647],{"class":553},[291,12692,12693],{"class":297},"limit",[291,12695,6808],{"class":553},[291,12697,12698],{"class":318},"6",[291,12700,6976],{"class":553},[291,12702,12703,12705,12707],{"class":293,"line":371},[291,12704,12647],{"class":553},[291,12706,12288],{"class":297},[291,12708,8140],{"class":553},[291,12710,12711],{"class":293,"line":380},[291,12712,6715],{"class":553},[16,12714,12715],{},"这样做有几个直接好处：",[39,12717,12718,12725,12728,12731],{},[42,12719,12720,12721,12724],{},"数据参与 SSR，不需要 ",[52,12722,12723],{},"onMounted"," 后再闪烁加载；",[42,12726,12727],{},"查询键明确，Nuxt 可以在 hydration 时复用 payload；",[42,12729,12730],{},"页面知道自己需要什么，不必经过一层通用 store；",[42,12732,12733],{},"草稿过滤与排序规则出现在真正相关的位置。",[16,12735,12736],{},"只有当多个页面共享了同一段复杂业务状态时，才值得抽成 composable 或 store。",[78,12738,12739],{"id":12739},"组件只抽取稳定重复",[16,12741,12742],{},"“看起来可能复用”不是组件边界。真正稳定的重复通常同时包含结构、语义和行为。",[16,12744,12745],{},"这个博客里，文章卡片是合适的组件：它在首页和索引页重复出现，拥有固定的元数据、标题、摘要、标签和跳转行为。",[16,12747,12748],{},"相反，首页 Hero 即使很大，也未必需要拆成五个小组件。它只在一个页面出现，内部元素共同完成一段视觉叙事。过早拆分只会让样式关系跨文件跳转。",[72,12750,12751],{},[16,12752,12753],{},"组件不是文件整理工具，而是可独立理解和复用的 UI 契约。",[78,12755,12757],{"id":12756},"服务端-api-保持窄接口","服务端 API 保持窄接口",[16,12759,12760,12761,12764],{},"登录与留言属于运行时数据，放在 ",[52,12762,12763],{},"server\u002Fapi","。每个 endpoint 只做一件事：",[193,12766,12769],{"className":12767,"code":12768,"language":198,"meta":199},[196],"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",[52,12770,12768],{"__ignoreMap":199},[16,12772,12773],{},"窄接口使授权规则更容易检查。读取留言是公开的，创建留言需要会话；退出只销毁当前令牌，不需要触碰用户资料。",[16,12775,12776],{},"如果一个 endpoint 同时处理读取、写入、登录和格式转换，它就已经失去了清晰边界。",[78,12778,12779],{"id":12779},"把状态放回正确的位置",[16,12781,12782],{},"Vue 的响应式状态很方便，但不是所有状态都应该进入 JavaScript：",[1376,12784,12785,12794],{},[1379,12786,12787],{},[1382,12788,12789,12791],{},[1385,12790,1545],{},[1385,12792,12793],{},"最合适的位置",[1398,12795,12796,12804,12814,12822,12833,12840],{},[1382,12797,12798,12801],{},[1403,12799,12800],{},"当前文章",[1403,12802,12803],{},"路由",[1382,12805,12806,12809],{},[1403,12807,12808],{},"搜索词",[1403,12810,12811,12812],{},"页面 ",[52,12813,10108],{},[1382,12815,12816,12819],{},[1403,12817,12818],{},"标签筛选",[1403,12820,12821],{},"URL query + 页面状态",[1382,12823,12824,12827],{},[1403,12825,12826],{},"登录用户",[1403,12828,12829,12830],{},"SSR 安全的 ",[52,12831,12832],{},"useState",[1382,12834,12835,12837],{},[1403,12836,12092],{},[1403,12838,12839],{},"Nuxt Content",[1382,12841,12842,12845],{},[1403,12843,12844],{},"会话真实性",[1403,12846,12847],{},"服务端 D1",[16,12849,12850],{},"当状态位置正确时，大量“同步逻辑”会自然消失。我们不再需要监听路由去修复 store，也不需要用 localStorage 假装登录状态可靠。",[78,12852,12853],{"id":12853},"减法不是功能更少",[16,12855,12856],{},"好的架构减法不会让用户少得到什么。相反，它把复杂度预算留给真正有感知的部分：",[39,12858,12859,12862,12865,12868,12871],{},[42,12860,12861],{},"更清晰的排版；",[42,12863,12864],{},"更快的首屏；",[42,12866,12867],{},"更可靠的登录；",[42,12869,12870],{},"更舒服的移动端阅读；",[42,12872,12873],{},"更容易坚持的 Markdown 写作流程。",[16,12875,12876],{},"Nuxt 4 提供了足够多的能力。项目成熟的标志，不是把它们全部用上，而是知道哪些能力可以暂时不用。",[2134,12878,12879],{},"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":199,"searchDepth":328,"depth":328,"links":12881},[12882,12883,12884,12885,12886,12887],{"id":12517,"depth":308,"text":12518},{"id":12585,"depth":308,"text":12585},{"id":12739,"depth":308,"text":12739},{"id":12756,"depth":308,"text":12757},{"id":12779,"depth":308,"text":12779},{"id":12853,"depth":308,"text":12853},"一个内容型站点不需要复杂前端状态。让路由、服务端渲染和内容查询各自回到最擅长的位置。",{"src":12890,"alt":12891,"caption":12892,"width":10076,"height":10077},"\u002Fmedia\u002Fposts\u002Fnuxt-4-architecture-subtraction\u002Fcover-v1.webp","复杂模块逐步收敛为清晰精简的 Nuxt 应用核心","用架构减法换取清晰边界",{},"\u002Fblog\u002Fnuxt-4-architecture-subtraction","2026-07-16",{"title":12503,"description":12888},"blog\u002Fnuxt-4-architecture-subtraction",[12899,11791,12900],"Nuxt","架构","Czr5nd2U1OiyR7yB5-ruM4B8FGFAAjFQQISDmWg9Fis",{"id":12903,"title":12904,"accent":4503,"body":12905,"description":13252,"draft":2165,"experience":2166,"extension":2167,"featured":2165,"image":13253,"meta":13257,"navigation":331,"path":13258,"publishedAt":13259,"readingTime":380,"seo":13260,"stem":13261,"tags":13262,"updatedAt":2166,"video":2166,"__hash__":13264,"contentLocale":4,"isFallback":2165},"posts_zh\u002Fblog\u002Fobservable-ai-systems.md","AI 系统的可观测性，不只是 Token 账单",{"type":13,"value":12906,"toc":13239},[12907,12910,12915,12918,12921,12924,12928,12931,12935,12938,12942,12945,12949,12952,12955,12959,12966,13079,13082,13085,13102,13105,13108,13111,13165,13168,13171,13174,13191,13194,13197,13200,13203,13217,13220,13224,13227,13233,13236],[16,12908,12909],{},"传统服务出问题时，我们习惯查看状态码、延迟和错误堆栈。AI 应用出问题时，这些指标仍然重要，却经常不足以回答最关键的问题：",[16,12911,12912],{},[20,12913,12914],{},"模型为什么做出了这个选择？",[16,12916,12917],{},"答案不在某一条日志里，而在从用户目标、上下文组装、模型决策、工具执行到最终结果的整条链路中。",[78,12919,12920],{"id":12920},"先区分四种失败",[16,12922,12923],{},"把所有失败都记成“模型效果不好”，团队就无法采取具体行动。至少应该区分：",[576,12925,12927],{"id":12926},"_1-输入失败","1. 输入失败",[16,12929,12930],{},"用户目标不完整，或系统没有拿到完成任务所需的上下文。此时升级模型通常没有帮助。",[576,12932,12934],{"id":12933},"_2-推理失败","2. 推理失败",[16,12936,12937],{},"上下文正确，但模型选择了错误路径、遗漏约束或过早得出结论。",[576,12939,12941],{"id":12940},"_3-工具失败","3. 工具失败",[16,12943,12944],{},"模型选择正确，实际执行却因为权限、网络、参数或外部状态失败。",[576,12946,12948],{"id":12947},"_4-交付失败","4. 交付失败",[16,12950,12951],{},"内部工作已经完成，但最终回答没有把结果、限制和下一步说清楚。",[16,12953,12954],{},"这四类问题需要不同的修复者：产品、提示设计、平台工程或交互设计。可观测性的第一份价值，是让问题回到正确的人手里。",[78,12956,12958],{"id":12957},"一次运行需要共同的-trace","一次运行需要共同的 Trace",[16,12960,12961,12962,12965],{},"每个用户请求都应该有稳定的 ",[52,12963,12964],{},"trace_id","，贯穿模型调用和工具执行：",[193,12967,12969],{"className":6582,"code":12968,"language":6584,"meta":199,"style":199},"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",[52,12970,12971,12983,12992,13001,13011,13036,13045,13055,13075],{"__ignoreMap":199},[291,12972,12973,12976,12979,12981],{"class":293,"line":294},[291,12974,12975],{"class":409},"type",[291,12977,12978],{"class":297}," AgentEvent",[291,12980,6615],{"class":409},[291,12982,7115],{"class":553},[291,12984,12985,12988,12990],{"class":293,"line":308},[291,12986,12987],{"class":6919},"  traceId",[291,12989,6922],{"class":409},[291,12991,8069],{"class":318},[291,12993,12994,12997,12999],{"class":293,"line":328},[291,12995,12996],{"class":6919},"  spanId",[291,12998,6922],{"class":409},[291,13000,8069],{"class":318},[291,13002,13003,13006,13009],{"class":293,"line":335},[291,13004,13005],{"class":6919},"  parentSpanId",[291,13007,13008],{"class":409},"?:",[291,13010,8069],{"class":318},[291,13012,13013,13016,13018,13021,13023,13026,13028,13031,13033],{"class":293,"line":354},[291,13014,13015],{"class":6919},"  type",[291,13017,6922],{"class":409},[291,13019,13020],{"class":301}," 'model'",[291,13022,2462],{"class":409},[291,13024,13025],{"class":301}," 'tool'",[291,13027,2462],{"class":409},[291,13029,13030],{"class":301}," 'handoff'",[291,13032,2462],{"class":409},[291,13034,13035],{"class":301}," 'result'\n",[291,13037,13038,13041,13043],{"class":293,"line":371},[291,13039,13040],{"class":6919},"  startedAt",[291,13042,6922],{"class":409},[291,13044,8069],{"class":318},[291,13046,13047,13050,13052],{"class":293,"line":380},[291,13048,13049],{"class":6919},"  durationMs",[291,13051,13008],{"class":409},[291,13053,13054],{"class":318}," number\n",[291,13056,13057,13060,13062,13065,13067,13070,13072],{"class":293,"line":393},[291,13058,13059],{"class":6919},"  status",[291,13061,6922],{"class":409},[291,13063,13064],{"class":301}," 'running'",[291,13066,2462],{"class":409},[291,13068,13069],{"class":301}," 'ok'",[291,13071,2462],{"class":409},[291,13073,13074],{"class":301}," 'error'\n",[291,13076,13077],{"class":293,"line":398},[291,13078,5305],{"class":553},[16,13080,13081],{},"工具调用是模型决策的外部表现。只记录工具返回值而不记录调用原因，会失去决策上下文；只保存提示词而不保存工具结果，又无法判断执行是否符合预期。",[16,13083,13084],{},"需要注意的是，观测并不意味着无限制收集。原始提示词可能包含个人信息、凭据或业务数据。更稳妥的做法是：",[39,13086,13087,13090,13093,13096,13099],{},[42,13088,13089],{},"默认记录结构化元数据；",[42,13091,13092],{},"对正文做字段级脱敏；",[42,13094,13095],{},"为调试样本设置短保留期；",[42,13097,13098],{},"允许用户和业务明确选择不进入训练或分析；",[42,13100,13101],{},"不在日志中保存密钥与完整身份信息。",[78,13103,13104],{"id":13104},"质量指标必须贴近任务",[16,13106,13107],{},"Token、延迟和模型错误率是平台指标，但它们不能代表用户是否得到结果。",[16,13109,13110],{},"一个代码修复 Agent 更有意义的指标可能是：",[1376,13112,13113,13123],{},[1379,13114,13115],{},[1382,13116,13117,13120],{},[1385,13118,13119],{},"层级",[1385,13121,13122],{},"指标示例",[1398,13124,13125,13133,13141,13149,13157],{},[1382,13126,13127,13130],{},[1403,13128,13129],{},"结果",[1403,13131,13132],{},"受影响测试是否通过",[1382,13134,13135,13138],{},[1403,13136,13137],{},"过程",[1403,13139,13140],{},"是否读取了相关约束文件",[1382,13142,13143,13146],{},[1403,13144,13145],{},"安全",[1403,13147,13148],{},"是否修改了任务范围外文件",[1382,13150,13151,13154],{},[1403,13152,13153],{},"效率",[1403,13155,13156],{},"有效工具调用 \u002F 总工具调用",[1382,13158,13159,13162],{},[1403,13160,13161],{},"交付",[1403,13163,13164],{},"是否提供可验证的文件或提交",[16,13166,13167],{},"“有效工具调用”比单纯调用次数更有价值。一次准确的搜索可能替代十次猜测式读取。",[78,13169,13170],{"id":13170},"把评估变成日常反馈",[16,13172,13173],{},"离线 benchmark 能比较模型版本，却不能覆盖你的真实工作流。更可持续的评估集来自生产中的高价值样本：",[136,13175,13176,13179,13182,13185,13188],{},[42,13177,13178],{},"收集明确成功与明确失败的运行；",[42,13180,13181],{},"删除隐私信息，保留决策结构；",[42,13183,13184],{},"为失败标注类型，而不是只打一个总分；",[42,13186,13187],{},"在提示、模型或工具变化时重新运行；",[42,13189,13190],{},"同时比较结果质量、成本和延迟。",[16,13192,13193],{},"评估集不需要一开始就很大。二十个边界清楚、能稳定复现的案例，通常比两千个来源模糊的样本更能指导工程。",[78,13195,13196],{"id":13196},"面向人的可观测性",[16,13198,13199],{},"最后一层观测不在仪表盘，而在产品界面。",[16,13201,13202],{},"当 Agent 正在读取文件、等待授权或执行部署时，用户需要知道它处于什么阶段。好的状态更新不是倾倒内部思维，而是给出：",[39,13204,13205,13208,13211,13214],{},[42,13206,13207],{},"当前正在完成什么；",[42,13209,13210],{},"已经确认了什么；",[42,13212,13213],{},"是否需要用户采取行动；",[42,13215,13216],{},"哪些风险仍未关闭。",[16,13218,13219],{},"这类可见性会直接改变用户对系统可靠性的判断。一个需要几十秒完成的任务，如果过程清楚，往往比十秒后突然返回不透明结果更让人放心。",[78,13221,13223],{"id":13222},"可解释不等于暴露全部过程","可解释，不等于暴露全部过程",[16,13225,13226],{},"AI 系统不需要把每一个内部 token 展示给用户，也不应该把敏感上下文原样写入日志。真正需要的是一条足够清楚的因果链：",[193,13228,13231],{"className":13229,"code":13230,"language":198,"meta":199},[196],"用户目标\n  → 使用了哪些上下文\n  → 选择了什么工具\n  → 工具返回了什么状态\n  → 最终交付了什么结果\n",[52,13232,13230],{"__ignoreMap":199},[16,13234,13235],{},"当这条链路可以被查询、比较和复现时，团队才有能力把“偶尔很聪明”变成“长期可信赖”。",[2134,13237,13238],{},"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":199,"searchDepth":328,"depth":328,"links":13240},[13241,13247,13248,13249,13250,13251],{"id":12920,"depth":308,"text":12920,"children":13242},[13243,13244,13245,13246],{"id":12926,"depth":328,"text":12927},{"id":12933,"depth":328,"text":12934},{"id":12940,"depth":328,"text":12941},{"id":12947,"depth":328,"text":12948},{"id":12957,"depth":308,"text":12958},{"id":13104,"depth":308,"text":13104},{"id":13170,"depth":308,"text":13170},{"id":13196,"depth":308,"text":13196},{"id":13222,"depth":308,"text":13223},"从提示词到工具调用，再到用户可见结果，AI 应用需要一条能解释“为什么”的观测链路。",{"src":13254,"alt":13255,"caption":13256,"width":10076,"height":10077},"\u002Fmedia\u002Fposts\u002Fobservable-ai-systems\u002Fcover-v1.webp","AI 请求从输入经过模型和工具节点形成完整观测链路","从提示词到结果的可解释 Trace",{},"\u002Fblog\u002Fobservable-ai-systems","2026-07-09",{"title":12904,"description":13252},"blog\u002Fobservable-ai-systems",[12048,13263,12498],"可观测性","HwqaP2v2TJ3UNRY0hMgfErIMN1Z-buqu7-QNk_P-4Vk",1787735716833]